• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / auth-js / 16192477863

10 Jul 2025 10:19AM UTC coverage: 74.777% (+0.03%) from 74.751%
16192477863

push

github

web-flow
feat: fallback to `getUser()` if the `kid` of the JWT is not found (#1080)

Because the `/.well-known/jwks.json` is heavily cached, a developer may
rotate the standby key to in use faster than those caches expire. In
that case the `getClaims()` method may receive a JWT signed with a key
ID it doesn't recognize. Instead of failing with an error, it should
reach out directly to the Auth server to verify the JWT.

860 of 1304 branches covered (65.95%)

Branch coverage included in aggregate %.

3 of 5 new or added lines in 2 files covered. (60.0%)

1316 of 1606 relevant lines covered (81.94%)

166.24 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

68.26
/src/GoTrueClient.ts
1
import GoTrueAdminApi from './GoTrueAdminApi'
10✔
2
import {
10✔
3
  DEFAULT_HEADERS,
4
  EXPIRY_MARGIN_MS,
5
  AUTO_REFRESH_TICK_DURATION_MS,
6
  AUTO_REFRESH_TICK_THRESHOLD,
7
  GOTRUE_URL,
8
  STORAGE_KEY,
9
  JWKS_TTL,
10
} from './lib/constants'
11
import {
10✔
12
  AuthError,
13
  AuthImplicitGrantRedirectError,
14
  AuthPKCEGrantCodeExchangeError,
15
  AuthInvalidCredentialsError,
16
  AuthSessionMissingError,
17
  AuthInvalidTokenResponseError,
18
  AuthUnknownError,
19
  isAuthApiError,
20
  isAuthError,
21
  isAuthRetryableFetchError,
22
  isAuthSessionMissingError,
23
  isAuthImplicitGrantRedirectError,
24
  AuthInvalidJwtError,
25
} from './lib/errors'
26
import {
10✔
27
  Fetch,
28
  _request,
29
  _sessionResponse,
30
  _sessionResponsePassword,
31
  _userResponse,
32
  _ssoResponse,
33
} from './lib/fetch'
34
import {
10✔
35
  Deferred,
36
  getItemAsync,
37
  isBrowser,
38
  removeItemAsync,
39
  resolveFetch,
40
  setItemAsync,
41
  uuid,
42
  retryable,
43
  sleep,
44
  parseParametersFromURL,
45
  getCodeChallengeAndMethod,
46
  getAlgorithm,
47
  validateExp,
48
  decodeJWT,
49
  userNotAvailableProxy,
50
  supportsLocalStorage,
51
} from './lib/helpers'
52
import { memoryLocalStorageAdapter } from './lib/local-storage'
10✔
53
import { polyfillGlobalThis } from './lib/polyfills'
10✔
54
import { version } from './lib/version'
10✔
55
import { LockAcquireTimeoutError, navigatorLock } from './lib/locks'
10✔
56

57
import type {
58
  AuthChangeEvent,
59
  AuthResponse,
60
  AuthResponsePassword,
61
  AuthTokenResponse,
62
  AuthTokenResponsePassword,
63
  AuthOtpResponse,
64
  CallRefreshTokenResult,
65
  GoTrueClientOptions,
66
  InitializeResult,
67
  OAuthResponse,
68
  SSOResponse,
69
  Provider,
70
  Session,
71
  SignInWithIdTokenCredentials,
72
  SignInWithOAuthCredentials,
73
  SignInWithPasswordCredentials,
74
  SignInWithPasswordlessCredentials,
75
  SignUpWithPasswordCredentials,
76
  SignInWithSSO,
77
  SignOut,
78
  Subscription,
79
  SupportedStorage,
80
  User,
81
  UserAttributes,
82
  UserResponse,
83
  VerifyOtpParams,
84
  GoTrueMFAApi,
85
  MFAEnrollParams,
86
  AuthMFAEnrollResponse,
87
  MFAChallengeParams,
88
  AuthMFAChallengeResponse,
89
  MFAUnenrollParams,
90
  AuthMFAUnenrollResponse,
91
  MFAVerifyParams,
92
  AuthMFAVerifyResponse,
93
  AuthMFAListFactorsResponse,
94
  AuthMFAGetAuthenticatorAssuranceLevelResponse,
95
  AuthenticatorAssuranceLevels,
96
  Factor,
97
  MFAChallengeAndVerifyParams,
98
  ResendParams,
99
  AuthFlowType,
100
  LockFunc,
101
  UserIdentity,
102
  SignInAnonymouslyCredentials,
103
  MFAEnrollTOTPParams,
104
  MFAEnrollPhoneParams,
105
  AuthMFAEnrollTOTPResponse,
106
  AuthMFAEnrollPhoneResponse,
107
  JWK,
108
  JwtPayload,
109
  JwtHeader,
110
  SolanaWeb3Credentials,
111
  SolanaWallet,
112
  Web3Credentials,
113
} from './lib/types'
114
import { stringToUint8Array, bytesToBase64URL } from './lib/base64url'
10✔
115

116
polyfillGlobalThis() // Make "globalThis" available
10✔
117

118
const DEFAULT_OPTIONS: Omit<
119
  Required<GoTrueClientOptions>,
120
  'fetch' | 'storage' | 'userStorage' | 'lock'
121
> = {
10✔
122
  url: GOTRUE_URL,
123
  storageKey: STORAGE_KEY,
124
  autoRefreshToken: true,
125
  persistSession: true,
126
  detectSessionInUrl: true,
127
  headers: DEFAULT_HEADERS,
128
  flowType: 'implicit',
129
  debug: false,
130
  hasCustomAuthorizationHeader: false,
131
}
132

133
async function lockNoOp<R>(name: string, acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
134
  return await fn()
433✔
135
}
136

137
/**
138
 * Caches JWKS values for all clients created in the same environment. This is
139
 * especially useful for shared-memory execution environments such as Vercel's
140
 * Fluid Compute, AWS Lambda or Supabase's Edge Functions. Regardless of how
141
 * many clients are created, if they share the same storage key they will use
142
 * the same JWKS cache, significantly speeding up getClaims() with asymmetric
143
 * JWTs.
144
 */
145
const GLOBAL_JWKS: { [storageKey: string]: { cachedAt: number; jwks: { keys: JWK[] } } } = {}
10✔
146

147
export default class GoTrueClient {
10✔
148
  private static nextInstanceID = 0
10✔
149

150
  private instanceID: number
151

152
  /**
153
   * Namespace for the GoTrue admin methods.
154
   * These methods should only be used in a trusted server-side environment.
155
   */
156
  admin: GoTrueAdminApi
157
  /**
158
   * Namespace for the MFA methods.
159
   */
160
  mfa: GoTrueMFAApi
161
  /**
162
   * The storage key used to identify the values saved in localStorage
163
   */
164
  protected storageKey: string
165

166
  protected flowType: AuthFlowType
167

168
  /**
169
   * The JWKS used for verifying asymmetric JWTs
170
   */
171
  protected get jwks() {
172
    return GLOBAL_JWKS[this.storageKey]?.jwks ?? { keys: [] }
128✔
173
  }
174

175
  protected set jwks(value: { keys: JWK[] }) {
176
    GLOBAL_JWKS[this.storageKey] = { ...GLOBAL_JWKS[this.storageKey], jwks: value }
15✔
177
  }
178

179
  protected get jwks_cached_at() {
180
    return GLOBAL_JWKS[this.storageKey]?.cachedAt ?? Number.MIN_SAFE_INTEGER
5!
181
  }
182

183
  protected set jwks_cached_at(value: number) {
184
    GLOBAL_JWKS[this.storageKey] = { ...GLOBAL_JWKS[this.storageKey], cachedAt: value }
15✔
185
  }
186

187
  protected autoRefreshToken: boolean
188
  protected persistSession: boolean
189
  protected storage: SupportedStorage
190
  /**
191
   * @experimental
192
   */
193
  protected userStorage: SupportedStorage | null = null
118✔
194
  protected memoryStorage: { [key: string]: string } | null = null
118✔
195
  protected stateChangeEmitters: Map<string, Subscription> = new Map()
118✔
196
  protected autoRefreshTicker: ReturnType<typeof setInterval> | null = null
118✔
197
  protected visibilityChangedCallback: (() => Promise<any>) | null = null
118✔
198
  protected refreshingDeferred: Deferred<CallRefreshTokenResult> | null = null
118✔
199
  /**
200
   * Keeps track of the async client initialization.
201
   * When null or not yet resolved the auth state is `unknown`
202
   * Once resolved the the auth state is known and it's save to call any further client methods.
203
   * Keep extra care to never reject or throw uncaught errors
204
   */
205
  protected initializePromise: Promise<InitializeResult> | null = null
118✔
206
  protected detectSessionInUrl = true
118✔
207
  protected url: string
208
  protected headers: {
209
    [key: string]: string
210
  }
211
  protected hasCustomAuthorizationHeader = false
118✔
212
  protected suppressGetSessionWarning = false
118✔
213
  protected fetch: Fetch
214
  protected lock: LockFunc
215
  protected lockAcquired = false
118✔
216
  protected pendingInLock: Promise<any>[] = []
118✔
217

218
  /**
219
   * Used to broadcast state change events to other tabs listening.
220
   */
221
  protected broadcastChannel: BroadcastChannel | null = null
118✔
222

223
  protected logDebugMessages: boolean
224
  protected logger: (message: string, ...args: any[]) => void = console.log
118✔
225

226
  /**
227
   * Create a new client for use in the browser.
228
   */
229
  constructor(options: GoTrueClientOptions) {
230
    this.instanceID = GoTrueClient.nextInstanceID
118✔
231
    GoTrueClient.nextInstanceID += 1
118✔
232

233
    if (this.instanceID > 0 && isBrowser()) {
118✔
234
      console.warn(
24✔
235
        'Multiple GoTrueClient instances detected in the same browser context. It is not an error, but this should be avoided as it may produce undefined behavior when used concurrently under the same storage key.'
236
      )
237
    }
238

239
    const settings = { ...DEFAULT_OPTIONS, ...options }
118✔
240

241
    this.logDebugMessages = !!settings.debug
118✔
242
    if (typeof settings.debug === 'function') {
118!
243
      this.logger = settings.debug
×
244
    }
245

246
    this.persistSession = settings.persistSession
118✔
247
    this.storageKey = settings.storageKey
118✔
248
    this.autoRefreshToken = settings.autoRefreshToken
118✔
249
    this.admin = new GoTrueAdminApi({
118✔
250
      url: settings.url,
251
      headers: settings.headers,
252
      fetch: settings.fetch,
253
    })
254

255
    this.url = settings.url
118✔
256
    this.headers = settings.headers
118✔
257
    this.fetch = resolveFetch(settings.fetch)
118✔
258
    this.lock = settings.lock || lockNoOp
118✔
259
    this.detectSessionInUrl = settings.detectSessionInUrl
118✔
260
    this.flowType = settings.flowType
118✔
261
    this.hasCustomAuthorizationHeader = settings.hasCustomAuthorizationHeader
118✔
262

263
    if (settings.lock) {
118✔
264
      this.lock = settings.lock
2✔
265
    } else if (isBrowser() && globalThis?.navigator?.locks) {
116!
266
      this.lock = navigatorLock
×
267
    } else {
268
      this.lock = lockNoOp
116✔
269
    }
270

271
    if (!this.jwks) {
118!
272
      this.jwks = { keys: [] }
×
273
      this.jwks_cached_at = Number.MIN_SAFE_INTEGER
×
274
    }
275

276
    this.mfa = {
118✔
277
      verify: this._verify.bind(this),
278
      enroll: this._enroll.bind(this),
279
      unenroll: this._unenroll.bind(this),
280
      challenge: this._challenge.bind(this),
281
      listFactors: this._listFactors.bind(this),
282
      challengeAndVerify: this._challengeAndVerify.bind(this),
283
      getAuthenticatorAssuranceLevel: this._getAuthenticatorAssuranceLevel.bind(this),
284
    }
285

286
    if (this.persistSession) {
118✔
287
      if (settings.storage) {
114✔
288
        this.storage = settings.storage
106✔
289
      } else {
290
        if (supportsLocalStorage()) {
8✔
291
          this.storage = globalThis.localStorage
2✔
292
        } else {
293
          this.memoryStorage = {}
6✔
294
          this.storage = memoryLocalStorageAdapter(this.memoryStorage)
6✔
295
        }
296
      }
297

298
      if (settings.userStorage) {
114✔
299
        this.userStorage = settings.userStorage
2✔
300
      }
301
    } else {
302
      this.memoryStorage = {}
4✔
303
      this.storage = memoryLocalStorageAdapter(this.memoryStorage)
4✔
304
    }
305

306
    if (isBrowser() && globalThis.BroadcastChannel && this.persistSession && this.storageKey) {
118✔
307
      try {
2✔
308
        this.broadcastChannel = new globalThis.BroadcastChannel(this.storageKey)
2✔
309
      } catch (e: any) {
310
        console.error(
×
311
          'Failed to create a new BroadcastChannel, multi-tab state changes will not be available',
312
          e
313
        )
314
      }
315

316
      this.broadcastChannel?.addEventListener('message', async (event) => {
2!
317
        this._debug('received broadcast notification from other tab or client', event)
2✔
318

319
        await this._notifyAllSubscribers(event.data.event, event.data.session, false) // broadcast = false so we don't get an endless loop of messages
2✔
320
      })
321
    }
322

323
    this.initialize()
118✔
324
  }
325

326
  private _debug(...args: any[]): GoTrueClient {
327
    if (this.logDebugMessages) {
5,281!
328
      this.logger(
×
329
        `GoTrueClient@${this.instanceID} (${version}) ${new Date().toISOString()}`,
330
        ...args
331
      )
332
    }
333

334
    return this
5,281✔
335
  }
336

337
  /**
338
   * Initializes the client session either from the url or from storage.
339
   * This method is automatically called when instantiating the client, but should also be called
340
   * manually when checking for an error from an auth redirect (oauth, magiclink, password recovery, etc).
341
   */
342
  async initialize(): Promise<InitializeResult> {
343
    if (this.initializePromise) {
134✔
344
      return await this.initializePromise
16✔
345
    }
346

347
    this.initializePromise = (async () => {
118✔
348
      return await this._acquireLock(-1, async () => {
118✔
349
        return await this._initialize()
118✔
350
      })
351
    })()
352

353
    return await this.initializePromise
118✔
354
  }
355

356
  /**
357
   * IMPORTANT:
358
   * 1. Never throw in this method, as it is called from the constructor
359
   * 2. Never return a session from this method as it would be cached over
360
   *    the whole lifetime of the client
361
   */
362
  private async _initialize(): Promise<InitializeResult> {
363
    try {
118✔
364
      const params = parseParametersFromURL(window.location.href)
118✔
365
      let callbackUrlType = 'none'
26✔
366
      if (this._isImplicitGrantCallback(params)) {
26✔
367
        callbackUrlType = 'implicit'
6✔
368
      } else if (await this._isPKCECallback(params)) {
20!
369
        callbackUrlType = 'pkce'
×
370
      }
371

372
      /**
373
       * Attempt to get the session from the URL only if these conditions are fulfilled
374
       *
375
       * Note: If the URL isn't one of the callback url types (implicit or pkce),
376
       * then there could be an existing session so we don't want to prematurely remove it
377
       */
378
      if (isBrowser() && this.detectSessionInUrl && callbackUrlType !== 'none') {
26✔
379
        const { data, error } = await this._getSessionFromURL(params, callbackUrlType)
6✔
380
        if (error) {
6✔
381
          this._debug('#_initialize()', 'error detecting session from URL', error)
4✔
382

383
          if (isAuthImplicitGrantRedirectError(error)) {
4✔
384
            const errorCode = error.details?.code
4!
385
            if (
4!
386
              errorCode === 'identity_already_exists' ||
12✔
387
              errorCode === 'identity_not_found' ||
388
              errorCode === 'single_identity_not_deletable'
389
            ) {
390
              return { error }
×
391
            }
392
          }
393

394
          // failed login attempt via url,
395
          // remove old session as in verifyOtp, signUp and signInWith*
396
          await this._removeSession()
4✔
397

398
          return { error }
4✔
399
        }
400

401
        const { session, redirectType } = data
2✔
402

403
        this._debug(
2✔
404
          '#_initialize()',
405
          'detected session in URL',
406
          session,
407
          'redirect type',
408
          redirectType
409
        )
410

411
        await this._saveSession(session)
2✔
412

413
        setTimeout(async () => {
2✔
414
          if (redirectType === 'recovery') {
×
415
            await this._notifyAllSubscribers('PASSWORD_RECOVERY', session)
×
416
          } else {
417
            await this._notifyAllSubscribers('SIGNED_IN', session)
×
418
          }
419
        }, 0)
420

421
        return { error: null }
2✔
422
      }
423
      // no login attempt via callback url try to recover session from storage
424
      await this._recoverAndRefresh()
20✔
425
      return { error: null }
20✔
426
    } catch (error) {
427
      if (isAuthError(error)) {
92!
428
        return { error }
×
429
      }
430

431
      return {
92✔
432
        error: new AuthUnknownError('Unexpected error during initialization', error),
433
      }
434
    } finally {
435
      await this._handleVisibilityChange()
118✔
436
      this._debug('#_initialize()', 'end')
118✔
437
    }
438
  }
439

440
  /**
441
   * Creates a new anonymous user.
442
   *
443
   * @returns A session where the is_anonymous claim in the access token JWT set to true
444
   */
445
  async signInAnonymously(credentials?: SignInAnonymouslyCredentials): Promise<AuthResponse> {
446
    try {
6✔
447
      const res = await _request(this.fetch, 'POST', `${this.url}/signup`, {
6✔
448
        headers: this.headers,
449
        body: {
450
          data: credentials?.options?.data ?? {},
54✔
451
          gotrue_meta_security: { captcha_token: credentials?.options?.captchaToken },
36✔
452
        },
453
        xform: _sessionResponse,
454
      })
455
      const { data, error } = res
4✔
456

457
      if (error || !data) {
4!
458
        return { data: { user: null, session: null }, error: error }
×
459
      }
460
      const session: Session | null = data.session
4✔
461
      const user: User | null = data.user
4✔
462

463
      if (data.session) {
4✔
464
        await this._saveSession(data.session)
4✔
465
        await this._notifyAllSubscribers('SIGNED_IN', session)
4✔
466
      }
467

468
      return { data: { user, session }, error: null }
4✔
469
    } catch (error) {
470
      if (isAuthError(error)) {
2✔
471
        return { data: { user: null, session: null }, error }
2✔
472
      }
473

474
      throw error
×
475
    }
476
  }
477

478
  /**
479
   * Creates a new user.
480
   *
481
   * Be aware that if a user account exists in the system you may get back an
482
   * error message that attempts to hide this information from the user.
483
   * This method has support for PKCE via email signups. The PKCE flow cannot be used when autoconfirm is enabled.
484
   *
485
   * @returns A logged-in session if the server has "autoconfirm" ON
486
   * @returns A user if the server has "autoconfirm" OFF
487
   */
488
  async signUp(credentials: SignUpWithPasswordCredentials): Promise<AuthResponse> {
489
    try {
123✔
490
      let res: AuthResponse
491
      if ('email' in credentials) {
123✔
492
        const { email, password, options } = credentials
117✔
493
        let codeChallenge: string | null = null
117✔
494
        let codeChallengeMethod: string | null = null
117✔
495
        if (this.flowType === 'pkce') {
117✔
496
          ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
2✔
497
            this.storage,
498
            this.storageKey
499
          )
500
        }
501
        res = await _request(this.fetch, 'POST', `${this.url}/signup`, {
117✔
502
          headers: this.headers,
503
          redirectTo: options?.emailRedirectTo,
351✔
504
          body: {
505
            email,
506
            password,
507
            data: options?.data ?? {},
702✔
508
            gotrue_meta_security: { captcha_token: options?.captchaToken },
351✔
509
            code_challenge: codeChallenge,
510
            code_challenge_method: codeChallengeMethod,
511
          },
512
          xform: _sessionResponse,
513
        })
514
      } else if ('phone' in credentials) {
6✔
515
        const { phone, password, options } = credentials
4✔
516
        res = await _request(this.fetch, 'POST', `${this.url}/signup`, {
4✔
517
          headers: this.headers,
518
          body: {
519
            phone,
520
            password,
521
            data: options?.data ?? {},
24✔
522
            channel: options?.channel ?? 'sms',
24✔
523
            gotrue_meta_security: { captcha_token: options?.captchaToken },
12✔
524
          },
525
          xform: _sessionResponse,
526
        })
527
      } else {
528
        throw new AuthInvalidCredentialsError(
2✔
529
          'You must provide either an email or phone number and a password'
530
        )
531
      }
532

533
      const { data, error } = res
111✔
534

535
      if (error || !data) {
111!
536
        return { data: { user: null, session: null }, error: error }
×
537
      }
538

539
      const session: Session | null = data.session
111✔
540
      const user: User | null = data.user
111✔
541

542
      if (data.session) {
111✔
543
        await this._saveSession(data.session)
109✔
544
        await this._notifyAllSubscribers('SIGNED_IN', session)
109✔
545
      }
546

547
      return { data: { user, session }, error: null }
111✔
548
    } catch (error) {
549
      if (isAuthError(error)) {
12✔
550
        return { data: { user: null, session: null }, error }
12✔
551
      }
552

553
      throw error
×
554
    }
555
  }
556

557
  /**
558
   * Log in an existing user with an email and password or phone and password.
559
   *
560
   * Be aware that you may get back an error message that will not distinguish
561
   * between the cases where the account does not exist or that the
562
   * email/phone and password combination is wrong or that the account can only
563
   * be accessed via social login.
564
   */
565
  async signInWithPassword(
566
    credentials: SignInWithPasswordCredentials
567
  ): Promise<AuthTokenResponsePassword> {
568
    try {
32✔
569
      let res: AuthResponsePassword
570
      if ('email' in credentials) {
32✔
571
        const { email, password, options } = credentials
26✔
572
        res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=password`, {
26✔
573
          headers: this.headers,
574
          body: {
575
            email,
576
            password,
577
            gotrue_meta_security: { captcha_token: options?.captchaToken },
78✔
578
          },
579
          xform: _sessionResponsePassword,
580
        })
581
      } else if ('phone' in credentials) {
6✔
582
        const { phone, password, options } = credentials
4✔
583
        res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=password`, {
4✔
584
          headers: this.headers,
585
          body: {
586
            phone,
587
            password,
588
            gotrue_meta_security: { captcha_token: options?.captchaToken },
12✔
589
          },
590
          xform: _sessionResponsePassword,
591
        })
592
      } else {
593
        throw new AuthInvalidCredentialsError(
2✔
594
          'You must provide either an email or phone number and a password'
595
        )
596
      }
597
      const { data, error } = res
26✔
598

599
      if (error) {
26!
600
        return { data: { user: null, session: null }, error }
×
601
      } else if (!data || !data.session || !data.user) {
26!
602
        return { data: { user: null, session: null }, error: new AuthInvalidTokenResponseError() }
×
603
      }
604
      if (data.session) {
26✔
605
        await this._saveSession(data.session)
26✔
606
        await this._notifyAllSubscribers('SIGNED_IN', data.session)
26✔
607
      }
608
      return {
26✔
609
        data: {
610
          user: data.user,
611
          session: data.session,
612
          ...(data.weak_password ? { weakPassword: data.weak_password } : null),
26!
613
        },
614
        error,
615
      }
616
    } catch (error) {
617
      if (isAuthError(error)) {
6✔
618
        return { data: { user: null, session: null }, error }
6✔
619
      }
620
      throw error
×
621
    }
622
  }
