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

supabase / auth-js / 17576420578

09 Sep 2025 08:21AM UTC coverage: 80.822% (+0.01%) from 80.811%
17576420578

Pull #1116

github

web-flow
Merge 5ac41397d into dacd705b4
Pull Request #1116: refactor: streamline types and enhance type safety for MFA methods

1081 of 1443 branches covered (74.91%)

Branch coverage included in aggregate %.

13 of 13 new or added lines in 2 files covered. (100.0%)

136 existing lines in 1 file now uncovered.

1456 of 1696 relevant lines covered (85.85%)

172.86 hits per line

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

76.18
/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 type {
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
} from './lib/types'
118
import { stringToUint8Array, bytesToBase64URL } from './lib/base64url'
10✔
119
import {
10✔
120
  fromHex,
121
  getAddress,
122
  Hex,
123
  toHex,
124
  createSiweMessage,
125
  SiweMessage,
126
} from './lib/web3/ethereum'
127

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

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

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

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

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

162
  private instanceID: number
163

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

178
  protected flowType: AuthFlowType
179

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

335
    this.initialize()
182✔
336
  }
337

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

346
    return this
7,239✔
347
  }
348

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

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

365
    return await this.initializePromise
182✔
366
  }
367

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

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

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

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

410
          return { error }
4✔
411
        }
412

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

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

423
        await this._saveSession(session)
2✔
424

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

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

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

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

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

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

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

UNCOV
486
      throw error
×
487
    }
488
  }
489

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

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

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

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

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

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

UNCOV
565
      throw error
×
566
    }
567
  }
568

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

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

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

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

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

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

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

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

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

702
      let resolvedWallet: EthereumWallet
703

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

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

UNCOV
717
        if (
×
718
          'ethereum' in windowAny &&
×
719
          typeof windowAny.ethereum === 'object' &&
720
          'request' in windowAny.ethereum &&
721
          typeof windowAny.ethereum.request === 'function'
722
        ) {
UNCOV
723
          resolvedWallet = windowAny.ethereum
×
724
        } else {
UNCOV
725
          throw new Error(
×
726
            `@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.`
727
          )
728
        }
729
      }
730

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

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

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

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

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

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

UNCOV
775
      message = createSiweMessage(siweMessage)
×
776

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

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

UNCOV
821
      throw error
×
822
    }
823
  }
824

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

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

835
      let resolvedWallet: SolanaWallet
836

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

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

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

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

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

871
          ...options?.signInWithSolana,
×
872

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

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

881
        let outputToProcess: any
882

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

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

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

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

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

UNCOV
967
        signature = maybeSignature
×
968
      }
969
    }
970

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

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

UNCOV
1009
      throw error
×
1010
    }
1011
  }
1012

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

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

UNCOV
1057
      throw error
×
1058
    }
1059
  }
1060

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

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

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

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

UNCOV
1166
      throw error
×
1167
    }
1168
  }
1169

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

UNCOV
1191
      if (error) {
×
1192
        throw error
×
1193
      }
1194

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

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

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

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

UNCOV
1216
      throw error
×
1217
    }
1218
  }
1219

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

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

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

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

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

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

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

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

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

1365
    return result
71✔
1366
  }
1367

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

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

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

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

1395
        return result
2✔
1396
      }
1397

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

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

1404
          const result = fn()
595✔
1405

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

1416
          await result
595✔
1417

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

1422
            await Promise.all(waitOn)
587✔
1423

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1605
    await this.initializePromise
16✔
1606

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

1611
    return result
16✔
1612
  }
1613

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

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

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

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

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

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

1654
      throw error
8✔
1655
    }
1656
  }
1657

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

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

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

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

UNCOV
1721
      throw error
×
1722
    }
1723
  }
1724

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

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

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

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

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

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

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

UNCOV
1795
      throw error
×
1796
    }
1797
  }
1798

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

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

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

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

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

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

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

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

UNCOV
1847
      throw error
×
1848
    }
1849
  }
1850

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1981
      throw error
4✔
1982
    }
1983
  }
1984

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
2151
      throw error
×
2152
    }
2153
  }
2154

2155
  /**
2156
   * Gets all the identities linked to a user.
2157
   */
2158
  async getUserIdentities(): Promise<
2159
    | {
2160
        data: {
2161
          identities: UserIdentity[]
2162
        }
2163
        error: null
2164
      }
2165
    | { data: null; error: AuthError }
2166
  > {
2167
    try {
6✔
2168
      const { data, error } = await this.getUser()
6✔
2169
      if (error) throw error
6✔
2170
      return { data: { identities: data.user.identities ?? [] }, error: null }
4!
2171
    } catch (error) {
2172
      if (isAuthError(error)) {
2✔
2173
        return { data: null, error }
2✔
2174
      }
UNCOV
2175
      throw error
×
2176
    }
2177
  }
2178
  /**
2179
   * Links an oauth identity to an existing user.
2180
   * This method supports the PKCE flow.
2181
   */
2182
  async linkIdentity(credentials: SignInWithOAuthCredentials): Promise<OAuthResponse> {
2183
    try {
4✔
2184
      const { data, error } = await this._useSession(async (result) => {
4✔
2185
        const { data, error } = result
4✔
2186
        if (error) throw error
4!
2187
        const url: string = await this._getUrlForProvider(
4✔
2188
          `${this.url}/user/identities/authorize`,
2189
          credentials.provider,
2190
          {
2191
            redirectTo: credentials.options?.redirectTo,
12!
2192
            scopes: credentials.options?.scopes,
12!
2193
            queryParams: credentials.options?.queryParams,
12!
2194
            skipBrowserRedirect: true,
2195
          }
2196
        )
2197
        return await _request(this.fetch, 'GET', url, {
4✔
2198
          headers: this.headers,
2199
          jwt: data.session?.access_token ?? undefined,
22✔
2200
        })
2201
      })
2202
      if (error) throw error
2!
2203
      if (isBrowser() && !credentials.options?.skipBrowserRedirect) {
2!
2204
        window.location.assign(data?.url)
2!
2205
      }
2206
      return { data: { provider: credentials.provider, url: data?.url }, error: null }
2!
2207
    } catch (error) {
2208
      if (isAuthError(error)) {
2✔
2209
        return { data: { provider: credentials.provider, url: null }, error }
2✔
2210
      }
UNCOV
2211
      throw error
×
2212
    }
2213
  }
