gotrue.ts193 lines · main
| 1 | import { AuthClient, navigatorLock, User } from '@supabase/auth-js' |
| 2 | import { isBrowser } from './helpers' |
| 3 | |
| 4 | export const STORAGE_KEY = process.env.NEXT_PUBLIC_STORAGE_KEY || 'briven.dashboard.auth.token' |
| 5 | export const AUTH_DEBUG_KEY = |
| 6 | process.env.NEXT_PUBLIC_AUTH_DEBUG_KEY || 'briven.dashboard.auth.debug' |
| 7 | export const AUTH_DEBUG_PERSISTED_KEY = |
| 8 | process.env.NEXT_PUBLIC_AUTH_DEBUG_PERSISTED_KEY || 'briven.dashboard.auth.debug.persist' |
| 9 | export 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 | */ |
| 17 | function safeGetLocalStorage(key: string) { |
| 18 | try { |
| 19 | return globalThis?.localStorage?.getItem(key) |
| 20 | } catch { |
| 21 | return null |
| 22 | } |
| 23 | } |
| 24 | |
| 25 | const debug = |
| 26 | process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' && safeGetLocalStorage(AUTH_DEBUG_KEY) === 'true' |
| 27 | |
| 28 | const persistedDebug = |
| 29 | process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' && |
| 30 | safeGetLocalStorage(AUTH_DEBUG_PERSISTED_KEY) === 'true' |
| 31 | |
| 32 | const shouldEnableNavigatorLock = |
| 33 | process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' && |
| 34 | !(safeGetLocalStorage(AUTH_NAVIGATOR_LOCK_DISABLED_KEY) === 'true') |
| 35 | |
| 36 | const shouldDetectSessionInUrl = process.env.NEXT_PUBLIC_AUTH_DETECT_SESSION_IN_URL |
| 37 | ? process.env.NEXT_PUBLIC_AUTH_DETECT_SESSION_IN_URL === 'true' |
| 38 | : true |
| 39 | |
| 40 | const navigatorLockEnabled = !!(shouldEnableNavigatorLock && globalThis?.navigator?.locks) |
| 41 | |
| 42 | if (isBrowser && shouldEnableNavigatorLock && !globalThis?.navigator?.locks) { |
| 43 | console.warn('This browser does not support the Navigator Locks API. Please update it.') |
| 44 | } |
| 45 | |
| 46 | const tabId = Math.random().toString(16).substring(2) |
| 47 | |
| 48 | let 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 | |
| 77 | const 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 | */ |
| 122 | let captureException: ((e: any) => any) | null = null |
| 123 | |
| 124 | export function setCaptureException(fn: typeof captureException) { |
| 125 | captureException = fn |
| 126 | } |
| 127 | |
| 128 | async 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 | |
| 182 | export 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 | |
| 193 | export type { User } |