623

624
  /**
625
   * Log in an existing user via a third-party provider.
626
   * This method supports the PKCE flow.
627
   */
628
  async signInWithOAuth(credentials: SignInWithOAuthCredentials): Promise<OAuthResponse> {
629
    return await this._handleProviderSignIn(credentials.provider, {
10✔
630
      redirectTo: credentials.options?.redirectTo,
30✔
631
      scopes: credentials.options?.scopes,
30✔
632
      queryParams: credentials.options?.queryParams,
30✔
633
      skipBrowserRedirect: credentials.options?.skipBrowserRedirect,
30✔
634
    })
635
  }
636

637
  /**
638
   * Log in an existing user by exchanging an Auth Code issued during the PKCE flow.
639
   */
640
  async exchangeCodeForSession(authCode: string): Promise<AuthTokenResponse> {
641
    await this.initializePromise
2✔
642

643
    return this._acquireLock(-1, async () => {
2✔
644
      return this._exchangeCodeForSession(authCode)
2✔
645
    })
646
  }
647

648
  /**
649
   * Signs in a user by verifying a message signed by the user's private key.
650
   * Only Solana supported at this time, using the Sign in with Solana standard.
651
   */
652
  async signInWithWeb3(credentials: Web3Credentials): Promise<
653
    | {
654
        data: { session: Session; user: User }
655
        error: null
656
      }
657
    | { data: { session: null; user: null }; error: AuthError }
658
  > {
659
    const { chain } = credentials
10✔
660

661
    if (chain === 'solana') {
10✔
662
      return await this.signInWithSolana(credentials)
8✔
663
    }
664

665
    throw new Error(`@supabase/auth-js: Unsupported chain "${chain}"`)
2✔
666
  }
667

668
  private async signInWithSolana(credentials: SolanaWeb3Credentials) {
669
    let message: string
670
    let signature: Uint8Array
671

672
    if ('message' in credentials) {
8✔
673
      message = credentials.message
2✔
674
      signature = credentials.signature
2✔
675
    } else {
676
      const { chain, wallet, statement, options } = credentials
6✔
677

678
      let resolvedWallet: SolanaWallet
679

680
      if (!isBrowser()) {
6!
681
        if (typeof wallet !== 'object' || !options?.url) {
6!
682
          throw new Error(
2✔
683
            '@supabase/auth-js: Both wallet and url must be specified in non-browser environments.'
684
          )
685
        }
686

687
        resolvedWallet = wallet
4✔
688
      } else if (typeof wallet === 'object') {
×
689
        resolvedWallet = wallet
×
690
      } else {
691
        const windowAny = window as any
×
692

693
        if (
×
694
          'solana' in windowAny &&
×
695
          typeof windowAny.solana === 'object' &&
696
          (('signIn' in windowAny.solana && typeof windowAny.solana.signIn === 'function') ||
697
            ('signMessage' in windowAny.solana &&
698
              typeof windowAny.solana.signMessage === 'function'))
699
        ) {
700
          resolvedWallet = windowAny.solana
×
701
        } else {
702
          throw new Error(
×
703
            `@supabase/auth-js: No compatible Solana wallet interface on the window object (window.solana) detected. Make sure the user already has a wallet installed and connected for this app. Prefer passing the wallet interface object directly to signInWithWeb3({ chain: 'solana', wallet: resolvedUserWallet }) instead.`
704
          )
705
        }
706
      }
707

708
      const url = new URL(options?.url ?? window.location.href)
4!
709

710
      if ('signIn' in resolvedWallet && resolvedWallet.signIn) {
4!
711
        const output = await resolvedWallet.signIn({
×
712
          issuedAt: new Date().toISOString(),
713

714
          ...options?.signInWithSolana,
×
715

716
          // non-overridable properties
717
          version: '1',
718
          domain: url.host,
719
          uri: url.href,
720

721
          ...(statement ? { statement } : null),
×
722
        })
723

724
        let outputToProcess: any
725

726
        if (Array.isArray(output) && output[0] && typeof output[0] === 'object') {
×
727
          outputToProcess = output[0]
×
728
        } else if (
×
729
          output &&
×
730
          typeof output === 'object' &&
731
          'signedMessage' in output &&
732
          'signature' in output
733
        ) {
734
          outputToProcess = output
×
735
        } else {
736
          throw new Error('@supabase/auth-js: Wallet method signIn() returned unrecognized value')
×
737
        }
738

739
        if (
×
740
          'signedMessage' in outputToProcess &&
×
741
          'signature' in outputToProcess &&
742
          (typeof outputToProcess.signedMessage === 'string' ||
743
            outputToProcess.signedMessage instanceof Uint8Array) &&
744
          outputToProcess.signature instanceof Uint8Array
745
        ) {
746
          message =
×
747
            typeof outputToProcess.signedMessage === 'string'
×
748
              ? outputToProcess.signedMessage
749
              : new TextDecoder().decode(outputToProcess.signedMessage)
750
          signature = outputToProcess.signature
×
751
        } else {
752
          throw new Error(
×
753
            '@supabase/auth-js: Wallet method signIn() API returned object without signedMessage and signature fields'
754
          )
755
        }
756
      } else {
757
        if (
4✔
758
          !('signMessage' in resolvedWallet) ||
8!
759
          typeof resolvedWallet.signMessage !== 'function' ||
760
          !('publicKey' in resolvedWallet) ||
761
          typeof resolvedWallet !== 'object' ||
762
          !resolvedWallet.publicKey ||
763
          !('toBase58' in resolvedWallet.publicKey) ||
764
          typeof resolvedWallet.publicKey.toBase58 !== 'function'
765
        ) {
766
          throw new Error(
4✔
767
            '@supabase/auth-js: Wallet does not have a compatible signMessage() and publicKey.toBase58() API'
768
          )
769
        }
770

771
        message = [
×
772
          `${url.host} wants you to sign in with your Solana account:`,
773
          resolvedWallet.publicKey.toBase58(),
774
          ...(statement ? ['', statement, ''] : ['']),
×
775
          'Version: 1',
776
          `URI: ${url.href}`,
777
          `Issued At: ${options?.signInWithSolana?.issuedAt ?? new Date().toISOString()}`,
×
778
          ...(options?.signInWithSolana?.notBefore
×
779
            ? [`Not Before: ${options.signInWithSolana.notBefore}`]
780
            : []),
781
          ...(options?.signInWithSolana?.expirationTime
×
782
            ? [`Expiration Time: ${options.signInWithSolana.expirationTime}`]
783
            : []),
784
          ...(options?.signInWithSolana?.chainId
×
785
            ? [`Chain ID: ${options.signInWithSolana.chainId}`]
786
            : []),
787
          ...(options?.signInWithSolana?.nonce ? [`Nonce: ${options.signInWithSolana.nonce}`] : []),
×
788
          ...(options?.signInWithSolana?.requestId
×
789
            ? [`Request ID: ${options.signInWithSolana.requestId}`]
790
            : []),
791
          ...(options?.signInWithSolana?.resources?.length
×
792
            ? [
793
                'Resources',
794
                ...options.signInWithSolana.resources.map((resource) => `- ${resource}`),
×
795
              ]
796
            : []),
797
        ].join('\n')
798

799
        const maybeSignature = await resolvedWallet.signMessage(
×
800
          new TextEncoder().encode(message),
801
          'utf8'
802
        )
803

804
        if (!maybeSignature || !(maybeSignature instanceof Uint8Array)) {
×
805
          throw new Error(
×
806
            '@supabase/auth-js: Wallet signMessage() API returned an recognized value'
807
          )
808
        }
809

810
        signature = maybeSignature
×
811
      }
812
    }
813

814
    try {
2✔
815
      const { data, error } = await _request(
2✔
816
        this.fetch,
817
        'POST',
818
        `${this.url}/token?grant_type=web3`,
819
        {
820
          headers: this.headers,
821
          body: {
822
            chain: 'solana',
823
            message,
824
            signature: bytesToBase64URL(signature),
825

826
            ...(credentials.options?.captchaToken
8!
827
              ? { gotrue_meta_security: { captcha_token: credentials.options?.captchaToken } }
×
828
              : null),
829
          },
830
          xform: _sessionResponse,
831
        }
832
      )
833
      if (error) {
×
834
        throw error
×
835
      }
836
      if (!data || !data.session || !data.user) {
×
837
        return {
×
838
          data: { user: null, session: null },
839
          error: new AuthInvalidTokenResponseError(),
840
        }
841
      }
842
      if (data.session) {
×
843
        await this._saveSession(data.session)
×
844
        await this._notifyAllSubscribers('SIGNED_IN', data.session)
×
845
      }
846
      return { data: { ...data }, error }
×
847
    } catch (error) {
848
      if (isAuthError(error)) {
2✔
849
        return { data: { user: null, session: null }, error }
2✔
850
      }
851

852
      throw error
×
853
    }
854
  }