2214

2215
  /**
2216
   * 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.
2217
   */
2218
  async unlinkIdentity(identity: UserIdentity): Promise<
2219
    | {
2220
        data: {}
2221
        error: null
2222
      }
2223
    | { data: null; error: AuthError }
2224
  > {
2225
    try {
2✔
2226
      return await this._useSession(async (result) => {
2✔
2227
        const { data, error } = result
2✔
2228
        if (error) {
2!
UNCOV
2229
          throw error
×
2230
        }
2231
        return await _request(
2✔
2232
          this.fetch,
2233
          'DELETE',
2234
          `${this.url}/user/identities/${identity.identity_id}`,
2235
          {
2236
            headers: this.headers,
2237
            jwt: data.session?.access_token ?? undefined,
12!
2238
          }
2239
        )
2240
      })
2241
    } catch (error) {
2242
      if (isAuthError(error)) {
2✔
2243
        return { data: null, error }
2✔
2244
      }
UNCOV
2245
      throw error
×
2246
    }
2247
  }
2248

2249
  /**
2250
   * Generates a new JWT.
2251
   * @param refreshToken A valid refresh token that was returned on login.
2252
   */
2253
  private async _refreshAccessToken(refreshToken: string): Promise<AuthResponse> {
2254
    const debugName = `#_refreshAccessToken(${refreshToken.substring(0, 5)}...)`
28✔
2255
    this._debug(debugName, 'begin')
28✔
2256

2257
    try {
28✔
2258
      const startedAt = Date.now()
28✔
2259

2260
      // will attempt to refresh the token with exponential backoff
2261
      return await retryable(
28✔
2262
        async (attempt) => {
2263
          if (attempt > 0) {
28!
UNCOV
2264
            await sleep(200 * Math.pow(2, attempt - 1)) // 200, 400, 800, ...
×
2265
          }
2266

2267
          this._debug(debugName, 'refreshing attempt', attempt)
28✔
2268

2269
          return await _request(this.fetch, 'POST', `${this.url}/token?grant_type=refresh_token`, {
28✔
2270
            body: { refresh_token: refreshToken },
2271
            headers: this.headers,
2272
            xform: _sessionResponse,
2273
          })
2274
        },
2275
        (attempt, error) => {
2276
          const nextBackOffInterval = 200 * Math.pow(2, attempt)
28✔
2277
          return (
28✔
2278
            error &&
38!
2279
            isAuthRetryableFetchError(error) &&
2280
            // retryable only if the request can be sent before the backoff overflows the tick duration
2281
            Date.now() + nextBackOffInterval - startedAt < AUTO_REFRESH_TICK_DURATION_MS
2282
          )
2283
        }
2284
      )
2285
    } catch (error) {
2286
      this._debug(debugName, 'error', error)
10✔
2287

2288
      if (isAuthError(error)) {
10✔
2289
        return { data: { session: null, user: null }, error }
10✔
2290
      }
UNCOV
2291
      throw error
×
2292
    } finally {
2293
      this._debug(debugName, 'end')
28✔
2294
    }
2295
  }
2296

2297
  private _isValidSession(maybeSession: unknown): maybeSession is Session {
2298
    const isValidSession =
2299
      typeof maybeSession === 'object' &&
253✔
2300
      maybeSession !== null &&
2301
      'access_token' in maybeSession &&
2302
      'refresh_token' in maybeSession &&
2303
      'expires_at' in maybeSession
2304

2305
    return isValidSession
253✔
2306
  }
2307

2308
  private async _handleProviderSignIn(
2309
    provider: Provider,
2310
    options: {
2311
      redirectTo?: string
2312
      scopes?: string
2313
      queryParams?: { [key: string]: string }
2314
      skipBrowserRedirect?: boolean
2315
    }
2316
  ) {
2317
    const url: string = await this._getUrlForProvider(`${this.url}/authorize`, provider, {
20✔
2318
      redirectTo: options.redirectTo,
2319
      scopes: options.scopes,
2320
      queryParams: options.queryParams,
2321
    })
2322

2323
    this._debug('#_handleProviderSignIn()', 'provider', provider, 'options', options, 'url', url)
20✔
2324

2325
    // try to open on the browser
2326
    if (isBrowser() && !options.skipBrowserRedirect) {
20✔
2327
      window.location.assign(url)
6✔
2328
    }
2329

2330
    return { data: { provider, url }, error: null }
20✔
2331
  }
2332

2333
  /**
2334
   * Recovers the session from LocalStorage and refreshes the token
2335
   * Note: this method is async to accommodate for AsyncStorage e.g. in React native.
2336
   */
2337
  private async _recoverAndRefresh() {
2338
    const debugName = '#_recoverAndRefresh()'
66✔
2339
    this._debug(debugName, 'begin')
66✔
2340

2341
    try {
66✔
2342
      const currentSession = (await getItemAsync(this.storage, this.storageKey)) as Session | null
66✔
2343

2344
      if (currentSession && this.userStorage) {
66✔
2345
        let maybeUser: { user: User | null } | null = (await getItemAsync(
6✔
2346
          this.userStorage,
2347
          this.storageKey + '-user'
2348
        )) as any
2349

2350
        if (!this.storage.isServer && Object.is(this.storage, this.userStorage) && !maybeUser) {
6!
2351
          // storage and userStorage are the same storage medium, for example
2352
          // window.localStorage if userStorage does not have the user from
2353
          // storage stored, store it first thereby migrating the user object
2354
          // from storage -> userStorage
2355

UNCOV
2356
          maybeUser = { user: currentSession.user }
×
2357
          await setItemAsync(this.userStorage, this.storageKey + '-user', maybeUser)
×
2358
        }
2359

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

2365
        if (!currentSession.user) {
2✔
2366
          // test if userStorage was previously enabled and the storage medium was the same, to move the user back under the same key
2367
          const separateUser: { user: User | null } | null = (await getItemAsync(
2✔
2368
            this.storage,
2369
            this.storageKey + '-user'
2370
          )) as any
2371

2372
          if (separateUser && separateUser?.user) {
2!
UNCOV
2373
            currentSession.user = separateUser.user
×
2374

UNCOV
2375
            await removeItemAsync(this.storage, this.storageKey + '-user')
×
2376
            await setItemAsync(this.storage, this.storageKey, currentSession)
×
2377
          } else {
2378
            currentSession.user = userNotAvailableProxy()
2✔
2379
          }
2380
        }
2381
      }
2382

2383
      this._debug(debugName, 'session from storage', currentSession)
64✔
2384

2385
      if (!this._isValidSession(currentSession)) {
64✔
2386
        this._debug(debugName, 'session is not valid')
48✔
2387
        if (currentSession !== null) {
48✔
2388
          await this._removeSession()
6✔
2389
        }
2390

2391
        return
48✔
2392
      }
2393

2394
      const expiresWithMargin =
2395
        (currentSession.expires_at ?? Infinity) * 1000 - Date.now() < EXPIRY_MARGIN_MS
16✔
2396

2397
      this._debug(
16✔
2398
        debugName,
2399
        `session has${expiresWithMargin ? '' : ' not'} expired with margin of ${EXPIRY_MARGIN_MS}s`
16✔
2400
      )
2401

2402
      if (expiresWithMargin) {
16✔
2403
        if (this.autoRefreshToken && currentSession.refresh_token) {
4✔
2404
          const { error } = await this._callRefreshToken(currentSession.refresh_token)
4✔
2405

2406
          if (error) {
4✔
2407
            console.error(error)
2✔
2408

2409
            if (!isAuthRetryableFetchError(error)) {
2✔
2410
              this._debug(
2✔
2411
                debugName,
2412
                'refresh failed with a non-retryable error, removing the session',
2413
                error
2414
              )
2415
              await this._removeSession()
2✔
2416
            }
2417
          }
2418
        }
2419
      } else if (
12✔
2420
        currentSession.user &&
24✔
2421
        (currentSession.user as any).__isUserNotAvailableProxy === true
2422
      ) {
2423
        // If we have a proxy user, try to get the real user data
2424
        try {
6✔
2425
          const { data, error: userError } = await this._getUser(currentSession.access_token)
6✔
2426

2427
          if (!userError && data?.user) {
2!
2428
            currentSession.user = data.user
2✔
2429
            await this._saveSession(currentSession)
2✔
2430
            await this._notifyAllSubscribers('SIGNED_IN', currentSession)
2✔
2431
          } else {
UNCOV
2432
            this._debug(debugName, 'could not get user data, skipping SIGNED_IN notification')
×
2433
          }
2434
        } catch (getUserError) {
2435
          console.error('Error getting user data:', getUserError)
4✔
2436
          this._debug(
4✔
2437
            debugName,
2438
            'error getting user data, skipping SIGNED_IN notification',
2439
            getUserError
2440
          )
2441
        }
2442
      } else {
2443
        // no need to persist currentSession again, as we just loaded it from
2444
        // local storage; persisting it again may overwrite a value saved by
2445
        // another client with access to the same local storage
2446
        await this._notifyAllSubscribers('SIGNED_IN', currentSession)
6✔
2447
      }
2448
    } catch (err) {
2449
      this._debug(debugName, 'error', err)
2✔
2450

2451
      console.error(err)
2✔
2452
      return
2✔
2453
    } finally {
2454
      this._debug(debugName, 'end')
66✔
2455
    }
2456
  }
2457

2458
  private async _callRefreshToken(refreshToken: string): Promise<CallRefreshTokenResult> {
2459
    if (!refreshToken) {
38!
UNCOV
2460
      throw new AuthSessionMissingError()
×
2461
    }
2462

2463
    // refreshing is already in progress
2464
    if (this.refreshingDeferred) {
38✔
2465
      return this.refreshingDeferred.promise
6✔
2466
    }
2467

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

2470
    this._debug(debugName, 'begin')
32✔
2471

2472
    try {
32✔
2473
      this.refreshingDeferred = new Deferred<CallRefreshTokenResult>()
32✔
2474

2475
      const { data, error } = await this._refreshAccessToken(refreshToken)
32✔
2476
      if (error) throw error
30✔
2477
      if (!data.session) throw new AuthSessionMissingError()
18✔
2478

2479
      await this._saveSession(data.session)
16✔
2480
      await this._notifyAllSubscribers('TOKEN_REFRESHED', data.session)
16✔
2481

2482
      const result = { data: data.session, error: null }
16✔
2483

2484
      this.refreshingDeferred.resolve(result)
16✔
2485

2486
      return result
16✔
2487
    } catch (error) {
2488
      this._debug(debugName, 'error', error)
16✔
2489

2490
      if (isAuthError(error)) {
16✔
2491
        const result = { data: null, error }
14✔
2492

2493
        if (!isAuthRetryableFetchError(error)) {
14✔
2494
          await this._removeSession()
14✔
2495
        }
2496

2497
        this.refreshingDeferred?.resolve(result)
14!
2498

2499
        return result
14✔
2500
      }
2501

2502
      this.refreshingDeferred?.reject(error)
2!
2503
      throw error
2✔
2504
    } finally {
2505
      this.refreshingDeferred = null
32✔
2506
      this._debug(debugName, 'end')
32✔
2507
    }
2508
  }
2509

2510
  private async _notifyAllSubscribers(
2511
    event: AuthChangeEvent,
2512
    session: Session | null,
2513
    broadcast = true
443✔
2514
  ) {
2515
    const debugName = `#_notifyAllSubscribers(${event})`
445✔
2516
    this._debug(debugName, 'begin', session, `broadcast = ${broadcast}`)
445✔
2517

2518
    try {
445✔
2519
      if (this.broadcastChannel && broadcast) {
445!
UNCOV
2520
        this.broadcastChannel.postMessage({ event, session })
×
2521
      }
2522

2523
      const errors: any[] = []
445✔
2524
      const promises = Array.from(this.stateChangeEmitters.values()).map(async (x) => {
445✔
2525
        try {
10✔
2526
          await x.callback(event, session)
10✔
2527
        } catch (e: any) {
UNCOV
2528
          errors.push(e)
×
2529
        }
2530
      })
2531

2532
      await Promise.all(promises)
445✔
2533

2534
      if (errors.length > 0) {
445!
UNCOV
2535
        for (let i = 0; i < errors.length; i += 1) {
×
2536
          console.error(errors[i])
×
2537
        }
2538

UNCOV
2539
        throw errors[0]
×
2540
      }
2541
    } finally {
2542
      this._debug(debugName, 'end')
445✔
2543
    }
2544
  }
2545

2546
  /**
2547
   * set currentSession and currentUser
2548
   * process to _startAutoRefreshToken if possible
2549
   */
2550
  private async _saveSession(session: Session) {
2551
    this._debug('#_saveSession()', session)
177✔
2552
    // _saveSession is always called whenever a new session has been acquired
2553
    // so we can safely suppress the warning returned by future getSession calls
2554
    this.suppressGetSessionWarning = true
177✔
2555

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

2559
    const userIsProxy =
2560
      sessionToProcess.user && (sessionToProcess.user as any).__isUserNotAvailableProxy === true
177✔
2561
    if (this.userStorage) {
177!
UNCOV
2562
      if (!userIsProxy && sessionToProcess.user) {
×
2563
        // If it's a real user object, save it to userStorage.
UNCOV
2564
        await setItemAsync(this.userStorage, this.storageKey + '-user', {
×
2565
          user: sessionToProcess.user,
2566
        })
UNCOV
2567
      } else if (userIsProxy) {
×
2568
        // If it's the proxy, it means user was not found in userStorage.
2569
        // We should ensure no stale user data for this key exists in userStorage if we were to save null,
2570
        // or simply not save the proxy. For now, we don't save the proxy here.
2571
        // If there's a need to clear userStorage if user becomes proxy, that logic would go here.
2572
      }
2573

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

UNCOV
2579
      const clonedMainSessionData = deepClone(mainSessionData)
×
2580
      await setItemAsync(this.storage, this.storageKey, clonedMainSessionData)
×
2581
    } else {
2582
      // No userStorage is configured.
2583
      // In this case, session.user should ideally not be a proxy.
2584
      // If it were, structuredClone would fail. This implies an issue elsewhere if user is a proxy here
2585
      const clonedSession = deepClone(sessionToProcess) // sessionToProcess still has its original user property
177✔
2586
      await setItemAsync(this.storage, this.storageKey, clonedSession)
177✔
2587
    }
2588
  }
2589

2590
  private async _removeSession() {
2591
    this._debug('#_removeSession()')
268✔
2592

2593
    await removeItemAsync(this.storage, this.storageKey)
268✔
2594
    await removeItemAsync(this.storage, this.storageKey + '-code-verifier')
266✔
2595
    await removeItemAsync(this.storage, this.storageKey + '-user')
266✔
2596

2597
    if (this.userStorage) {
266✔
2598
      await removeItemAsync(this.userStorage, this.storageKey + '-user')
2✔
2599
    }
2600

2601
    await this._notifyAllSubscribers('SIGNED_OUT', null)
266✔
2602
  }
2603

2604
  /**
2605
   * Removes any registered visibilitychange callback.
2606
   *
2607
   * {@see #startAutoRefresh}
2608
   * {@see #stopAutoRefresh}
2609
   */
2610
  private _removeVisibilityChangedCallback() {
2611
    this._debug('#_removeVisibilityChangedCallback()')
34✔
2612

2613
    const callback = this.visibilityChangedCallback
34✔
2614
    this.visibilityChangedCallback = null
34✔
2615

2616
    try {
34✔
2617
      if (callback && isBrowser() && window?.removeEventListener) {
34!
2618
        window.removeEventListener('visibilitychange', callback)
2✔
2619
      }
2620
    } catch (e) {
UNCOV
2621
      console.error('removing visibilitychange callback failed', e)
×
2622
    }
2623
  }
2624

2625
  /**
2626
   * This is the private implementation of {@link #startAutoRefresh}. Use this
2627
   * within the library.
2628
   */
2629
  private async _startAutoRefresh() {
2630
    await this._stopAutoRefresh()
32✔
2631

2632
    this._debug('#_startAutoRefresh()')
32✔
2633

2634
    const ticker = setInterval(() => this._autoRefreshTokenTick(), AUTO_REFRESH_TICK_DURATION_MS)
32✔
2635
    this.autoRefreshTicker = ticker
32✔
2636

2637
    if (ticker && typeof ticker === 'object' && typeof ticker.unref === 'function') {
32✔
2638
      // ticker is a NodeJS Timeout object that has an `unref` method
2639
      // https://nodejs.org/api/timers.html#timeoutunref
2640
      // When auto refresh is used in NodeJS (like for testing) the
2641
      // `setInterval` is preventing the process from being marked as
2642
      // finished and tests run endlessly. This can be prevented by calling
2643
      // `unref()` on the returned object.
2644
      ticker.unref()
24✔
2645
      // @ts-expect-error TS has no context of Deno
2646
    } else if (typeof Deno !== 'undefined' && typeof Deno.unrefTimer === 'function') {
8!
2647
      // similar like for NodeJS, but with the Deno API
2648
      // https://deno.land/api@latest?unstable&s=Deno.unrefTimer
2649
      // @ts-expect-error TS has no context of Deno
UNCOV
2650
      Deno.unrefTimer(ticker)
×
2651
    }
2652

2653
    // run the tick immediately, but in the next pass of the event loop so that
2654
    // #_initialize can be allowed to complete without recursively waiting on
2655
    // itself
2656
    setTimeout(async () => {
32✔
2657
      await this.initializePromise
32✔
2658
      await this._autoRefreshTokenTick()
32✔
2659
    }, 0)
2660
  }
