gotrue.ts193 lines · main
1import { AuthClient, navigatorLock, User } from '@supabase/auth-js'
2import { isBrowser } from './helpers'
3
4export const STORAGE_KEY = process.env.NEXT_PUBLIC_STORAGE_KEY || 'briven.dashboard.auth.token'
5export const AUTH_DEBUG_KEY =
6 process.env.NEXT_PUBLIC_AUTH_DEBUG_KEY || 'briven.dashboard.auth.debug'
7export const AUTH_DEBUG_PERSISTED_KEY =
8 process.env.NEXT_PUBLIC_AUTH_DEBUG_PERSISTED_KEY || 'briven.dashboard.auth.debug.persist'
9export const AUTH_NAVIGATOR_LOCK_DISABLED_KEY =
10 process.env.NEXT_PUBLIC_AUTH_NAVIGATOR_LOCK_KEY ||
11 'briven.dashboard.auth.navigatorLock.disabled'
12
13/**
14 * Catches errors thrown when accessing localStorage. Safari with certain
15 * security settings throws when localStorage is accessed.
16 */
17function safeGetLocalStorage(key: string) {
18 try {
19 return globalThis?.localStorage?.getItem(key)
20 } catch {
21 return null
22 }
23}
24
25const debug =
26 process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' && safeGetLocalStorage(AUTH_DEBUG_KEY) === 'true'
27
28const persistedDebug =
29 process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' &&
30 safeGetLocalStorage(AUTH_DEBUG_PERSISTED_KEY) === 'true'
31
32const shouldEnableNavigatorLock =
33 process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' &&
34 !(safeGetLocalStorage(AUTH_NAVIGATOR_LOCK_DISABLED_KEY) === 'true')
35
36const shouldDetectSessionInUrl = process.env.NEXT_PUBLIC_AUTH_DETECT_SESSION_IN_URL
37 ? process.env.NEXT_PUBLIC_AUTH_DETECT_SESSION_IN_URL === 'true'
38 : true
39
40const navigatorLockEnabled = !!(shouldEnableNavigatorLock && globalThis?.navigator?.locks)
41
42if (isBrowser && shouldEnableNavigatorLock && !globalThis?.navigator?.locks) {
43 console.warn('This browser does not support the Navigator Locks API. Please update it.')
44}
45
46const tabId = Math.random().toString(16).substring(2)
47
48let dbHandle = new Promise<IDBDatabase | null>((accept, _) => {
49 if (!persistedDebug) {
50 accept(null)
51 return
52 }
53
54 const request = indexedDB.open('auth-debug-log', 1)
55
56 request.onupgradeneeded = (event: any) => {
57 const db = event?.target?.result
58
59 if (!db) {
60 return
61 }
62
63 db.createObjectStore('events', { autoIncrement: true })
64 }
65
66 request.onsuccess = (event: any) => {
67 console.log('Opened persisted auth debug log IndexedDB database', tabId)
68 accept(event.target.result)
69 }
70
71 request.onerror = (event: any) => {
72 console.error('Failed to open persisted auth debug log IndexedDB database', event)
73 accept(null)
74 }
75})
76
77const logIndexedDB = (message: string, ...args: any[]) => {
78 console.log(message, ...args)
79
80 const copyArgs = structuredClone(args)
81
82 copyArgs.forEach((value) => {
83 if (typeof value === 'object' && value !== null) {
84 delete value.user
85 delete value.access_token
86 delete value.token_type
87 delete value.provider_token
88 }
89 })
90 ;(async () => {
91 try {
92 const db = await dbHandle
93
94 if (!db) {
95 return
96 }
97
98 const tx = db.transaction(['events'], 'readwrite')
99 tx.onerror = (event: any) => {
100 console.error('Failed to write to persisted auth debug log IndexedDB database', event)
101 dbHandle = Promise.resolve(null)
102 }
103
104 const events = tx.objectStore('events')
105
106 events.add({
107 m: message.replace(/^GoTrueClient@/i, ''),
108 a: copyArgs,
109 l: window.location.pathname,
110 t: tabId,
111 })
112 } catch (e: any) {
113 console.error('Failed to log to persisted auth debug log IndexedDB database', e)
114 dbHandle = Promise.resolve(null)
115 }
116 })()
117}
118
119/**
120 * Reference to a function that captures exceptions for debugging purposes to be sent to Sentry.
121 */
122let captureException: ((e: any) => any) | null = null
123
124export function setCaptureException(fn: typeof captureException) {
125 captureException = fn
126}
127
128async function debuggableNavigatorLock<R>(
129 name: string,
130 acquireTimeout: number,
131 fn: () => Promise<R>
132): Promise<R> {
133 let stackException: any
134
135 try {
136 throw new Error('Lock is being held for over 10s here')
137 } catch (e: any) {
138 stackException = e
139 }
140
141 const debugTimeout = setTimeout(() => {
142 ;(async () => {
143 const bc = new BroadcastChannel('who-is-holding-the-lock')
144 try {
145 bc.postMessage({})
146 } finally {
147 bc.close()
148 }
149
150 console.error(
151 `Waited for over 10s to acquire an Auth client lock`,
152 await navigator.locks.query(),
153 stackException
154 )
155 })()
156 }, 10000)
157
158 try {
159 return await navigatorLock(name, acquireTimeout, async () => {
160 clearTimeout(debugTimeout)
161
162 const bc = new BroadcastChannel('who-is-holding-the-lock')
163 bc.addEventListener('message', () => {
164 console.error('Lock is held here', stackException)
165
166 if (captureException) {
167 captureException(stackException)
168 }
169 })
170
171 try {
172 return await fn()
173 } finally {
174 bc.close()
175 }
176 })
177 } finally {
178 clearTimeout(debugTimeout)
179 }
180}
181
182export const gotrueClient = new AuthClient({
183 url: process.env.NEXT_PUBLIC_GOTRUE_URL,
184 storageKey: STORAGE_KEY,
185 detectSessionInUrl: shouldDetectSessionInUrl,
186 debug: debug ? (persistedDebug ? logIndexedDB : true) : false,
187 lock: navigatorLockEnabled ? debuggableNavigatorLock : undefined,
188 ...('localStorage' in globalThis
189 ? { storage: globalThis.localStorage, userStorage: globalThis.localStorage }
190 : null),
191})
192
193export type { User }