855

856
  private async _exchangeCodeForSession(authCode: string): Promise<
857
    | {
858
        data: { session: Session; user: User; redirectType: string | null }
859
        error: null
860
      }
861
    | { data: { session: null; user: null; redirectType: null }; error: AuthError }
862
  > {
863
    const storageItem = await getItemAsync(this.storage, `${this.storageKey}-code-verifier`)
2✔
864
    const [codeVerifier, redirectType] = ((storageItem ?? '') as string).split('/')
2!
865

866
    try {
2✔
867
      const { data, error } = await _request(
2✔
868
        this.fetch,
869
        'POST',
870
        `${this.url}/token?grant_type=pkce`,
871
        {
872
          headers: this.headers,
873
          body: {
874
            auth_code: authCode,
875
            code_verifier: codeVerifier,
876
          },
877
          xform: _sessionResponse,
878
        }
879
      )
880
      await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
×
881
      if (error) {
×
882
        throw error
×
883
      }
884
      if (!data || !data.session || !data.user) {
×
885
        return {
×
886
          data: { user: null, session: null, redirectType: null },
887
          error: new AuthInvalidTokenResponseError(),
888
        }
889
      }
890
      if (data.session) {
×
891
        await this._saveSession(data.session)
×
892
        await this._notifyAllSubscribers('SIGNED_IN', data.session)
×
893
      }
894
      return { data: { ...data, redirectType: redirectType ?? null }, error }
×
895
    } catch (error) {
896
      if (isAuthError(error)) {
2✔
897
        return { data: { user: null, session: null, redirectType: null }, error }
2✔
898
      }
899

900
      throw error
×
901
    }
902
  }
903

904
  /**
905
   * Allows signing in with an OIDC ID token. The authentication provider used
906
   * should be enabled and configured.
907
   */