2661

2662
  /**
2663
   * This is the private implementation of {@link #stopAutoRefresh}. Use this
2664
   * within the library.
2665
   */
2666
  private async _stopAutoRefresh() {
2667
    this._debug('#_stopAutoRefresh()')
40✔
2668

2669
    const ticker = this.autoRefreshTicker
40✔
2670
    this.autoRefreshTicker = null
40✔
2671

2672
    if (ticker) {
40✔
2673
      clearInterval(ticker)
10✔
2674
    }
2675
  }
2676

2677
  /**
2678
   * Starts an auto-refresh process in the background. The session is checked
2679
   * every few seconds. Close to the time of expiration a process is started to
2680
   * refresh the session. If refreshing fails it will be retried for as long as
2681
   * necessary.
2682
   *
2683
   * If you set the {@link GoTrueClientOptions#autoRefreshToken} you don't need
2684
   * to call this function, it will be called for you.
2685
   *
2686
   * On browsers the refresh process works only when the tab/window is in the
2687
   * foreground to conserve resources as well as prevent race conditions and
2688
   * flooding auth with requests. If you call this method any managed
2689
   * visibility change callback will be removed and you must manage visibility
2690
   * changes on your own.
2691
   *
2692
   * On non-browser platforms the refresh process works *continuously* in the
2693
   * background, which may not be desirable. You should hook into your
2694
   * platform's foreground indication mechanism and call these methods
2695
   * appropriately to conserve resources.
2696
   *
2697
   * {@see #stopAutoRefresh}
2698
   */
2699
  async startAutoRefresh() {
2700
    this._removeVisibilityChangedCallback()
26✔
2701
    await this._startAutoRefresh()
26✔
2702
  }
2703

2704
  /**
2705
   * Stops an active auto refresh process running in the background (if any).
2706
   *
2707
   * If you call this method any managed visibility change callback will be
2708
   * removed and you must manage visibility changes on your own.
2709
   *
2710
   * See {@link #startAutoRefresh} for more details.
2711
   */
2712
  async stopAutoRefresh() {
2713
    this._removeVisibilityChangedCallback()
8✔
2714
    await this._stopAutoRefresh()
8✔
2715
  }
2716

2717
  /**
2718
   * Runs the auto refresh token tick.
2719
   */
2720
  private async _autoRefreshTokenTick() {
2721
    this._debug('#_autoRefreshTokenTick()', 'begin')
32✔
2722

2723
    try {
32✔
2724
      await this._acquireLock(0, async () => {
32✔
2725
        try {
32✔
2726
          const now = Date.now()
32✔
2727

2728
          try {
32✔
2729
            return await this._useSession(async (result) => {
32✔
2730
              const {
2731
                data: { session },
2732
              } = result
32✔
2733

2734
              if (!session || !session.refresh_token || !session.expires_at) {
32✔
2735
                this._debug('#_autoRefreshTokenTick()', 'no session')
20✔
2736
                return
20✔
2737
              }
2738

2739
              // session will expire in this many ticks (or has already expired if <= 0)
2740
              const expiresInTicks = Math.floor(
12✔
2741
                (session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS
2742
              )
2743

2744
              this._debug(
12✔
2745
                '#_autoRefreshTokenTick()',
2746
                `access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks`
2747
              )
2748

2749
              if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) {
12!
UNCOV
2750
                await this._callRefreshToken(session.refresh_token)
×
2751
              }
2752
            })
2753
          } catch (e: any) {
UNCOV
2754
            console.error(
×
2755
              'Auto refresh tick failed with error. This is likely a transient error.',
2756
              e
2757
            )
2758
          }
2759
        } finally {
2760
          this._debug('#_autoRefreshTokenTick()', 'end')
32✔
2761
        }
2762
      })
2763
    } catch (e: any) {
UNCOV
2764
      if (e.isAcquireTimeout || e instanceof LockAcquireTimeoutError) {
×
2765
        this._debug('auto refresh token tick lock not available')
×
2766
      } else {
UNCOV
2767
        throw e
×
2768
      }
2769
    }
2770
  }
2771

2772
  /**
2773
   * Registers callbacks on the browser / platform, which in-turn run
2774
   * algorithms when the browser window/tab are in foreground. On non-browser
2775
   * platforms it assumes always foreground.
2776
   */
2777
  private async _handleVisibilityChange() {
2778
    this._debug('#_handleVisibilityChange()')
176✔
2779

2780
    if (!isBrowser() || !window?.addEventListener) {
176!
2781
      if (this.autoRefreshToken) {
106✔
2782
        // in non-browser environments the refresh token ticker runs always
2783
        this.startAutoRefresh()
20✔
2784
      }
2785

2786
      return false
106✔
2787
    }
2788

2789
    try {
70✔
2790
      this.visibilityChangedCallback = async () => await this._onVisibilityChanged(false)
70✔
2791

2792
      window?.addEventListener('visibilitychange', this.visibilityChangedCallback)
70!
2793

2794
      // now immediately call the visbility changed callback to setup with the
2795
      // current visbility state
2796
      await this._onVisibilityChanged(true) // initial call
68✔
2797
    } catch (error) {
2798
      console.error('_handleVisibilityChange', error)
2✔
2799
    }
2800
  }
2801

2802
  /**
2803
   * Callback registered with `window.addEventListener('visibilitychange')`.
2804
   */
2805
  private async _onVisibilityChanged(calledFromInitialize: boolean) {
2806
    const methodName = `#_onVisibilityChanged(${calledFromInitialize})`
68✔
2807
    this._debug(methodName, 'visibilityState', document.visibilityState)
68✔
2808

2809
    if (document.visibilityState === 'visible') {
68!
2810
      if (this.autoRefreshToken) {
68✔
2811
        // in browser environments the refresh token ticker runs only on focused tabs
2812
        // which prevents race conditions
2813
        this._startAutoRefresh()
6✔
2814
      }
2815

2816
      if (!calledFromInitialize) {
68!
2817
        // called when the visibility has changed, i.e. the browser
2818
        // transitioned from hidden -> visible so we need to see if the session
2819
        // should be recovered immediately... but to do that we need to acquire
2820
        // the lock first asynchronously
UNCOV
2821
        await this.initializePromise
×
2822

UNCOV
2823
        await this._acquireLock(-1, async () => {
×
2824
          if (document.visibilityState !== 'visible') {
×
2825
            this._debug(
×
2826
              methodName,
2827
              'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting'
2828
            )
2829

2830
            // visibility has changed while waiting for the lock, abort
UNCOV
2831
            return
×
2832
          }
2833

2834
          // recover the session
UNCOV
2835
          await this._recoverAndRefresh()
×
2836
        })
2837
      }
UNCOV
2838
    } else if (document.visibilityState === 'hidden') {
×
2839
      if (this.autoRefreshToken) {
×
2840
        this._stopAutoRefresh()
×
2841
      }
2842
    }
2843
  }
2844

2845
  /**
2846
   * Generates the relevant login URL for a third-party provider.
2847
   * @param options.redirectTo A URL or mobile address to send the user to after they are confirmed.
2848
   * @param options.scopes A space-separated list of scopes granted to the OAuth application.
2849
   * @param options.queryParams An object of key-value pairs containing query parameters granted to the OAuth application.
2850
   */
2851
  private async _getUrlForProvider(
2852
    url: string,
2853
    provider: Provider,
2854
    options: {
2855
      redirectTo?: string
2856
      scopes?: string
2857
      queryParams?: { [key: string]: string }
2858
      skipBrowserRedirect?: boolean
2859
    }
2860
  ) {
2861
    const urlParams: string[] = [`provider=${encodeURIComponent(provider)}`]
28✔
2862
    if (options?.redirectTo) {
28!
2863
      urlParams.push(`redirect_to=${encodeURIComponent(options.redirectTo)}`)
14✔
2864
    }
2865
    if (options?.scopes) {
28!
2866
      urlParams.push(`scopes=${encodeURIComponent(options.scopes)}`)
6✔
2867
    }
2868
    if (this.flowType === 'pkce') {
28✔
2869
      const [codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
4✔
2870
        this.storage,
2871
        this.storageKey
2872
      )
2873

2874
      const flowParams = new URLSearchParams({
4✔
2875
        code_challenge: `${encodeURIComponent(codeChallenge)}`,
2876
        code_challenge_method: `${encodeURIComponent(codeChallengeMethod)}`,
2877
      })
2878
      urlParams.push(flowParams.toString())
4✔
2879
    }
2880
    if (options?.queryParams) {
28!
2881
      const query = new URLSearchParams(options.queryParams)
4✔
2882
      urlParams.push(query.toString())
4✔
2883
    }
2884
    if (options?.skipBrowserRedirect) {
28!
2885
      urlParams.push(`skip_http_redirect=${options.skipBrowserRedirect}`)
4✔
2886
    }
2887

2888
    return `${url}?${urlParams.join('&')}`
28✔
2889
  }
2890

2891
  private async _unenroll(params: MFAUnenrollParams): Promise<AuthMFAUnenrollResponse> {
2892
    try {
4✔
2893
      return await this._useSession(async (result) => {
4✔
2894
        const { data: sessionData, error: sessionError } = result
4✔
2895
        if (sessionError) {
4!
UNCOV
2896
          return { data: null, error: sessionError }
×
2897
        }
2898

2899
        return await _request(this.fetch, 'DELETE', `${this.url}/factors/${params.factorId}`, {
4✔
2900
          headers: this.headers,
2901
          jwt: sessionData?.session?.access_token,
22!
2902
        })
2903
      })
2904
    } catch (error) {
2905
      if (isAuthError(error)) {
2✔
2906
        return { data: null, error }
2✔
2907
      }
UNCOV
2908
      throw error
×
2909
    }
2910
  }
2911

2912
  /**
2913
   * {@see GoTrueMFAApi#enroll}
2914
   */
2915
  private async _enroll(params: MFAEnrollTOTPParams): Promise<AuthMFAEnrollTOTPResponse>
2916
  private async _enroll(params: MFAEnrollPhoneParams): Promise<AuthMFAEnrollPhoneResponse>
2917
  private async _enroll(params: MFAEnrollParams): Promise<AuthMFAEnrollResponse> {
2918
    try {
20✔
2919
      return await this._useSession(async (result) => {
20✔
2920
        const { data: sessionData, error: sessionError } = result
20✔
2921
        if (sessionError) {
20!
UNCOV
2922
          return { data: null, error: sessionError }
×
2923
        }
2924

2925
        const body = {
20✔
2926
          friendly_name: params.friendlyName,
2927
          factor_type: params.factorType,
2928
          ...(params.factorType === 'phone' ? { phone: params.phone } : { issuer: params.issuer }),
20✔
2929
        }
2930

2931
        const { data, error } = await _request(this.fetch, 'POST', `${this.url}/factors`, {
20✔
2932
          body,
2933
          headers: this.headers,
2934
          jwt: sessionData?.session?.access_token,
116!
2935
        })
2936

2937
        if (error) {
16!
UNCOV
2938
          return { data: null, error }
×
2939
        }
2940

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

2945
        return { data, error: null }
16✔
2946
      })
2947
    } catch (error) {
2948
      if (isAuthError(error)) {
4✔
2949
        return { data: null, error }
4✔
2950
      }
UNCOV
2951
      throw error
×
2952
    }
2953
  }
2954

2955
  /**
2956
   * {@see GoTrueMFAApi#verify}
2957
   */
2958
  private async _verify(params: MFAVerifyParams): Promise<AuthMFAVerifyResponse> {
2959
    return this._acquireLock(-1, async () => {
6✔
2960
      try {
6✔
2961
        return await this._useSession(async (result) => {
6✔
2962
          const { data: sessionData, error: sessionError } = result
6✔
2963
          if (sessionError) {
6!
UNCOV
2964
            return { data: null, error: sessionError }
×
2965
          }
2966

2967
          const { data, error } = await _request(
6✔
2968
            this.fetch,
2969
            'POST',
2970
            `${this.url}/factors/${params.factorId}/verify`,
2971
            {
2972
              body: { code: params.code, challenge_id: params.challengeId },
2973
              headers: this.headers,
2974
              jwt: sessionData?.session?.access_token,
34!
2975
            }
2976
          )
UNCOV
2977
          if (error) {
×
2978
            return { data: null, error }
×
2979
          }
2980

UNCOV
2981
          await this._saveSession({
×
2982
            expires_at: Math.round(Date.now() / 1000) + data.expires_in,
2983
            ...data,
2984
          })
UNCOV
2985
          await this._notifyAllSubscribers('MFA_CHALLENGE_VERIFIED', data)
×
2986

UNCOV
2987
          return { data, error }
×
2988
        })
2989
      } catch (error) {
2990
        if (isAuthError(error)) {
6✔
2991
          return { data: null, error }
6✔
2992
        }
UNCOV
2993
        throw error
×
2994
      }
2995
    })
2996
  }
2997

2998
  /**
2999
   * {@see GoTrueMFAApi#challenge}
3000
   */
3001
  private async _challenge<T extends FactorType>(
3002
    params: MFAChallengeParams
3003
  ): Promise<AuthMFAChallengeResponse<T>> {
3004
    return this._acquireLock(-1, async () => {
8✔
3005
      try {
8✔
3006
        return await this._useSession(async (result) => {
8✔
3007
          const { data: sessionData, error: sessionError } = result
8✔
3008
          if (sessionError) {
8!
UNCOV
3009
            return { data: null, error: sessionError }
×
3010
          }
3011

3012
          return await _request(
8✔
3013
            this.fetch,
3014
            'POST',
3015
            `${this.url}/factors/${params.factorId}/challenge`,
3016
            {
3017
              body: 'channel' in params ? { channel: params.channel } : {},
8!
3018
              headers: this.headers,
3019
              jwt: sessionData?.session?.access_token,
46!
3020
            }
3021
          )
3022
        })
3023
      } catch (error) {
3024
        if (isAuthError(error)) {
2✔
3025
          return { data: null, error }
2✔
3026
        }
UNCOV
3027
        throw error
×
3028
      }
3029
    })
3030
  }
3031

3032
  /**
3033
   * {@see GoTrueMFAApi#challengeAndVerify}
3034
   */
3035
  private async _challengeAndVerify(
3036
    params: MFAChallengeAndVerifyParams
3037
  ): Promise<AuthMFAVerifyResponse> {
3038
    // both _challenge and _verify independently acquire the lock, so no need
3039
    // to acquire it here
3040

3041
    const { data: challengeData, error: challengeError } = await this._challenge({
2✔
3042
      factorId: params.factorId,
3043
    })
3044
    if (challengeError) {
2!
UNCOV
3045
      return { data: null, error: challengeError }
×
3046
    }
3047

3048
    return await this._verify({
2✔
3049
      factorId: params.factorId,
3050
      challengeId: challengeData.id,
3051
      code: params.code,
3052
    })
3053
  }
3054

3055
  /**
3056
   * {@see GoTrueMFAApi#listFactors}
3057
   */
3058
  private async _listFactors(): Promise<AuthMFAListFactorsResponse> {
3059
    // use #getUser instead of #_getUser as the former acquires a lock
3060
    const {
3061
      data: { user },
3062
      error: userError,
3063
    } = await this.getUser()
4✔
3064
    if (userError) {
4!
UNCOV
3065
      return { data: null, error: userError }
×
3066
    }
3067

3068
    const factors = user?.factors || []
4!
3069
    const totp = factors.filter(
4✔
3070
      (factor): factor is Factor<'totp', 'verified'> =>
3071
        factor.factor_type === 'totp' && factor.status === 'verified'
8✔
3072
    )
3073
    const phone = factors.filter(
4✔
3074
      (factor): factor is Factor<'phone', 'verified'> =>
3075
        factor.factor_type === 'phone' && factor.status === 'verified'
8✔
3076
    )
3077

3078
    return {
4✔
3079
      data: {
3080
        all: factors,
3081
        totp,
3082
        phone,
3083
      },
3084
      error: null,
3085
    }
3086
  }
3087

3088
  /**
3089
   * {@see GoTrueMFAApi#getAuthenticatorAssuranceLevel}
3090
   */
3091
  private async _getAuthenticatorAssuranceLevel(): Promise<AuthMFAGetAuthenticatorAssuranceLevelResponse> {
3092
    return this._acquireLock(-1, async () => {
2✔
3093
      return await this._useSession(async (result) => {
2✔
3094
        const {
3095
          data: { session },
3096
          error: sessionError,
3097
        } = result
2✔
3098
        if (sessionError) {
2!
3099
          return { data: null, error: sessionError }
×
3100
        }
3101
        if (!session) {
2!
3102
          return {
×
3103
            data: { currentLevel: null, nextLevel: null, currentAuthenticationMethods: [] },
3104
            error: null,
3105
          }
3106
        }
3107

3108
        const { payload } = decodeJWT(session.access_token)
2✔
3109

3110
        let currentLevel: AuthenticatorAssuranceLevels | null = null
2✔
3111

3112
        if (payload.aal) {
2✔
3113
          currentLevel = payload.aal
2✔
3114
        }
3115

3116
        let nextLevel: AuthenticatorAssuranceLevels | null = currentLevel
2✔
3117

3118
        const verifiedFactors =
3119
          session.user.factors?.filter((factor: Factor) => factor.status === 'verified') ?? []
2!
3120

3121
        if (verifiedFactors.length > 0) {
2!
3122
          nextLevel = 'aal2'
×
3123
        }
3124

3125
        const currentAuthenticationMethods = payload.amr || []
2!
3126

3127
        return { data: { currentLevel, nextLevel, currentAuthenticationMethods }, error: null }
2✔
3128
      })
3129
    })
3130
  }
3131

3132
  private async fetchJwk(kid: string, jwks: { keys: JWK[] } = { keys: [] }): Promise<JWK | null> {
10✔
3133
    // try fetching from the supplied jwks
3134
    let jwk = jwks.keys.find((key) => key.kid === kid)
10✔
3135
    if (jwk) {
10!
3136
      return jwk
×
3137
    }
3138

3139
    const now = Date.now()
10✔
3140

3141
    // try fetching from cache
3142
    jwk = this.jwks.keys.find((key) => key.kid === kid)
10✔
3143

3144
    // jwk exists and jwks isn't stale
3145
    if (jwk && this.jwks_cached_at + JWKS_TTL > now) {
10✔
3146
      return jwk
3✔
3147
    }
3148
    // jwk isn't cached in memory so we need to fetch it from the well-known endpoint
3149
    const { data, error } = await _request(this.fetch, 'GET', `${this.url}/.well-known/jwks.json`, {
7✔
3150
      headers: this.headers,
3151
    })
3152
    if (error) {
7!
3153
      throw error
×
3154
    }
3155
    if (!data.keys || data.keys.length === 0) {
7!
3156
      return null
×
3157
    }
3158

3159
    this.jwks = data
7✔
3160
    this.jwks_cached_at = now
7✔
3161

3162
    // Find the signing key
3163
    jwk = data.keys.find((key: any) => key.kid === kid)
7✔
3164
    if (!jwk) {
7!
3165
      return null
×
3166
    }
3167
    return jwk
7✔
3168
  }
3169

3170
  /**
3171
   * Extracts the JWT claims present in the access token by first verifying the
3172
   * JWT against the server's JSON Web Key Set endpoint
3173
   * `/.well-known/jwks.json` which is often cached, resulting in significantly
3174
   * faster responses. Prefer this method over {@link #getUser} which always
3175
   * sends a request to the Auth server for each JWT.
3176
   *
3177
   * If the project is not using an asymmetric JWT signing key (like ECC or
3178
   * RSA) it always sends a request to the Auth server (similar to {@link
3179
   * #getUser}) to verify the JWT.
3180
   *
3181
   * @param jwt An optional specific JWT you wish to verify, not the one you
3182
   *            can obtain from {@link #getSession}.
3183
   * @param options Various additional options that allow you to customize the
3184
   *                behavior of this method.
3185
   */
3186
  async getClaims(
3187
    jwt?: string,
3188
    options: {
15✔
3189
      /**
3190
       * @deprecated Please use options.jwks instead.
3191
       */
3192
      keys?: JWK[]
3193

3194
      /** If set to `true` the `exp` claim will not be validated against the current time. */
3195
      allowExpired?: boolean
3196

3197
      /** If set, this JSON Web Key Set is going to have precedence over the cached value available on the server. */
3198
      jwks?: { keys: JWK[] }
3199
    } = {}
3200
  ): Promise<
3201
    | {
3202
        data: { claims: JwtPayload; header: JwtHeader; signature: Uint8Array }
3203
        error: null
3204
      }
3205
    | { data: null; error: AuthError }
3206
    | { data: null; error: null }
3207
  > {
3208
    try {
15✔
3209
      let token = jwt
15✔
3210
      if (!token) {
15✔
3211
        const { data, error } = await this.getSession()
15✔
3212
        if (error || !data.session) {
15✔
3213
          return { data: null, error }
2✔
3214
        }
3215
        token = data.session.access_token
13✔
3216
      }
3217

3218
      const {
3219
        header,
3220
        payload,
3221
        signature,
3222
        raw: { header: rawHeader, payload: rawPayload },
3223
      } = decodeJWT(token)
13✔
3224

3225
      if (!options?.allowExpired) {
11!
3226
        // Reject expired JWTs should only happen if jwt argument was passed
3227
        validateExp(payload.exp)
11✔
3228
      }
3229

3230
      const signingKey =
3231
        !header.alg ||
9✔
3232
        header.alg.startsWith('HS') ||
3233
        !header.kid ||
3234
        !('crypto' in globalThis && 'subtle' in globalThis.crypto)
5✔
3235
          ? null
3236
          : await this.fetchJwk(header.kid, options?.keys ? { keys: options.keys } : options?.jwks)
14!
3237

3238
      // If symmetric algorithm or WebCrypto API is unavailable, fallback to getUser()
3239
      if (!signingKey) {
9✔
3240
        const { error } = await this.getUser(token)
7✔
3241
        if (error) {
7✔
3242
          throw error
2✔
3243
        }
3244
        // getUser succeeds so the claims in the JWT can be trusted
3245
        return {
5✔
3246
          data: {
3247
            claims: payload,
3248
            header,
3249
            signature,
3250
          },
3251
          error: null,
3252
        }
3253
      }
3254

3255
      const algorithm = getAlgorithm(header.alg)
2✔
3256

3257
      // Convert JWK to CryptoKey
3258
      const publicKey = await crypto.subtle.importKey('jwk', signingKey, algorithm, true, [
2✔
3259
        'verify',
3260
      ])
3261

3262
      // Verify the signature
3263
      const isValid = await crypto.subtle.verify(
2✔
3264
        algorithm,
3265
        publicKey,
3266
        signature,
3267
        stringToUint8Array(`${rawHeader}.${rawPayload}`)
3268
      )
3269

3270
      if (!isValid) {
2✔
3271
        throw new AuthInvalidJwtError('Invalid JWT signature')
1✔
3272
      }
3273

3274
      // If verification succeeds, decode and return claims
3275
      return {
1✔
3276
        data: {
3277
          claims: payload,
3278
          header,
3279
          signature,
3280
        },
3281
        error: null,
3282
      }
3283
    } catch (error) {
3284
      if (isAuthError(error)) {
7✔
3285
        return { data: null, error }
5✔
3286
      }
3287
      throw error
2✔
3288
    }
3289
  }
3290
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc