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

supabase / auth-js / 17634713707

11 Sep 2025 05:13AM UTC coverage: 79.764% (-1.0%) from 80.811%
17634713707

Pull #1116

github

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

1067 of 1442 branches covered (73.99%)

Branch coverage included in aggregate %.

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

20 existing lines in 3 files now uncovered.

1436 of 1696 relevant lines covered (84.67%)

79.66 hits per line

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

75.41
/src/GoTrueClient.ts
1
import GoTrueAdminApi from './GoTrueAdminApi'
5✔
2
import {
5✔
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 {
5✔
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 {
5✔
27
  Fetch,
28
  _request,
29
  _sessionResponse,
30
  _sessionResponsePassword,
31
  _userResponse,
32
  _ssoResponse,
33
} from './lib/fetch'
34
import {
5✔
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'
5✔
54
import { polyfillGlobalThis } from './lib/polyfills'
5✔
55
import { version } from './lib/version'
5✔
56
import { LockAcquireTimeoutError, navigatorLock } from './lib/locks'
5✔
57

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

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

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

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

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

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

163
  private instanceID: number
164

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

179
  protected flowType: AuthFlowType
180

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

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

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

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

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

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

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

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

246
    if (this.instanceID > 0 && isBrowser()) {
91✔
247
      console.warn(
36✔
248
        'Multiple GoTrueClient instances detected in the same browser context. It is not an error, but this should be avoided as it may produce undefined behavior when used concurrently under the same storage key.'
249
      )
250
    }
251

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

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

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

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

276
    if (settings.lock) {
91✔
277
      this.lock = settings.lock
4✔
278
    } else if (isBrowser() && globalThis?.navigator?.locks) {
87!
279
      this.lock = navigatorLock
18✔
280
    } else {
281
      this.lock = lockNoOp
69✔
282
    }
283

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

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

299
    if (this.persistSession) {
91✔
300
      if (settings.storage) {
86✔
301
        this.storage = settings.storage
68✔
302
      } else {
303
        if (supportsLocalStorage()) {
18✔
304
          this.storage = globalThis.localStorage
1✔
305
        } else {
306
          this.memoryStorage = {}
17✔
307
          this.storage = memoryLocalStorageAdapter(this.memoryStorage)
17✔
308
        }
309
      }
310

311
      if (settings.userStorage) {
86✔
312
        this.userStorage = settings.userStorage
6✔
313
      }
314
    } else {
315
      this.memoryStorage = {}
5✔
316
      this.storage = memoryLocalStorageAdapter(this.memoryStorage)
5✔
317
    }
318

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

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

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

336
    this.initialize()
91✔
337
  }
338

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

347
    return this
3,613✔
348
  }
349

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

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

366
    return await this.initializePromise
91✔
367
  }
368

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

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

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

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

411
          return { error }
2✔
412
        }
413

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

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

424
        await this._saveSession(session)
1✔
425

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

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

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

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

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

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

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

487
      throw error
×
488
    }
489
  }
490

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

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

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

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

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

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

566
      throw error
×
567
    }
568
  }
569

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

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

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

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

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

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

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

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

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

703
      let resolvedWallet: EthereumWallet
704

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

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

718
        if (
×
719
          'ethereum' in windowAny &&
×
720
          typeof windowAny.ethereum === 'object' &&
721
          'request' in windowAny.ethereum &&
722
          typeof windowAny.ethereum.request === 'function'
723
        ) {
724
          resolvedWallet = windowAny.ethereum
×
725
        } else {
726
          throw new Error(
×
727
            `@supabase/auth-js: No compatible Ethereum wallet interface on the window object (window.ethereum) detected. Make sure the user already has a wallet installed and connected for this app. Prefer passing the wallet interface object directly to signInWithWeb3({ chain: 'ethereum', wallet: resolvedUserWallet }) instead.`
728
          )
729
        }
730
      }
731

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

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

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

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

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

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

776
      message = createSiweMessage(siweMessage)
×
777

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

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

822
      throw error
×
823
    }
824
  }
825

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

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

836
      let resolvedWallet: SolanaWallet
837

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

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

851
        if (
×
852
          'solana' in windowAny &&
×
853
          typeof windowAny.solana === 'object' &&
854
          (('signIn' in windowAny.solana && typeof windowAny.solana.signIn === 'function') ||
855
            ('signMessage' in windowAny.solana &&
856
              typeof windowAny.solana.signMessage === 'function'))
857
        ) {
858
          resolvedWallet = windowAny.solana
×
859
        } else {
860
          throw new Error(
×
861
            `@supabase/auth-js: No compatible Solana wallet interface on the window object (window.solana) detected. Make sure the user already has a wallet installed and connected for this app. Prefer passing the wallet interface object directly to signInWithWeb3({ chain: 'solana', wallet: resolvedUserWallet }) instead.`
862
          )
863
        }
864
      }
865

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

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

872
          ...options?.signInWithSolana,
×
873

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

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

882
        let outputToProcess: any
883

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

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

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

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

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

968
        signature = maybeSignature
×
969
      }
970
    }
971

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

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

1010
      throw error
×
1011
    }
1012
  }
1013

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

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

1058
      throw error
×
1059
    }
1060
  }
1061

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

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

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

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

1167
      throw error
×
1168
    }
1169
  }
1170

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

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

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

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

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

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

1217
      throw error
×
1218
    }
1219
  }
1220

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

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

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

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

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

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

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

1346
  /**
1347
   * Returns the session, refreshing it if necessary.
1348
   *
1349
   * The session returned can be null if the session is not detected which can happen in the event a user is not signed-in or has logged out.
1350
   *
1351
   * **IMPORTANT:** This method loads values directly from the storage attached
1352
   * to the client. If that storage is based on request cookies for example,
1353
   * the values in it may not be authentic and therefore it's strongly advised
1354
   * against using this method and its results in such circumstances. A warning
1355
   * will be emitted if this is detected. Use {@link #getUser()} instead.
1356
   */
1357
  async getSession() {
1358
    await this.initializePromise
37✔
1359

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

1366
    return result
35✔
1367
  }
1368

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

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

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

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

1396
        return result
1✔
1397
      }
1398

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

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

1405
          const result = fn()
297✔
1406

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

1417
          await result
297✔
1418

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

1423
            await Promise.all(waitOn)
293✔
1424

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1562
        if (this.storage.isServer && currentSession.user) {
83✔
1563
          let suppressWarning = this.suppressGetSessionWarning
7✔
1564
          const proxySession: Session = new Proxy(currentSession, {
7✔
1565
            get: (target: any, prop: string, receiver: any) => {
1566
              if (!suppressWarning && prop === 'user') {
12✔
1567
                // only show warning when the user object is being accessed from the server
1568
                console.warn(
1✔
1569
                  'Using the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.'
1570
                )
1571
                suppressWarning = true // keeps this proxy instance from logging additional warnings
1✔
1572
                this.suppressGetSessionWarning = true // keeps this client's future proxy instances from warning
1✔
1573
              }
1574
              return Reflect.get(target, prop, receiver)
12✔
1575
            },
1576
          })
1577
          currentSession = proxySession
7✔
1578
        }
1579

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

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

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

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

1606
    await this.initializePromise
8✔
1607

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

1612
    return result
8✔
1613
  }
1614

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

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

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

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

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

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

1655
      throw error
4✔
1656
    }
1657
  }
1658

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

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

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

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

1722
      throw error
×
1723
    }
1724
  }
1725

1726
  /**
1727
   * Sets the session data from the current session. If the current session is expired, setSession will take care of refreshing it to obtain a new session.
1728
   * If the refresh token or access token in the current session is invalid, an error will be thrown.
1729
   * @param currentSession The current session that minimally contains an access token and refresh token.
1730
   */
1731
  async setSession(currentSession: {
1732
    access_token: string
1733
    refresh_token: string
1734
  }): Promise<AuthResponse> {
1735
    await this.initializePromise
6✔
1736

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

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

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

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

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

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

1796
      throw error
×
1797
    }
1798
  }
1799

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

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

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

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

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

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

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

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

1848
      throw error
×
1849
    }
1850
  }
1851

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

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

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

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

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

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

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

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

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

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

1930
      if (expires_at) {
3✔
1931
        expiresAt = parseInt(expires_at)
1✔
1932
      }
1933

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

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

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

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

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

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

1982
      throw error
2✔
1983
    }
1984
  }
1985

1986
  /**
1987
   * Checks if the current URL contains parameters given by an implicit oauth grant flow (https://www.rfc-editor.org/rfc/rfc6749.html#section-4.2)
1988
   */
1989
  private _isImplicitGrantCallback(params: { [parameter: string]: string }): boolean {
1990
    return Boolean(params.access_token || params.error_description)
36✔
1991
  }
1992

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

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

2005
  /**
2006
   * Inside a browser context, `signOut()` will remove the logged in user from the browser session and log them out - removing all items from localstorage and then trigger a `"SIGNED_OUT"` event.
2007
   *
2008
   * For server-side management, you can revoke all refresh tokens for a user by passing a user's JWT through to `auth.api.signOut(JWT: string)`.
2009
   * There is no way to revoke a user's access token jwt until it expires. It is recommended to set a shorter expiry on the jwt for this reason.
2010
   *
2011
   * If using `others` scope, no `SIGNED_OUT` event is fired!
2012
   */
2013
  async signOut(options: SignOut = { scope: 'global' }): Promise<{ error: AuthError | null }> {
115✔
2014
    await this.initializePromise
116✔
2015

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

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

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

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

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

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

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

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

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

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

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

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

2152
      throw error
×
2153
    }
2154
  }
2155

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

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

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

2258
    try {
14✔
2259
      const startedAt = Date.now()
14✔
2260

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

2268
          this._debug(debugName, 'refreshing attempt', attempt)
14✔
2269

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

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

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

2306
    return isValidSession
126✔
2307
  }
2308

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

2324
    this._debug('#_handleProviderSignIn()', 'provider', provider, 'options', options, 'url', url)
10✔
2325

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

2331
    return { data: { provider, url }, error: null }
10✔
2332
  }
2333

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

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

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

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

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

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

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

2373
          if (separateUser && separateUser?.user) {
1!
2374
            currentSession.user = separateUser.user
×
2375

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

2384
      this._debug(debugName, 'session from storage', currentSession)
32✔
2385

2386
      if (!this._isValidSession(currentSession)) {
32✔
2387
        this._debug(debugName, 'session is not valid')
24✔
2388
        if (currentSession !== null) {
24✔
2389
          await this._removeSession()
3✔
2390
        }
2391

2392
        return
24✔
2393
      }
2394

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

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

2403
      if (expiresWithMargin) {
8✔
2404
        if (this.autoRefreshToken && currentSession.refresh_token) {
2✔
2405
          const { error } = await this._callRefreshToken(currentSession.refresh_token)
2✔
2406

2407
          if (error) {
2✔
2408
            console.error(error)
1✔
2409

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

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

2452
      console.error(err)
1✔
2453
      return
1✔
2454
    } finally {
2455
      this._debug(debugName, 'end')
33✔
2456
    }
2457
  }
2458

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

2464
    // refreshing is already in progress
2465
    if (this.refreshingDeferred) {
19✔
2466
      return this.refreshingDeferred.promise
3✔
2467
    }
2468

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

2471
    this._debug(debugName, 'begin')
16✔
2472

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

2476
      const { data, error } = await this._refreshAccessToken(refreshToken)
16✔
2477
      if (error) throw error
15✔
2478
      if (!data.session) throw new AuthSessionMissingError()
9✔
2479

2480
      await this._saveSession(data.session)
8✔
2481
      await this._notifyAllSubscribers('TOKEN_REFRESHED', data.session)
8✔
2482

2483
      const result = { data: data.session, error: null }
8✔
2484

2485
      this.refreshingDeferred.resolve(result)
8✔
2486

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

2491
      if (isAuthError(error)) {
8✔
2492
        const result = { data: null, error }
7✔
2493

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

2498
        this.refreshingDeferred?.resolve(result)
7!
2499

2500
        return result
7✔
2501
      }
2502

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

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

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

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

2533
      await Promise.all(promises)
222✔
2534

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

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

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

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

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

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

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

2591
  private async _removeSession() {
2592
    this._debug('#_removeSession()')
134✔
2593

2594
    await removeItemAsync(this.storage, this.storageKey)
134✔
2595
    await removeItemAsync(this.storage, this.storageKey + '-code-verifier')
133✔
2596
    await removeItemAsync(this.storage, this.storageKey + '-user')
133✔
2597

2598
    if (this.userStorage) {
133✔
2599
      await removeItemAsync(this.userStorage, this.storageKey + '-user')
1✔
2600
    }
2601

2602
    await this._notifyAllSubscribers('SIGNED_OUT', null)
133✔
2603
  }
2604

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

2614
    const callback = this.visibilityChangedCallback
17✔
2615
    this.visibilityChangedCallback = null
17✔
2616

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

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

2633
    this._debug('#_startAutoRefresh()')
16✔
2634

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

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

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

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

2670
    const ticker = this.autoRefreshTicker
20✔
2671
    this.autoRefreshTicker = null
20✔
2672

2673
    if (ticker) {
20✔
2674
      clearInterval(ticker)
5✔
2675
    }
2676
  }
2677

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

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

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

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

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

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

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

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

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

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

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

2787
      return false
53✔
2788
    }
2789

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

2793
      window?.addEventListener('visibilitychange', this.visibilityChangedCallback)
35!
2794

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2938
        if (error) {
8!
2939
          return { data: null, error }
×
2940
        }
2941

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

2946
        return { data, error: null }
8✔
2947
      })
2948
    } catch (error) {
2949
      if (isAuthError(error)) {
2✔
2950
        return { data: null, error }
2✔
2951
      }
2952
      throw error
×
2953
    }
2954
  }
2955

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

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

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

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

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

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

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

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

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

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

3069
    const data: AuthMFAListFactorsResponse['data'] = {
2✔
3070
      all: [],
3071
      phone: [],
3072
      totp: [],
3073
    }
3074

3075
    for (const factor of user?.factors ?? []) {
2!
3076
      data.all.push(factor)
4✔
3077
      if (factor.status === 'verified') {
4✔
3078
        ;(data[factor.factor_type] as typeof factor[]).push(factor)
2✔
3079
      }
3080
    }
3081

3082
    return {
2✔
3083
      data,
3084
      error: null,
3085
    }
3086
  }
3087

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

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

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

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

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

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

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

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

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

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

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

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

3144
    // jwk exists and jwks isn't stale
3145
    if (jwk && this.jwks_cached_at + JWKS_TTL > now) {
4✔
3146
      return jwk
1✔
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`, {
3✔
3150
      headers: this.headers,
3151
    })
3152
    if (error) {
3!
3153
      throw error
×
3154
    }
3155
    if (!data.keys || data.keys.length === 0) {
3!
3156
      return null
×
3157
    }
3158

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

3162
    // Find the signing key
3163
    jwk = data.keys.find((key: any) => key.kid === kid)
3✔
3164
    if (!jwk) {
3!
3165
      return null
×
3166
    }
3167
    return jwk
3✔
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: {
7✔
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 {
7✔
3209
      let token = jwt
7✔
3210
      if (!token) {
7✔
3211
        const { data, error } = await this.getSession()
7✔
3212
        if (error || !data.session) {
7✔
3213
          return { data: null, error }
1✔
3214
        }
3215
        token = data.session.access_token
6✔
3216
      }
3217

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

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

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

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

UNCOV
3255
      const algorithm = getAlgorithm(header.alg)
×
3256

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

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

UNCOV
3270
      if (!isValid) {
×
UNCOV
3271
        throw new AuthInvalidJwtError('Invalid JWT signature')
×
3272
      }
3273

3274
      // If verification succeeds, decode and return claims
UNCOV
3275
      return {
×
3276
        data: {
3277
          claims: payload,
3278
          header,
3279
          signature,
3280
        },
3281
        error: null,
3282
      }
3283
    } catch (error) {
3284
      if (isAuthError(error)) {
3✔
3285
        return { data: null, error }
2✔
3286
      }
3287
      throw error
1✔
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