908
  async signInWithIdToken(credentials: SignInWithIdTokenCredentials): Promise<AuthTokenResponse> {
909
    try {
2✔
910
      const { options, provider, token, access_token, nonce } = credentials
2✔
911

912
      const res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=id_token`, {
2✔
913
        headers: this.headers,
914
        body: {
915
          provider,
916
          id_token: token,
917
          access_token,
918
          nonce,
919
          gotrue_meta_security: { captcha_token: options?.captchaToken },
6!
920
        },
921
        xform: _sessionResponse,
922
      })
923

924
      const { data, error } = res
×
925
      if (error) {
×
926
        return { data: { user: null, session: null }, error }
×
927
      } else if (!data || !data.session || !data.user) {
×
928
        return {
×
929
          data: { user: null, session: null },
930
          error: new AuthInvalidTokenResponseError(),
931
        }
932
      }
933
      if (data.session) {
×
934
        await this._saveSession(data.session)
×
935
        await this._notifyAllSubscribers('SIGNED_IN', data.session)
×
936
      }
937
      return { data, error }
×
938
    } catch (error) {
939
      if (isAuthError(error)) {
2✔
940
        return { data: { user: null, session: null }, error }
2✔
941
      }
942
      throw error
×
943
    }
944
  }
945

946
  /**
947
   * Log in a user using magiclink or a one-time password (OTP).
948
   *
949
   * If the `{{ .ConfirmationURL }}` variable is specified in the email template, a magiclink will be sent.
950
   * If the `{{ .Token }}` variable is specified in the email template, an OTP will be sent.
951
   * If you're using phone sign-ins, only an OTP will be sent. You won't be able to send a magiclink for phone sign-ins.
952
   *
953
   * Be aware that you may get back an error message that will not distinguish
954
   * between the cases where the account does not exist or, that the account
955
   * can only be accessed via social login.
956
   *
957
   * Do note that you will need to configure a Whatsapp sender on Twilio
958
   * if you are using phone sign in with the 'whatsapp' channel. The whatsapp
959
   * channel is not supported on other providers
960
   * at this time.
961
   * This method supports PKCE when an email is passed.
962
   */
963
  async signInWithOtp(credentials: SignInWithPasswordlessCredentials): Promise<AuthOtpResponse> {
964
    try {
10✔
965
      if ('email' in credentials) {
10✔
966
        const { email, options } = credentials
4✔
967
        let codeChallenge: string | null = null
4✔
968
        let codeChallengeMethod: string | null = null
4✔
969
        if (this.flowType === 'pkce') {
4✔
970
          ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
2✔
971
            this.storage,
972
            this.storageKey
973
          )
974
        }
975
        const { error } = await _request(this.fetch, 'POST', `${this.url}/otp`, {
4✔
976
          headers: this.headers,
977
          body: {
978
            email,
979
            data: options?.data ?? {},
24!
980
            create_user: options?.shouldCreateUser ?? true,
24!
981
            gotrue_meta_security: { captcha_token: options?.captchaToken },
12!
982
            code_challenge: codeChallenge,
983
            code_challenge_method: codeChallengeMethod,
984
          },
985
          redirectTo: options?.emailRedirectTo,
12!
986
        })
987
        return { data: { user: null, session: null }, error }
2✔
988
      }
989
      if ('phone' in credentials) {
6✔
990
        const { phone, options } = credentials
4✔
991
        const { data, error } = await _request(this.fetch, 'POST', `${this.url}/otp`, {
4✔
992
          headers: this.headers,
993
          body: {
994
            phone,
995
            data: options?.data ?? {},
24✔
996
            create_user: options?.shouldCreateUser ?? true,
24✔
997
            gotrue_meta_security: { captcha_token: options?.captchaToken },
12✔
998
            channel: options?.channel ?? 'sms',
24✔
999
          },
1000
        })
1001
        return { data: { user: null, session: null, messageId: data?.message_id }, error }
×
1002
      }
1003
      throw new AuthInvalidCredentialsError('You must provide either an email or phone number.')
2✔
1004
    } catch (error) {
1005
      if (isAuthError(error)) {
8✔
1006
        return { data: { user: null, session: null }, error }
8✔
1007
      }
1008

1009
      throw error
×
1010
    }
1011
  }
1012

1013
  /**
1014
   * Log in a user given a User supplied OTP or TokenHash received through mobile or email.
1015
   */
1016
  async verifyOtp(params: VerifyOtpParams): Promise<AuthResponse> {
1017
    try {
6✔
1018
      let redirectTo: string | undefined = undefined
6✔
1019
      let captchaToken: string | undefined = undefined
6✔
1020
      if ('options' in params) {
6✔
1021
        redirectTo = params.options?.redirectTo
2!
1022
        captchaToken = params.options?.captchaToken
2!
1023
      }
1024
      const { data, error } = await _request(this.fetch, 'POST', `${this.url}/verify`, {
6✔
1025
        headers: this.headers,
1026
        body: {
1027
          ...params,
1028
          gotrue_meta_security: { captcha_token: captchaToken },
1029
        },
1030
        redirectTo,
1031
        xform: _sessionResponse,
1032
      })
1033

1034
      if (error) {
×
1035
        throw error
×
1036
      }
1037

1038
      if (!data) {
×
1039
        throw new Error('An error occurred on token verification.')
×
1040
      }
1041

1042
      const session: Session | null = data.session
×
1043
      const user: User = data.user
×
1044

1045
      if (session?.access_token) {
×
1046
        await this._saveSession(session as Session)
×
1047
        await this._notifyAllSubscribers(
×
1048
          params.type == 'recovery' ? 'PASSWORD_RECOVERY' : 'SIGNED_IN',
×
1049
          session
1050
        )
1051
      }
1052

1053
      return { data: { user, session }, error: null }
×
1054
    } catch (error) {
1055
      if (isAuthError(error)) {
6✔
1056
        return { data: { user: null, session: null }, error }
6✔
1057
      }
1058

1059
      throw error
×
1060
    }
1061
  }
1062

1063
  /**
1064
   * Attempts a single-sign on using an enterprise Identity Provider. A
1065
   * successful SSO attempt will redirect the current page to the identity
1066
   * provider authorization page. The redirect URL is implementation and SSO
1067
   * protocol specific.
1068
   *
1069
   * You can use it by providing a SSO domain. Typically you can extract this
1070
   * domain by asking users for their email address. If this domain is
1071
   * registered on the Auth instance the redirect will use that organization's
1072
   * currently active SSO Identity Provider for the login.
1073
   *
1074
   * If you have built an organization-specific login page, you can use the
1075
   * organization's SSO Identity Provider UUID directly instead.
1076
   */
1077
  async signInWithSSO(params: SignInWithSSO): Promise<SSOResponse> {
1078
    try {
2✔
1079
      let codeChallenge: string | null = null
2✔
1080
      let codeChallengeMethod: string | null = null
2✔
1081
      if (this.flowType === 'pkce') {
2✔
1082
        ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
2✔
1083
          this.storage,
1084
          this.storageKey
1085
        )
1086
      }
1087

1088
      return await _request(this.fetch, 'POST', `${this.url}/sso`, {
2✔
1089
        body: {
1090
          ...('providerId' in params ? { provider_id: params.providerId } : null),
2!
1091
          ...('domain' in params ? { domain: params.domain } : null),
2!
1092
          redirect_to: params.options?.redirectTo ?? undefined,
12!
1093
          ...(params?.options?.captchaToken
14!
1094
            ? { gotrue_meta_security: { captcha_token: params.options.captchaToken } }
1095
            : null),
1096
          skip_http_redirect: true, // fetch does not handle redirects
1097
          code_challenge: codeChallenge,
1098
          code_challenge_method: codeChallengeMethod,
1099
        },
1100
        headers: this.headers,
1101
        xform: _ssoResponse,
1102
      })
1103
    } catch (error) {
1104
      if (isAuthError(error)) {
2✔
1105
        return { data: null, error }
2✔
1106
      }
1107
      throw error
×
1108
    }
1109
  }
1110

1111
  /**
1112
   * Sends a reauthentication OTP to the user's email or phone number.
1113
   * Requires the user to be signed-in.
1114
   */
1115
  async reauthenticate(): Promise<AuthResponse> {
1116
    await this.initializePromise
4✔
1117

1118
    return await this._acquireLock(-1, async () => {
4✔
1119
      return await this._reauthenticate()
4✔
1120
    })
1121
  }
1122

1123
  private async _reauthenticate(): Promise<AuthResponse> {
1124
    try {
4✔
1125
      return await this._useSession(async (result) => {
4✔
1126
        const {
1127
          data: { session },
1128
          error: sessionError,
1129
        } = result
4✔
1130
        if (sessionError) throw sessionError
4!
1131
        if (!session) throw new AuthSessionMissingError()
4!
1132

1133
        const { error } = await _request(this.fetch, 'GET', `${this.url}/reauthenticate`, {
4✔
1134
          headers: this.headers,
1135
          jwt: session.access_token,
1136
        })
1137
        return { data: { user: null, session: null }, error }
2✔
1138
      })
1139
    } catch (error) {
1140
      if (isAuthError(error)) {
2✔
1141
        return { data: { user: null, session: null }, error }
2✔
1142
      }
1143
      throw error
×
1144
    }
1145
  }
1146

1147
  /**
1148
   * Resends an existing signup confirmation email, email change email, SMS OTP or phone change OTP.
1149
   */
1150
  async resend(credentials: ResendParams): Promise<AuthOtpResponse> {
1151
    try {
6✔
1152
      const endpoint = `${this.url}/resend`
6✔
1153
      if ('email' in credentials) {
6✔
1154
        const { email, type, options } = credentials
2✔
1155
        const { error } = await _request(this.fetch, 'POST', endpoint, {
2✔
1156
          headers: this.headers,
1157
          body: {
1158
            email,
1159
            type,
1160
            gotrue_meta_security: { captcha_token: options?.captchaToken },
6!
1161
          },
1162
          redirectTo: options?.emailRedirectTo,
6!
1163
        })
1164
        return { data: { user: null, session: null }, error }
2✔
1165
      } else if ('phone' in credentials) {
4✔
1166
        const { phone, type, options } = credentials
2✔
1167
        const { data, error } = await _request(this.fetch, 'POST', endpoint, {
2✔
1168
          headers: this.headers,
1169
          body: {
1170
            phone,
1171
            type,
1172
            gotrue_meta_security: { captcha_token: options?.captchaToken },
6!
1173
          },
1174
        })
1175
        return { data: { user: null, session: null, messageId: data?.message_id }, error }
2!
1176
      }
1177
      throw new AuthInvalidCredentialsError(
2✔
1178
        'You must provide either an email or phone number and a type'
1179
      )
1180
    } catch (error) {
1181
      if (isAuthError(error)) {
2✔
1182
        return { data: { user: null, session: null }, error }
2✔
1183
      }
1184
      throw error
×
1185
    }
1186
  }
1187

1188
  /**
1189
   * Returns the session, refreshing it if necessary.
1190
   *
1191
   * The session returned can be null if the session is not detected which can happen in the event a user is not signed-in or has logged out.
1192
   *
1193
   * **IMPORTANT:** This method loads values directly from the storage attached
1194
   * to the client. If that storage is based on request cookies for example,
1195
   * the values in it may not be authentic and therefore it's strongly advised
1196
   * against using this method and its results in such circumstances. A warning
1197
   * will be emitted if this is detected. Use {@link #getUser()} instead.
1198
   */
1199
  async getSession() {
1200
    await this.initializePromise
69✔
1201

1202
    const result = await this._acquireLock(-1, async () => {
69✔
1203
      return this._useSession(async (result) => {
69✔
1204
        return result
65✔
1205
      })
1206
    })
1207

1208
    return result
65✔
1209
  }
1210

1211
  /**
1212
   * Acquires a global lock based on the storage key.
1213
   */
1214
  private async _acquireLock<R>(acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
1215
    this._debug('#_acquireLock', 'begin', acquireTimeout)
439✔
1216

1217
    try {
439✔
1218
      if (this.lockAcquired) {
439✔
1219
        const last = this.pendingInLock.length
2!
1220
          ? this.pendingInLock[this.pendingInLock.length - 1]
1221
          : Promise.resolve()
1222

1223
        const result = (async () => {
2✔
1224
          await last
2✔
1225
          return await fn()
2✔
1226
        })()
1227

1228
        this.pendingInLock.push(
2✔
1229
          (async () => {
1230
            try {
2✔
1231
              await result
2✔
1232
            } catch (e: any) {
1233
              // we just care if it finished
1234
            }
1235
          })()
1236
        )
1237

1238
        return result
2✔
1239
      }
1240

1241
      return await this.lock(`lock:${this.storageKey}`, acquireTimeout, async () => {
437✔
1242
        this._debug('#_acquireLock', 'lock acquired for storage key', this.storageKey)
435✔
1243

1244
        try {
435✔
1245
          this.lockAcquired = true
435✔
1246

1247
          const result = fn()
435✔
1248

1249
          this.pendingInLock.push(
435✔
1250
            (async () => {
1251
              try {
435✔
1252
                await result
435✔
1253
              } catch (e: any) {
1254
                // we just care if it finished
1255
              }
1256
            })()
1257
          )
1258

1259
          await result
435✔
1260

1261
          // keep draining the queue until there's nothing to wait on
1262
          while (this.pendingInLock.length) {
427✔
1263
            const waitOn = [...this.pendingInLock]
427✔
1264

1265
            await Promise.all(waitOn)
427✔
1266

1267
            this.pendingInLock.splice(0, waitOn.length)
427✔
1268
          }
1269

1270
          return await result
427✔
1271
        } finally {
1272
          this._debug('#_acquireLock', 'lock released for storage key', this.storageKey)
435✔
1273

1274
          this.lockAcquired = false
435✔
1275
        }
1276
      })
1277
    } finally {
1278
      this._debug('#_acquireLock', 'end')
439✔
1279
    }
1280
  }
1281

1282
  /**
1283
   * Use instead of {@link #getSession} inside the library. It is
1284
   * semantically usually what you want, as getting a session involves some
1285
   * processing afterwards that requires only one client operating on the
1286
   * session at once across multiple tabs or processes.
1287
   */
1288
  private async _useSession<R>(
1289
    fn: (
1290
      result:
1291
        | {
1292
            data: {
1293
              session: Session
1294
            }
1295
            error: null
1296
          }
1297
        | {
1298
            data: {
1299
              session: null
1300
            }
1301
            error: AuthError
1302
          }
1303
        | {
1304
            data: {
1305
              session: null
1306
            }
1307
            error: null
1308
          }
1309
    ) => Promise<R>
1310
  ): Promise<R> {
1311
    this._debug('#_useSession', 'begin')
327✔
1312

1313
    try {
327✔
1314
      // the use of __loadSession here is the only correct use of the function!
1315
      const result = await this.__loadSession()
327✔
1316

1317
      return await fn(result)
321✔
1318
    } finally {
1319
      this._debug('#_useSession', 'end')
327✔
1320
    }
1321
  }
1322

1323
  /**
1324
   * NEVER USE DIRECTLY!
1325
   *
1326
   * Always use {@link #_useSession}.
1327
   */
1328
  private async __loadSession(): Promise<
1329
    | {
1330
        data: {
1331
          session: Session
1332
        }
1333
        error: null
1334
      }
1335
    | {
1336
        data: {
1337
          session: null
1338
        }
1339
        error: AuthError
1340
      }
1341
    | {
1342
        data: {
1343
          session: null
1344
        }
1345
        error: null
1346
      }
1347
  > {
1348
    this._debug('#__loadSession()', 'begin')
327✔
1349

1350
    if (!this.lockAcquired) {
327✔
1351
      this._debug('#__loadSession()', 'used outside of an acquired lock!', new Error().stack)
22✔
1352
    }
1353

1354
    try {
327✔
1355
      let currentSession: Session | null = null
327✔
1356

1357
      const maybeSession = await getItemAsync(this.storage, this.storageKey)
327✔
1358

1359
      this._debug('#getSession()', 'session from storage', maybeSession)
323✔
1360

1361
      if (maybeSession !== null) {
323✔
1362
        if (this._isValidSession(maybeSession)) {
165✔
1363
          currentSession = maybeSession
157✔
1364
        } else {
1365
          this._debug('#getSession()', 'session from storage is not valid')
8✔
1366
          await this._removeSession()
8✔
1367
        }
1368
      }
1369

1370
      if (!currentSession) {
321✔
1371
        return { data: { session: null }, error: null }
164✔
1372
      }
1373

1374
      // A session is considered expired before the access token _actually_
1375
      // expires. When the autoRefreshToken option is off (or when the tab is
1376
      // in the background), very eager users of getSession() -- like
1377
      // realtime-js -- might send a valid JWT which will expire by the time it
1378
      // reaches the server.
1379
      const hasExpired = currentSession.expires_at
157!
1380
        ? currentSession.expires_at * 1000 - Date.now() < EXPIRY_MARGIN_MS
1381
        : false
1382

1383
      this._debug(
157✔
1384
        '#__loadSession()',
1385
        `session has${hasExpired ? '' : ' not'} expired`,
157✔
1386
        'expires_at',
1387
        currentSession.expires_at
1388
      )
1389

1390
      if (!hasExpired) {
157✔
1391
        if (this.userStorage) {
149✔
1392
          const maybeUser: { user?: User | null } | null = (await getItemAsync(
2✔
1393
            this.userStorage,
1394
            this.storageKey + '-user'
1395
          )) as any
1396

1397
          if (maybeUser?.user) {
2!
1398
            currentSession.user = maybeUser.user
×
1399
          } else {
1400
            currentSession.user = userNotAvailableProxy()
2✔
1401
          }
1402
        }
1403

1404
        if (this.storage.isServer && currentSession.user) {
149✔
1405
          let suppressWarning = this.suppressGetSessionWarning
14✔
1406
          const proxySession: Session = new Proxy(currentSession, {
14✔
1407
            get: (target: any, prop: string, receiver: any) => {
1408
              if (!suppressWarning && prop === 'user') {
24✔
1409
                // only show warning when the user object is being accessed from the server
1410
                console.warn(
2✔
1411
                  'Using the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.'
1412
                )
1413
                suppressWarning = true // keeps this proxy instance from logging additional warnings
2✔
1414
                this.suppressGetSessionWarning = true // keeps this client's future proxy instances from warning
2✔
1415
              }
1416
              return Reflect.get(target, prop, receiver)
24✔
1417
            },
1418
          })
1419
          currentSession = proxySession
14✔
1420
        }
1421

1422
        return { data: { session: currentSession }, error: null }
149✔
1423
      }
1424

1425
      const { session, error } = await this._callRefreshToken(currentSession.refresh_token)
8✔
1426
      if (error) {
8✔
1427
        return { data: { session: null }, error }
4✔
1428
      }
1429

1430
      return { data: { session }, error: null }
4✔
1431
    } finally {
1432
      this._debug('#__loadSession()', 'end')
327✔
1433
    }
1434
  }
1435

1436
  /**
1437
   * Gets the current user details if there is an existing session. This method
1438
   * performs a network request to the Supabase Auth server, so the returned
1439
   * value is authentic and can be used to base authorization rules on.
1440
   *
1441
   * @param jwt Takes in an optional access token JWT. If no JWT is provided, the JWT from the current session is used.
1442
   */
1443
  async getUser(jwt?: string): Promise<UserResponse> {
1444
    if (jwt) {
21✔
1445
      return await this._getUser(jwt)
7✔
1446
    }
1447

1448
    await this.initializePromise
14✔
1449

1450
    const result = await this._acquireLock(-1, async () => {
14✔
1451
      return await this._getUser()
14✔
1452
    })
1453

1454
    return result
14✔
1455
  }
1456

1457
  private async _getUser(jwt?: string): Promise<UserResponse> {
1458
    try {
25✔
1459
      if (jwt) {
25✔
1460
        return await _request(this.fetch, 'GET', `${this.url}/user`, {
11✔
1461
          headers: this.headers,
1462
          jwt: jwt,
1463
          xform: _userResponse,
1464
        })
1465
      }
1466

1467
      return await this._useSession(async (result) => {
14✔
1468
        const { data, error } = result
14✔
1469
        if (error) {
14!
1470
          throw error
×
1471
        }
1472

1473
        // returns an error if there is no access_token or custom authorization header
1474
        if (!data.session?.access_token && !this.hasCustomAuthorizationHeader) {
14✔
1475
          return { data: { user: null }, error: new AuthSessionMissingError() }
4✔
1476
        }
1477

1478
        return await _request(this.fetch, 'GET', `${this.url}/user`, {
10✔
1479
          headers: this.headers,
1480
          jwt: data.session?.access_token ?? undefined,
60!
1481
          xform: _userResponse,
1482
        })
1483
      })
1484
    } catch (error) {
1485
      if (isAuthError(error)) {
2✔
1486
        if (isAuthSessionMissingError(error)) {
2!
1487
          // JWT contains a `session_id` which does not correspond to an active
1488
          // session in the database, indicating the user is signed out.
1489

1490
          await this._removeSession()
×
1491
          await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
×
1492
        }
1493

1494
        return { data: { user: null }, error }
2✔
1495
      }
1496

1497
      throw error
×
1498
    }
1499
  }
1500

1501
  /**
1502
   * Updates user data for a logged in user.
1503
   */
1504
  async updateUser(
1505
    attributes: UserAttributes,
1506
    options: {
6✔
1507
      emailRedirectTo?: string | undefined
1508
    } = {}
1509
  ): Promise<UserResponse> {
1510
    await this.initializePromise
6✔
1511

1512
    return await this._acquireLock(-1, async () => {
6✔
1513
      return await this._updateUser(attributes, options)
6✔
1514
    })
1515
  }
1516

1517
  protected async _updateUser(
1518
    attributes: UserAttributes,
1519
    options: {
×
1520
      emailRedirectTo?: string | undefined
1521
    } = {}
1522
  ): Promise<UserResponse> {
1523
    try {
6✔
1524
      return await this._useSession(async (result) => {
6✔
1525
        const { data: sessionData, error: sessionError } = result
6✔
1526
        if (sessionError) {
6!
1527
          throw sessionError
×
1528
        }
1529
        if (!sessionData.session) {
6!
1530
          throw new AuthSessionMissingError()
×
1531
        }
1532
        const session: Session = sessionData.session
6✔
1533
        let codeChallenge: string | null = null
6✔
1534
        let codeChallengeMethod: string | null = null
6✔
1535
        if (this.flowType === 'pkce' && attributes.email != null) {
6!
1536
          ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
×
1537
            this.storage,
1538
            this.storageKey
1539
          )
1540
        }
1541

1542
        const { data, error: userError } = await _request(this.fetch, 'PUT', `${this.url}/user`, {
6✔
1543
          headers: this.headers,
1544
          redirectTo: options?.emailRedirectTo,
18!
1545
          body: {
1546
            ...attributes,
1547
            code_challenge: codeChallenge,
1548
            code_challenge_method: codeChallengeMethod,
1549
          },
1550
          jwt: session.access_token,
1551
          xform: _userResponse,
1552
        })
1553
        if (userError) throw userError
6!
1554
        session.user = data.user as User
6✔
1555
        await this._saveSession(session)
6✔
1556
        await this._notifyAllSubscribers('USER_UPDATED', session)
6✔
1557
        return { data: { user: session.user }, error: null }
6✔
1558
      })
1559
    } catch (error) {
1560
      if (isAuthError(error)) {
×
1561
        return { data: { user: null }, error }
×
1562
      }
1563

1564
      throw error
×
1565
    }
1566
  }
1567

1568
  /**
1569
   * Sets the session data from the current session. If the current session is expired, setSession will take care of refreshing it to obtain a new session.
1570
   * If the refresh token or access token in the current session is invalid, an error will be thrown.
1571
   * @param currentSession The current session that minimally contains an access token and refresh token.
1572
   */
1573
  async setSession(currentSession: {
1574
    access_token: string
1575
    refresh_token: string
1576
  }): Promise<AuthResponse> {
1577
    await this.initializePromise
10✔
1578

1579
    return await this._acquireLock(-1, async () => {
10✔
1580
      return await this._setSession(currentSession)
10✔
1581
    })
1582
  }
1583

1584
  protected async _setSession(currentSession: {
1585
    access_token: string
1586
    refresh_token: string
1587
  }): Promise<AuthResponse> {
1588
    try {
10✔
1589
      if (!currentSession.access_token || !currentSession.refresh_token) {
10✔
1590
        throw new AuthSessionMissingError()
4✔
1591
      }
1592

1593
      const timeNow = Date.now() / 1000
6✔
1594
      let expiresAt = timeNow
6✔
1595
      let hasExpired = true
6✔
1596
      let session: Session | null = null
6✔
1597
      const { payload } = decodeJWT(currentSession.access_token)
6✔
1598
      if (payload.exp) {
4✔
1599
        expiresAt = payload.exp
4✔
1600
        hasExpired = expiresAt <= timeNow
4✔
1601
      }
1602

1603
      if (hasExpired) {
4✔
1604
        const { session: refreshedSession, error } = await this._callRefreshToken(
2✔
1605
          currentSession.refresh_token
1606
        )
1607
        if (error) {
2✔
1608
          return { data: { user: null, session: null }, error: error }
2✔
1609
        }
1610

1611
        if (!refreshedSession) {
×
1612
          return { data: { user: null, session: null }, error: null }
×
1613
        }
1614
        session = refreshedSession
×
1615
      } else {
1616
        const { data, error } = await this._getUser(currentSession.access_token)
2✔
1617
        if (error) {
2!
1618
          throw error
×
1619
        }
1620
        session = {
2✔
1621
          access_token: currentSession.access_token,
1622
          refresh_token: currentSession.refresh_token,
1623
          user: data.user,
1624
          token_type: 'bearer',
1625
          expires_in: expiresAt - timeNow,
1626
          expires_at: expiresAt,
1627
        }
1628
        await this._saveSession(session)
2✔
1629
        await this._notifyAllSubscribers('SIGNED_IN', session)
2✔
1630
      }
1631

1632
      return { data: { user: session.user, session }, error: null }
2✔
1633
    } catch (error) {
1634
      if (isAuthError(error)) {
6✔
1635
        return { data: { session: null, user: null }, error }
6✔
1636
      }
1637

1638
      throw error
×
1639
    }
1640
  }
1641

1642
  /**
1643
   * Returns a new session, regardless of expiry status.
1644
   * Takes in an optional current session. If not passed in, then refreshSession() will attempt to retrieve it from getSession().
1645
   * If the current session's refresh token is invalid, an error will be thrown.
1646
   * @param currentSession The current session. If passed in, it must contain a refresh token.
1647
   */
1648
  async refreshSession(currentSession?: { refresh_token: string }): Promise<AuthResponse> {
1649
    await this.initializePromise
4✔
1650

1651
    return await this._acquireLock(-1, async () => {
4✔
1652
      return await this._refreshSession(currentSession)
4✔
1653
    })
1654
  }
1655

1656
  protected async _refreshSession(currentSession?: {
1657
    refresh_token: string
1658
  }): Promise<AuthResponse> {
1659
    try {
4✔
1660
      return await this._useSession(async (result) => {
4✔
1661
        if (!currentSession) {
4✔
1662
          const { data, error } = result
2✔
1663
          if (error) {
2!
1664
            throw error
×
1665
          }
1666

1667
          currentSession = data.session ?? undefined
2!
1668
        }
1669

1670
        if (!currentSession?.refresh_token) {
4!
1671
          throw new AuthSessionMissingError()
×
1672
        }
1673

1674
        const { session, error } = await this._callRefreshToken(currentSession.refresh_token)
4✔
1675
        if (error) {
4!
1676
          return { data: { user: null, session: null }, error: error }
×
1677
        }
1678

1679
        if (!session) {
4!
1680
          return { data: { user: null, session: null }, error: null }
×
1681
        }
1682

1683
        return { data: { user: session.user, session }, error: null }
4✔
1684
      })
1685
    } catch (error) {
1686
      if (isAuthError(error)) {
×
1687
        return { data: { user: null, session: null }, error }
×
1688
      }
1689

1690
      throw error
×
1691
    }
1692
  }
1693

1694
  /**
1695
   * Gets the session data from a URL string
1696
   */
1697
  private async _getSessionFromURL(
1698
    params: { [parameter: string]: string },
1699
    callbackUrlType: string
1700
  ): Promise<
1701
    | {
1702
        data: { session: Session; redirectType: string | null }
1703
        error: null
1704
      }
1705
    | { data: { session: null; redirectType: null }; error: AuthError }
1706
  > {
1707
    try {
8✔
1708
      if (!isBrowser()) throw new AuthImplicitGrantRedirectError('No browser detected.')
8✔
1709

1710
      // If there's an error in the URL, it doesn't matter what flow it is, we just return the error.
1711
      if (params.error || params.error_description || params.error_code) {
6✔
1712
        // The error class returned implies that the redirect is from an implicit grant flow
1713
        // but it could also be from a redirect error from a PKCE flow.
1714
        throw new AuthImplicitGrantRedirectError(
4✔
1715
          params.error_description || 'Error in URL with unspecified error_description',
4!
1716
          {
1717
            error: params.error || 'unspecified_error',
4!
1718
            code: params.error_code || 'unspecified_code',
8✔
1719
          }
1720
        )
1721
      }
1722

1723
      // Checks for mismatches between the flowType initialised in the client and the URL parameters
1724
      switch (callbackUrlType) {
2!
1725
        case 'implicit':
1726
          if (this.flowType === 'pkce') {
2!
1727
            throw new AuthPKCEGrantCodeExchangeError('Not a valid PKCE flow url.')
×
1728
          }
1729
          break
2✔
1730
        case 'pkce':
1731
          if (this.flowType === 'implicit') {
×
1732
            throw new AuthImplicitGrantRedirectError('Not a valid implicit grant flow url.')
×
1733
          }
1734
          break
×
1735
        default:
1736
        // there's no mismatch so we continue
1737
      }
1738

1739
      // Since this is a redirect for PKCE, we attempt to retrieve the code from the URL for the code exchange
1740
      if (callbackUrlType === 'pkce') {
2!
1741
        this._debug('#_initialize()', 'begin', 'is PKCE flow', true)
×
1742
        if (!params.code) throw new AuthPKCEGrantCodeExchangeError('No code detected.')
×
1743
        const { data, error } = await this._exchangeCodeForSession(params.code)
×
1744
        if (error) throw error
×
1745

1746
        const url = new URL(window.location.href)
×
1747
        url.searchParams.delete('code')
×
1748

1749
        window.history.replaceState(window.history.state, '', url.toString())
×
1750

1751
        return { data: { session: data.session, redirectType: null }, error: null }
×
1752
      }
1753

1754
      const {
1755
        provider_token,
1756
        provider_refresh_token,
1757
        access_token,
1758
        refresh_token,
1759
        expires_in,
1760
        expires_at,
1761
        token_type,
1762
      } = params
2✔
1763

1764
      if (!access_token || !expires_in || !refresh_token || !token_type) {
2!
1765
        throw new AuthImplicitGrantRedirectError('No session defined in URL')
×
1766
      }
1767

1768
      const timeNow = Math.round(Date.now() / 1000)
2✔
1769
      const expiresIn = parseInt(expires_in)
2✔
1770
      let expiresAt = timeNow + expiresIn
2✔
1771

1772
      if (expires_at) {
2!
1773
        expiresAt = parseInt(expires_at)
×
1774
      }
1775

1776
      const actuallyExpiresIn = expiresAt - timeNow
2✔
1777
      if (actuallyExpiresIn * 1000 <= AUTO_REFRESH_TICK_DURATION_MS) {
2!
1778
        console.warn(
×
1779
          `@supabase/gotrue-js: Session as retrieved from URL expires in ${actuallyExpiresIn}s, should have been closer to ${expiresIn}s`
1780
        )
1781
      }
1782

1783
      const issuedAt = expiresAt - expiresIn
2✔
1784
      if (timeNow - issuedAt >= 120) {
2!
1785
        console.warn(
×
1786
          '@supabase/gotrue-js: Session as retrieved from URL was issued over 120s ago, URL could be stale',
1787
          issuedAt,
1788
          expiresAt,
1789
          timeNow
1790
        )
1791
      } else if (timeNow - issuedAt < 0) {
2!
1792
        console.warn(
×
1793
          '@supabase/gotrue-js: Session as retrieved from URL was issued in the future? Check the device clock for skew',
1794
          issuedAt,
1795
          expiresAt,
1796
          timeNow
1797
        )
1798
      }
1799

1800
      const { data, error } = await this._getUser(access_token)
2✔
1801
      if (error) throw error
2!
1802

1803
      const session: Session = {
2✔
1804
        provider_token,
1805
        provider_refresh_token,
1806
        access_token,
1807
        expires_in: expiresIn,
1808
        expires_at: expiresAt,
1809
        refresh_token,
1810
        token_type,
1811
        user: data.user,
1812
      }
1813

1814
      // Remove tokens from URL
1815
      window.location.hash = ''
2✔
1816
      this._debug('#_getSessionFromURL()', 'clearing window.location.hash')
2✔
1817

1818
      return { data: { session, redirectType: params.type }, error: null }
2✔
1819
    } catch (error) {
1820
      if (isAuthError(error)) {
6✔
1821
        return { data: { session: null, redirectType: null }, error }
6✔
1822
      }
1823

1824
      throw error
×
1825
    }
1826
  }
1827

1828
  /**
1829
   * Checks if the current URL contains parameters given by an implicit oauth grant flow (https://www.rfc-editor.org/rfc/rfc6749.html#section-4.2)
1830
   */
1831
  private _isImplicitGrantCallback(params: { [parameter: string]: string }): boolean {
1832
    return Boolean(params.access_token || params.error_description)
28✔
1833
  }
1834

1835
  /**
1836
   * Checks if the current URL and backing storage contain parameters given by a PKCE flow
1837
   */
1838
  private async _isPKCECallback(params: { [parameter: string]: string }): Promise<boolean> {
1839
    const currentStorageContent = await getItemAsync(
22✔
1840
      this.storage,
1841
      `${this.storageKey}-code-verifier`
1842
    )
1843

1844
    return !!(params.code && currentStorageContent)
22!
1845
  }
1846

1847
  /**
1848
   * Inside a browser context, `signOut()` will remove the logged in user from the browser session and log them out - removing all items from localstorage and then trigger a `"SIGNED_OUT"` event.
1849
   *
1850
   * For server-side management, you can revoke all refresh tokens for a user by passing a user's JWT through to `auth.api.signOut(JWT: string)`.
1851
   * There is no way to revoke a user's access token jwt until it expires. It is recommended to set a shorter expiry on the jwt for this reason.
1852
   *
1853
   * If using `others` scope, no `SIGNED_OUT` event is fired!
1854
   */
1855
  async signOut(options: SignOut = { scope: 'global' }): Promise<{ error: AuthError | null }> {
170✔
1856
    await this.initializePromise
172✔
1857

1858
    return await this._acquireLock(-1, async () => {
172✔
1859
      return await this._signOut(options)
172✔
1860
    })
1861
  }
1862

1863
  protected async _signOut(
1864
    { scope }: SignOut = { scope: 'global' }
×
1865
  ): Promise<{ error: AuthError | null }> {
1866
    return await this._useSession(async (result) => {
172✔
1867
      const { data, error: sessionError } = result
170✔
1868
      if (sessionError) {
170!
1869
        return { error: sessionError }
×
1870
      }
1871
      const accessToken = data.session?.access_token
170✔
1872
      if (accessToken) {
170✔
1873
        const { error } = await this.admin.signOut(accessToken, scope)
50✔
1874
        if (error) {
48✔
1875
          // ignore 404s since user might not exist anymore
1876
          // ignore 401s since an invalid or expired JWT should sign out the current session
1877
          if (
2!
1878
            !(
1879
              isAuthApiError(error) &&
8✔
1880
              (error.status === 404 || error.status === 401 || error.status === 403)
1881
            )
1882
          ) {
1883
            return { error }
×
1884
          }
1885
        }
1886
      }
1887
      if (scope !== 'others') {
168✔
1888
        await this._removeSession()
168✔
1889
        await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
168✔
1890
      }
1891
      return { error: null }
168✔
1892
    })
1893
  }
1894

1895
  /**
1896
   * Receive a notification every time an auth event happens.
1897
   * @param callback A callback function to be invoked when an auth event happens.
1898
   */
1899
  onAuthStateChange(
1900
    callback: (event: AuthChangeEvent, session: Session | null) => void | Promise<void>
1901
  ): {
1902
    data: { subscription: Subscription }
1903
  } {
1904
    const id: string = uuid()
10✔
1905
    const subscription: Subscription = {
10✔
1906
      id,
1907
      callback,
1908
      unsubscribe: () => {
1909
        this._debug('#unsubscribe()', 'state change callback with id removed', id)
10✔
1910

1911
        this.stateChangeEmitters.delete(id)
10✔
1912
      },
1913
    }
1914

1915
    this._debug('#onAuthStateChange()', 'registered callback with id', id)
10✔
1916

1917
    this.stateChangeEmitters.set(id, subscription)
10✔
1918
    ;(async () => {
10✔
1919
      await this.initializePromise
10✔
1920

1921
      await this._acquireLock(-1, async () => {
10✔
1922
        this._emitInitialSession(id)
10✔
1923
      })
1924
    })()
1925

1926
    return { data: { subscription } }
10✔
1927
  }
1928

1929
  private async _emitInitialSession(id: string): Promise<void> {
1930
    return await this._useSession(async (result) => {
10✔
1931
      try {
10✔
1932
        const {
1933
          data: { session },
1934
          error,
1935
        } = result
10✔
1936
        if (error) throw error
10!
1937

1938
        await this.stateChangeEmitters.get(id)?.callback('INITIAL_SESSION', session)
10✔
1939
        this._debug('INITIAL_SESSION', 'callback id', id, 'session', session)
10✔
1940
      } catch (err) {
1941
        await this.stateChangeEmitters.get(id)?.callback('INITIAL_SESSION', null)
×
1942
        this._debug('INITIAL_SESSION', 'callback id', id, 'error', err)
×
1943
        console.error(err)
×
1944
      }
1945
    })
1946
  }
1947

1948
  /**
1949
   * Sends a password reset request to an email address. This method supports the PKCE flow.
1950
   *
1951
   * @param email The email address of the user.
1952
   * @param options.redirectTo The URL to send the user to after they click the password reset link.
1953
   * @param options.captchaToken Verification token received when the user completes the captcha on the site.
1954
   */
1955
  async resetPasswordForEmail(
1956
    email: string,
1957
    options: {
×
1958
      redirectTo?: string
1959
      captchaToken?: string
1960
    } = {}
1961
  ): Promise<
1962
    | {
1963
        data: {}
1964
        error: null
1965
      }
1966
    | { data: null; error: AuthError }
1967
  > {
1968
    let codeChallenge: string | null = null
4✔
1969
    let codeChallengeMethod: string | null = null
4✔
1970

1971
    if (this.flowType === 'pkce') {
4!
1972
      ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
×
1973
        this.storage,
1974
        this.storageKey,
1975
        true // isPasswordRecovery
1976
      )
1977
    }
1978
    try {
4✔
1979
      return await _request(this.fetch, 'POST', `${this.url}/recover`, {
4✔
1980
        body: {
1981
          email,
1982
          code_challenge: codeChallenge,
1983
          code_challenge_method: codeChallengeMethod,
1984
          gotrue_meta_security: { captcha_token: options.captchaToken },
1985
        },
1986
        headers: this.headers,
1987
        redirectTo: options.redirectTo,
1988
      })
1989
    } catch (error) {
1990
      if (isAuthError(error)) {
×
1991
        return { data: null, error }
×
1992
      }
1993

1994
      throw error
×
1995
    }
1996
  }
1997

1998
  /**
1999
   * Gets all the identities linked to a user.
2000
   */
2001
  async getUserIdentities(): Promise<
2002
    | {
2003
        data: {
2004
          identities: UserIdentity[]
2005
        }
2006
        error: null
2007
      }
2008
    | { data: null; error: AuthError }
2009
  > {
2010
    try {
6✔
2011
      const { data, error } = await this.getUser()
6✔
2012
      if (error) throw error
6✔
2013
      return { data: { identities: data.user.identities ?? [] }, error: null }
4!
2014
    } catch (error) {
2015
      if (isAuthError(error)) {
2✔
2016
        return { data: null, error }
2✔
2017
      }
2018
      throw error
×
2019
    }
2020
  }
2021
  /**
2022
   * Links an oauth identity to an existing user.
2023
   * This method supports the PKCE flow.
2024
   */
2025
  async linkIdentity(credentials: SignInWithOAuthCredentials): Promise<OAuthResponse> {
2026
    try {
2✔
2027
      const { data, error } = await this._useSession(async (result) => {
2✔
2028
        const { data, error } = result
2✔
2029
        if (error) throw error
2!
2030
        const url: string = await this._getUrlForProvider(
2✔
2031
          `${this.url}/user/identities/authorize`,
2032
          credentials.provider,
2033
          {
2034
            redirectTo: credentials.options?.redirectTo,
6!
2035
            scopes: credentials.options?.scopes,
6!
2036
            queryParams: credentials.options?.queryParams,
6!
2037
            skipBrowserRedirect: true,
2038
          }
2039
        )
2040
        return await _request(this.fetch, 'GET', url, {
2✔
2041
          headers: this.headers,
2042
          jwt: data.session?.access_token ?? undefined,
12!
2043
        })
2044
      })
2045
      if (error) throw error
×
2046
      if (isBrowser() && !credentials.options?.skipBrowserRedirect) {
×
2047
        window.location.assign(data?.url)
×
2048
      }
2049
      return { data: { provider: credentials.provider, url: data?.url }, error: null }
×
2050
    } catch (error) {
2051
      if (isAuthError(error)) {
2✔
2052
        return { data: { provider: credentials.provider, url: null }, error }
2✔
2053
      }
2054
      throw error
×
2055
    }
2056
  }
2057

2058
  /**
2059
   * Unlinks an identity from a user by deleting it. The user will no longer be able to sign in with that identity once it's unlinked.
2060
   */
2061
  async unlinkIdentity(identity: UserIdentity): Promise<
2062
    | {
2063
        data: {}
2064
        error: null
2065
      }
2066
    | { data: null; error: AuthError }
2067
  > {
2068
    try {
2✔
2069
      return await this._useSession(async (result) => {
2✔
2070
        const { data, error } = result
2✔
2071
        if (error) {
2!
2072
          throw error
×
2073
        }
2074
        return await _request(
2✔
2075
          this.fetch,
2076
          'DELETE',
2077
          `${this.url}/user/identities/${identity.identity_id}`,
2078
          {
2079
            headers: this.headers,
2080
            jwt: data.session?.access_token ?? undefined,
12!
2081
          }
2082
        )
2083
      })
2084
    } catch (error) {
2085
      if (isAuthError(error)) {
2✔
2086
        return { data: null, error }
2✔
2087
      }
2088
      throw error
×
2089
    }
2090
  }
2091

2092
  /**
2093
   * Generates a new JWT.
2094
   * @param refreshToken A valid refresh token that was returned on login.
2095
   */
2096
  private async _refreshAccessToken(refreshToken: string): Promise<AuthResponse> {
2097
    const debugName = `#_refreshAccessToken(${refreshToken.substring(0, 5)}...)`
22✔
2098
    this._debug(debugName, 'begin')
22✔
2099

2100
    try {
22✔
2101
      const startedAt = Date.now()
22✔
2102

2103
      // will attempt to refresh the token with exponential backoff
2104
      return await retryable(
22✔
2105
        async (attempt) => {
2106
          if (attempt > 0) {
22!
2107
            await sleep(200 * Math.pow(2, attempt - 1)) // 200, 400, 800, ...
×
2108
          }
2109

2110
          this._debug(debugName, 'refreshing attempt', attempt)
22✔
2111

2112
          return await _request(this.fetch, 'POST', `${this.url}/token?grant_type=refresh_token`, {
22✔
2113
            body: { refresh_token: refreshToken },
2114
            headers: this.headers,
2115
            xform: _sessionResponse,
2116
          })
2117
        },
2118
        (attempt, error) => {
2119
          const nextBackOffInterval = 200 * Math.pow(2, attempt)
22✔
2120
          return (
22✔
2121
            error &&
26!
2122
            isAuthRetryableFetchError(error) &&
2123
            // retryable only if the request can be sent before the backoff overflows the tick duration
2124
            Date.now() + nextBackOffInterval - startedAt < AUTO_REFRESH_TICK_DURATION_MS
2125
          )
2126
        }
2127
      )
2128
    } catch (error) {
2129
      this._debug(debugName, 'error', error)
4✔
2130

2131
      if (isAuthError(error)) {
4✔
2132
        return { data: { session: null, user: null }, error }
4✔
2133
      }
2134
      throw error
×
2135
    } finally {
2136
      this._debug(debugName, 'end')
22✔
2137
    }
2138
  }
2139

2140
  private _isValidSession(maybeSession: unknown): maybeSession is Session {
2141
    const isValidSession =
2142
      typeof maybeSession === 'object' &&
191✔
2143
      maybeSession !== null &&
2144
      'access_token' in maybeSession &&
2145
      'refresh_token' in maybeSession &&
2146
      'expires_at' in maybeSession
2147

2148
    return isValidSession
191✔
2149
  }
2150

2151
  private async _handleProviderSignIn(
2152
    provider: Provider,
2153
    options: {
2154
      redirectTo?: string
2155
      scopes?: string
2156
      queryParams?: { [key: string]: string }
2157
      skipBrowserRedirect?: boolean
2158
    }
2159
  ) {
2160
    const url: string = await this._getUrlForProvider(`${this.url}/authorize`, provider, {
10✔
2161
      redirectTo: options.redirectTo,
2162
      scopes: options.scopes,
2163
      queryParams: options.queryParams,
2164
    })
2165

2166
    this._debug('#_handleProviderSignIn()', 'provider', provider, 'options', options, 'url', url)
10✔
2167

2168
    // try to open on the browser
2169
    if (isBrowser() && !options.skipBrowserRedirect) {
10✔
2170
      window.location.assign(url)
2✔
2171
    }
2172

2173
    return { data: { provider, url }, error: null }
10✔
2174
  }
2175

2176
  /**
2177
   * Recovers the session from LocalStorage and refreshes the token
2178
   * Note: this method is async to accommodate for AsyncStorage e.g. in React native.
2179
   */
2180
  private async _recoverAndRefresh() {
2181
    const debugName = '#_recoverAndRefresh()'
26✔
2182
    this._debug(debugName, 'begin')
26✔
2183

2184
    try {
26✔
2185
      const currentSession = (await getItemAsync(this.storage, this.storageKey)) as Session | null
26✔
2186

2187
      if (currentSession && this.userStorage) {
26!
2188
        let maybeUser: { user: User | null } | null = (await getItemAsync(
×
2189
          this.userStorage,
2190
          this.storageKey + '-user'
2191
        )) as any
2192

2193
        if (!this.storage.isServer && Object.is(this.storage, this.userStorage) && !maybeUser) {
×
2194
          // storage and userStorage are the same storage medium, for example
2195
          // window.localStorage if userStorage does not have the user from
2196
          // storage stored, store it first thereby migrating the user object
2197
          // from storage -> userStorage
2198

2199
          maybeUser = { user: currentSession.user }
×
2200
          await setItemAsync(this.userStorage, this.storageKey + '-user', maybeUser)
×
2201
        }
2202

2203
        currentSession.user = maybeUser?.user ?? userNotAvailableProxy()
×
2204
      } else if (currentSession && !currentSession.user) {
26!
2205
        // user storage is not set, let's check if it was previously enabled so
2206
        // we bring back the storage as it should be
2207

2208
        if (!currentSession.user) {
×
2209
          // test if userStorage was previously enabled and the storage medium was the same, to move the user back under the same key
2210
          const separateUser: { user: User | null } | null = (await getItemAsync(
×
2211
            this.storage,
2212
            this.storageKey + '-user'
2213
          )) as any
2214

2215
          if (separateUser && separateUser?.user) {
×
2216
            currentSession.user = separateUser.user
×
2217

2218
            await removeItemAsync(this.storage, this.storageKey + '-user')
×
2219
            await setItemAsync(this.storage, this.storageKey, currentSession)
×
2220
          } else {
2221
            currentSession.user = userNotAvailableProxy()
×
2222
          }
2223
        }
2224
      }
2225

2226
      this._debug(debugName, 'session from storage', currentSession)
26✔
2227

2228
      if (!this._isValidSession(currentSession)) {
26✔
2229
        this._debug(debugName, 'session is not valid')
22✔
2230
        if (currentSession !== null) {
22!
2231
          await this._removeSession()
×
2232
        }
2233

2234
        return
22✔
2235
      }
2236

2237
      const expiresWithMargin =
2238
        (currentSession.expires_at ?? Infinity) * 1000 - Date.now() < EXPIRY_MARGIN_MS
4!
2239

2240
      this._debug(
4✔
2241
        debugName,
2242
        `session has${expiresWithMargin ? '' : ' not'} expired with margin of ${EXPIRY_MARGIN_MS}s`
4✔
2243
      )
2244

2245
      if (expiresWithMargin) {
4✔
2246
        if (this.autoRefreshToken && currentSession.refresh_token) {
2✔
2247
          const { error } = await this._callRefreshToken(currentSession.refresh_token)
2✔
2248

2249
          if (error) {
2!
2250
            console.error(error)
×
2251

2252
            if (!isAuthRetryableFetchError(error)) {
×
2253
              this._debug(
×
2254
                debugName,
2255
                'refresh failed with a non-retryable error, removing the session',
2256
                error
2257
              )
2258
              await this._removeSession()
×
2259
            }
2260
          }
2261
        }
2262
      } else if (
2!
2263
        currentSession.user &&
4✔
2264
        (currentSession.user as any).__isUserNotAvailableProxy === true
2265
      ) {
2266
        // If we have a proxy user, try to get the real user data
2267
        try {
×
2268
          const { data, error: userError } = await this._getUser(currentSession.access_token)
×
2269

2270
          if (!userError && data?.user) {
×
2271
            currentSession.user = data.user
×
2272
            await this._saveSession(currentSession)
×
2273
            await this._notifyAllSubscribers('SIGNED_IN', currentSession)
×
2274
          } else {
2275
            this._debug(debugName, 'could not get user data, skipping SIGNED_IN notification')
×
2276
          }
2277
        } catch (getUserError) {
2278
          console.error('Error getting user data:', getUserError)
×
2279
          this._debug(
×
2280
            debugName,
2281
            'error getting user data, skipping SIGNED_IN notification',
2282
            getUserError
2283
          )
2284
        }
2285
      } else {
2286
        // no need to persist currentSession again, as we just loaded it from
2287
        // local storage; persisting it again may overwrite a value saved by
2288
        // another client with access to the same local storage
2289
        await this._notifyAllSubscribers('SIGNED_IN', currentSession)
2✔
2290
      }
2291
    } catch (err) {
2292
      this._debug(debugName, 'error', err)
×
2293

2294
      console.error(err)
×
2295
      return
×
2296
    } finally {
2297
      this._debug(debugName, 'end')
26✔
2298
    }
2299
  }
2300

2301
  private async _callRefreshToken(refreshToken: string): Promise<CallRefreshTokenResult> {
2302
    if (!refreshToken) {
32!
2303
      throw new AuthSessionMissingError()
×
2304
    }
2305

2306
    // refreshing is already in progress
2307
    if (this.refreshingDeferred) {
32✔
2308
      return this.refreshingDeferred.promise
6✔
2309
    }
2310

2311
    const debugName = `#_callRefreshToken(${refreshToken.substring(0, 5)}...)`
26✔
2312

2313
    this._debug(debugName, 'begin')
26✔
2314

2315
    try {
26✔
2316
      this.refreshingDeferred = new Deferred<CallRefreshTokenResult>()
26✔
2317

2318
      const { data, error } = await this._refreshAccessToken(refreshToken)
26✔
2319
      if (error) throw error
24✔
2320
      if (!data.session) throw new AuthSessionMissingError()
18✔
2321

2322
      await this._saveSession(data.session)
16✔
2323
      await this._notifyAllSubscribers('TOKEN_REFRESHED', data.session)
16✔
2324

2325
      const result = { session: data.session, error: null }
16✔
2326

2327
      this.refreshingDeferred.resolve(result)
16✔
2328

2329
      return result
16✔
2330
    } catch (error) {
2331
      this._debug(debugName, 'error', error)
10✔
2332

2333
      if (isAuthError(error)) {
10✔
2334
        const result = { session: null, error }
8✔
2335

2336
        if (!isAuthRetryableFetchError(error)) {
8✔
2337
          await this._removeSession()
8✔
2338
        }
2339

2340
        this.refreshingDeferred?.resolve(result)
8!
2341

2342
        return result
8✔
2343
      }
2344

2345
      this.refreshingDeferred?.reject(error)
2!
2346
      throw error
2✔
2347
    } finally {
2348
      this.refreshingDeferred = null
26✔
2349
      this._debug(debugName, 'end')
26✔
2350
    }
2351
  }
2352

2353
  private async _notifyAllSubscribers(
2354
    event: AuthChangeEvent,
2355
    session: Session | null,
2356
    broadcast = true
353✔
2357
  ) {
2358
    const debugName = `#_notifyAllSubscribers(${event})`
355✔
2359
    this._debug(debugName, 'begin', session, `broadcast = ${broadcast}`)
355✔
2360

2361
    try {
355✔
2362
      if (this.broadcastChannel && broadcast) {
355✔
2363
        this.broadcastChannel.postMessage({ event, session })
2✔
2364
      }
2365

2366
      const errors: any[] = []
355✔
2367
      const promises = Array.from(this.stateChangeEmitters.values()).map(async (x) => {
355✔
2368
        try {
10✔
2369
          await x.callback(event, session)
10✔
2370
        } catch (e: any) {
2371
          errors.push(e)
×
2372
        }
2373
      })
2374

2375
      await Promise.all(promises)
355✔
2376

2377
      if (errors.length > 0) {
355!
2378
        for (let i = 0; i < errors.length; i += 1) {
×
2379
          console.error(errors[i])
×
2380
        }
2381

2382
        throw errors[0]
×
2383
      }
2384
    } finally {
2385
      this._debug(debugName, 'end')
355✔
2386
    }
2387
  }
2388

2389
  /**
2390
   * set currentSession and currentUser
2391
   * process to _startAutoRefreshToken if possible
2392
   */
2393
  private async _saveSession(session: Session) {
2394
    this._debug('#_saveSession()', session)
171✔
2395
    // _saveSession is always called whenever a new session has been acquired
2396
    // so we can safely suppress the warning returned by future getSession calls
2397
    this.suppressGetSessionWarning = true
171✔
2398

2399
    // Create a shallow copy to work with, to avoid mutating the original session object if it's used elsewhere
2400
    const sessionToProcess = { ...session }
171✔
2401

2402
    const userIsProxy =
2403
      sessionToProcess.user && (sessionToProcess.user as any).__isUserNotAvailableProxy === true
171✔
2404
    if (this.userStorage) {
171!
2405
      if (!userIsProxy && sessionToProcess.user) {
×
2406
        // If it's a real user object, save it to userStorage.
2407
        await setItemAsync(this.userStorage, this.storageKey + '-user', {
×
2408
          user: sessionToProcess.user,
2409
        })
2410
      } else if (userIsProxy) {
×
2411
        // If it's the proxy, it means user was not found in userStorage.
2412
        // We should ensure no stale user data for this key exists in userStorage if we were to save null,
2413
        // or simply not save the proxy. For now, we don't save the proxy here.
2414
        // If there's a need to clear userStorage if user becomes proxy, that logic would go here.
2415
      }
2416

2417
      // Prepare the main session data for primary storage: remove the user property before cloning
2418
      // This is important because the original session.user might be the proxy
2419
      const mainSessionData: Omit<Session, 'user'> & { user?: User } = { ...sessionToProcess }
×
2420
      delete mainSessionData.user // Remove user (real or proxy) before cloning for main storage
×
2421

2422
      const clonedMainSessionData = structuredClone(mainSessionData)
×
2423
      await setItemAsync(this.storage, this.storageKey, clonedMainSessionData)
×
2424
    } else {
2425
      // No userStorage is configured.
2426
      // In this case, session.user should ideally not be a proxy.
2427
      // If it were, structuredClone would fail. This implies an issue elsewhere if user is a proxy here
2428
      const clonedSession = structuredClone(sessionToProcess) // sessionToProcess still has its original user property
171✔
2429
      await setItemAsync(this.storage, this.storageKey, clonedSession)
171✔
2430
    }
2431
  }
2432

2433
  private async _removeSession() {
2434
    this._debug('#_removeSession()')
190✔
2435

2436
    await removeItemAsync(this.storage, this.storageKey)
190✔
2437
    await removeItemAsync(this.storage, this.storageKey + '-code-verifier')
188✔
2438
    await removeItemAsync(this.storage, this.storageKey + '-user')
188✔
2439

2440
    if (this.userStorage) {
188!
2441
      await removeItemAsync(this.userStorage, this.storageKey + '-user')
×
2442
    }
2443

2444
    await this._notifyAllSubscribers('SIGNED_OUT', null)
188✔
2445
  }
2446

2447
  /**
2448
   * Removes any registered visibilitychange callback.
2449
   *
2450
   * {@see #startAutoRefresh}
2451
   * {@see #stopAutoRefresh}
2452
   */
2453
  private _removeVisibilityChangedCallback() {
2454
    this._debug('#_removeVisibilityChangedCallback()')
18✔
2455

2456
    const callback = this.visibilityChangedCallback
18✔
2457
    this.visibilityChangedCallback = null
18✔
2458

2459
    try {
18✔
2460
      if (callback && isBrowser() && window?.removeEventListener) {
18!
2461
        window.removeEventListener('visibilitychange', callback)
2✔
2462
      }
2463
    } catch (e) {
2464
      console.error('removing visibilitychange callback failed', e)
×
2465
    }
2466
  }
2467

2468
  /**
2469
   * This is the private implementation of {@link #startAutoRefresh}. Use this
2470
   * within the library.
2471
   */
2472
  private async _startAutoRefresh() {
2473
    await this._stopAutoRefresh()
16✔
2474

2475
    this._debug('#_startAutoRefresh()')
16✔
2476

2477
    const ticker = setInterval(() => this._autoRefreshTokenTick(), AUTO_REFRESH_TICK_DURATION_MS)
16✔
2478
    this.autoRefreshTicker = ticker
16✔
2479

2480
    if (ticker && typeof ticker === 'object' && typeof ticker.unref === 'function') {
16✔
2481
      // ticker is a NodeJS Timeout object that has an `unref` method
2482
      // https://nodejs.org/api/timers.html#timeoutunref
2483
      // When auto refresh is used in NodeJS (like for testing) the
2484
      // `setInterval` is preventing the process from being marked as
2485
      // finished and tests run endlessly. This can be prevented by calling
2486
      // `unref()` on the returned object.
2487
      ticker.unref()
12✔
2488
      // @ts-expect-error TS has no context of Deno
2489
    } else if (typeof Deno !== 'undefined' && typeof Deno.unrefTimer === 'function') {
4!
2490
      // similar like for NodeJS, but with the Deno API
2491
      // https://deno.land/api@latest?unstable&s=Deno.unrefTimer
2492
      // @ts-expect-error TS has no context of Deno
2493
      Deno.unrefTimer(ticker)
×
2494
    }
2495

2496
    // run the tick immediately, but in the next pass of the event loop so that
2497
    // #_initialize can be allowed to complete without recursively waiting on
2498
    // itself
2499
    setTimeout(async () => {
16✔
2500
      await this.initializePromise
14✔
2501
      await this._autoRefreshTokenTick()
14✔
2502
    }, 0)
2503
  }
2504

2505
  /**
2506
   * This is the private implementation of {@link #stopAutoRefresh}. Use this
2507
   * within the library.
2508
   */
2509
  private async _stopAutoRefresh() {
2510
    this._debug('#_stopAutoRefresh()')
20✔
2511

2512
    const ticker = this.autoRefreshTicker
20✔
2513
    this.autoRefreshTicker = null
20✔
2514

2515
    if (ticker) {
20✔
2516
      clearInterval(ticker)
6✔
2517
    }
2518
  }
2519

2520
  /**
2521
   * Starts an auto-refresh process in the background. The session is checked
2522
   * every few seconds. Close to the time of expiration a process is started to
2523
   * refresh the session. If refreshing fails it will be retried for as long as
2524
   * necessary.
2525
   *
2526
   * If you set the {@link GoTrueClientOptions#autoRefreshToken} you don't need
2527
   * to call this function, it will be called for you.
2528
   *
2529
   * On browsers the refresh process works only when the tab/window is in the
2530
   * foreground to conserve resources as well as prevent race conditions and
2531
   * flooding auth with requests. If you call this method any managed
2532
   * visibility change callback will be removed and you must manage visibility
2533
   * changes on your own.
2534
   *
2535
   * On non-browser platforms the refresh process works *continuously* in the
2536
   * background, which may not be desirable. You should hook into your
2537
   * platform's foreground indication mechanism and call these methods
2538
   * appropriately to conserve resources.
2539
   *
2540
   * {@see #stopAutoRefresh}
2541
   */
2542
  async startAutoRefresh() {
2543
    this._removeVisibilityChangedCallback()
14✔
2544
    await this._startAutoRefresh()
14✔
2545
  }
2546

2547
  /**
2548
   * Stops an active auto refresh process running in the background (if any).
2549
   *
2550
   * If you call this method any managed visibility change callback will be
2551
   * removed and you must manage visibility changes on your own.
2552
   *
2553
   * See {@link #startAutoRefresh} for more details.
2554
   */
2555
  async stopAutoRefresh() {
2556
    this._removeVisibilityChangedCallback()
4✔
2557
    await this._stopAutoRefresh()
4✔
2558
  }
2559

2560
  /**
2561
   * Runs the auto refresh token tick.
2562
   */
2563
  private async _autoRefreshTokenTick() {
2564
    this._debug('#_autoRefreshTokenTick()', 'begin')
14✔
2565

2566
    try {
14✔
2567
      await this._acquireLock(0, async () => {
14✔
2568
        try {
14✔
2569
          const now = Date.now()
14✔
2570

2571
          try {
14✔
2572
            return await this._useSession(async (result) => {
14✔
2573
              const {
2574
                data: { session },
2575
              } = result
14✔
2576

2577
              if (!session || !session.refresh_token || !session.expires_at) {
14✔
2578
                this._debug('#_autoRefreshTokenTick()', 'no session')
10✔
2579
                return
10✔
2580
              }
2581

2582
              // session will expire in this many ticks (or has already expired if <= 0)
2583
              const expiresInTicks = Math.floor(
4✔
2584
                (session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS
2585
              )
2586

2587
              this._debug(
4✔
2588
                '#_autoRefreshTokenTick()',
2589
                `access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks`
2590
              )
2591

2592
              if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) {
4!
2593
                await this._callRefreshToken(session.refresh_token)
×
2594
              }
2595
            })
2596
          } catch (e: any) {
2597
            console.error(
×
2598
              'Auto refresh tick failed with error. This is likely a transient error.',
2599
              e
2600
            )
2601
          }
2602
        } finally {
2603
          this._debug('#_autoRefreshTokenTick()', 'end')
14✔
2604
        }
2605
      })
2606
    } catch (e: any) {
2607
      if (e.isAcquireTimeout || e instanceof LockAcquireTimeoutError) {
×
2608
        this._debug('auto refresh token tick lock not available')
×
2609
      } else {
2610
        throw e
×
2611
      }
2612
    }
2613
  }
2614

2615
  /**
2616
   * Registers callbacks on the browser / platform, which in-turn run
2617
   * algorithms when the browser window/tab are in foreground. On non-browser
2618
   * platforms it assumes always foreground.
2619
   */
2620
  private async _handleVisibilityChange() {
2621
    this._debug('#_handleVisibilityChange()')
118✔
2622

2623
    if (!isBrowser() || !window?.addEventListener) {
118!
2624
      if (this.autoRefreshToken) {
92✔
2625
        // in non-browser environments the refresh token ticker runs always
2626
        this.startAutoRefresh()
12✔
2627
      }
2628

2629
      return false
92✔
2630
    }
2631

2632
    try {
26✔
2633
      this.visibilityChangedCallback = async () => await this._onVisibilityChanged(false)
26✔
2634

2635
      window?.addEventListener('visibilitychange', this.visibilityChangedCallback)
26!
2636

2637
      // now immediately call the visbility changed callback to setup with the
2638
      // current visbility state
2639
      await this._onVisibilityChanged(true) // initial call
26✔
2640
    } catch (error) {
2641
      console.error('_handleVisibilityChange', error)
×
2642
    }
2643
  }
2644

2645
  /**
2646
   * Callback registered with `window.addEventListener('visibilitychange')`.
2647
   */
2648
  private async _onVisibilityChanged(calledFromInitialize: boolean) {
2649
    const methodName = `#_onVisibilityChanged(${calledFromInitialize})`
26✔
2650
    this._debug(methodName, 'visibilityState', document.visibilityState)
26✔
2651

2652
    if (document.visibilityState === 'visible') {
26!
2653
      if (this.autoRefreshToken) {
26✔
2654
        // in browser environments the refresh token ticker runs only on focused tabs
2655
        // which prevents race conditions
2656
        this._startAutoRefresh()
2✔
2657
      }
2658

2659
      if (!calledFromInitialize) {
26!
2660
        // called when the visibility has changed, i.e. the browser
2661
        // transitioned from hidden -> visible so we need to see if the session
2662
        // should be recovered immediately... but to do that we need to acquire
2663
        // the lock first asynchronously
2664
        await this.initializePromise
×
2665

2666
        await this._acquireLock(-1, async () => {
×
2667
          if (document.visibilityState !== 'visible') {
×
2668
            this._debug(
×
2669
              methodName,
2670
              'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting'
2671
            )
2672

2673
            // visibility has changed while waiting for the lock, abort
2674
            return
×
2675
          }
2676

2677
          // recover the session
2678
          await this._recoverAndRefresh()
×
2679
        })
2680
      }
2681
    } else if (document.visibilityState === 'hidden') {
×
2682
      if (this.autoRefreshToken) {
×
2683
        this._stopAutoRefresh()
×
2684
      }
2685
    }
2686
  }
2687

2688
  /**
2689
   * Generates the relevant login URL for a third-party provider.
2690
   * @param options.redirectTo A URL or mobile address to send the user to after they are confirmed.
2691
   * @param options.scopes A space-separated list of scopes granted to the OAuth application.
2692
   * @param options.queryParams An object of key-value pairs containing query parameters granted to the OAuth application.
2693
   */
2694
  private async _getUrlForProvider(
2695
    url: string,
2696
    provider: Provider,
2697
    options: {
2698
      redirectTo?: string
2699
      scopes?: string
2700
      queryParams?: { [key: string]: string }
2701
      skipBrowserRedirect?: boolean
2702
    }
2703
  ) {
2704
    const urlParams: string[] = [`provider=${encodeURIComponent(provider)}`]
16✔
2705
    if (options?.redirectTo) {
16!
2706
      urlParams.push(`redirect_to=${encodeURIComponent(options.redirectTo)}`)
12✔
2707
    }
2708
    if (options?.scopes) {
16!
2709
      urlParams.push(`scopes=${encodeURIComponent(options.scopes)}`)
6✔
2710
    }
2711
    if (this.flowType === 'pkce') {
16✔
2712
      const [codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
4✔
2713
        this.storage,
2714
        this.storageKey
2715
      )
2716

2717
      const flowParams = new URLSearchParams({
4✔
2718
        code_challenge: `${encodeURIComponent(codeChallenge)}`,
2719
        code_challenge_method: `${encodeURIComponent(codeChallengeMethod)}`,
2720
      })
2721
      urlParams.push(flowParams.toString())
4✔
2722
    }
2723
    if (options?.queryParams) {
16!
2724
      const query = new URLSearchParams(options.queryParams)
2✔
2725
      urlParams.push(query.toString())
2✔
2726
    }
2727
    if (options?.skipBrowserRedirect) {
16!
2728
      urlParams.push(`skip_http_redirect=${options.skipBrowserRedirect}`)
2✔
2729
    }
2730

2731
    return `${url}?${urlParams.join('&')}`
16✔
2732
  }
2733

2734
  private async _unenroll(params: MFAUnenrollParams): Promise<AuthMFAUnenrollResponse> {
2735
    try {
2✔
2736
      return await this._useSession(async (result) => {
2✔
2737
        const { data: sessionData, error: sessionError } = result
2✔
2738
        if (sessionError) {
2!
2739
          return { data: null, error: sessionError }
×
2740
        }
2741

2742
        return await _request(this.fetch, 'DELETE', `${this.url}/factors/${params.factorId}`, {
2✔
2743
          headers: this.headers,
2744
          jwt: sessionData?.session?.access_token,
12!
2745
        })
2746
      })
2747
    } catch (error) {
2748
      if (isAuthError(error)) {
×
2749
        return { data: null, error }
×
2750
      }
2751
      throw error
×
2752
    }
2753
  }
2754

2755
  /**
2756
   * {@see GoTrueMFAApi#enroll}
2757
   */
2758
  private async _enroll(params: MFAEnrollTOTPParams): Promise<AuthMFAEnrollTOTPResponse>
2759
  private async _enroll(params: MFAEnrollPhoneParams): Promise<AuthMFAEnrollPhoneResponse>
2760
  private async _enroll(params: MFAEnrollParams): Promise<AuthMFAEnrollResponse> {
2761
    try {
16✔
2762
      return await this._useSession(async (result) => {
16✔
2763
        const { data: sessionData, error: sessionError } = result
16✔
2764
        if (sessionError) {
16!
2765
          return { data: null, error: sessionError }
×
2766
        }
2767

2768
        const body = {
16✔
2769
          friendly_name: params.friendlyName,
2770
          factor_type: params.factorType,
2771
          ...(params.factorType === 'phone' ? { phone: params.phone } : { issuer: params.issuer }),
16!
2772
        }
2773

2774
        const { data, error } = await _request(this.fetch, 'POST', `${this.url}/factors`, {
16✔
2775
          body,
2776
          headers: this.headers,
2777
          jwt: sessionData?.session?.access_token,
94!
2778
        })
2779

2780
        if (error) {
14!
2781
          return { data: null, error }
×
2782
        }
2783

2784
        if (params.factorType === 'totp' && data?.totp?.qr_code) {
14!
2785
          data.totp.qr_code = `data:image/svg+xml;utf-8,${data.totp.qr_code}`
14✔
2786
        }
2787

2788
        return { data, error: null }
14✔
2789
      })
2790
    } catch (error) {
2791
      if (isAuthError(error)) {
2✔
2792
        return { data: null, error }
2✔
2793
      }
2794
      throw error
×
2795
    }
2796
  }
2797

2798
  /**
2799
   * {@see GoTrueMFAApi#verify}
2800
   */
2801
  private async _verify(params: MFAVerifyParams): Promise<AuthMFAVerifyResponse> {
2802
    return this._acquireLock(-1, async () => {
4✔
2803
      try {
4✔
2804
        return await this._useSession(async (result) => {
4✔
2805
          const { data: sessionData, error: sessionError } = result
4✔
2806
          if (sessionError) {
4!
2807
            return { data: null, error: sessionError }
×
2808
          }
2809

2810
          const { data, error } = await _request(
4✔
2811
            this.fetch,
2812
            'POST',
2813
            `${this.url}/factors/${params.factorId}/verify`,
2814
            {
2815
              body: { code: params.code, challenge_id: params.challengeId },
2816
              headers: this.headers,
2817
              jwt: sessionData?.session?.access_token,
24!
2818
            }
2819
          )
2820
          if (error) {
×
2821
            return { data: null, error }
×
2822
          }
2823

2824
          await this._saveSession({
×
2825
            expires_at: Math.round(Date.now() / 1000) + data.expires_in,
2826
            ...data,
2827
          })
2828
          await this._notifyAllSubscribers('MFA_CHALLENGE_VERIFIED', data)
×
2829

2830
          return { data, error }
×
2831
        })
2832
      } catch (error) {
2833
        if (isAuthError(error)) {
4✔
2834
          return { data: null, error }
4✔
2835
        }
2836
        throw error
×
2837
      }
2838
    })
2839
  }
2840

2841
  /**
2842
   * {@see GoTrueMFAApi#challenge}
2843
   */
2844
  private async _challenge(params: MFAChallengeParams): Promise<AuthMFAChallengeResponse> {
2845
    return this._acquireLock(-1, async () => {
6✔
2846
      try {
6✔
2847
        return await this._useSession(async (result) => {
6✔
2848
          const { data: sessionData, error: sessionError } = result
6✔
2849
          if (sessionError) {
6!
2850
            return { data: null, error: sessionError }
×
2851
          }
2852

2853
          return await _request(
6✔
2854
            this.fetch,
2855
            'POST',
2856
            `${this.url}/factors/${params.factorId}/challenge`,
2857
            {
2858
              body: { channel: params.channel },
2859
              headers: this.headers,
2860
              jwt: sessionData?.session?.access_token,
36!
2861
            }
2862
          )
2863
        })
2864
      } catch (error) {
2865
        if (isAuthError(error)) {
×
2866
          return { data: null, error }
×
2867
        }
2868
        throw error
×
2869
      }
2870
    })
2871
  }
2872

2873
  /**
2874
   * {@see GoTrueMFAApi#challengeAndVerify}
2875
   */
2876
  private async _challengeAndVerify(
2877
    params: MFAChallengeAndVerifyParams
2878
  ): Promise<AuthMFAVerifyResponse> {
2879
    // both _challenge and _verify independently acquire the lock, so no need
2880
    // to acquire it here
2881

2882
    const { data: challengeData, error: challengeError } = await this._challenge({
2✔
2883
      factorId: params.factorId,
2884
    })
2885
    if (challengeError) {
2!
2886
      return { data: null, error: challengeError }
×
2887
    }
2888

2889
    return await this._verify({
2✔
2890
      factorId: params.factorId,
2891
      challengeId: challengeData.id,
2892
      code: params.code,
2893
    })
2894
  }
2895

2896
  /**
2897
   * {@see GoTrueMFAApi#listFactors}
2898
   */
2899
  private async _listFactors(): Promise<AuthMFAListFactorsResponse> {
2900
    // use #getUser instead of #_getUser as the former acquires a lock
2901
    const {
2902
      data: { user },
2903
      error: userError,
2904
    } = await this.getUser()
4✔
2905
    if (userError) {
4!
2906
      return { data: null, error: userError }
×
2907
    }
2908

2909
    const factors = user?.factors || []
4!
2910
    const totp = factors.filter(
4✔
2911
      (factor) => factor.factor_type === 'totp' && factor.status === 'verified'
8✔
2912
    )
2913
    const phone = factors.filter(
4✔
2914
      (factor) => factor.factor_type === 'phone' && factor.status === 'verified'
8✔
2915
    )
2916

2917
    return {
4✔
2918
      data: {
2919
        all: factors,
2920
        totp,
2921
        phone,
2922
      },
2923
      error: null,
2924
    }
2925
  }
2926

2927
  /**
2928
   * {@see GoTrueMFAApi#getAuthenticatorAssuranceLevel}
2929
   */
2930
  private async _getAuthenticatorAssuranceLevel(): Promise<AuthMFAGetAuthenticatorAssuranceLevelResponse> {
2931
    return this._acquireLock(-1, async () => {
2✔
2932
      return await this._useSession(async (result) => {
2✔
2933
        const {
2934
          data: { session },
2935
          error: sessionError,
2936
        } = result
2✔
2937
        if (sessionError) {
2!
2938
          return { data: null, error: sessionError }
×
2939
        }
2940
        if (!session) {
2!
2941
          return {
×
2942
            data: { currentLevel: null, nextLevel: null, currentAuthenticationMethods: [] },
2943
            error: null,
2944
          }
2945
        }
2946

2947
        const { payload } = decodeJWT(session.access_token)
2✔
2948

2949
        let currentLevel: AuthenticatorAssuranceLevels | null = null
2✔
2950

2951
        if (payload.aal) {
2✔
2952
          currentLevel = payload.aal
2✔
2953
        }
2954

2955
        let nextLevel: AuthenticatorAssuranceLevels | null = currentLevel
2✔
2956

2957
        const verifiedFactors =
2958
          session.user.factors?.filter((factor: Factor) => factor.status === 'verified') ?? []
2!
2959

2960
        if (verifiedFactors.length > 0) {
2!
2961
          nextLevel = 'aal2'
×
2962
        }
2963

2964
        const currentAuthenticationMethods = payload.amr || []
2!
2965

2966
        return { data: { currentLevel, nextLevel, currentAuthenticationMethods }, error: null }
2✔
2967
      })
2968
    })
2969
  }
2970

2971
  private async fetchJwk(kid: string, jwks: { keys: JWK[] } = { keys: [] }): Promise<JWK | null> {
10✔
2972
    // try fetching from the supplied jwks
2973
    let jwk = jwks.keys.find((key) => key.kid === kid)
10✔
2974
    if (jwk) {
10!
2975
      return jwk
×
2976
    }
2977

2978
    const now = Date.now()
10✔
2979

2980
    // try fetching from cache
2981
    jwk = this.jwks.keys.find((key) => key.kid === kid)
10✔
2982

2983
    // jwk exists and jwks isn't stale
2984
    if (jwk && this.jwks_cached_at + JWKS_TTL > now) {
10✔
2985
      return jwk
3✔
2986
    }
2987
    // jwk isn't cached in memory so we need to fetch it from the well-known endpoint
2988
    const { data, error } = await _request(this.fetch, 'GET', `${this.url}/.well-known/jwks.json`, {
7✔
2989
      headers: this.headers,
2990
    })
2991
    if (error) {
7!
2992
      throw error
×
2993
    }
2994
    if (!data.keys || data.keys.length === 0) {
7!
NEW
2995
      return null
×
2996
    }
2997

2998
    this.jwks = data
7✔
2999
    this.jwks_cached_at = now
7✔
3000

3001
    // Find the signing key
3002
    jwk = data.keys.find((key: any) => key.kid === kid)
7✔
3003
    if (!jwk) {
7!
NEW
3004
      return null
×
3005
    }
3006
    return jwk
7✔
3007
  }
3008

3009
  /**
3010
   * Extracts the JWT claims present in the access token by first verifying the
3011
   * JWT against the server's JSON Web Key Set endpoint
3012
   * `/.well-known/jwks.json` which is often cached, resulting in significantly
3013
   * faster responses. Prefer this method over {@link #getUser} which always
3014
   * sends a request to the Auth server for each JWT.
3015
   *
3016
   * If the project is not using an asymmetric JWT signing key (like ECC or
3017
   * RSA) it always sends a request to the Auth server (similar to {@link
3018
   * #getUser}) to verify the JWT.
3019
   *
3020
   * @param jwt An optional specific JWT you wish to verify, not the one you
3021
   *            can obtain from {@link #getSession}.
3022
   * @param options Various additional options that allow you to customize the
3023
   *                behavior of this method.
3024
   */
3025
  async getClaims(
3026
    jwt?: string,
3027
    options: {
15✔
3028
      /**
3029
       * @deprecated Please use options.jwks instead.
3030
       */
3031
      keys?: JWK[]
3032

3033
      /** If set to `true` the `exp` claim will not be validated against the current time. */
3034
      allowExpired?: boolean
3035

3036
      /** If set, this JSON Web Key Set is going to have precedence over the cached value available on the server. */
3037
      jwks?: { keys: JWK[] }
3038
    } = {}
3039
  ): Promise<
3040
    | {
3041
        data: { claims: JwtPayload; header: JwtHeader; signature: Uint8Array }
3042
        error: null
3043
      }
3044
    | { data: null; error: AuthError }
3045
    | { data: null; error: null }
3046
  > {
3047
    try {
15✔
3048
      let token = jwt
15✔
3049
      if (!token) {
15✔
3050
        const { data, error } = await this.getSession()
15✔
3051
        if (error || !data.session) {
15✔
3052
          return { data: null, error }
2✔
3053
        }
3054
        token = data.session.access_token
13✔
3055
      }
3056

3057
      const {
3058
        header,
3059
        payload,
3060
        signature,
3061
        raw: { header: rawHeader, payload: rawPayload },
3062
      } = decodeJWT(token)
13✔
3063

3064
      if (!options?.allowExpired) {
11!
3065
        // Reject expired JWTs should only happen if jwt argument was passed
3066
        validateExp(payload.exp)
11✔
3067
      }
3068

3069
      const signingKey =
3070
        !header.alg ||
9✔
3071
        header.alg.startsWith('HS') ||
3072
        !header.kid ||
3073
        !('crypto' in globalThis && 'subtle' in globalThis.crypto)
5✔
3074
          ? null
3075
          : await this.fetchJwk(header.kid, options?.keys ? { keys: options.keys } : options?.jwks)
14!
3076

3077
      // If symmetric algorithm or WebCrypto API is unavailable, fallback to getUser()
3078
      if (!signingKey) {
9✔
3079
        const { error } = await this.getUser(token)
7✔
3080
        if (error) {
7✔
3081
          throw error
2✔
3082
        }
3083
        // getUser succeeds so the claims in the JWT can be trusted
3084
        return {
5✔
3085
          data: {
3086
            claims: payload,
3087
            header,
3088
            signature,
3089
          },
3090
          error: null,
3091
        }
3092
      }
3093

3094
      const algorithm = getAlgorithm(header.alg)
2✔
3095

3096
      // Convert JWK to CryptoKey
3097
      const publicKey = await crypto.subtle.importKey('jwk', signingKey, algorithm, true, [
2✔
3098
        'verify',
3099
      ])
3100

3101
      // Verify the signature
3102
      const isValid = await crypto.subtle.verify(
2✔
3103
        algorithm,
3104
        publicKey,
3105
        signature,
3106
        stringToUint8Array(`${rawHeader}.${rawPayload}`)
3107
      )
3108

3109
      if (!isValid) {
2✔
3110
        throw new AuthInvalidJwtError('Invalid JWT signature')
1✔
3111
      }
3112

3113
      // If verification succeeds, decode and return claims
3114
      return {
1✔
3115
        data: {
3116
          claims: payload,
3117
          header,
3118
          signature,
3119
        },
3120
        error: null,
3121
      }
3122
    } catch (error) {
3123
      if (isAuthError(error)) {
7✔
3124
        return { data: null, error }
5✔
3125
      }
3126
      throw error
2✔
3127
    }
3128
  }
3129
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc