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

supabase / auth-js / 15182547465

22 May 2025 09:07AM UTC coverage: 59.434%. Remained the same
15182547465

push

github

web-flow
chore(master): release 2.70.0 (#1048)

:robot: I have created a release *beep* *boop*
---


##
[2.70.0](https://github.com/supabase/auth-js/compare/v2.69.1...v2.70.0)
(2025-05-16)


### Features

* add `signInWithWeb3` with solana
([#1037](https://github.com/supabase/auth-js/issues/1037))
([cff5bcb](https://github.com/supabase/auth-js/commit/cff5bcb83))
* validate uuid and sign out scope parameters to functions
([#1063](https://github.com/supabase/auth-js/issues/1063))
([1bcb76e](https://github.com/supabase/auth-js/commit/1bcb76e47))


### Bug Fixes

* add missing `deleted_at` property to `User` interface
([#1059](https://github.com/supabase/auth-js/issues/1059))
([96da194](https://github.com/supabase/auth-js/commit/96da194b9))
* export `processLock` from toplevel
([#1057](https://github.com/supabase/auth-js/issues/1057))
([d99695a](https://github.com/supabase/auth-js/commit/d99695af9))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

592 of 1206 branches covered (49.09%)

Branch coverage included in aggregate %.

1046 of 1550 relevant lines covered (67.48%)

115.09 hits per line

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

47.28
/src/GoTrueClient.ts
1
import GoTrueAdminApi from './GoTrueAdminApi'
8✔
2
import {
8✔
3
  DEFAULT_HEADERS,
4
  EXPIRY_MARGIN_MS,
5
  AUTO_REFRESH_TICK_DURATION_MS,
6
  AUTO_REFRESH_TICK_THRESHOLD,
7
  GOTRUE_URL,
8
  STORAGE_KEY,
9
  JWKS_TTL,
10
} from './lib/constants'
11
import {
8✔
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 {
8✔
27
  Fetch,
28
  _request,
29
  _sessionResponse,
30
  _sessionResponsePassword,
31
  _userResponse,
32
  _ssoResponse,
33
} from './lib/fetch'
34
import {
8✔
35
  Deferred,
36
  getItemAsync,
37
  isBrowser,
38
  removeItemAsync,
39
  resolveFetch,
40
  setItemAsync,
41
  uuid,
42
  retryable,
43
  sleep,
44
  supportsLocalStorage,
45
  parseParametersFromURL,
46
  getCodeChallengeAndMethod,
47
  getAlgorithm,
48
  validateExp,
49
  decodeJWT,
50
} from './lib/helpers'
51
import { localStorageAdapter, memoryLocalStorageAdapter } from './lib/local-storage'
8✔
52
import { polyfillGlobalThis } from './lib/polyfills'
8✔
53
import { version } from './lib/version'
8✔
54
import { LockAcquireTimeoutError, navigatorLock } from './lib/locks'
8✔
55

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

115
polyfillGlobalThis() // Make "globalThis" available
8✔
116

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

129
async function lockNoOp<R>(name: string, acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
130
  return await fn()
250✔
131
}
132

133
export default class GoTrueClient {
8✔
134
  private static nextInstanceID = 0
8✔
135

136
  private instanceID: number
137

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

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

185
  /**
186
   * Used to broadcast state change events to other tabs listening.
187
   */
188
  protected broadcastChannel: BroadcastChannel | null = null
56✔
189

190
  protected logDebugMessages: boolean
191
  protected logger: (message: string, ...args: any[]) => void = console.log
56✔
192

193
  /**
194
   * Create a new client for use in the browser.
195
   */
196
  constructor(options: GoTrueClientOptions) {
197
    this.instanceID = GoTrueClient.nextInstanceID
56✔
198
    GoTrueClient.nextInstanceID += 1
56✔
199

200
    if (this.instanceID > 0 && isBrowser()) {
56!
201
      console.warn(
×
202
        '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.'
203
      )
204
    }
205

206
    const settings = { ...DEFAULT_OPTIONS, ...options }
56✔
207

208
    this.logDebugMessages = !!settings.debug
56✔
209
    if (typeof settings.debug === 'function') {
56!
210
      this.logger = settings.debug
×
211
    }
212

213
    this.persistSession = settings.persistSession
56✔
214
    this.storageKey = settings.storageKey
56✔
215
    this.autoRefreshToken = settings.autoRefreshToken
56✔
216
    this.admin = new GoTrueAdminApi({
56✔
217
      url: settings.url,
218
      headers: settings.headers,
219
      fetch: settings.fetch,
220
    })
221

222
    this.url = settings.url
56✔
223
    this.headers = settings.headers
56✔
224
    this.fetch = resolveFetch(settings.fetch)
56✔
225
    this.lock = settings.lock || lockNoOp
56✔
226
    this.detectSessionInUrl = settings.detectSessionInUrl
56✔
227
    this.flowType = settings.flowType
56✔
228
    this.hasCustomAuthorizationHeader = settings.hasCustomAuthorizationHeader
56✔
229

230
    if (settings.lock) {
56!
231
      this.lock = settings.lock
×
232
    } else if (isBrowser() && globalThis?.navigator?.locks) {
56!
233
      this.lock = navigatorLock
×
234
    } else {
235
      this.lock = lockNoOp
56✔
236
    }
237
    this.jwks = { keys: [] }
56✔
238
    this.jwks_cached_at = Number.MIN_SAFE_INTEGER
56✔
239
    this.mfa = {
56✔
240
      verify: this._verify.bind(this),
241
      enroll: this._enroll.bind(this),
242
      unenroll: this._unenroll.bind(this),
243
      challenge: this._challenge.bind(this),
244
      listFactors: this._listFactors.bind(this),
245
      challengeAndVerify: this._challengeAndVerify.bind(this),
246
      getAuthenticatorAssuranceLevel: this._getAuthenticatorAssuranceLevel.bind(this),
247
    }
248

249
    if (this.persistSession) {
56!
250
      if (settings.storage) {
56!
251
        this.storage = settings.storage
56✔
252
      } else {
253
        if (supportsLocalStorage()) {
×
254
          this.storage = localStorageAdapter
×
255
        } else {
256
          this.memoryStorage = {}
×
257
          this.storage = memoryLocalStorageAdapter(this.memoryStorage)
×
258
        }
259
      }
260
    } else {
261
      this.memoryStorage = {}
×
262
      this.storage = memoryLocalStorageAdapter(this.memoryStorage)
×
263
    }
264

265
    if (isBrowser() && globalThis.BroadcastChannel && this.persistSession && this.storageKey) {
56!
266
      try {
×
267
        this.broadcastChannel = new globalThis.BroadcastChannel(this.storageKey)
×
268
      } catch (e: any) {
269
        console.error(
×
270
          'Failed to create a new BroadcastChannel, multi-tab state changes will not be available',
271
          e
272
        )
273
      }
274

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

278
        await this._notifyAllSubscribers(event.data.event, event.data.session, false) // broadcast = false so we don't get an endless loop of messages
×
279
      })
280
    }
281

282
    this.initialize()
56✔
283
  }
284

285
  private _debug(...args: any[]): GoTrueClient {
286
    if (this.logDebugMessages) {
3,146!
287
      this.logger(
×
288
        `GoTrueClient@${this.instanceID} (${version}) ${new Date().toISOString()}`,
289
        ...args
290
      )
291
    }
292

293
    return this
3,146✔
294
  }
295

296
  /**
297
   * Initializes the client session either from the url or from storage.
298
   * This method is automatically called when instantiating the client, but should also be called
299
   * manually when checking for an error from an auth redirect (oauth, magiclink, password recovery, etc).
300
   */
301
  async initialize(): Promise<InitializeResult> {
302
    if (this.initializePromise) {
68✔
303
      return await this.initializePromise
12✔
304
    }
305

306
    this.initializePromise = (async () => {
56✔
307
      return await this._acquireLock(-1, async () => {
56✔
308
        return await this._initialize()
56✔
309
      })
310
    })()
311

312
    return await this.initializePromise
56✔
313
  }
314

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

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

342
          if (isAuthImplicitGrantRedirectError(error)) {
×
343
            const errorCode = error.details?.code
×
344
            if (
×
345
              errorCode === 'identity_already_exists' ||
×
346
              errorCode === 'identity_not_found' ||
347
              errorCode === 'single_identity_not_deletable'
348
            ) {
349
              return { error }
×
350
            }
351
          }
352

353
          // failed login attempt via url,
354
          // remove old session as in verifyOtp, signUp and signInWith*
355
          await this._removeSession()
×
356

357
          return { error }
×
358
        }
359

360
        const { session, redirectType } = data
×
361

362
        this._debug(
×
363
          '#_initialize()',
364
          'detected session in URL',
365
          session,
366
          'redirect type',
367
          redirectType
368
        )
369

370
        await this._saveSession(session)
×
371

372
        setTimeout(async () => {
×
373
          if (redirectType === 'recovery') {
×
374
            await this._notifyAllSubscribers('PASSWORD_RECOVERY', session)
×
375
          } else {
376
            await this._notifyAllSubscribers('SIGNED_IN', session)
×
377
          }
378
        }, 0)
379

380
        return { error: null }
×
381
      }
382
      // no login attempt via callback url try to recover session from storage
383
      await this._recoverAndRefresh()
×
384
      return { error: null }
×
385
    } catch (error) {
386
      if (isAuthError(error)) {
56!
387
        return { error }
×
388
      }
389

390
      return {
56✔
391
        error: new AuthUnknownError('Unexpected error during initialization', error),
392
      }
393
    } finally {
394
      await this._handleVisibilityChange()
56✔
395
      this._debug('#_initialize()', 'end')
56✔
396
    }
397
  }
398

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

416
      if (error || !data) {
4!
417
        return { data: { user: null, session: null }, error: error }
×
418
      }
419
      const session: Session | null = data.session
4✔
420
      const user: User | null = data.user
4✔
421

422
      if (data.session) {
4✔
423
        await this._saveSession(data.session)
4✔
424
        await this._notifyAllSubscribers('SIGNED_IN', session)
4✔
425
      }
426

427
      return { data: { user, session }, error: null }
4✔
428
    } catch (error) {
429
      if (isAuthError(error)) {
2✔
430
        return { data: { user: null, session: null }, error }
2✔
431
      }
432

433
      throw error
×
434
    }
435
  }
436

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

492
      const { data, error } = res
74✔
493

494
      if (error || !data) {
74!
495
        return { data: { user: null, session: null }, error: error }
×
496
      }
497

498
      const session: Session | null = data.session
74✔
499
      const user: User | null = data.user
74✔
500

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

506
      return { data: { user, session }, error: null }
74✔
507
    } catch (error) {
508
      if (isAuthError(error)) {
12✔
509
        return { data: { user: null, session: null }, error }
12✔
510
      }
511

512
      throw error
×
513
    }
514
  }
515

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

558
      if (error) {
22!
559
        return { data: { user: null, session: null }, error }
×
560
      } else if (!data || !data.session || !data.user) {
22!
561
        return { data: { user: null, session: null }, error: new AuthInvalidTokenResponseError() }
×
562
      }
563
      if (data.session) {
22✔
564
        await this._saveSession(data.session)
22✔
565
        await this._notifyAllSubscribers('SIGNED_IN', data.session)
22✔
566
      }
567
      return {
22✔
568
        data: {
569
          user: data.user,
570
          session: data.session,
571
          ...(data.weak_password ? { weakPassword: data.weak_password } : null),
22!
572
        },
573
        error,
574
      }
575
    } catch (error) {
576
      if (isAuthError(error)) {
2✔
577
        return { data: { user: null, session: null }, error }
2✔
578
      }
579
      throw error
×
580
    }
581
  }
582

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

596
  /**
597
   * Log in an existing user by exchanging an Auth Code issued during the PKCE flow.
598
   */
599
  async exchangeCodeForSession(authCode: string): Promise<AuthTokenResponse> {
600
    await this.initializePromise
2✔
601

602
    return this._acquireLock(-1, async () => {
2✔
603
      return this._exchangeCodeForSession(authCode)
2✔
604
    })
605
  }
606

607
  /**
608
   * Signs in a user by verifying a message signed by the user's private key.
609
   * Only Solana supported at this time, using the Sign in with Solana standard.
610
   */
611
  async signInWithWeb3(credentials: Web3Credentials): Promise<
612
    | {
613
        data: { session: Session; user: User }
614
        error: null
615
      }
616
    | { data: { session: null; user: null }; error: AuthError }
617
  > {
618
    const { chain } = credentials
×
619

620
    if (chain === 'solana') {
×
621
      return await this.signInWithSolana(credentials)
×
622
    }
623

624
    throw new Error(`@supabase/auth-js: Unsupported chain "${chain}"`)
×
625
  }
626

627
  private async signInWithSolana(credentials: SolanaWeb3Credentials) {
628
    let message: string
629
    let signature: Uint8Array
630

631
    if ('message' in credentials) {
×
632
      message = credentials.message
×
633
      signature = credentials.signature
×
634
    } else {
635
      const { chain, wallet, statement, options } = credentials
×
636

637
      let resolvedWallet: SolanaWallet
638

639
      if (!isBrowser()) {
×
640
        if (typeof wallet !== 'object' || !options?.url) {
×
641
          throw new Error(
×
642
            '@supabase/auth-js: Both wallet and url must be specified in non-browser environments.'
643
          )
644
        }
645

646
        resolvedWallet = wallet
×
647
      } else if (typeof wallet === 'object') {
×
648
        resolvedWallet = wallet
×
649
      } else {
650
        const windowAny = window as any
×
651

652
        if (
×
653
          'solana' in windowAny &&
×
654
          typeof windowAny.solana === 'object' &&
655
          (('signIn' in windowAny.solana && typeof windowAny.solana.signIn === 'function') ||
656
            ('signMessage' in windowAny.solana &&
657
              typeof windowAny.solana.signMessage === 'function'))
658
        ) {
659
          resolvedWallet = windowAny.solana
×
660
        } else {
661
          throw new Error(
×
662
            `@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.`
663
          )
664
        }
665
      }
666

667
      const url = new URL(options?.url ?? window.location.href)
×
668

669
      if ('signIn' in resolvedWallet && resolvedWallet.signIn) {
×
670
        const output = await resolvedWallet.signIn({
×
671
          issuedAt: new Date().toISOString(),
672

673
          ...options?.signInWithSolana,
×
674

675
          // non-overridable properties
676
          version: '1',
677
          domain: url.host,
678
          uri: url.href,
679

680
          ...(statement ? { statement } : null),
×
681
        })
682

683
        let outputToProcess: any
684

685
        if (Array.isArray(output) && output[0] && typeof output[0] === 'object') {
×
686
          outputToProcess = output[0]
×
687
        } else if (
×
688
          output &&
×
689
          typeof output === 'object' &&
690
          'signedMessage' in output &&
691
          'signature' in output
692
        ) {
693
          outputToProcess = output
×
694
        } else {
695
          throw new Error('@supabase/auth-js: Wallet method signIn() returned unrecognized value')
×
696
        }
697

698
        if (
×
699
          'signedMessage' in outputToProcess &&
×
700
          'signature' in outputToProcess &&
701
          (typeof outputToProcess.signedMessage === 'string' ||
702
            outputToProcess.signedMessage instanceof Uint8Array) &&
703
          outputToProcess.signature instanceof Uint8Array
704
        ) {
705
          message =
×
706
            typeof outputToProcess.signedMessage === 'string'
×
707
              ? outputToProcess.signedMessage
708
              : new TextDecoder().decode(outputToProcess.signedMessage)
709
          signature = outputToProcess.signature
×
710
        } else {
711
          throw new Error(
×
712
            '@supabase/auth-js: Wallet method signIn() API returned object without signedMessage and signature fields'
713
          )
714
        }
715
      } else {
716
        if (
×
717
          !('signMessage' in resolvedWallet) ||
×
718
          typeof resolvedWallet.signMessage !== 'function' ||
719
          !('publicKey' in resolvedWallet) ||
720
          typeof resolvedWallet !== 'object' ||
721
          !resolvedWallet.publicKey ||
722
          !('toBase58' in resolvedWallet.publicKey) ||
723
          typeof resolvedWallet.publicKey.toBase58 !== 'function'
724
        ) {
725
          throw new Error(
×
726
            '@supabase/auth-js: Wallet does not have a compatible signMessage() and publicKey.toBase58() API'
727
          )
728
        }
729

730
        message = [
×
731
          `${url.host} wants you to sign in with your Solana account:`,
732
          resolvedWallet.publicKey.toBase58(),
733
          ...(statement ? ['', statement, ''] : ['']),
×
734
          'Version: 1',
735
          `URI: ${url.href}`,
736
          `Issued At: ${options?.signInWithSolana?.issuedAt ?? new Date().toISOString()}`,
×
737
          ...(options?.signInWithSolana?.notBefore
×
738
            ? [`Not Before: ${options.signInWithSolana.notBefore}`]
739
            : []),
740
          ...(options?.signInWithSolana?.expirationTime
×
741
            ? [`Expiration Time: ${options.signInWithSolana.expirationTime}`]
742
            : []),
743
          ...(options?.signInWithSolana?.chainId
×
744
            ? [`Chain ID: ${options.signInWithSolana.chainId}`]
745
            : []),
746
          ...(options?.signInWithSolana?.nonce ? [`Nonce: ${options.signInWithSolana.nonce}`] : []),
×
747
          ...(options?.signInWithSolana?.requestId
×
748
            ? [`Request ID: ${options.signInWithSolana.requestId}`]
749
            : []),
750
          ...(options?.signInWithSolana?.resources?.length
×
751
            ? [
752
                'Resources',
753
                ...options.signInWithSolana.resources.map((resource) => `- ${resource}`),
×
754
              ]
755
            : []),
756
        ].join('\n')
757

758
        const maybeSignature = await resolvedWallet.signMessage(
×
759
          new TextEncoder().encode(message),
760
          'utf8'
761
        )
762

763
        if (!maybeSignature || !(maybeSignature instanceof Uint8Array)) {
×
764
          throw new Error(
×
765
            '@supabase/auth-js: Wallet signMessage() API returned an recognized value'
766
          )
767
        }
768

769
        signature = maybeSignature
×
770
      }
771
    }
772

773
    try {
×
774
      const { data, error } = await _request(
×
775
        this.fetch,
776
        'POST',
777
        `${this.url}/token?grant_type=web3`,
778
        {
779
          headers: this.headers,
780
          body: {
781
            chain: 'solana',
782
            message,
783
            signature: bytesToBase64URL(signature),
784

785
            ...(credentials.options?.captchaToken
×
786
              ? { gotrue_meta_security: { captcha_token: credentials.options?.captchaToken } }
×
787
              : null),
788
          },
789
          xform: _sessionResponse,
790
        }
791
      )
792
      if (error) {
×
793
        throw error
×
794
      }
795
      if (!data || !data.session || !data.user) {
×
796
        return {
×
797
          data: { user: null, session: null },
798
          error: new AuthInvalidTokenResponseError(),
799
        }
800
      }
801
      if (data.session) {
×
802
        await this._saveSession(data.session)
×
803
        await this._notifyAllSubscribers('SIGNED_IN', data.session)
×
804
      }
805
      return { data: { ...data }, error }
×
806
    } catch (error) {
807
      if (isAuthError(error)) {
×
808
        return { data: { user: null, session: null }, error }
×
809
      }
810

811
      throw error
×
812
    }
813
  }
814

815
  private async _exchangeCodeForSession(authCode: string): Promise<
816
    | {
817
        data: { session: Session; user: User; redirectType: string | null }
818
        error: null
819
      }
820
    | { data: { session: null; user: null; redirectType: null }; error: AuthError }
821
  > {
822
    const storageItem = await getItemAsync(this.storage, `${this.storageKey}-code-verifier`)
2✔
823
    const [codeVerifier, redirectType] = ((storageItem ?? '') as string).split('/')
2!
824

825
    try {
2✔
826
      const { data, error } = await _request(
2✔
827
        this.fetch,
828
        'POST',
829
        `${this.url}/token?grant_type=pkce`,
830
        {
831
          headers: this.headers,
832
          body: {
833
            auth_code: authCode,
834
            code_verifier: codeVerifier,
835
          },
836
          xform: _sessionResponse,
837
        }
838
      )
839
      await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
×
840
      if (error) {
×
841
        throw error
×
842
      }
843
      if (!data || !data.session || !data.user) {
×
844
        return {
×
845
          data: { user: null, session: null, redirectType: null },
846
          error: new AuthInvalidTokenResponseError(),
847
        }
848
      }
849
      if (data.session) {
×
850
        await this._saveSession(data.session)
×
851
        await this._notifyAllSubscribers('SIGNED_IN', data.session)
×
852
      }
853
      return { data: { ...data, redirectType: redirectType ?? null }, error }
×
854
    } catch (error) {
855
      if (isAuthError(error)) {
2✔
856
        return { data: { user: null, session: null, redirectType: null }, error }
2✔
857
      }
858

859
      throw error
×
860
    }
861
  }
862

863
  /**
864
   * Allows signing in with an OIDC ID token. The authentication provider used
865
   * should be enabled and configured.
866
   */
867
  async signInWithIdToken(credentials: SignInWithIdTokenCredentials): Promise<AuthTokenResponse> {
868
    try {
×
869
      const { options, provider, token, access_token, nonce } = credentials
×
870

871
      const res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=id_token`, {
×
872
        headers: this.headers,
873
        body: {
874
          provider,
875
          id_token: token,
876
          access_token,
877
          nonce,
878
          gotrue_meta_security: { captcha_token: options?.captchaToken },
×
879
        },
880
        xform: _sessionResponse,
881
      })
882

883
      const { data, error } = res
×
884
      if (error) {
×
885
        return { data: { user: null, session: null }, error }
×
886
      } else if (!data || !data.session || !data.user) {
×
887
        return {
×
888
          data: { user: null, session: null },
889
          error: new AuthInvalidTokenResponseError(),
890
        }
891
      }
892
      if (data.session) {
×
893
        await this._saveSession(data.session)
×
894
        await this._notifyAllSubscribers('SIGNED_IN', data.session)
×
895
      }
896
      return { data, error }
×
897
    } catch (error) {
898
      if (isAuthError(error)) {
×
899
        return { data: { user: null, session: null }, error }
×
900
      }
901
      throw error
×
902
    }
903
  }
904

905
  /**
906
   * Log in a user using magiclink or a one-time password (OTP).
907
   *
908
   * If the `{{ .ConfirmationURL }}` variable is specified in the email template, a magiclink will be sent.
909
   * If the `{{ .Token }}` variable is specified in the email template, an OTP will be sent.
910
   * 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.
911
   *
912
   * Be aware that you may get back an error message that will not distinguish
913
   * between the cases where the account does not exist or, that the account
914
   * can only be accessed via social login.
915
   *
916
   * Do note that you will need to configure a Whatsapp sender on Twilio
917
   * if you are using phone sign in with the 'whatsapp' channel. The whatsapp
918
   * channel is not supported on other providers
919
   * at this time.
920
   * This method supports PKCE when an email is passed.
921
   */
922
  async signInWithOtp(credentials: SignInWithPasswordlessCredentials): Promise<AuthOtpResponse> {
923
    try {
4✔
924
      if ('email' in credentials) {
4✔
925
        const { email, options } = credentials
2✔
926
        let codeChallenge: string | null = null
2✔
927
        let codeChallengeMethod: string | null = null
2✔
928
        if (this.flowType === 'pkce') {
2!
929
          ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
×
930
            this.storage,
931
            this.storageKey
932
          )
933
        }
934
        const { error } = await _request(this.fetch, 'POST', `${this.url}/otp`, {
2✔
935
          headers: this.headers,
936
          body: {
937
            email,
938
            data: options?.data ?? {},
12!
939
            create_user: options?.shouldCreateUser ?? true,
12!
940
            gotrue_meta_security: { captcha_token: options?.captchaToken },
6!
941
            code_challenge: codeChallenge,
942
            code_challenge_method: codeChallengeMethod,
943
          },
944
          redirectTo: options?.emailRedirectTo,
6!
945
        })
946
        return { data: { user: null, session: null }, error }
2✔
947
      }
948
      if ('phone' in credentials) {
2✔
949
        const { phone, options } = credentials
2✔
950
        const { data, error } = await _request(this.fetch, 'POST', `${this.url}/otp`, {
2✔
951
          headers: this.headers,
952
          body: {
953
            phone,
954
            data: options?.data ?? {},
12!
955
            create_user: options?.shouldCreateUser ?? true,
12!
956
            gotrue_meta_security: { captcha_token: options?.captchaToken },
6!
957
            channel: options?.channel ?? 'sms',
12!
958
          },
959
        })
960
        return { data: { user: null, session: null, messageId: data?.message_id }, error }
×
961
      }
962
      throw new AuthInvalidCredentialsError('You must provide either an email or phone number.')
×
963
    } catch (error) {
964
      if (isAuthError(error)) {
2✔
965
        return { data: { user: null, session: null }, error }
2✔
966
      }
967

968
      throw error
×
969
    }
970
  }
971

972
  /**
973
   * Log in a user given a User supplied OTP or TokenHash received through mobile or email.
974
   */
975
  async verifyOtp(params: VerifyOtpParams): Promise<AuthResponse> {
976
    try {
4✔
977
      let redirectTo: string | undefined = undefined
4✔
978
      let captchaToken: string | undefined = undefined
4✔
979
      if ('options' in params) {
4!
980
        redirectTo = params.options?.redirectTo
×
981
        captchaToken = params.options?.captchaToken
×
982
      }
983
      const { data, error } = await _request(this.fetch, 'POST', `${this.url}/verify`, {
4✔
984
        headers: this.headers,
985
        body: {
986
          ...params,
987
          gotrue_meta_security: { captcha_token: captchaToken },
988
        },
989
        redirectTo,
990
        xform: _sessionResponse,
991
      })
992

993
      if (error) {
×
994
        throw error
×
995
      }
996

997
      if (!data) {
×
998
        throw new Error('An error occurred on token verification.')
×
999
      }
1000

1001
      const session: Session | null = data.session
×
1002
      const user: User = data.user
×
1003

1004
      if (session?.access_token) {
×
1005
        await this._saveSession(session as Session)
×
1006
        await this._notifyAllSubscribers(
×
1007
          params.type == 'recovery' ? 'PASSWORD_RECOVERY' : 'SIGNED_IN',
×
1008
          session
1009
        )
1010
      }
1011

1012
      return { data: { user, session }, error: null }
×
1013
    } catch (error) {
1014
      if (isAuthError(error)) {
4✔
1015
        return { data: { user: null, session: null }, error }
4✔
1016
      }
1017

1018
      throw error
×
1019
    }
1020
  }
1021

1022
  /**
1023
   * Attempts a single-sign on using an enterprise Identity Provider. A
1024
   * successful SSO attempt will redirect the current page to the identity
1025
   * provider authorization page. The redirect URL is implementation and SSO
1026
   * protocol specific.
1027
   *
1028
   * You can use it by providing a SSO domain. Typically you can extract this
1029
   * domain by asking users for their email address. If this domain is
1030
   * registered on the Auth instance the redirect will use that organization's
1031
   * currently active SSO Identity Provider for the login.
1032
   *
1033
   * If you have built an organization-specific login page, you can use the
1034
   * organization's SSO Identity Provider UUID directly instead.
1035
   */
1036
  async signInWithSSO(params: SignInWithSSO): Promise<SSOResponse> {
1037
    try {
×
1038
      let codeChallenge: string | null = null
×
1039
      let codeChallengeMethod: string | null = null
×
1040
      if (this.flowType === 'pkce') {
×
1041
        ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
×
1042
          this.storage,
1043
          this.storageKey
1044
        )
1045
      }
1046

1047
      return await _request(this.fetch, 'POST', `${this.url}/sso`, {
×
1048
        body: {
1049
          ...('providerId' in params ? { provider_id: params.providerId } : null),
×
1050
          ...('domain' in params ? { domain: params.domain } : null),
×
1051
          redirect_to: params.options?.redirectTo ?? undefined,
×
1052
          ...(params?.options?.captchaToken
×
1053
            ? { gotrue_meta_security: { captcha_token: params.options.captchaToken } }
1054
            : null),
1055
          skip_http_redirect: true, // fetch does not handle redirects
1056
          code_challenge: codeChallenge,
1057
          code_challenge_method: codeChallengeMethod,
1058
        },
1059
        headers: this.headers,
1060
        xform: _ssoResponse,
1061
      })
1062
    } catch (error) {
1063
      if (isAuthError(error)) {
×
1064
        return { data: null, error }
×
1065
      }
1066
      throw error
×
1067
    }
1068
  }
1069

1070
  /**
1071
   * Sends a reauthentication OTP to the user's email or phone number.
1072
   * Requires the user to be signed-in.
1073
   */
1074
  async reauthenticate(): Promise<AuthResponse> {
1075
    await this.initializePromise
×
1076

1077
    return await this._acquireLock(-1, async () => {
×
1078
      return await this._reauthenticate()
×
1079
    })
1080
  }
1081

1082
  private async _reauthenticate(): Promise<AuthResponse> {
1083
    try {
×
1084
      return await this._useSession(async (result) => {
×
1085
        const {
1086
          data: { session },
1087
          error: sessionError,
1088
        } = result
×
1089
        if (sessionError) throw sessionError
×
1090
        if (!session) throw new AuthSessionMissingError()
×
1091

1092
        const { error } = await _request(this.fetch, 'GET', `${this.url}/reauthenticate`, {
×
1093
          headers: this.headers,
1094
          jwt: session.access_token,
1095
        })
1096
        return { data: { user: null, session: null }, error }
×
1097
      })
1098
    } catch (error) {
1099
      if (isAuthError(error)) {
×
1100
        return { data: { user: null, session: null }, error }
×
1101
      }
1102
      throw error
×
1103
    }
1104
  }
1105

1106
  /**
1107
   * Resends an existing signup confirmation email, email change email, SMS OTP or phone change OTP.
1108
   */
1109
  async resend(credentials: ResendParams): Promise<AuthOtpResponse> {
1110
    try {
4✔
1111
      const endpoint = `${this.url}/resend`
4✔
1112
      if ('email' in credentials) {
4✔
1113
        const { email, type, options } = credentials
2✔
1114
        const { error } = await _request(this.fetch, 'POST', endpoint, {
2✔
1115
          headers: this.headers,
1116
          body: {
1117
            email,
1118
            type,
1119
            gotrue_meta_security: { captcha_token: options?.captchaToken },
6!
1120
          },
1121
          redirectTo: options?.emailRedirectTo,
6!
1122
        })
1123
        return { data: { user: null, session: null }, error }
2✔
1124
      } else if ('phone' in credentials) {
2✔
1125
        const { phone, type, options } = credentials
2✔
1126
        const { data, error } = await _request(this.fetch, 'POST', endpoint, {
2✔
1127
          headers: this.headers,
1128
          body: {
1129
            phone,
1130
            type,
1131
            gotrue_meta_security: { captcha_token: options?.captchaToken },
6!
1132
          },
1133
        })
1134
        return { data: { user: null, session: null, messageId: data?.message_id }, error }
2!
1135
      }
1136
      throw new AuthInvalidCredentialsError(
×
1137
        'You must provide either an email or phone number and a type'
1138
      )
1139
    } catch (error) {
1140
      if (isAuthError(error)) {
×
1141
        return { data: { user: null, session: null }, error }
×
1142
      }
1143
      throw error
×
1144
    }
1145
  }
1146

1147
  /**
1148
   * Returns the session, refreshing it if necessary.
1149
   *
1150
   * 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.
1151
   *
1152
   * **IMPORTANT:** This method loads values directly from the storage attached
1153
   * to the client. If that storage is based on request cookies for example,
1154
   * the values in it may not be authentic and therefore it's strongly advised
1155
   * against using this method and its results in such circumstances. A warning
1156
   * will be emitted if this is detected. Use {@link #getUser()} instead.
1157
   */
1158
  async getSession() {
1159
    await this.initializePromise
28✔
1160

1161
    const result = await this._acquireLock(-1, async () => {
28✔
1162
      return this._useSession(async (result) => {
28✔
1163
        return result
28✔
1164
      })
1165
    })
1166

1167
    return result
28✔
1168
  }
1169

1170
  /**
1171
   * Acquires a global lock based on the storage key.
1172
   */
1173
  private async _acquireLock<R>(acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
1174
    this._debug('#_acquireLock', 'begin', acquireTimeout)
250✔
1175

1176
    try {
250✔
1177
      if (this.lockAcquired) {
250!
1178
        const last = this.pendingInLock.length
×
1179
          ? this.pendingInLock[this.pendingInLock.length - 1]
1180
          : Promise.resolve()
1181

1182
        const result = (async () => {
×
1183
          await last
×
1184
          return await fn()
×
1185
        })()
1186

1187
        this.pendingInLock.push(
×
1188
          (async () => {
1189
            try {
×
1190
              await result
×
1191
            } catch (e: any) {
1192
              // we just care if it finished
1193
            }
1194
          })()
1195
        )
1196

1197
        return result
×
1198
      }
1199

1200
      return await this.lock(`lock:${this.storageKey}`, acquireTimeout, async () => {
250✔
1201
        this._debug('#_acquireLock', 'lock acquired for storage key', this.storageKey)
250✔
1202

1203
        try {
250✔
1204
          this.lockAcquired = true
250✔
1205

1206
          const result = fn()
250✔
1207

1208
          this.pendingInLock.push(
250✔
1209
            (async () => {
1210
              try {
250✔
1211
                await result
250✔
1212
              } catch (e: any) {
1213
                // we just care if it finished
1214
              }
1215
            })()
1216
          )
1217

1218
          await result
250✔
1219

1220
          // keep draining the queue until there's nothing to wait on
1221
          while (this.pendingInLock.length) {
250✔
1222
            const waitOn = [...this.pendingInLock]
250✔
1223

1224
            await Promise.all(waitOn)
250✔
1225

1226
            this.pendingInLock.splice(0, waitOn.length)
250✔
1227
          }
1228

1229
          return await result
250✔
1230
        } finally {
1231
          this._debug('#_acquireLock', 'lock released for storage key', this.storageKey)
250✔
1232

1233
          this.lockAcquired = false
250✔
1234
        }
1235
      })
1236
    } finally {
1237
      this._debug('#_acquireLock', 'end')
250✔
1238
    }
1239
  }
1240

1241
  /**
1242
   * Use instead of {@link #getSession} inside the library. It is
1243
   * semantically usually what you want, as getting a session involves some
1244
   * processing afterwards that requires only one client operating on the
1245
   * session at once across multiple tabs or processes.
1246
   */
1247
  private async _useSession<R>(
1248
    fn: (
1249
      result:
1250
        | {
1251
            data: {
1252
              session: Session
1253
            }
1254
            error: null
1255
          }
1256
        | {
1257
            data: {
1258
              session: null
1259
            }
1260
            error: AuthError
1261
          }
1262
        | {
1263
            data: {
1264
              session: null
1265
            }
1266
            error: null
1267
          }
1268
    ) => Promise<R>
1269
  ): Promise<R> {
1270
    this._debug('#_useSession', 'begin')
208✔
1271

1272
    try {
208✔
1273
      // the use of __loadSession here is the only correct use of the function!
1274
      const result = await this.__loadSession()
208✔
1275

1276
      return await fn(result)
208✔
1277
    } finally {
1278
      this._debug('#_useSession', 'end')
208✔
1279
    }
1280
  }
1281

1282
  /**
1283
   * NEVER USE DIRECTLY!
1284
   *
1285
   * Always use {@link #_useSession}.
1286
   */
1287
  private async __loadSession(): Promise<
1288
    | {
1289
        data: {
1290
          session: Session
1291
        }
1292
        error: null
1293
      }
1294
    | {
1295
        data: {
1296
          session: null
1297
        }
1298
        error: AuthError
1299
      }
1300
    | {
1301
        data: {
1302
          session: null
1303
        }
1304
        error: null
1305
      }
1306
  > {
1307
    this._debug('#__loadSession()', 'begin')
208✔
1308

1309
    if (!this.lockAcquired) {
208✔
1310
      this._debug('#__loadSession()', 'used outside of an acquired lock!', new Error().stack)
18✔
1311
    }
1312

1313
    try {
208✔
1314
      let currentSession: Session | null = null
208✔
1315

1316
      const maybeSession = await getItemAsync(this.storage, this.storageKey)
208✔
1317

1318
      this._debug('#getSession()', 'session from storage', maybeSession)
208✔
1319

1320
      if (maybeSession !== null) {
208✔
1321
        if (this._isValidSession(maybeSession)) {
118!
1322
          currentSession = maybeSession
118✔
1323
        } else {
1324
          this._debug('#getSession()', 'session from storage is not valid')
×
1325
          await this._removeSession()
×
1326
        }
1327
      }
1328

1329
      if (!currentSession) {
208✔
1330
        return { data: { session: null }, error: null }
90✔
1331
      }
1332

1333
      // A session is considered expired before the access token _actually_
1334
      // expires. When the autoRefreshToken option is off (or when the tab is
1335
      // in the background), very eager users of getSession() -- like
1336
      // realtime-js -- might send a valid JWT which will expire by the time it
1337
      // reaches the server.
1338
      const hasExpired = currentSession.expires_at
118!
1339
        ? currentSession.expires_at * 1000 - Date.now() < EXPIRY_MARGIN_MS
1340
        : false
1341

1342
      this._debug(
118✔
1343
        '#__loadSession()',
1344
        `session has${hasExpired ? '' : ' not'} expired`,
118✔
1345
        'expires_at',
1346
        currentSession.expires_at
1347
      )
1348

1349
      if (!hasExpired) {
118✔
1350
        if (this.storage.isServer) {
116✔
1351
          let suppressWarning = this.suppressGetSessionWarning
14✔
1352
          const proxySession: Session = new Proxy(currentSession, {
14✔
1353
            get: (target: any, prop: string, receiver: any) => {
1354
              if (!suppressWarning && prop === 'user') {
24✔
1355
                // only show warning when the user object is being accessed from the server
1356
                console.warn(
2✔
1357
                  '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.'
1358
                )
1359
                suppressWarning = true // keeps this proxy instance from logging additional warnings
2✔
1360
                this.suppressGetSessionWarning = true // keeps this client's future proxy instances from warning
2✔
1361
              }
1362
              return Reflect.get(target, prop, receiver)
24✔
1363
            },
1364
          })
1365
          currentSession = proxySession
14✔
1366
        }
1367

1368
        return { data: { session: currentSession }, error: null }
116✔
1369
      }
1370

1371
      const { session, error } = await this._callRefreshToken(currentSession.refresh_token)
2✔
1372
      if (error) {
2!
1373
        return { data: { session: null }, error }
×
1374
      }
1375

1376
      return { data: { session }, error: null }
2✔
1377
    } finally {
1378
      this._debug('#__loadSession()', 'end')
208✔
1379
    }
1380
  }
1381

1382
  /**
1383
   * Gets the current user details if there is an existing session. This method
1384
   * performs a network request to the Supabase Auth server, so the returned
1385
   * value is authentic and can be used to base authorization rules on.
1386
   *
1387
   * @param jwt Takes in an optional access token JWT. If no JWT is provided, the JWT from the current session is used.
1388
   */
1389
  async getUser(jwt?: string): Promise<UserResponse> {
1390
    if (jwt) {
11✔
1391
      return await this._getUser(jwt)
3✔
1392
    }
1393

1394
    await this.initializePromise
8✔
1395

1396
    const result = await this._acquireLock(-1, async () => {
8✔
1397
      return await this._getUser()
8✔
1398
    })
1399

1400
    return result
8✔
1401
  }
1402

1403
  private async _getUser(jwt?: string): Promise<UserResponse> {
1404
    try {
13✔
1405
      if (jwt) {
13✔
1406
        return await _request(this.fetch, 'GET', `${this.url}/user`, {
5✔
1407
          headers: this.headers,
1408
          jwt: jwt,
1409
          xform: _userResponse,
1410
        })
1411
      }
1412

1413
      return await this._useSession(async (result) => {
8✔
1414
        const { data, error } = result
8✔
1415
        if (error) {
8!
1416
          throw error
×
1417
        }
1418

1419
        // returns an error if there is no access_token or custom authorization header
1420
        if (!data.session?.access_token && !this.hasCustomAuthorizationHeader) {
8✔
1421
          return { data: { user: null }, error: new AuthSessionMissingError() }
2✔
1422
        }
1423

1424
        return await _request(this.fetch, 'GET', `${this.url}/user`, {
6✔
1425
          headers: this.headers,
1426
          jwt: data.session?.access_token ?? undefined,
36!
1427
          xform: _userResponse,
1428
        })
1429
      })
1430
    } catch (error) {
1431
      if (isAuthError(error)) {
×
1432
        if (isAuthSessionMissingError(error)) {
×
1433
          // JWT contains a `session_id` which does not correspond to an active
1434
          // session in the database, indicating the user is signed out.
1435

1436
          await this._removeSession()
×
1437
          await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
×
1438
        }
1439

1440
        return { data: { user: null }, error }
×
1441
      }
1442

1443
      throw error
×
1444
    }
1445
  }
1446

1447
  /**
1448
   * Updates user data for a logged in user.
1449
   */
1450
  async updateUser(
1451
    attributes: UserAttributes,
1452
    options: {
6✔
1453
      emailRedirectTo?: string | undefined
1454
    } = {}
1455
  ): Promise<UserResponse> {
1456
    await this.initializePromise
6✔
1457

1458
    return await this._acquireLock(-1, async () => {
6✔
1459
      return await this._updateUser(attributes, options)
6✔
1460
    })
1461
  }
1462

1463
  protected async _updateUser(
1464
    attributes: UserAttributes,
1465
    options: {
×
1466
      emailRedirectTo?: string | undefined
1467
    } = {}
1468
  ): Promise<UserResponse> {
1469
    try {
6✔
1470
      return await this._useSession(async (result) => {
6✔
1471
        const { data: sessionData, error: sessionError } = result
6✔
1472
        if (sessionError) {
6!
1473
          throw sessionError
×
1474
        }
1475
        if (!sessionData.session) {
6!
1476
          throw new AuthSessionMissingError()
×
1477
        }
1478
        const session: Session = sessionData.session
6✔
1479
        let codeChallenge: string | null = null
6✔
1480
        let codeChallengeMethod: string | null = null
6✔
1481
        if (this.flowType === 'pkce' && attributes.email != null) {
6!
1482
          ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
×
1483
            this.storage,
1484
            this.storageKey
1485
          )
1486
        }
1487

1488
        const { data, error: userError } = await _request(this.fetch, 'PUT', `${this.url}/user`, {
6✔
1489
          headers: this.headers,
1490
          redirectTo: options?.emailRedirectTo,
18!
1491
          body: {
1492
            ...attributes,
1493
            code_challenge: codeChallenge,
1494
            code_challenge_method: codeChallengeMethod,
1495
          },
1496
          jwt: session.access_token,
1497
          xform: _userResponse,
1498
        })
1499
        if (userError) throw userError
6!
1500
        session.user = data.user as User
6✔
1501
        await this._saveSession(session)
6✔
1502
        await this._notifyAllSubscribers('USER_UPDATED', session)
6✔
1503
        return { data: { user: session.user }, error: null }
6✔
1504
      })
1505
    } catch (error) {
1506
      if (isAuthError(error)) {
×
1507
        return { data: { user: null }, error }
×
1508
      }
1509

1510
      throw error
×
1511
    }
1512
  }
1513

1514
  /**
1515
   * 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.
1516
   * If the refresh token or access token in the current session is invalid, an error will be thrown.
1517
   * @param currentSession The current session that minimally contains an access token and refresh token.
1518
   */
1519
  async setSession(currentSession: {
1520
    access_token: string
1521
    refresh_token: string
1522
  }): Promise<AuthResponse> {
1523
    await this.initializePromise
2✔
1524

1525
    return await this._acquireLock(-1, async () => {
2✔
1526
      return await this._setSession(currentSession)
2✔
1527
    })
1528
  }
1529

1530
  protected async _setSession(currentSession: {
1531
    access_token: string
1532
    refresh_token: string
1533
  }): Promise<AuthResponse> {
1534
    try {
2✔
1535
      if (!currentSession.access_token || !currentSession.refresh_token) {
2!
1536
        throw new AuthSessionMissingError()
×
1537
      }
1538

1539
      const timeNow = Date.now() / 1000
2✔
1540
      let expiresAt = timeNow
2✔
1541
      let hasExpired = true
2✔
1542
      let session: Session | null = null
2✔
1543
      const { payload } = decodeJWT(currentSession.access_token)
2✔
1544
      if (payload.exp) {
2✔
1545
        expiresAt = payload.exp
2✔
1546
        hasExpired = expiresAt <= timeNow
2✔
1547
      }
1548

1549
      if (hasExpired) {
2!
1550
        const { session: refreshedSession, error } = await this._callRefreshToken(
×
1551
          currentSession.refresh_token
1552
        )
1553
        if (error) {
×
1554
          return { data: { user: null, session: null }, error: error }
×
1555
        }
1556

1557
        if (!refreshedSession) {
×
1558
          return { data: { user: null, session: null }, error: null }
×
1559
        }
1560
        session = refreshedSession
×
1561
      } else {
1562
        const { data, error } = await this._getUser(currentSession.access_token)
2✔
1563
        if (error) {
2!
1564
          throw error
×
1565
        }
1566
        session = {
2✔
1567
          access_token: currentSession.access_token,
1568
          refresh_token: currentSession.refresh_token,
1569
          user: data.user,
1570
          token_type: 'bearer',
1571
          expires_in: expiresAt - timeNow,
1572
          expires_at: expiresAt,
1573
        }
1574
        await this._saveSession(session)
2✔
1575
        await this._notifyAllSubscribers('SIGNED_IN', session)
2✔
1576
      }
1577

1578
      return { data: { user: session.user, session }, error: null }
2✔
1579
    } catch (error) {
1580
      if (isAuthError(error)) {
×
1581
        return { data: { session: null, user: null }, error }
×
1582
      }
1583

1584
      throw error
×
1585
    }
1586
  }
1587

1588
  /**
1589
   * Returns a new session, regardless of expiry status.
1590
   * Takes in an optional current session. If not passed in, then refreshSession() will attempt to retrieve it from getSession().
1591
   * If the current session's refresh token is invalid, an error will be thrown.
1592
   * @param currentSession The current session. If passed in, it must contain a refresh token.
1593
   */
1594
  async refreshSession(currentSession?: { refresh_token: string }): Promise<AuthResponse> {
1595
    await this.initializePromise
4✔
1596

1597
    return await this._acquireLock(-1, async () => {
4✔
1598
      return await this._refreshSession(currentSession)
4✔
1599
    })
1600
  }
1601

1602
  protected async _refreshSession(currentSession?: {
1603
    refresh_token: string
1604
  }): Promise<AuthResponse> {
1605
    try {
4✔
1606
      return await this._useSession(async (result) => {
4✔
1607
        if (!currentSession) {
4✔
1608
          const { data, error } = result
2✔
1609
          if (error) {
2!
1610
            throw error
×
1611
          }
1612

1613
          currentSession = data.session ?? undefined
2!
1614
        }
1615

1616
        if (!currentSession?.refresh_token) {
4!
1617
          throw new AuthSessionMissingError()
×
1618
        }
1619

1620
        const { session, error } = await this._callRefreshToken(currentSession.refresh_token)
4✔
1621
        if (error) {
4!
1622
          return { data: { user: null, session: null }, error: error }
×
1623
        }
1624

1625
        if (!session) {
4!
1626
          return { data: { user: null, session: null }, error: null }
×
1627
        }
1628

1629
        return { data: { user: session.user, session }, error: null }
4✔
1630
      })
1631
    } catch (error) {
1632
      if (isAuthError(error)) {
×
1633
        return { data: { user: null, session: null }, error }
×
1634
      }
1635

1636
      throw error
×
1637
    }
1638
  }
1639

1640
  /**
1641
   * Gets the session data from a URL string
1642
   */
1643
  private async _getSessionFromURL(
1644
    params: { [parameter: string]: string },
1645
    callbackUrlType: string
1646
  ): Promise<
1647
    | {
1648
        data: { session: Session; redirectType: string | null }
1649
        error: null
1650
      }
1651
    | { data: { session: null; redirectType: null }; error: AuthError }
1652
  > {
1653
    try {
2✔
1654
      if (!isBrowser()) throw new AuthImplicitGrantRedirectError('No browser detected.')
2✔
1655

1656
      // If there's an error in the URL, it doesn't matter what flow it is, we just return the error.
1657
      if (params.error || params.error_description || params.error_code) {
×
1658
        // The error class returned implies that the redirect is from an implicit grant flow
1659
        // but it could also be from a redirect error from a PKCE flow.
1660
        throw new AuthImplicitGrantRedirectError(
×
1661
          params.error_description || 'Error in URL with unspecified error_description',
×
1662
          {
1663
            error: params.error || 'unspecified_error',
×
1664
            code: params.error_code || 'unspecified_code',
×
1665
          }
1666
        )
1667
      }
1668

1669
      // Checks for mismatches between the flowType initialised in the client and the URL parameters
1670
      switch (callbackUrlType) {
×
1671
        case 'implicit':
1672
          if (this.flowType === 'pkce') {
×
1673
            throw new AuthPKCEGrantCodeExchangeError('Not a valid PKCE flow url.')
×
1674
          }
1675
          break
×
1676
        case 'pkce':
1677
          if (this.flowType === 'implicit') {
×
1678
            throw new AuthImplicitGrantRedirectError('Not a valid implicit grant flow url.')
×
1679
          }
1680
          break
×
1681
        default:
1682
        // there's no mismatch so we continue
1683
      }
1684

1685
      // Since this is a redirect for PKCE, we attempt to retrieve the code from the URL for the code exchange
1686
      if (callbackUrlType === 'pkce') {
×
1687
        this._debug('#_initialize()', 'begin', 'is PKCE flow', true)
×
1688
        if (!params.code) throw new AuthPKCEGrantCodeExchangeError('No code detected.')
×
1689
        const { data, error } = await this._exchangeCodeForSession(params.code)
×
1690
        if (error) throw error
×
1691

1692
        const url = new URL(window.location.href)
×
1693
        url.searchParams.delete('code')
×
1694

1695
        window.history.replaceState(window.history.state, '', url.toString())
×
1696

1697
        return { data: { session: data.session, redirectType: null }, error: null }
×
1698
      }
1699

1700
      const {
1701
        provider_token,
1702
        provider_refresh_token,
1703
        access_token,
1704
        refresh_token,
1705
        expires_in,
1706
        expires_at,
1707
        token_type,
1708
      } = params
×
1709

1710
      if (!access_token || !expires_in || !refresh_token || !token_type) {
×
1711
        throw new AuthImplicitGrantRedirectError('No session defined in URL')
×
1712
      }
1713

1714
      const timeNow = Math.round(Date.now() / 1000)
×
1715
      const expiresIn = parseInt(expires_in)
×
1716
      let expiresAt = timeNow + expiresIn
×
1717

1718
      if (expires_at) {
×
1719
        expiresAt = parseInt(expires_at)
×
1720
      }
1721

1722
      const actuallyExpiresIn = expiresAt - timeNow
×
1723
      if (actuallyExpiresIn * 1000 <= AUTO_REFRESH_TICK_DURATION_MS) {
×
1724
        console.warn(
×
1725
          `@supabase/gotrue-js: Session as retrieved from URL expires in ${actuallyExpiresIn}s, should have been closer to ${expiresIn}s`
1726
        )
1727
      }
1728

1729
      const issuedAt = expiresAt - expiresIn
×
1730
      if (timeNow - issuedAt >= 120) {
×
1731
        console.warn(
×
1732
          '@supabase/gotrue-js: Session as retrieved from URL was issued over 120s ago, URL could be stale',
1733
          issuedAt,
1734
          expiresAt,
1735
          timeNow
1736
        )
1737
      } else if (timeNow - issuedAt < 0) {
×
1738
        console.warn(
×
1739
          '@supabase/gotrue-js: Session as retrieved from URL was issued in the future? Check the device clock for skew',
1740
          issuedAt,
1741
          expiresAt,
1742
          timeNow
1743
        )
1744
      }
1745

1746
      const { data, error } = await this._getUser(access_token)
×
1747
      if (error) throw error
×
1748

1749
      const session: Session = {
×
1750
        provider_token,
1751
        provider_refresh_token,
1752
        access_token,
1753
        expires_in: expiresIn,
1754
        expires_at: expiresAt,
1755
        refresh_token,
1756
        token_type,
1757
        user: data.user,
1758
      }
1759

1760
      // Remove tokens from URL
1761
      window.location.hash = ''
×
1762
      this._debug('#_getSessionFromURL()', 'clearing window.location.hash')
×
1763

1764
      return { data: { session, redirectType: params.type }, error: null }
×
1765
    } catch (error) {
1766
      if (isAuthError(error)) {
2✔
1767
        return { data: { session: null, redirectType: null }, error }
2✔
1768
      }
1769

1770
      throw error
×
1771
    }
1772
  }
1773

1774
  /**
1775
   * 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)
1776
   */
1777
  private _isImplicitGrantCallback(params: { [parameter: string]: string }): boolean {
1778
    return Boolean(params.access_token || params.error_description)
×
1779
  }
1780

1781
  /**
1782
   * Checks if the current URL and backing storage contain parameters given by a PKCE flow
1783
   */
1784
  private async _isPKCECallback(params: { [parameter: string]: string }): Promise<boolean> {
1785
    const currentStorageContent = await getItemAsync(
×
1786
      this.storage,
1787
      `${this.storageKey}-code-verifier`
1788
    )
1789

1790
    return !!(params.code && currentStorageContent)
×
1791
  }
1792

1793
  /**
1794
   * 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.
1795
   *
1796
   * 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)`.
1797
   * 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.
1798
   *
1799
   * If using `others` scope, no `SIGNED_OUT` event is fired!
1800
   */
1801
  async signOut(options: SignOut = { scope: 'global' }): Promise<{ error: AuthError | null }> {
126✔
1802
    await this.initializePromise
126✔
1803

1804
    return await this._acquireLock(-1, async () => {
126✔
1805
      return await this._signOut(options)
126✔
1806
    })
1807
  }
1808

1809
  protected async _signOut(
1810
    { scope }: SignOut = { scope: 'global' }
×
1811
  ): Promise<{ error: AuthError | null }> {
1812
    return await this._useSession(async (result) => {
126✔
1813
      const { data, error: sessionError } = result
126✔
1814
      if (sessionError) {
126!
1815
        return { error: sessionError }
×
1816
      }
1817
      const accessToken = data.session?.access_token
126✔
1818
      if (accessToken) {
126✔
1819
        const { error } = await this.admin.signOut(accessToken, scope)
46✔
1820
        if (error) {
46✔
1821
          // ignore 404s since user might not exist anymore
1822
          // ignore 401s since an invalid or expired JWT should sign out the current session
1823
          if (
2!
1824
            !(
1825
              isAuthApiError(error) &&
8✔
1826
              (error.status === 404 || error.status === 401 || error.status === 403)
1827
            )
1828
          ) {
1829
            return { error }
×
1830
          }
1831
        }
1832
      }
1833
      if (scope !== 'others') {
126✔
1834
        await this._removeSession()
126✔
1835
        await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
126✔
1836
      }
1837
      return { error: null }
126✔
1838
    })
1839
  }
1840

1841
  /**
1842
   * Receive a notification every time an auth event happens.
1843
   * @param callback A callback function to be invoked when an auth event happens.
1844
   */
1845
  onAuthStateChange(
1846
    callback: (event: AuthChangeEvent, session: Session | null) => void | Promise<void>
1847
  ): {
1848
    data: { subscription: Subscription }
1849
  } {
1850
    const id: string = uuid()
2✔
1851
    const subscription: Subscription = {
2✔
1852
      id,
1853
      callback,
1854
      unsubscribe: () => {
1855
        this._debug('#unsubscribe()', 'state change callback with id removed', id)
2✔
1856

1857
        this.stateChangeEmitters.delete(id)
2✔
1858
      },
1859
    }
1860

1861
    this._debug('#onAuthStateChange()', 'registered callback with id', id)
2✔
1862

1863
    this.stateChangeEmitters.set(id, subscription)
2✔
1864
    ;(async () => {
2✔
1865
      await this.initializePromise
2✔
1866

1867
      await this._acquireLock(-1, async () => {
2✔
1868
        this._emitInitialSession(id)
2✔
1869
      })
1870
    })()
1871

1872
    return { data: { subscription } }
2✔
1873
  }
1874

1875
  private async _emitInitialSession(id: string): Promise<void> {
1876
    return await this._useSession(async (result) => {
2✔
1877
      try {
2✔
1878
        const {
1879
          data: { session },
1880
          error,
1881
        } = result
2✔
1882
        if (error) throw error
2!
1883

1884
        await this.stateChangeEmitters.get(id)?.callback('INITIAL_SESSION', session)
2!
1885
        this._debug('INITIAL_SESSION', 'callback id', id, 'session', session)
2✔
1886
      } catch (err) {
1887
        await this.stateChangeEmitters.get(id)?.callback('INITIAL_SESSION', null)
×
1888
        this._debug('INITIAL_SESSION', 'callback id', id, 'error', err)
×
1889
        console.error(err)
×
1890
      }
1891
    })
1892
  }
1893

1894
  /**
1895
   * Sends a password reset request to an email address. This method supports the PKCE flow.
1896
   *
1897
   * @param email The email address of the user.
1898
   * @param options.redirectTo The URL to send the user to after they click the password reset link.
1899
   * @param options.captchaToken Verification token received when the user completes the captcha on the site.
1900
   */
1901
  async resetPasswordForEmail(
1902
    email: string,
1903
    options: {
×
1904
      redirectTo?: string
1905
      captchaToken?: string
1906
    } = {}
1907
  ): Promise<
1908
    | {
1909
        data: {}
1910
        error: null
1911
      }
1912
    | { data: null; error: AuthError }
1913
  > {
1914
    let codeChallenge: string | null = null
4✔
1915
    let codeChallengeMethod: string | null = null
4✔
1916

1917
    if (this.flowType === 'pkce') {
4!
1918
      ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
×
1919
        this.storage,
1920
        this.storageKey,
1921
        true // isPasswordRecovery
1922
      )
1923
    }
1924
    try {
4✔
1925
      return await _request(this.fetch, 'POST', `${this.url}/recover`, {
4✔
1926
        body: {
1927
          email,
1928
          code_challenge: codeChallenge,
1929
          code_challenge_method: codeChallengeMethod,
1930
          gotrue_meta_security: { captcha_token: options.captchaToken },
1931
        },
1932
        headers: this.headers,
1933
        redirectTo: options.redirectTo,
1934
      })
1935
    } catch (error) {
1936
      if (isAuthError(error)) {
×
1937
        return { data: null, error }
×
1938
      }
1939

1940
      throw error
×
1941
    }
1942
  }
1943

1944
  /**
1945
   * Gets all the identities linked to a user.
1946
   */
1947
  async getUserIdentities(): Promise<
1948
    | {
1949
        data: {
1950
          identities: UserIdentity[]
1951
        }
1952
        error: null
1953
      }
1954
    | { data: null; error: AuthError }
1955
  > {
1956
    try {
×
1957
      const { data, error } = await this.getUser()
×
1958
      if (error) throw error
×
1959
      return { data: { identities: data.user.identities ?? [] }, error: null }
×
1960
    } catch (error) {
1961
      if (isAuthError(error)) {
×
1962
        return { data: null, error }
×
1963
      }
1964
      throw error
×
1965
    }
1966
  }
1967
  /**
1968
   * Links an oauth identity to an existing user.
1969
   * This method supports the PKCE flow.
1970
   */
1971
  async linkIdentity(credentials: SignInWithOAuthCredentials): Promise<OAuthResponse> {
1972
    try {
×
1973
      const { data, error } = await this._useSession(async (result) => {
×
1974
        const { data, error } = result
×
1975
        if (error) throw error
×
1976
        const url: string = await this._getUrlForProvider(
×
1977
          `${this.url}/user/identities/authorize`,
1978
          credentials.provider,
1979
          {
1980
            redirectTo: credentials.options?.redirectTo,
×
1981
            scopes: credentials.options?.scopes,
×
1982
            queryParams: credentials.options?.queryParams,
×
1983
            skipBrowserRedirect: true,
1984
          }
1985
        )
1986
        return await _request(this.fetch, 'GET', url, {
×
1987
          headers: this.headers,
1988
          jwt: data.session?.access_token ?? undefined,
×
1989
        })
1990
      })
1991
      if (error) throw error
×
1992
      if (isBrowser() && !credentials.options?.skipBrowserRedirect) {
×
1993
        window.location.assign(data?.url)
×
1994
      }
1995
      return { data: { provider: credentials.provider, url: data?.url }, error: null }
×
1996
    } catch (error) {
1997
      if (isAuthError(error)) {
×
1998
        return { data: { provider: credentials.provider, url: null }, error }
×
1999
      }
2000
      throw error
×
2001
    }
2002
  }
2003

2004
  /**
2005
   * 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.
2006
   */
2007
  async unlinkIdentity(identity: UserIdentity): Promise<
2008
    | {
2009
        data: {}
2010
        error: null
2011
      }
2012
    | { data: null; error: AuthError }
2013
  > {
2014
    try {
×
2015
      return await this._useSession(async (result) => {
×
2016
        const { data, error } = result
×
2017
        if (error) {
×
2018
          throw error
×
2019
        }
2020
        return await _request(
×
2021
          this.fetch,
2022
          'DELETE',
2023
          `${this.url}/user/identities/${identity.identity_id}`,
2024
          {
2025
            headers: this.headers,
2026
            jwt: data.session?.access_token ?? undefined,
×
2027
          }
2028
        )
2029
      })
2030
    } catch (error) {
2031
      if (isAuthError(error)) {
×
2032
        return { data: null, error }
×
2033
      }
2034
      throw error
×
2035
    }
2036
  }
2037

2038
  /**
2039
   * Generates a new JWT.
2040
   * @param refreshToken A valid refresh token that was returned on login.
2041
   */
2042
  private async _refreshAccessToken(refreshToken: string): Promise<AuthResponse> {
2043
    const debugName = `#_refreshAccessToken(${refreshToken.substring(0, 5)}...)`
14✔
2044
    this._debug(debugName, 'begin')
14✔
2045

2046
    try {
14✔
2047
      const startedAt = Date.now()
14✔
2048

2049
      // will attempt to refresh the token with exponential backoff
2050
      return await retryable(
14✔
2051
        async (attempt) => {
2052
          if (attempt > 0) {
14!
2053
            await sleep(200 * Math.pow(2, attempt - 1)) // 200, 400, 800, ...
×
2054
          }
2055

2056
          this._debug(debugName, 'refreshing attempt', attempt)
14✔
2057

2058
          return await _request(this.fetch, 'POST', `${this.url}/token?grant_type=refresh_token`, {
14✔
2059
            body: { refresh_token: refreshToken },
2060
            headers: this.headers,
2061
            xform: _sessionResponse,
2062
          })
2063
        },
2064
        (attempt, error) => {
2065
          const nextBackOffInterval = 200 * Math.pow(2, attempt)
14✔
2066
          return (
14✔
2067
            error &&
14!
2068
            isAuthRetryableFetchError(error) &&
2069
            // retryable only if the request can be sent before the backoff overflows the tick duration
2070
            Date.now() + nextBackOffInterval - startedAt < AUTO_REFRESH_TICK_DURATION_MS
2071
          )
2072
        }
2073
      )
2074
    } catch (error) {
2075
      this._debug(debugName, 'error', error)
×
2076

2077
      if (isAuthError(error)) {
×
2078
        return { data: { session: null, user: null }, error }
×
2079
      }
2080
      throw error
×
2081
    } finally {
2082
      this._debug(debugName, 'end')
14✔
2083
    }
2084
  }
2085

2086
  private _isValidSession(maybeSession: unknown): maybeSession is Session {
2087
    const isValidSession =
2088
      typeof maybeSession === 'object' &&
118✔
2089
      maybeSession !== null &&
2090
      'access_token' in maybeSession &&
2091
      'refresh_token' in maybeSession &&
2092
      'expires_at' in maybeSession
2093

2094
    return isValidSession
118✔
2095
  }
2096

2097
  private async _handleProviderSignIn(
2098
    provider: Provider,
2099
    options: {
2100
      redirectTo?: string
2101
      scopes?: string
2102
      queryParams?: { [key: string]: string }
2103
      skipBrowserRedirect?: boolean
2104
    }
2105
  ) {
2106
    const url: string = await this._getUrlForProvider(`${this.url}/authorize`, provider, {
8✔
2107
      redirectTo: options.redirectTo,
2108
      scopes: options.scopes,
2109
      queryParams: options.queryParams,
2110
    })
2111

2112
    this._debug('#_handleProviderSignIn()', 'provider', provider, 'options', options, 'url', url)
8✔
2113

2114
    // try to open on the browser
2115
    if (isBrowser() && !options.skipBrowserRedirect) {
8!
2116
      window.location.assign(url)
×
2117
    }
2118

2119
    return { data: { provider, url }, error: null }
8✔
2120
  }
2121

2122
  /**
2123
   * Recovers the session from LocalStorage and refreshes the token
2124
   * Note: this method is async to accommodate for AsyncStorage e.g. in React native.
2125
   */
2126
  private async _recoverAndRefresh() {
2127
    const debugName = '#_recoverAndRefresh()'
×
2128
    this._debug(debugName, 'begin')
×
2129

2130
    try {
×
2131
      const currentSession = await getItemAsync(this.storage, this.storageKey)
×
2132
      this._debug(debugName, 'session from storage', currentSession)
×
2133

2134
      if (!this._isValidSession(currentSession)) {
×
2135
        this._debug(debugName, 'session is not valid')
×
2136
        if (currentSession !== null) {
×
2137
          await this._removeSession()
×
2138
        }
2139

2140
        return
×
2141
      }
2142

2143
      const expiresWithMargin =
2144
        (currentSession.expires_at ?? Infinity) * 1000 - Date.now() < EXPIRY_MARGIN_MS
×
2145

2146
      this._debug(
×
2147
        debugName,
2148
        `session has${expiresWithMargin ? '' : ' not'} expired with margin of ${EXPIRY_MARGIN_MS}s`
×
2149
      )
2150

2151
      if (expiresWithMargin) {
×
2152
        if (this.autoRefreshToken && currentSession.refresh_token) {
×
2153
          const { error } = await this._callRefreshToken(currentSession.refresh_token)
×
2154

2155
          if (error) {
×
2156
            console.error(error)
×
2157

2158
            if (!isAuthRetryableFetchError(error)) {
×
2159
              this._debug(
×
2160
                debugName,
2161
                'refresh failed with a non-retryable error, removing the session',
2162
                error
2163
              )
2164
              await this._removeSession()
×
2165
            }
2166
          }
2167
        }
2168
      } else {
2169
        // no need to persist currentSession again, as we just loaded it from
2170
        // local storage; persisting it again may overwrite a value saved by
2171
        // another client with access to the same local storage
2172
        await this._notifyAllSubscribers('SIGNED_IN', currentSession)
×
2173
      }
2174
    } catch (err) {
2175
      this._debug(debugName, 'error', err)
×
2176

2177
      console.error(err)
×
2178
      return
×
2179
    } finally {
2180
      this._debug(debugName, 'end')
×
2181
    }
2182
  }
2183

2184
  private async _callRefreshToken(refreshToken: string): Promise<CallRefreshTokenResult> {
2185
    if (!refreshToken) {
22!
2186
      throw new AuthSessionMissingError()
×
2187
    }
2188

2189
    // refreshing is already in progress
2190
    if (this.refreshingDeferred) {
22✔
2191
      return this.refreshingDeferred.promise
6✔
2192
    }
2193

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

2196
    this._debug(debugName, 'begin')
16✔
2197

2198
    try {
16✔
2199
      this.refreshingDeferred = new Deferred<CallRefreshTokenResult>()
16✔
2200

2201
      const { data, error } = await this._refreshAccessToken(refreshToken)
16✔
2202
      if (error) throw error
14✔
2203
      if (!data.session) throw new AuthSessionMissingError()
12!
2204

2205
      await this._saveSession(data.session)
12✔
2206
      await this._notifyAllSubscribers('TOKEN_REFRESHED', data.session)
12✔
2207

2208
      const result = { session: data.session, error: null }
12✔
2209

2210
      this.refreshingDeferred.resolve(result)
12✔
2211

2212
      return result
12✔
2213
    } catch (error) {
2214
      this._debug(debugName, 'error', error)
4✔
2215

2216
      if (isAuthError(error)) {
4✔
2217
        const result = { session: null, error }
2✔
2218

2219
        if (!isAuthRetryableFetchError(error)) {
2✔
2220
          await this._removeSession()
2✔
2221
        }
2222

2223
        this.refreshingDeferred?.resolve(result)
2!
2224

2225
        return result
2✔
2226
      }
2227

2228
      this.refreshingDeferred?.reject(error)
2!
2229
      throw error
2✔
2230
    } finally {
2231
      this.refreshingDeferred = null
16✔
2232
      this._debug(debugName, 'end')
16✔
2233
    }
2234
  }
2235

2236
  private async _notifyAllSubscribers(
2237
    event: AuthChangeEvent,
2238
    session: Session | null,
2239
    broadcast = true
246✔
2240
  ) {
2241
    const debugName = `#_notifyAllSubscribers(${event})`
246✔
2242
    this._debug(debugName, 'begin', session, `broadcast = ${broadcast}`)
246✔
2243

2244
    try {
246✔
2245
      if (this.broadcastChannel && broadcast) {
246!
2246
        this.broadcastChannel.postMessage({ event, session })
×
2247
      }
2248

2249
      const errors: any[] = []
246✔
2250
      const promises = Array.from(this.stateChangeEmitters.values()).map(async (x) => {
246✔
2251
        try {
×
2252
          await x.callback(event, session)
×
2253
        } catch (e: any) {
2254
          errors.push(e)
×
2255
        }
2256
      })
2257

2258
      await Promise.all(promises)
246✔
2259

2260
      if (errors.length > 0) {
246!
2261
        for (let i = 0; i < errors.length; i += 1) {
×
2262
          console.error(errors[i])
×
2263
        }
2264

2265
        throw errors[0]
×
2266
      }
2267
    } finally {
2268
      this._debug(debugName, 'end')
246✔
2269
    }
2270
  }
2271

2272
  /**
2273
   * set currentSession and currentUser
2274
   * process to _startAutoRefreshToken if possible
2275
   */
2276
  private async _saveSession(session: Session) {
2277
    this._debug('#_saveSession()', session)
122✔
2278
    // _saveSession is always called whenever a new session has been acquired
2279
    // so we can safely suppress the warning returned by future getSession calls
2280
    this.suppressGetSessionWarning = true
122✔
2281
    await setItemAsync(this.storage, this.storageKey, session)
122✔
2282
  }
2283

2284
  private async _removeSession() {
2285
    this._debug('#_removeSession()')
128✔
2286

2287
    await removeItemAsync(this.storage, this.storageKey)
128✔
2288
    await this._notifyAllSubscribers('SIGNED_OUT', null)
128✔
2289
  }
2290

2291
  /**
2292
   * Removes any registered visibilitychange callback.
2293
   *
2294
   * {@see #startAutoRefresh}
2295
   * {@see #stopAutoRefresh}
2296
   */
2297
  private _removeVisibilityChangedCallback() {
2298
    this._debug('#_removeVisibilityChangedCallback()')
4✔
2299

2300
    const callback = this.visibilityChangedCallback
4✔
2301
    this.visibilityChangedCallback = null
4✔
2302

2303
    try {
4✔
2304
      if (callback && isBrowser() && window?.removeEventListener) {
4!
2305
        window.removeEventListener('visibilitychange', callback)
×
2306
      }
2307
    } catch (e) {
2308
      console.error('removing visibilitychange callback failed', e)
×
2309
    }
2310
  }
2311

2312
  /**
2313
   * This is the private implementation of {@link #startAutoRefresh}. Use this
2314
   * within the library.
2315
   */
2316
  private async _startAutoRefresh() {
2317
    await this._stopAutoRefresh()
4✔
2318

2319
    this._debug('#_startAutoRefresh()')
4✔
2320

2321
    const ticker = setInterval(() => this._autoRefreshTokenTick(), AUTO_REFRESH_TICK_DURATION_MS)
4✔
2322
    this.autoRefreshTicker = ticker
4✔
2323

2324
    if (ticker && typeof ticker === 'object' && typeof ticker.unref === 'function') {
4!
2325
      // ticker is a NodeJS Timeout object that has an `unref` method
2326
      // https://nodejs.org/api/timers.html#timeoutunref
2327
      // When auto refresh is used in NodeJS (like for testing) the
2328
      // `setInterval` is preventing the process from being marked as
2329
      // finished and tests run endlessly. This can be prevented by calling
2330
      // `unref()` on the returned object.
2331
      ticker.unref()
4✔
2332
      // @ts-expect-error TS has no context of Deno
2333
    } else if (typeof Deno !== 'undefined' && typeof Deno.unrefTimer === 'function') {
×
2334
      // similar like for NodeJS, but with the Deno API
2335
      // https://deno.land/api@latest?unstable&s=Deno.unrefTimer
2336
      // @ts-expect-error TS has no context of Deno
2337
      Deno.unrefTimer(ticker)
×
2338
    }
2339

2340
    // run the tick immediately, but in the next pass of the event loop so that
2341
    // #_initialize can be allowed to complete without recursively waiting on
2342
    // itself
2343
    setTimeout(async () => {
4✔
2344
      await this.initializePromise
4✔
2345
      await this._autoRefreshTokenTick()
4✔
2346
    }, 0)
2347
  }
2348

2349
  /**
2350
   * This is the private implementation of {@link #stopAutoRefresh}. Use this
2351
   * within the library.
2352
   */
2353
  private async _stopAutoRefresh() {
2354
    this._debug('#_stopAutoRefresh()')
4✔
2355

2356
    const ticker = this.autoRefreshTicker
4✔
2357
    this.autoRefreshTicker = null
4✔
2358

2359
    if (ticker) {
4!
2360
      clearInterval(ticker)
×
2361
    }
2362
  }
2363

2364
  /**
2365
   * Starts an auto-refresh process in the background. The session is checked
2366
   * every few seconds. Close to the time of expiration a process is started to
2367
   * refresh the session. If refreshing fails it will be retried for as long as
2368
   * necessary.
2369
   *
2370
   * If you set the {@link GoTrueClientOptions#autoRefreshToken} you don't need
2371
   * to call this function, it will be called for you.
2372
   *
2373
   * On browsers the refresh process works only when the tab/window is in the
2374
   * foreground to conserve resources as well as prevent race conditions and
2375
   * flooding auth with requests. If you call this method any managed
2376
   * visibility change callback will be removed and you must manage visibility
2377
   * changes on your own.
2378
   *
2379
   * On non-browser platforms the refresh process works *continuously* in the
2380
   * background, which may not be desirable. You should hook into your
2381
   * platform's foreground indication mechanism and call these methods
2382
   * appropriately to conserve resources.
2383
   *
2384
   * {@see #stopAutoRefresh}
2385
   */
2386
  async startAutoRefresh() {
2387
    this._removeVisibilityChangedCallback()
4✔
2388
    await this._startAutoRefresh()
4✔
2389
  }
2390

2391
  /**
2392
   * Stops an active auto refresh process running in the background (if any).
2393
   *
2394
   * If you call this method any managed visibility change callback will be
2395
   * removed and you must manage visibility changes on your own.
2396
   *
2397
   * See {@link #startAutoRefresh} for more details.
2398
   */
2399
  async stopAutoRefresh() {
2400
    this._removeVisibilityChangedCallback()
×
2401
    await this._stopAutoRefresh()
×
2402
  }
2403

2404
  /**
2405
   * Runs the auto refresh token tick.
2406
   */
2407
  private async _autoRefreshTokenTick() {
2408
    this._debug('#_autoRefreshTokenTick()', 'begin')
4✔
2409

2410
    try {
4✔
2411
      await this._acquireLock(0, async () => {
4✔
2412
        try {
4✔
2413
          const now = Date.now()
4✔
2414

2415
          try {
4✔
2416
            return await this._useSession(async (result) => {
4✔
2417
              const {
2418
                data: { session },
2419
              } = result
4✔
2420

2421
              if (!session || !session.refresh_token || !session.expires_at) {
4!
2422
                this._debug('#_autoRefreshTokenTick()', 'no session')
×
2423
                return
×
2424
              }
2425

2426
              // session will expire in this many ticks (or has already expired if <= 0)
2427
              const expiresInTicks = Math.floor(
4✔
2428
                (session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS
2429
              )
2430

2431
              this._debug(
4✔
2432
                '#_autoRefreshTokenTick()',
2433
                `access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks`
2434
              )
2435

2436
              if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) {
4!
2437
                await this._callRefreshToken(session.refresh_token)
×
2438
              }
2439
            })
2440
          } catch (e: any) {
2441
            console.error(
×
2442
              'Auto refresh tick failed with error. This is likely a transient error.',
2443
              e
2444
            )
2445
          }
2446
        } finally {
2447
          this._debug('#_autoRefreshTokenTick()', 'end')
4✔
2448
        }
2449
      })
2450
    } catch (e: any) {
2451
      if (e.isAcquireTimeout || e instanceof LockAcquireTimeoutError) {
×
2452
        this._debug('auto refresh token tick lock not available')
×
2453
      } else {
2454
        throw e
×
2455
      }
2456
    }
2457
  }
2458

2459
  /**
2460
   * Registers callbacks on the browser / platform, which in-turn run
2461
   * algorithms when the browser window/tab are in foreground. On non-browser
2462
   * platforms it assumes always foreground.
2463
   */
2464
  private async _handleVisibilityChange() {
2465
    this._debug('#_handleVisibilityChange()')
56✔
2466

2467
    if (!isBrowser() || !window?.addEventListener) {
56!
2468
      if (this.autoRefreshToken) {
56✔
2469
        // in non-browser environments the refresh token ticker runs always
2470
        this.startAutoRefresh()
4✔
2471
      }
2472

2473
      return false
56✔
2474
    }
2475

2476
    try {
×
2477
      this.visibilityChangedCallback = async () => await this._onVisibilityChanged(false)
×
2478

2479
      window?.addEventListener('visibilitychange', this.visibilityChangedCallback)
×
2480

2481
      // now immediately call the visbility changed callback to setup with the
2482
      // current visbility state
2483
      await this._onVisibilityChanged(true) // initial call
×
2484
    } catch (error) {
2485
      console.error('_handleVisibilityChange', error)
×
2486
    }
2487
  }
2488

2489
  /**
2490
   * Callback registered with `window.addEventListener('visibilitychange')`.
2491
   */
2492
  private async _onVisibilityChanged(calledFromInitialize: boolean) {
2493
    const methodName = `#_onVisibilityChanged(${calledFromInitialize})`
×
2494
    this._debug(methodName, 'visibilityState', document.visibilityState)
×
2495

2496
    if (document.visibilityState === 'visible') {
×
2497
      if (this.autoRefreshToken) {
×
2498
        // in browser environments the refresh token ticker runs only on focused tabs
2499
        // which prevents race conditions
2500
        this._startAutoRefresh()
×
2501
      }
2502

2503
      if (!calledFromInitialize) {
×
2504
        // called when the visibility has changed, i.e. the browser
2505
        // transitioned from hidden -> visible so we need to see if the session
2506
        // should be recovered immediately... but to do that we need to acquire
2507
        // the lock first asynchronously
2508
        await this.initializePromise
×
2509

2510
        await this._acquireLock(-1, async () => {
×
2511
          if (document.visibilityState !== 'visible') {
×
2512
            this._debug(
×
2513
              methodName,
2514
              'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting'
2515
            )
2516

2517
            // visibility has changed while waiting for the lock, abort
2518
            return
×
2519
          }
2520

2521
          // recover the session
2522
          await this._recoverAndRefresh()
×
2523
        })
2524
      }
2525
    } else if (document.visibilityState === 'hidden') {
×
2526
      if (this.autoRefreshToken) {
×
2527
        this._stopAutoRefresh()
×
2528
      }
2529
    }
2530
  }
2531

2532
  /**
2533
   * Generates the relevant login URL for a third-party provider.
2534
   * @param options.redirectTo A URL or mobile address to send the user to after they are confirmed.
2535
   * @param options.scopes A space-separated list of scopes granted to the OAuth application.
2536
   * @param options.queryParams An object of key-value pairs containing query parameters granted to the OAuth application.
2537
   */
2538
  private async _getUrlForProvider(
2539
    url: string,
2540
    provider: Provider,
2541
    options: {
2542
      redirectTo?: string
2543
      scopes?: string
2544
      queryParams?: { [key: string]: string }
2545
      skipBrowserRedirect?: boolean
2546
    }
2547
  ) {
2548
    const urlParams: string[] = [`provider=${encodeURIComponent(provider)}`]
8✔
2549
    if (options?.redirectTo) {
8!
2550
      urlParams.push(`redirect_to=${encodeURIComponent(options.redirectTo)}`)
4✔
2551
    }
2552
    if (options?.scopes) {
8!
2553
      urlParams.push(`scopes=${encodeURIComponent(options.scopes)}`)
4✔
2554
    }
2555
    if (this.flowType === 'pkce') {
8!
2556
      const [codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
×
2557
        this.storage,
2558
        this.storageKey
2559
      )
2560

2561
      const flowParams = new URLSearchParams({
×
2562
        code_challenge: `${encodeURIComponent(codeChallenge)}`,
2563
        code_challenge_method: `${encodeURIComponent(codeChallengeMethod)}`,
2564
      })
2565
      urlParams.push(flowParams.toString())
×
2566
    }
2567
    if (options?.queryParams) {
8!
2568
      const query = new URLSearchParams(options.queryParams)
×
2569
      urlParams.push(query.toString())
×
2570
    }
2571
    if (options?.skipBrowserRedirect) {
8!
2572
      urlParams.push(`skip_http_redirect=${options.skipBrowserRedirect}`)
×
2573
    }
2574

2575
    return `${url}?${urlParams.join('&')}`
8✔
2576
  }
2577

2578
  private async _unenroll(params: MFAUnenrollParams): Promise<AuthMFAUnenrollResponse> {
2579
    try {
2✔
2580
      return await this._useSession(async (result) => {
2✔
2581
        const { data: sessionData, error: sessionError } = result
2✔
2582
        if (sessionError) {
2!
2583
          return { data: null, error: sessionError }
×
2584
        }
2585

2586
        return await _request(this.fetch, 'DELETE', `${this.url}/factors/${params.factorId}`, {
2✔
2587
          headers: this.headers,
2588
          jwt: sessionData?.session?.access_token,
12!
2589
        })
2590
      })
2591
    } catch (error) {
2592
      if (isAuthError(error)) {
×
2593
        return { data: null, error }
×
2594
      }
2595
      throw error
×
2596
    }
2597
  }
2598

2599
  /**
2600
   * {@see GoTrueMFAApi#enroll}
2601
   */
2602
  private async _enroll(params: MFAEnrollTOTPParams): Promise<AuthMFAEnrollTOTPResponse>
2603
  private async _enroll(params: MFAEnrollPhoneParams): Promise<AuthMFAEnrollPhoneResponse>
2604
  private async _enroll(params: MFAEnrollParams): Promise<AuthMFAEnrollResponse> {
2605
    try {
16✔
2606
      return await this._useSession(async (result) => {
16✔
2607
        const { data: sessionData, error: sessionError } = result
16✔
2608
        if (sessionError) {
16!
2609
          return { data: null, error: sessionError }
×
2610
        }
2611

2612
        const body = {
16✔
2613
          friendly_name: params.friendlyName,
2614
          factor_type: params.factorType,
2615
          ...(params.factorType === 'phone' ? { phone: params.phone } : { issuer: params.issuer }),
16!
2616
        }
2617

2618
        const { data, error } = await _request(this.fetch, 'POST', `${this.url}/factors`, {
16✔
2619
          body,
2620
          headers: this.headers,
2621
          jwt: sessionData?.session?.access_token,
94!
2622
        })
2623

2624
        if (error) {
14!
2625
          return { data: null, error }
×
2626
        }
2627

2628
        if (params.factorType === 'totp' && data?.totp?.qr_code) {
14!
2629
          data.totp.qr_code = `data:image/svg+xml;utf-8,${data.totp.qr_code}`
14✔
2630
        }
2631

2632
        return { data, error: null }
14✔
2633
      })
2634
    } catch (error) {
2635
      if (isAuthError(error)) {
2✔
2636
        return { data: null, error }
2✔
2637
      }
2638
      throw error
×
2639
    }
2640
  }
2641

2642
  /**
2643
   * {@see GoTrueMFAApi#verify}
2644
   */
2645
  private async _verify(params: MFAVerifyParams): Promise<AuthMFAVerifyResponse> {
2646
    return this._acquireLock(-1, async () => {
4✔
2647
      try {
4✔
2648
        return await this._useSession(async (result) => {
4✔
2649
          const { data: sessionData, error: sessionError } = result
4✔
2650
          if (sessionError) {
4!
2651
            return { data: null, error: sessionError }
×
2652
          }
2653

2654
          const { data, error } = await _request(
4✔
2655
            this.fetch,
2656
            'POST',
2657
            `${this.url}/factors/${params.factorId}/verify`,
2658
            {
2659
              body: { code: params.code, challenge_id: params.challengeId },
2660
              headers: this.headers,
2661
              jwt: sessionData?.session?.access_token,
24!
2662
            }
2663
          )
2664
          if (error) {
×
2665
            return { data: null, error }
×
2666
          }
2667

2668
          await this._saveSession({
×
2669
            expires_at: Math.round(Date.now() / 1000) + data.expires_in,
2670
            ...data,
2671
          })
2672
          await this._notifyAllSubscribers('MFA_CHALLENGE_VERIFIED', data)
×
2673

2674
          return { data, error }
×
2675
        })
2676
      } catch (error) {
2677
        if (isAuthError(error)) {
4✔
2678
          return { data: null, error }
4✔
2679
        }
2680
        throw error
×
2681
      }
2682
    })
2683
  }
2684

2685
  /**
2686
   * {@see GoTrueMFAApi#challenge}
2687
   */
2688
  private async _challenge(params: MFAChallengeParams): Promise<AuthMFAChallengeResponse> {
2689
    return this._acquireLock(-1, async () => {
6✔
2690
      try {
6✔
2691
        return await this._useSession(async (result) => {
6✔
2692
          const { data: sessionData, error: sessionError } = result
6✔
2693
          if (sessionError) {
6!
2694
            return { data: null, error: sessionError }
×
2695
          }
2696

2697
          return await _request(
6✔
2698
            this.fetch,
2699
            'POST',
2700
            `${this.url}/factors/${params.factorId}/challenge`,
2701
            {
2702
              body: { channel: params.channel },
2703
              headers: this.headers,
2704
              jwt: sessionData?.session?.access_token,
36!
2705
            }
2706
          )
2707
        })
2708
      } catch (error) {
2709
        if (isAuthError(error)) {
×
2710
          return { data: null, error }
×
2711
        }
2712
        throw error
×
2713
      }
2714
    })
2715
  }
2716

2717
  /**
2718
   * {@see GoTrueMFAApi#challengeAndVerify}
2719
   */
2720
  private async _challengeAndVerify(
2721
    params: MFAChallengeAndVerifyParams
2722
  ): Promise<AuthMFAVerifyResponse> {
2723
    // both _challenge and _verify independently acquire the lock, so no need
2724
    // to acquire it here
2725

2726
    const { data: challengeData, error: challengeError } = await this._challenge({
2✔
2727
      factorId: params.factorId,
2728
    })
2729
    if (challengeError) {
2!
2730
      return { data: null, error: challengeError }
×
2731
    }
2732

2733
    return await this._verify({
2✔
2734
      factorId: params.factorId,
2735
      challengeId: challengeData.id,
2736
      code: params.code,
2737
    })
2738
  }
2739

2740
  /**
2741
   * {@see GoTrueMFAApi#listFactors}
2742
   */
2743
  private async _listFactors(): Promise<AuthMFAListFactorsResponse> {
2744
    // use #getUser instead of #_getUser as the former acquires a lock
2745
    const {
2746
      data: { user },
2747
      error: userError,
2748
    } = await this.getUser()
2✔
2749
    if (userError) {
2!
2750
      return { data: null, error: userError }
×
2751
    }
2752

2753
    const factors = user?.factors || []
2!
2754
    const totp = factors.filter(
2✔
2755
      (factor) => factor.factor_type === 'totp' && factor.status === 'verified'
×
2756
    )
2757
    const phone = factors.filter(
2✔
2758
      (factor) => factor.factor_type === 'phone' && factor.status === 'verified'
×
2759
    )
2760

2761
    return {
2✔
2762
      data: {
2763
        all: factors,
2764
        totp,
2765
        phone,
2766
      },
2767
      error: null,
2768
    }
2769
  }
2770

2771
  /**
2772
   * {@see GoTrueMFAApi#getAuthenticatorAssuranceLevel}
2773
   */
2774
  private async _getAuthenticatorAssuranceLevel(): Promise<AuthMFAGetAuthenticatorAssuranceLevelResponse> {
2775
    return this._acquireLock(-1, async () => {
2✔
2776
      return await this._useSession(async (result) => {
2✔
2777
        const {
2778
          data: { session },
2779
          error: sessionError,
2780
        } = result
2✔
2781
        if (sessionError) {
2!
2782
          return { data: null, error: sessionError }
×
2783
        }
2784
        if (!session) {
2!
2785
          return {
×
2786
            data: { currentLevel: null, nextLevel: null, currentAuthenticationMethods: [] },
2787
            error: null,
2788
          }
2789
        }
2790

2791
        const { payload } = decodeJWT(session.access_token)
2✔
2792

2793
        let currentLevel: AuthenticatorAssuranceLevels | null = null
2✔
2794

2795
        if (payload.aal) {
2✔
2796
          currentLevel = payload.aal
2✔
2797
        }
2798

2799
        let nextLevel: AuthenticatorAssuranceLevels | null = currentLevel
2✔
2800

2801
        const verifiedFactors =
2802
          session.user.factors?.filter((factor: Factor) => factor.status === 'verified') ?? []
2!
2803

2804
        if (verifiedFactors.length > 0) {
2!
2805
          nextLevel = 'aal2'
×
2806
        }
2807

2808
        const currentAuthenticationMethods = payload.amr || []
2!
2809

2810
        return { data: { currentLevel, nextLevel, currentAuthenticationMethods }, error: null }
2✔
2811
      })
2812
    })
2813
  }
2814

2815
  private async fetchJwk(kid: string, jwks: { keys: JWK[] } = { keys: [] }): Promise<JWK> {
8✔
2816
    // try fetching from the supplied jwks
2817
    let jwk = jwks.keys.find((key) => key.kid === kid)
9✔
2818
    if (jwk) {
9!
2819
      return jwk
×
2820
    }
2821

2822
    // try fetching from cache
2823
    jwk = this.jwks.keys.find((key) => key.kid === kid)
9✔
2824

2825
    // jwk exists and jwks isn't stale
2826
    if (jwk && this.jwks_cached_at + JWKS_TTL > Date.now()) {
9✔
2827
      return jwk
2✔
2828
    }
2829
    // jwk isn't cached in memory so we need to fetch it from the well-known endpoint
2830
    const { data, error } = await _request(this.fetch, 'GET', `${this.url}/.well-known/jwks.json`, {
7✔
2831
      headers: this.headers,
2832
    })
2833
    if (error) {
7!
2834
      throw error
×
2835
    }
2836
    if (!data.keys || data.keys.length === 0) {
7!
2837
      throw new AuthInvalidJwtError('JWKS is empty')
×
2838
    }
2839
    this.jwks = data
7✔
2840
    this.jwks_cached_at = Date.now()
7✔
2841
    // Find the signing key
2842
    jwk = data.keys.find((key: any) => key.kid === kid)
7✔
2843
    if (!jwk) {
7!
2844
      throw new AuthInvalidJwtError('No matching signing key found in JWKS')
×
2845
    }
2846
    return jwk
7✔
2847
  }
2848

2849
  /**
2850
   * @experimental This method may change in future versions.
2851
   * @description Gets the claims from a JWT. If the JWT is symmetric JWTs, it will call getUser() to verify against the server. If the JWT is asymmetric, it will be verified against the JWKS using the WebCrypto API.
2852
   */
2853
  async getClaims(
2854
    jwt?: string,
2855
    jwks: { keys: JWK[] } = { keys: [] }
6✔
2856
  ): Promise<
2857
    | {
2858
        data: { claims: JwtPayload; header: JwtHeader; signature: Uint8Array }
2859
        error: null
2860
      }
2861
    | { data: null; error: AuthError }
2862
    | { data: null; error: null }
2863
  > {
2864
    try {
6✔
2865
      let token = jwt
6✔
2866
      if (!token) {
6✔
2867
        const { data, error } = await this.getSession()
6✔
2868
        if (error || !data.session) {
6✔
2869
          return { data: null, error }
2✔
2870
        }
2871
        token = data.session.access_token
4✔
2872
      }
2873

2874
      const {
2875
        header,
2876
        payload,
2877
        signature,
2878
        raw: { header: rawHeader, payload: rawPayload },
2879
      } = decodeJWT(token)
4✔
2880

2881
      // Reject expired JWTs
2882
      validateExp(payload.exp)
4✔
2883

2884
      // If symmetric algorithm or WebCrypto API is unavailable, fallback to getUser()
2885
      if (
4✔
2886
        !header.kid ||
8✔
2887
        header.alg === 'HS256' ||
2888
        !('crypto' in globalThis && 'subtle' in globalThis.crypto)
3✔
2889
      ) {
2890
        const { error } = await this.getUser(token)
3✔
2891
        if (error) {
3!
2892
          throw error
×
2893
        }
2894
        // getUser succeeds so the claims in the JWT can be trusted
2895
        return {
3✔
2896
          data: {
2897
            claims: payload,
2898
            header,
2899
            signature,
2900
          },
2901
          error: null,
2902
        }
2903
      }
2904

2905
      const algorithm = getAlgorithm(header.alg)
1✔
2906
      const signingKey = await this.fetchJwk(header.kid, jwks)
1✔
2907

2908
      // Convert JWK to CryptoKey
2909
      const publicKey = await crypto.subtle.importKey('jwk', signingKey, algorithm, true, [
1✔
2910
        'verify',
2911
      ])
2912

2913
      // Verify the signature
2914
      const isValid = await crypto.subtle.verify(
1✔
2915
        algorithm,
2916
        publicKey,
2917
        signature,
2918
        stringToUint8Array(`${rawHeader}.${rawPayload}`)
2919
      )
2920

2921
      if (!isValid) {
1!
2922
        throw new AuthInvalidJwtError('Invalid JWT signature')
×
2923
      }
2924

2925
      // If verification succeeds, decode and return claims
2926
      return {
1✔
2927
        data: {
2928
          claims: payload,
2929
          header,
2930
          signature,
2931
        },
2932
        error: null,
2933
      }
2934
    } catch (error) {
2935
      if (isAuthError(error)) {
×
2936
        return { data: null, error }
×
2937
      }
2938
      throw error
×
2939
    }
2940
  }
2941
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc