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

supabase / auth-js / 16344332478

17 Jul 2025 11:50AM UTC coverage: 74.803%. Remained the same
16344332478

push

github

web-flow
chore(master): release 2.71.1 (#1085)

:robot: I have created a release *beep* *boop*
---


##
[2.71.1](https://github.com/supabase/auth-js/compare/v2.71.0...v2.71.1)
(2025-07-17)


### Bug Fixes

* use JSON-based deep clone instead of structuredClone
([#1084](https://github.com/supabase/auth-js/issues/1084))
([9a6edb9](https://github.com/supabase/auth-js/commit/9a6edb9d0))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

860 of 1304 branches covered (65.95%)

Branch coverage included in aggregate %.

1319 of 1609 relevant lines covered (81.98%)

165.49 hits per line

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

68.27
/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
import { deepClone } from './lib/helpers'
10✔
116

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

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

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

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

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

151
  private instanceID: number
152

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

167
  protected flowType: AuthFlowType
168

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

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

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

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

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

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

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

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

234
    if (this.instanceID > 0 && isBrowser()) {
118✔
235
      console.warn(
24✔
236
        '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.'
237
      )
238
    }
239

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

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

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

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

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

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

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

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

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

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

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

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

324
    this.initialize()
118✔
325
  }
326

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

335
    return this
5,281✔
336
  }
337

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

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

354
    return await this.initializePromise
118✔
355
  }
356

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

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

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

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

399
          return { error }
4✔
400
        }
401

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

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

412
        await this._saveSession(session)
2✔
413

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

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

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

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

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

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

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

475
      throw error
×
476
    }
477
  }
478

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

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

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

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

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

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

554
      throw error
×
555
    }
556
  }
557

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

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

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

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

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

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

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

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

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

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

679
      let resolvedWallet: SolanaWallet
680

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

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

694
        if (
×
695
          'solana' in windowAny &&
×
696
          typeof windowAny.solana === 'object' &&
697
          (('signIn' in windowAny.solana && typeof windowAny.solana.signIn === 'function') ||
698
            ('signMessage' in windowAny.solana &&
699
              typeof windowAny.solana.signMessage === 'function'))
700
        ) {
701
          resolvedWallet = windowAny.solana
×
702
        } else {
703
          throw new Error(
×
704
            `@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.`
705
          )
706
        }
707
      }
708

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

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

715
          ...options?.signInWithSolana,
×
716

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

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

725
        let outputToProcess: any
726

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

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

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

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

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

811
        signature = maybeSignature
×
812
      }
813
    }
814

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

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

853
      throw error
×
854
    }
855
  }
856

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

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

901
      throw error
×
902
    }
903
  }
904

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

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

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

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

1010
      throw error
×
1011
    }
1012
  }
1013

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

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

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

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

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

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

1060
      throw error
×
1061
    }
1062
  }
1063

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

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

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

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

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

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

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

1189
  /**
1190
   * Returns the session, refreshing it if necessary.
1191
   *
1192
   * 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.
1193
   *
1194
   * **IMPORTANT:** This method loads values directly from the storage attached
1195
   * to the client. If that storage is based on request cookies for example,
1196
   * the values in it may not be authentic and therefore it's strongly advised
1197
   * against using this method and its results in such circumstances. A warning
1198
   * will be emitted if this is detected. Use {@link #getUser()} instead.
1199
   */
1200
  async getSession() {
1201
    await this.initializePromise
69✔
1202

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

1209
    return result
65✔
1210
  }
1211

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

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

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

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

1239
        return result
2✔
1240
      }
1241

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

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

1248
          const result = fn()
435✔
1249

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

1260
          await result
435✔
1261

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

1266
            await Promise.all(waitOn)
427✔
1267

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1405
        if (this.storage.isServer && currentSession.user) {
149✔
1406
          let suppressWarning = this.suppressGetSessionWarning
14✔
1407
          const proxySession: Session = new Proxy(currentSession, {
14✔
1408
            get: (target: any, prop: string, receiver: any) => {
1409
              if (!suppressWarning && prop === 'user') {
24✔
1410
                // only show warning when the user object is being accessed from the server
1411
                console.warn(
2✔
1412
                  '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.'
1413
                )
1414
                suppressWarning = true // keeps this proxy instance from logging additional warnings
2✔
1415
                this.suppressGetSessionWarning = true // keeps this client's future proxy instances from warning
2✔
1416
              }
1417
              return Reflect.get(target, prop, receiver)
24✔
1418
            },
1419
          })
1420
          currentSession = proxySession
14✔
1421
        }
1422

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

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

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

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

1449
    await this.initializePromise
14✔
1450

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

1455
    return result
14✔
1456
  }
1457

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

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

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

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

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

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

1498
      throw error
×
1499
    }
1500
  }
1501

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

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

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

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

1565
      throw error
×
1566
    }
1567
  }
1568

1569
  /**
1570
   * 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.
1571
   * If the refresh token or access token in the current session is invalid, an error will be thrown.
1572
   * @param currentSession The current session that minimally contains an access token and refresh token.
1573
   */
1574
  async setSession(currentSession: {
1575
    access_token: string
1576
    refresh_token: string
1577
  }): Promise<AuthResponse> {
1578
    await this.initializePromise
10✔
1579

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

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

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

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

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

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

1639
      throw error
×
1640
    }
1641
  }
1642

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

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

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

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

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

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

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

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

1691
      throw error
×
1692
    }
1693
  }
1694

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1825
      throw error
×
1826
    }
1827
  }
1828

1829
  /**
1830
   * 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)
1831
   */
1832
  private _isImplicitGrantCallback(params: { [parameter: string]: string }): boolean {
1833
    return Boolean(params.access_token || params.error_description)
28✔
1834
  }
1835

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

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

1848
  /**
1849
   * 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.
1850
   *
1851
   * 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)`.
1852
   * 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.
1853
   *
1854
   * If using `others` scope, no `SIGNED_OUT` event is fired!
1855
   */
1856
  async signOut(options: SignOut = { scope: 'global' }): Promise<{ error: AuthError | null }> {
170✔
1857
    await this.initializePromise
172✔
1858

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

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

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

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

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

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

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

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

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

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

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

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

1995
      throw error
×
1996
    }
1997
  }
1998

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

2059
  /**
2060
   * 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.
2061
   */
2062
  async unlinkIdentity(identity: UserIdentity): Promise<
2063
    | {
2064
        data: {}
2065
        error: null
2066
      }
2067
    | { data: null; error: AuthError }
2068
  > {
2069
    try {
2✔
2070
      return await this._useSession(async (result) => {
2✔
2071
        const { data, error } = result
2✔
2072
        if (error) {
2!
2073
          throw error
×
2074
        }
2075
        return await _request(
2✔
2076
          this.fetch,
2077
          'DELETE',
2078
          `${this.url}/user/identities/${identity.identity_id}`,
2079
          {
2080
            headers: this.headers,
2081
            jwt: data.session?.access_token ?? undefined,
12!
2082
          }
2083
        )
2084
      })
2085
    } catch (error) {
2086
      if (isAuthError(error)) {
2✔
2087
        return { data: null, error }
2✔
2088
      }
2089
      throw error
×
2090
    }
2091
  }
2092

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

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

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

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

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

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

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

2149
    return isValidSession
191✔
2150
  }
2151

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2235
        return
22✔
2236
      }
2237

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2343
        return result
8✔
2344
      }
2345

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

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

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

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

2376
      await Promise.all(promises)
355✔
2377

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2630
      return false
92✔
2631
    }
2632

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

© 2026 Coveralls, Inc