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

supabase / auth-js / 13245526603

10 Feb 2025 04:24PM UTC coverage: 53.607% (+3.6%) from 50.022%
13245526603

push

github

web-flow
feat: introduce getClaims method to verify asymmetric JWTs (#1030)

## What kind of change does this PR introduce?
* `getClaims` supports verifying JWTs (both asymmetric and symmetric)
and returns the entire set of claims in the JWT payload

---------

Co-authored-by: Stojan Dimitrovski <sdimitrovski@gmail.com>

469 of 1036 branches covered (45.27%)

Branch coverage included in aggregate %.

177 of 191 new or added lines in 5 files covered. (92.67%)

876 of 1473 relevant lines covered (59.47%)

101.27 hits per line

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

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

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

111
polyfillGlobalThis() // Make "globalThis" available
8✔
112

113
const DEFAULT_OPTIONS: Omit<Required<GoTrueClientOptions>, 'fetch' | 'storage' | 'lock'> = {
8✔
114
  url: GOTRUE_URL,
115
  storageKey: STORAGE_KEY,
116
  autoRefreshToken: true,
117
  persistSession: true,
118
  detectSessionInUrl: true,
119
  headers: DEFAULT_HEADERS,
120
  flowType: 'implicit',
121
  debug: false,
122
  hasCustomAuthorizationHeader: false,
123
}
124

125
async function lockNoOp<R>(name: string, acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
126
  return await fn()
182✔
127
}
128

129
export default class GoTrueClient {
8✔
130
  private static nextInstanceID = 0
8✔
131

132
  private instanceID: number
133

134
  /**
135
   * Namespace for the GoTrue admin methods.
136
   * These methods should only be used in a trusted server-side environment.
137
   */
138
  admin: GoTrueAdminApi
139
  /**
140
   * Namespace for the MFA methods.
141
   */
142
  mfa: GoTrueMFAApi
143
  /**
144
   * The storage key used to identify the values saved in localStorage
145
   */
146
  protected storageKey: string
147

148
  protected flowType: AuthFlowType
149
  /**
150
   * The JWKS used for verifying asymmetric JWTs
151
   */
152
  protected jwks: { keys: JWK[] }
153
  protected autoRefreshToken: boolean
154
  protected persistSession: boolean
155
  protected storage: SupportedStorage
156
  protected memoryStorage: { [key: string]: string } | null = null
50✔
157
  protected stateChangeEmitters: Map<string, Subscription> = new Map()
50✔
158
  protected autoRefreshTicker: ReturnType<typeof setInterval> | null = null
50✔
159
  protected visibilityChangedCallback: (() => Promise<any>) | null = null
50✔
160
  protected refreshingDeferred: Deferred<CallRefreshTokenResult> | null = null
50✔
161
  /**
162
   * Keeps track of the async client initialization.
163
   * When null or not yet resolved the auth state is `unknown`
164
   * Once resolved the the auth state is known and it's save to call any further client methods.
165
   * Keep extra care to never reject or throw uncaught errors
166
   */
167
  protected initializePromise: Promise<InitializeResult> | null = null
50✔
168
  protected detectSessionInUrl = true
50✔
169
  protected url: string
170
  protected headers: {
171
    [key: string]: string
172
  }
173
  protected hasCustomAuthorizationHeader = false
50✔
174
  protected suppressGetSessionWarning = false
50✔
175
  protected fetch: Fetch
176
  protected lock: LockFunc
177
  protected lockAcquired = false
50✔
178
  protected pendingInLock: Promise<any>[] = []
50✔
179

180
  /**
181
   * Used to broadcast state change events to other tabs listening.
182
   */
183
  protected broadcastChannel: BroadcastChannel | null = null
50✔
184

185
  protected logDebugMessages: boolean
186
  protected logger: (message: string, ...args: any[]) => void = console.log
50✔
187

188
  /**
189
   * Create a new client for use in the browser.
190
   */
191
  constructor(options: GoTrueClientOptions) {
192
    this.instanceID = GoTrueClient.nextInstanceID
50✔
193
    GoTrueClient.nextInstanceID += 1
50✔
194

195
    if (this.instanceID > 0 && isBrowser()) {
50!
196
      console.warn(
×
197
        '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.'
198
      )
199
    }
200

201
    const settings = { ...DEFAULT_OPTIONS, ...options }
50✔
202

203
    this.logDebugMessages = !!settings.debug
50✔
204
    if (typeof settings.debug === 'function') {
50!
205
      this.logger = settings.debug
×
206
    }
207

208
    this.persistSession = settings.persistSession
50✔
209
    this.storageKey = settings.storageKey
50✔
210
    this.autoRefreshToken = settings.autoRefreshToken
50✔
211
    this.admin = new GoTrueAdminApi({
50✔
212
      url: settings.url,
213
      headers: settings.headers,
214
      fetch: settings.fetch,
215
    })
216

217
    this.url = settings.url
50✔
218
    this.headers = settings.headers
50✔
219
    this.fetch = resolveFetch(settings.fetch)
50✔
220
    this.lock = settings.lock || lockNoOp
50✔
221
    this.detectSessionInUrl = settings.detectSessionInUrl
50✔
222
    this.flowType = settings.flowType
50✔
223
    this.hasCustomAuthorizationHeader = settings.hasCustomAuthorizationHeader
50✔
224

225
    if (settings.lock) {
50!
226
      this.lock = settings.lock
×
227
    } else if (isBrowser() && globalThis?.navigator?.locks) {
50!
228
      this.lock = navigatorLock
×
229
    } else {
230
      this.lock = lockNoOp
50✔
231
    }
232
    this.jwks = { keys: [] }
50✔
233
    this.mfa = {
50✔
234
      verify: this._verify.bind(this),
235
      enroll: this._enroll.bind(this),
236
      unenroll: this._unenroll.bind(this),
237
      challenge: this._challenge.bind(this),
238
      listFactors: this._listFactors.bind(this),
239
      challengeAndVerify: this._challengeAndVerify.bind(this),
240
      getAuthenticatorAssuranceLevel: this._getAuthenticatorAssuranceLevel.bind(this),
241
    }
242

243
    if (this.persistSession) {
50!
244
      if (settings.storage) {
50!
245
        this.storage = settings.storage
50✔
246
      } else {
247
        if (supportsLocalStorage()) {
×
248
          this.storage = localStorageAdapter
×
249
        } else {
250
          this.memoryStorage = {}
×
251
          this.storage = memoryLocalStorageAdapter(this.memoryStorage)
×
252
        }
253
      }
254
    } else {
255
      this.memoryStorage = {}
×
256
      this.storage = memoryLocalStorageAdapter(this.memoryStorage)
×
257
    }
258

259
    if (isBrowser() && globalThis.BroadcastChannel && this.persistSession && this.storageKey) {
50!
260
      try {
×
261
        this.broadcastChannel = new globalThis.BroadcastChannel(this.storageKey)
×
262
      } catch (e: any) {
263
        console.error(
×
264
          'Failed to create a new BroadcastChannel, multi-tab state changes will not be available',
265
          e
266
        )
267
      }
268

269
      this.broadcastChannel?.addEventListener('message', async (event) => {
×
270
        this._debug('received broadcast notification from other tab or client', event)
×
271

272
        await this._notifyAllSubscribers(event.data.event, event.data.session, false) // broadcast = false so we don't get an endless loop of messages
×
273
      })
274
    }
275

276
    this.initialize()
50✔
277
  }
278

279
  private _debug(...args: any[]): GoTrueClient {
280
    if (this.logDebugMessages) {
2,188!
281
      this.logger(
×
282
        `GoTrueClient@${this.instanceID} (${version}) ${new Date().toISOString()}`,
283
        ...args
284
      )
285
    }
286

287
    return this
2,188✔
288
  }
289

290
  /**
291
   * Initializes the client session either from the url or from storage.
292
   * This method is automatically called when instantiating the client, but should also be called
293
   * manually when checking for an error from an auth redirect (oauth, magiclink, password recovery, etc).
294
   */
295
  async initialize(): Promise<InitializeResult> {
296
    if (this.initializePromise) {
50!
297
      return await this.initializePromise
×
298
    }
299

300
    this.initializePromise = (async () => {
50✔
301
      return await this._acquireLock(-1, async () => {
50✔
302
        return await this._initialize()
50✔
303
      })
304
    })()
305

306
    return await this.initializePromise
50✔
307
  }
308

309
  /**
310
   * IMPORTANT:
311
   * 1. Never throw in this method, as it is called from the constructor
312
   * 2. Never return a session from this method as it would be cached over
313
   *    the whole lifetime of the client
314
   */
315
  private async _initialize(): Promise<InitializeResult> {
316
    try {
50✔
317
      const params = parseParametersFromURL(window.location.href)
50✔
318
      let callbackUrlType = 'none'
×
319
      if (this._isImplicitGrantCallback(params)) {
×
320
        callbackUrlType = 'implicit'
×
321
      } else if (await this._isPKCECallback(params)) {
×
322
        callbackUrlType = 'pkce'
×
323
      }
324

325
      /**
326
       * Attempt to get the session from the URL only if these conditions are fulfilled
327
       *
328
       * Note: If the URL isn't one of the callback url types (implicit or pkce),
329
       * then there could be an existing session so we don't want to prematurely remove it
330
       */
331
      if (isBrowser() && this.detectSessionInUrl && callbackUrlType !== 'none') {
×
332
        const { data, error } = await this._getSessionFromURL(params, callbackUrlType)
×
333
        if (error) {
×
334
          this._debug('#_initialize()', 'error detecting session from URL', error)
×
335

336
          if (isAuthImplicitGrantRedirectError(error)) {
×
337
            const errorCode = error.details?.code
×
338
            if (
×
339
              errorCode === 'identity_already_exists' ||
×
340
              errorCode === 'identity_not_found' ||
341
              errorCode === 'single_identity_not_deletable'
342
            ) {
343
              return { error }
×
344
            }
345
          }
346

347
          // failed login attempt via url,
348
          // remove old session as in verifyOtp, signUp and signInWith*
349
          await this._removeSession()
×
350

351
          return { error }
×
352
        }
353

354
        const { session, redirectType } = data
×
355

356
        this._debug(
×
357
          '#_initialize()',
358
          'detected session in URL',
359
          session,
360
          'redirect type',
361
          redirectType
362
        )
363

364
        await this._saveSession(session)
×
365

366
        setTimeout(async () => {
×
367
          if (redirectType === 'recovery') {
×
368
            await this._notifyAllSubscribers('PASSWORD_RECOVERY', session)
×
369
          } else {
370
            await this._notifyAllSubscribers('SIGNED_IN', session)
×
371
          }
372
        }, 0)
373

374
        return { error: null }
×
375
      }
376
      // no login attempt via callback url try to recover session from storage
377
      await this._recoverAndRefresh()
×
378
      return { error: null }
×
379
    } catch (error) {
380
      if (isAuthError(error)) {
50!
381
        return { error }
×
382
      }
383

384
      return {
50✔
385
        error: new AuthUnknownError('Unexpected error during initialization', error),
386
      }
387
    } finally {
388
      await this._handleVisibilityChange()
50✔
389
      this._debug('#_initialize()', 'end')
50✔
390
    }
391
  }
392

393
  /**
394
   * Creates a new anonymous user.
395
   *
396
   * @returns A session where the is_anonymous claim in the access token JWT set to true
397
   */
398
  async signInAnonymously(credentials?: SignInAnonymouslyCredentials): Promise<AuthResponse> {
399
    try {
×
400
      const res = await _request(this.fetch, 'POST', `${this.url}/signup`, {
×
401
        headers: this.headers,
402
        body: {
403
          data: credentials?.options?.data ?? {},
×
404
          gotrue_meta_security: { captcha_token: credentials?.options?.captchaToken },
×
405
        },
406
        xform: _sessionResponse,
407
      })
408
      const { data, error } = res
×
409

410
      if (error || !data) {
×
411
        return { data: { user: null, session: null }, error: error }
×
412
      }
413
      const session: Session | null = data.session
×
414
      const user: User | null = data.user
×
415

416
      if (data.session) {
×
417
        await this._saveSession(data.session)
×
418
        await this._notifyAllSubscribers('SIGNED_IN', session)
×
419
      }
420

421
      return { data: { user, session }, error: null }
×
422
    } catch (error) {
423
      if (isAuthError(error)) {
×
424
        return { data: { user: null, session: null }, error }
×
425
      }
426

427
      throw error
×
428
    }
429
  }
430

431
  /**
432
   * Creates a new user.
433
   *
434
   * Be aware that if a user account exists in the system you may get back an
435
   * error message that attempts to hide this information from the user.
436
   * This method has support for PKCE via email signups. The PKCE flow cannot be used when autoconfirm is enabled.
437
   *
438
   * @returns A logged-in session if the server has "autoconfirm" ON
439
   * @returns A user if the server has "autoconfirm" OFF
440
   */
441
  async signUp(credentials: SignUpWithPasswordCredentials): Promise<AuthResponse> {
442
    try {
64✔
443
      let res: AuthResponse
444
      if ('email' in credentials) {
64✔
445
        const { email, password, options } = credentials
58✔
446
        let codeChallenge: string | null = null
58✔
447
        let codeChallengeMethod: string | null = null
58✔
448
        if (this.flowType === 'pkce') {
58!
449
          ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
×
450
            this.storage,
451
            this.storageKey
452
          )
453
        }
454
        res = await _request(this.fetch, 'POST', `${this.url}/signup`, {
58✔
455
          headers: this.headers,
456
          redirectTo: options?.emailRedirectTo,
174!
457
          body: {
458
            email,
459
            password,
460
            data: options?.data ?? {},
348!
461
            gotrue_meta_security: { captcha_token: options?.captchaToken },
174!
462
            code_challenge: codeChallenge,
463
            code_challenge_method: codeChallengeMethod,
464
          },
465
          xform: _sessionResponse,
466
        })
467
      } else if ('phone' in credentials) {
6!
468
        const { phone, password, options } = credentials
6✔
469
        res = await _request(this.fetch, 'POST', `${this.url}/signup`, {
6✔
470
          headers: this.headers,
471
          body: {
472
            phone,
473
            password,
474
            data: options?.data ?? {},
36!
475
            channel: options?.channel ?? 'sms',
36!
476
            gotrue_meta_security: { captcha_token: options?.captchaToken },
18!
477
          },
478
          xform: _sessionResponse,
479
        })
480
      } else {
481
        throw new AuthInvalidCredentialsError(
×
482
          'You must provide either an email or phone number and a password'
483
        )
484
      }
485

486
      const { data, error } = res
56✔
487

488
      if (error || !data) {
56!
489
        return { data: { user: null, session: null }, error: error }
×
490
      }
491

492
      const session: Session | null = data.session
56✔
493
      const user: User | null = data.user
56✔
494

495
      if (data.session) {
56✔
496
        await this._saveSession(data.session)
54✔
497
        await this._notifyAllSubscribers('SIGNED_IN', session)
54✔
498
      }
499

500
      return { data: { user, session }, error: null }
56✔
501
    } catch (error) {
502
      if (isAuthError(error)) {
8✔
503
        return { data: { user: null, session: null }, error }
8✔
504
      }
505

506
      throw error
×
507
    }
508
  }
509

510
  /**
511
   * Log in an existing user with an email and password or phone and password.
512
   *
513
   * Be aware that you may get back an error message that will not distinguish
514
   * between the cases where the account does not exist or that the
515
   * email/phone and password combination is wrong or that the account can only
516
   * be accessed via social login.
517
   */
518
  async signInWithPassword(
519
    credentials: SignInWithPasswordCredentials
520
  ): Promise<AuthTokenResponsePassword> {
521
    try {
12✔
522
      let res: AuthResponsePassword
523
      if ('email' in credentials) {
12✔
524
        const { email, password, options } = credentials
10✔
525
        res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=password`, {
10✔
526
          headers: this.headers,
527
          body: {
528
            email,
529
            password,
530
            gotrue_meta_security: { captcha_token: options?.captchaToken },
30!
531
          },
532
          xform: _sessionResponsePassword,
533
        })
534
      } else if ('phone' in credentials) {
2!
535
        const { phone, password, options } = credentials
2✔
536
        res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=password`, {
2✔
537
          headers: this.headers,
538
          body: {
539
            phone,
540
            password,
541
            gotrue_meta_security: { captcha_token: options?.captchaToken },
6!
542
          },
543
          xform: _sessionResponsePassword,
544
        })
545
      } else {
546
        throw new AuthInvalidCredentialsError(
×
547
          'You must provide either an email or phone number and a password'
548
        )
549
      }
550
      const { data, error } = res
10✔
551

552
      if (error) {
10!
553
        return { data: { user: null, session: null }, error }
×
554
      } else if (!data || !data.session || !data.user) {
10!
555
        return { data: { user: null, session: null }, error: new AuthInvalidTokenResponseError() }
×
556
      }
557
      if (data.session) {
10✔
558
        await this._saveSession(data.session)
10✔
559
        await this._notifyAllSubscribers('SIGNED_IN', data.session)
10✔
560
      }
561
      return {
10✔
562
        data: {
563
          user: data.user,
564
          session: data.session,
565
          ...(data.weak_password ? { weakPassword: data.weak_password } : null),
10!
566
        },
567
        error,
568
      }
569
    } catch (error) {
570
      if (isAuthError(error)) {
2✔
571
        return { data: { user: null, session: null }, error }
2✔
572
      }
573
      throw error
×
574
    }
575
  }
576

577
  /**
578
   * Log in an existing user via a third-party provider.
579
   * This method supports the PKCE flow.
580
   */
581
  async signInWithOAuth(credentials: SignInWithOAuthCredentials): Promise<OAuthResponse> {
582
    return await this._handleProviderSignIn(credentials.provider, {
8✔
583
      redirectTo: credentials.options?.redirectTo,
24✔
584
      scopes: credentials.options?.scopes,
24✔
585
      queryParams: credentials.options?.queryParams,
24✔
586
      skipBrowserRedirect: credentials.options?.skipBrowserRedirect,
24✔
587
    })
588
  }
589

590
  /**
591
   * Log in an existing user by exchanging an Auth Code issued during the PKCE flow.
592
   */
593
  async exchangeCodeForSession(authCode: string): Promise<AuthTokenResponse> {
594
    await this.initializePromise
×
595

596
    return this._acquireLock(-1, async () => {
×
597
      return this._exchangeCodeForSession(authCode)
×
598
    })
599
  }
600

601
  private async _exchangeCodeForSession(authCode: string): Promise<
602
    | {
603
        data: { session: Session; user: User; redirectType: string | null }
604
        error: null
605
      }
606
    | { data: { session: null; user: null; redirectType: null }; error: AuthError }
607
  > {
608
    const storageItem = await getItemAsync(this.storage, `${this.storageKey}-code-verifier`)
×
609
    const [codeVerifier, redirectType] = ((storageItem ?? '') as string).split('/')
×
610

611
    try {
×
612
      const { data, error } = await _request(
×
613
        this.fetch,
614
        'POST',
615
        `${this.url}/token?grant_type=pkce`,
616
        {
617
          headers: this.headers,
618
          body: {
619
            auth_code: authCode,
620
            code_verifier: codeVerifier,
621
          },
622
          xform: _sessionResponse,
623
        }
624
      )
625
      await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
×
626
      if (error) {
×
627
        throw error
×
628
      }
629
      if (!data || !data.session || !data.user) {
×
630
        return {
×
631
          data: { user: null, session: null, redirectType: null },
632
          error: new AuthInvalidTokenResponseError(),
633
        }
634
      }
635
      if (data.session) {
×
636
        await this._saveSession(data.session)
×
637
        await this._notifyAllSubscribers('SIGNED_IN', data.session)
×
638
      }
639
      return { data: { ...data, redirectType: redirectType ?? null }, error }
×
640
    } catch (error) {
641
      if (isAuthError(error)) {
×
642
        return { data: { user: null, session: null, redirectType: null }, error }
×
643
      }
644

645
      throw error
×
646
    }
647
  }
648

649
  /**
650
   * Allows signing in with an OIDC ID token. The authentication provider used
651
   * should be enabled and configured.
652
   */
653
  async signInWithIdToken(credentials: SignInWithIdTokenCredentials): Promise<AuthTokenResponse> {
654
    try {
×
655
      const { options, provider, token, access_token, nonce } = credentials
×
656

657
      const res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=id_token`, {
×
658
        headers: this.headers,
659
        body: {
660
          provider,
661
          id_token: token,
662
          access_token,
663
          nonce,
664
          gotrue_meta_security: { captcha_token: options?.captchaToken },
×
665
        },
666
        xform: _sessionResponse,
667
      })
668

669
      const { data, error } = res
×
670
      if (error) {
×
671
        return { data: { user: null, session: null }, error }
×
672
      } else if (!data || !data.session || !data.user) {
×
673
        return {
×
674
          data: { user: null, session: null },
675
          error: new AuthInvalidTokenResponseError(),
676
        }
677
      }
678
      if (data.session) {
×
679
        await this._saveSession(data.session)
×
680
        await this._notifyAllSubscribers('SIGNED_IN', data.session)
×
681
      }
682
      return { data, error }
×
683
    } catch (error) {
684
      if (isAuthError(error)) {
×
685
        return { data: { user: null, session: null }, error }
×
686
      }
687
      throw error
×
688
    }
689
  }
690

691
  /**
692
   * Log in a user using magiclink or a one-time password (OTP).
693
   *
694
   * If the `{{ .ConfirmationURL }}` variable is specified in the email template, a magiclink will be sent.
695
   * If the `{{ .Token }}` variable is specified in the email template, an OTP will be sent.
696
   * 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.
697
   *
698
   * Be aware that you may get back an error message that will not distinguish
699
   * between the cases where the account does not exist or, that the account
700
   * can only be accessed via social login.
701
   *
702
   * Do note that you will need to configure a Whatsapp sender on Twilio
703
   * if you are using phone sign in with the 'whatsapp' channel. The whatsapp
704
   * channel is not supported on other providers
705
   * at this time.
706
   * This method supports PKCE when an email is passed.
707
   */
708
  async signInWithOtp(credentials: SignInWithPasswordlessCredentials): Promise<AuthOtpResponse> {
709
    try {
4✔
710
      if ('email' in credentials) {
4✔
711
        const { email, options } = credentials
2✔
712
        let codeChallenge: string | null = null
2✔
713
        let codeChallengeMethod: string | null = null
2✔
714
        if (this.flowType === 'pkce') {
2!
715
          ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
×
716
            this.storage,
717
            this.storageKey
718
          )
719
        }
720
        const { error } = await _request(this.fetch, 'POST', `${this.url}/otp`, {
2✔
721
          headers: this.headers,
722
          body: {
723
            email,
724
            data: options?.data ?? {},
12!
725
            create_user: options?.shouldCreateUser ?? true,
12!
726
            gotrue_meta_security: { captcha_token: options?.captchaToken },
6!
727
            code_challenge: codeChallenge,
728
            code_challenge_method: codeChallengeMethod,
729
          },
730
          redirectTo: options?.emailRedirectTo,
6!
731
        })
732
        return { data: { user: null, session: null }, error }
2✔
733
      }
734
      if ('phone' in credentials) {
2✔
735
        const { phone, options } = credentials
2✔
736
        const { data, error } = await _request(this.fetch, 'POST', `${this.url}/otp`, {
2✔
737
          headers: this.headers,
738
          body: {
739
            phone,
740
            data: options?.data ?? {},
12!
741
            create_user: options?.shouldCreateUser ?? true,
12!
742
            gotrue_meta_security: { captcha_token: options?.captchaToken },
6!
743
            channel: options?.channel ?? 'sms',
12!
744
          },
745
        })
746
        return { data: { user: null, session: null, messageId: data?.message_id }, error }
×
747
      }
748
      throw new AuthInvalidCredentialsError('You must provide either an email or phone number.')
×
749
    } catch (error) {
750
      if (isAuthError(error)) {
2✔
751
        return { data: { user: null, session: null }, error }
2✔
752
      }
753

754
      throw error
×
755
    }
756
  }
757

758
  /**
759
   * Log in a user given a User supplied OTP or TokenHash received through mobile or email.
760
   */
761
  async verifyOtp(params: VerifyOtpParams): Promise<AuthResponse> {
762
    try {
4✔
763
      let redirectTo: string | undefined = undefined
4✔
764
      let captchaToken: string | undefined = undefined
4✔
765
      if ('options' in params) {
4!
766
        redirectTo = params.options?.redirectTo
×
767
        captchaToken = params.options?.captchaToken
×
768
      }
769
      const { data, error } = await _request(this.fetch, 'POST', `${this.url}/verify`, {
4✔
770
        headers: this.headers,
771
        body: {
772
          ...params,
773
          gotrue_meta_security: { captcha_token: captchaToken },
774
        },
775
        redirectTo,
776
        xform: _sessionResponse,
777
      })
778

779
      if (error) {
×
780
        throw error
×
781
      }
782

783
      if (!data) {
×
784
        throw new Error('An error occurred on token verification.')
×
785
      }
786

787
      const session: Session | null = data.session
×
788
      const user: User = data.user
×
789

790
      if (session?.access_token) {
×
791
        await this._saveSession(session as Session)
×
792
        await this._notifyAllSubscribers(
×
793
          params.type == 'recovery' ? 'PASSWORD_RECOVERY' : 'SIGNED_IN',
×
794
          session
795
        )
796
      }
797

798
      return { data: { user, session }, error: null }
×
799
    } catch (error) {
800
      if (isAuthError(error)) {
4✔
801
        return { data: { user: null, session: null }, error }
4✔
802
      }
803

804
      throw error
×
805
    }
806
  }
807

808
  /**
809
   * Attempts a single-sign on using an enterprise Identity Provider. A
810
   * successful SSO attempt will redirect the current page to the identity
811
   * provider authorization page. The redirect URL is implementation and SSO
812
   * protocol specific.
813
   *
814
   * You can use it by providing a SSO domain. Typically you can extract this
815
   * domain by asking users for their email address. If this domain is
816
   * registered on the Auth instance the redirect will use that organization's
817
   * currently active SSO Identity Provider for the login.
818
   *
819
   * If you have built an organization-specific login page, you can use the
820
   * organization's SSO Identity Provider UUID directly instead.
821
   */
822
  async signInWithSSO(params: SignInWithSSO): Promise<SSOResponse> {
823
    try {
×
824
      let codeChallenge: string | null = null
×
825
      let codeChallengeMethod: string | null = null
×
826
      if (this.flowType === 'pkce') {
×
827
        ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
×
828
          this.storage,
829
          this.storageKey
830
        )
831
      }
832

833
      return await _request(this.fetch, 'POST', `${this.url}/sso`, {
×
834
        body: {
835
          ...('providerId' in params ? { provider_id: params.providerId } : null),
×
836
          ...('domain' in params ? { domain: params.domain } : null),
×
837
          redirect_to: params.options?.redirectTo ?? undefined,
×
838
          ...(params?.options?.captchaToken
×
839
            ? { gotrue_meta_security: { captcha_token: params.options.captchaToken } }
840
            : null),
841
          skip_http_redirect: true, // fetch does not handle redirects
842
          code_challenge: codeChallenge,
843
          code_challenge_method: codeChallengeMethod,
844
        },
845
        headers: this.headers,
846
        xform: _ssoResponse,
847
      })
848
    } catch (error) {
849
      if (isAuthError(error)) {
×
850
        return { data: null, error }
×
851
      }
852
      throw error
×
853
    }
854
  }
855

856
  /**
857
   * Sends a reauthentication OTP to the user's email or phone number.
858
   * Requires the user to be signed-in.
859
   */
860
  async reauthenticate(): Promise<AuthResponse> {
861
    await this.initializePromise
×
862

863
    return await this._acquireLock(-1, async () => {
×
864
      return await this._reauthenticate()
×
865
    })
866
  }
867

868
  private async _reauthenticate(): Promise<AuthResponse> {
869
    try {
×
870
      return await this._useSession(async (result) => {
×
871
        const {
872
          data: { session },
873
          error: sessionError,
874
        } = result
×
875
        if (sessionError) throw sessionError
×
876
        if (!session) throw new AuthSessionMissingError()
×
877

878
        const { error } = await _request(this.fetch, 'GET', `${this.url}/reauthenticate`, {
×
879
          headers: this.headers,
880
          jwt: session.access_token,
881
        })
882
        return { data: { user: null, session: null }, error }
×
883
      })
884
    } catch (error) {
885
      if (isAuthError(error)) {
×
886
        return { data: { user: null, session: null }, error }
×
887
      }
888
      throw error
×
889
    }
890
  }
891

892
  /**
893
   * Resends an existing signup confirmation email, email change email, SMS OTP or phone change OTP.
894
   */
895
  async resend(credentials: ResendParams): Promise<AuthOtpResponse> {
896
    try {
×
897
      const endpoint = `${this.url}/resend`
×
898
      if ('email' in credentials) {
×
899
        const { email, type, options } = credentials
×
900
        const { error } = await _request(this.fetch, 'POST', endpoint, {
×
901
          headers: this.headers,
902
          body: {
903
            email,
904
            type,
905
            gotrue_meta_security: { captcha_token: options?.captchaToken },
×
906
          },
907
          redirectTo: options?.emailRedirectTo,
×
908
        })
909
        return { data: { user: null, session: null }, error }
×
910
      } else if ('phone' in credentials) {
×
911
        const { phone, type, options } = credentials
×
912
        const { data, error } = await _request(this.fetch, 'POST', endpoint, {
×
913
          headers: this.headers,
914
          body: {
915
            phone,
916
            type,
917
            gotrue_meta_security: { captcha_token: options?.captchaToken },
×
918
          },
919
        })
920
        return { data: { user: null, session: null, messageId: data?.message_id }, error }
×
921
      }
922
      throw new AuthInvalidCredentialsError(
×
923
        'You must provide either an email or phone number and a type'
924
      )
925
    } catch (error) {
926
      if (isAuthError(error)) {
×
927
        return { data: { user: null, session: null }, error }
×
928
      }
929
      throw error
×
930
    }
931
  }
932

933
  /**
934
   * Returns the session, refreshing it if necessary.
935
   *
936
   * 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.
937
   *
938
   * **IMPORTANT:** This method loads values directly from the storage attached
939
   * to the client. If that storage is based on request cookies for example,
940
   * the values in it may not be authentic and therefore it's strongly advised
941
   * against using this method and its results in such circumstances. A warning
942
   * will be emitted if this is detected. Use {@link #getUser()} instead.
943
   */
944
  async getSession() {
945
    await this.initializePromise
26✔
946

947
    const result = await this._acquireLock(-1, async () => {
26✔
948
      return this._useSession(async (result) => {
26✔
949
        return result
26✔
950
      })
951
    })
952

953
    return result
26✔
954
  }
955

956
  /**
957
   * Acquires a global lock based on the storage key.
958
   */
959
  private async _acquireLock<R>(acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
960
    this._debug('#_acquireLock', 'begin', acquireTimeout)
182✔
961

962
    try {
182✔
963
      if (this.lockAcquired) {
182!
964
        const last = this.pendingInLock.length
×
965
          ? this.pendingInLock[this.pendingInLock.length - 1]
966
          : Promise.resolve()
967

968
        const result = (async () => {
×
969
          await last
×
970
          return await fn()
×
971
        })()
972

973
        this.pendingInLock.push(
×
974
          (async () => {
975
            try {
×
976
              await result
×
977
            } catch (e: any) {
978
              // we just care if it finished
979
            }
980
          })()
981
        )
982

983
        return result
×
984
      }
985

986
      return await this.lock(`lock:${this.storageKey}`, acquireTimeout, async () => {
182✔
987
        this._debug('#_acquireLock', 'lock acquired for storage key', this.storageKey)
182✔
988

989
        try {
182✔
990
          this.lockAcquired = true
182✔
991

992
          const result = fn()
182✔
993

994
          this.pendingInLock.push(
182✔
995
            (async () => {
996
              try {
182✔
997
                await result
182✔
998
              } catch (e: any) {
999
                // we just care if it finished
1000
              }
1001
            })()
1002
          )
1003

1004
          await result
182✔
1005

1006
          // keep draining the queue until there's nothing to wait on
1007
          while (this.pendingInLock.length) {
182✔
1008
            const waitOn = [...this.pendingInLock]
182✔
1009

1010
            await Promise.all(waitOn)
182✔
1011

1012
            this.pendingInLock.splice(0, waitOn.length)
182✔
1013
          }
1014

1015
          return await result
182✔
1016
        } finally {
1017
          this._debug('#_acquireLock', 'lock released for storage key', this.storageKey)
182✔
1018

1019
          this.lockAcquired = false
182✔
1020
        }
1021
      })
1022
    } finally {
1023
      this._debug('#_acquireLock', 'end')
182✔
1024
    }
1025
  }
1026

1027
  /**
1028
   * Use instead of {@link #getSession} inside the library. It is
1029
   * semantically usually what you want, as getting a session involves some
1030
   * processing afterwards that requires only one client operating on the
1031
   * session at once across multiple tabs or processes.
1032
   */
1033
  private async _useSession<R>(
1034
    fn: (
1035
      result:
1036
        | {
1037
            data: {
1038
              session: Session
1039
            }
1040
            error: null
1041
          }
1042
        | {
1043
            data: {
1044
              session: null
1045
            }
1046
            error: AuthError
1047
          }
1048
        | {
1049
            data: {
1050
              session: null
1051
            }
1052
            error: null
1053
          }
1054
    ) => Promise<R>
1055
  ): Promise<R> {
1056
    this._debug('#_useSession', 'begin')
132✔
1057

1058
    try {
132✔
1059
      // the use of __loadSession here is the only correct use of the function!
1060
      const result = await this.__loadSession()
132✔
1061

1062
      return await fn(result)
132✔
1063
    } finally {
1064
      this._debug('#_useSession', 'end')
132✔
1065
    }
1066
  }
1067

1068
  /**
1069
   * NEVER USE DIRECTLY!
1070
   *
1071
   * Always use {@link #_useSession}.
1072
   */
1073
  private async __loadSession(): Promise<
1074
    | {
1075
        data: {
1076
          session: Session
1077
        }
1078
        error: null
1079
      }
1080
    | {
1081
        data: {
1082
          session: null
1083
        }
1084
        error: AuthError
1085
      }
1086
    | {
1087
        data: {
1088
          session: null
1089
        }
1090
        error: null
1091
      }
1092
  > {
1093
    this._debug('#__loadSession()', 'begin')
132✔
1094

1095
    if (!this.lockAcquired) {
132✔
1096
      this._debug('#__loadSession()', 'used outside of an acquired lock!', new Error().stack)
2✔
1097
    }
1098

1099
    try {
132✔
1100
      let currentSession: Session | null = null
132✔
1101

1102
      const maybeSession = await getItemAsync(this.storage, this.storageKey)
132✔
1103

1104
      this._debug('#getSession()', 'session from storage', maybeSession)
132✔
1105

1106
      if (maybeSession !== null) {
132✔
1107
        if (this._isValidSession(maybeSession)) {
74!
1108
          currentSession = maybeSession
74✔
1109
        } else {
1110
          this._debug('#getSession()', 'session from storage is not valid')
×
1111
          await this._removeSession()
×
1112
        }
1113
      }
1114

1115
      if (!currentSession) {
132✔
1116
        return { data: { session: null }, error: null }
58✔
1117
      }
1118

1119
      // A session is considered expired before the access token _actually_
1120
      // expires. When the autoRefreshToken option is off (or when the tab is
1121
      // in the background), very eager users of getSession() -- like
1122
      // realtime-js -- might send a valid JWT which will expire by the time it
1123
      // reaches the server.
1124
      const hasExpired = currentSession.expires_at
74!
1125
        ? currentSession.expires_at * 1000 - Date.now() < EXPIRY_MARGIN_MS
1126
        : false
1127

1128
      this._debug(
74✔
1129
        '#__loadSession()',
1130
        `session has${hasExpired ? '' : ' not'} expired`,
74✔
1131
        'expires_at',
1132
        currentSession.expires_at
1133
      )
1134

1135
      if (!hasExpired) {
74✔
1136
        if (this.storage.isServer) {
72✔
1137
          let suppressWarning = this.suppressGetSessionWarning
14✔
1138
          const proxySession: Session = new Proxy(currentSession, {
14✔
1139
            get: (target: any, prop: string, receiver: any) => {
1140
              if (!suppressWarning && prop === 'user') {
24✔
1141
                // only show warning when the user object is being accessed from the server
1142
                console.warn(
2✔
1143
                  '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.'
1144
                )
1145
                suppressWarning = true // keeps this proxy instance from logging additional warnings
2✔
1146
                this.suppressGetSessionWarning = true // keeps this client's future proxy instances from warning
2✔
1147
              }
1148
              return Reflect.get(target, prop, receiver)
24✔
1149
            },
1150
          })
1151
          currentSession = proxySession
14✔
1152
        }
1153

1154
        return { data: { session: currentSession }, error: null }
72✔
1155
      }
1156

1157
      const { session, error } = await this._callRefreshToken(currentSession.refresh_token)
2✔
1158
      if (error) {
2!
1159
        return { data: { session: null }, error }
×
1160
      }
1161

1162
      return { data: { session }, error: null }
2✔
1163
    } finally {
1164
      this._debug('#__loadSession()', 'end')
132✔
1165
    }
1166
  }
1167

1168
  /**
1169
   * Gets the current user details if there is an existing session. This method
1170
   * performs a network request to the Supabase Auth server, so the returned
1171
   * value is authentic and can be used to base authorization rules on.
1172
   *
1173
   * @param jwt Takes in an optional access token JWT. If no JWT is provided, the JWT from the current session is used.
1174
   */
1175
  async getUser(jwt?: string): Promise<UserResponse> {
1176
    if (jwt) {
9✔
1177
      return await this._getUser(jwt)
3✔
1178
    }
1179

1180
    await this.initializePromise
6✔
1181

1182
    const result = await this._acquireLock(-1, async () => {
6✔
1183
      return await this._getUser()
6✔
1184
    })
1185

1186
    return result
6✔
1187
  }
1188

1189
  private async _getUser(jwt?: string): Promise<UserResponse> {
1190
    try {
11✔
1191
      if (jwt) {
11✔
1192
        return await _request(this.fetch, 'GET', `${this.url}/user`, {
5✔
1193
          headers: this.headers,
1194
          jwt: jwt,
1195
          xform: _userResponse,
1196
        })
1197
      }
1198

1199
      return await this._useSession(async (result) => {
6✔
1200
        const { data, error } = result
6✔
1201
        if (error) {
6!
1202
          throw error
×
1203
        }
1204

1205
        // returns an error if there is no access_token or custom authorization header
1206
        if (!data.session?.access_token && !this.hasCustomAuthorizationHeader) {
6✔
1207
          return { data: { user: null }, error: new AuthSessionMissingError() }
2✔
1208
        }
1209

1210
        return await _request(this.fetch, 'GET', `${this.url}/user`, {
4✔
1211
          headers: this.headers,
1212
          jwt: data.session?.access_token ?? undefined,
24!
1213
          xform: _userResponse,
1214
        })
1215
      })
1216
    } catch (error) {
1217
      if (isAuthError(error)) {
×
1218
        if (isAuthSessionMissingError(error)) {
×
1219
          // JWT contains a `session_id` which does not correspond to an active
1220
          // session in the database, indicating the user is signed out.
1221

1222
          await this._removeSession()
×
1223
          await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
×
1224
        }
1225

1226
        return { data: { user: null }, error }
×
1227
      }
1228

1229
      throw error
×
1230
    }
1231
  }
1232

1233
  /**
1234
   * Updates user data for a logged in user.
1235
   */
1236
  async updateUser(
1237
    attributes: UserAttributes,
1238
    options: {
6✔
1239
      emailRedirectTo?: string | undefined
1240
    } = {}
1241
  ): Promise<UserResponse> {
1242
    await this.initializePromise
6✔
1243

1244
    return await this._acquireLock(-1, async () => {
6✔
1245
      return await this._updateUser(attributes, options)
6✔
1246
    })
1247
  }
1248

1249
  protected async _updateUser(
1250
    attributes: UserAttributes,
1251
    options: {
×
1252
      emailRedirectTo?: string | undefined
1253
    } = {}
1254
  ): Promise<UserResponse> {
1255
    try {
6✔
1256
      return await this._useSession(async (result) => {
6✔
1257
        const { data: sessionData, error: sessionError } = result
6✔
1258
        if (sessionError) {
6!
1259
          throw sessionError
×
1260
        }
1261
        if (!sessionData.session) {
6!
1262
          throw new AuthSessionMissingError()
×
1263
        }
1264
        const session: Session = sessionData.session
6✔
1265
        let codeChallenge: string | null = null
6✔
1266
        let codeChallengeMethod: string | null = null
6✔
1267
        if (this.flowType === 'pkce' && attributes.email != null) {
6!
1268
          ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
×
1269
            this.storage,
1270
            this.storageKey
1271
          )
1272
        }
1273

1274
        const { data, error: userError } = await _request(this.fetch, 'PUT', `${this.url}/user`, {
6✔
1275
          headers: this.headers,
1276
          redirectTo: options?.emailRedirectTo,
18!
1277
          body: {
1278
            ...attributes,
1279
            code_challenge: codeChallenge,
1280
            code_challenge_method: codeChallengeMethod,
1281
          },
1282
          jwt: session.access_token,
1283
          xform: _userResponse,
1284
        })
1285
        if (userError) throw userError
6!
1286
        session.user = data.user as User
6✔
1287
        await this._saveSession(session)
6✔
1288
        await this._notifyAllSubscribers('USER_UPDATED', session)
6✔
1289
        return { data: { user: session.user }, error: null }
6✔
1290
      })
1291
    } catch (error) {
1292
      if (isAuthError(error)) {
×
1293
        return { data: { user: null }, error }
×
1294
      }
1295

1296
      throw error
×
1297
    }
1298
  }
1299

1300
  /**
1301
   * 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.
1302
   * If the refresh token or access token in the current session is invalid, an error will be thrown.
1303
   * @param currentSession The current session that minimally contains an access token and refresh token.
1304
   */
1305
  async setSession(currentSession: {
1306
    access_token: string
1307
    refresh_token: string
1308
  }): Promise<AuthResponse> {
1309
    await this.initializePromise
2✔
1310

1311
    return await this._acquireLock(-1, async () => {
2✔
1312
      return await this._setSession(currentSession)
2✔
1313
    })
1314
  }
1315

1316
  protected async _setSession(currentSession: {
1317
    access_token: string
1318
    refresh_token: string
1319
  }): Promise<AuthResponse> {
1320
    try {
2✔
1321
      if (!currentSession.access_token || !currentSession.refresh_token) {
2!
1322
        throw new AuthSessionMissingError()
×
1323
      }
1324

1325
      const timeNow = Date.now() / 1000
2✔
1326
      let expiresAt = timeNow
2✔
1327
      let hasExpired = true
2✔
1328
      let session: Session | null = null
2✔
1329
      const { payload } = decodeJWT(currentSession.access_token)
2✔
1330
      if (payload.exp) {
2✔
1331
        expiresAt = payload.exp
2✔
1332
        hasExpired = expiresAt <= timeNow
2✔
1333
      }
1334

1335
      if (hasExpired) {
2!
1336
        const { session: refreshedSession, error } = await this._callRefreshToken(
×
1337
          currentSession.refresh_token
1338
        )
1339
        if (error) {
×
1340
          return { data: { user: null, session: null }, error: error }
×
1341
        }
1342

1343
        if (!refreshedSession) {
×
1344
          return { data: { user: null, session: null }, error: null }
×
1345
        }
1346
        session = refreshedSession
×
1347
      } else {
1348
        const { data, error } = await this._getUser(currentSession.access_token)
2✔
1349
        if (error) {
2!
1350
          throw error
×
1351
        }
1352
        session = {
2✔
1353
          access_token: currentSession.access_token,
1354
          refresh_token: currentSession.refresh_token,
1355
          user: data.user,
1356
          token_type: 'bearer',
1357
          expires_in: expiresAt - timeNow,
1358
          expires_at: expiresAt,
1359
        }
1360
        await this._saveSession(session)
2✔
1361
        await this._notifyAllSubscribers('SIGNED_IN', session)
2✔
1362
      }
1363

1364
      return { data: { user: session.user, session }, error: null }
2✔
1365
    } catch (error) {
1366
      if (isAuthError(error)) {
×
1367
        return { data: { session: null, user: null }, error }
×
1368
      }
1369

1370
      throw error
×
1371
    }
1372
  }
1373

1374
  /**
1375
   * Returns a new session, regardless of expiry status.
1376
   * Takes in an optional current session. If not passed in, then refreshSession() will attempt to retrieve it from getSession().
1377
   * If the current session's refresh token is invalid, an error will be thrown.
1378
   * @param currentSession The current session. If passed in, it must contain a refresh token.
1379
   */
1380
  async refreshSession(currentSession?: { refresh_token: string }): Promise<AuthResponse> {
1381
    await this.initializePromise
4✔
1382

1383
    return await this._acquireLock(-1, async () => {
4✔
1384
      return await this._refreshSession(currentSession)
4✔
1385
    })
1386
  }
1387

1388
  protected async _refreshSession(currentSession?: {
1389
    refresh_token: string
1390
  }): Promise<AuthResponse> {
1391
    try {
4✔
1392
      return await this._useSession(async (result) => {
4✔
1393
        if (!currentSession) {
4✔
1394
          const { data, error } = result
2✔
1395
          if (error) {
2!
1396
            throw error
×
1397
          }
1398

1399
          currentSession = data.session ?? undefined
2!
1400
        }
1401

1402
        if (!currentSession?.refresh_token) {
4!
1403
          throw new AuthSessionMissingError()
×
1404
        }
1405

1406
        const { session, error } = await this._callRefreshToken(currentSession.refresh_token)
4✔
1407
        if (error) {
4!
1408
          return { data: { user: null, session: null }, error: error }
×
1409
        }
1410

1411
        if (!session) {
4!
1412
          return { data: { user: null, session: null }, error: null }
×
1413
        }
1414

1415
        return { data: { user: session.user, session }, error: null }
4✔
1416
      })
1417
    } catch (error) {
1418
      if (isAuthError(error)) {
×
1419
        return { data: { user: null, session: null }, error }
×
1420
      }
1421

1422
      throw error
×
1423
    }
1424
  }
1425

1426
  /**
1427
   * Gets the session data from a URL string
1428
   */
1429
  private async _getSessionFromURL(
1430
    params: { [parameter: string]: string },
1431
    callbackUrlType: string
1432
  ): Promise<
1433
    | {
1434
        data: { session: Session; redirectType: string | null }
1435
        error: null
1436
      }
1437
    | { data: { session: null; redirectType: null }; error: AuthError }
1438
  > {
1439
    try {
2✔
1440
      if (!isBrowser()) throw new AuthImplicitGrantRedirectError('No browser detected.')
2✔
1441

1442
      // If there's an error in the URL, it doesn't matter what flow it is, we just return the error.
1443
      if (params.error || params.error_description || params.error_code) {
×
1444
        // The error class returned implies that the redirect is from an implicit grant flow
1445
        // but it could also be from a redirect error from a PKCE flow.
1446
        throw new AuthImplicitGrantRedirectError(
×
1447
          params.error_description || 'Error in URL with unspecified error_description',
×
1448
          {
1449
            error: params.error || 'unspecified_error',
×
1450
            code: params.error_code || 'unspecified_code',
×
1451
          }
1452
        )
1453
      }
1454

1455
      // Checks for mismatches between the flowType initialised in the client and the URL parameters
1456
      switch (callbackUrlType) {
×
1457
        case 'implicit':
1458
          if (this.flowType === 'pkce') {
×
1459
            throw new AuthPKCEGrantCodeExchangeError('Not a valid PKCE flow url.')
×
1460
          }
1461
          break
×
1462
        case 'pkce':
1463
          if (this.flowType === 'implicit') {
×
1464
            throw new AuthImplicitGrantRedirectError('Not a valid implicit grant flow url.')
×
1465
          }
1466
          break
×
1467
        default:
1468
        // there's no mismatch so we continue
1469
      }
1470

1471
      // Since this is a redirect for PKCE, we attempt to retrieve the code from the URL for the code exchange
1472
      if (callbackUrlType === 'pkce') {
×
1473
        this._debug('#_initialize()', 'begin', 'is PKCE flow', true)
×
1474
        if (!params.code) throw new AuthPKCEGrantCodeExchangeError('No code detected.')
×
1475
        const { data, error } = await this._exchangeCodeForSession(params.code)
×
1476
        if (error) throw error
×
1477

1478
        const url = new URL(window.location.href)
×
1479
        url.searchParams.delete('code')
×
1480

1481
        window.history.replaceState(window.history.state, '', url.toString())
×
1482

1483
        return { data: { session: data.session, redirectType: null }, error: null }
×
1484
      }
1485

1486
      const {
1487
        provider_token,
1488
        provider_refresh_token,
1489
        access_token,
1490
        refresh_token,
1491
        expires_in,
1492
        expires_at,
1493
        token_type,
1494
      } = params
×
1495

1496
      if (!access_token || !expires_in || !refresh_token || !token_type) {
×
1497
        throw new AuthImplicitGrantRedirectError('No session defined in URL')
×
1498
      }
1499

1500
      const timeNow = Math.round(Date.now() / 1000)
×
1501
      const expiresIn = parseInt(expires_in)
×
1502
      let expiresAt = timeNow + expiresIn
×
1503

1504
      if (expires_at) {
×
1505
        expiresAt = parseInt(expires_at)
×
1506
      }
1507

1508
      const actuallyExpiresIn = expiresAt - timeNow
×
1509
      if (actuallyExpiresIn * 1000 <= AUTO_REFRESH_TICK_DURATION_MS) {
×
1510
        console.warn(
×
1511
          `@supabase/gotrue-js: Session as retrieved from URL expires in ${actuallyExpiresIn}s, should have been closer to ${expiresIn}s`
1512
        )
1513
      }
1514

1515
      const issuedAt = expiresAt - expiresIn
×
1516
      if (timeNow - issuedAt >= 120) {
×
1517
        console.warn(
×
1518
          '@supabase/gotrue-js: Session as retrieved from URL was issued over 120s ago, URL could be stale',
1519
          issuedAt,
1520
          expiresAt,
1521
          timeNow
1522
        )
1523
      } else if (timeNow - issuedAt < 0) {
×
1524
        console.warn(
×
1525
          '@supabase/gotrue-js: Session as retrieved from URL was issued in the future? Check the device clock for skew',
1526
          issuedAt,
1527
          expiresAt,
1528
          timeNow
1529
        )
1530
      }
1531

1532
      const { data, error } = await this._getUser(access_token)
×
1533
      if (error) throw error
×
1534

1535
      const session: Session = {
×
1536
        provider_token,
1537
        provider_refresh_token,
1538
        access_token,
1539
        expires_in: expiresIn,
1540
        expires_at: expiresAt,
1541
        refresh_token,
1542
        token_type,
1543
        user: data.user,
1544
      }
1545

1546
      // Remove tokens from URL
1547
      window.location.hash = ''
×
1548
      this._debug('#_getSessionFromURL()', 'clearing window.location.hash')
×
1549

1550
      return { data: { session, redirectType: params.type }, error: null }
×
1551
    } catch (error) {
1552
      if (isAuthError(error)) {
2✔
1553
        return { data: { session: null, redirectType: null }, error }
2✔
1554
      }
1555

1556
      throw error
×
1557
    }
1558
  }
1559

1560
  /**
1561
   * 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)
1562
   */
1563
  private _isImplicitGrantCallback(params: { [parameter: string]: string }): boolean {
1564
    return Boolean(params.access_token || params.error_description)
×
1565
  }
1566

1567
  /**
1568
   * Checks if the current URL and backing storage contain parameters given by a PKCE flow
1569
   */
1570
  private async _isPKCECallback(params: { [parameter: string]: string }): Promise<boolean> {
1571
    const currentStorageContent = await getItemAsync(
×
1572
      this.storage,
1573
      `${this.storageKey}-code-verifier`
1574
    )
1575

1576
    return !!(params.code && currentStorageContent)
×
1577
  }
1578

1579
  /**
1580
   * 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.
1581
   *
1582
   * 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)`.
1583
   * 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.
1584
   *
1585
   * If using `others` scope, no `SIGNED_OUT` event is fired!
1586
   */
1587
  async signOut(options: SignOut = { scope: 'global' }): Promise<{ error: AuthError | null }> {
82✔
1588
    await this.initializePromise
82✔
1589

1590
    return await this._acquireLock(-1, async () => {
82✔
1591
      return await this._signOut(options)
82✔
1592
    })
1593
  }
1594

1595
  protected async _signOut(
1596
    { scope }: SignOut = { scope: 'global' }
×
1597
  ): Promise<{ error: AuthError | null }> {
1598
    return await this._useSession(async (result) => {
82✔
1599
      const { data, error: sessionError } = result
82✔
1600
      if (sessionError) {
82!
1601
        return { error: sessionError }
×
1602
      }
1603
      const accessToken = data.session?.access_token
82✔
1604
      if (accessToken) {
82✔
1605
        const { error } = await this.admin.signOut(accessToken, scope)
32✔
1606
        if (error) {
32✔
1607
          // ignore 404s since user might not exist anymore
1608
          // ignore 401s since an invalid or expired JWT should sign out the current session
1609
          if (
2!
1610
            !(
1611
              isAuthApiError(error) &&
8✔
1612
              (error.status === 404 || error.status === 401 || error.status === 403)
1613
            )
1614
          ) {
1615
            return { error }
×
1616
          }
1617
        }
1618
      }
1619
      if (scope !== 'others') {
82✔
1620
        await this._removeSession()
82✔
1621
        await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
82✔
1622
      }
1623
      return { error: null }
82✔
1624
    })
1625
  }
1626

1627
  /**
1628
   * Receive a notification every time an auth event happens.
1629
   * @param callback A callback function to be invoked when an auth event happens.
1630
   */
1631
  onAuthStateChange(
1632
    callback: (event: AuthChangeEvent, session: Session | null) => void | Promise<void>
1633
  ): {
1634
    data: { subscription: Subscription }
1635
  } {
1636
    const id: string = uuid()
2✔
1637
    const subscription: Subscription = {
2✔
1638
      id,
1639
      callback,
1640
      unsubscribe: () => {
1641
        this._debug('#unsubscribe()', 'state change callback with id removed', id)
2✔
1642

1643
        this.stateChangeEmitters.delete(id)
2✔
1644
      },
1645
    }
1646

1647
    this._debug('#onAuthStateChange()', 'registered callback with id', id)
2✔
1648

1649
    this.stateChangeEmitters.set(id, subscription)
2✔
1650
    ;(async () => {
2✔
1651
      await this.initializePromise
2✔
1652

1653
      await this._acquireLock(-1, async () => {
2✔
1654
        this._emitInitialSession(id)
2✔
1655
      })
1656
    })()
1657

1658
    return { data: { subscription } }
2✔
1659
  }
1660

1661
  private async _emitInitialSession(id: string): Promise<void> {
1662
    return await this._useSession(async (result) => {
2✔
1663
      try {
2✔
1664
        const {
1665
          data: { session },
1666
          error,
1667
        } = result
2✔
1668
        if (error) throw error
2!
1669

1670
        await this.stateChangeEmitters.get(id)?.callback('INITIAL_SESSION', session)
2!
1671
        this._debug('INITIAL_SESSION', 'callback id', id, 'session', session)
2✔
1672
      } catch (err) {
1673
        await this.stateChangeEmitters.get(id)?.callback('INITIAL_SESSION', null)
×
1674
        this._debug('INITIAL_SESSION', 'callback id', id, 'error', err)
×
1675
        console.error(err)
×
1676
      }
1677
    })
1678
  }
1679

1680
  /**
1681
   * Sends a password reset request to an email address. This method supports the PKCE flow.
1682
   *
1683
   * @param email The email address of the user.
1684
   * @param options.redirectTo The URL to send the user to after they click the password reset link.
1685
   * @param options.captchaToken Verification token received when the user completes the captcha on the site.
1686
   */
1687
  async resetPasswordForEmail(
1688
    email: string,
1689
    options: {
×
1690
      redirectTo?: string
1691
      captchaToken?: string
1692
    } = {}
1693
  ): Promise<
1694
    | {
1695
        data: {}
1696
        error: null
1697
      }
1698
    | { data: null; error: AuthError }
1699
  > {
1700
    let codeChallenge: string | null = null
4✔
1701
    let codeChallengeMethod: string | null = null
4✔
1702

1703
    if (this.flowType === 'pkce') {
4!
1704
      ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
×
1705
        this.storage,
1706
        this.storageKey,
1707
        true // isPasswordRecovery
1708
      )
1709
    }
1710
    try {
4✔
1711
      return await _request(this.fetch, 'POST', `${this.url}/recover`, {
4✔
1712
        body: {
1713
          email,
1714
          code_challenge: codeChallenge,
1715
          code_challenge_method: codeChallengeMethod,
1716
          gotrue_meta_security: { captcha_token: options.captchaToken },
1717
        },
1718
        headers: this.headers,
1719
        redirectTo: options.redirectTo,
1720
      })
1721
    } catch (error) {
1722
      if (isAuthError(error)) {
×
1723
        return { data: null, error }
×
1724
      }
1725

1726
      throw error
×
1727
    }
1728
  }
1729

1730
  /**
1731
   * Gets all the identities linked to a user.
1732
   */
1733
  async getUserIdentities(): Promise<
1734
    | {
1735
        data: {
1736
          identities: UserIdentity[]
1737
        }
1738
        error: null
1739
      }
1740
    | { data: null; error: AuthError }
1741
  > {
1742
    try {
×
1743
      const { data, error } = await this.getUser()
×
1744
      if (error) throw error
×
1745
      return { data: { identities: data.user.identities ?? [] }, error: null }
×
1746
    } catch (error) {
1747
      if (isAuthError(error)) {
×
1748
        return { data: null, error }
×
1749
      }
1750
      throw error
×
1751
    }
1752
  }
1753
  /**
1754
   * Links an oauth identity to an existing user.
1755
   * This method supports the PKCE flow.
1756
   */
1757
  async linkIdentity(credentials: SignInWithOAuthCredentials): Promise<OAuthResponse> {
1758
    try {
×
1759
      const { data, error } = await this._useSession(async (result) => {
×
1760
        const { data, error } = result
×
1761
        if (error) throw error
×
1762
        const url: string = await this._getUrlForProvider(
×
1763
          `${this.url}/user/identities/authorize`,
1764
          credentials.provider,
1765
          {
1766
            redirectTo: credentials.options?.redirectTo,
×
1767
            scopes: credentials.options?.scopes,
×
1768
            queryParams: credentials.options?.queryParams,
×
1769
            skipBrowserRedirect: true,
1770
          }
1771
        )
1772
        return await _request(this.fetch, 'GET', url, {
×
1773
          headers: this.headers,
1774
          jwt: data.session?.access_token ?? undefined,
×
1775
        })
1776
      })
1777
      if (error) throw error
×
1778
      if (isBrowser() && !credentials.options?.skipBrowserRedirect) {
×
1779
        window.location.assign(data?.url)
×
1780
      }
1781
      return { data: { provider: credentials.provider, url: data?.url }, error: null }
×
1782
    } catch (error) {
1783
      if (isAuthError(error)) {
×
1784
        return { data: { provider: credentials.provider, url: null }, error }
×
1785
      }
1786
      throw error
×
1787
    }
1788
  }
1789

1790
  /**
1791
   * 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.
1792
   */
1793
  async unlinkIdentity(identity: UserIdentity): Promise<
1794
    | {
1795
        data: {}
1796
        error: null
1797
      }
1798
    | { data: null; error: AuthError }
1799
  > {
1800
    try {
×
1801
      return await this._useSession(async (result) => {
×
1802
        const { data, error } = result
×
1803
        if (error) {
×
1804
          throw error
×
1805
        }
1806
        return await _request(
×
1807
          this.fetch,
1808
          'DELETE',
1809
          `${this.url}/user/identities/${identity.identity_id}`,
1810
          {
1811
            headers: this.headers,
1812
            jwt: data.session?.access_token ?? undefined,
×
1813
          }
1814
        )
1815
      })
1816
    } catch (error) {
1817
      if (isAuthError(error)) {
×
1818
        return { data: null, error }
×
1819
      }
1820
      throw error
×
1821
    }
1822
  }
1823

1824
  /**
1825
   * Generates a new JWT.
1826
   * @param refreshToken A valid refresh token that was returned on login.
1827
   */
1828
  private async _refreshAccessToken(refreshToken: string): Promise<AuthResponse> {
1829
    const debugName = `#_refreshAccessToken(${refreshToken.substring(0, 5)}...)`
14✔
1830
    this._debug(debugName, 'begin')
14✔
1831

1832
    try {
14✔
1833
      const startedAt = Date.now()
14✔
1834

1835
      // will attempt to refresh the token with exponential backoff
1836
      return await retryable(
14✔
1837
        async (attempt) => {
1838
          if (attempt > 0) {
14!
1839
            await sleep(200 * Math.pow(2, attempt - 1)) // 200, 400, 800, ...
×
1840
          }
1841

1842
          this._debug(debugName, 'refreshing attempt', attempt)
14✔
1843

1844
          return await _request(this.fetch, 'POST', `${this.url}/token?grant_type=refresh_token`, {
14✔
1845
            body: { refresh_token: refreshToken },
1846
            headers: this.headers,
1847
            xform: _sessionResponse,
1848
          })
1849
        },
1850
        (attempt, error) => {
1851
          const nextBackOffInterval = 200 * Math.pow(2, attempt)
14✔
1852
          return (
14✔
1853
            error &&
14!
1854
            isAuthRetryableFetchError(error) &&
1855
            // retryable only if the request can be sent before the backoff overflows the tick duration
1856
            Date.now() + nextBackOffInterval - startedAt < AUTO_REFRESH_TICK_DURATION_MS
1857
          )
1858
        }
1859
      )
1860
    } catch (error) {
1861
      this._debug(debugName, 'error', error)
×
1862

1863
      if (isAuthError(error)) {
×
1864
        return { data: { session: null, user: null }, error }
×
1865
      }
1866
      throw error
×
1867
    } finally {
1868
      this._debug(debugName, 'end')
14✔
1869
    }
1870
  }
1871

1872
  private _isValidSession(maybeSession: unknown): maybeSession is Session {
1873
    const isValidSession =
1874
      typeof maybeSession === 'object' &&
74✔
1875
      maybeSession !== null &&
1876
      'access_token' in maybeSession &&
1877
      'refresh_token' in maybeSession &&
1878
      'expires_at' in maybeSession
1879

1880
    return isValidSession
74✔
1881
  }
1882

1883
  private async _handleProviderSignIn(
1884
    provider: Provider,
1885
    options: {
1886
      redirectTo?: string
1887
      scopes?: string
1888
      queryParams?: { [key: string]: string }
1889
      skipBrowserRedirect?: boolean
1890
    }
1891
  ) {
1892
    const url: string = await this._getUrlForProvider(`${this.url}/authorize`, provider, {
8✔
1893
      redirectTo: options.redirectTo,
1894
      scopes: options.scopes,
1895
      queryParams: options.queryParams,
1896
    })
1897

1898
    this._debug('#_handleProviderSignIn()', 'provider', provider, 'options', options, 'url', url)
8✔
1899

1900
    // try to open on the browser
1901
    if (isBrowser() && !options.skipBrowserRedirect) {
8!
1902
      window.location.assign(url)
×
1903
    }
1904

1905
    return { data: { provider, url }, error: null }
8✔
1906
  }
1907

1908
  /**
1909
   * Recovers the session from LocalStorage and refreshes the token
1910
   * Note: this method is async to accommodate for AsyncStorage e.g. in React native.
1911
   */
1912
  private async _recoverAndRefresh() {
1913
    const debugName = '#_recoverAndRefresh()'
×
1914
    this._debug(debugName, 'begin')
×
1915

1916
    try {
×
1917
      const currentSession = await getItemAsync(this.storage, this.storageKey)
×
1918
      this._debug(debugName, 'session from storage', currentSession)
×
1919

1920
      if (!this._isValidSession(currentSession)) {
×
1921
        this._debug(debugName, 'session is not valid')
×
1922
        if (currentSession !== null) {
×
1923
          await this._removeSession()
×
1924
        }
1925

1926
        return
×
1927
      }
1928

1929
      const expiresWithMargin =
1930
        (currentSession.expires_at ?? Infinity) * 1000 - Date.now() < EXPIRY_MARGIN_MS
×
1931

1932
      this._debug(
×
1933
        debugName,
1934
        `session has${expiresWithMargin ? '' : ' not'} expired with margin of ${EXPIRY_MARGIN_MS}s`
×
1935
      )
1936

1937
      if (expiresWithMargin) {
×
1938
        if (this.autoRefreshToken && currentSession.refresh_token) {
×
1939
          const { error } = await this._callRefreshToken(currentSession.refresh_token)
×
1940

1941
          if (error) {
×
1942
            console.error(error)
×
1943

1944
            if (!isAuthRetryableFetchError(error)) {
×
1945
              this._debug(
×
1946
                debugName,
1947
                'refresh failed with a non-retryable error, removing the session',
1948
                error
1949
              )
1950
              await this._removeSession()
×
1951
            }
1952
          }
1953
        }
1954
      } else {
1955
        // no need to persist currentSession again, as we just loaded it from
1956
        // local storage; persisting it again may overwrite a value saved by
1957
        // another client with access to the same local storage
1958
        await this._notifyAllSubscribers('SIGNED_IN', currentSession)
×
1959
      }
1960
    } catch (err) {
1961
      this._debug(debugName, 'error', err)
×
1962

1963
      console.error(err)
×
1964
      return
×
1965
    } finally {
1966
      this._debug(debugName, 'end')
×
1967
    }
1968
  }
1969

1970
  private async _callRefreshToken(refreshToken: string): Promise<CallRefreshTokenResult> {
1971
    if (!refreshToken) {
22!
1972
      throw new AuthSessionMissingError()
×
1973
    }
1974

1975
    // refreshing is already in progress
1976
    if (this.refreshingDeferred) {
22✔
1977
      return this.refreshingDeferred.promise
6✔
1978
    }
1979

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

1982
    this._debug(debugName, 'begin')
16✔
1983

1984
    try {
16✔
1985
      this.refreshingDeferred = new Deferred<CallRefreshTokenResult>()
16✔
1986

1987
      const { data, error } = await this._refreshAccessToken(refreshToken)
16✔
1988
      if (error) throw error
14✔
1989
      if (!data.session) throw new AuthSessionMissingError()
12!
1990

1991
      await this._saveSession(data.session)
12✔
1992
      await this._notifyAllSubscribers('TOKEN_REFRESHED', data.session)
12✔
1993

1994
      const result = { session: data.session, error: null }
12✔
1995

1996
      this.refreshingDeferred.resolve(result)
12✔
1997

1998
      return result
12✔
1999
    } catch (error) {
2000
      this._debug(debugName, 'error', error)
4✔
2001

2002
      if (isAuthError(error)) {
4✔
2003
        const result = { session: null, error }
2✔
2004

2005
        if (!isAuthRetryableFetchError(error)) {
2✔
2006
          await this._removeSession()
2✔
2007
        }
2008

2009
        this.refreshingDeferred?.resolve(result)
2!
2010

2011
        return result
2✔
2012
      }
2013

2014
      this.refreshingDeferred?.reject(error)
2!
2015
      throw error
2✔
2016
    } finally {
2017
      this.refreshingDeferred = null
16✔
2018
      this._debug(debugName, 'end')
16✔
2019
    }
2020
  }
2021

2022
  private async _notifyAllSubscribers(
2023
    event: AuthChangeEvent,
2024
    session: Session | null,
2025
    broadcast = true
168✔
2026
  ) {
2027
    const debugName = `#_notifyAllSubscribers(${event})`
168✔
2028
    this._debug(debugName, 'begin', session, `broadcast = ${broadcast}`)
168✔
2029

2030
    try {
168✔
2031
      if (this.broadcastChannel && broadcast) {
168!
2032
        this.broadcastChannel.postMessage({ event, session })
×
2033
      }
2034

2035
      const errors: any[] = []
168✔
2036
      const promises = Array.from(this.stateChangeEmitters.values()).map(async (x) => {
168✔
2037
        try {
×
2038
          await x.callback(event, session)
×
2039
        } catch (e: any) {
2040
          errors.push(e)
×
2041
        }
2042
      })
2043

2044
      await Promise.all(promises)
168✔
2045

2046
      if (errors.length > 0) {
168!
2047
        for (let i = 0; i < errors.length; i += 1) {
×
2048
          console.error(errors[i])
×
2049
        }
2050

2051
        throw errors[0]
×
2052
      }
2053
    } finally {
2054
      this._debug(debugName, 'end')
168✔
2055
    }
2056
  }
2057

2058
  /**
2059
   * set currentSession and currentUser
2060
   * process to _startAutoRefreshToken if possible
2061
   */
2062
  private async _saveSession(session: Session) {
2063
    this._debug('#_saveSession()', session)
88✔
2064
    // _saveSession is always called whenever a new session has been acquired
2065
    // so we can safely suppress the warning returned by future getSession calls
2066
    this.suppressGetSessionWarning = true
88✔
2067
    await setItemAsync(this.storage, this.storageKey, session)
88✔
2068
  }
2069

2070
  private async _removeSession() {
2071
    this._debug('#_removeSession()')
84✔
2072

2073
    await removeItemAsync(this.storage, this.storageKey)
84✔
2074
    await this._notifyAllSubscribers('SIGNED_OUT', null)
84✔
2075
  }
2076

2077
  /**
2078
   * Removes any registered visibilitychange callback.
2079
   *
2080
   * {@see #startAutoRefresh}
2081
   * {@see #stopAutoRefresh}
2082
   */
2083
  private _removeVisibilityChangedCallback() {
2084
    this._debug('#_removeVisibilityChangedCallback()')
4✔
2085

2086
    const callback = this.visibilityChangedCallback
4✔
2087
    this.visibilityChangedCallback = null
4✔
2088

2089
    try {
4✔
2090
      if (callback && isBrowser() && window?.removeEventListener) {
4!
2091
        window.removeEventListener('visibilitychange', callback)
×
2092
      }
2093
    } catch (e) {
2094
      console.error('removing visibilitychange callback failed', e)
×
2095
    }
2096
  }
2097

2098
  /**
2099
   * This is the private implementation of {@link #startAutoRefresh}. Use this
2100
   * within the library.
2101
   */
2102
  private async _startAutoRefresh() {
2103
    await this._stopAutoRefresh()
4✔
2104

2105
    this._debug('#_startAutoRefresh()')
4✔
2106

2107
    const ticker = setInterval(() => this._autoRefreshTokenTick(), AUTO_REFRESH_TICK_DURATION_MS)
4✔
2108
    this.autoRefreshTicker = ticker
4✔
2109

2110
    if (ticker && typeof ticker === 'object' && typeof ticker.unref === 'function') {
4!
2111
      // ticker is a NodeJS Timeout object that has an `unref` method
2112
      // https://nodejs.org/api/timers.html#timeoutunref
2113
      // When auto refresh is used in NodeJS (like for testing) the
2114
      // `setInterval` is preventing the process from being marked as
2115
      // finished and tests run endlessly. This can be prevented by calling
2116
      // `unref()` on the returned object.
2117
      ticker.unref()
4✔
2118
      // @ts-expect-error TS has no context of Deno
2119
    } else if (typeof Deno !== 'undefined' && typeof Deno.unrefTimer === 'function') {
×
2120
      // similar like for NodeJS, but with the Deno API
2121
      // https://deno.land/api@latest?unstable&s=Deno.unrefTimer
2122
      // @ts-expect-error TS has no context of Deno
2123
      Deno.unrefTimer(ticker)
×
2124
    }
2125

2126
    // run the tick immediately, but in the next pass of the event loop so that
2127
    // #_initialize can be allowed to complete without recursively waiting on
2128
    // itself
2129
    setTimeout(async () => {
4✔
2130
      await this.initializePromise
4✔
2131
      await this._autoRefreshTokenTick()
4✔
2132
    }, 0)
2133
  }
2134

2135
  /**
2136
   * This is the private implementation of {@link #stopAutoRefresh}. Use this
2137
   * within the library.
2138
   */
2139
  private async _stopAutoRefresh() {
2140
    this._debug('#_stopAutoRefresh()')
4✔
2141

2142
    const ticker = this.autoRefreshTicker
4✔
2143
    this.autoRefreshTicker = null
4✔
2144

2145
    if (ticker) {
4!
2146
      clearInterval(ticker)
×
2147
    }
2148
  }
2149

2150
  /**
2151
   * Starts an auto-refresh process in the background. The session is checked
2152
   * every few seconds. Close to the time of expiration a process is started to
2153
   * refresh the session. If refreshing fails it will be retried for as long as
2154
   * necessary.
2155
   *
2156
   * If you set the {@link GoTrueClientOptions#autoRefreshToken} you don't need
2157
   * to call this function, it will be called for you.
2158
   *
2159
   * On browsers the refresh process works only when the tab/window is in the
2160
   * foreground to conserve resources as well as prevent race conditions and
2161
   * flooding auth with requests. If you call this method any managed
2162
   * visibility change callback will be removed and you must manage visibility
2163
   * changes on your own.
2164
   *
2165
   * On non-browser platforms the refresh process works *continuously* in the
2166
   * background, which may not be desirable. You should hook into your
2167
   * platform's foreground indication mechanism and call these methods
2168
   * appropriately to conserve resources.
2169
   *
2170
   * {@see #stopAutoRefresh}
2171
   */
2172
  async startAutoRefresh() {
2173
    this._removeVisibilityChangedCallback()
4✔
2174
    await this._startAutoRefresh()
4✔
2175
  }
2176

2177
  /**
2178
   * Stops an active auto refresh process running in the background (if any).
2179
   *
2180
   * If you call this method any managed visibility change callback will be
2181
   * removed and you must manage visibility changes on your own.
2182
   *
2183
   * See {@link #startAutoRefresh} for more details.
2184
   */
2185
  async stopAutoRefresh() {
2186
    this._removeVisibilityChangedCallback()
×
2187
    await this._stopAutoRefresh()
×
2188
  }
2189

2190
  /**
2191
   * Runs the auto refresh token tick.
2192
   */
2193
  private async _autoRefreshTokenTick() {
2194
    this._debug('#_autoRefreshTokenTick()', 'begin')
4✔
2195

2196
    try {
4✔
2197
      await this._acquireLock(0, async () => {
4✔
2198
        try {
4✔
2199
          const now = Date.now()
4✔
2200

2201
          try {
4✔
2202
            return await this._useSession(async (result) => {
4✔
2203
              const {
2204
                data: { session },
2205
              } = result
4✔
2206

2207
              if (!session || !session.refresh_token || !session.expires_at) {
4!
2208
                this._debug('#_autoRefreshTokenTick()', 'no session')
×
2209
                return
×
2210
              }
2211

2212
              // session will expire in this many ticks (or has already expired if <= 0)
2213
              const expiresInTicks = Math.floor(
4✔
2214
                (session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS
2215
              )
2216

2217
              this._debug(
4✔
2218
                '#_autoRefreshTokenTick()',
2219
                `access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks`
2220
              )
2221

2222
              if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) {
4!
2223
                await this._callRefreshToken(session.refresh_token)
×
2224
              }
2225
            })
2226
          } catch (e: any) {
2227
            console.error(
×
2228
              'Auto refresh tick failed with error. This is likely a transient error.',
2229
              e
2230
            )
2231
          }
2232
        } finally {
2233
          this._debug('#_autoRefreshTokenTick()', 'end')
4✔
2234
        }
2235
      })
2236
    } catch (e: any) {
2237
      if (e.isAcquireTimeout || e instanceof LockAcquireTimeoutError) {
×
2238
        this._debug('auto refresh token tick lock not available')
×
2239
      } else {
2240
        throw e
×
2241
      }
2242
    }
2243
  }
2244

2245
  /**
2246
   * Registers callbacks on the browser / platform, which in-turn run
2247
   * algorithms when the browser window/tab are in foreground. On non-browser
2248
   * platforms it assumes always foreground.
2249
   */
2250
  private async _handleVisibilityChange() {
2251
    this._debug('#_handleVisibilityChange()')
50✔
2252

2253
    if (!isBrowser() || !window?.addEventListener) {
50!
2254
      if (this.autoRefreshToken) {
50✔
2255
        // in non-browser environments the refresh token ticker runs always
2256
        this.startAutoRefresh()
4✔
2257
      }
2258

2259
      return false
50✔
2260
    }
2261

2262
    try {
×
2263
      this.visibilityChangedCallback = async () => await this._onVisibilityChanged(false)
×
2264

2265
      window?.addEventListener('visibilitychange', this.visibilityChangedCallback)
×
2266

2267
      // now immediately call the visbility changed callback to setup with the
2268
      // current visbility state
2269
      await this._onVisibilityChanged(true) // initial call
×
2270
    } catch (error) {
2271
      console.error('_handleVisibilityChange', error)
×
2272
    }
2273
  }
2274

2275
  /**
2276
   * Callback registered with `window.addEventListener('visibilitychange')`.
2277
   */
2278
  private async _onVisibilityChanged(calledFromInitialize: boolean) {
2279
    const methodName = `#_onVisibilityChanged(${calledFromInitialize})`
×
2280
    this._debug(methodName, 'visibilityState', document.visibilityState)
×
2281

2282
    if (document.visibilityState === 'visible') {
×
2283
      if (this.autoRefreshToken) {
×
2284
        // in browser environments the refresh token ticker runs only on focused tabs
2285
        // which prevents race conditions
2286
        this._startAutoRefresh()
×
2287
      }
2288

2289
      if (!calledFromInitialize) {
×
2290
        // called when the visibility has changed, i.e. the browser
2291
        // transitioned from hidden -> visible so we need to see if the session
2292
        // should be recovered immediately... but to do that we need to acquire
2293
        // the lock first asynchronously
2294
        await this.initializePromise
×
2295

2296
        await this._acquireLock(-1, async () => {
×
2297
          if (document.visibilityState !== 'visible') {
×
2298
            this._debug(
×
2299
              methodName,
2300
              'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting'
2301
            )
2302

2303
            // visibility has changed while waiting for the lock, abort
2304
            return
×
2305
          }
2306

2307
          // recover the session
2308
          await this._recoverAndRefresh()
×
2309
        })
2310
      }
2311
    } else if (document.visibilityState === 'hidden') {
×
2312
      if (this.autoRefreshToken) {
×
2313
        this._stopAutoRefresh()
×
2314
      }
2315
    }
2316
  }
2317

2318
  /**
2319
   * Generates the relevant login URL for a third-party provider.
2320
   * @param options.redirectTo A URL or mobile address to send the user to after they are confirmed.
2321
   * @param options.scopes A space-separated list of scopes granted to the OAuth application.
2322
   * @param options.queryParams An object of key-value pairs containing query parameters granted to the OAuth application.
2323
   */
2324
  private async _getUrlForProvider(
2325
    url: string,
2326
    provider: Provider,
2327
    options: {
2328
      redirectTo?: string
2329
      scopes?: string
2330
      queryParams?: { [key: string]: string }
2331
      skipBrowserRedirect?: boolean
2332
    }
2333
  ) {
2334
    const urlParams: string[] = [`provider=${encodeURIComponent(provider)}`]
8✔
2335
    if (options?.redirectTo) {
8!
2336
      urlParams.push(`redirect_to=${encodeURIComponent(options.redirectTo)}`)
4✔
2337
    }
2338
    if (options?.scopes) {
8!
2339
      urlParams.push(`scopes=${encodeURIComponent(options.scopes)}`)
4✔
2340
    }
2341
    if (this.flowType === 'pkce') {
8!
2342
      const [codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
×
2343
        this.storage,
2344
        this.storageKey
2345
      )
2346

2347
      const flowParams = new URLSearchParams({
×
2348
        code_challenge: `${encodeURIComponent(codeChallenge)}`,
2349
        code_challenge_method: `${encodeURIComponent(codeChallengeMethod)}`,
2350
      })
2351
      urlParams.push(flowParams.toString())
×
2352
    }
2353
    if (options?.queryParams) {
8!
2354
      const query = new URLSearchParams(options.queryParams)
×
2355
      urlParams.push(query.toString())
×
2356
    }
2357
    if (options?.skipBrowserRedirect) {
8!
2358
      urlParams.push(`skip_http_redirect=${options.skipBrowserRedirect}`)
×
2359
    }
2360

2361
    return `${url}?${urlParams.join('&')}`
8✔
2362
  }
2363

2364
  private async _unenroll(params: MFAUnenrollParams): Promise<AuthMFAUnenrollResponse> {
2365
    try {
×
2366
      return await this._useSession(async (result) => {
×
2367
        const { data: sessionData, error: sessionError } = result
×
2368
        if (sessionError) {
×
2369
          return { data: null, error: sessionError }
×
2370
        }
2371

2372
        return await _request(this.fetch, 'DELETE', `${this.url}/factors/${params.factorId}`, {
×
2373
          headers: this.headers,
2374
          jwt: sessionData?.session?.access_token,
×
2375
        })
2376
      })
2377
    } catch (error) {
2378
      if (isAuthError(error)) {
×
2379
        return { data: null, error }
×
2380
      }
2381
      throw error
×
2382
    }
2383
  }
2384

2385
  /**
2386
   * {@see GoTrueMFAApi#enroll}
2387
   */
2388
  private async _enroll(params: MFAEnrollTOTPParams): Promise<AuthMFAEnrollTOTPResponse>
2389
  private async _enroll(params: MFAEnrollPhoneParams): Promise<AuthMFAEnrollPhoneResponse>
2390
  private async _enroll(params: MFAEnrollParams): Promise<AuthMFAEnrollResponse> {
2391
    try {
2✔
2392
      return await this._useSession(async (result) => {
2✔
2393
        const { data: sessionData, error: sessionError } = result
2✔
2394
        if (sessionError) {
2!
2395
          return { data: null, error: sessionError }
×
2396
        }
2397

2398
        const body = {
2✔
2399
          friendly_name: params.friendlyName,
2400
          factor_type: params.factorType,
2401
          ...(params.factorType === 'phone' ? { phone: params.phone } : { issuer: params.issuer }),
2!
2402
        }
2403

2404
        const { data, error } = await _request(this.fetch, 'POST', `${this.url}/factors`, {
2✔
2405
          body,
2406
          headers: this.headers,
2407
          jwt: sessionData?.session?.access_token,
12!
2408
        })
2409

2410
        if (error) {
2!
2411
          return { data: null, error }
×
2412
        }
2413

2414
        if (params.factorType === 'totp' && data?.totp?.qr_code) {
2!
2415
          data.totp.qr_code = `data:image/svg+xml;utf-8,${data.totp.qr_code}`
2✔
2416
        }
2417

2418
        return { data, error: null }
2✔
2419
      })
2420
    } catch (error) {
2421
      if (isAuthError(error)) {
×
2422
        return { data: null, error }
×
2423
      }
2424
      throw error
×
2425
    }
2426
  }
2427

2428
  /**
2429
   * {@see GoTrueMFAApi#verify}
2430
   */
2431
  private async _verify(params: MFAVerifyParams): Promise<AuthMFAVerifyResponse> {
2432
    return this._acquireLock(-1, async () => {
×
2433
      try {
×
2434
        return await this._useSession(async (result) => {
×
2435
          const { data: sessionData, error: sessionError } = result
×
2436
          if (sessionError) {
×
2437
            return { data: null, error: sessionError }
×
2438
          }
2439

2440
          const { data, error } = await _request(
×
2441
            this.fetch,
2442
            'POST',
2443
            `${this.url}/factors/${params.factorId}/verify`,
2444
            {
2445
              body: { code: params.code, challenge_id: params.challengeId },
2446
              headers: this.headers,
2447
              jwt: sessionData?.session?.access_token,
×
2448
            }
2449
          )
2450
          if (error) {
×
2451
            return { data: null, error }
×
2452
          }
2453

2454
          await this._saveSession({
×
2455
            expires_at: Math.round(Date.now() / 1000) + data.expires_in,
2456
            ...data,
2457
          })
2458
          await this._notifyAllSubscribers('MFA_CHALLENGE_VERIFIED', data)
×
2459

2460
          return { data, error }
×
2461
        })
2462
      } catch (error) {
2463
        if (isAuthError(error)) {
×
2464
          return { data: null, error }
×
2465
        }
2466
        throw error
×
2467
      }
2468
    })
2469
  }
2470

2471
  /**
2472
   * {@see GoTrueMFAApi#challenge}
2473
   */
2474
  private async _challenge(params: MFAChallengeParams): Promise<AuthMFAChallengeResponse> {
2475
    return this._acquireLock(-1, async () => {
×
2476
      try {
×
2477
        return await this._useSession(async (result) => {
×
2478
          const { data: sessionData, error: sessionError } = result
×
2479
          if (sessionError) {
×
2480
            return { data: null, error: sessionError }
×
2481
          }
2482

2483
          return await _request(
×
2484
            this.fetch,
2485
            'POST',
2486
            `${this.url}/factors/${params.factorId}/challenge`,
2487
            {
2488
              body: { channel: params.channel },
2489
              headers: this.headers,
2490
              jwt: sessionData?.session?.access_token,
×
2491
            }
2492
          )
2493
        })
2494
      } catch (error) {
2495
        if (isAuthError(error)) {
×
2496
          return { data: null, error }
×
2497
        }
2498
        throw error
×
2499
      }
2500
    })
2501
  }
2502

2503
  /**
2504
   * {@see GoTrueMFAApi#challengeAndVerify}
2505
   */
2506
  private async _challengeAndVerify(
2507
    params: MFAChallengeAndVerifyParams
2508
  ): Promise<AuthMFAVerifyResponse> {
2509
    // both _challenge and _verify independently acquire the lock, so no need
2510
    // to acquire it here
2511

2512
    const { data: challengeData, error: challengeError } = await this._challenge({
×
2513
      factorId: params.factorId,
2514
    })
2515
    if (challengeError) {
×
2516
      return { data: null, error: challengeError }
×
2517
    }
2518

2519
    return await this._verify({
×
2520
      factorId: params.factorId,
2521
      challengeId: challengeData.id,
2522
      code: params.code,
2523
    })
2524
  }
2525

2526
  /**
2527
   * {@see GoTrueMFAApi#listFactors}
2528
   */
2529
  private async _listFactors(): Promise<AuthMFAListFactorsResponse> {
2530
    // use #getUser instead of #_getUser as the former acquires a lock
2531
    const {
2532
      data: { user },
2533
      error: userError,
2534
    } = await this.getUser()
×
2535
    if (userError) {
×
2536
      return { data: null, error: userError }
×
2537
    }
2538

2539
    const factors = user?.factors || []
×
2540
    const totp = factors.filter(
×
2541
      (factor) => factor.factor_type === 'totp' && factor.status === 'verified'
×
2542
    )
2543
    const phone = factors.filter(
×
2544
      (factor) => factor.factor_type === 'phone' && factor.status === 'verified'
×
2545
    )
2546

2547
    return {
×
2548
      data: {
2549
        all: factors,
2550
        totp,
2551
        phone,
2552
      },
2553
      error: null,
2554
    }
2555
  }
2556

2557
  /**
2558
   * {@see GoTrueMFAApi#getAuthenticatorAssuranceLevel}
2559
   */
2560
  private async _getAuthenticatorAssuranceLevel(): Promise<AuthMFAGetAuthenticatorAssuranceLevelResponse> {
2561
    return this._acquireLock(-1, async () => {
×
2562
      return await this._useSession(async (result) => {
×
2563
        const {
2564
          data: { session },
2565
          error: sessionError,
2566
        } = result
×
2567
        if (sessionError) {
×
2568
          return { data: null, error: sessionError }
×
2569
        }
2570
        if (!session) {
×
2571
          return {
×
2572
            data: { currentLevel: null, nextLevel: null, currentAuthenticationMethods: [] },
2573
            error: null,
2574
          }
2575
        }
2576

NEW
2577
        const { payload } = decodeJWT(session.access_token)
×
2578

2579
        let currentLevel: AuthenticatorAssuranceLevels | null = null
×
2580

2581
        if (payload.aal) {
×
2582
          currentLevel = payload.aal
×
2583
        }
2584

2585
        let nextLevel: AuthenticatorAssuranceLevels | null = currentLevel
×
2586

2587
        const verifiedFactors =
2588
          session.user.factors?.filter((factor: Factor) => factor.status === 'verified') ?? []
×
2589

2590
        if (verifiedFactors.length > 0) {
×
2591
          nextLevel = 'aal2'
×
2592
        }
2593

2594
        const currentAuthenticationMethods = payload.amr || []
×
2595

2596
        return { data: { currentLevel, nextLevel, currentAuthenticationMethods }, error: null }
×
2597
      })
2598
    })
2599
  }
2600

2601
  private async fetchJwk(kid: string, jwks: { keys: JWK[] } = { keys: [] }): Promise<JWK> {
×
2602
    // try fetching from the supplied jwks
2603
    let jwk = jwks.keys.find((key) => key.kid === kid)
1✔
2604
    if (jwk) {
1!
NEW
2605
      return jwk
×
2606
    }
2607

2608
    // try fetching from cache
2609
    jwk = this.jwks.keys.find((key) => key.kid === kid)
1✔
2610
    if (jwk) {
1!
NEW
2611
      return jwk
×
2612
    }
2613
    // jwk isn't cached in memory so we need to fetch it from the well-known endpoint
2614
    const { data, error } = await _request(this.fetch, 'GET', `${this.url}/.well-known/jwks.json`, {
1✔
2615
      headers: this.headers,
2616
    })
2617
    if (error) {
1!
NEW
2618
      throw error
×
2619
    }
2620
    if (!data.keys || data.keys.length === 0) {
1!
NEW
2621
      throw new AuthInvalidJwtError('JWKS is empty')
×
2622
    }
2623
    this.jwks = data
1✔
2624
    // Find the signing key
2625
    jwk = data.keys.find((key: any) => key.kid === kid)
1✔
2626
    if (!jwk) {
1!
NEW
2627
      throw new AuthInvalidJwtError('No matching signing key found in JWKS')
×
2628
    }
2629
    return jwk
1✔
2630
  }
2631

2632
  /**
2633
   * @experimental This method may change in future versions.
2634
   * @description Gets the claims from a JWT. If the JWT is symmetric JWTs, it will call getUser() to verify against the server. If the JWT is asymmetric, it will be verified against the JWKS using the WebCrypto API.
2635
   */
2636
  async getClaims(
2637
    jwt?: string,
2638
    jwks: { keys: JWK[] } = { keys: [] }
6✔
2639
  ): Promise<
2640
    | {
2641
        data: { claims: JwtPayload; header: JwtHeader; signature: Uint8Array }
2642
        error: null
2643
      }
2644
    | { data: null; error: AuthError }
2645
    | { data: null; error: null }
2646
  > {
2647
    try {
6✔
2648
      let token = jwt
6✔
2649
      if (!token) {
6✔
2650
        const { data, error } = await this.getSession()
6✔
2651
        if (error || !data.session) {
6✔
2652
          return { data: null, error }
2✔
2653
        }
2654
        token = data.session.access_token
4✔
2655
      }
2656

2657
      const {
2658
        header,
2659
        payload,
2660
        signature,
2661
        raw: { header: rawHeader, payload: rawPayload },
2662
      } = decodeJWT(token)
4✔
2663

2664
      // Reject expired JWTs
2665
      validateExp(payload.exp)
4✔
2666

2667
      // If symmetric algorithm or WebCrypto API is unavailable, fallback to getUser()
2668
      if (
4✔
2669
        !header.kid ||
8✔
2670
        header.alg === 'HS256' ||
2671
        !('crypto' in globalThis && 'subtle' in globalThis.crypto)
3✔
2672
      ) {
2673
        const { error } = await this.getUser(token)
3✔
2674
        if (error) {
3!
NEW
2675
          throw error
×
2676
        }
2677
        // getUser succeeds so the claims in the JWT can be trusted
2678
        return {
3✔
2679
          data: {
2680
            claims: payload,
2681
            header,
2682
            signature,
2683
          },
2684
          error: null,
2685
        }
2686
      }
2687

2688
      const algorithm = getAlgorithm(header.alg)
1✔
2689
      const signingKey = await this.fetchJwk(header.kid, jwks)
1✔
2690

2691
      // Convert JWK to CryptoKey
2692
      const publicKey = await crypto.subtle.importKey('jwk', signingKey, algorithm, true, [
1✔
2693
        'verify',
2694
      ])
2695

2696
      // Verify the signature
2697
      const isValid = await crypto.subtle.verify(
1✔
2698
        algorithm,
2699
        publicKey,
2700
        signature,
2701
        stringToUint8Array(`${rawHeader}.${rawPayload}`)
2702
      )
2703

2704
      if (!isValid) {
1!
NEW
2705
        throw new AuthInvalidJwtError('Invalid JWT signature')
×
2706
      }
2707

2708
      // If verification succeeds, decode and return claims
2709
      return {
1✔
2710
        data: {
2711
          claims: payload,
2712
          header,
2713
          signature,
2714
        },
2715
        error: null,
2716
      }
2717
    } catch (error) {
NEW
2718
      if (isAuthError(error)) {
×
NEW
2719
        return { data: null, error }
×
2720
      }
NEW
2721
      throw error
×
2722
    }
2723
  }
2724
}
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