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

supabase / auth-js / 17707640729

14 Sep 2025 06:42AM UTC coverage: 71.695% (-9.1%) from 80.811%
17707640729

Pull #1118

github

web-flow
Merge 769208bf0 into aadf02e63
Pull Request #1118: feat: Webauthn support

1083 of 1656 branches covered (65.4%)

Branch coverage included in aggregate %.

59 of 263 new or added lines in 5 files covered. (22.43%)

151 existing lines in 1 file now uncovered.

1493 of 1937 relevant lines covered (77.08%)

152.22 hits per line

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

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

58
import { bytesToBase64URL, stringToUint8Array } from './lib/base64url'
10✔
59
import type {
60
  AuthChangeEvent,
61
  AuthenticatorAssuranceLevels,
62
  AuthFlowType,
63
  AuthMFAChallengePhoneResponse,
64
  AuthMFAChallengeResponse,
65
  AuthMFAChallengeTOTPResponse,
66
  AuthMFAChallengeWebauthnResponse,
67
  AuthMFAChallengeWebauthnServerResponse,
68
  AuthMFAEnrollPhoneResponse,
69
  AuthMFAEnrollResponse,
70
  AuthMFAEnrollTOTPResponse,
71
  AuthMFAEnrollWebauthnResponse,
72
  AuthMFAGetAuthenticatorAssuranceLevelResponse,
73
  AuthMFAListFactorsResponse,
74
  AuthMFAUnenrollResponse,
75
  AuthMFAVerifyResponse,
76
  AuthOtpResponse,
77
  AuthResponse,
78
  AuthResponsePassword,
79
  AuthTokenResponse,
80
  AuthTokenResponsePassword,
81
  CallRefreshTokenResult,
82
  EthereumWallet,
83
  EthereumWeb3Credentials,
84
  Factor,
85
  GoTrueClientOptions,
86
  GoTrueMFAApi,
87
  InitializeResult,
88
  JWK,
89
  JwtHeader,
90
  JwtPayload,
91
  LockFunc,
92
  MFAChallengeAndVerifyParams,
93
  MFAChallengeParams,
94
  MFAChallengePhoneParams,
95
  MFAChallengeTOTPParams,
96
  MFAChallengeWebauthnParams,
97
  MFAEnrollParams,
98
  MFAEnrollPhoneParams,
99
  MFAEnrollTOTPParams,
100
  MFAEnrollWebauthnParams,
101
  MFAUnenrollParams,
102
  MFAVerifyParams,
103
  MFAVerifyPhoneParams,
104
  MFAVerifyTOTPParams,
105
  MFAVerifyWebauthnParamFields,
106
  MFAVerifyWebauthnParams,
107
  OAuthResponse,
108
  Prettify,
109
  Provider,
110
  ResendParams,
111
  Session,
112
  SignInAnonymouslyCredentials,
113
  SignInWithIdTokenCredentials,
114
  SignInWithOAuthCredentials,
115
  SignInWithPasswordCredentials,
116
  SignInWithPasswordlessCredentials,
117
  SignInWithSSO,
118
  SignOut,
119
  SignUpWithPasswordCredentials,
120
  SolanaWallet,
121
  SolanaWeb3Credentials,
122
  SSOResponse,
123
  StrictOmit,
124
  Subscription,
125
  SupportedStorage,
126
  User,
127
  UserAttributes,
128
  UserIdentity,
129
  UserResponse,
130
  VerifyOtpParams,
131
  Web3Credentials,
132
} from './lib/types'
133
import {
10✔
134
  createSiweMessage,
135
  fromHex,
136
  getAddress,
137
  Hex,
138
  SiweMessage,
139
  toHex,
140
} from './lib/web3/ethereum'
141
import {
10✔
142
  deserializeCredentialCreationOptions,
143
  deserializeCredentialRequestOptions,
144
  serializeCredentialCreationResponse,
145
  serializeCredentialRequestResponse,
146
  WebAuthnApi,
147
} from './lib/webauthn'
148
import {
149
  AuthenticationCredential,
150
  PublicKeyCredentialJSON,
151
  RegistrationCredential,
152
} from './lib/webauthn.dom'
153

154
polyfillGlobalThis() // Make "globalThis" available
10✔
155

156
const DEFAULT_OPTIONS: Omit<
157
  Required<GoTrueClientOptions>,
158
  'fetch' | 'storage' | 'userStorage' | 'lock'
159
> = {
10✔
160
  url: GOTRUE_URL,
161
  storageKey: STORAGE_KEY,
162
  autoRefreshToken: true,
163
  persistSession: true,
164
  detectSessionInUrl: true,
165
  headers: DEFAULT_HEADERS,
166
  flowType: 'implicit',
167
  debug: false,
168
  hasCustomAuthorizationHeader: false,
169
}
170

171
async function lockNoOp<R>(name: string, acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
172
  return await fn()
547✔
173
}
174

175
/**
176
 * Caches JWKS values for all clients created in the same environment. This is
177
 * especially useful for shared-memory execution environments such as Vercel's
178
 * Fluid Compute, AWS Lambda or Supabase's Edge Functions. Regardless of how
179
 * many clients are created, if they share the same storage key they will use
180
 * the same JWKS cache, significantly speeding up getClaims() with asymmetric
181
 * JWTs.
182
 */
183
const GLOBAL_JWKS: { [storageKey: string]: { cachedAt: number; jwks: { keys: JWK[] } } } = {}
10✔
184

185
export default class GoTrueClient {
10✔
186
  private static nextInstanceID = 0
10✔
187

188
  private instanceID: number
189

190
  /**
191
   * Namespace for the GoTrue admin methods.
192
   * These methods should only be used in a trusted server-side environment.
193
   */
194
  admin: GoTrueAdminApi
195
  /**
196
   * Namespace for the MFA methods.
197
   */
198
  mfa: GoTrueMFAApi
199
  /**
200
   * The storage key used to identify the values saved in localStorage
201
   */
202
  protected storageKey: string
203

204
  protected flowType: AuthFlowType
205

206
  /**
207
   * The JWKS used for verifying asymmetric JWTs
208
   */
209
  protected get jwks() {
210
    return GLOBAL_JWKS[this.storageKey]?.jwks ?? { keys: [] }
198✔
211
  }
212

213
  protected set jwks(value: { keys: JWK[] }) {
214
    GLOBAL_JWKS[this.storageKey] = { ...GLOBAL_JWKS[this.storageKey], jwks: value }
15✔
215
  }
216

217
  protected get jwks_cached_at() {
218
    return GLOBAL_JWKS[this.storageKey]?.cachedAt ?? Number.MIN_SAFE_INTEGER
11✔
219
  }
220

221
  protected set jwks_cached_at(value: number) {
222
    GLOBAL_JWKS[this.storageKey] = { ...GLOBAL_JWKS[this.storageKey], cachedAt: value }
15✔
223
  }
224

225
  protected autoRefreshToken: boolean
226
  protected persistSession: boolean
227
  protected storage: SupportedStorage
228
  /**
229
   * @experimental
230
   */
231
  protected userStorage: SupportedStorage | null = null
182✔
232
  protected memoryStorage: { [key: string]: string } | null = null
182✔
233
  protected stateChangeEmitters: Map<string, Subscription> = new Map()
182✔
234
  protected autoRefreshTicker: ReturnType<typeof setInterval> | null = null
182✔
235
  protected visibilityChangedCallback: (() => Promise<any>) | null = null
182✔
236
  protected refreshingDeferred: Deferred<CallRefreshTokenResult> | null = null
182✔
237
  /**
238
   * Keeps track of the async client initialization.
239
   * When null or not yet resolved the auth state is `unknown`
240
   * Once resolved the auth state is known and it's safe to call any further client methods.
241
   * Keep extra care to never reject or throw uncaught errors
242
   */
243
  protected initializePromise: Promise<InitializeResult> | null = null
182✔
244
  protected detectSessionInUrl = true
182✔
245
  protected url: string
246
  protected headers: {
247
    [key: string]: string
248
  }
249
  protected hasCustomAuthorizationHeader = false
182✔
250
  protected suppressGetSessionWarning = false
182✔
251
  protected fetch: Fetch
252
  protected lock: LockFunc
253
  protected lockAcquired = false
182✔
254
  protected pendingInLock: Promise<any>[] = []
182✔
255

256
  /**
257
   * Used to broadcast state change events to other tabs listening.
258
   */
259
  protected broadcastChannel: BroadcastChannel | null = null
182✔
260

261
  protected logDebugMessages: boolean
262
  protected logger: (message: string, ...args: any[]) => void = console.log
182✔
263

264
  /**
265
   * Create a new client for use in the browser.
266
   */
267
  constructor(options: GoTrueClientOptions) {
268
    this.instanceID = GoTrueClient.nextInstanceID
182✔
269
    GoTrueClient.nextInstanceID += 1
182✔
270

271
    if (this.instanceID > 0 && isBrowser()) {
182✔
272
      console.warn(
72✔
273
        '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.'
274
      )
275
    }
276

277
    const settings = { ...DEFAULT_OPTIONS, ...options }
182✔
278

279
    this.logDebugMessages = !!settings.debug
182✔
280
    if (typeof settings.debug === 'function') {
182✔
281
      this.logger = settings.debug
2✔
282
    }
283

284
    this.persistSession = settings.persistSession
182✔
285
    this.storageKey = settings.storageKey
182✔
286
    this.autoRefreshToken = settings.autoRefreshToken
182✔
287
    this.admin = new GoTrueAdminApi({
182✔
288
      url: settings.url,
289
      headers: settings.headers,
290
      fetch: settings.fetch,
291
    })
292

293
    this.url = settings.url
182✔
294
    this.headers = settings.headers
182✔
295
    this.fetch = resolveFetch(settings.fetch)
182✔
296
    this.lock = settings.lock || lockNoOp
182✔
297
    this.detectSessionInUrl = settings.detectSessionInUrl
182✔
298
    this.flowType = settings.flowType
182✔
299
    this.hasCustomAuthorizationHeader = settings.hasCustomAuthorizationHeader
182✔
300

301
    if (settings.lock) {
182✔
302
      this.lock = settings.lock
8✔
303
    } else if (isBrowser() && globalThis?.navigator?.locks) {
174!
304
      this.lock = navigatorLock
36✔
305
    } else {
306
      this.lock = lockNoOp
138✔
307
    }
308

309
    if (!this.jwks) {
182!
UNCOV
310
      this.jwks = { keys: [] }
×
UNCOV
311
      this.jwks_cached_at = Number.MIN_SAFE_INTEGER
×
312
    }
313

314
    this.mfa = {
182✔
315
      verify: this._verify.bind(this),
316
      enroll: this._enroll.bind(this),
317
      unenroll: this._unenroll.bind(this),
318
      challenge: this._challenge.bind(this),
319
      listFactors: this._listFactors.bind(this),
320
      challengeAndVerify: this._challengeAndVerify.bind(this),
321
      getAuthenticatorAssuranceLevel: this._getAuthenticatorAssuranceLevel.bind(this),
322
      webauthn: new WebAuthnApi(this),
323
    }
324

325
    if (this.persistSession) {
182✔
326
      if (settings.storage) {
172✔
327
        this.storage = settings.storage
136✔
328
      } else {
329
        if (supportsLocalStorage()) {
36✔
330
          this.storage = globalThis.localStorage
2✔
331
        } else {
332
          this.memoryStorage = {}
34✔
333
          this.storage = memoryLocalStorageAdapter(this.memoryStorage)
34✔
334
        }
335
      }
336

337
      if (settings.userStorage) {
172✔
338
        this.userStorage = settings.userStorage
12✔
339
      }
340
    } else {
341
      this.memoryStorage = {}
10✔
342
      this.storage = memoryLocalStorageAdapter(this.memoryStorage)
10✔
343
    }
344

345
    if (isBrowser() && globalThis.BroadcastChannel && this.persistSession && this.storageKey) {
182✔
346
      try {
44✔
347
        this.broadcastChannel = new globalThis.BroadcastChannel(this.storageKey)
44✔
348
      } catch (e: any) {
349
        console.error(
42✔
350
          'Failed to create a new BroadcastChannel, multi-tab state changes will not be available',
351
          e
352
        )
353
      }
354

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

358
        await this._notifyAllSubscribers(event.data.event, event.data.session, false) // broadcast = false so we don't get an endless loop of messages
2✔
359
      })
360
    }
361

362
    this.initialize()
182✔
363
  }
364

365
  private _debug(...args: any[]): GoTrueClient {
366
    if (this.logDebugMessages) {
7,239✔
367
      this.logger(
42✔
368
        `GoTrueClient@${this.instanceID} (${version}) ${new Date().toISOString()}`,
369
        ...args
370
      )
371
    }
372

373
    return this
7,239✔
374
  }
375

376
  /**
377
   * Initializes the client session either from the url or from storage.
378
   * This method is automatically called when instantiating the client, but should also be called
379
   * manually when checking for an error from an auth redirect (oauth, magiclink, password recovery, etc).
380
   */
381
  async initialize(): Promise<InitializeResult> {
382
    if (this.initializePromise) {
238✔
383
      return await this.initializePromise
56✔
384
    }
385

386
    this.initializePromise = (async () => {
182✔
387
      return await this._acquireLock(-1, async () => {
182✔
388
        return await this._initialize()
176✔
389
      })
390
    })()
391

392
    return await this.initializePromise
182✔
393
  }
394

395
  /**
396
   * IMPORTANT:
397
   * 1. Never throw in this method, as it is called from the constructor
398
   * 2. Never return a session from this method as it would be cached over
399
   *    the whole lifetime of the client
400
   */
401
  private async _initialize(): Promise<InitializeResult> {
402
    try {
176✔
403
      const params = parseParametersFromURL(window.location.href)
176✔
404
      let callbackUrlType = 'none'
70✔
405
      if (this._isImplicitGrantCallback(params)) {
70✔
406
        callbackUrlType = 'implicit'
8✔
407
      } else if (await this._isPKCECallback(params)) {
62✔
408
        callbackUrlType = 'pkce'
2✔
409
      }
410

411
      /**
412
       * Attempt to get the session from the URL only if these conditions are fulfilled
413
       *
414
       * Note: If the URL isn't one of the callback url types (implicit or pkce),
415
       * then there could be an existing session so we don't want to prematurely remove it
416
       */
417
      if (isBrowser() && this.detectSessionInUrl && callbackUrlType !== 'none') {
70✔
418
        const { data, error } = await this._getSessionFromURL(params, callbackUrlType)
10✔
419
        if (error) {
6✔
420
          this._debug('#_initialize()', 'error detecting session from URL', error)
4✔
421

422
          if (isAuthImplicitGrantRedirectError(error)) {
4✔
423
            const errorCode = error.details?.code
4✔
424
            if (
4!
425
              errorCode === 'identity_already_exists' ||
12✔
426
              errorCode === 'identity_not_found' ||
427
              errorCode === 'single_identity_not_deletable'
428
            ) {
UNCOV
429
              return { error }
×
430
            }
431
          }
432

433
          // failed login attempt via url,
434
          // remove old session as in verifyOtp, signUp and signInWith*
435
          await this._removeSession()
4✔
436

437
          return { error }
4✔
438
        }
439

440
        const { session, redirectType } = data
2✔
441

442
        this._debug(
2✔
443
          '#_initialize()',
444
          'detected session in URL',
445
          session,
446
          'redirect type',
447
          redirectType
448
        )
449

450
        await this._saveSession(session)
2✔
451

452
        setTimeout(async () => {
2✔
453
          if (redirectType === 'recovery') {
2!
UNCOV
454
            await this._notifyAllSubscribers('PASSWORD_RECOVERY', session)
×
455
          } else {
456
            await this._notifyAllSubscribers('SIGNED_IN', session)
2✔
457
          }
458
        }, 0)
459

460
        return { error: null }
2✔
461
      }
462
      // no login attempt via callback url try to recover session from storage
463
      await this._recoverAndRefresh()
60✔
464
      return { error: null }
60✔
465
    } catch (error) {
466
      if (isAuthError(error)) {
110!
UNCOV
467
        return { error }
×
468
      }
469

470
      return {
110✔
471
        error: new AuthUnknownError('Unexpected error during initialization', error),
472
      }
473
    } finally {
474
      await this._handleVisibilityChange()
176✔
475
      this._debug('#_initialize()', 'end')
176✔
476
    }
477
  }
478

479
  /**
480
   * Creates a new anonymous user.
481
   *
482
   * @returns A session where the is_anonymous claim in the access token JWT set to true
483
   */
484
  async signInAnonymously(credentials?: SignInAnonymouslyCredentials): Promise<AuthResponse> {
485
    try {
6✔
486
      const res = await _request(this.fetch, 'POST', `${this.url}/signup`, {
6✔
487
        headers: this.headers,
488
        body: {
489
          data: credentials?.options?.data ?? {},
54✔
490
          gotrue_meta_security: { captcha_token: credentials?.options?.captchaToken },
36✔
491
        },
492
        xform: _sessionResponse,
493
      })
494
      const { data, error } = res
4✔
495

496
      if (error || !data) {
4!
UNCOV
497
        return { data: { user: null, session: null }, error: error }
×
498
      }
499
      const session: Session | null = data.session
4✔
500
      const user: User | null = data.user
4✔
501

502
      if (data.session) {
4✔
503
        await this._saveSession(data.session)
4✔
504
        await this._notifyAllSubscribers('SIGNED_IN', session)
4✔
505
      }
506

507
      return { data: { user, session }, error: null }
4✔
508
    } catch (error) {
509
      if (isAuthError(error)) {
2✔
510
        return { data: { user: null, session: null }, error }
2✔
511
      }
512

UNCOV
513
      throw error
×
514
    }
515
  }
516

517
  /**
518
   * Creates a new user.
519
   *
520
   * Be aware that if a user account exists in the system you may get back an
521
   * error message that attempts to hide this information from the user.
522
   * This method has support for PKCE via email signups. The PKCE flow cannot be used when autoconfirm is enabled.
523
   *
524
   * @returns A logged-in session if the server has "autoconfirm" ON
525
   * @returns A user if the server has "autoconfirm" OFF
526
   */
527
  async signUp(credentials: SignUpWithPasswordCredentials): Promise<AuthResponse> {
528
    try {
127✔
529
      let res: AuthResponse
530
      if ('email' in credentials) {
127✔
531
        const { email, password, options } = credentials
121✔
532
        let codeChallenge: string | null = null
121✔
533
        let codeChallengeMethod: string | null = null
121✔
534
        if (this.flowType === 'pkce') {
121✔
535
          ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
4✔
536
            this.storage,
537
            this.storageKey
538
          )
539
        }
540
        res = await _request(this.fetch, 'POST', `${this.url}/signup`, {
121✔
541
          headers: this.headers,
542
          redirectTo: options?.emailRedirectTo,
363✔
543
          body: {
544
            email,
545
            password,
546
            data: options?.data ?? {},
726✔
547
            gotrue_meta_security: { captcha_token: options?.captchaToken },
363✔
548
            code_challenge: codeChallenge,
549
            code_challenge_method: codeChallengeMethod,
550
          },
551
          xform: _sessionResponse,
552
        })
553
      } else if ('phone' in credentials) {
6✔
554
        const { phone, password, options } = credentials
4✔
555
        res = await _request(this.fetch, 'POST', `${this.url}/signup`, {
4✔
556
          headers: this.headers,
557
          body: {
558
            phone,
559
            password,
560
            data: options?.data ?? {},
24✔
561
            channel: options?.channel ?? 'sms',
24✔
562
            gotrue_meta_security: { captcha_token: options?.captchaToken },
12✔
563
          },
564
          xform: _sessionResponse,
565
        })
566
      } else {
567
        throw new AuthInvalidCredentialsError(
2✔
568
          'You must provide either an email or phone number and a password'
569
        )
570
      }
571

572
      const { data, error } = res
113✔
573

574
      if (error || !data) {
113!
UNCOV
575
        return { data: { user: null, session: null }, error: error }
×
576
      }
577

578
      const session: Session | null = data.session
113✔
579
      const user: User | null = data.user
113✔
580

581
      if (data.session) {
113✔
582
        await this._saveSession(data.session)
111✔
583
        await this._notifyAllSubscribers('SIGNED_IN', session)
111✔
584
      }
585

586
      return { data: { user, session }, error: null }
113✔
587
    } catch (error) {
588
      if (isAuthError(error)) {
14✔
589
        return { data: { user: null, session: null }, error }
14✔
590
      }
591

UNCOV
592
      throw error
×
593
    }
594
  }
595

596
  /**
597
   * Log in an existing user with an email and password or phone and password.
598
   *
599
   * Be aware that you may get back an error message that will not distinguish
600
   * between the cases where the account does not exist or that the
601
   * email/phone and password combination is wrong or that the account can only
602
   * be accessed via social login.
603
   */
604
  async signInWithPassword(
605
    credentials: SignInWithPasswordCredentials
606
  ): Promise<AuthTokenResponsePassword> {
607
    try {
36✔
608
      let res: AuthResponsePassword
609
      if ('email' in credentials) {
36✔
610
        const { email, password, options } = credentials
28✔
611
        res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=password`, {
28✔
612
          headers: this.headers,
613
          body: {
614
            email,
615
            password,
616
            gotrue_meta_security: { captcha_token: options?.captchaToken },
84✔
617
          },
618
          xform: _sessionResponsePassword,
619
        })
620
      } else if ('phone' in credentials) {
8✔
621
        const { phone, password, options } = credentials
6✔
622
        res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=password`, {
6✔
623
          headers: this.headers,
624
          body: {
625
            phone,
626
            password,
627
            gotrue_meta_security: { captcha_token: options?.captchaToken },
18✔
628
          },
629
          xform: _sessionResponsePassword,
630
        })
631
      } else {
632
        throw new AuthInvalidCredentialsError(
2✔
633
          'You must provide either an email or phone number and a password'
634
        )
635
      }
636
      const { data, error } = res
28✔
637

638
      if (error) {
28!
UNCOV
639
        return { data: { user: null, session: null }, error }
×
640
      } else if (!data || !data.session || !data.user) {
28✔
641
        return { data: { user: null, session: null }, error: new AuthInvalidTokenResponseError() }
2✔
642
      }
643
      if (data.session) {
26✔
644
        await this._saveSession(data.session)
26✔
645
        await this._notifyAllSubscribers('SIGNED_IN', data.session)
26✔
646
      }
647
      return {
26✔
648
        data: {
649
          user: data.user,
650
          session: data.session,
651
          ...(data.weak_password ? { weakPassword: data.weak_password } : null),
26!
652
        },
653
        error,
654
      }
655
    } catch (error) {
656
      if (isAuthError(error)) {
8✔
657
        return { data: { user: null, session: null }, error }
8✔
658
      }
UNCOV
659
      throw error
×
660
    }
661
  }
662

663
  /**
664
   * Log in an existing user via a third-party provider.
665
   * This method supports the PKCE flow.
666
   */
667
  async signInWithOAuth(credentials: SignInWithOAuthCredentials): Promise<OAuthResponse> {
668
    return await this._handleProviderSignIn(credentials.provider, {
20✔
669
      redirectTo: credentials.options?.redirectTo,
60✔
670
      scopes: credentials.options?.scopes,
60✔
671
      queryParams: credentials.options?.queryParams,
60✔
672
      skipBrowserRedirect: credentials.options?.skipBrowserRedirect,
60✔
673
    })
674
  }
675

676
  /**
677
   * Log in an existing user by exchanging an Auth Code issued during the PKCE flow.
678
   */
679
  async exchangeCodeForSession(authCode: string): Promise<AuthTokenResponse> {
680
    await this.initializePromise
4✔
681

682
    return this._acquireLock(-1, async () => {
4✔
683
      return this._exchangeCodeForSession(authCode)
4✔
684
    })
685
  }
686

687
  /**
688
   * Signs in a user by verifying a message signed by the user's private key.
689
   * Supports Ethereum (via Sign-In-With-Ethereum) & Solana (Sign-In-With-Solana) standards,
690
   * both of which derive from the EIP-4361 standard
691
   * With slight variation on Solana's side.
692
   * @reference https://eips.ethereum.org/EIPS/eip-4361
693
   */
694
  async signInWithWeb3(credentials: Web3Credentials): Promise<
695
    | {
696
        data: { session: Session; user: User }
697
        error: null
698
      }
699
    | { data: { session: null; user: null }; error: AuthError }
700
  > {
701
    const { chain } = credentials
36✔
702

703
    switch (chain) {
36✔
704
      case 'ethereum':
705
        return await this.signInWithEthereum(credentials)
20✔
706
      case 'solana':
707
        return await this.signInWithSolana(credentials)
12✔
708
      default:
709
        throw new Error(`@supabase/auth-js: Unsupported chain "${chain}"`)
4✔
710
    }
711
  }
712

713
  private async signInWithEthereum(
714
    credentials: EthereumWeb3Credentials
715
  ): Promise<
716
    | { data: { session: Session; user: User }; error: null }
717
    | { data: { session: null; user: null }; error: AuthError }
718
  > {
719
    // TODO: flatten type
720
    let message: string
721
    let signature: Hex
722

723
    if ('message' in credentials) {
20✔
724
      message = credentials.message
8✔
725
      signature = credentials.signature
8✔
726
    } else {
727
      const { chain, wallet, statement, options } = credentials
12✔
728

729
      let resolvedWallet: EthereumWallet
730

731
      if (!isBrowser()) {
12✔
732
        if (typeof wallet !== 'object' || !options?.url) {
8!
733
          throw new Error(
4✔
734
            '@supabase/auth-js: Both wallet and url must be specified in non-browser environments.'
735
          )
736
        }
737

738
        resolvedWallet = wallet
4✔
739
      } else if (typeof wallet === 'object') {
4!
740
        resolvedWallet = wallet
4✔
741
      } else {
742
        const windowAny = window as any
×
743

UNCOV
744
        if (
×
745
          'ethereum' in windowAny &&
×
746
          typeof windowAny.ethereum === 'object' &&
747
          'request' in windowAny.ethereum &&
748
          typeof windowAny.ethereum.request === 'function'
749
        ) {
750
          resolvedWallet = windowAny.ethereum
×
751
        } else {
UNCOV
752
          throw new Error(
×
753
            `@supabase/auth-js: No compatible Ethereum wallet interface on the window object (window.ethereum) detected. Make sure the user already has a wallet installed and connected for this app. Prefer passing the wallet interface object directly to signInWithWeb3({ chain: 'ethereum', wallet: resolvedUserWallet }) instead.`
754
          )
755
        }
756
      }
757

758
      const url = new URL(options?.url ?? window.location.href)
8✔
759

760
      const accounts = await resolvedWallet
8✔
761
        .request({
762
          method: 'eth_requestAccounts',
763
        })
764
        .then((accs) => accs as string[])
4✔
765
        .catch(() => {
766
          throw new Error(
2✔
767
            `@supabase/auth-js: Wallet method eth_requestAccounts is missing or invalid`
768
          )
769
        })
770

771
      if (!accounts || accounts.length === 0) {
4✔
772
        throw new Error(
2✔
773
          `@supabase/auth-js: No accounts available. Please ensure the wallet is connected.`
774
        )
775
      }
776

777
      const address = getAddress(accounts[0])
2✔
778

779
      let chainId = options?.signInWithEthereum?.chainId
×
UNCOV
780
      if (!chainId) {
×
UNCOV
781
        const chainIdHex = await resolvedWallet.request({
×
782
          method: 'eth_chainId',
783
        })
UNCOV
784
        chainId = fromHex(chainIdHex as Hex)
×
785
      }
786

UNCOV
787
      const siweMessage: SiweMessage = {
×
788
        domain: url.host,
789
        address: address,
790
        statement: statement,
791
        uri: url.href,
792
        version: '1',
793
        chainId: chainId,
794
        nonce: options?.signInWithEthereum?.nonce,
×
795
        issuedAt: options?.signInWithEthereum?.issuedAt ?? new Date(),
×
796
        expirationTime: options?.signInWithEthereum?.expirationTime,
×
797
        notBefore: options?.signInWithEthereum?.notBefore,
×
798
        requestId: options?.signInWithEthereum?.requestId,
×
799
        resources: options?.signInWithEthereum?.resources,
×
800
      }
801

UNCOV
802
      message = createSiweMessage(siweMessage)
×
803

804
      // Sign message
UNCOV
805
      signature = (await resolvedWallet.request({
×
806
        method: 'personal_sign',
807
        params: [toHex(message), address],
808
      })) as Hex
809
    }
810

811
    try {
8✔
812
      const { data, error } = await _request(
8✔
813
        this.fetch,
814
        'POST',
815
        `${this.url}/token?grant_type=web3`,
816
        {
817
          headers: this.headers,
818
          body: {
819
            chain: 'ethereum',
820
            message,
821
            signature,
822
            ...(credentials.options?.captchaToken
32!
823
              ? { gotrue_meta_security: { captcha_token: credentials.options?.captchaToken } }
×
824
              : null),
825
          },
826
          xform: _sessionResponse,
827
        }
828
      )
UNCOV
829
      if (error) {
×
830
        throw error
×
831
      }
UNCOV
832
      if (!data || !data.session || !data.user) {
×
UNCOV
833
        return {
×
834
          data: { user: null, session: null },
835
          error: new AuthInvalidTokenResponseError(),
836
        }
837
      }
838
      if (data.session) {
×
UNCOV
839
        await this._saveSession(data.session)
×
840
        await this._notifyAllSubscribers('SIGNED_IN', data.session)
×
841
      }
UNCOV
842
      return { data: { ...data }, error }
×
843
    } catch (error) {
844
      if (isAuthError(error)) {
8✔
845
        return { data: { user: null, session: null }, error }
8✔
846
      }
847

UNCOV
848
      throw error
×
849
    }
850
  }
851

852
  private async signInWithSolana(credentials: SolanaWeb3Credentials) {
853
    let message: string
854
    let signature: Uint8Array
855

856
    if ('message' in credentials) {
12✔
857
      message = credentials.message
2✔
858
      signature = credentials.signature
2✔
859
    } else {
860
      const { chain, wallet, statement, options } = credentials
10✔
861

862
      let resolvedWallet: SolanaWallet
863

864
      if (!isBrowser()) {
10✔
865
        if (typeof wallet !== 'object' || !options?.url) {
6!
866
          throw new Error(
2✔
867
            '@supabase/auth-js: Both wallet and url must be specified in non-browser environments.'
868
          )
869
        }
870

871
        resolvedWallet = wallet
4✔
872
      } else if (typeof wallet === 'object') {
4!
873
        resolvedWallet = wallet
4✔
874
      } else {
875
        const windowAny = window as any
×
876

UNCOV
877
        if (
×
878
          'solana' in windowAny &&
×
879
          typeof windowAny.solana === 'object' &&
880
          (('signIn' in windowAny.solana && typeof windowAny.solana.signIn === 'function') ||
881
            ('signMessage' in windowAny.solana &&
882
              typeof windowAny.solana.signMessage === 'function'))
883
        ) {
884
          resolvedWallet = windowAny.solana
×
885
        } else {
UNCOV
886
          throw new Error(
×
887
            `@supabase/auth-js: No compatible Solana wallet interface on the window object (window.solana) detected. Make sure the user already has a wallet installed and connected for this app. Prefer passing the wallet interface object directly to signInWithWeb3({ chain: 'solana', wallet: resolvedUserWallet }) instead.`
888
          )
889
        }
890
      }
891

892
      const url = new URL(options?.url ?? window.location.href)
8✔
893

894
      if ('signIn' in resolvedWallet && resolvedWallet.signIn) {
8!
UNCOV
895
        const output = await resolvedWallet.signIn({
×
896
          issuedAt: new Date().toISOString(),
897

898
          ...options?.signInWithSolana,
×
899

900
          // non-overridable properties
901
          version: '1',
902
          domain: url.host,
903
          uri: url.href,
904

905
          ...(statement ? { statement } : null),
×
906
        })
907

908
        let outputToProcess: any
909

910
        if (Array.isArray(output) && output[0] && typeof output[0] === 'object') {
×
UNCOV
911
          outputToProcess = output[0]
×
UNCOV
912
        } else if (
×
913
          output &&
×
914
          typeof output === 'object' &&
915
          'signedMessage' in output &&
916
          'signature' in output
917
        ) {
918
          outputToProcess = output
×
919
        } else {
UNCOV
920
          throw new Error('@supabase/auth-js: Wallet method signIn() returned unrecognized value')
×
921
        }
922

UNCOV
923
        if (
×
924
          'signedMessage' in outputToProcess &&
×
925
          'signature' in outputToProcess &&
926
          (typeof outputToProcess.signedMessage === 'string' ||
927
            outputToProcess.signedMessage instanceof Uint8Array) &&
928
          outputToProcess.signature instanceof Uint8Array
929
        ) {
UNCOV
930
          message =
×
931
            typeof outputToProcess.signedMessage === 'string'
×
932
              ? outputToProcess.signedMessage
933
              : new TextDecoder().decode(outputToProcess.signedMessage)
934
          signature = outputToProcess.signature
×
935
        } else {
UNCOV
936
          throw new Error(
×
937
            '@supabase/auth-js: Wallet method signIn() API returned object without signedMessage and signature fields'
938
          )
939
        }
940
      } else {
941
        if (
8✔
942
          !('signMessage' in resolvedWallet) ||
24✔
943
          typeof resolvedWallet.signMessage !== 'function' ||
944
          !('publicKey' in resolvedWallet) ||
945
          typeof resolvedWallet !== 'object' ||
946
          !resolvedWallet.publicKey ||
947
          !('toBase58' in resolvedWallet.publicKey) ||
948
          typeof resolvedWallet.publicKey.toBase58 !== 'function'
949
        ) {
950
          throw new Error(
6✔
951
            '@supabase/auth-js: Wallet does not have a compatible signMessage() and publicKey.toBase58() API'
952
          )
953
        }
954

955
        message = [
2✔
956
          `${url.host} wants you to sign in with your Solana account:`,
957
          resolvedWallet.publicKey.toBase58(),
958
          ...(statement ? ['', statement, ''] : ['']),
2!
959
          'Version: 1',
960
          `URI: ${url.href}`,
961
          `Issued At: ${options?.signInWithSolana?.issuedAt ?? new Date().toISOString()}`,
18!
962
          ...(options?.signInWithSolana?.notBefore
14!
963
            ? [`Not Before: ${options.signInWithSolana.notBefore}`]
964
            : []),
965
          ...(options?.signInWithSolana?.expirationTime
14!
966
            ? [`Expiration Time: ${options.signInWithSolana.expirationTime}`]
967
            : []),
968
          ...(options?.signInWithSolana?.chainId
14!
969
            ? [`Chain ID: ${options.signInWithSolana.chainId}`]
970
            : []),
971
          ...(options?.signInWithSolana?.nonce ? [`Nonce: ${options.signInWithSolana.nonce}`] : []),
14!
972
          ...(options?.signInWithSolana?.requestId
14!
973
            ? [`Request ID: ${options.signInWithSolana.requestId}`]
974
            : []),
975
          ...(options?.signInWithSolana?.resources?.length
20!
976
            ? [
977
                'Resources',
UNCOV
978
                ...options.signInWithSolana.resources.map((resource) => `- ${resource}`),
×
979
              ]
980
            : []),
981
        ].join('\n')
982

983
        const maybeSignature = await resolvedWallet.signMessage(
2✔
984
          new TextEncoder().encode(message),
985
          'utf8'
986
        )
987

UNCOV
988
        if (!maybeSignature || !(maybeSignature instanceof Uint8Array)) {
×
UNCOV
989
          throw new Error(
×
990
            '@supabase/auth-js: Wallet signMessage() API returned an recognized value'
991
          )
992
        }
993

UNCOV
994
        signature = maybeSignature
×
995
      }
996
    }
997

998
    try {
2✔
999
      const { data, error } = await _request(
2✔
1000
        this.fetch,
1001
        'POST',
1002
        `${this.url}/token?grant_type=web3`,
1003
        {
1004
          headers: this.headers,
1005
          body: {
1006
            chain: 'solana',
1007
            message,
1008
            signature: bytesToBase64URL(signature),
1009

1010
            ...(credentials.options?.captchaToken
8!
1011
              ? { gotrue_meta_security: { captcha_token: credentials.options?.captchaToken } }
×
1012
              : null),
1013
          },
1014
          xform: _sessionResponse,
1015
        }
1016
      )
UNCOV
1017
      if (error) {
×
1018
        throw error
×
1019
      }
UNCOV
1020
      if (!data || !data.session || !data.user) {
×
UNCOV
1021
        return {
×
1022
          data: { user: null, session: null },
1023
          error: new AuthInvalidTokenResponseError(),
1024
        }
1025
      }
1026
      if (data.session) {
×
UNCOV
1027
        await this._saveSession(data.session)
×
1028
        await this._notifyAllSubscribers('SIGNED_IN', data.session)
×
1029
      }
UNCOV
1030
      return { data: { ...data }, error }
×
1031
    } catch (error) {
1032
      if (isAuthError(error)) {
2✔
1033
        return { data: { user: null, session: null }, error }
2✔
1034
      }
1035

UNCOV
1036
      throw error
×
1037
    }
1038
  }
1039

1040
  private async _exchangeCodeForSession(authCode: string): Promise<
1041
    | {
1042
        data: { session: Session; user: User; redirectType: string | null }
1043
        error: null
1044
      }
1045
    | { data: { session: null; user: null; redirectType: null }; error: AuthError }
1046
  > {
1047
    const storageItem = await getItemAsync(this.storage, `${this.storageKey}-code-verifier`)
6✔
1048
    const [codeVerifier, redirectType] = ((storageItem ?? '') as string).split('/')
6✔
1049

1050
    try {
6✔
1051
      const { data, error } = await _request(
6✔
1052
        this.fetch,
1053
        'POST',
1054
        `${this.url}/token?grant_type=pkce`,
1055
        {
1056
          headers: this.headers,
1057
          body: {
1058
            auth_code: authCode,
1059
            code_verifier: codeVerifier,
1060
          },
1061
          xform: _sessionResponse,
1062
        }
1063
      )
1064
      await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
×
UNCOV
1065
      if (error) {
×
1066
        throw error
×
1067
      }
UNCOV
1068
      if (!data || !data.session || !data.user) {
×
UNCOV
1069
        return {
×
1070
          data: { user: null, session: null, redirectType: null },
1071
          error: new AuthInvalidTokenResponseError(),
1072
        }
1073
      }
1074
      if (data.session) {
×
UNCOV
1075
        await this._saveSession(data.session)
×
1076
        await this._notifyAllSubscribers('SIGNED_IN', data.session)
×
1077
      }
UNCOV
1078
      return { data: { ...data, redirectType: redirectType ?? null }, error }
×
1079
    } catch (error) {
1080
      if (isAuthError(error)) {
6✔
1081
        return { data: { user: null, session: null, redirectType: null }, error }
6✔
1082
      }
1083

UNCOV
1084
      throw error
×
1085
    }
1086
  }
1087

1088
  /**
1089
   * Allows signing in with an OIDC ID token. The authentication provider used
1090
   * should be enabled and configured.
1091
   */
1092
  async signInWithIdToken(credentials: SignInWithIdTokenCredentials): Promise<AuthTokenResponse> {
1093
    try {
6✔
1094
      const { options, provider, token, access_token, nonce } = credentials
6✔
1095

1096
      const res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=id_token`, {
6✔
1097
        headers: this.headers,
1098
        body: {
1099
          provider,
1100
          id_token: token,
1101
          access_token,
1102
          nonce,
1103
          gotrue_meta_security: { captcha_token: options?.captchaToken },
18✔
1104
        },
1105
        xform: _sessionResponse,
1106
      })
1107

1108
      const { data, error } = res
2✔
1109
      if (error) {
2!
UNCOV
1110
        return { data: { user: null, session: null }, error }
×
1111
      } else if (!data || !data.session || !data.user) {
2!
1112
        return {
2✔
1113
          data: { user: null, session: null },
1114
          error: new AuthInvalidTokenResponseError(),
1115
        }
1116
      }
1117
      if (data.session) {
×
UNCOV
1118
        await this._saveSession(data.session)
×
1119
        await this._notifyAllSubscribers('SIGNED_IN', data.session)
×
1120
      }
UNCOV
1121
      return { data, error }
×
1122
    } catch (error) {
1123
      if (isAuthError(error)) {
4✔
1124
        return { data: { user: null, session: null }, error }
4✔
1125
      }
UNCOV
1126
      throw error
×
1127
    }
1128
  }
1129

1130
  /**
1131
   * Log in a user using magiclink or a one-time password (OTP).
1132
   *
1133
   * If the `{{ .ConfirmationURL }}` variable is specified in the email template, a magiclink will be sent.
1134
   * If the `{{ .Token }}` variable is specified in the email template, an OTP will be sent.
1135
   * 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.
1136
   *
1137
   * Be aware that you may get back an error message that will not distinguish
1138
   * between the cases where the account does not exist or, that the account
1139
   * can only be accessed via social login.
1140
   *
1141
   * Do note that you will need to configure a Whatsapp sender on Twilio
1142
   * if you are using phone sign in with the 'whatsapp' channel. The whatsapp
1143
   * channel is not supported on other providers
1144
   * at this time.
1145
   * This method supports PKCE when an email is passed.
1146
   */
1147
  async signInWithOtp(credentials: SignInWithPasswordlessCredentials): Promise<AuthOtpResponse> {
1148
    try {
16✔
1149
      if ('email' in credentials) {
16✔
1150
        const { email, options } = credentials
8✔
1151
        let codeChallenge: string | null = null
8✔
1152
        let codeChallengeMethod: string | null = null
8✔
1153
        if (this.flowType === 'pkce') {
8✔
1154
          ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
2✔
1155
            this.storage,
1156
            this.storageKey
1157
          )
1158
        }
1159
        const { error } = await _request(this.fetch, 'POST', `${this.url}/otp`, {
8✔
1160
          headers: this.headers,
1161
          body: {
1162
            email,
1163
            data: options?.data ?? {},
48✔
1164
            create_user: options?.shouldCreateUser ?? true,
48✔
1165
            gotrue_meta_security: { captcha_token: options?.captchaToken },
24✔
1166
            code_challenge: codeChallenge,
1167
            code_challenge_method: codeChallengeMethod,
1168
          },
1169
          redirectTo: options?.emailRedirectTo,
24✔
1170
        })
1171
        return { data: { user: null, session: null }, error }
4✔
1172
      }
1173
      if ('phone' in credentials) {
8✔
1174
        const { phone, options } = credentials
6✔
1175
        const { data, error } = await _request(this.fetch, 'POST', `${this.url}/otp`, {
6✔
1176
          headers: this.headers,
1177
          body: {
1178
            phone,
1179
            data: options?.data ?? {},
36✔
1180
            create_user: options?.shouldCreateUser ?? true,
36✔
1181
            gotrue_meta_security: { captcha_token: options?.captchaToken },
18✔
1182
            channel: options?.channel ?? 'sms',
36✔
1183
          },
1184
        })
UNCOV
1185
        return { data: { user: null, session: null, messageId: data?.message_id }, error }
×
1186
      }
1187
      throw new AuthInvalidCredentialsError('You must provide either an email or phone number.')
2✔
1188
    } catch (error) {
1189
      if (isAuthError(error)) {
12✔
1190
        return { data: { user: null, session: null }, error }
12✔
1191
      }
1192

UNCOV
1193
      throw error
×
1194
    }
1195
  }
1196

1197
  /**
1198
   * Log in a user given a User supplied OTP or TokenHash received through mobile or email.
1199
   */
1200
  async verifyOtp(params: VerifyOtpParams): Promise<AuthResponse> {
1201
    try {
10✔
1202
      let redirectTo: string | undefined = undefined
10✔
1203
      let captchaToken: string | undefined = undefined
10✔
1204
      if ('options' in params) {
10✔
1205
        redirectTo = params.options?.redirectTo
4!
1206
        captchaToken = params.options?.captchaToken
4!
1207
      }
1208
      const { data, error } = await _request(this.fetch, 'POST', `${this.url}/verify`, {
10✔
1209
        headers: this.headers,
1210
        body: {
1211
          ...params,
1212
          gotrue_meta_security: { captcha_token: captchaToken },
1213
        },
1214
        redirectTo,
1215
        xform: _sessionResponse,
1216
      })
1217

UNCOV
1218
      if (error) {
×
UNCOV
1219
        throw error
×
1220
      }
1221

UNCOV
1222
      if (!data) {
×
UNCOV
1223
        throw new Error('An error occurred on token verification.')
×
1224
      }
1225

UNCOV
1226
      const session: Session | null = data.session
×
1227
      const user: User = data.user
×
1228

1229
      if (session?.access_token) {
×
UNCOV
1230
        await this._saveSession(session as Session)
×
UNCOV
1231
        await this._notifyAllSubscribers(
×
1232
          params.type == 'recovery' ? 'PASSWORD_RECOVERY' : 'SIGNED_IN',
×
1233
          session
1234
        )
1235
      }
1236

UNCOV
1237
      return { data: { user, session }, error: null }
×
1238
    } catch (error) {
1239
      if (isAuthError(error)) {
10✔
1240
        return { data: { user: null, session: null }, error }
10✔
1241
      }
1242

UNCOV
1243
      throw error
×
1244
    }
1245
  }
1246

1247
  /**
1248
   * Attempts a single-sign on using an enterprise Identity Provider. A
1249
   * successful SSO attempt will redirect the current page to the identity
1250
   * provider authorization page. The redirect URL is implementation and SSO
1251
   * protocol specific.
1252
   *
1253
   * You can use it by providing a SSO domain. Typically you can extract this
1254
   * domain by asking users for their email address. If this domain is
1255
   * registered on the Auth instance the redirect will use that organization's
1256
   * currently active SSO Identity Provider for the login.
1257
   *
1258
   * If you have built an organization-specific login page, you can use the
1259
   * organization's SSO Identity Provider UUID directly instead.
1260
   */
1261
  async signInWithSSO(params: SignInWithSSO): Promise<SSOResponse> {
1262
    try {
8✔
1263
      let codeChallenge: string | null = null
8✔
1264
      let codeChallengeMethod: string | null = null
8✔
1265
      if (this.flowType === 'pkce') {
8✔
1266
        ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
8✔
1267
          this.storage,
1268
          this.storageKey
1269
        )
1270
      }
1271

1272
      return await _request(this.fetch, 'POST', `${this.url}/sso`, {
8✔
1273
        body: {
1274
          ...('providerId' in params ? { provider_id: params.providerId } : null),
8✔
1275
          ...('domain' in params ? { domain: params.domain } : null),
8✔
1276
          redirect_to: params.options?.redirectTo ?? undefined,
48✔
1277
          ...(params?.options?.captchaToken
56!
1278
            ? { gotrue_meta_security: { captcha_token: params.options.captchaToken } }
1279
            : null),
1280
          skip_http_redirect: true, // fetch does not handle redirects
1281
          code_challenge: codeChallenge,
1282
          code_challenge_method: codeChallengeMethod,
1283
        },
1284
        headers: this.headers,
1285
        xform: _ssoResponse,
1286
      })
1287
    } catch (error) {
1288
      if (isAuthError(error)) {
8✔
1289
        return { data: null, error }
8✔
1290
      }
UNCOV
1291
      throw error
×
1292
    }
1293
  }
1294

1295
  /**
1296
   * Sends a reauthentication OTP to the user's email or phone number.
1297
   * Requires the user to be signed-in.
1298
   */
1299
  async reauthenticate(): Promise<AuthResponse> {
1300
    await this.initializePromise
4✔
1301

1302
    return await this._acquireLock(-1, async () => {
4✔
1303
      return await this._reauthenticate()
4✔
1304
    })
1305
  }
1306

1307
  private async _reauthenticate(): Promise<AuthResponse> {
1308
    try {
4✔
1309
      return await this._useSession(async (result) => {
4✔
1310
        const {
1311
          data: { session },
1312
          error: sessionError,
1313
        } = result
4✔
1314
        if (sessionError) throw sessionError
4!
1315
        if (!session) throw new AuthSessionMissingError()
4!
1316

1317
        const { error } = await _request(this.fetch, 'GET', `${this.url}/reauthenticate`, {
4✔
1318
          headers: this.headers,
1319
          jwt: session.access_token,
1320
        })
1321
        return { data: { user: null, session: null }, error }
2✔
1322
      })
1323
    } catch (error) {
1324
      if (isAuthError(error)) {
2✔
1325
        return { data: { user: null, session: null }, error }
2✔
1326
      }
UNCOV
1327
      throw error
×
1328
    }
1329
  }
1330

1331
  /**
1332
   * Resends an existing signup confirmation email, email change email, SMS OTP or phone change OTP.
1333
   */
1334
  async resend(credentials: ResendParams): Promise<AuthOtpResponse> {
1335
    try {
10✔
1336
      const endpoint = `${this.url}/resend`
10✔
1337
      if ('email' in credentials) {
10✔
1338
        const { email, type, options } = credentials
4✔
1339
        const { error } = await _request(this.fetch, 'POST', endpoint, {
4✔
1340
          headers: this.headers,
1341
          body: {
1342
            email,
1343
            type,
1344
            gotrue_meta_security: { captcha_token: options?.captchaToken },
12!
1345
          },
1346
          redirectTo: options?.emailRedirectTo,
12!
1347
        })
1348
        return { data: { user: null, session: null }, error }
4✔
1349
      } else if ('phone' in credentials) {
6✔
1350
        const { phone, type, options } = credentials
4✔
1351
        const { data, error } = await _request(this.fetch, 'POST', endpoint, {
4✔
1352
          headers: this.headers,
1353
          body: {
1354
            phone,
1355
            type,
1356
            gotrue_meta_security: { captcha_token: options?.captchaToken },
12!
1357
          },
1358
        })
1359
        return { data: { user: null, session: null, messageId: data?.message_id }, error }
4!
1360
      }
1361
      throw new AuthInvalidCredentialsError(
2✔
1362
        'You must provide either an email or phone number and a type'
1363
      )
1364
    } catch (error) {
1365
      if (isAuthError(error)) {
2✔
1366
        return { data: { user: null, session: null }, error }
2✔
1367
      }
UNCOV
1368
      throw error
×
1369
    }
1370
  }
1371

1372
  /**
1373
   * Returns the session, refreshing it if necessary.
1374
   *
1375
   * 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.
1376
   *
1377
   * **IMPORTANT:** This method loads values directly from the storage attached
1378
   * to the client. If that storage is based on request cookies for example,
1379
   * the values in it may not be authentic and therefore it's strongly advised
1380
   * against using this method and its results in such circumstances. A warning
1381
   * will be emitted if this is detected. Use {@link #getUser()} instead.
1382
   */
1383
  async getSession() {
1384
    await this.initializePromise
75✔
1385

1386
    const result = await this._acquireLock(-1, async () => {
75✔
1387
      return this._useSession(async (result) => {
75✔
1388
        return result
71✔
1389
      })
1390
    })
1391

1392
    return result
71✔
1393
  }
1394

1395
  /**
1396
   * Acquires a global lock based on the storage key.
1397
   */
1398
  private async _acquireLock<R>(acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
1399
    this._debug('#_acquireLock', 'begin', acquireTimeout)
605✔
1400

1401
    try {
605✔
1402
      if (this.lockAcquired) {
605✔
1403
        const last = this.pendingInLock.length
2!
1404
          ? this.pendingInLock[this.pendingInLock.length - 1]
1405
          : Promise.resolve()
1406

1407
        const result = (async () => {
2✔
1408
          await last
2✔
1409
          return await fn()
2✔
1410
        })()
1411

1412
        this.pendingInLock.push(
2✔
1413
          (async () => {
1414
            try {
2✔
1415
              await result
2✔
1416
            } catch (e: any) {
1417
              // we just care if it finished
1418
            }
1419
          })()
1420
        )
1421

1422
        return result
2✔
1423
      }
1424

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

1428
        try {
595✔
1429
          this.lockAcquired = true
595✔
1430

1431
          const result = fn()
595✔
1432

1433
          this.pendingInLock.push(
595✔
1434
            (async () => {
1435
              try {
595✔
1436
                await result
595✔
1437
              } catch (e: any) {
1438
                // we just care if it finished
1439
              }
1440
            })()
1441
          )
1442

1443
          await result
595✔
1444

1445
          // keep draining the queue until there's nothing to wait on
1446
          while (this.pendingInLock.length) {
587✔
1447
            const waitOn = [...this.pendingInLock]
587✔
1448

1449
            await Promise.all(waitOn)
587✔
1450

1451
            this.pendingInLock.splice(0, waitOn.length)
587✔
1452
          }
1453

1454
          return await result
587✔
1455
        } finally {
1456
          this._debug('#_acquireLock', 'lock released for storage key', this.storageKey)
595✔
1457

1458
          this.lockAcquired = false
595✔
1459
        }
1460
      })
1461
    } finally {
1462
      this._debug('#_acquireLock', 'end')
605✔
1463
    }
1464
  }
1465

1466
  /**
1467
   * Use instead of {@link #getSession} inside the library. It is
1468
   * semantically usually what you want, as getting a session involves some
1469
   * processing afterwards that requires only one client operating on the
1470
   * session at once across multiple tabs or processes.
1471
   */
1472
  private async _useSession<R>(
1473
    fn: (
1474
      result:
1475
        | {
1476
            data: {
1477
              session: Session
1478
            }
1479
            error: null
1480
          }
1481
        | {
1482
            data: {
1483
              session: null
1484
            }
1485
            error: AuthError
1486
          }
1487
        | {
1488
            data: {
1489
              session: null
1490
            }
1491
            error: null
1492
          }
1493
    ) => Promise<R>
1494
  ): Promise<R> {
1495
    this._debug('#_useSession', 'begin')
433✔
1496

1497
    try {
433✔
1498
      // the use of __loadSession here is the only correct use of the function!
1499
      const result = await this.__loadSession()
433✔
1500

1501
      return await fn(result)
427✔
1502
    } finally {
1503
      this._debug('#_useSession', 'end')
433✔
1504
    }
1505
  }
1506

1507
  /**
1508
   * NEVER USE DIRECTLY!
1509
   *
1510
   * Always use {@link #_useSession}.
1511
   */
1512
  private async __loadSession(): Promise<
1513
    | {
1514
        data: {
1515
          session: Session
1516
        }
1517
        error: null
1518
      }
1519
    | {
1520
        data: {
1521
          session: null
1522
        }
1523
        error: AuthError
1524
      }
1525
    | {
1526
        data: {
1527
          session: null
1528
        }
1529
        error: null
1530
      }
1531
  > {
1532
    this._debug('#__loadSession()', 'begin')
433✔
1533

1534
    if (!this.lockAcquired) {
433✔
1535
      this._debug('#__loadSession()', 'used outside of an acquired lock!', new Error().stack)
30✔
1536
    }
1537

1538
    try {
433✔
1539
      let currentSession: Session | null = null
433✔
1540

1541
      const maybeSession = await getItemAsync(this.storage, this.storageKey)
433✔
1542

1543
      this._debug('#getSession()', 'session from storage', maybeSession)
429✔
1544

1545
      if (maybeSession !== null) {
429✔
1546
        if (this._isValidSession(maybeSession)) {
189✔
1547
          currentSession = maybeSession
177✔
1548
        } else {
1549
          this._debug('#getSession()', 'session from storage is not valid')
12✔
1550
          await this._removeSession()
12✔
1551
        }
1552
      }
1553

1554
      if (!currentSession) {
427✔
1555
        return { data: { session: null }, error: null }
250✔
1556
      }
1557

1558
      // A session is considered expired before the access token _actually_
1559
      // expires. When the autoRefreshToken option is off (or when the tab is
1560
      // in the background), very eager users of getSession() -- like
1561
      // realtime-js -- might send a valid JWT which will expire by the time it
1562
      // reaches the server.
1563
      const hasExpired = currentSession.expires_at
177!
1564
        ? currentSession.expires_at * 1000 - Date.now() < EXPIRY_MARGIN_MS
1565
        : false
1566

1567
      this._debug(
177✔
1568
        '#__loadSession()',
1569
        `session has${hasExpired ? '' : ' not'} expired`,
177✔
1570
        'expires_at',
1571
        currentSession.expires_at
1572
      )
1573

1574
      if (!hasExpired) {
177✔
1575
        if (this.userStorage) {
167✔
1576
          const maybeUser: { user?: User | null } | null = (await getItemAsync(
8✔
1577
            this.userStorage,
1578
            this.storageKey + '-user'
1579
          )) as any
1580

1581
          if (maybeUser?.user) {
8!
UNCOV
1582
            currentSession.user = maybeUser.user
×
1583
          } else {
1584
            currentSession.user = userNotAvailableProxy()
8✔
1585
          }
1586
        }
1587

1588
        if (this.storage.isServer && currentSession.user) {
167✔
1589
          let suppressWarning = this.suppressGetSessionWarning
14✔
1590
          const proxySession: Session = new Proxy(currentSession, {
14✔
1591
            get: (target: any, prop: string, receiver: any) => {
1592
              if (!suppressWarning && prop === 'user') {
24✔
1593
                // only show warning when the user object is being accessed from the server
1594
                console.warn(
2✔
1595
                  '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.'
1596
                )
1597
                suppressWarning = true // keeps this proxy instance from logging additional warnings
2✔
1598
                this.suppressGetSessionWarning = true // keeps this client's future proxy instances from warning
2✔
1599
              }
1600
              return Reflect.get(target, prop, receiver)
24✔
1601
            },
1602
          })
1603
          currentSession = proxySession
14✔
1604
        }
1605

1606
        return { data: { session: currentSession }, error: null }
167✔
1607
      }
1608

1609
      const { data: session, error } = await this._callRefreshToken(currentSession.refresh_token)
10✔
1610
      if (error) {
10✔
1611
        return { data: { session: null }, error }
6✔
1612
      }
1613

1614
      return { data: { session }, error: null }
4✔
1615
    } finally {
1616
      this._debug('#__loadSession()', 'end')
433✔
1617
    }
1618
  }
1619

1620
  /**
1621
   * Gets the current user details if there is an existing session. This method
1622
   * performs a network request to the Supabase Auth server, so the returned
1623
   * value is authentic and can be used to base authorization rules on.
1624
   *
1625
   * @param jwt Takes in an optional access token JWT. If no JWT is provided, the JWT from the current session is used.
1626
   */
1627
  async getUser(jwt?: string): Promise<UserResponse> {
1628
    if (jwt) {
23✔
1629
      return await this._getUser(jwt)
7✔
1630
    }
1631

1632
    await this.initializePromise
16✔
1633

1634
    const result = await this._acquireLock(-1, async () => {
16✔
1635
      return await this._getUser()
16✔
1636
    })
1637

1638
    return result
16✔
1639
  }
1640

1641
  private async _getUser(jwt?: string): Promise<UserResponse> {
1642
    try {
37✔
1643
      if (jwt) {
37✔
1644
        return await _request(this.fetch, 'GET', `${this.url}/user`, {
21✔
1645
          headers: this.headers,
1646
          jwt: jwt,
1647
          xform: _userResponse,
1648
        })
1649
      }
1650

1651
      return await this._useSession(async (result) => {
16✔
1652
        const { data, error } = result
16✔
1653
        if (error) {
16!
UNCOV
1654
          throw error
×
1655
        }
1656

1657
        // returns an error if there is no access_token or custom authorization header
1658
        if (!data.session?.access_token && !this.hasCustomAuthorizationHeader) {
16✔
1659
          return { data: { user: null }, error: new AuthSessionMissingError() }
6✔
1660
        }
1661

1662
        return await _request(this.fetch, 'GET', `${this.url}/user`, {
10✔
1663
          headers: this.headers,
1664
          jwt: data.session?.access_token ?? undefined,
60!
1665
          xform: _userResponse,
1666
        })
1667
      })
1668
    } catch (error) {
1669
      if (isAuthError(error)) {
10✔
1670
        if (isAuthSessionMissingError(error)) {
2!
1671
          // JWT contains a `session_id` which does not correspond to an active
1672
          // session in the database, indicating the user is signed out.
1673

UNCOV
1674
          await this._removeSession()
×
UNCOV
1675
          await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
×
1676
        }
1677

1678
        return { data: { user: null }, error }
2✔
1679
      }
1680

1681
      throw error
8✔
1682
    }
1683
  }
1684

1685
  /**
1686
   * Updates user data for a logged in user.
1687
   */
1688
  async updateUser(
1689
    attributes: UserAttributes,
1690
    options: {
10✔
1691
      emailRedirectTo?: string | undefined
1692
    } = {}
1693
  ): Promise<UserResponse> {
1694
    await this.initializePromise
10✔
1695

1696
    return await this._acquireLock(-1, async () => {
10✔
1697
      return await this._updateUser(attributes, options)
10✔
1698
    })
1699
  }
1700

1701
  protected async _updateUser(
1702
    attributes: UserAttributes,
1703
    options: {
×
1704
      emailRedirectTo?: string | undefined
1705
    } = {}
1706
  ): Promise<UserResponse> {
1707
    try {
10✔
1708
      return await this._useSession(async (result) => {
10✔
1709
        const { data: sessionData, error: sessionError } = result
10✔
1710
        if (sessionError) {
10!
UNCOV
1711
          throw sessionError
×
1712
        }
1713
        if (!sessionData.session) {
10✔
1714
          throw new AuthSessionMissingError()
2✔
1715
        }
1716
        const session: Session = sessionData.session
8✔
1717
        let codeChallenge: string | null = null
8✔
1718
        let codeChallengeMethod: string | null = null
8✔
1719
        if (this.flowType === 'pkce' && attributes.email != null) {
8✔
1720
          ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
2✔
1721
            this.storage,
1722
            this.storageKey
1723
          )
1724
        }
1725

1726
        const { data, error: userError } = await _request(this.fetch, 'PUT', `${this.url}/user`, {
8✔
1727
          headers: this.headers,
1728
          redirectTo: options?.emailRedirectTo,
24!
1729
          body: {
1730
            ...attributes,
1731
            code_challenge: codeChallenge,
1732
            code_challenge_method: codeChallengeMethod,
1733
          },
1734
          jwt: session.access_token,
1735
          xform: _userResponse,
1736
        })
1737
        if (userError) throw userError
8!
1738
        session.user = data.user as User
8✔
1739
        await this._saveSession(session)
8✔
1740
        await this._notifyAllSubscribers('USER_UPDATED', session)
8✔
1741
        return { data: { user: session.user }, error: null }
8✔
1742
      })
1743
    } catch (error) {
1744
      if (isAuthError(error)) {
2✔
1745
        return { data: { user: null }, error }
2✔
1746
      }
1747

UNCOV
1748
      throw error
×
1749
    }
1750
  }
1751

1752
  /**
1753
   * 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.
1754
   * If the refresh token or access token in the current session is invalid, an error will be thrown.
1755
   * @param currentSession The current session that minimally contains an access token and refresh token.
1756
   */
1757
  async setSession(currentSession: {
1758
    access_token: string
1759
    refresh_token: string
1760
  }): Promise<AuthResponse> {
1761
    await this.initializePromise
12✔
1762

1763
    return await this._acquireLock(-1, async () => {
12✔
1764
      return await this._setSession(currentSession)
12✔
1765
    })
1766
  }
1767

1768
  protected async _setSession(currentSession: {
1769
    access_token: string
1770
    refresh_token: string
1771
  }): Promise<AuthResponse> {
1772
    try {
12✔
1773
      if (!currentSession.access_token || !currentSession.refresh_token) {
12✔
1774
        throw new AuthSessionMissingError()
4✔
1775
      }
1776

1777
      const timeNow = Date.now() / 1000
8✔
1778
      let expiresAt = timeNow
8✔
1779
      let hasExpired = true
8✔
1780
      let session: Session | null = null
8✔
1781
      const { payload } = decodeJWT(currentSession.access_token)
8✔
1782
      if (payload.exp) {
4✔
1783
        expiresAt = payload.exp
4✔
1784
        hasExpired = expiresAt <= timeNow
4✔
1785
      }
1786

1787
      if (hasExpired) {
4✔
1788
        const { data: refreshedSession, error } = await this._callRefreshToken(
2✔
1789
          currentSession.refresh_token
1790
        )
1791
        if (error) {
2✔
1792
          return { data: { user: null, session: null }, error: error }
2✔
1793
        }
1794

UNCOV
1795
        if (!refreshedSession) {
×
1796
          return { data: { user: null, session: null }, error: null }
×
1797
        }
UNCOV
1798
        session = refreshedSession
×
1799
      } else {
1800
        const { data, error } = await this._getUser(currentSession.access_token)
2✔
1801
        if (error) {
2!
UNCOV
1802
          throw error
×
1803
        }
1804
        session = {
2✔
1805
          access_token: currentSession.access_token,
1806
          refresh_token: currentSession.refresh_token,
1807
          user: data.user,
1808
          token_type: 'bearer',
1809
          expires_in: expiresAt - timeNow,
1810
          expires_at: expiresAt,
1811
        }
1812
        await this._saveSession(session)
2✔
1813
        await this._notifyAllSubscribers('SIGNED_IN', session)
2✔
1814
      }
1815

1816
      return { data: { user: session.user, session }, error: null }
2✔
1817
    } catch (error) {
1818
      if (isAuthError(error)) {
8✔
1819
        return { data: { session: null, user: null }, error }
8✔
1820
      }
1821

UNCOV
1822
      throw error
×
1823
    }
1824
  }
1825

1826
  /**
1827
   * Returns a new session, regardless of expiry status.
1828
   * Takes in an optional current session. If not passed in, then refreshSession() will attempt to retrieve it from getSession().
1829
   * If the current session's refresh token is invalid, an error will be thrown.
1830
   * @param currentSession The current session. If passed in, it must contain a refresh token.
1831
   */
1832
  async refreshSession(currentSession?: { refresh_token: string }): Promise<AuthResponse> {
1833
    await this.initializePromise
8✔
1834

1835
    return await this._acquireLock(-1, async () => {
8✔
1836
      return await this._refreshSession(currentSession)
8✔
1837
    })
1838
  }
1839

1840
  protected async _refreshSession(currentSession?: {
1841
    refresh_token: string
1842
  }): Promise<AuthResponse> {
1843
    try {
8✔
1844
      return await this._useSession(async (result) => {
8✔
1845
        if (!currentSession) {
8✔
1846
          const { data, error } = result
4✔
1847
          if (error) {
4!
UNCOV
1848
            throw error
×
1849
          }
1850

1851
          currentSession = data.session ?? undefined
4✔
1852
        }
1853

1854
        if (!currentSession?.refresh_token) {
8✔
1855
          throw new AuthSessionMissingError()
2✔
1856
        }
1857

1858
        const { data: session, error } = await this._callRefreshToken(currentSession.refresh_token)
6✔
1859
        if (error) {
6✔
1860
          return { data: { user: null, session: null }, error: error }
2✔
1861
        }
1862

1863
        if (!session) {
4!
UNCOV
1864
          return { data: { user: null, session: null }, error: null }
×
1865
        }
1866

1867
        return { data: { user: session.user, session }, error: null }
4✔
1868
      })
1869
    } catch (error) {
1870
      if (isAuthError(error)) {
2✔
1871
        return { data: { user: null, session: null }, error }
2✔
1872
      }
1873

UNCOV
1874
      throw error
×
1875
    }
1876
  }
1877

1878
  /**
1879
   * Gets the session data from a URL string
1880
   */
1881
  private async _getSessionFromURL(
1882
    params: { [parameter: string]: string },
1883
    callbackUrlType: string
1884
  ): Promise<
1885
    | {
1886
        data: { session: Session; redirectType: string | null }
1887
        error: null
1888
      }
1889
    | { data: { session: null; redirectType: null }; error: AuthError }
1890
  > {
1891
    try {
12✔
1892
      if (!isBrowser()) throw new AuthImplicitGrantRedirectError('No browser detected.')
12✔
1893

1894
      // If there's an error in the URL, it doesn't matter what flow it is, we just return the error.
1895
      if (params.error || params.error_description || params.error_code) {
10✔
1896
        // The error class returned implies that the redirect is from an implicit grant flow
1897
        // but it could also be from a redirect error from a PKCE flow.
1898
        throw new AuthImplicitGrantRedirectError(
2✔
1899
          params.error_description || 'Error in URL with unspecified error_description',
2!
1900
          {
1901
            error: params.error || 'unspecified_error',
2!
1902
            code: params.error_code || 'unspecified_code',
4✔
1903
          }
1904
        )
1905
      }
1906

1907
      // Checks for mismatches between the flowType initialised in the client and the URL parameters
1908
      switch (callbackUrlType) {
8!
1909
        case 'implicit':
1910
          if (this.flowType === 'pkce') {
6!
UNCOV
1911
            throw new AuthPKCEGrantCodeExchangeError('Not a valid PKCE flow url.')
×
1912
          }
1913
          break
6✔
1914
        case 'pkce':
1915
          if (this.flowType === 'implicit') {
2✔
1916
            throw new AuthImplicitGrantRedirectError('Not a valid implicit grant flow url.')
2✔
1917
          }
UNCOV
1918
          break
×
1919
        default:
1920
        // there's no mismatch so we continue
1921
      }
1922

1923
      // Since this is a redirect for PKCE, we attempt to retrieve the code from the URL for the code exchange
1924
      if (callbackUrlType === 'pkce') {
6!
1925
        this._debug('#_initialize()', 'begin', 'is PKCE flow', true)
×
1926
        if (!params.code) throw new AuthPKCEGrantCodeExchangeError('No code detected.')
×
UNCOV
1927
        const { data, error } = await this._exchangeCodeForSession(params.code)
×
1928
        if (error) throw error
×
1929

UNCOV
1930
        const url = new URL(window.location.href)
×
1931
        url.searchParams.delete('code')
×
1932

1933
        window.history.replaceState(window.history.state, '', url.toString())
×
1934

UNCOV
1935
        return { data: { session: data.session, redirectType: null }, error: null }
×
1936
      }
1937

1938
      const {
1939
        provider_token,
1940
        provider_refresh_token,
1941
        access_token,
1942
        refresh_token,
1943
        expires_in,
1944
        expires_at,
1945
        token_type,
1946
      } = params
6✔
1947

1948
      if (!access_token || !expires_in || !refresh_token || !token_type) {
6!
UNCOV
1949
        throw new AuthImplicitGrantRedirectError('No session defined in URL')
×
1950
      }
1951

1952
      const timeNow = Math.round(Date.now() / 1000)
6✔
1953
      const expiresIn = parseInt(expires_in)
6✔
1954
      let expiresAt = timeNow + expiresIn
6✔
1955

1956
      if (expires_at) {
6✔
1957
        expiresAt = parseInt(expires_at)
2✔
1958
      }
1959

1960
      const actuallyExpiresIn = expiresAt - timeNow
6✔
1961
      if (actuallyExpiresIn * 1000 <= AUTO_REFRESH_TICK_DURATION_MS) {
6✔
1962
        console.warn(
2✔
1963
          `@supabase/gotrue-js: Session as retrieved from URL expires in ${actuallyExpiresIn}s, should have been closer to ${expiresIn}s`
1964
        )
1965
      }
1966

1967
      const issuedAt = expiresAt - expiresIn
6✔
1968
      if (timeNow - issuedAt >= 120) {
6✔
1969
        console.warn(
2✔
1970
          '@supabase/gotrue-js: Session as retrieved from URL was issued over 120s ago, URL could be stale',
1971
          issuedAt,
1972
          expiresAt,
1973
          timeNow
1974
        )
1975
      } else if (timeNow - issuedAt < 0) {
4!
UNCOV
1976
        console.warn(
×
1977
          '@supabase/gotrue-js: Session as retrieved from URL was issued in the future? Check the device clock for skew',
1978
          issuedAt,
1979
          expiresAt,
1980
          timeNow
1981
        )
1982
      }
1983

1984
      const { data, error } = await this._getUser(access_token)
6✔
1985
      if (error) throw error
2!
1986

1987
      const session: Session = {
2✔
1988
        provider_token,
1989
        provider_refresh_token,
1990
        access_token,
1991
        expires_in: expiresIn,
1992
        expires_at: expiresAt,
1993
        refresh_token,
1994
        token_type: token_type as 'bearer',
1995
        user: data.user,
1996
      }
1997

1998
      // Remove tokens from URL
1999
      window.location.hash = ''
2✔
2000
      this._debug('#_getSessionFromURL()', 'clearing window.location.hash')
2✔
2001

2002
      return { data: { session, redirectType: params.type }, error: null }
2✔
2003
    } catch (error) {
2004
      if (isAuthError(error)) {
10✔
2005
        return { data: { session: null, redirectType: null }, error }
6✔
2006
      }
2007

2008
      throw error
4✔
2009
    }
2010
  }
2011

2012
  /**
2013
   * 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)
2014
   */
2015
  private _isImplicitGrantCallback(params: { [parameter: string]: string }): boolean {
2016
    return Boolean(params.access_token || params.error_description)
72✔
2017
  }
2018

2019
  /**
2020
   * Checks if the current URL and backing storage contain parameters given by a PKCE flow
2021
   */
2022
  private async _isPKCECallback(params: { [parameter: string]: string }): Promise<boolean> {
2023
    const currentStorageContent = await getItemAsync(
64✔
2024
      this.storage,
2025
      `${this.storageKey}-code-verifier`
2026
    )
2027

2028
    return !!(params.code && currentStorageContent)
64✔
2029
  }
2030

2031
  /**
2032
   * 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.
2033
   *
2034
   * 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)`.
2035
   * 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.
2036
   *
2037
   * If using `others` scope, no `SIGNED_OUT` event is fired!
2038
   */
2039
  async signOut(options: SignOut = { scope: 'global' }): Promise<{ error: AuthError | null }> {
230✔
2040
    await this.initializePromise
232✔
2041

2042
    return await this._acquireLock(-1, async () => {
232✔
2043
      return await this._signOut(options)
232✔
2044
    })
2045
  }
2046

2047
  protected async _signOut(
2048
    { scope }: SignOut = { scope: 'global' }
×
2049
  ): Promise<{ error: AuthError | null }> {
2050
    return await this._useSession(async (result) => {
232✔
2051
      const { data, error: sessionError } = result
230✔
2052
      if (sessionError) {
230!
UNCOV
2053
        return { error: sessionError }
×
2054
      }
2055
      const accessToken = data.session?.access_token
230✔
2056
      if (accessToken) {
230✔
2057
        const { error } = await this.admin.signOut(accessToken, scope)
52✔
2058
        if (error) {
50✔
2059
          // ignore 404s since user might not exist anymore
2060
          // ignore 401s since an invalid or expired JWT should sign out the current session
2061
          if (
2!
2062
            !(
2063
              isAuthApiError(error) &&
8✔
2064
              (error.status === 404 || error.status === 401 || error.status === 403)
2065
            )
2066
          ) {
UNCOV
2067
            return { error }
×
2068
          }
2069
        }
2070
      }
2071
      if (scope !== 'others') {
228✔
2072
        await this._removeSession()
228✔
2073
        await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
228✔
2074
      }
2075
      return { error: null }
228✔
2076
    })
2077
  }
2078

2079
  /**
2080
   * Receive a notification every time an auth event happens.
2081
   * @param callback A callback function to be invoked when an auth event happens.
2082
   */
2083
  onAuthStateChange(
2084
    callback: (event: AuthChangeEvent, session: Session | null) => void | Promise<void>
2085
  ): {
2086
    data: { subscription: Subscription }
2087
  } {
2088
    const id: string = uuid()
10✔
2089
    const subscription: Subscription = {
10✔
2090
      id,
2091
      callback,
2092
      unsubscribe: () => {
2093
        this._debug('#unsubscribe()', 'state change callback with id removed', id)
10✔
2094

2095
        this.stateChangeEmitters.delete(id)
10✔
2096
      },
2097
    }
2098

2099
    this._debug('#onAuthStateChange()', 'registered callback with id', id)
10✔
2100

2101
    this.stateChangeEmitters.set(id, subscription)
10✔
2102
    ;(async () => {
10✔
2103
      await this.initializePromise
10✔
2104

2105
      await this._acquireLock(-1, async () => {
10✔
2106
        this._emitInitialSession(id)
10✔
2107
      })
2108
    })()
2109

2110
    return { data: { subscription } }
10✔
2111
  }
2112

2113
  private async _emitInitialSession(id: string): Promise<void> {
2114
    return await this._useSession(async (result) => {
10✔
2115
      try {
10✔
2116
        const {
2117
          data: { session },
2118
          error,
2119
        } = result
10✔
2120
        if (error) throw error
10!
2121

2122
        await this.stateChangeEmitters.get(id)?.callback('INITIAL_SESSION', session)
10✔
2123
        this._debug('INITIAL_SESSION', 'callback id', id, 'session', session)
10✔
2124
      } catch (err) {
2125
        await this.stateChangeEmitters.get(id)?.callback('INITIAL_SESSION', null)
×
UNCOV
2126
        this._debug('INITIAL_SESSION', 'callback id', id, 'error', err)
×
UNCOV
2127
        console.error(err)
×
2128
      }
2129
    })
2130
  }
2131

2132
  /**
2133
   * Sends a password reset request to an email address. This method supports the PKCE flow.
2134
   *
2135
   * @param email The email address of the user.
2136
   * @param options.redirectTo The URL to send the user to after they click the password reset link.
2137
   * @param options.captchaToken Verification token received when the user completes the captcha on the site.
2138
   */
2139
  async resetPasswordForEmail(
2140
    email: string,
2141
    options: {
×
2142
      redirectTo?: string
2143
      captchaToken?: string
2144
    } = {}
2145
  ): Promise<
2146
    | {
2147
        data: {}
2148
        error: null
2149
      }
2150
    | { data: null; error: AuthError }
2151
  > {
2152
    let codeChallenge: string | null = null
4✔
2153
    let codeChallengeMethod: string | null = null
4✔
2154

2155
    if (this.flowType === 'pkce') {
4!
UNCOV
2156
      ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
×
2157
        this.storage,
2158
        this.storageKey,
2159
        true // isPasswordRecovery
2160
      )
2161
    }
2162
    try {
4✔
2163
      return await _request(this.fetch, 'POST', `${this.url}/recover`, {
4✔
2164
        body: {
2165
          email,
2166
          code_challenge: codeChallenge,
2167
          code_challenge_method: codeChallengeMethod,
2168
          gotrue_meta_security: { captcha_token: options.captchaToken },
2169
        },
2170
        headers: this.headers,
2171
        redirectTo: options.redirectTo,
2172
      })
2173
    } catch (error) {
UNCOV
2174
      if (isAuthError(error)) {
×
UNCOV
2175
        return { data: null, error }
×
2176
      }
2177

UNCOV
2178
      throw error
×
2179
    }
2180
  }
2181

2182
  /**
2183
   * Gets all the identities linked to a user.
2184
   */
2185
  async getUserIdentities(): Promise<
2186
    | {
2187
        data: {
2188
          identities: UserIdentity[]
2189
        }
2190
        error: null
2191
      }
2192
    | { data: null; error: AuthError }
2193
  > {
2194
    try {
6✔
2195
      const { data, error } = await this.getUser()
6✔
2196
      if (error) throw error
6✔
2197
      return { data: { identities: data.user.identities ?? [] }, error: null }
4!
2198
    } catch (error) {
2199
      if (isAuthError(error)) {
2✔
2200
        return { data: null, error }
2✔
2201
      }
UNCOV
2202
      throw error
×
2203
    }
2204
  }
2205
  /**
2206
   * Links an oauth identity to an existing user.
2207
   * This method supports the PKCE flow.
2208
   */
2209
  async linkIdentity(credentials: SignInWithOAuthCredentials): Promise<OAuthResponse> {
2210
    try {
4✔
2211
      const { data, error } = await this._useSession(async (result) => {
4✔
2212
        const { data, error } = result
4✔
2213
        if (error) throw error
4!
2214
        const url: string = await this._getUrlForProvider(
4✔
2215
          `${this.url}/user/identities/authorize`,
2216
          credentials.provider,
2217
          {
2218
            redirectTo: credentials.options?.redirectTo,
12!
2219
            scopes: credentials.options?.scopes,
12!
2220
            queryParams: credentials.options?.queryParams,
12!
2221
            skipBrowserRedirect: true,
2222
          }
2223
        )
2224
        return await _request(this.fetch, 'GET', url, {
4✔
2225
          headers: this.headers,
2226
          jwt: data.session?.access_token ?? undefined,
22✔
2227
        })
2228
      })
2229
      if (error) throw error
2!
2230
      if (isBrowser() && !credentials.options?.skipBrowserRedirect) {
2!
2231
        window.location.assign(data?.url)
2!
2232
      }
2233
      return { data: { provider: credentials.provider, url: data?.url }, error: null }
2!
2234
    } catch (error) {
2235
      if (isAuthError(error)) {
2✔
2236
        return { data: { provider: credentials.provider, url: null }, error }
2✔
2237
      }
UNCOV
2238
      throw error
×
2239
    }
2240
  }
2241

2242
  /**
2243
   * 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.
2244
   */
2245
  async unlinkIdentity(identity: UserIdentity): Promise<
2246
    | {
2247
        data: {}
2248
        error: null
2249
      }
2250
    | { data: null; error: AuthError }
2251
  > {
2252
    try {
2✔
2253
      return await this._useSession(async (result) => {
2✔
2254
        const { data, error } = result
2✔
2255
        if (error) {
2!
UNCOV
2256
          throw error
×
2257
        }
2258
        return await _request(
2✔
2259
          this.fetch,
2260
          'DELETE',
2261
          `${this.url}/user/identities/${identity.identity_id}`,
2262
          {
2263
            headers: this.headers,
2264
            jwt: data.session?.access_token ?? undefined,
12!
2265
          }
2266
        )
2267
      })
2268
    } catch (error) {
2269
      if (isAuthError(error)) {
2✔
2270
        return { data: null, error }
2✔
2271
      }
UNCOV
2272
      throw error
×
2273
    }
2274
  }
2275

2276
  /**
2277
   * Generates a new JWT.
2278
   * @param refreshToken A valid refresh token that was returned on login.
2279
   */
2280
  private async _refreshAccessToken(refreshToken: string): Promise<AuthResponse> {
2281
    const debugName = `#_refreshAccessToken(${refreshToken.substring(0, 5)}...)`
28✔
2282
    this._debug(debugName, 'begin')
28✔
2283

2284
    try {
28✔
2285
      const startedAt = Date.now()
28✔
2286

2287
      // will attempt to refresh the token with exponential backoff
2288
      return await retryable(
28✔
2289
        async (attempt) => {
2290
          if (attempt > 0) {
28!
UNCOV
2291
            await sleep(200 * Math.pow(2, attempt - 1)) // 200, 400, 800, ...
×
2292
          }
2293

2294
          this._debug(debugName, 'refreshing attempt', attempt)
28✔
2295

2296
          return await _request(this.fetch, 'POST', `${this.url}/token?grant_type=refresh_token`, {
28✔
2297
            body: { refresh_token: refreshToken },
2298
            headers: this.headers,
2299
            xform: _sessionResponse,
2300
          })
2301
        },
2302
        (attempt, error) => {
2303
          const nextBackOffInterval = 200 * Math.pow(2, attempt)
28✔
2304
          return (
28✔
2305
            error &&
38!
2306
            isAuthRetryableFetchError(error) &&
2307
            // retryable only if the request can be sent before the backoff overflows the tick duration
2308
            Date.now() + nextBackOffInterval - startedAt < AUTO_REFRESH_TICK_DURATION_MS
2309
          )
2310
        }
2311
      )
2312
    } catch (error) {
2313
      this._debug(debugName, 'error', error)
10✔
2314

2315
      if (isAuthError(error)) {
10✔
2316
        return { data: { session: null, user: null }, error }
10✔
2317
      }
UNCOV
2318
      throw error
×
2319
    } finally {
2320
      this._debug(debugName, 'end')
28✔
2321
    }
2322
  }
2323

2324
  private _isValidSession(maybeSession: unknown): maybeSession is Session {
2325
    const isValidSession =
2326
      typeof maybeSession === 'object' &&
253✔
2327
      maybeSession !== null &&
2328
      'access_token' in maybeSession &&
2329
      'refresh_token' in maybeSession &&
2330
      'expires_at' in maybeSession
2331

2332
    return isValidSession
253✔
2333
  }
2334

2335
  private async _handleProviderSignIn(
2336
    provider: Provider,
2337
    options: {
2338
      redirectTo?: string
2339
      scopes?: string
2340
      queryParams?: { [key: string]: string }
2341
      skipBrowserRedirect?: boolean
2342
    }
2343
  ) {
2344
    const url: string = await this._getUrlForProvider(`${this.url}/authorize`, provider, {
20✔
2345
      redirectTo: options.redirectTo,
2346
      scopes: options.scopes,
2347
      queryParams: options.queryParams,
2348
    })
2349

2350
    this._debug('#_handleProviderSignIn()', 'provider', provider, 'options', options, 'url', url)
20✔
2351

2352
    // try to open on the browser
2353
    if (isBrowser() && !options.skipBrowserRedirect) {
20✔
2354
      window.location.assign(url)
6✔
2355
    }
2356

2357
    return { data: { provider, url }, error: null }
20✔
2358
  }
2359

2360
  /**
2361
   * Recovers the session from LocalStorage and refreshes the token
2362
   * Note: this method is async to accommodate for AsyncStorage e.g. in React native.
2363
   */
2364
  private async _recoverAndRefresh() {
2365
    const debugName = '#_recoverAndRefresh()'
66✔
2366
    this._debug(debugName, 'begin')
66✔
2367

2368
    try {
66✔
2369
      const currentSession = (await getItemAsync(this.storage, this.storageKey)) as Session | null
66✔
2370

2371
      if (currentSession && this.userStorage) {
66✔
2372
        let maybeUser: { user: User | null } | null = (await getItemAsync(
6✔
2373
          this.userStorage,
2374
          this.storageKey + '-user'
2375
        )) as any
2376

2377
        if (!this.storage.isServer && Object.is(this.storage, this.userStorage) && !maybeUser) {
6!
2378
          // storage and userStorage are the same storage medium, for example
2379
          // window.localStorage if userStorage does not have the user from
2380
          // storage stored, store it first thereby migrating the user object
2381
          // from storage -> userStorage
2382

UNCOV
2383
          maybeUser = { user: currentSession.user }
×
UNCOV
2384
          await setItemAsync(this.userStorage, this.storageKey + '-user', maybeUser)
×
2385
        }
2386

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

2392
        if (!currentSession.user) {
2✔
2393
          // test if userStorage was previously enabled and the storage medium was the same, to move the user back under the same key
2394
          const separateUser: { user: User | null } | null = (await getItemAsync(
2✔
2395
            this.storage,
2396
            this.storageKey + '-user'
2397
          )) as any
2398

2399
          if (separateUser && separateUser?.user) {
2!
2400
            currentSession.user = separateUser.user
×
2401

UNCOV
2402
            await removeItemAsync(this.storage, this.storageKey + '-user')
×
UNCOV
2403
            await setItemAsync(this.storage, this.storageKey, currentSession)
×
2404
          } else {
2405
            currentSession.user = userNotAvailableProxy()
2✔
2406
          }
2407
        }
2408
      }
2409

2410
      this._debug(debugName, 'session from storage', currentSession)
64✔
2411

2412
      if (!this._isValidSession(currentSession)) {
64✔
2413
        this._debug(debugName, 'session is not valid')
48✔
2414
        if (currentSession !== null) {
48✔
2415
          await this._removeSession()
6✔
2416
        }
2417

2418
        return
48✔
2419
      }
2420

2421
      const expiresWithMargin =
2422
        (currentSession.expires_at ?? Infinity) * 1000 - Date.now() < EXPIRY_MARGIN_MS
16✔
2423

2424
      this._debug(
16✔
2425
        debugName,
2426
        `session has${expiresWithMargin ? '' : ' not'} expired with margin of ${EXPIRY_MARGIN_MS}s`
16✔
2427
      )
2428

2429
      if (expiresWithMargin) {
16✔
2430
        if (this.autoRefreshToken && currentSession.refresh_token) {
4✔
2431
          const { error } = await this._callRefreshToken(currentSession.refresh_token)
4✔
2432

2433
          if (error) {
4✔
2434
            console.error(error)
2✔
2435

2436
            if (!isAuthRetryableFetchError(error)) {
2✔
2437
              this._debug(
2✔
2438
                debugName,
2439
                'refresh failed with a non-retryable error, removing the session',
2440
                error
2441
              )
2442
              await this._removeSession()
2✔
2443
            }
2444
          }
2445
        }
2446
      } else if (
12✔
2447
        currentSession.user &&
24✔
2448
        (currentSession.user as any).__isUserNotAvailableProxy === true
2449
      ) {
2450
        // If we have a proxy user, try to get the real user data
2451
        try {
6✔
2452
          const { data, error: userError } = await this._getUser(currentSession.access_token)
6✔
2453

2454
          if (!userError && data?.user) {
2!
2455
            currentSession.user = data.user
2✔
2456
            await this._saveSession(currentSession)
2✔
2457
            await this._notifyAllSubscribers('SIGNED_IN', currentSession)
2✔
2458
          } else {
UNCOV
2459
            this._debug(debugName, 'could not get user data, skipping SIGNED_IN notification')
×
2460
          }
2461
        } catch (getUserError) {
2462
          console.error('Error getting user data:', getUserError)
4✔
2463
          this._debug(
4✔
2464
            debugName,
2465
            'error getting user data, skipping SIGNED_IN notification',
2466
            getUserError
2467
          )
2468
        }
2469
      } else {
2470
        // no need to persist currentSession again, as we just loaded it from
2471
        // local storage; persisting it again may overwrite a value saved by
2472
        // another client with access to the same local storage
2473
        await this._notifyAllSubscribers('SIGNED_IN', currentSession)
6✔
2474
      }
2475
    } catch (err) {
2476
      this._debug(debugName, 'error', err)
2✔
2477

2478
      console.error(err)
2✔
2479
      return
2✔
2480
    } finally {
2481
      this._debug(debugName, 'end')
66✔
2482
    }
2483
  }
2484

2485
  private async _callRefreshToken(refreshToken: string): Promise<CallRefreshTokenResult> {
2486
    if (!refreshToken) {
38!
UNCOV
2487
      throw new AuthSessionMissingError()
×
2488
    }
2489

2490
    // refreshing is already in progress
2491
    if (this.refreshingDeferred) {
38✔
2492
      return this.refreshingDeferred.promise
6✔
2493
    }
2494

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

2497
    this._debug(debugName, 'begin')
32✔
2498

2499
    try {
32✔
2500
      this.refreshingDeferred = new Deferred<CallRefreshTokenResult>()
32✔
2501

2502
      const { data, error } = await this._refreshAccessToken(refreshToken)
32✔
2503
      if (error) throw error
30✔
2504
      if (!data.session) throw new AuthSessionMissingError()
18✔
2505

2506
      await this._saveSession(data.session)
16✔
2507
      await this._notifyAllSubscribers('TOKEN_REFRESHED', data.session)
16✔
2508

2509
      const result = { data: data.session, error: null }
16✔
2510

2511
      this.refreshingDeferred.resolve(result)
16✔
2512

2513
      return result
16✔
2514
    } catch (error) {
2515
      this._debug(debugName, 'error', error)
16✔
2516

2517
      if (isAuthError(error)) {
16✔
2518
        const result = { data: null, error }
14✔
2519

2520
        if (!isAuthRetryableFetchError(error)) {
14✔
2521
          await this._removeSession()
14✔
2522
        }
2523

2524
        this.refreshingDeferred?.resolve(result)
14!
2525

2526
        return result
14✔
2527
      }
2528

2529
      this.refreshingDeferred?.reject(error)
2!
2530
      throw error
2✔
2531
    } finally {
2532
      this.refreshingDeferred = null
32✔
2533
      this._debug(debugName, 'end')
32✔
2534
    }
2535
  }
2536

2537
  private async _notifyAllSubscribers(
2538
    event: AuthChangeEvent,
2539
    session: Session | null,
2540
    broadcast = true
443✔
2541
  ) {
2542
    const debugName = `#_notifyAllSubscribers(${event})`
445✔
2543
    this._debug(debugName, 'begin', session, `broadcast = ${broadcast}`)
445✔
2544

2545
    try {
445✔
2546
      if (this.broadcastChannel && broadcast) {
445!
UNCOV
2547
        this.broadcastChannel.postMessage({ event, session })
×
2548
      }
2549

2550
      const errors: any[] = []
445✔
2551
      const promises = Array.from(this.stateChangeEmitters.values()).map(async (x) => {
445✔
2552
        try {
10✔
2553
          await x.callback(event, session)
10✔
2554
        } catch (e: any) {
UNCOV
2555
          errors.push(e)
×
2556
        }
2557
      })
2558

2559
      await Promise.all(promises)
445✔
2560

2561
      if (errors.length > 0) {
445!
UNCOV
2562
        for (let i = 0; i < errors.length; i += 1) {
×
UNCOV
2563
          console.error(errors[i])
×
2564
        }
2565

UNCOV
2566
        throw errors[0]
×
2567
      }
2568
    } finally {
2569
      this._debug(debugName, 'end')
445✔
2570
    }
2571
  }
2572

2573
  /**
2574
   * set currentSession and currentUser
2575
   * process to _startAutoRefreshToken if possible
2576
   */
2577
  private async _saveSession(session: Session) {
2578
    this._debug('#_saveSession()', session)
177✔
2579
    // _saveSession is always called whenever a new session has been acquired
2580
    // so we can safely suppress the warning returned by future getSession calls
2581
    this.suppressGetSessionWarning = true
177✔
2582

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

2586
    const userIsProxy =
2587
      sessionToProcess.user && (sessionToProcess.user as any).__isUserNotAvailableProxy === true
177✔
2588
    if (this.userStorage) {
177!
2589
      if (!userIsProxy && sessionToProcess.user) {
×
2590
        // If it's a real user object, save it to userStorage.
UNCOV
2591
        await setItemAsync(this.userStorage, this.storageKey + '-user', {
×
2592
          user: sessionToProcess.user,
2593
        })
UNCOV
2594
      } else if (userIsProxy) {
×
2595
        // If it's the proxy, it means user was not found in userStorage.
2596
        // We should ensure no stale user data for this key exists in userStorage if we were to save null,
2597
        // or simply not save the proxy. For now, we don't save the proxy here.
2598
        // If there's a need to clear userStorage if user becomes proxy, that logic would go here.
2599
      }
2600

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

UNCOV
2606
      const clonedMainSessionData = deepClone(mainSessionData)
×
UNCOV
2607
      await setItemAsync(this.storage, this.storageKey, clonedMainSessionData)
×
2608
    } else {
2609
      // No userStorage is configured.
2610
      // In this case, session.user should ideally not be a proxy.
2611
      // If it were, structuredClone would fail. This implies an issue elsewhere if user is a proxy here
2612
      const clonedSession = deepClone(sessionToProcess) // sessionToProcess still has its original user property
177✔
2613
      await setItemAsync(this.storage, this.storageKey, clonedSession)
177✔
2614
    }
2615
  }
2616

2617
  private async _removeSession() {
2618
    this._debug('#_removeSession()')
268✔
2619

2620
    await removeItemAsync(this.storage, this.storageKey)
268✔
2621
    await removeItemAsync(this.storage, this.storageKey + '-code-verifier')
266✔
2622
    await removeItemAsync(this.storage, this.storageKey + '-user')
266✔
2623

2624
    if (this.userStorage) {
266✔
2625
      await removeItemAsync(this.userStorage, this.storageKey + '-user')
2✔
2626
    }
2627

2628
    await this._notifyAllSubscribers('SIGNED_OUT', null)
266✔
2629
  }
2630

2631
  /**
2632
   * Removes any registered visibilitychange callback.
2633
   *
2634
   * {@see #startAutoRefresh}
2635
   * {@see #stopAutoRefresh}
2636
   */
2637
  private _removeVisibilityChangedCallback() {
2638
    this._debug('#_removeVisibilityChangedCallback()')
34✔
2639

2640
    const callback = this.visibilityChangedCallback
34✔
2641
    this.visibilityChangedCallback = null
34✔
2642

2643
    try {
34✔
2644
      if (callback && isBrowser() && window?.removeEventListener) {
34!
2645
        window.removeEventListener('visibilitychange', callback)
2✔
2646
      }
2647
    } catch (e) {
UNCOV
2648
      console.error('removing visibilitychange callback failed', e)
×
2649
    }
2650
  }
2651

2652
  /**
2653
   * This is the private implementation of {@link #startAutoRefresh}. Use this
2654
   * within the library.
2655
   */
2656
  private async _startAutoRefresh() {
2657
    await this._stopAutoRefresh()
32✔
2658

2659
    this._debug('#_startAutoRefresh()')
32✔
2660

2661
    const ticker = setInterval(() => this._autoRefreshTokenTick(), AUTO_REFRESH_TICK_DURATION_MS)
32✔
2662
    this.autoRefreshTicker = ticker
32✔
2663

2664
    if (ticker && typeof ticker === 'object' && typeof ticker.unref === 'function') {
32✔
2665
      // ticker is a NodeJS Timeout object that has an `unref` method
2666
      // https://nodejs.org/api/timers.html#timeoutunref
2667
      // When auto refresh is used in NodeJS (like for testing) the
2668
      // `setInterval` is preventing the process from being marked as
2669
      // finished and tests run endlessly. This can be prevented by calling
2670
      // `unref()` on the returned object.
2671
      ticker.unref()
24✔
2672
      // @ts-expect-error TS has no context of Deno
2673
    } else if (typeof Deno !== 'undefined' && typeof Deno.unrefTimer === 'function') {
8!
2674
      // similar like for NodeJS, but with the Deno API
2675
      // https://deno.land/api@latest?unstable&s=Deno.unrefTimer
2676
      // @ts-expect-error TS has no context of Deno
UNCOV
2677
      Deno.unrefTimer(ticker)
×
2678
    }
2679

2680
    // run the tick immediately, but in the next pass of the event loop so that
2681
    // #_initialize can be allowed to complete without recursively waiting on
2682
    // itself
2683
    setTimeout(async () => {
32✔
2684
      await this.initializePromise
32✔
2685
      await this._autoRefreshTokenTick()
32✔
2686
    }, 0)
2687
  }
2688

2689
  /**
2690
   * This is the private implementation of {@link #stopAutoRefresh}. Use this
2691
   * within the library.
2692
   */
2693
  private async _stopAutoRefresh() {
2694
    this._debug('#_stopAutoRefresh()')
40✔
2695

2696
    const ticker = this.autoRefreshTicker
40✔
2697
    this.autoRefreshTicker = null
40✔
2698

2699
    if (ticker) {
40✔
2700
      clearInterval(ticker)
10✔
2701
    }
2702
  }
2703

2704
  /**
2705
   * Starts an auto-refresh process in the background. The session is checked
2706
   * every few seconds. Close to the time of expiration a process is started to
2707
   * refresh the session. If refreshing fails it will be retried for as long as
2708
   * necessary.
2709
   *
2710
   * If you set the {@link GoTrueClientOptions#autoRefreshToken} you don't need
2711
   * to call this function, it will be called for you.
2712
   *
2713
   * On browsers the refresh process works only when the tab/window is in the
2714
   * foreground to conserve resources as well as prevent race conditions and
2715
   * flooding auth with requests. If you call this method any managed
2716
   * visibility change callback will be removed and you must manage visibility
2717
   * changes on your own.
2718
   *
2719
   * On non-browser platforms the refresh process works *continuously* in the
2720
   * background, which may not be desirable. You should hook into your
2721
   * platform's foreground indication mechanism and call these methods
2722
   * appropriately to conserve resources.
2723
   *
2724
   * {@see #stopAutoRefresh}
2725
   */
2726
  async startAutoRefresh() {
2727
    this._removeVisibilityChangedCallback()
26✔
2728
    await this._startAutoRefresh()
26✔
2729
  }
2730

2731
  /**
2732
   * Stops an active auto refresh process running in the background (if any).
2733
   *
2734
   * If you call this method any managed visibility change callback will be
2735
   * removed and you must manage visibility changes on your own.
2736
   *
2737
   * See {@link #startAutoRefresh} for more details.
2738
   */
2739
  async stopAutoRefresh() {
2740
    this._removeVisibilityChangedCallback()
8✔
2741
    await this._stopAutoRefresh()
8✔
2742
  }
2743

2744
  /**
2745
   * Runs the auto refresh token tick.
2746
   */
2747
  private async _autoRefreshTokenTick() {
2748
    this._debug('#_autoRefreshTokenTick()', 'begin')
32✔
2749

2750
    try {
32✔
2751
      await this._acquireLock(0, async () => {
32✔
2752
        try {
32✔
2753
          const now = Date.now()
32✔
2754

2755
          try {
32✔
2756
            return await this._useSession(async (result) => {
32✔
2757
              const {
2758
                data: { session },
2759
              } = result
32✔
2760

2761
              if (!session || !session.refresh_token || !session.expires_at) {
32✔
2762
                this._debug('#_autoRefreshTokenTick()', 'no session')
20✔
2763
                return
20✔
2764
              }
2765

2766
              // session will expire in this many ticks (or has already expired if <= 0)
2767
              const expiresInTicks = Math.floor(
12✔
2768
                (session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS
2769
              )
2770

2771
              this._debug(
12✔
2772
                '#_autoRefreshTokenTick()',
2773
                `access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks`
2774
              )
2775

2776
              if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) {
12!
UNCOV
2777
                await this._callRefreshToken(session.refresh_token)
×
2778
              }
2779
            })
2780
          } catch (e: any) {
UNCOV
2781
            console.error(
×
2782
              'Auto refresh tick failed with error. This is likely a transient error.',
2783
              e
2784
            )
2785
          }
2786
        } finally {
2787
          this._debug('#_autoRefreshTokenTick()', 'end')
32✔
2788
        }
2789
      })
2790
    } catch (e: any) {
UNCOV
2791
      if (e.isAcquireTimeout || e instanceof LockAcquireTimeoutError) {
×
2792
        this._debug('auto refresh token tick lock not available')
×
2793
      } else {
UNCOV
2794
        throw e
×
2795
      }
2796
    }
2797
  }
2798

2799
  /**
2800
   * Registers callbacks on the browser / platform, which in-turn run
2801
   * algorithms when the browser window/tab are in foreground. On non-browser
2802
   * platforms it assumes always foreground.
2803
   */
2804
  private async _handleVisibilityChange() {
2805
    this._debug('#_handleVisibilityChange()')
176✔
2806

2807
    if (!isBrowser() || !window?.addEventListener) {
176!
2808
      if (this.autoRefreshToken) {
106✔
2809
        // in non-browser environments the refresh token ticker runs always
2810
        this.startAutoRefresh()
20✔
2811
      }
2812

2813
      return false
106✔
2814
    }
2815

2816
    try {
70✔
2817
      this.visibilityChangedCallback = async () => await this._onVisibilityChanged(false)
70✔
2818

2819
      window?.addEventListener('visibilitychange', this.visibilityChangedCallback)
70!
2820

2821
      // now immediately call the visbility changed callback to setup with the
2822
      // current visbility state
2823
      await this._onVisibilityChanged(true) // initial call
68✔
2824
    } catch (error) {
2825
      console.error('_handleVisibilityChange', error)
2✔
2826
    }
2827
  }
2828

2829
  /**
2830
   * Callback registered with `window.addEventListener('visibilitychange')`.
2831
   */
2832
  private async _onVisibilityChanged(calledFromInitialize: boolean) {
2833
    const methodName = `#_onVisibilityChanged(${calledFromInitialize})`
68✔
2834
    this._debug(methodName, 'visibilityState', document.visibilityState)
68✔
2835

2836
    if (document.visibilityState === 'visible') {
68!
2837
      if (this.autoRefreshToken) {
68✔
2838
        // in browser environments the refresh token ticker runs only on focused tabs
2839
        // which prevents race conditions
2840
        this._startAutoRefresh()
6✔
2841
      }
2842

2843
      if (!calledFromInitialize) {
68!
2844
        // called when the visibility has changed, i.e. the browser
2845
        // transitioned from hidden -> visible so we need to see if the session
2846
        // should be recovered immediately... but to do that we need to acquire
2847
        // the lock first asynchronously
2848
        await this.initializePromise
×
2849

2850
        await this._acquireLock(-1, async () => {
×
UNCOV
2851
          if (document.visibilityState !== 'visible') {
×
UNCOV
2852
            this._debug(
×
2853
              methodName,
2854
              'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting'
2855
            )
2856

2857
            // visibility has changed while waiting for the lock, abort
UNCOV
2858
            return
×
2859
          }
2860

2861
          // recover the session
UNCOV
2862
          await this._recoverAndRefresh()
×
2863
        })
2864
      }
2865
    } else if (document.visibilityState === 'hidden') {
×
UNCOV
2866
      if (this.autoRefreshToken) {
×
UNCOV
2867
        this._stopAutoRefresh()
×
2868
      }
2869
    }
2870
  }
2871

2872
  /**
2873
   * Generates the relevant login URL for a third-party provider.
2874
   * @param options.redirectTo A URL or mobile address to send the user to after they are confirmed.
2875
   * @param options.scopes A space-separated list of scopes granted to the OAuth application.
2876
   * @param options.queryParams An object of key-value pairs containing query parameters granted to the OAuth application.
2877
   */
2878
  private async _getUrlForProvider(
2879
    url: string,
2880
    provider: Provider,
2881
    options: {
2882
      redirectTo?: string
2883
      scopes?: string
2884
      queryParams?: { [key: string]: string }
2885
      skipBrowserRedirect?: boolean
2886
    }
2887
  ) {
2888
    const urlParams: string[] = [`provider=${encodeURIComponent(provider)}`]
28✔
2889
    if (options?.redirectTo) {
28!
2890
      urlParams.push(`redirect_to=${encodeURIComponent(options.redirectTo)}`)
14✔
2891
    }
2892
    if (options?.scopes) {
28!
2893
      urlParams.push(`scopes=${encodeURIComponent(options.scopes)}`)
6✔
2894
    }
2895
    if (this.flowType === 'pkce') {
28✔
2896
      const [codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
4✔
2897
        this.storage,
2898
        this.storageKey
2899
      )
2900

2901
      const flowParams = new URLSearchParams({
4✔
2902
        code_challenge: `${encodeURIComponent(codeChallenge)}`,
2903
        code_challenge_method: `${encodeURIComponent(codeChallengeMethod)}`,
2904
      })
2905
      urlParams.push(flowParams.toString())
4✔
2906
    }
2907
    if (options?.queryParams) {
28!
2908
      const query = new URLSearchParams(options.queryParams)
4✔
2909
      urlParams.push(query.toString())
4✔
2910
    }
2911
    if (options?.skipBrowserRedirect) {
28!
2912
      urlParams.push(`skip_http_redirect=${options.skipBrowserRedirect}`)
4✔
2913
    }
2914

2915
    return `${url}?${urlParams.join('&')}`
28✔
2916
  }
2917

2918
  private async _unenroll(params: MFAUnenrollParams): Promise<AuthMFAUnenrollResponse> {
2919
    try {
4✔
2920
      return await this._useSession(async (result) => {
4✔
2921
        const { data: sessionData, error: sessionError } = result
4✔
2922
        if (sessionError) {
4!
UNCOV
2923
          return { data: null, error: sessionError }
×
2924
        }
2925

2926
        return await _request(this.fetch, 'DELETE', `${this.url}/factors/${params.factorId}`, {
4✔
2927
          headers: this.headers,
2928
          jwt: sessionData?.session?.access_token,
22!
2929
        })
2930
      })
2931
    } catch (error) {
2932
      if (isAuthError(error)) {
2✔
2933
        return { data: null, error }
2✔
2934
      }
UNCOV
2935
      throw error
×
2936
    }
2937
  }
2938

2939
  /**
2940
   * {@see GoTrueMFAApi#enroll}
2941
   */
2942
  private async _enroll(params: MFAEnrollTOTPParams): Promise<AuthMFAEnrollTOTPResponse>
2943
  private async _enroll(params: MFAEnrollPhoneParams): Promise<AuthMFAEnrollPhoneResponse>
2944
  private async _enroll(params: MFAEnrollWebauthnParams): Promise<AuthMFAEnrollWebauthnResponse>
2945
  private async _enroll(params: MFAEnrollParams): Promise<AuthMFAEnrollResponse> {
2946
    try {
20✔
2947
      return await this._useSession(async (result) => {
20✔
2948
        const { data: sessionData, error: sessionError } = result
20✔
2949
        if (sessionError) {
20!
UNCOV
2950
          return { data: null, error: sessionError }
×
2951
        }
2952

2953
        const body = {
20✔
2954
          friendly_name: params.friendlyName,
2955
          factor_type: params.factorType,
2956
          ...(params.factorType === 'phone'
20✔
2957
            ? { phone: params.phone }
2958
            : params.factorType === 'totp'
18!
2959
            ? { issuer: params.issuer }
2960
            : {}),
2961
        }
2962

2963
        const { data, error } = (await _request(this.fetch, 'POST', `${this.url}/factors`, {
20✔
2964
          body,
2965
          headers: this.headers,
2966
          jwt: sessionData?.session?.access_token,
116!
2967
        })) as AuthMFAEnrollResponse
2968
        if (error) {
16!
UNCOV
2969
          return { data: null, error }
×
2970
        }
2971

2972
        if (params.factorType === 'totp' && data.type === 'totp' && data?.totp?.qr_code) {
16!
2973
          data.totp.qr_code = `data:image/svg+xml;utf-8,${data.totp.qr_code}`
14✔
2974
        }
2975

2976
        return { data, error: null }
16✔
2977
      })
2978
    } catch (error) {
2979
      if (isAuthError(error)) {
4✔
2980
        return { data: null, error }
4✔
2981
      }
UNCOV
2982
      throw error
×
2983
    }
2984
  }
2985

2986
  /**
2987
   * {@see GoTrueMFAApi#verify}
2988
   */
2989
  private async _verify(params: MFAVerifyTOTPParams): Promise<AuthMFAVerifyResponse>
2990
  private async _verify(params: MFAVerifyPhoneParams): Promise<AuthMFAVerifyResponse>
2991
  private async _verify<T extends 'create' | 'request'>(
2992
    params: MFAVerifyWebauthnParams<T>
2993
  ): Promise<AuthMFAVerifyResponse>
2994
  private async _verify(params: MFAVerifyParams): Promise<AuthMFAVerifyResponse> {
2995
    return this._acquireLock(-1, async () => {
6✔
2996
      try {
6✔
2997
        return await this._useSession(async (result) => {
6✔
2998
          const { data: sessionData, error: sessionError } = result
6✔
2999
          if (sessionError) {
6!
UNCOV
3000
            return { data: null, error: sessionError }
×
3001
          }
3002

3003
          const body: StrictOmit<
6✔
3004
            | Exclude<MFAVerifyParams, MFAVerifyWebauthnParams>
3005
            /** Exclude out the webauthn params from here because we're going to need to serialize them in the response */
3006
            | Prettify<
3007
                StrictOmit<MFAVerifyWebauthnParams, 'webauthn'> & {
3008
                  webauthn: Prettify<
3009
                    | StrictOmit<
3010
                        MFAVerifyWebauthnParamFields['webauthn'],
3011
                        'credential_response'
3012
                      > & {
3013
                        credential_response: PublicKeyCredentialJSON
3014
                      }
3015
                  >
3016
                }
3017
              >,
3018
            /*  Exclude challengeId because the backend expects snake_case, and exclude factorId since it's passed in the path params */
3019
            'challengeId' | 'factorId'
3020
          > & {
3021
            challenge_id: string
3022
          } = {
3023
            challenge_id: params.challengeId,
3024
            ...('webauthn' in params
6!
3025
              ? {
3026
                  webauthn: {
3027
                    ...params.webauthn,
3028
                    credential_response:
3029
                      params.webauthn.type === 'create'
×
3030
                        ? serializeCredentialCreationResponse(
3031
                            params.webauthn.credential_response as RegistrationCredential
3032
                          )
3033
                        : serializeCredentialRequestResponse(
3034
                            params.webauthn.credential_response as AuthenticationCredential
3035
                          ),
3036
                  },
3037
                }
3038
              : { code: params.code }),
3039
          }
3040

3041
          const { data, error } = await _request(
6✔
3042
            this.fetch,
3043
            'POST',
3044
            `${this.url}/factors/${params.factorId}/verify`,
3045
            {
3046
              body,
3047
              headers: this.headers,
3048
              jwt: sessionData?.session?.access_token,
34!
3049
            }
3050
          )
UNCOV
3051
          if (error) {
×
UNCOV
3052
            return { data: null, error }
×
3053
          }
3054

3055
          await this._saveSession({
×
3056
            expires_at: Math.round(Date.now() / 1000) + data.expires_in,
3057
            ...data,
3058
          })
3059
          await this._notifyAllSubscribers('MFA_CHALLENGE_VERIFIED', data)
×
3060

UNCOV
3061
          return { data, error }
×
3062
        })
3063
      } catch (error) {
3064
        if (isAuthError(error)) {
6✔
3065
          return { data: null, error }
6✔
3066
        }
UNCOV
3067
        throw error
×
3068
      }
3069
    })
3070
  }
3071

3072
  /**
3073
   * {@see GoTrueMFAApi#challenge}
3074
   */
3075
  private async _challenge(
3076
    params: MFAChallengeTOTPParams
3077
  ): Promise<Prettify<AuthMFAChallengeTOTPResponse>>
3078
  private async _challenge(
3079
    params: MFAChallengePhoneParams
3080
  ): Promise<Prettify<AuthMFAChallengePhoneResponse>>
3081
  private async _challenge(
3082
    params: MFAChallengeWebauthnParams
3083
  ): Promise<Prettify<AuthMFAChallengeWebauthnResponse>>
3084
  private async _challenge(params: MFAChallengeParams): Promise<AuthMFAChallengeResponse> {
3085
    return this._acquireLock(-1, async () => {
8✔
3086
      try {
8✔
3087
        return await this._useSession(async (result) => {
8✔
3088
          const { data: sessionData, error: sessionError } = result
8✔
3089
          if (sessionError) {
8!
3090
            return { data: null, error: sessionError }
×
3091
          }
3092

3093
          const response = (await _request(
8✔
3094
            this.fetch,
3095
            'POST',
3096
            `${this.url}/factors/${params.factorId}/challenge`,
3097
            {
3098
              body: params,
3099
              headers: this.headers,
3100
              jwt: sessionData?.session?.access_token,
46!
3101
            }
3102
          )) as
3103
            | Exclude<AuthMFAChallengeResponse, AuthMFAChallengeWebauthnResponse>
3104
            /** The server will send `serialized` data, so we assert the serialized response */
3105
            | AuthMFAChallengeWebauthnServerResponse
3106

3107
          if (response.error) {
6!
NEW
UNCOV
3108
            return response
×
3109
          }
3110

3111
          const { data } = response
6✔
3112

3113
          if (data.type !== 'webauthn') {
6✔
3114
            return { data, error: null }
6✔
3115
          }
3116

NEW
UNCOV
3117
          switch (data.webauthn.type) {
×
3118
            case 'create':
NEW
UNCOV
3119
              return {
×
3120
                data: {
3121
                  ...data,
3122
                  webauthn: {
3123
                    ...data.webauthn,
3124
                    credential_options: {
3125
                      ...data.webauthn.credential_options,
3126
                      publicKey: deserializeCredentialCreationOptions(
3127
                        data.webauthn.credential_options.publicKey
3128
                      ),
3129
                    },
3130
                  },
3131
                },
3132
                error: null,
3133
              }
3134
            case 'request':
NEW
3135
              return {
×
3136
                data: {
3137
                  ...data,
3138
                  webauthn: {
3139
                    ...data.webauthn,
3140
                    credential_options: {
3141
                      ...data.webauthn.credential_options,
3142
                      publicKey: deserializeCredentialRequestOptions(
3143
                        data.webauthn.credential_options.publicKey
3144
                      ),
3145
                    },
3146
                  },
3147
                },
3148
                error: null,
3149
              }
3150
          }
3151
        })
3152
      } catch (error) {
3153
        if (isAuthError(error)) {
2✔
3154
          return { data: null, error }
2✔
3155
        }
UNCOV
3156
        throw error
×
3157
      }
3158
    })
3159
  }
3160

3161
  /**
3162
   * {@see GoTrueMFAApi#challengeAndVerify}
3163
   */
3164
  private async _challengeAndVerify(
3165
    params: MFAChallengeAndVerifyParams
3166
  ): Promise<AuthMFAVerifyResponse> {
3167
    // both _challenge and _verify independently acquire the lock, so no need
3168
    // to acquire it here
3169

3170
    const { data: challengeData, error: challengeError } = await this._challenge({
2✔
3171
      factorId: params.factorId,
3172
    })
3173
    if (challengeError) {
2!
3174
      return { data: null, error: challengeError }
×
3175
    }
3176

3177
    return await this._verify({
2✔
3178
      factorId: params.factorId,
3179
      challengeId: challengeData.id,
3180
      code: params.code,
3181
    })
3182
  }
3183

3184
  /**
3185
   * {@see GoTrueMFAApi#listFactors}
3186
   */
3187
  private async _listFactors(): Promise<AuthMFAListFactorsResponse> {
3188
    // use #getUser instead of #_getUser as the former acquires a lock
3189
    const {
3190
      data: { user },
3191
      error: userError,
3192
    } = await this.getUser()
4✔
3193
    if (userError) {
4!
3194
      return { data: null, error: userError }
×
3195
    }
3196

3197
    const data: AuthMFAListFactorsResponse['data'] = {
4✔
3198
      all: [],
3199
      phone: [],
3200
      totp: [],
3201
      webauthn: [],
3202
    }
3203

3204
    // loop over the factors ONCE
3205
    for (const factor of user?.factors ?? []) {
4!
3206
      data.all.push(factor)
8✔
3207
      if (factor.status === 'verified') {
8✔
3208
        ;(data[factor.factor_type] as typeof factor[]).push(factor)
4✔
3209
      }
3210
    }
3211

3212
    return {
4✔
3213
      data,
3214
      error: null,
3215
    }
3216
  }
3217

3218
  /**
3219
   * {@see GoTrueMFAApi#getAuthenticatorAssuranceLevel}
3220
   */
3221
  private async _getAuthenticatorAssuranceLevel(): Promise<AuthMFAGetAuthenticatorAssuranceLevelResponse> {
3222
    return this._acquireLock(-1, async () => {
2✔
3223
      return await this._useSession(async (result) => {
2✔
3224
        const {
3225
          data: { session },
3226
          error: sessionError,
3227
        } = result
2✔
3228
        if (sessionError) {
2!
UNCOV
3229
          return { data: null, error: sessionError }
×
3230
        }
3231
        if (!session) {
2!
UNCOV
3232
          return {
×
3233
            data: { currentLevel: null, nextLevel: null, currentAuthenticationMethods: [] },
3234
            error: null,
3235
          }
3236
        }
3237

3238
        const { payload } = decodeJWT(session.access_token)
2✔
3239

3240
        let currentLevel: AuthenticatorAssuranceLevels | null = null
2✔
3241

3242
        if (payload.aal) {
2✔
3243
          currentLevel = payload.aal
2✔
3244
        }
3245

3246
        let nextLevel: AuthenticatorAssuranceLevels | null = currentLevel
2✔
3247

3248
        const verifiedFactors =
3249
          session.user.factors?.filter((factor: Factor) => factor.status === 'verified') ?? []
2!
3250

3251
        if (verifiedFactors.length > 0) {
2!
UNCOV
3252
          nextLevel = 'aal2'
×
3253
        }
3254

3255
        const currentAuthenticationMethods = payload.amr || []
2!
3256

3257
        return { data: { currentLevel, nextLevel, currentAuthenticationMethods }, error: null }
2✔
3258
      })
3259
    })
3260
  }
3261

3262
  private async fetchJwk(kid: string, jwks: { keys: JWK[] } = { keys: [] }): Promise<JWK | null> {
10✔
3263
    // try fetching from the supplied jwks
3264
    let jwk = jwks.keys.find((key) => key.kid === kid)
10✔
3265
    if (jwk) {
10!
UNCOV
3266
      return jwk
×
3267
    }
3268

3269
    const now = Date.now()
10✔
3270

3271
    // try fetching from cache
3272
    jwk = this.jwks.keys.find((key) => key.kid === kid)
10✔
3273

3274
    // jwk exists and jwks isn't stale
3275
    if (jwk && this.jwks_cached_at + JWKS_TTL > now) {
10✔
3276
      return jwk
3✔
3277
    }
3278
    // jwk isn't cached in memory so we need to fetch it from the well-known endpoint
3279
    const { data, error } = await _request(this.fetch, 'GET', `${this.url}/.well-known/jwks.json`, {
7✔
3280
      headers: this.headers,
3281
    })
3282
    if (error) {
7!
UNCOV
3283
      throw error
×
3284
    }
3285
    if (!data.keys || data.keys.length === 0) {
7!
UNCOV
3286
      return null
×
3287
    }
3288

3289
    this.jwks = data
7✔
3290
    this.jwks_cached_at = now
7✔
3291

3292
    // Find the signing key
3293
    jwk = data.keys.find((key: any) => key.kid === kid)
7✔
3294
    if (!jwk) {
7!
UNCOV
3295
      return null
×
3296
    }
3297
    return jwk
7✔
3298
  }
3299

3300
  /**
3301
   * Extracts the JWT claims present in the access token by first verifying the
3302
   * JWT against the server's JSON Web Key Set endpoint
3303
   * `/.well-known/jwks.json` which is often cached, resulting in significantly
3304
   * faster responses. Prefer this method over {@link #getUser} which always
3305
   * sends a request to the Auth server for each JWT.
3306
   *
3307
   * If the project is not using an asymmetric JWT signing key (like ECC or
3308
   * RSA) it always sends a request to the Auth server (similar to {@link
3309
   * #getUser}) to verify the JWT.
3310
   *
3311
   * @param jwt An optional specific JWT you wish to verify, not the one you
3312
   *            can obtain from {@link #getSession}.
3313
   * @param options Various additional options that allow you to customize the
3314
   *                behavior of this method.
3315
   */
3316
  async getClaims(
3317
    jwt?: string,
3318
    options: {
15✔
3319
      /**
3320
       * @deprecated Please use options.jwks instead.
3321
       */
3322
      keys?: JWK[]
3323

3324
      /** If set to `true` the `exp` claim will not be validated against the current time. */
3325
      allowExpired?: boolean
3326

3327
      /** If set, this JSON Web Key Set is going to have precedence over the cached value available on the server. */
3328
      jwks?: { keys: JWK[] }
3329
    } = {}
3330
  ): Promise<
3331
    | {
3332
        data: { claims: JwtPayload; header: JwtHeader; signature: Uint8Array }
3333
        error: null
3334
      }
3335
    | { data: null; error: AuthError }
3336
    | { data: null; error: null }
3337
  > {
3338
    try {
15✔
3339
      let token = jwt
15✔
3340
      if (!token) {
15✔
3341
        const { data, error } = await this.getSession()
15✔
3342
        if (error || !data.session) {
15✔
3343
          return { data: null, error }
2✔
3344
        }
3345
        token = data.session.access_token
13✔
3346
      }
3347

3348
      const {
3349
        header,
3350
        payload,
3351
        signature,
3352
        raw: { header: rawHeader, payload: rawPayload },
3353
      } = decodeJWT(token)
13✔
3354

3355
      if (!options?.allowExpired) {
11!
3356
        // Reject expired JWTs should only happen if jwt argument was passed
3357
        validateExp(payload.exp)
11✔
3358
      }
3359

3360
      const signingKey =
3361
        !header.alg ||
9✔
3362
        header.alg.startsWith('HS') ||
3363
        !header.kid ||
3364
        !('crypto' in globalThis && 'subtle' in globalThis.crypto)
5✔
3365
          ? null
3366
          : await this.fetchJwk(header.kid, options?.keys ? { keys: options.keys } : options?.jwks)
14!
3367

3368
      // If symmetric algorithm or WebCrypto API is unavailable, fallback to getUser()
3369
      if (!signingKey) {
9✔
3370
        const { error } = await this.getUser(token)
7✔
3371
        if (error) {
7✔
3372
          throw error
2✔
3373
        }
3374
        // getUser succeeds so the claims in the JWT can be trusted
3375
        return {
5✔
3376
          data: {
3377
            claims: payload,
3378
            header,
3379
            signature,
3380
          },
3381
          error: null,
3382
        }
3383
      }
3384

3385
      const algorithm = getAlgorithm(header.alg)
2✔
3386

3387
      // Convert JWK to CryptoKey
3388
      const publicKey = await crypto.subtle.importKey('jwk', signingKey, algorithm, true, [
2✔
3389
        'verify',
3390
      ])
3391

3392
      // Verify the signature
3393
      const isValid = await crypto.subtle.verify(
2✔
3394
        algorithm,
3395
        publicKey,
3396
        signature,
3397
        stringToUint8Array(`${rawHeader}.${rawPayload}`)
3398
      )
3399

3400
      if (!isValid) {
2✔
3401
        throw new AuthInvalidJwtError('Invalid JWT signature')
1✔
3402
      }
3403

3404
      // If verification succeeds, decode and return claims
3405
      return {
1✔
3406
        data: {
3407
          claims: payload,
3408
          header,
3409
          signature,
3410
        },
3411
        error: null,
3412
      }
3413
    } catch (error) {
3414
      if (isAuthError(error)) {
7✔
3415
        return { data: null, error }
5✔
3416
      }
3417
      throw error
2✔
3418
    }
3419
  }
3420
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc