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

supabase / auth-js / 17979196376

24 Sep 2025 02:03PM UTC coverage: 79.786% (-1.0%) from 80.816%
17979196376

push

github

web-flow
feat: implement `linkIdentity` for oidc / native sign-in (#1096)

Adds `linkIdentity()` method which allows passing OIDC credentials. The
ID token will be linked to the currently signed in user.

See also:
- https://github.com/supabase/auth/pull/2108

1080 of 1464 branches covered (73.77%)

Branch coverage included in aggregate %.

2 of 21 new or added lines in 1 file covered. (9.52%)

1458 of 1717 relevant lines covered (84.92%)

170.21 hits per line

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

74.75
/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
  deepClone,
36
  Deferred,
37
  getItemAsync,
38
  isBrowser,
39
  removeItemAsync,
40
  resolveFetch,
41
  setItemAsync,
42
  uuid,
43
  retryable,
44
  sleep,
45
  parseParametersFromURL,
46
  getCodeChallengeAndMethod,
47
  getAlgorithm,
48
  validateExp,
49
  decodeJWT,
50
  userNotAvailableProxy,
51
  supportsLocalStorage,
52
} from './lib/helpers'
53
import { memoryLocalStorageAdapter } from './lib/local-storage'
10✔
54
import { polyfillGlobalThis } from './lib/polyfills'
10✔
55
import { version } from './lib/version'
10✔
56
import { LockAcquireTimeoutError, navigatorLock } from './lib/locks'
10✔
57

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

129
polyfillGlobalThis() // Make "globalThis" available
10✔
130

131
const DEFAULT_OPTIONS: Omit<
132
  Required<GoTrueClientOptions>,
133
  'fetch' | 'storage' | 'userStorage' | 'lock'
134
> = {
10✔
135
  url: GOTRUE_URL,
136
  storageKey: STORAGE_KEY,
137
  autoRefreshToken: true,
138
  persistSession: true,
139
  detectSessionInUrl: true,
140
  headers: DEFAULT_HEADERS,
141
  flowType: 'implicit',
142
  debug: false,
143
  hasCustomAuthorizationHeader: false,
144
}
145

146
async function lockNoOp<R>(name: string, acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
147
  return await fn()
547✔
148
}
149

150
/**
151
 * Caches JWKS values for all clients created in the same environment. This is
152
 * especially useful for shared-memory execution environments such as Vercel's
153
 * Fluid Compute, AWS Lambda or Supabase's Edge Functions. Regardless of how
154
 * many clients are created, if they share the same storage key they will use
155
 * the same JWKS cache, significantly speeding up getClaims() with asymmetric
156
 * JWTs.
157
 */
158
const GLOBAL_JWKS: { [storageKey: string]: { cachedAt: number; jwks: { keys: JWK[] } } } = {}
10✔
159

160
export default class GoTrueClient {
10✔
161
  private static nextInstanceID = 0
10✔
162

163
  private instanceID: number
164

165
  /**
166
   * Namespace for the GoTrue admin methods.
167
   * These methods should only be used in a trusted server-side environment.
168
   */
169
  admin: GoTrueAdminApi
170
  /**
171
   * Namespace for the MFA methods.
172
   */
173
  mfa: GoTrueMFAApi
174
  /**
175
   * The storage key used to identify the values saved in localStorage
176
   */
177
  protected storageKey: string
178

179
  protected flowType: AuthFlowType
180

181
  /**
182
   * The JWKS used for verifying asymmetric JWTs
183
   */
184
  protected get jwks() {
185
    return GLOBAL_JWKS[this.storageKey]?.jwks ?? { keys: [] }
198✔
186
  }
187

188
  protected set jwks(value: { keys: JWK[] }) {
189
    GLOBAL_JWKS[this.storageKey] = { ...GLOBAL_JWKS[this.storageKey], jwks: value }
15✔
190
  }
191

192
  protected get jwks_cached_at() {
193
    return GLOBAL_JWKS[this.storageKey]?.cachedAt ?? Number.MIN_SAFE_INTEGER
11✔
194
  }
195

196
  protected set jwks_cached_at(value: number) {
197
    GLOBAL_JWKS[this.storageKey] = { ...GLOBAL_JWKS[this.storageKey], cachedAt: value }
15✔
198
  }
199

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

231
  /**
232
   * Used to broadcast state change events to other tabs listening.
233
   */
234
  protected broadcastChannel: BroadcastChannel | null = null
182✔
235

236
  protected logDebugMessages: boolean
237
  protected logger: (message: string, ...args: any[]) => void = console.log
182✔
238

239
  /**
240
   * Create a new client for use in the browser.
241
   */
242
  constructor(options: GoTrueClientOptions) {
243
    this.instanceID = GoTrueClient.nextInstanceID
182✔
244
    GoTrueClient.nextInstanceID += 1
182✔
245

246
    if (this.instanceID > 0 && isBrowser()) {
182✔
247
      console.warn(
72✔
248
        '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.'
249
      )
250
    }
251

252
    const settings = { ...DEFAULT_OPTIONS, ...options }
182✔
253

254
    this.logDebugMessages = !!settings.debug
182✔
255
    if (typeof settings.debug === 'function') {
182✔
256
      this.logger = settings.debug
2✔
257
    }
258

259
    this.persistSession = settings.persistSession
182✔
260
    this.storageKey = settings.storageKey
182✔
261
    this.autoRefreshToken = settings.autoRefreshToken
182✔
262
    this.admin = new GoTrueAdminApi({
182✔
263
      url: settings.url,
264
      headers: settings.headers,
265
      fetch: settings.fetch,
266
    })
267

268
    this.url = settings.url
182✔
269
    this.headers = settings.headers
182✔
270
    this.fetch = resolveFetch(settings.fetch)
182✔
271
    this.lock = settings.lock || lockNoOp
182✔
272
    this.detectSessionInUrl = settings.detectSessionInUrl
182✔
273
    this.flowType = settings.flowType
182✔
274
    this.hasCustomAuthorizationHeader = settings.hasCustomAuthorizationHeader
182✔
275

276
    if (settings.lock) {
182✔
277
      this.lock = settings.lock
8✔
278
    } else if (isBrowser() && globalThis?.navigator?.locks) {
174!
279
      this.lock = navigatorLock
36✔
280
    } else {
281
      this.lock = lockNoOp
138✔
282
    }
283

284
    if (!this.jwks) {
182!
285
      this.jwks = { keys: [] }
×
286
      this.jwks_cached_at = Number.MIN_SAFE_INTEGER
×
287
    }
288

289
    this.mfa = {
182✔
290
      verify: this._verify.bind(this),
291
      enroll: this._enroll.bind(this),
292
      unenroll: this._unenroll.bind(this),
293
      challenge: this._challenge.bind(this),
294
      listFactors: this._listFactors.bind(this),
295
      challengeAndVerify: this._challengeAndVerify.bind(this),
296
      getAuthenticatorAssuranceLevel: this._getAuthenticatorAssuranceLevel.bind(this),
297
    }
298

299
    if (this.persistSession) {
182✔
300
      if (settings.storage) {
172✔
301
        this.storage = settings.storage
136✔
302
      } else {
303
        if (supportsLocalStorage()) {
36✔
304
          this.storage = globalThis.localStorage
2✔
305
        } else {
306
          this.memoryStorage = {}
34✔
307
          this.storage = memoryLocalStorageAdapter(this.memoryStorage)
34✔
308
        }
309
      }
310

311
      if (settings.userStorage) {
172✔
312
        this.userStorage = settings.userStorage
12✔
313
      }
314
    } else {
315
      this.memoryStorage = {}
10✔
316
      this.storage = memoryLocalStorageAdapter(this.memoryStorage)
10✔
317
    }
318

319
    if (isBrowser() && globalThis.BroadcastChannel && this.persistSession && this.storageKey) {
182✔
320
      try {
44✔
321
        this.broadcastChannel = new globalThis.BroadcastChannel(this.storageKey)
44✔
322
      } catch (e: any) {
323
        console.error(
42✔
324
          'Failed to create a new BroadcastChannel, multi-tab state changes will not be available',
325
          e
326
        )
327
      }
328

329
      this.broadcastChannel?.addEventListener('message', async (event) => {
44✔
330
        this._debug('received broadcast notification from other tab or client', event)
2✔
331

332
        await this._notifyAllSubscribers(event.data.event, event.data.session, false) // broadcast = false so we don't get an endless loop of messages
2✔
333
      })
334
    }
335

336
    this.initialize()
182✔
337
  }
338

339
  private _debug(...args: any[]): GoTrueClient {
340
    if (this.logDebugMessages) {
7,239✔
341
      this.logger(
42✔
342
        `GoTrueClient@${this.instanceID} (${version}) ${new Date().toISOString()}`,
343
        ...args
344
      )
345
    }
346

347
    return this
7,239✔
348
  }
349

350
  /**
351
   * Initializes the client session either from the url or from storage.
352
   * This method is automatically called when instantiating the client, but should also be called
353
   * manually when checking for an error from an auth redirect (oauth, magiclink, password recovery, etc).
354
   */
355
  async initialize(): Promise<InitializeResult> {
356
    if (this.initializePromise) {
238✔
357
      return await this.initializePromise
56✔
358
    }
359

360
    this.initializePromise = (async () => {
182✔
361
      return await this._acquireLock(-1, async () => {
182✔
362
        return await this._initialize()
176✔
363
      })
364
    })()
365

366
    return await this.initializePromise
182✔
367
  }
368

369
  /**
370
   * IMPORTANT:
371
   * 1. Never throw in this method, as it is called from the constructor
372
   * 2. Never return a session from this method as it would be cached over
373
   *    the whole lifetime of the client
374
   */
375
  private async _initialize(): Promise<InitializeResult> {
376
    try {
176✔
377
      const params = parseParametersFromURL(window.location.href)
176✔
378
      let callbackUrlType = 'none'
70✔
379
      if (this._isImplicitGrantCallback(params)) {
70✔
380
        callbackUrlType = 'implicit'
8✔
381
      } else if (await this._isPKCECallback(params)) {
62✔
382
        callbackUrlType = 'pkce'
2✔
383
      }
384

385
      /**
386
       * Attempt to get the session from the URL only if these conditions are fulfilled
387
       *
388
       * Note: If the URL isn't one of the callback url types (implicit or pkce),
389
       * then there could be an existing session so we don't want to prematurely remove it
390
       */
391
      if (isBrowser() && this.detectSessionInUrl && callbackUrlType !== 'none') {
70✔
392
        const { data, error } = await this._getSessionFromURL(params, callbackUrlType)
10✔
393
        if (error) {
6✔
394
          this._debug('#_initialize()', 'error detecting session from URL', error)
4✔
395

396
          if (isAuthImplicitGrantRedirectError(error)) {
4✔
397
            const errorCode = error.details?.code
4✔
398
            if (
4!
399
              errorCode === 'identity_already_exists' ||
12✔
400
              errorCode === 'identity_not_found' ||
401
              errorCode === 'single_identity_not_deletable'
402
            ) {
403
              return { error }
×
404
            }
405
          }
406

407
          // failed login attempt via url,
408
          // remove old session as in verifyOtp, signUp and signInWith*
409
          await this._removeSession()
4✔
410

411
          return { error }
4✔
412
        }
413

414
        const { session, redirectType } = data
2✔
415

416
        this._debug(
2✔
417
          '#_initialize()',
418
          'detected session in URL',
419
          session,
420
          'redirect type',
421
          redirectType
422
        )
423

424
        await this._saveSession(session)
2✔
425

426
        setTimeout(async () => {
2✔
427
          if (redirectType === 'recovery') {
2!
428
            await this._notifyAllSubscribers('PASSWORD_RECOVERY', session)
×
429
          } else {
430
            await this._notifyAllSubscribers('SIGNED_IN', session)
2✔
431
          }
432
        }, 0)
433

434
        return { error: null }
2✔
435
      }
436
      // no login attempt via callback url try to recover session from storage
437
      await this._recoverAndRefresh()
60✔
438
      return { error: null }
60✔
439
    } catch (error) {
440
      if (isAuthError(error)) {
110!
441
        return { error }
×
442
      }
443

444
      return {
110✔
445
        error: new AuthUnknownError('Unexpected error during initialization', error),
446
      }
447
    } finally {
448
      await this._handleVisibilityChange()
176✔
449
      this._debug('#_initialize()', 'end')
176✔
450
    }
451
  }
452

453
  /**
454
   * Creates a new anonymous user.
455
   *
456
   * @returns A session where the is_anonymous claim in the access token JWT set to true
457
   */
458
  async signInAnonymously(credentials?: SignInAnonymouslyCredentials): Promise<AuthResponse> {
459
    try {
6✔
460
      const res = await _request(this.fetch, 'POST', `${this.url}/signup`, {
6✔
461
        headers: this.headers,
462
        body: {
463
          data: credentials?.options?.data ?? {},
54✔
464
          gotrue_meta_security: { captcha_token: credentials?.options?.captchaToken },
36✔
465
        },
466
        xform: _sessionResponse,
467
      })
468
      const { data, error } = res
4✔
469

470
      if (error || !data) {
4!
471
        return { data: { user: null, session: null }, error: error }
×
472
      }
473
      const session: Session | null = data.session
4✔
474
      const user: User | null = data.user
4✔
475

476
      if (data.session) {
4✔
477
        await this._saveSession(data.session)
4✔
478
        await this._notifyAllSubscribers('SIGNED_IN', session)
4✔
479
      }
480

481
      return { data: { user, session }, error: null }
4✔
482
    } catch (error) {
483
      if (isAuthError(error)) {
2✔
484
        return { data: { user: null, session: null }, error }
2✔
485
      }
486

487
      throw error
×
488
    }
489
  }
490

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

546
      const { data, error } = res
113✔
547

548
      if (error || !data) {
113!
549
        return { data: { user: null, session: null }, error: error }
×
550
      }
551

552
      const session: Session | null = data.session
113✔
553
      const user: User | null = data.user
113✔
554

555
      if (data.session) {
113✔
556
        await this._saveSession(data.session)
111✔
557
        await this._notifyAllSubscribers('SIGNED_IN', session)
111✔
558
      }
559

560
      return { data: { user, session }, error: null }
113✔
561
    } catch (error) {
562
      if (isAuthError(error)) {
14✔
563
        return { data: { user: null, session: null }, error }
14✔
564
      }
565

566
      throw error
×
567
    }
568
  }
569

570
  /**
571
   * Log in an existing user with an email and password or phone and password.
572
   *
573
   * Be aware that you may get back an error message that will not distinguish
574
   * between the cases where the account does not exist or that the
575
   * email/phone and password combination is wrong or that the account can only
576
   * be accessed via social login.
577
   */
578
  async signInWithPassword(
579
    credentials: SignInWithPasswordCredentials
580
  ): Promise<AuthTokenResponsePassword> {
581
    try {
36✔
582
      let res: AuthResponsePassword
583
      if ('email' in credentials) {
36✔
584
        const { email, password, options } = credentials
28✔
585
        res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=password`, {
28✔
586
          headers: this.headers,
587
          body: {
588
            email,
589
            password,
590
            gotrue_meta_security: { captcha_token: options?.captchaToken },
84✔
591
          },
592
          xform: _sessionResponsePassword,
593
        })
594
      } else if ('phone' in credentials) {
8✔
595
        const { phone, password, options } = credentials
6✔
596
        res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=password`, {
6✔
597
          headers: this.headers,
598
          body: {
599
            phone,
600
            password,
601
            gotrue_meta_security: { captcha_token: options?.captchaToken },
18✔
602
          },
603
          xform: _sessionResponsePassword,
604
        })
605
      } else {
606
        throw new AuthInvalidCredentialsError(
2✔
607
          'You must provide either an email or phone number and a password'
608
        )
609
      }
610
      const { data, error } = res
28✔
611

612
      if (error) {
28!
613
        return { data: { user: null, session: null }, error }
×
614
      } else if (!data || !data.session || !data.user) {
28✔
615
        return { data: { user: null, session: null }, error: new AuthInvalidTokenResponseError() }
2✔
616
      }
617
      if (data.session) {
26✔
618
        await this._saveSession(data.session)
26✔
619
        await this._notifyAllSubscribers('SIGNED_IN', data.session)
26✔
620
      }
621
      return {
26✔
622
        data: {
623
          user: data.user,
624
          session: data.session,
625
          ...(data.weak_password ? { weakPassword: data.weak_password } : null),
26!
626
        },
627
        error,
628
      }
629
    } catch (error) {
630
      if (isAuthError(error)) {
8✔
631
        return { data: { user: null, session: null }, error }
8✔
632
      }
633
      throw error
×
634
    }
635
  }
636

637
  /**
638
   * Log in an existing user via a third-party provider.
639
   * This method supports the PKCE flow.
640
   */
641
  async signInWithOAuth(credentials: SignInWithOAuthCredentials): Promise<OAuthResponse> {
642
    return await this._handleProviderSignIn(credentials.provider, {
20✔
643
      redirectTo: credentials.options?.redirectTo,
60✔
644
      scopes: credentials.options?.scopes,
60✔
645
      queryParams: credentials.options?.queryParams,
60✔
646
      skipBrowserRedirect: credentials.options?.skipBrowserRedirect,
60✔
647
    })
648
  }
649

650
  /**
651
   * Log in an existing user by exchanging an Auth Code issued during the PKCE flow.
652
   */
653
  async exchangeCodeForSession(authCode: string): Promise<AuthTokenResponse> {
654
    await this.initializePromise
4✔
655

656
    return this._acquireLock(-1, async () => {
4✔
657
      return this._exchangeCodeForSession(authCode)
4✔
658
    })
659
  }
660

661
  /**
662
   * Signs in a user by verifying a message signed by the user's private key.
663
   * Supports Ethereum (via Sign-In-With-Ethereum) & Solana (Sign-In-With-Solana) standards,
664
   * both of which derive from the EIP-4361 standard
665
   * With slight variation on Solana's side.
666
   * @reference https://eips.ethereum.org/EIPS/eip-4361
667
   */
668
  async signInWithWeb3(credentials: Web3Credentials): Promise<
669
    | {
670
        data: { session: Session; user: User }
671
        error: null
672
      }
673
    | { data: { session: null; user: null }; error: AuthError }
674
  > {
675
    const { chain } = credentials
36✔
676

677
    switch (chain) {
36✔
678
      case 'ethereum':
679
        return await this.signInWithEthereum(credentials)
20✔
680
      case 'solana':
681
        return await this.signInWithSolana(credentials)
12✔
682
      default:
683
        throw new Error(`@supabase/auth-js: Unsupported chain "${chain}"`)
4✔
684
    }
685
  }
686

687
  private async signInWithEthereum(
688
    credentials: EthereumWeb3Credentials
689
  ): Promise<
690
    | { data: { session: Session; user: User }; error: null }
691
    | { data: { session: null; user: null }; error: AuthError }
692
  > {
693
    // TODO: flatten type
694
    let message: string
695
    let signature: Hex
696

697
    if ('message' in credentials) {
20✔
698
      message = credentials.message
8✔
699
      signature = credentials.signature
8✔
700
    } else {
701
      const { chain, wallet, statement, options } = credentials
12✔
702

703
      let resolvedWallet: EthereumWallet
704

705
      if (!isBrowser()) {
12✔
706
        if (typeof wallet !== 'object' || !options?.url) {
8!
707
          throw new Error(
4✔
708
            '@supabase/auth-js: Both wallet and url must be specified in non-browser environments.'
709
          )
710
        }
711

712
        resolvedWallet = wallet
4✔
713
      } else if (typeof wallet === 'object') {
4!
714
        resolvedWallet = wallet
4✔
715
      } else {
716
        const windowAny = window as any
×
717

718
        if (
×
719
          'ethereum' in windowAny &&
×
720
          typeof windowAny.ethereum === 'object' &&
721
          'request' in windowAny.ethereum &&
722
          typeof windowAny.ethereum.request === 'function'
723
        ) {
724
          resolvedWallet = windowAny.ethereum
×
725
        } else {
726
          throw new Error(
×
727
            `@supabase/auth-js: No compatible Ethereum wallet interface on the window object (window.ethereum) 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: 'ethereum', wallet: resolvedUserWallet }) instead.`
728
          )
729
        }
730
      }
731

732
      const url = new URL(options?.url ?? window.location.href)
8✔
733

734
      const accounts = await resolvedWallet
8✔
735
        .request({
736
          method: 'eth_requestAccounts',
737
        })
738
        .then((accs) => accs as string[])
4✔
739
        .catch(() => {
740
          throw new Error(
2✔
741
            `@supabase/auth-js: Wallet method eth_requestAccounts is missing or invalid`
742
          )
743
        })
744

745
      if (!accounts || accounts.length === 0) {
4✔
746
        throw new Error(
2✔
747
          `@supabase/auth-js: No accounts available. Please ensure the wallet is connected.`
748
        )
749
      }
750

751
      const address = getAddress(accounts[0])
2✔
752

753
      let chainId = options?.signInWithEthereum?.chainId
×
754
      if (!chainId) {
×
755
        const chainIdHex = await resolvedWallet.request({
×
756
          method: 'eth_chainId',
757
        })
758
        chainId = fromHex(chainIdHex as Hex)
×
759
      }
760

761
      const siweMessage: SiweMessage = {
×
762
        domain: url.host,
763
        address: address,
764
        statement: statement,
765
        uri: url.href,
766
        version: '1',
767
        chainId: chainId,
768
        nonce: options?.signInWithEthereum?.nonce,
×
769
        issuedAt: options?.signInWithEthereum?.issuedAt ?? new Date(),
×
770
        expirationTime: options?.signInWithEthereum?.expirationTime,
×
771
        notBefore: options?.signInWithEthereum?.notBefore,
×
772
        requestId: options?.signInWithEthereum?.requestId,
×
773
        resources: options?.signInWithEthereum?.resources,
×
774
      }
775

776
      message = createSiweMessage(siweMessage)
×
777

778
      // Sign message
779
      signature = (await resolvedWallet.request({
×
780
        method: 'personal_sign',
781
        params: [toHex(message), address],
782
      })) as Hex
783
    }
784

785
    try {
8✔
786
      const { data, error } = await _request(
8✔
787
        this.fetch,
788
        'POST',
789
        `${this.url}/token?grant_type=web3`,
790
        {
791
          headers: this.headers,
792
          body: {
793
            chain: 'ethereum',
794
            message,
795
            signature,
796
            ...(credentials.options?.captchaToken
32!
797
              ? { gotrue_meta_security: { captcha_token: credentials.options?.captchaToken } }
×
798
              : null),
799
          },
800
          xform: _sessionResponse,
801
        }
802
      )
803
      if (error) {
×
804
        throw error
×
805
      }
806
      if (!data || !data.session || !data.user) {
×
807
        return {
×
808
          data: { user: null, session: null },
809
          error: new AuthInvalidTokenResponseError(),
810
        }
811
      }
812
      if (data.session) {
×
813
        await this._saveSession(data.session)
×
814
        await this._notifyAllSubscribers('SIGNED_IN', data.session)
×
815
      }
816
      return { data: { ...data }, error }
×
817
    } catch (error) {
818
      if (isAuthError(error)) {
8✔
819
        return { data: { user: null, session: null }, error }
8✔
820
      }
821

822
      throw error
×
823
    }
824
  }
825

826
  private async signInWithSolana(credentials: SolanaWeb3Credentials) {
827
    let message: string
828
    let signature: Uint8Array
829

830
    if ('message' in credentials) {
12✔
831
      message = credentials.message
2✔
832
      signature = credentials.signature
2✔
833
    } else {
834
      const { chain, wallet, statement, options } = credentials
10✔
835

836
      let resolvedWallet: SolanaWallet
837

838
      if (!isBrowser()) {
10✔
839
        if (typeof wallet !== 'object' || !options?.url) {
6!
840
          throw new Error(
2✔
841
            '@supabase/auth-js: Both wallet and url must be specified in non-browser environments.'
842
          )
843
        }
844

845
        resolvedWallet = wallet
4✔
846
      } else if (typeof wallet === 'object') {
4!
847
        resolvedWallet = wallet
4✔
848
      } else {
849
        const windowAny = window as any
×
850

851
        if (
×
852
          'solana' in windowAny &&
×
853
          typeof windowAny.solana === 'object' &&
854
          (('signIn' in windowAny.solana && typeof windowAny.solana.signIn === 'function') ||
855
            ('signMessage' in windowAny.solana &&
856
              typeof windowAny.solana.signMessage === 'function'))
857
        ) {
858
          resolvedWallet = windowAny.solana
×
859
        } else {
860
          throw new Error(
×
861
            `@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.`
862
          )
863
        }
864
      }
865

866
      const url = new URL(options?.url ?? window.location.href)
8✔
867

868
      if ('signIn' in resolvedWallet && resolvedWallet.signIn) {
8!
869
        const output = await resolvedWallet.signIn({
×
870
          issuedAt: new Date().toISOString(),
871

872
          ...options?.signInWithSolana,
×
873

874
          // non-overridable properties
875
          version: '1',
876
          domain: url.host,
877
          uri: url.href,
878

879
          ...(statement ? { statement } : null),
×
880
        })
881

882
        let outputToProcess: any
883

884
        if (Array.isArray(output) && output[0] && typeof output[0] === 'object') {
×
885
          outputToProcess = output[0]
×
886
        } else if (
×
887
          output &&
×
888
          typeof output === 'object' &&
889
          'signedMessage' in output &&
890
          'signature' in output
891
        ) {
892
          outputToProcess = output
×
893
        } else {
894
          throw new Error('@supabase/auth-js: Wallet method signIn() returned unrecognized value')
×
895
        }
896

897
        if (
×
898
          'signedMessage' in outputToProcess &&
×
899
          'signature' in outputToProcess &&
900
          (typeof outputToProcess.signedMessage === 'string' ||
901
            outputToProcess.signedMessage instanceof Uint8Array) &&
902
          outputToProcess.signature instanceof Uint8Array
903
        ) {
904
          message =
×
905
            typeof outputToProcess.signedMessage === 'string'
×
906
              ? outputToProcess.signedMessage
907
              : new TextDecoder().decode(outputToProcess.signedMessage)
908
          signature = outputToProcess.signature
×
909
        } else {
910
          throw new Error(
×
911
            '@supabase/auth-js: Wallet method signIn() API returned object without signedMessage and signature fields'
912
          )
913
        }
914
      } else {
915
        if (
8✔
916
          !('signMessage' in resolvedWallet) ||
24✔
917
          typeof resolvedWallet.signMessage !== 'function' ||
918
          !('publicKey' in resolvedWallet) ||
919
          typeof resolvedWallet !== 'object' ||
920
          !resolvedWallet.publicKey ||
921
          !('toBase58' in resolvedWallet.publicKey) ||
922
          typeof resolvedWallet.publicKey.toBase58 !== 'function'
923
        ) {
924
          throw new Error(
6✔
925
            '@supabase/auth-js: Wallet does not have a compatible signMessage() and publicKey.toBase58() API'
926
          )
927
        }
928

929
        message = [
2✔
930
          `${url.host} wants you to sign in with your Solana account:`,
931
          resolvedWallet.publicKey.toBase58(),
932
          ...(statement ? ['', statement, ''] : ['']),
2!
933
          'Version: 1',
934
          `URI: ${url.href}`,
935
          `Issued At: ${options?.signInWithSolana?.issuedAt ?? new Date().toISOString()}`,
18!
936
          ...(options?.signInWithSolana?.notBefore
14!
937
            ? [`Not Before: ${options.signInWithSolana.notBefore}`]
938
            : []),
939
          ...(options?.signInWithSolana?.expirationTime
14!
940
            ? [`Expiration Time: ${options.signInWithSolana.expirationTime}`]
941
            : []),
942
          ...(options?.signInWithSolana?.chainId
14!
943
            ? [`Chain ID: ${options.signInWithSolana.chainId}`]
944
            : []),
945
          ...(options?.signInWithSolana?.nonce ? [`Nonce: ${options.signInWithSolana.nonce}`] : []),
14!
946
          ...(options?.signInWithSolana?.requestId
14!
947
            ? [`Request ID: ${options.signInWithSolana.requestId}`]
948
            : []),
949
          ...(options?.signInWithSolana?.resources?.length
20!
950
            ? [
951
                'Resources',
952
                ...options.signInWithSolana.resources.map((resource) => `- ${resource}`),
×
953
              ]
954
            : []),
955
        ].join('\n')
956

957
        const maybeSignature = await resolvedWallet.signMessage(
2✔
958
          new TextEncoder().encode(message),
959
          'utf8'
960
        )
961

962
        if (!maybeSignature || !(maybeSignature instanceof Uint8Array)) {
×
963
          throw new Error(
×
964
            '@supabase/auth-js: Wallet signMessage() API returned an recognized value'
965
          )
966
        }
967

968
        signature = maybeSignature
×
969
      }
970
    }
971

972
    try {
2✔
973
      const { data, error } = await _request(
2✔
974
        this.fetch,
975
        'POST',
976
        `${this.url}/token?grant_type=web3`,
977
        {
978
          headers: this.headers,
979
          body: {
980
            chain: 'solana',
981
            message,
982
            signature: bytesToBase64URL(signature),
983

984
            ...(credentials.options?.captchaToken
8!
985
              ? { gotrue_meta_security: { captcha_token: credentials.options?.captchaToken } }
×
986
              : null),
987
          },
988
          xform: _sessionResponse,
989
        }
990
      )
991
      if (error) {
×
992
        throw error
×
993
      }
994
      if (!data || !data.session || !data.user) {
×
995
        return {
×
996
          data: { user: null, session: null },
997
          error: new AuthInvalidTokenResponseError(),
998
        }
999
      }
1000
      if (data.session) {
×
1001
        await this._saveSession(data.session)
×
1002
        await this._notifyAllSubscribers('SIGNED_IN', data.session)
×
1003
      }
1004
      return { data: { ...data }, error }
×
1005
    } catch (error) {
1006
      if (isAuthError(error)) {
2✔
1007
        return { data: { user: null, session: null }, error }
2✔
1008
      }
1009

1010
      throw error
×
1011
    }
1012
  }
1013

1014
  private async _exchangeCodeForSession(authCode: string): Promise<
1015
    | {
1016
        data: { session: Session; user: User; redirectType: string | null }
1017
        error: null
1018
      }
1019
    | { data: { session: null; user: null; redirectType: null }; error: AuthError }
1020
  > {
1021
    const storageItem = await getItemAsync(this.storage, `${this.storageKey}-code-verifier`)
6✔
1022
    const [codeVerifier, redirectType] = ((storageItem ?? '') as string).split('/')
6✔
1023

1024
    try {
6✔
1025
      const { data, error } = await _request(
6✔
1026
        this.fetch,
1027
        'POST',
1028
        `${this.url}/token?grant_type=pkce`,
1029
        {
1030
          headers: this.headers,
1031
          body: {
1032
            auth_code: authCode,
1033
            code_verifier: codeVerifier,
1034
          },
1035
          xform: _sessionResponse,
1036
        }
1037
      )
1038
      await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
×
1039
      if (error) {
×
1040
        throw error
×
1041
      }
1042
      if (!data || !data.session || !data.user) {
×
1043
        return {
×
1044
          data: { user: null, session: null, redirectType: null },
1045
          error: new AuthInvalidTokenResponseError(),
1046
        }
1047
      }
1048
      if (data.session) {
×
1049
        await this._saveSession(data.session)
×
1050
        await this._notifyAllSubscribers('SIGNED_IN', data.session)
×
1051
      }
1052
      return { data: { ...data, redirectType: redirectType ?? null }, error }
×
1053
    } catch (error) {
1054
      if (isAuthError(error)) {
6✔
1055
        return { data: { user: null, session: null, redirectType: null }, error }
6✔
1056
      }
1057

1058
      throw error
×
1059
    }
1060
  }
1061

1062
  /**
1063
   * Allows signing in with an OIDC ID token. The authentication provider used
1064
   * should be enabled and configured.
1065
   */
1066
  async signInWithIdToken(credentials: SignInWithIdTokenCredentials): Promise<AuthTokenResponse> {
1067
    try {
6✔
1068
      const { options, provider, token, access_token, nonce } = credentials
6✔
1069

1070
      const res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=id_token`, {
6✔
1071
        headers: this.headers,
1072
        body: {
1073
          provider,
1074
          id_token: token,
1075
          access_token,
1076
          nonce,
1077
          gotrue_meta_security: { captcha_token: options?.captchaToken },
18✔
1078
        },
1079
        xform: _sessionResponse,
1080
      })
1081

1082
      const { data, error } = res
2✔
1083
      if (error) {
2!
1084
        return { data: { user: null, session: null }, error }
×
1085
      } else if (!data || !data.session || !data.user) {
2!
1086
        return {
2✔
1087
          data: { user: null, session: null },
1088
          error: new AuthInvalidTokenResponseError(),
1089
        }
1090
      }
1091
      if (data.session) {
×
1092
        await this._saveSession(data.session)
×
1093
        await this._notifyAllSubscribers('SIGNED_IN', data.session)
×
1094
      }
1095
      return { data, error }
×
1096
    } catch (error) {
1097
      if (isAuthError(error)) {
4✔
1098
        return { data: { user: null, session: null }, error }
4✔
1099
      }
1100
      throw error
×
1101
    }
1102
  }
1103

1104
  /**
1105
   * Log in a user using magiclink or a one-time password (OTP).
1106
   *
1107
   * If the `{{ .ConfirmationURL }}` variable is specified in the email template, a magiclink will be sent.
1108
   * If the `{{ .Token }}` variable is specified in the email template, an OTP will be sent.
1109
   * 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.
1110
   *
1111
   * Be aware that you may get back an error message that will not distinguish
1112
   * between the cases where the account does not exist or, that the account
1113
   * can only be accessed via social login.
1114
   *
1115
   * Do note that you will need to configure a Whatsapp sender on Twilio
1116
   * if you are using phone sign in with the 'whatsapp' channel. The whatsapp
1117
   * channel is not supported on other providers
1118
   * at this time.
1119
   * This method supports PKCE when an email is passed.
1120
   */
1121
  async signInWithOtp(credentials: SignInWithPasswordlessCredentials): Promise<AuthOtpResponse> {
1122
    try {
16✔
1123
      if ('email' in credentials) {
16✔
1124
        const { email, options } = credentials
8✔
1125
        let codeChallenge: string | null = null
8✔
1126
        let codeChallengeMethod: string | null = null
8✔
1127
        if (this.flowType === 'pkce') {
8✔
1128
          ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
2✔
1129
            this.storage,
1130
            this.storageKey
1131
          )
1132
        }
1133
        const { error } = await _request(this.fetch, 'POST', `${this.url}/otp`, {
8✔
1134
          headers: this.headers,
1135
          body: {
1136
            email,
1137
            data: options?.data ?? {},
48✔
1138
            create_user: options?.shouldCreateUser ?? true,
48✔
1139
            gotrue_meta_security: { captcha_token: options?.captchaToken },
24✔
1140
            code_challenge: codeChallenge,
1141
            code_challenge_method: codeChallengeMethod,
1142
          },
1143
          redirectTo: options?.emailRedirectTo,
24✔
1144
        })
1145
        return { data: { user: null, session: null }, error }
4✔
1146
      }
1147
      if ('phone' in credentials) {
8✔
1148
        const { phone, options } = credentials
6✔
1149
        const { data, error } = await _request(this.fetch, 'POST', `${this.url}/otp`, {
6✔
1150
          headers: this.headers,
1151
          body: {
1152
            phone,
1153
            data: options?.data ?? {},
36✔
1154
            create_user: options?.shouldCreateUser ?? true,
36✔
1155
            gotrue_meta_security: { captcha_token: options?.captchaToken },
18✔
1156
            channel: options?.channel ?? 'sms',
36✔
1157
          },
1158
        })
1159
        return { data: { user: null, session: null, messageId: data?.message_id }, error }
×
1160
      }
1161
      throw new AuthInvalidCredentialsError('You must provide either an email or phone number.')
2✔
1162
    } catch (error) {
1163
      if (isAuthError(error)) {
12✔
1164
        return { data: { user: null, session: null }, error }
12✔
1165
      }
1166

1167
      throw error
×
1168
    }
1169
  }
1170

1171
  /**
1172
   * Log in a user given a User supplied OTP or TokenHash received through mobile or email.
1173
   */
1174
  async verifyOtp(params: VerifyOtpParams): Promise<AuthResponse> {
1175
    try {
10✔
1176
      let redirectTo: string | undefined = undefined
10✔
1177
      let captchaToken: string | undefined = undefined
10✔
1178
      if ('options' in params) {
10✔
1179
        redirectTo = params.options?.redirectTo
4!
1180
        captchaToken = params.options?.captchaToken
4!
1181
      }
1182
      const { data, error } = await _request(this.fetch, 'POST', `${this.url}/verify`, {
10✔
1183
        headers: this.headers,
1184
        body: {
1185
          ...params,
1186
          gotrue_meta_security: { captcha_token: captchaToken },
1187
        },
1188
        redirectTo,
1189
        xform: _sessionResponse,
1190
      })
1191

1192
      if (error) {
×
1193
        throw error
×
1194
      }
1195

1196
      if (!data) {
×
1197
        throw new Error('An error occurred on token verification.')
×
1198
      }
1199

1200
      const session: Session | null = data.session
×
1201
      const user: User = data.user
×
1202

1203
      if (session?.access_token) {
×
1204
        await this._saveSession(session as Session)
×
1205
        await this._notifyAllSubscribers(
×
1206
          params.type == 'recovery' ? 'PASSWORD_RECOVERY' : 'SIGNED_IN',
×
1207
          session
1208
        )
1209
      }
1210

1211
      return { data: { user, session }, error: null }
×
1212
    } catch (error) {
1213
      if (isAuthError(error)) {
10✔
1214
        return { data: { user: null, session: null }, error }
10✔
1215
      }
1216

1217
      throw error
×
1218
    }
1219
  }
1220

1221
  /**
1222
   * Attempts a single-sign on using an enterprise Identity Provider. A
1223
   * successful SSO attempt will redirect the current page to the identity
1224
   * provider authorization page. The redirect URL is implementation and SSO
1225
   * protocol specific.
1226
   *
1227
   * You can use it by providing a SSO domain. Typically you can extract this
1228
   * domain by asking users for their email address. If this domain is
1229
   * registered on the Auth instance the redirect will use that organization's
1230
   * currently active SSO Identity Provider for the login.
1231
   *
1232
   * If you have built an organization-specific login page, you can use the
1233
   * organization's SSO Identity Provider UUID directly instead.
1234
   */
1235
  async signInWithSSO(params: SignInWithSSO): Promise<SSOResponse> {
1236
    try {
8✔
1237
      let codeChallenge: string | null = null
8✔
1238
      let codeChallengeMethod: string | null = null
8✔
1239
      if (this.flowType === 'pkce') {
8✔
1240
        ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
8✔
1241
          this.storage,
1242
          this.storageKey
1243
        )
1244
      }
1245

1246
      return await _request(this.fetch, 'POST', `${this.url}/sso`, {
8✔
1247
        body: {
1248
          ...('providerId' in params ? { provider_id: params.providerId } : null),
8✔
1249
          ...('domain' in params ? { domain: params.domain } : null),
8✔
1250
          redirect_to: params.options?.redirectTo ?? undefined,
48✔
1251
          ...(params?.options?.captchaToken
56!
1252
            ? { gotrue_meta_security: { captcha_token: params.options.captchaToken } }
1253
            : null),
1254
          skip_http_redirect: true, // fetch does not handle redirects
1255
          code_challenge: codeChallenge,
1256
          code_challenge_method: codeChallengeMethod,
1257
        },
1258
        headers: this.headers,
1259
        xform: _ssoResponse,
1260
      })
1261
    } catch (error) {
1262
      if (isAuthError(error)) {
8✔
1263
        return { data: null, error }
8✔
1264
      }
1265
      throw error
×
1266
    }
1267
  }
1268

1269
  /**
1270
   * Sends a reauthentication OTP to the user's email or phone number.
1271
   * Requires the user to be signed-in.
1272
   */
1273
  async reauthenticate(): Promise<AuthResponse> {
1274
    await this.initializePromise
4✔
1275

1276
    return await this._acquireLock(-1, async () => {
4✔
1277
      return await this._reauthenticate()
4✔
1278
    })
1279
  }
1280

1281
  private async _reauthenticate(): Promise<AuthResponse> {
1282
    try {
4✔
1283
      return await this._useSession(async (result) => {
4✔
1284
        const {
1285
          data: { session },
1286
          error: sessionError,
1287
        } = result
4✔
1288
        if (sessionError) throw sessionError
4!
1289
        if (!session) throw new AuthSessionMissingError()
4!
1290

1291
        const { error } = await _request(this.fetch, 'GET', `${this.url}/reauthenticate`, {
4✔
1292
          headers: this.headers,
1293
          jwt: session.access_token,
1294
        })
1295
        return { data: { user: null, session: null }, error }
2✔
1296
      })
1297
    } catch (error) {
1298
      if (isAuthError(error)) {
2✔
1299
        return { data: { user: null, session: null }, error }
2✔
1300
      }
1301
      throw error
×
1302
    }
1303
  }
1304

1305
  /**
1306
   * Resends an existing signup confirmation email, email change email, SMS OTP or phone change OTP.
1307
   */
1308
  async resend(credentials: ResendParams): Promise<AuthOtpResponse> {
1309
    try {
10✔
1310
      const endpoint = `${this.url}/resend`
10✔
1311
      if ('email' in credentials) {
10✔
1312
        const { email, type, options } = credentials
4✔
1313
        const { error } = await _request(this.fetch, 'POST', endpoint, {
4✔
1314
          headers: this.headers,
1315
          body: {
1316
            email,
1317
            type,
1318
            gotrue_meta_security: { captcha_token: options?.captchaToken },
12!
1319
          },
1320
          redirectTo: options?.emailRedirectTo,
12!
1321
        })
1322
        return { data: { user: null, session: null }, error }
4✔
1323
      } else if ('phone' in credentials) {
6✔
1324
        const { phone, type, options } = credentials
4✔
1325
        const { data, error } = await _request(this.fetch, 'POST', endpoint, {
4✔
1326
          headers: this.headers,
1327
          body: {
1328
            phone,
1329
            type,
1330
            gotrue_meta_security: { captcha_token: options?.captchaToken },
12!
1331
          },
1332
        })
1333
        return { data: { user: null, session: null, messageId: data?.message_id }, error }
4!
1334
      }
1335
      throw new AuthInvalidCredentialsError(
2✔
1336
        'You must provide either an email or phone number and a type'
1337
      )
1338
    } catch (error) {
1339
      if (isAuthError(error)) {
2✔
1340
        return { data: { user: null, session: null }, error }
2✔
1341
      }
1342
      throw error
×
1343
    }
1344
  }
1345

1346
  /**
1347
   * Returns the session, refreshing it if necessary.
1348
   *
1349
   * 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.
1350
   *
1351
   * **IMPORTANT:** This method loads values directly from the storage attached
1352
   * to the client. If that storage is based on request cookies for example,
1353
   * the values in it may not be authentic and therefore it's strongly advised
1354
   * against using this method and its results in such circumstances. A warning
1355
   * will be emitted if this is detected. Use {@link #getUser()} instead.
1356
   */
1357
  async getSession() {
1358
    await this.initializePromise
75✔
1359

1360
    const result = await this._acquireLock(-1, async () => {
75✔
1361
      return this._useSession(async (result) => {
75✔
1362
        return result
71✔
1363
      })
1364
    })
1365

1366
    return result
71✔
1367
  }
1368

1369
  /**
1370
   * Acquires a global lock based on the storage key.
1371
   */
1372
  private async _acquireLock<R>(acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
1373
    this._debug('#_acquireLock', 'begin', acquireTimeout)
605✔
1374

1375
    try {
605✔
1376
      if (this.lockAcquired) {
605✔
1377
        const last = this.pendingInLock.length
2!
1378
          ? this.pendingInLock[this.pendingInLock.length - 1]
1379
          : Promise.resolve()
1380

1381
        const result = (async () => {
2✔
1382
          await last
2✔
1383
          return await fn()
2✔
1384
        })()
1385

1386
        this.pendingInLock.push(
2✔
1387
          (async () => {
1388
            try {
2✔
1389
              await result
2✔
1390
            } catch (e: any) {
1391
              // we just care if it finished
1392
            }
1393
          })()
1394
        )
1395

1396
        return result
2✔
1397
      }
1398

1399
      return await this.lock(`lock:${this.storageKey}`, acquireTimeout, async () => {
603✔
1400
        this._debug('#_acquireLock', 'lock acquired for storage key', this.storageKey)
595✔
1401

1402
        try {
595✔
1403
          this.lockAcquired = true
595✔
1404

1405
          const result = fn()
595✔
1406

1407
          this.pendingInLock.push(
595✔
1408
            (async () => {
1409
              try {
595✔
1410
                await result
595✔
1411
              } catch (e: any) {
1412
                // we just care if it finished
1413
              }
1414
            })()
1415
          )
1416

1417
          await result
595✔
1418

1419
          // keep draining the queue until there's nothing to wait on
1420
          while (this.pendingInLock.length) {
587✔
1421
            const waitOn = [...this.pendingInLock]
587✔
1422

1423
            await Promise.all(waitOn)
587✔
1424

1425
            this.pendingInLock.splice(0, waitOn.length)
587✔
1426
          }
1427

1428
          return await result
587✔
1429
        } finally {
1430
          this._debug('#_acquireLock', 'lock released for storage key', this.storageKey)
595✔
1431

1432
          this.lockAcquired = false
595✔
1433
        }
1434
      })
1435
    } finally {
1436
      this._debug('#_acquireLock', 'end')
605✔
1437
    }
1438
  }
1439

1440
  /**
1441
   * Use instead of {@link #getSession} inside the library. It is
1442
   * semantically usually what you want, as getting a session involves some
1443
   * processing afterwards that requires only one client operating on the
1444
   * session at once across multiple tabs or processes.
1445
   */
1446
  private async _useSession<R>(
1447
    fn: (
1448
      result:
1449
        | {
1450
            data: {
1451
              session: Session
1452
            }
1453
            error: null
1454
          }
1455
        | {
1456
            data: {
1457
              session: null
1458
            }
1459
            error: AuthError
1460
          }
1461
        | {
1462
            data: {
1463
              session: null
1464
            }
1465
            error: null
1466
          }
1467
    ) => Promise<R>
1468
  ): Promise<R> {
1469
    this._debug('#_useSession', 'begin')
433✔
1470

1471
    try {
433✔
1472
      // the use of __loadSession here is the only correct use of the function!
1473
      const result = await this.__loadSession()
433✔
1474

1475
      return await fn(result)
427✔
1476
    } finally {
1477
      this._debug('#_useSession', 'end')
433✔
1478
    }
1479
  }
1480

1481
  /**
1482
   * NEVER USE DIRECTLY!
1483
   *
1484
   * Always use {@link #_useSession}.
1485
   */
1486
  private async __loadSession(): Promise<
1487
    | {
1488
        data: {
1489
          session: Session
1490
        }
1491
        error: null
1492
      }
1493
    | {
1494
        data: {
1495
          session: null
1496
        }
1497
        error: AuthError
1498
      }
1499
    | {
1500
        data: {
1501
          session: null
1502
        }
1503
        error: null
1504
      }
1505
  > {
1506
    this._debug('#__loadSession()', 'begin')
433✔
1507

1508
    if (!this.lockAcquired) {
433✔
1509
      this._debug('#__loadSession()', 'used outside of an acquired lock!', new Error().stack)
30✔
1510
    }
1511

1512
    try {
433✔
1513
      let currentSession: Session | null = null
433✔
1514

1515
      const maybeSession = await getItemAsync(this.storage, this.storageKey)
433✔
1516

1517
      this._debug('#getSession()', 'session from storage', maybeSession)
429✔
1518

1519
      if (maybeSession !== null) {
429✔
1520
        if (this._isValidSession(maybeSession)) {
189✔
1521
          currentSession = maybeSession
177✔
1522
        } else {
1523
          this._debug('#getSession()', 'session from storage is not valid')
12✔
1524
          await this._removeSession()
12✔
1525
        }
1526
      }
1527

1528
      if (!currentSession) {
427✔
1529
        return { data: { session: null }, error: null }
250✔
1530
      }
1531

1532
      // A session is considered expired before the access token _actually_
1533
      // expires. When the autoRefreshToken option is off (or when the tab is
1534
      // in the background), very eager users of getSession() -- like
1535
      // realtime-js -- might send a valid JWT which will expire by the time it
1536
      // reaches the server.
1537
      const hasExpired = currentSession.expires_at
177!
1538
        ? currentSession.expires_at * 1000 - Date.now() < EXPIRY_MARGIN_MS
1539
        : false
1540

1541
      this._debug(
177✔
1542
        '#__loadSession()',
1543
        `session has${hasExpired ? '' : ' not'} expired`,
177✔
1544
        'expires_at',
1545
        currentSession.expires_at
1546
      )
1547

1548
      if (!hasExpired) {
177✔
1549
        if (this.userStorage) {
167✔
1550
          const maybeUser: { user?: User | null } | null = (await getItemAsync(
8✔
1551
            this.userStorage,
1552
            this.storageKey + '-user'
1553
          )) as any
1554

1555
          if (maybeUser?.user) {
8!
1556
            currentSession.user = maybeUser.user
×
1557
          } else {
1558
            currentSession.user = userNotAvailableProxy()
8✔
1559
          }
1560
        }
1561

1562
        if (this.storage.isServer && currentSession.user) {
167✔
1563
          let suppressWarning = this.suppressGetSessionWarning
14✔
1564
          const proxySession: Session = new Proxy(currentSession, {
14✔
1565
            get: (target: any, prop: string, receiver: any) => {
1566
              if (!suppressWarning && prop === 'user') {
24✔
1567
                // only show warning when the user object is being accessed from the server
1568
                console.warn(
2✔
1569
                  '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.'
1570
                )
1571
                suppressWarning = true // keeps this proxy instance from logging additional warnings
2✔
1572
                this.suppressGetSessionWarning = true // keeps this client's future proxy instances from warning
2✔
1573
              }
1574
              return Reflect.get(target, prop, receiver)
24✔
1575
            },
1576
          })
1577
          currentSession = proxySession
14✔
1578
        }
1579

1580
        return { data: { session: currentSession }, error: null }
167✔
1581
      }
1582

1583
      const { data: session, error } = await this._callRefreshToken(currentSession.refresh_token)
10✔
1584
      if (error) {
10✔
1585
        return { data: { session: null }, error }
6✔
1586
      }
1587

1588
      return { data: { session }, error: null }
4✔
1589
    } finally {
1590
      this._debug('#__loadSession()', 'end')
433✔
1591
    }
1592
  }
1593

1594
  /**
1595
   * Gets the current user details if there is an existing session. This method
1596
   * performs a network request to the Supabase Auth server, so the returned
1597
   * value is authentic and can be used to base authorization rules on.
1598
   *
1599
   * @param jwt Takes in an optional access token JWT. If no JWT is provided, the JWT from the current session is used.
1600
   */
1601
  async getUser(jwt?: string): Promise<UserResponse> {
1602
    if (jwt) {
23✔
1603
      return await this._getUser(jwt)
7✔
1604
    }
1605

1606
    await this.initializePromise
16✔
1607

1608
    const result = await this._acquireLock(-1, async () => {
16✔
1609
      return await this._getUser()
16✔
1610
    })
1611

1612
    return result
16✔
1613
  }
1614

1615
  private async _getUser(jwt?: string): Promise<UserResponse> {
1616
    try {
37✔
1617
      if (jwt) {
37✔
1618
        return await _request(this.fetch, 'GET', `${this.url}/user`, {
21✔
1619
          headers: this.headers,
1620
          jwt: jwt,
1621
          xform: _userResponse,
1622
        })
1623
      }
1624

1625
      return await this._useSession(async (result) => {
16✔
1626
        const { data, error } = result
16✔
1627
        if (error) {
16!
1628
          throw error
×
1629
        }
1630

1631
        // returns an error if there is no access_token or custom authorization header
1632
        if (!data.session?.access_token && !this.hasCustomAuthorizationHeader) {
16✔
1633
          return { data: { user: null }, error: new AuthSessionMissingError() }
6✔
1634
        }
1635

1636
        return await _request(this.fetch, 'GET', `${this.url}/user`, {
10✔
1637
          headers: this.headers,
1638
          jwt: data.session?.access_token ?? undefined,
60!
1639
          xform: _userResponse,
1640
        })
1641
      })
1642
    } catch (error) {
1643
      if (isAuthError(error)) {
10✔
1644
        if (isAuthSessionMissingError(error)) {
2!
1645
          // JWT contains a `session_id` which does not correspond to an active
1646
          // session in the database, indicating the user is signed out.
1647

1648
          await this._removeSession()
×
1649
          await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
×
1650
        }
1651

1652
        return { data: { user: null }, error }
2✔
1653
      }
1654

1655
      throw error
8✔
1656
    }
1657
  }
1658

1659
  /**
1660
   * Updates user data for a logged in user.
1661
   */
1662
  async updateUser(
1663
    attributes: UserAttributes,
1664
    options: {
10✔
1665
      emailRedirectTo?: string | undefined
1666
    } = {}
1667
  ): Promise<UserResponse> {
1668
    await this.initializePromise
10✔
1669

1670
    return await this._acquireLock(-1, async () => {
10✔
1671
      return await this._updateUser(attributes, options)
10✔
1672
    })
1673
  }
1674

1675
  protected async _updateUser(
1676
    attributes: UserAttributes,
1677
    options: {
×
1678
      emailRedirectTo?: string | undefined
1679
    } = {}
1680
  ): Promise<UserResponse> {
1681
    try {
10✔
1682
      return await this._useSession(async (result) => {
10✔
1683
        const { data: sessionData, error: sessionError } = result
10✔
1684
        if (sessionError) {
10!
1685
          throw sessionError
×
1686
        }
1687
        if (!sessionData.session) {
10✔
1688
          throw new AuthSessionMissingError()
2✔
1689
        }
1690
        const session: Session = sessionData.session
8✔
1691
        let codeChallenge: string | null = null
8✔
1692
        let codeChallengeMethod: string | null = null
8✔
1693
        if (this.flowType === 'pkce' && attributes.email != null) {
8✔
1694
          ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
2✔
1695
            this.storage,
1696
            this.storageKey
1697
          )
1698
        }
1699

1700
        const { data, error: userError } = await _request(this.fetch, 'PUT', `${this.url}/user`, {
8✔
1701
          headers: this.headers,
1702
          redirectTo: options?.emailRedirectTo,
24!
1703
          body: {
1704
            ...attributes,
1705
            code_challenge: codeChallenge,
1706
            code_challenge_method: codeChallengeMethod,
1707
          },
1708
          jwt: session.access_token,
1709
          xform: _userResponse,
1710
        })
1711
        if (userError) throw userError
8!
1712
        session.user = data.user as User
8✔
1713
        await this._saveSession(session)
8✔
1714
        await this._notifyAllSubscribers('USER_UPDATED', session)
8✔
1715
        return { data: { user: session.user }, error: null }
8✔
1716
      })
1717
    } catch (error) {
1718
      if (isAuthError(error)) {
2✔
1719
        return { data: { user: null }, error }
2✔
1720
      }
1721

1722
      throw error
×
1723
    }
1724
  }
1725

1726
  /**
1727
   * 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.
1728
   * If the refresh token or access token in the current session is invalid, an error will be thrown.
1729
   * @param currentSession The current session that minimally contains an access token and refresh token.
1730
   */
1731
  async setSession(currentSession: {
1732
    access_token: string
1733
    refresh_token: string
1734
  }): Promise<AuthResponse> {
1735
    await this.initializePromise
12✔
1736

1737
    return await this._acquireLock(-1, async () => {
12✔
1738
      return await this._setSession(currentSession)
12✔
1739
    })
1740
  }
1741

1742
  protected async _setSession(currentSession: {
1743
    access_token: string
1744
    refresh_token: string
1745
  }): Promise<AuthResponse> {
1746
    try {
12✔
1747
      if (!currentSession.access_token || !currentSession.refresh_token) {
12✔
1748
        throw new AuthSessionMissingError()
4✔
1749
      }
1750

1751
      const timeNow = Date.now() / 1000
8✔
1752
      let expiresAt = timeNow
8✔
1753
      let hasExpired = true
8✔
1754
      let session: Session | null = null
8✔
1755
      const { payload } = decodeJWT(currentSession.access_token)
8✔
1756
      if (payload.exp) {
4✔
1757
        expiresAt = payload.exp
4✔
1758
        hasExpired = expiresAt <= timeNow
4✔
1759
      }
1760

1761
      if (hasExpired) {
4✔
1762
        const { data: refreshedSession, error } = await this._callRefreshToken(
2✔
1763
          currentSession.refresh_token
1764
        )
1765
        if (error) {
2✔
1766
          return { data: { user: null, session: null }, error: error }
2✔
1767
        }
1768

1769
        if (!refreshedSession) {
×
1770
          return { data: { user: null, session: null }, error: null }
×
1771
        }
1772
        session = refreshedSession
×
1773
      } else {
1774
        const { data, error } = await this._getUser(currentSession.access_token)
2✔
1775
        if (error) {
2!
1776
          throw error
×
1777
        }
1778
        session = {
2✔
1779
          access_token: currentSession.access_token,
1780
          refresh_token: currentSession.refresh_token,
1781
          user: data.user,
1782
          token_type: 'bearer',
1783
          expires_in: expiresAt - timeNow,
1784
          expires_at: expiresAt,
1785
        }
1786
        await this._saveSession(session)
2✔
1787
        await this._notifyAllSubscribers('SIGNED_IN', session)
2✔
1788
      }
1789

1790
      return { data: { user: session.user, session }, error: null }
2✔
1791
    } catch (error) {
1792
      if (isAuthError(error)) {
8✔
1793
        return { data: { session: null, user: null }, error }
8✔
1794
      }
1795

1796
      throw error
×
1797
    }
1798
  }
1799

1800
  /**
1801
   * Returns a new session, regardless of expiry status.
1802
   * Takes in an optional current session. If not passed in, then refreshSession() will attempt to retrieve it from getSession().
1803
   * If the current session's refresh token is invalid, an error will be thrown.
1804
   * @param currentSession The current session. If passed in, it must contain a refresh token.
1805
   */
1806
  async refreshSession(currentSession?: { refresh_token: string }): Promise<AuthResponse> {
1807
    await this.initializePromise
8✔
1808

1809
    return await this._acquireLock(-1, async () => {
8✔
1810
      return await this._refreshSession(currentSession)
8✔
1811
    })
1812
  }
1813

1814
  protected async _refreshSession(currentSession?: {
1815
    refresh_token: string
1816
  }): Promise<AuthResponse> {
1817
    try {
8✔
1818
      return await this._useSession(async (result) => {
8✔
1819
        if (!currentSession) {
8✔
1820
          const { data, error } = result
4✔
1821
          if (error) {
4!
1822
            throw error
×
1823
          }
1824

1825
          currentSession = data.session ?? undefined
4✔
1826
        }
1827

1828
        if (!currentSession?.refresh_token) {
8✔
1829
          throw new AuthSessionMissingError()
2✔
1830
        }
1831

1832
        const { data: session, error } = await this._callRefreshToken(currentSession.refresh_token)
6✔
1833
        if (error) {
6✔
1834
          return { data: { user: null, session: null }, error: error }
2✔
1835
        }
1836

1837
        if (!session) {
4!
1838
          return { data: { user: null, session: null }, error: null }
×
1839
        }
1840

1841
        return { data: { user: session.user, session }, error: null }
4✔
1842
      })
1843
    } catch (error) {
1844
      if (isAuthError(error)) {
2✔
1845
        return { data: { user: null, session: null }, error }
2✔
1846
      }
1847

1848
      throw error
×
1849
    }
1850
  }
1851

1852
  /**
1853
   * Gets the session data from a URL string
1854
   */
1855
  private async _getSessionFromURL(
1856
    params: { [parameter: string]: string },
1857
    callbackUrlType: string
1858
  ): Promise<
1859
    | {
1860
        data: { session: Session; redirectType: string | null }
1861
        error: null
1862
      }
1863
    | { data: { session: null; redirectType: null }; error: AuthError }
1864
  > {
1865
    try {
12✔
1866
      if (!isBrowser()) throw new AuthImplicitGrantRedirectError('No browser detected.')
12✔
1867

1868
      // If there's an error in the URL, it doesn't matter what flow it is, we just return the error.
1869
      if (params.error || params.error_description || params.error_code) {
10✔
1870
        // The error class returned implies that the redirect is from an implicit grant flow
1871
        // but it could also be from a redirect error from a PKCE flow.
1872
        throw new AuthImplicitGrantRedirectError(
2✔
1873
          params.error_description || 'Error in URL with unspecified error_description',
2!
1874
          {
1875
            error: params.error || 'unspecified_error',
2!
1876
            code: params.error_code || 'unspecified_code',
4✔
1877
          }
1878
        )
1879
      }
1880

1881
      // Checks for mismatches between the flowType initialised in the client and the URL parameters
1882
      switch (callbackUrlType) {
8!
1883
        case 'implicit':
1884
          if (this.flowType === 'pkce') {
6!
1885
            throw new AuthPKCEGrantCodeExchangeError('Not a valid PKCE flow url.')
×
1886
          }
1887
          break
6✔
1888
        case 'pkce':
1889
          if (this.flowType === 'implicit') {
2✔
1890
            throw new AuthImplicitGrantRedirectError('Not a valid implicit grant flow url.')
2✔
1891
          }
1892
          break
×
1893
        default:
1894
        // there's no mismatch so we continue
1895
      }
1896

1897
      // Since this is a redirect for PKCE, we attempt to retrieve the code from the URL for the code exchange
1898
      if (callbackUrlType === 'pkce') {
6!
1899
        this._debug('#_initialize()', 'begin', 'is PKCE flow', true)
×
1900
        if (!params.code) throw new AuthPKCEGrantCodeExchangeError('No code detected.')
×
1901
        const { data, error } = await this._exchangeCodeForSession(params.code)
×
1902
        if (error) throw error
×
1903

1904
        const url = new URL(window.location.href)
×
1905
        url.searchParams.delete('code')
×
1906

1907
        window.history.replaceState(window.history.state, '', url.toString())
×
1908

1909
        return { data: { session: data.session, redirectType: null }, error: null }
×
1910
      }
1911

1912
      const {
1913
        provider_token,
1914
        provider_refresh_token,
1915
        access_token,
1916
        refresh_token,
1917
        expires_in,
1918
        expires_at,
1919
        token_type,
1920
      } = params
6✔
1921

1922
      if (!access_token || !expires_in || !refresh_token || !token_type) {
6!
1923
        throw new AuthImplicitGrantRedirectError('No session defined in URL')
×
1924
      }
1925

1926
      const timeNow = Math.round(Date.now() / 1000)
6✔
1927
      const expiresIn = parseInt(expires_in)
6✔
1928
      let expiresAt = timeNow + expiresIn
6✔
1929

1930
      if (expires_at) {
6✔
1931
        expiresAt = parseInt(expires_at)
2✔
1932
      }
1933

1934
      const actuallyExpiresIn = expiresAt - timeNow
6✔
1935
      if (actuallyExpiresIn * 1000 <= AUTO_REFRESH_TICK_DURATION_MS) {
6✔
1936
        console.warn(
2✔
1937
          `@supabase/gotrue-js: Session as retrieved from URL expires in ${actuallyExpiresIn}s, should have been closer to ${expiresIn}s`
1938
        )
1939
      }
1940

1941
      const issuedAt = expiresAt - expiresIn
6✔
1942
      if (timeNow - issuedAt >= 120) {
6✔
1943
        console.warn(
2✔
1944
          '@supabase/gotrue-js: Session as retrieved from URL was issued over 120s ago, URL could be stale',
1945
          issuedAt,
1946
          expiresAt,
1947
          timeNow
1948
        )
1949
      } else if (timeNow - issuedAt < 0) {
4!
1950
        console.warn(
×
1951
          '@supabase/gotrue-js: Session as retrieved from URL was issued in the future? Check the device clock for skew',
1952
          issuedAt,
1953
          expiresAt,
1954
          timeNow
1955
        )
1956
      }
1957

1958
      const { data, error } = await this._getUser(access_token)
6✔
1959
      if (error) throw error
2!
1960

1961
      const session: Session = {
2✔
1962
        provider_token,
1963
        provider_refresh_token,
1964
        access_token,
1965
        expires_in: expiresIn,
1966
        expires_at: expiresAt,
1967
        refresh_token,
1968
        token_type: token_type as 'bearer',
1969
        user: data.user,
1970
      }
1971

1972
      // Remove tokens from URL
1973
      window.location.hash = ''
2✔
1974
      this._debug('#_getSessionFromURL()', 'clearing window.location.hash')
2✔
1975

1976
      return { data: { session, redirectType: params.type }, error: null }
2✔
1977
    } catch (error) {
1978
      if (isAuthError(error)) {
10✔
1979
        return { data: { session: null, redirectType: null }, error }
6✔
1980
      }
1981

1982
      throw error
4✔
1983
    }
1984
  }
1985

1986
  /**
1987
   * 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)
1988
   */
1989
  private _isImplicitGrantCallback(params: { [parameter: string]: string }): boolean {
1990
    return Boolean(params.access_token || params.error_description)
72✔
1991
  }
1992

1993
  /**
1994
   * Checks if the current URL and backing storage contain parameters given by a PKCE flow
1995
   */
1996
  private async _isPKCECallback(params: { [parameter: string]: string }): Promise<boolean> {
1997
    const currentStorageContent = await getItemAsync(
64✔
1998
      this.storage,
1999
      `${this.storageKey}-code-verifier`
2000
    )
2001

2002
    return !!(params.code && currentStorageContent)
64✔
2003
  }
2004

2005
  /**
2006
   * 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.
2007
   *
2008
   * 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)`.
2009
   * 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.
2010
   *
2011
   * If using `others` scope, no `SIGNED_OUT` event is fired!
2012
   */
2013
  async signOut(options: SignOut = { scope: 'global' }): Promise<{ error: AuthError | null }> {
230✔
2014
    await this.initializePromise
232✔
2015

2016
    return await this._acquireLock(-1, async () => {
232✔
2017
      return await this._signOut(options)
232✔
2018
    })
2019
  }
2020

2021
  protected async _signOut(
2022
    { scope }: SignOut = { scope: 'global' }
×
2023
  ): Promise<{ error: AuthError | null }> {
2024
    return await this._useSession(async (result) => {
232✔
2025
      const { data, error: sessionError } = result
230✔
2026
      if (sessionError) {
230!
2027
        return { error: sessionError }
×
2028
      }
2029
      const accessToken = data.session?.access_token
230✔
2030
      if (accessToken) {
230✔
2031
        const { error } = await this.admin.signOut(accessToken, scope)
52✔
2032
        if (error) {
50✔
2033
          // ignore 404s since user might not exist anymore
2034
          // ignore 401s since an invalid or expired JWT should sign out the current session
2035
          if (
2!
2036
            !(
2037
              isAuthApiError(error) &&
8✔
2038
              (error.status === 404 || error.status === 401 || error.status === 403)
2039
            )
2040
          ) {
2041
            return { error }
×
2042
          }
2043
        }
2044
      }
2045
      if (scope !== 'others') {
228✔
2046
        await this._removeSession()
228✔
2047
        await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
228✔
2048
      }
2049
      return { error: null }
228✔
2050
    })
2051
  }
2052

2053
  /**
2054
   * Receive a notification every time an auth event happens.
2055
   * @param callback A callback function to be invoked when an auth event happens.
2056
   */
2057
  onAuthStateChange(
2058
    callback: (event: AuthChangeEvent, session: Session | null) => void | Promise<void>
2059
  ): {
2060
    data: { subscription: Subscription }
2061
  } {
2062
    const id: string = uuid()
10✔
2063
    const subscription: Subscription = {
10✔
2064
      id,
2065
      callback,
2066
      unsubscribe: () => {
2067
        this._debug('#unsubscribe()', 'state change callback with id removed', id)
10✔
2068

2069
        this.stateChangeEmitters.delete(id)
10✔
2070
      },
2071
    }
2072

2073
    this._debug('#onAuthStateChange()', 'registered callback with id', id)
10✔
2074

2075
    this.stateChangeEmitters.set(id, subscription)
10✔
2076
    ;(async () => {
10✔
2077
      await this.initializePromise
10✔
2078

2079
      await this._acquireLock(-1, async () => {
10✔
2080
        this._emitInitialSession(id)
10✔
2081
      })
2082
    })()
2083

2084
    return { data: { subscription } }
10✔
2085
  }
2086

2087
  private async _emitInitialSession(id: string): Promise<void> {
2088
    return await this._useSession(async (result) => {
10✔
2089
      try {
10✔
2090
        const {
2091
          data: { session },
2092
          error,
2093
        } = result
10✔
2094
        if (error) throw error
10!
2095

2096
        await this.stateChangeEmitters.get(id)?.callback('INITIAL_SESSION', session)
10✔
2097
        this._debug('INITIAL_SESSION', 'callback id', id, 'session', session)
10✔
2098
      } catch (err) {
2099
        await this.stateChangeEmitters.get(id)?.callback('INITIAL_SESSION', null)
×
2100
        this._debug('INITIAL_SESSION', 'callback id', id, 'error', err)
×
2101
        console.error(err)
×
2102
      }
2103
    })
2104
  }
2105

2106
  /**
2107
   * Sends a password reset request to an email address. This method supports the PKCE flow.
2108
   *
2109
   * @param email The email address of the user.
2110
   * @param options.redirectTo The URL to send the user to after they click the password reset link.
2111
   * @param options.captchaToken Verification token received when the user completes the captcha on the site.
2112
   */
2113
  async resetPasswordForEmail(
2114
    email: string,
2115
    options: {
×
2116
      redirectTo?: string
2117
      captchaToken?: string
2118
    } = {}
2119
  ): Promise<
2120
    | {
2121
        data: {}
2122
        error: null
2123
      }
2124
    | { data: null; error: AuthError }
2125
  > {
2126
    let codeChallenge: string | null = null
4✔
2127
    let codeChallengeMethod: string | null = null
4✔
2128

2129
    if (this.flowType === 'pkce') {
4!
2130
      ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
×
2131
        this.storage,
2132
        this.storageKey,
2133
        true // isPasswordRecovery
2134
      )
2135
    }
2136
    try {
4✔
2137
      return await _request(this.fetch, 'POST', `${this.url}/recover`, {
4✔
2138
        body: {
2139
          email,
2140
          code_challenge: codeChallenge,
2141
          code_challenge_method: codeChallengeMethod,
2142
          gotrue_meta_security: { captcha_token: options.captchaToken },
2143
        },
2144
        headers: this.headers,
2145
        redirectTo: options.redirectTo,
2146
      })
2147
    } catch (error) {
2148
      if (isAuthError(error)) {
×
2149
        return { data: null, error }
×
2150
      }
2151

2152
      throw error
×
2153
    }
2154
  }
2155

2156
  /**
2157
   * Gets all the identities linked to a user.
2158
   */
2159
  async getUserIdentities(): Promise<
2160
    | {
2161
        data: {
2162
          identities: UserIdentity[]
2163
        }
2164
        error: null
2165
      }
2166
    | { data: null; error: AuthError }
2167
  > {
2168
    try {
6✔
2169
      const { data, error } = await this.getUser()
6✔
2170
      if (error) throw error
6✔
2171
      return { data: { identities: data.user.identities ?? [] }, error: null }
4!
2172
    } catch (error) {
2173
      if (isAuthError(error)) {
2✔
2174
        return { data: null, error }
2✔
2175
      }
2176
      throw error
×
2177
    }
2178
  }
2179

2180
  /**
2181
   * Links an oauth identity to an existing user.
2182
   * This method supports the PKCE flow.
2183
   */
2184
  async linkIdentity(credentials: SignInWithOAuthCredentials): Promise<OAuthResponse>
2185

2186
  /**
2187
   * Links an OIDC identity to an existing user.
2188
   */
2189
  async linkIdentity(credentials: SignInWithIdTokenCredentials): Promise<AuthTokenResponse>
2190

2191
  async linkIdentity(credentials: any): Promise<any> {
2192
    if ('token' in credentials) {
4!
NEW
2193
      return this.linkIdentityIdToken(credentials)
×
2194
    }
2195

2196
    return this.linkIdentityOAuth(credentials)
4✔
2197
  }
2198

2199
  private async linkIdentityOAuth(credentials: SignInWithOAuthCredentials): Promise<OAuthResponse> {
2200
    try {
4✔
2201
      const { data, error } = await this._useSession(async (result) => {
4✔
2202
        const { data, error } = result
4✔
2203
        if (error) throw error
4!
2204
        const url: string = await this._getUrlForProvider(
4✔
2205
          `${this.url}/user/identities/authorize`,
2206
          credentials.provider,
2207
          {
2208
            redirectTo: credentials.options?.redirectTo,
12!
2209
            scopes: credentials.options?.scopes,
12!
2210
            queryParams: credentials.options?.queryParams,
12!
2211
            skipBrowserRedirect: true,
2212
          }
2213
        )
2214
        return await _request(this.fetch, 'GET', url, {
4✔
2215
          headers: this.headers,
2216
          jwt: data.session?.access_token ?? undefined,
22✔
2217
        })
2218
      })
2219
      if (error) throw error
2!
2220
      if (isBrowser() && !credentials.options?.skipBrowserRedirect) {
2!
2221
        window.location.assign(data?.url)
2!
2222
      }
2223
      return { data: { provider: credentials.provider, url: data?.url }, error: null }
2!
2224
    } catch (error) {
2225
      if (isAuthError(error)) {
2✔
2226
        return { data: { provider: credentials.provider, url: null }, error }
2✔
2227
      }
2228
      throw error
×
2229
    }
2230
  }
2231

2232
  private async linkIdentityIdToken(
2233
    credentials: SignInWithIdTokenCredentials
2234
  ): Promise<AuthTokenResponse> {
NEW
2235
    return await this._useSession(async (result) => {
×
NEW
2236
      try {
×
2237
        const {
2238
          error: sessionError,
2239
          data: { session },
NEW
2240
        } = result
×
NEW
2241
        if (sessionError) throw sessionError
×
2242

NEW
2243
        const { options, provider, token, access_token, nonce } = credentials
×
2244

NEW
2245
        const res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=id_token`, {
×
2246
          headers: this.headers,
2247
          jwt: session?.access_token ?? undefined,
×
2248
          body: {
2249
            provider,
2250
            id_token: token,
2251
            access_token,
2252
            nonce,
2253
            link_identity: true,
2254
            gotrue_meta_security: { captcha_token: options?.captchaToken },
×
2255
          },
2256
          xform: _sessionResponse,
2257
        })
2258

NEW
2259
        const { data, error } = res
×
NEW
2260
        if (error) {
×
NEW
2261
          return { data: { user: null, session: null }, error }
×
NEW
2262
        } else if (!data || !data.session || !data.user) {
×
NEW
2263
          return {
×
2264
            data: { user: null, session: null },
2265
            error: new AuthInvalidTokenResponseError(),
2266
          }
2267
        }
NEW
2268
        if (data.session) {
×
NEW
2269
          await this._saveSession(data.session)
×
NEW
2270
          await this._notifyAllSubscribers('USER_UPDATED', data.session)
×
2271
        }
NEW
2272
        return { data, error }
×
2273
      } catch (error) {
NEW
2274
        if (isAuthError(error)) {
×
NEW
2275
          return { data: { user: null, session: null }, error }
×
2276
        }
NEW
2277
        throw error
×
2278
      }
2279
    })
2280
  }
2281

2282
  /**
2283
   * 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.
2284
   */
2285
  async unlinkIdentity(identity: UserIdentity): Promise<
2286
    | {
2287
        data: {}
2288
        error: null
2289
      }
2290
    | { data: null; error: AuthError }
2291
  > {
2292
    try {
2✔
2293
      return await this._useSession(async (result) => {
2✔
2294
        const { data, error } = result
2✔
2295
        if (error) {
2!
2296
          throw error
×
2297
        }
2298
        return await _request(
2✔
2299
          this.fetch,
2300
          'DELETE',
2301
          `${this.url}/user/identities/${identity.identity_id}`,
2302
          {
2303
            headers: this.headers,
2304
            jwt: data.session?.access_token ?? undefined,
12!
2305
          }
2306
        )
2307
      })
2308
    } catch (error) {
2309
      if (isAuthError(error)) {
2✔
2310
        return { data: null, error }
2✔
2311
      }
2312
      throw error
×
2313
    }
2314
  }
2315

2316
  /**
2317
   * Generates a new JWT.
2318
   * @param refreshToken A valid refresh token that was returned on login.
2319
   */
2320
  private async _refreshAccessToken(refreshToken: string): Promise<AuthResponse> {
2321
    const debugName = `#_refreshAccessToken(${refreshToken.substring(0, 5)}...)`
28✔
2322
    this._debug(debugName, 'begin')
28✔
2323

2324
    try {
28✔
2325
      const startedAt = Date.now()
28✔
2326

2327
      // will attempt to refresh the token with exponential backoff
2328
      return await retryable(
28✔
2329
        async (attempt) => {
2330
          if (attempt > 0) {
28!
2331
            await sleep(200 * Math.pow(2, attempt - 1)) // 200, 400, 800, ...
×
2332
          }
2333

2334
          this._debug(debugName, 'refreshing attempt', attempt)
28✔
2335

2336
          return await _request(this.fetch, 'POST', `${this.url}/token?grant_type=refresh_token`, {
28✔
2337
            body: { refresh_token: refreshToken },
2338
            headers: this.headers,
2339
            xform: _sessionResponse,
2340
          })
2341
        },
2342
        (attempt, error) => {
2343
          const nextBackOffInterval = 200 * Math.pow(2, attempt)
28✔
2344
          return (
28✔
2345
            error &&
38!
2346
            isAuthRetryableFetchError(error) &&
2347
            // retryable only if the request can be sent before the backoff overflows the tick duration
2348
            Date.now() + nextBackOffInterval - startedAt < AUTO_REFRESH_TICK_DURATION_MS
2349
          )
2350
        }
2351
      )
2352
    } catch (error) {
2353
      this._debug(debugName, 'error', error)
10✔
2354

2355
      if (isAuthError(error)) {
10✔
2356
        return { data: { session: null, user: null }, error }
10✔
2357
      }
2358
      throw error
×
2359
    } finally {
2360
      this._debug(debugName, 'end')
28✔
2361
    }
2362
  }
2363

2364
  private _isValidSession(maybeSession: unknown): maybeSession is Session {
2365
    const isValidSession =
2366
      typeof maybeSession === 'object' &&
253✔
2367
      maybeSession !== null &&
2368
      'access_token' in maybeSession &&
2369
      'refresh_token' in maybeSession &&
2370
      'expires_at' in maybeSession
2371

2372
    return isValidSession
253✔
2373
  }
2374

2375
  private async _handleProviderSignIn(
2376
    provider: Provider,
2377
    options: {
2378
      redirectTo?: string
2379
      scopes?: string
2380
      queryParams?: { [key: string]: string }
2381
      skipBrowserRedirect?: boolean
2382
    }
2383
  ) {
2384
    const url: string = await this._getUrlForProvider(`${this.url}/authorize`, provider, {
20✔
2385
      redirectTo: options.redirectTo,
2386
      scopes: options.scopes,
2387
      queryParams: options.queryParams,
2388
    })
2389

2390
    this._debug('#_handleProviderSignIn()', 'provider', provider, 'options', options, 'url', url)
20✔
2391

2392
    // try to open on the browser
2393
    if (isBrowser() && !options.skipBrowserRedirect) {
20✔
2394
      window.location.assign(url)
6✔
2395
    }
2396

2397
    return { data: { provider, url }, error: null }
20✔
2398
  }
2399

2400
  /**
2401
   * Recovers the session from LocalStorage and refreshes the token
2402
   * Note: this method is async to accommodate for AsyncStorage e.g. in React native.
2403
   */
2404
  private async _recoverAndRefresh() {
2405
    const debugName = '#_recoverAndRefresh()'
66✔
2406
    this._debug(debugName, 'begin')
66✔
2407

2408
    try {
66✔
2409
      const currentSession = (await getItemAsync(this.storage, this.storageKey)) as Session | null
66✔
2410

2411
      if (currentSession && this.userStorage) {
66✔
2412
        let maybeUser: { user: User | null } | null = (await getItemAsync(
6✔
2413
          this.userStorage,
2414
          this.storageKey + '-user'
2415
        )) as any
2416

2417
        if (!this.storage.isServer && Object.is(this.storage, this.userStorage) && !maybeUser) {
6!
2418
          // storage and userStorage are the same storage medium, for example
2419
          // window.localStorage if userStorage does not have the user from
2420
          // storage stored, store it first thereby migrating the user object
2421
          // from storage -> userStorage
2422

2423
          maybeUser = { user: currentSession.user }
×
2424
          await setItemAsync(this.userStorage, this.storageKey + '-user', maybeUser)
×
2425
        }
2426

2427
        currentSession.user = maybeUser?.user ?? userNotAvailableProxy()
6✔
2428
      } else if (currentSession && !currentSession.user) {
60✔
2429
        // user storage is not set, let's check if it was previously enabled so
2430
        // we bring back the storage as it should be
2431

2432
        if (!currentSession.user) {
2✔
2433
          // test if userStorage was previously enabled and the storage medium was the same, to move the user back under the same key
2434
          const separateUser: { user: User | null } | null = (await getItemAsync(
2✔
2435
            this.storage,
2436
            this.storageKey + '-user'
2437
          )) as any
2438

2439
          if (separateUser && separateUser?.user) {
2!
2440
            currentSession.user = separateUser.user
×
2441

2442
            await removeItemAsync(this.storage, this.storageKey + '-user')
×
2443
            await setItemAsync(this.storage, this.storageKey, currentSession)
×
2444
          } else {
2445
            currentSession.user = userNotAvailableProxy()
2✔
2446
          }
2447
        }
2448
      }
2449

2450
      this._debug(debugName, 'session from storage', currentSession)
64✔
2451

2452
      if (!this._isValidSession(currentSession)) {
64✔
2453
        this._debug(debugName, 'session is not valid')
48✔
2454
        if (currentSession !== null) {
48✔
2455
          await this._removeSession()
6✔
2456
        }
2457

2458
        return
48✔
2459
      }
2460

2461
      const expiresWithMargin =
2462
        (currentSession.expires_at ?? Infinity) * 1000 - Date.now() < EXPIRY_MARGIN_MS
16✔
2463

2464
      this._debug(
16✔
2465
        debugName,
2466
        `session has${expiresWithMargin ? '' : ' not'} expired with margin of ${EXPIRY_MARGIN_MS}s`
16✔
2467
      )
2468

2469
      if (expiresWithMargin) {
16✔
2470
        if (this.autoRefreshToken && currentSession.refresh_token) {
4✔
2471
          const { error } = await this._callRefreshToken(currentSession.refresh_token)
4✔
2472

2473
          if (error) {
4✔
2474
            console.error(error)
2✔
2475

2476
            if (!isAuthRetryableFetchError(error)) {
2✔
2477
              this._debug(
2✔
2478
                debugName,
2479
                'refresh failed with a non-retryable error, removing the session',
2480
                error
2481
              )
2482
              await this._removeSession()
2✔
2483
            }
2484
          }
2485
        }
2486
      } else if (
12✔
2487
        currentSession.user &&
24✔
2488
        (currentSession.user as any).__isUserNotAvailableProxy === true
2489
      ) {
2490
        // If we have a proxy user, try to get the real user data
2491
        try {
6✔
2492
          const { data, error: userError } = await this._getUser(currentSession.access_token)
6✔
2493

2494
          if (!userError && data?.user) {
2!
2495
            currentSession.user = data.user
2✔
2496
            await this._saveSession(currentSession)
2✔
2497
            await this._notifyAllSubscribers('SIGNED_IN', currentSession)
2✔
2498
          } else {
2499
            this._debug(debugName, 'could not get user data, skipping SIGNED_IN notification')
×
2500
          }
2501
        } catch (getUserError) {
2502
          console.error('Error getting user data:', getUserError)
4✔
2503
          this._debug(
4✔
2504
            debugName,
2505
            'error getting user data, skipping SIGNED_IN notification',
2506
            getUserError
2507
          )
2508
        }
2509
      } else {
2510
        // no need to persist currentSession again, as we just loaded it from
2511
        // local storage; persisting it again may overwrite a value saved by
2512
        // another client with access to the same local storage
2513
        await this._notifyAllSubscribers('SIGNED_IN', currentSession)
6✔
2514
      }
2515
    } catch (err) {
2516
      this._debug(debugName, 'error', err)
2✔
2517

2518
      console.error(err)
2✔
2519
      return
2✔
2520
    } finally {
2521
      this._debug(debugName, 'end')
66✔
2522
    }
2523
  }
2524

2525
  private async _callRefreshToken(refreshToken: string): Promise<CallRefreshTokenResult> {
2526
    if (!refreshToken) {
38!
2527
      throw new AuthSessionMissingError()
×
2528
    }
2529

2530
    // refreshing is already in progress
2531
    if (this.refreshingDeferred) {
38✔
2532
      return this.refreshingDeferred.promise
6✔
2533
    }
2534

2535
    const debugName = `#_callRefreshToken(${refreshToken.substring(0, 5)}...)`
32✔
2536

2537
    this._debug(debugName, 'begin')
32✔
2538

2539
    try {
32✔
2540
      this.refreshingDeferred = new Deferred<CallRefreshTokenResult>()
32✔
2541

2542
      const { data, error } = await this._refreshAccessToken(refreshToken)
32✔
2543
      if (error) throw error
30✔
2544
      if (!data.session) throw new AuthSessionMissingError()
18✔
2545

2546
      await this._saveSession(data.session)
16✔
2547
      await this._notifyAllSubscribers('TOKEN_REFRESHED', data.session)
16✔
2548

2549
      const result = { data: data.session, error: null }
16✔
2550

2551
      this.refreshingDeferred.resolve(result)
16✔
2552

2553
      return result
16✔
2554
    } catch (error) {
2555
      this._debug(debugName, 'error', error)
16✔
2556

2557
      if (isAuthError(error)) {
16✔
2558
        const result = { data: null, error }
14✔
2559

2560
        if (!isAuthRetryableFetchError(error)) {
14✔
2561
          await this._removeSession()
14✔
2562
        }
2563

2564
        this.refreshingDeferred?.resolve(result)
14!
2565

2566
        return result
14✔
2567
      }
2568

2569
      this.refreshingDeferred?.reject(error)
2!
2570
      throw error
2✔
2571
    } finally {
2572
      this.refreshingDeferred = null
32✔
2573
      this._debug(debugName, 'end')
32✔
2574
    }
2575
  }
2576

2577
  private async _notifyAllSubscribers(
2578
    event: AuthChangeEvent,
2579
    session: Session | null,
2580
    broadcast = true
443✔
2581
  ) {
2582
    const debugName = `#_notifyAllSubscribers(${event})`
445✔
2583
    this._debug(debugName, 'begin', session, `broadcast = ${broadcast}`)
445✔
2584

2585
    try {
445✔
2586
      if (this.broadcastChannel && broadcast) {
445!
2587
        this.broadcastChannel.postMessage({ event, session })
×
2588
      }
2589

2590
      const errors: any[] = []
445✔
2591
      const promises = Array.from(this.stateChangeEmitters.values()).map(async (x) => {
445✔
2592
        try {
10✔
2593
          await x.callback(event, session)
10✔
2594
        } catch (e: any) {
2595
          errors.push(e)
×
2596
        }
2597
      })
2598

2599
      await Promise.all(promises)
445✔
2600

2601
      if (errors.length > 0) {
445!
2602
        for (let i = 0; i < errors.length; i += 1) {
×
2603
          console.error(errors[i])
×
2604
        }
2605

2606
        throw errors[0]
×
2607
      }
2608
    } finally {
2609
      this._debug(debugName, 'end')
445✔
2610
    }
2611
  }
2612

2613
  /**
2614
   * set currentSession and currentUser
2615
   * process to _startAutoRefreshToken if possible
2616
   */
2617
  private async _saveSession(session: Session) {
2618
    this._debug('#_saveSession()', session)
177✔
2619
    // _saveSession is always called whenever a new session has been acquired
2620
    // so we can safely suppress the warning returned by future getSession calls
2621
    this.suppressGetSessionWarning = true
177✔
2622

2623
    // Create a shallow copy to work with, to avoid mutating the original session object if it's used elsewhere
2624
    const sessionToProcess = { ...session }
177✔
2625

2626
    const userIsProxy =
2627
      sessionToProcess.user && (sessionToProcess.user as any).__isUserNotAvailableProxy === true
177✔
2628
    if (this.userStorage) {
177!
2629
      if (!userIsProxy && sessionToProcess.user) {
×
2630
        // If it's a real user object, save it to userStorage.
2631
        await setItemAsync(this.userStorage, this.storageKey + '-user', {
×
2632
          user: sessionToProcess.user,
2633
        })
2634
      } else if (userIsProxy) {
×
2635
        // If it's the proxy, it means user was not found in userStorage.
2636
        // We should ensure no stale user data for this key exists in userStorage if we were to save null,
2637
        // or simply not save the proxy. For now, we don't save the proxy here.
2638
        // If there's a need to clear userStorage if user becomes proxy, that logic would go here.
2639
      }
2640

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

2646
      const clonedMainSessionData = deepClone(mainSessionData)
×
2647
      await setItemAsync(this.storage, this.storageKey, clonedMainSessionData)
×
2648
    } else {
2649
      // No userStorage is configured.
2650
      // In this case, session.user should ideally not be a proxy.
2651
      // If it were, structuredClone would fail. This implies an issue elsewhere if user is a proxy here
2652
      const clonedSession = deepClone(sessionToProcess) // sessionToProcess still has its original user property
177✔
2653
      await setItemAsync(this.storage, this.storageKey, clonedSession)
177✔
2654
    }
2655
  }
2656

2657
  private async _removeSession() {
2658
    this._debug('#_removeSession()')
268✔
2659

2660
    await removeItemAsync(this.storage, this.storageKey)
268✔
2661
    await removeItemAsync(this.storage, this.storageKey + '-code-verifier')
266✔
2662
    await removeItemAsync(this.storage, this.storageKey + '-user')
266✔
2663

2664
    if (this.userStorage) {
266✔
2665
      await removeItemAsync(this.userStorage, this.storageKey + '-user')
2✔
2666
    }
2667

2668
    await this._notifyAllSubscribers('SIGNED_OUT', null)
266✔
2669
  }
2670

2671
  /**
2672
   * Removes any registered visibilitychange callback.
2673
   *
2674
   * {@see #startAutoRefresh}
2675
   * {@see #stopAutoRefresh}
2676
   */
2677
  private _removeVisibilityChangedCallback() {
2678
    this._debug('#_removeVisibilityChangedCallback()')
34✔
2679

2680
    const callback = this.visibilityChangedCallback
34✔
2681
    this.visibilityChangedCallback = null
34✔
2682

2683
    try {
34✔
2684
      if (callback && isBrowser() && window?.removeEventListener) {
34!
2685
        window.removeEventListener('visibilitychange', callback)
2✔
2686
      }
2687
    } catch (e) {
2688
      console.error('removing visibilitychange callback failed', e)
×
2689
    }
2690
  }
2691

2692
  /**
2693
   * This is the private implementation of {@link #startAutoRefresh}. Use this
2694
   * within the library.
2695
   */
2696
  private async _startAutoRefresh() {
2697
    await this._stopAutoRefresh()
32✔
2698

2699
    this._debug('#_startAutoRefresh()')
32✔
2700

2701
    const ticker = setInterval(() => this._autoRefreshTokenTick(), AUTO_REFRESH_TICK_DURATION_MS)
32✔
2702
    this.autoRefreshTicker = ticker
32✔
2703

2704
    if (ticker && typeof ticker === 'object' && typeof ticker.unref === 'function') {
32✔
2705
      // ticker is a NodeJS Timeout object that has an `unref` method
2706
      // https://nodejs.org/api/timers.html#timeoutunref
2707
      // When auto refresh is used in NodeJS (like for testing) the
2708
      // `setInterval` is preventing the process from being marked as
2709
      // finished and tests run endlessly. This can be prevented by calling
2710
      // `unref()` on the returned object.
2711
      ticker.unref()
24✔
2712
      // @ts-expect-error TS has no context of Deno
2713
    } else if (typeof Deno !== 'undefined' && typeof Deno.unrefTimer === 'function') {
8!
2714
      // similar like for NodeJS, but with the Deno API
2715
      // https://deno.land/api@latest?unstable&s=Deno.unrefTimer
2716
      // @ts-expect-error TS has no context of Deno
2717
      Deno.unrefTimer(ticker)
×
2718
    }
2719

2720
    // run the tick immediately, but in the next pass of the event loop so that
2721
    // #_initialize can be allowed to complete without recursively waiting on
2722
    // itself
2723
    setTimeout(async () => {
32✔
2724
      await this.initializePromise
32✔
2725
      await this._autoRefreshTokenTick()
32✔
2726
    }, 0)
2727
  }
2728

2729
  /**
2730
   * This is the private implementation of {@link #stopAutoRefresh}. Use this
2731
   * within the library.
2732
   */
2733
  private async _stopAutoRefresh() {
2734
    this._debug('#_stopAutoRefresh()')
40✔
2735

2736
    const ticker = this.autoRefreshTicker
40✔
2737
    this.autoRefreshTicker = null
40✔
2738

2739
    if (ticker) {
40✔
2740
      clearInterval(ticker)
10✔
2741
    }
2742
  }
2743

2744
  /**
2745
   * Starts an auto-refresh process in the background. The session is checked
2746
   * every few seconds. Close to the time of expiration a process is started to
2747
   * refresh the session. If refreshing fails it will be retried for as long as
2748
   * necessary.
2749
   *
2750
   * If you set the {@link GoTrueClientOptions#autoRefreshToken} you don't need
2751
   * to call this function, it will be called for you.
2752
   *
2753
   * On browsers the refresh process works only when the tab/window is in the
2754
   * foreground to conserve resources as well as prevent race conditions and
2755
   * flooding auth with requests. If you call this method any managed
2756
   * visibility change callback will be removed and you must manage visibility
2757
   * changes on your own.
2758
   *
2759
   * On non-browser platforms the refresh process works *continuously* in the
2760
   * background, which may not be desirable. You should hook into your
2761
   * platform's foreground indication mechanism and call these methods
2762
   * appropriately to conserve resources.
2763
   *
2764
   * {@see #stopAutoRefresh}
2765
   */
2766
  async startAutoRefresh() {
2767
    this._removeVisibilityChangedCallback()
26✔
2768
    await this._startAutoRefresh()
26✔
2769
  }
2770

2771
  /**
2772
   * Stops an active auto refresh process running in the background (if any).
2773
   *
2774
   * If you call this method any managed visibility change callback will be
2775
   * removed and you must manage visibility changes on your own.
2776
   *
2777
   * See {@link #startAutoRefresh} for more details.
2778
   */
2779
  async stopAutoRefresh() {
2780
    this._removeVisibilityChangedCallback()
8✔
2781
    await this._stopAutoRefresh()
8✔
2782
  }
2783

2784
  /**
2785
   * Runs the auto refresh token tick.
2786
   */
2787
  private async _autoRefreshTokenTick() {
2788
    this._debug('#_autoRefreshTokenTick()', 'begin')
32✔
2789

2790
    try {
32✔
2791
      await this._acquireLock(0, async () => {
32✔
2792
        try {
32✔
2793
          const now = Date.now()
32✔
2794

2795
          try {
32✔
2796
            return await this._useSession(async (result) => {
32✔
2797
              const {
2798
                data: { session },
2799
              } = result
32✔
2800

2801
              if (!session || !session.refresh_token || !session.expires_at) {
32✔
2802
                this._debug('#_autoRefreshTokenTick()', 'no session')
20✔
2803
                return
20✔
2804
              }
2805

2806
              // session will expire in this many ticks (or has already expired if <= 0)
2807
              const expiresInTicks = Math.floor(
12✔
2808
                (session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS
2809
              )
2810

2811
              this._debug(
12✔
2812
                '#_autoRefreshTokenTick()',
2813
                `access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks`
2814
              )
2815

2816
              if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) {
12!
2817
                await this._callRefreshToken(session.refresh_token)
×
2818
              }
2819
            })
2820
          } catch (e: any) {
2821
            console.error(
×
2822
              'Auto refresh tick failed with error. This is likely a transient error.',
2823
              e
2824
            )
2825
          }
2826
        } finally {
2827
          this._debug('#_autoRefreshTokenTick()', 'end')
32✔
2828
        }
2829
      })
2830
    } catch (e: any) {
2831
      if (e.isAcquireTimeout || e instanceof LockAcquireTimeoutError) {
×
2832
        this._debug('auto refresh token tick lock not available')
×
2833
      } else {
2834
        throw e
×
2835
      }
2836
    }
2837
  }
2838

2839
  /**
2840
   * Registers callbacks on the browser / platform, which in-turn run
2841
   * algorithms when the browser window/tab are in foreground. On non-browser
2842
   * platforms it assumes always foreground.
2843
   */
2844
  private async _handleVisibilityChange() {
2845
    this._debug('#_handleVisibilityChange()')
176✔
2846

2847
    if (!isBrowser() || !window?.addEventListener) {
176!
2848
      if (this.autoRefreshToken) {
106✔
2849
        // in non-browser environments the refresh token ticker runs always
2850
        this.startAutoRefresh()
20✔
2851
      }
2852

2853
      return false
106✔
2854
    }
2855

2856
    try {
70✔
2857
      this.visibilityChangedCallback = async () => await this._onVisibilityChanged(false)
70✔
2858

2859
      window?.addEventListener('visibilitychange', this.visibilityChangedCallback)
70!
2860

2861
      // now immediately call the visbility changed callback to setup with the
2862
      // current visbility state
2863
      await this._onVisibilityChanged(true) // initial call
68✔
2864
    } catch (error) {
2865
      console.error('_handleVisibilityChange', error)
2✔
2866
    }
2867
  }
2868

2869
  /**
2870
   * Callback registered with `window.addEventListener('visibilitychange')`.
2871
   */
2872
  private async _onVisibilityChanged(calledFromInitialize: boolean) {
2873
    const methodName = `#_onVisibilityChanged(${calledFromInitialize})`
68✔
2874
    this._debug(methodName, 'visibilityState', document.visibilityState)
68✔
2875

2876
    if (document.visibilityState === 'visible') {
68!
2877
      if (this.autoRefreshToken) {
68✔
2878
        // in browser environments the refresh token ticker runs only on focused tabs
2879
        // which prevents race conditions
2880
        this._startAutoRefresh()
6✔
2881
      }
2882

2883
      if (!calledFromInitialize) {
68!
2884
        // called when the visibility has changed, i.e. the browser
2885
        // transitioned from hidden -> visible so we need to see if the session
2886
        // should be recovered immediately... but to do that we need to acquire
2887
        // the lock first asynchronously
2888
        await this.initializePromise
×
2889

2890
        await this._acquireLock(-1, async () => {
×
2891
          if (document.visibilityState !== 'visible') {
×
2892
            this._debug(
×
2893
              methodName,
2894
              'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting'
2895
            )
2896

2897
            // visibility has changed while waiting for the lock, abort
2898
            return
×
2899
          }
2900

2901
          // recover the session
2902
          await this._recoverAndRefresh()
×
2903
        })
2904
      }
2905
    } else if (document.visibilityState === 'hidden') {
×
2906
      if (this.autoRefreshToken) {
×
2907
        this._stopAutoRefresh()
×
2908
      }
2909
    }
2910
  }
2911

2912
  /**
2913
   * Generates the relevant login URL for a third-party provider.
2914
   * @param options.redirectTo A URL or mobile address to send the user to after they are confirmed.
2915
   * @param options.scopes A space-separated list of scopes granted to the OAuth application.
2916
   * @param options.queryParams An object of key-value pairs containing query parameters granted to the OAuth application.
2917
   */
2918
  private async _getUrlForProvider(
2919
    url: string,
2920
    provider: Provider,
2921
    options: {
2922
      redirectTo?: string
2923
      scopes?: string
2924
      queryParams?: { [key: string]: string }
2925
      skipBrowserRedirect?: boolean
2926
    }
2927
  ) {
2928
    const urlParams: string[] = [`provider=${encodeURIComponent(provider)}`]
28✔
2929
    if (options?.redirectTo) {
28!
2930
      urlParams.push(`redirect_to=${encodeURIComponent(options.redirectTo)}`)
14✔
2931
    }
2932
    if (options?.scopes) {
28!
2933
      urlParams.push(`scopes=${encodeURIComponent(options.scopes)}`)
6✔
2934
    }
2935
    if (this.flowType === 'pkce') {
28✔
2936
      const [codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
4✔
2937
        this.storage,
2938
        this.storageKey
2939
      )
2940

2941
      const flowParams = new URLSearchParams({
4✔
2942
        code_challenge: `${encodeURIComponent(codeChallenge)}`,
2943
        code_challenge_method: `${encodeURIComponent(codeChallengeMethod)}`,
2944
      })
2945
      urlParams.push(flowParams.toString())
4✔
2946
    }
2947
    if (options?.queryParams) {
28!
2948
      const query = new URLSearchParams(options.queryParams)
4✔
2949
      urlParams.push(query.toString())
4✔
2950
    }
2951
    if (options?.skipBrowserRedirect) {
28!
2952
      urlParams.push(`skip_http_redirect=${options.skipBrowserRedirect}`)
4✔
2953
    }
2954

2955
    return `${url}?${urlParams.join('&')}`
28✔
2956
  }
2957

2958
  private async _unenroll(params: MFAUnenrollParams): Promise<AuthMFAUnenrollResponse> {
2959
    try {
4✔
2960
      return await this._useSession(async (result) => {
4✔
2961
        const { data: sessionData, error: sessionError } = result
4✔
2962
        if (sessionError) {
4!
2963
          return { data: null, error: sessionError }
×
2964
        }
2965

2966
        return await _request(this.fetch, 'DELETE', `${this.url}/factors/${params.factorId}`, {
4✔
2967
          headers: this.headers,
2968
          jwt: sessionData?.session?.access_token,
22!
2969
        })
2970
      })
2971
    } catch (error) {
2972
      if (isAuthError(error)) {
2✔
2973
        return { data: null, error }
2✔
2974
      }
2975
      throw error
×
2976
    }
2977
  }
2978

2979
  /**
2980
   * {@see GoTrueMFAApi#enroll}
2981
   */
2982
  private async _enroll(params: MFAEnrollTOTPParams): Promise<AuthMFAEnrollTOTPResponse>
2983
  private async _enroll(params: MFAEnrollPhoneParams): Promise<AuthMFAEnrollPhoneResponse>
2984
  private async _enroll(params: MFAEnrollParams): Promise<AuthMFAEnrollResponse> {
2985
    try {
20✔
2986
      return await this._useSession(async (result) => {
20✔
2987
        const { data: sessionData, error: sessionError } = result
20✔
2988
        if (sessionError) {
20!
2989
          return { data: null, error: sessionError }
×
2990
        }
2991

2992
        const body = {
20✔
2993
          friendly_name: params.friendlyName,
2994
          factor_type: params.factorType,
2995
          ...(params.factorType === 'phone' ? { phone: params.phone } : { issuer: params.issuer }),
20✔
2996
        }
2997

2998
        const { data, error } = await _request(this.fetch, 'POST', `${this.url}/factors`, {
20✔
2999
          body,
3000
          headers: this.headers,
3001
          jwt: sessionData?.session?.access_token,
116!
3002
        })
3003

3004
        if (error) {
16!
3005
          return { data: null, error }
×
3006
        }
3007

3008
        if (params.factorType === 'totp' && data?.totp?.qr_code) {
16!
3009
          data.totp.qr_code = `data:image/svg+xml;utf-8,${data.totp.qr_code}`
14✔
3010
        }
3011

3012
        return { data, error: null }
16✔
3013
      })
3014
    } catch (error) {
3015
      if (isAuthError(error)) {
4✔
3016
        return { data: null, error }
4✔
3017
      }
3018
      throw error
×
3019
    }
3020
  }
3021

3022
  /**
3023
   * {@see GoTrueMFAApi#verify}
3024
   */
3025
  private async _verify(params: MFAVerifyParams): Promise<AuthMFAVerifyResponse> {
3026
    return this._acquireLock(-1, async () => {
6✔
3027
      try {
6✔
3028
        return await this._useSession(async (result) => {
6✔
3029
          const { data: sessionData, error: sessionError } = result
6✔
3030
          if (sessionError) {
6!
3031
            return { data: null, error: sessionError }
×
3032
          }
3033

3034
          const { data, error } = await _request(
6✔
3035
            this.fetch,
3036
            'POST',
3037
            `${this.url}/factors/${params.factorId}/verify`,
3038
            {
3039
              body: { code: params.code, challenge_id: params.challengeId },
3040
              headers: this.headers,
3041
              jwt: sessionData?.session?.access_token,
34!
3042
            }
3043
          )
3044
          if (error) {
×
3045
            return { data: null, error }
×
3046
          }
3047

3048
          await this._saveSession({
×
3049
            expires_at: Math.round(Date.now() / 1000) + data.expires_in,
3050
            ...data,
3051
          })
3052
          await this._notifyAllSubscribers('MFA_CHALLENGE_VERIFIED', data)
×
3053

3054
          return { data, error }
×
3055
        })
3056
      } catch (error) {
3057
        if (isAuthError(error)) {
6✔
3058
          return { data: null, error }
6✔
3059
        }
3060
        throw error
×
3061
      }
3062
    })
3063
  }
3064

3065
  /**
3066
   * {@see GoTrueMFAApi#challenge}
3067
   */
3068
  private async _challenge<T extends FactorType>(
3069
    params: MFAChallengeParams
3070
  ): Promise<AuthMFAChallengeResponse<T>> {
3071
    return this._acquireLock(-1, async () => {
8✔
3072
      try {
8✔
3073
        return await this._useSession(async (result) => {
8✔
3074
          const { data: sessionData, error: sessionError } = result
8✔
3075
          if (sessionError) {
8!
3076
            return { data: null, error: sessionError }
×
3077
          }
3078

3079
          return await _request(
8✔
3080
            this.fetch,
3081
            'POST',
3082
            `${this.url}/factors/${params.factorId}/challenge`,
3083
            {
3084
              body: 'channel' in params ? { channel: params.channel } : {},
8!
3085
              headers: this.headers,
3086
              jwt: sessionData?.session?.access_token,
46!
3087
            }
3088
          )
3089
        })
3090
      } catch (error) {
3091
        if (isAuthError(error)) {
2✔
3092
          return { data: null, error }
2✔
3093
        }
3094
        throw error
×
3095
      }
3096
    })
3097
  }
3098

3099
  /**
3100
   * {@see GoTrueMFAApi#challengeAndVerify}
3101
   */
3102
  private async _challengeAndVerify(
3103
    params: MFAChallengeAndVerifyParams
3104
  ): Promise<AuthMFAVerifyResponse> {
3105
    // both _challenge and _verify independently acquire the lock, so no need
3106
    // to acquire it here
3107

3108
    const { data: challengeData, error: challengeError } = await this._challenge({
2✔
3109
      factorId: params.factorId,
3110
    })
3111
    if (challengeError) {
2!
3112
      return { data: null, error: challengeError }
×
3113
    }
3114

3115
    return await this._verify({
2✔
3116
      factorId: params.factorId,
3117
      challengeId: challengeData.id,
3118
      code: params.code,
3119
    })
3120
  }
3121

3122
  /**
3123
   * {@see GoTrueMFAApi#listFactors}
3124
   */
3125
  private async _listFactors(): Promise<AuthMFAListFactorsResponse> {
3126
    // use #getUser instead of #_getUser as the former acquires a lock
3127
    const {
3128
      data: { user },
3129
      error: userError,
3130
    } = await this.getUser()
4✔
3131
    if (userError) {
4!
3132
      return { data: null, error: userError }
×
3133
    }
3134

3135
    const data: AuthMFAListFactorsResponse['data'] = {
4✔
3136
      all: [],
3137
      phone: [],
3138
      totp: [],
3139
    }
3140

3141
    for (const factor of user?.factors ?? []) {
4!
3142
      data.all.push(factor)
8✔
3143
      if (factor.status === 'verified') {
8✔
3144
        ;(data[factor.factor_type] as typeof factor[]).push(factor)
4✔
3145
      }
3146
    }
3147

3148
    return {
4✔
3149
      data,
3150
      error: null,
3151
    }
3152
  }
3153

3154
  /**
3155
   * {@see GoTrueMFAApi#getAuthenticatorAssuranceLevel}
3156
   */
3157
  private async _getAuthenticatorAssuranceLevel(): Promise<AuthMFAGetAuthenticatorAssuranceLevelResponse> {
3158
    return this._acquireLock(-1, async () => {
2✔
3159
      return await this._useSession(async (result) => {
2✔
3160
        const {
3161
          data: { session },
3162
          error: sessionError,
3163
        } = result
2✔
3164
        if (sessionError) {
2!
3165
          return { data: null, error: sessionError }
×
3166
        }
3167
        if (!session) {
2!
3168
          return {
×
3169
            data: { currentLevel: null, nextLevel: null, currentAuthenticationMethods: [] },
3170
            error: null,
3171
          }
3172
        }
3173

3174
        const { payload } = decodeJWT(session.access_token)
2✔
3175

3176
        let currentLevel: AuthenticatorAssuranceLevels | null = null
2✔
3177

3178
        if (payload.aal) {
2✔
3179
          currentLevel = payload.aal
2✔
3180
        }
3181

3182
        let nextLevel: AuthenticatorAssuranceLevels | null = currentLevel
2✔
3183

3184
        const verifiedFactors =
3185
          session.user.factors?.filter((factor: Factor) => factor.status === 'verified') ?? []
2!
3186

3187
        if (verifiedFactors.length > 0) {
2!
3188
          nextLevel = 'aal2'
×
3189
        }
3190

3191
        const currentAuthenticationMethods = payload.amr || []
2!
3192

3193
        return { data: { currentLevel, nextLevel, currentAuthenticationMethods }, error: null }
2✔
3194
      })
3195
    })
3196
  }
3197

3198
  private async fetchJwk(kid: string, jwks: { keys: JWK[] } = { keys: [] }): Promise<JWK | null> {
10✔
3199
    // try fetching from the supplied jwks
3200
    let jwk = jwks.keys.find((key) => key.kid === kid)
10✔
3201
    if (jwk) {
10!
3202
      return jwk
×
3203
    }
3204

3205
    const now = Date.now()
10✔
3206

3207
    // try fetching from cache
3208
    jwk = this.jwks.keys.find((key) => key.kid === kid)
10✔
3209

3210
    // jwk exists and jwks isn't stale
3211
    if (jwk && this.jwks_cached_at + JWKS_TTL > now) {
10✔
3212
      return jwk
3✔
3213
    }
3214
    // jwk isn't cached in memory so we need to fetch it from the well-known endpoint
3215
    const { data, error } = await _request(this.fetch, 'GET', `${this.url}/.well-known/jwks.json`, {
7✔
3216
      headers: this.headers,
3217
    })
3218
    if (error) {
7!
3219
      throw error
×
3220
    }
3221
    if (!data.keys || data.keys.length === 0) {
7!
3222
      return null
×
3223
    }
3224

3225
    this.jwks = data
7✔
3226
    this.jwks_cached_at = now
7✔
3227

3228
    // Find the signing key
3229
    jwk = data.keys.find((key: any) => key.kid === kid)
7✔
3230
    if (!jwk) {
7!
3231
      return null
×
3232
    }
3233
    return jwk
7✔
3234
  }
3235

3236
  /**
3237
   * Extracts the JWT claims present in the access token by first verifying the
3238
   * JWT against the server's JSON Web Key Set endpoint
3239
   * `/.well-known/jwks.json` which is often cached, resulting in significantly
3240
   * faster responses. Prefer this method over {@link #getUser} which always
3241
   * sends a request to the Auth server for each JWT.
3242
   *
3243
   * If the project is not using an asymmetric JWT signing key (like ECC or
3244
   * RSA) it always sends a request to the Auth server (similar to {@link
3245
   * #getUser}) to verify the JWT.
3246
   *
3247
   * @param jwt An optional specific JWT you wish to verify, not the one you
3248
   *            can obtain from {@link #getSession}.
3249
   * @param options Various additional options that allow you to customize the
3250
   *                behavior of this method.
3251
   */
3252
  async getClaims(
3253
    jwt?: string,
3254
    options: {
15✔
3255
      /**
3256
       * @deprecated Please use options.jwks instead.
3257
       */
3258
      keys?: JWK[]
3259

3260
      /** If set to `true` the `exp` claim will not be validated against the current time. */
3261
      allowExpired?: boolean
3262

3263
      /** If set, this JSON Web Key Set is going to have precedence over the cached value available on the server. */
3264
      jwks?: { keys: JWK[] }
3265
    } = {}
3266
  ): Promise<
3267
    | {
3268
        data: { claims: JwtPayload; header: JwtHeader; signature: Uint8Array }
3269
        error: null
3270
      }
3271
    | { data: null; error: AuthError }
3272
    | { data: null; error: null }
3273
  > {
3274
    try {
15✔
3275
      let token = jwt
15✔
3276
      if (!token) {
15✔
3277
        const { data, error } = await this.getSession()
15✔
3278
        if (error || !data.session) {
15✔
3279
          return { data: null, error }
2✔
3280
        }
3281
        token = data.session.access_token
13✔
3282
      }
3283

3284
      const {
3285
        header,
3286
        payload,
3287
        signature,
3288
        raw: { header: rawHeader, payload: rawPayload },
3289
      } = decodeJWT(token)
13✔
3290

3291
      if (!options?.allowExpired) {
11!
3292
        // Reject expired JWTs should only happen if jwt argument was passed
3293
        validateExp(payload.exp)
11✔
3294
      }
3295

3296
      const signingKey =
3297
        !header.alg ||
9✔
3298
        header.alg.startsWith('HS') ||
3299
        !header.kid ||
3300
        !('crypto' in globalThis && 'subtle' in globalThis.crypto)
5✔
3301
          ? null
3302
          : await this.fetchJwk(header.kid, options?.keys ? { keys: options.keys } : options?.jwks)
14!
3303

3304
      // If symmetric algorithm or WebCrypto API is unavailable, fallback to getUser()
3305
      if (!signingKey) {
9✔
3306
        const { error } = await this.getUser(token)
7✔
3307
        if (error) {
7✔
3308
          throw error
2✔
3309
        }
3310
        // getUser succeeds so the claims in the JWT can be trusted
3311
        return {
5✔
3312
          data: {
3313
            claims: payload,
3314
            header,
3315
            signature,
3316
          },
3317
          error: null,
3318
        }
3319
      }
3320

3321
      const algorithm = getAlgorithm(header.alg)
2✔
3322

3323
      // Convert JWK to CryptoKey
3324
      const publicKey = await crypto.subtle.importKey('jwk', signingKey, algorithm, true, [
2✔
3325
        'verify',
3326
      ])
3327

3328
      // Verify the signature
3329
      const isValid = await crypto.subtle.verify(
2✔
3330
        algorithm,
3331
        publicKey,
3332
        signature,
3333
        stringToUint8Array(`${rawHeader}.${rawPayload}`)
3334
      )
3335

3336
      if (!isValid) {
2✔
3337
        throw new AuthInvalidJwtError('Invalid JWT signature')
1✔
3338
      }
3339

3340
      // If verification succeeds, decode and return claims
3341
      return {
1✔
3342
        data: {
3343
          claims: payload,
3344
          header,
3345
          signature,
3346
        },
3347
        error: null,
3348
      }
3349
    } catch (error) {
3350
      if (isAuthError(error)) {
7✔
3351
        return { data: null, error }
5✔
3352
      }
3353
      throw error
2✔
3354
    }
3355
  }
3356
}
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