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

supabase / auth-js / 17292897055

28 Aug 2025 10:18AM UTC coverage: 80.811%. Remained the same
17292897055

Pull #1105

github

web-flow
Merge a6a43573f into be9a27cc7
Pull Request #1105: chore: secure-proof workflows

1080 of 1441 branches covered (74.95%)

Branch coverage included in aggregate %.

1451 of 1691 relevant lines covered (85.81%)

173.18 hits per line

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

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

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

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

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

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

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

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

161
  private instanceID: number
162

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

177
  protected flowType: AuthFlowType
178

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

334
    this.initialize()
182✔
335
  }
336

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

345
    return this
7,239✔
346
  }
347

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

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

364
    return await this.initializePromise
182✔
365
  }
366

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

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

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

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

409
          return { error }
4✔
410
        }
411

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

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

422
        await this._saveSession(session)
2✔
423

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

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

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

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

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

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

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

485
      throw error
×
486
    }
487
  }
488

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

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

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

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

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

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

564
      throw error
×
565
    }
566
  }
567

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

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

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

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

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

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

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

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

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

701
      let resolvedWallet: EthereumWallet
702

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

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

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

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

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

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

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

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

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

774
      message = createSiweMessage(siweMessage)
×
775

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

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

820
      throw error
×
821
    }
822
  }
823

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

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

834
      let resolvedWallet: SolanaWallet
835

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

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

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

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

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

870
          ...options?.signInWithSolana,
×
871

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

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

880
        let outputToProcess: any
881

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

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

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

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

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

966
        signature = maybeSignature
×
967
      }
968
    }
969

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

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

1008
      throw error
×
1009
    }
1010
  }
1011

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

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

1056
      throw error
×
1057
    }
1058
  }
1059

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

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

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

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

1165
      throw error
×
1166
    }
1167
  }
1168

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

1190
      if (error) {
×
1191
        throw error
×
1192
      }
1193

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

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

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

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

1215
      throw error
×
1216
    }
1217
  }
1218

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

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

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

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

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

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

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

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

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

1364
    return result
71✔
1365
  }
1366

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

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

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

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

1394
        return result
2✔
1395
      }
1396

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

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

1403
          const result = fn()
595✔
1404

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

1415
          await result
595✔
1416

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

1421
            await Promise.all(waitOn)
587✔
1422

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1604
    await this.initializePromise
16✔
1605

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

1610
    return result
16✔
1611
  }
1612

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

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

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

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

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

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

1653
      throw error
8✔
1654
    }
1655
  }
1656

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

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

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

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

1720
      throw error
×
1721
    }
1722
  }
1723

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

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

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

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

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

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

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

1794
      throw error
×
1795
    }
1796
  }
1797

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

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

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

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

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

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

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

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

1846
      throw error
×
1847
    }
1848
  }
1849

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1980
      throw error
4✔
1981
    }
1982
  }
1983

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2150
      throw error
×
2151
    }
2152
  }
2153

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

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

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

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

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

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

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

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

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

2304
    return isValidSession
253✔
2305
  }
2306

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2390
        return
48✔
2391
      }
2392

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2498
        return result
14✔
2499
      }
2500

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

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

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

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

2531
      await Promise.all(promises)
445✔
2532

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2785
      return false
106✔
2786
    }
2787

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3045
    return await this._verify({
2✔
3046
      factorId: params.factorId,
3047
      challengeId: challengeData.id,
3048
      code: params.code,
3049
    })
3050
  }
3051

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

3065
    const factors = user?.factors || []
4!
3066
    const totp = factors.filter(
4✔
3067
      (factor) => factor.factor_type === 'totp' && factor.status === 'verified'
8✔
3068
    )
3069
    const phone = factors.filter(
4✔
3070
      (factor) => factor.factor_type === 'phone' && factor.status === 'verified'
8✔
3071
    )
3072

3073
    return {
4✔
3074
      data: {
3075
        all: factors,
3076
        totp,
3077
        phone,
3078
      },
3079
      error: null,
3080
    }
3081
  }
3082

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

3103
        const { payload } = decodeJWT(session.access_token)
2✔
3104

3105
        let currentLevel: AuthenticatorAssuranceLevels | null = null
2✔
3106

3107
        if (payload.aal) {
2✔
3108
          currentLevel = payload.aal
2✔
3109
        }
3110

3111
        let nextLevel: AuthenticatorAssuranceLevels | null = currentLevel
2✔
3112

3113
        const verifiedFactors =
3114
          session.user.factors?.filter((factor: Factor) => factor.status === 'verified') ?? []
2!
3115

3116
        if (verifiedFactors.length > 0) {
2!
3117
          nextLevel = 'aal2'
×
3118
        }
3119

3120
        const currentAuthenticationMethods = payload.amr || []
2!
3121

3122
        return { data: { currentLevel, nextLevel, currentAuthenticationMethods }, error: null }
2✔
3123
      })
3124
    })
3125
  }
3126

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

3134
    const now = Date.now()
10✔
3135

3136
    // try fetching from cache
3137
    jwk = this.jwks.keys.find((key) => key.kid === kid)
10✔
3138

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

3154
    this.jwks = data
7✔
3155
    this.jwks_cached_at = now
7✔
3156

3157
    // Find the signing key
3158
    jwk = data.keys.find((key: any) => key.kid === kid)
7✔
3159
    if (!jwk) {
7!
3160
      return null
×
3161
    }
3162
    return jwk
7✔
3163
  }
3164

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

3189
      /** If set to `true` the `exp` claim will not be validated against the current time. */
3190
      allowExpired?: boolean
3191

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

3213
      const {
3214
        header,
3215
        payload,
3216
        signature,
3217
        raw: { header: rawHeader, payload: rawPayload },
3218
      } = decodeJWT(token)
13✔
3219

3220
      if (!options?.allowExpired) {
11!
3221
        // Reject expired JWTs should only happen if jwt argument was passed
3222
        validateExp(payload.exp)
11✔
3223
      }
3224

3225
      const signingKey =
3226
        !header.alg ||
9✔
3227
        header.alg.startsWith('HS') ||
3228
        !header.kid ||
3229
        !('crypto' in globalThis && 'subtle' in globalThis.crypto)
5✔
3230
          ? null
3231
          : await this.fetchJwk(header.kid, options?.keys ? { keys: options.keys } : options?.jwks)
14!
3232

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

3250
      const algorithm = getAlgorithm(header.alg)
2✔
3251

3252
      // Convert JWK to CryptoKey
3253
      const publicKey = await crypto.subtle.importKey('jwk', signingKey, algorithm, true, [
2✔
3254
        'verify',
3255
      ])
3256

3257
      // Verify the signature
3258
      const isValid = await crypto.subtle.verify(
2✔
3259
        algorithm,
3260
        publicKey,
3261
        signature,
3262
        stringToUint8Array(`${rawHeader}.${rawPayload}`)
3263
      )
3264

3265
      if (!isValid) {
2✔
3266
        throw new AuthInvalidJwtError('Invalid JWT signature')
1✔
3267
      }
3268

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

© 2025 Coveralls, Inc