@supabase/gotrue-js 2.108.2 → 2.112.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +6 -2
  2. package/dist/main/GoTrueAdminApi.d.ts +1 -4
  3. package/dist/main/GoTrueAdminApi.d.ts.map +1 -1
  4. package/dist/main/GoTrueAdminApi.js +1 -4
  5. package/dist/main/GoTrueAdminApi.js.map +1 -1
  6. package/dist/main/GoTrueClient.d.ts +67 -21
  7. package/dist/main/GoTrueClient.d.ts.map +1 -1
  8. package/dist/main/GoTrueClient.js +217 -68
  9. package/dist/main/GoTrueClient.js.map +1 -1
  10. package/dist/main/lib/constants.d.ts +13 -0
  11. package/dist/main/lib/constants.d.ts.map +1 -1
  12. package/dist/main/lib/constants.js +14 -1
  13. package/dist/main/lib/constants.js.map +1 -1
  14. package/dist/main/lib/errors.d.ts +1 -1
  15. package/dist/main/lib/fetch.d.ts.map +1 -1
  16. package/dist/main/lib/fetch.js +12 -6
  17. package/dist/main/lib/fetch.js.map +1 -1
  18. package/dist/main/lib/helpers.d.ts +50 -1
  19. package/dist/main/lib/helpers.d.ts.map +1 -1
  20. package/dist/main/lib/helpers.js +161 -5
  21. package/dist/main/lib/helpers.js.map +1 -1
  22. package/dist/main/lib/locks.d.ts +1 -1
  23. package/dist/main/lib/locks.js +1 -1
  24. package/dist/main/lib/types.d.ts +71 -9
  25. package/dist/main/lib/types.d.ts.map +1 -1
  26. package/dist/main/lib/types.js.map +1 -1
  27. package/dist/main/lib/version.d.ts +1 -1
  28. package/dist/main/lib/version.js +1 -1
  29. package/dist/module/GoTrueAdminApi.d.ts +1 -4
  30. package/dist/module/GoTrueAdminApi.d.ts.map +1 -1
  31. package/dist/module/GoTrueAdminApi.js +1 -4
  32. package/dist/module/GoTrueAdminApi.js.map +1 -1
  33. package/dist/module/GoTrueClient.d.ts +67 -21
  34. package/dist/module/GoTrueClient.d.ts.map +1 -1
  35. package/dist/module/GoTrueClient.js +219 -70
  36. package/dist/module/GoTrueClient.js.map +1 -1
  37. package/dist/module/lib/constants.d.ts +13 -0
  38. package/dist/module/lib/constants.d.ts.map +1 -1
  39. package/dist/module/lib/constants.js +13 -0
  40. package/dist/module/lib/constants.js.map +1 -1
  41. package/dist/module/lib/errors.d.ts +1 -1
  42. package/dist/module/lib/fetch.d.ts.map +1 -1
  43. package/dist/module/lib/fetch.js +12 -6
  44. package/dist/module/lib/fetch.js.map +1 -1
  45. package/dist/module/lib/helpers.d.ts +50 -1
  46. package/dist/module/lib/helpers.d.ts.map +1 -1
  47. package/dist/module/lib/helpers.js +153 -5
  48. package/dist/module/lib/helpers.js.map +1 -1
  49. package/dist/module/lib/locks.d.ts +1 -1
  50. package/dist/module/lib/locks.js +1 -1
  51. package/dist/module/lib/types.d.ts +71 -9
  52. package/dist/module/lib/types.d.ts.map +1 -1
  53. package/dist/module/lib/types.js.map +1 -1
  54. package/dist/module/lib/version.d.ts +1 -1
  55. package/dist/module/lib/version.js +1 -1
  56. package/dist/tsconfig.module.tsbuildinfo +1 -1
  57. package/dist/tsconfig.tsbuildinfo +1 -1
  58. package/package.json +4 -4
  59. package/src/GoTrueAdminApi.ts +1 -4
  60. package/src/GoTrueClient.ts +268 -91
  61. package/src/lib/constants.ts +15 -0
  62. package/src/lib/errors.ts +1 -1
  63. package/src/lib/fetch.ts +13 -8
  64. package/src/lib/helpers.ts +198 -6
  65. package/src/lib/locks.ts +1 -1
  66. package/src/lib/types.ts +71 -9
  67. package/src/lib/version.ts +1 -1
@@ -6,6 +6,7 @@ import {
6
6
  EXPIRY_MARGIN_MS,
7
7
  GOTRUE_URL,
8
8
  JWKS_TTL,
9
+ PKCE_FLOW_ID_PARAM,
9
10
  REFRESH_FAILURE_COOLDOWN_MS,
10
11
  STORAGE_KEY,
11
12
  } from './lib/constants'
@@ -36,6 +37,7 @@ import {
36
37
  _userResponse,
37
38
  } from './lib/fetch'
38
39
  import {
40
+ appendFlowIdToRedirectTo,
39
41
  assertPasskeyExperimentalEnabled,
40
42
  decodeJWT,
41
43
  deepClone,
@@ -47,14 +49,19 @@ import {
47
49
  insecureUserWarningProxy,
48
50
  isBrowser,
49
51
  parseParametersFromURL,
52
+ pkceVerifierSlotKey,
53
+ removeAllPKCEVerifiers,
50
54
  removeItemAsync,
55
+ removePKCEVerifier,
51
56
  resolveFetch,
57
+ retrievePKCEVerifier,
52
58
  retryable,
53
59
  setItemAsync,
54
60
  sleep,
55
61
  supportsLocalStorage,
56
62
  userNotAvailableProxy,
57
63
  validateExp,
64
+ validatePKCEFlowId,
58
65
  } from './lib/helpers'
59
66
  import { memoryLocalStorageAdapter } from './lib/local-storage'
60
67
  import { LockAcquireTimeoutError, navigatorLock } from './lib/locks'
@@ -323,6 +330,23 @@ export default class GoTrueClient {
323
330
  * Keep extra care to never reject or throw uncaught errors
324
331
  */
325
332
  protected initializePromise: Promise<InitializeResult> | null = null
333
+ /**
334
+ * Non-null only while `initialize()` is running. While open,
335
+ * `_notifyAllSubscribers` enqueues init-chain notifications (those fired with
336
+ * `broadcast = true`, i.e. from `_recoverAndRefresh`) into this array instead
337
+ * of firing directly, so that `initializePromise` is guaranteed to be
338
+ * resolved before any subscriber callback runs. Callbacks that call
339
+ * `getSession()` / `getUser()` etc. would otherwise deadlock because those
340
+ * methods await `initializePromise`. Notifications from the incoming
341
+ * BroadcastChannel handler (`broadcast = false`) are not enqueued — they fire
342
+ * immediately. Flushed (in order) by `initialize()` after `initializePromise`
343
+ * settles.
344
+ */
345
+ private _pendingInitNotifications: Array<{
346
+ event: AuthChangeEvent
347
+ session: Session | null
348
+ broadcast: boolean
349
+ }> | null = null
326
350
  protected detectSessionInUrl:
327
351
  | boolean
328
352
  | ((url: URL, params: { [parameter: string]: string }) => boolean) = true
@@ -584,6 +608,16 @@ export default class GoTrueClient {
584
608
  return await this.initializePromise
585
609
  }
586
610
 
611
+ // Open the notification queue before _initialize() runs so that every
612
+ // _notifyAllSubscribers call inside the init chain enqueues instead of
613
+ // firing. Without this, a callback receiving SIGNED_IN (or TOKEN_REFRESHED
614
+ // / SIGNED_OUT) during _recoverAndRefresh would deadlock if it called
615
+ // getSession() / getUser() — those methods await initializePromise, which
616
+ // can only resolve after the callback returns, which can only return after
617
+ // getSession() resolves. The queue is flushed below after initializePromise
618
+ // has settled, so callbacks run with a fully resolved initializePromise.
619
+ this._pendingInitNotifications = []
620
+
587
621
  this.initializePromise = (async () => {
588
622
  if (this.lock != null) {
589
623
  // TODO(v3): remove legacy lock path
@@ -594,7 +628,17 @@ export default class GoTrueClient {
594
628
  return await this._initialize()
595
629
  })()
596
630
 
597
- return await this.initializePromise
631
+ const result = await this.initializePromise
632
+
633
+ // initializePromise is now resolved — flush queued notifications in order.
634
+ // Callbacks can safely call getSession() / getUser() / signOut() etc.
635
+ const queue = this._pendingInitNotifications ?? []
636
+ this._pendingInitNotifications = null
637
+ for (const n of queue) {
638
+ await this._notifyAllSubscribers(n.event, n.session, n.broadcast)
639
+ }
640
+
641
+ return result
598
642
  }
599
643
 
600
644
  /**
@@ -968,6 +1012,7 @@ export default class GoTrueClient {
968
1012
  * ```
969
1013
  */
970
1014
  async signUp(credentials: SignUpWithPasswordCredentials): Promise<AuthResponse> {
1015
+ let flowId: string | null = null
971
1016
  try {
972
1017
  let res: AuthResponse
973
1018
  if ('email' in credentials) {
@@ -975,14 +1020,11 @@ export default class GoTrueClient {
975
1020
  let codeChallenge: string | null = null
976
1021
  let codeChallengeMethod: string | null = null
977
1022
  if (this.flowType === 'pkce') {
978
- ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
979
- this.storage,
980
- this.storageKey
981
- )
1023
+ ;[codeChallenge, codeChallengeMethod, flowId] = await this._getCodeChallengeAndMethod()
982
1024
  }
983
1025
  res = await _request(this.fetch, 'POST', `${this.url}/signup`, {
984
1026
  headers: this.headers,
985
- redirectTo: options?.emailRedirectTo,
1027
+ redirectTo: this._maybeAppendFlowIdToRedirect(options?.emailRedirectTo, flowId),
986
1028
  body: {
987
1029
  email,
988
1030
  password,
@@ -1015,7 +1057,7 @@ export default class GoTrueClient {
1015
1057
  const { data, error } = res
1016
1058
 
1017
1059
  if (error || !data) {
1018
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
1060
+ await removePKCEVerifier(this.storage, this.storageKey, flowId)
1019
1061
  return this._returnResult({ data: { user: null, session: null }, error: error })
1020
1062
  }
1021
1063
 
@@ -1029,7 +1071,7 @@ export default class GoTrueClient {
1029
1071
 
1030
1072
  return this._returnResult({ data: { user, session }, error: null })
1031
1073
  } catch (error) {
1032
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
1074
+ await removePKCEVerifier(this.storage, this.storageKey, flowId)
1033
1075
  if (isAuthError(error)) {
1034
1076
  return this._returnResult({ data: { user: null, session: null }, error })
1035
1077
  }
@@ -1252,7 +1294,8 @@ export default class GoTrueClient {
1252
1294
  * {
1253
1295
  * data: {
1254
1296
  * provider: 'github',
1255
- * url: <PROVIDER_URL_TO_REDIRECT_TO>
1297
+ * url: <PROVIDER_URL_TO_REDIRECT_TO>,
1298
+ * flowId: <PKCE_FLOW_ID_OR_NULL>
1256
1299
  * },
1257
1300
  * error: null
1258
1301
  * }
@@ -1325,12 +1368,29 @@ export default class GoTrueClient {
1325
1368
  *
1326
1369
  * @remarks
1327
1370
  * - Used when `flowType` is set to `pkce` in client options.
1371
+ * - When several PKCE flows are in flight at once, pass `options.flowId` so
1372
+ * the code is exchanged with the verifier created by that specific flow.
1373
+ * The flow id is returned by `signInWithOAuth`, and with
1374
+ * `experimental.appendPkceFlowIdToRedirects` enabled it also arrives on
1375
+ * your callback URL as the reserved `sb_flow_id` query parameter (read
1376
+ * automatically in a browser).
1377
+ * - When a flow id is present but its stored verifier is gone (evicted,
1378
+ * already used, or from another device), the call fails with a verifier
1379
+ * missing error instead of trying another flow's verifier — a mismatched
1380
+ * verifier would consume the single-use code. Without any flow id the
1381
+ * most recently stored verifier is used, as before.
1328
1382
  *
1329
1383
  * @example Exchange Auth Code
1330
1384
  * ```js
1331
1385
  * supabase.auth.exchangeCodeForSession('34e770dd-9ff9-416c-87fa-43b31d7ef225')
1332
1386
  * ```
1333
1387
  *
1388
+ * @example Exchange Auth Code for a specific flow (e.g. in a server-side callback handler)
1389
+ * ```js
1390
+ * const flowId = requestUrl.searchParams.get('sb_flow_id')
1391
+ * supabase.auth.exchangeCodeForSession(code, flowId ? { flowId } : undefined)
1392
+ * ```
1393
+ *
1334
1394
  * @exampleResponse Exchange Auth Code
1335
1395
  * ```json
1336
1396
  * {
@@ -1488,17 +1548,20 @@ export default class GoTrueClient {
1488
1548
  * }
1489
1549
  * ```
1490
1550
  */
1491
- async exchangeCodeForSession(authCode: string): Promise<AuthTokenResponse> {
1551
+ async exchangeCodeForSession(
1552
+ authCode: string,
1553
+ options?: { flowId?: string }
1554
+ ): Promise<AuthTokenResponse> {
1492
1555
  await this.initializePromise
1493
1556
 
1494
1557
  if (this.lock != null) {
1495
1558
  // TODO(v3): remove legacy lock path
1496
1559
  return this._acquireLock(this.lockAcquireTimeout, async () => {
1497
- return this._exchangeCodeForSession(authCode)
1560
+ return this._exchangeCodeForSession(authCode, options)
1498
1561
  })
1499
1562
  }
1500
1563
 
1501
- return this._exchangeCodeForSession(authCode)
1564
+ return this._exchangeCodeForSession(authCode, options)
1502
1565
  }
1503
1566
 
1504
1567
  /**
@@ -1931,15 +1994,40 @@ export default class GoTrueClient {
1931
1994
  }
1932
1995
  }
1933
1996
 
1934
- private async _exchangeCodeForSession(authCode: string): Promise<
1997
+ private async _exchangeCodeForSession(
1998
+ authCode: string,
1999
+ options?: { flowId?: string }
2000
+ ): Promise<
1935
2001
  | {
1936
2002
  data: { session: Session; user: User; redirectType: string | null }
1937
2003
  error: null
1938
2004
  }
1939
2005
  | { data: { session: null; user: null; redirectType: null }; error: AuthError }
1940
2006
  > {
1941
- const storageItem = await getItemAsync(this.storage, `${this.storageKey}-code-verifier`)
1942
- const [codeVerifier, redirectType] = ((storageItem ?? '') as string).split('/')
2007
+ const hasExplicitFlowId = options?.flowId != null
2008
+ const requestedFlowId = hasExplicitFlowId
2009
+ ? validatePKCEFlowId(options?.flowId)
2010
+ : isBrowser()
2011
+ ? validatePKCEFlowId(parseParametersFromURL(window.location.href)[PKCE_FLOW_ID_PARAM])
2012
+ : null
2013
+
2014
+ if (hasExplicitFlowId && !requestedFlowId) {
2015
+ this._debug(
2016
+ '#_exchangeCodeForSession()',
2017
+ 'provided flowId is not a valid flow id',
2018
+ options?.flowId
2019
+ )
2020
+ }
2021
+
2022
+ // With a flow id (explicit or from the callback URL) the lookup is
2023
+ // slot-only and a miss fails fast — see retrievePKCEVerifier. An invalid
2024
+ // explicit flow id also fails fast rather than borrowing another flow's
2025
+ // verifier.
2026
+ const { verifier: storageItem, flowId } =
2027
+ hasExplicitFlowId && !requestedFlowId
2028
+ ? { verifier: null, flowId: null }
2029
+ : await retrievePKCEVerifier(this.storage, this.storageKey, requestedFlowId)
2030
+ const [codeVerifier, redirectType] = (storageItem ?? '').split('/')
1943
2031
 
1944
2032
  try {
1945
2033
  if (!codeVerifier && this.flowType === 'pkce') {
@@ -1959,7 +2047,7 @@ export default class GoTrueClient {
1959
2047
  xform: _sessionResponse,
1960
2048
  }
1961
2049
  )
1962
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
2050
+ await removePKCEVerifier(this.storage, this.storageKey, flowId)
1963
2051
  if (error) {
1964
2052
  throw error
1965
2053
  }
@@ -1979,7 +2067,7 @@ export default class GoTrueClient {
1979
2067
  }
1980
2068
  return this._returnResult({ data: { ...data, redirectType: redirectType ?? null }, error })
1981
2069
  } catch (error) {
1982
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
2070
+ await removePKCEVerifier(this.storage, this.storageKey, flowId)
1983
2071
  if (isAuthError(error)) {
1984
2072
  return this._returnResult({
1985
2073
  data: { user: null, session: null, redirectType: null },
@@ -2179,16 +2267,14 @@ export default class GoTrueClient {
2179
2267
  * ```
2180
2268
  */
2181
2269
  async signInWithOtp(credentials: SignInWithPasswordlessCredentials): Promise<AuthOtpResponse> {
2270
+ let flowId: string | null = null
2182
2271
  try {
2183
2272
  if ('email' in credentials) {
2184
2273
  const { email, options } = credentials
2185
2274
  let codeChallenge: string | null = null
2186
2275
  let codeChallengeMethod: string | null = null
2187
2276
  if (this.flowType === 'pkce') {
2188
- ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
2189
- this.storage,
2190
- this.storageKey
2191
- )
2277
+ ;[codeChallenge, codeChallengeMethod, flowId] = await this._getCodeChallengeAndMethod()
2192
2278
  }
2193
2279
  const { error } = await _request(this.fetch, 'POST', `${this.url}/otp`, {
2194
2280
  headers: this.headers,
@@ -2200,7 +2286,7 @@ export default class GoTrueClient {
2200
2286
  code_challenge: codeChallenge,
2201
2287
  code_challenge_method: codeChallengeMethod,
2202
2288
  },
2203
- redirectTo: options?.emailRedirectTo,
2289
+ redirectTo: this._maybeAppendFlowIdToRedirect(options?.emailRedirectTo, flowId),
2204
2290
  })
2205
2291
  return this._returnResult({ data: { user: null, session: null }, error })
2206
2292
  }
@@ -2223,7 +2309,7 @@ export default class GoTrueClient {
2223
2309
  }
2224
2310
  throw new AuthInvalidCredentialsError('You must provide either an email or phone number.')
2225
2311
  } catch (error) {
2226
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
2312
+ await removePKCEVerifier(this.storage, this.storageKey, flowId)
2227
2313
  if (isAuthError(error)) {
2228
2314
  return this._returnResult({ data: { user: null, session: null }, error })
2229
2315
  }
@@ -2470,21 +2556,19 @@ export default class GoTrueClient {
2470
2556
  * ```
2471
2557
  */
2472
2558
  async signInWithSSO(params: SignInWithSSO): Promise<SSOResponse> {
2559
+ let flowId: string | null = null
2473
2560
  try {
2474
2561
  let codeChallenge: string | null = null
2475
2562
  let codeChallengeMethod: string | null = null
2476
2563
  if (this.flowType === 'pkce') {
2477
- ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
2478
- this.storage,
2479
- this.storageKey
2480
- )
2564
+ ;[codeChallenge, codeChallengeMethod, flowId] = await this._getCodeChallengeAndMethod()
2481
2565
  }
2482
2566
 
2483
2567
  const result = await _request(this.fetch, 'POST', `${this.url}/sso`, {
2484
2568
  body: {
2485
2569
  ...('providerId' in params ? { provider_id: params.providerId } : null),
2486
2570
  ...('domain' in params ? { domain: params.domain } : null),
2487
- redirect_to: params.options?.redirectTo ?? undefined,
2571
+ redirect_to: this._maybeAppendFlowIdToRedirect(params.options?.redirectTo, flowId),
2488
2572
  ...(params?.options?.captchaToken
2489
2573
  ? { gotrue_meta_security: { captcha_token: params.options.captchaToken } }
2490
2574
  : null),
@@ -2503,7 +2587,7 @@ export default class GoTrueClient {
2503
2587
 
2504
2588
  return this._returnResult(result)
2505
2589
  } catch (error) {
2506
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
2590
+ await removePKCEVerifier(this.storage, this.storageKey, flowId)
2507
2591
  if (isAuthError(error)) {
2508
2592
  return this._returnResult({ data: null, error })
2509
2593
  }
@@ -2629,6 +2713,7 @@ export default class GoTrueClient {
2629
2713
  * ```
2630
2714
  */
2631
2715
  async resend(credentials: ResendParams): Promise<AuthOtpResponse> {
2716
+ let flowId: string | null = null
2632
2717
  try {
2633
2718
  const endpoint = `${this.url}/resend`
2634
2719
  if ('email' in credentials) {
@@ -2636,10 +2721,7 @@ export default class GoTrueClient {
2636
2721
  let codeChallenge: string | null = null
2637
2722
  let codeChallengeMethod: string | null = null
2638
2723
  if (this.flowType === 'pkce') {
2639
- ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
2640
- this.storage,
2641
- this.storageKey
2642
- )
2724
+ ;[codeChallenge, codeChallengeMethod, flowId] = await this._getCodeChallengeAndMethod()
2643
2725
  }
2644
2726
  const { error } = await _request(this.fetch, 'POST', endpoint, {
2645
2727
  headers: this.headers,
@@ -2650,10 +2732,10 @@ export default class GoTrueClient {
2650
2732
  code_challenge: codeChallenge,
2651
2733
  code_challenge_method: codeChallengeMethod,
2652
2734
  },
2653
- redirectTo: options?.emailRedirectTo,
2735
+ redirectTo: this._maybeAppendFlowIdToRedirect(options?.emailRedirectTo, flowId),
2654
2736
  })
2655
2737
  if (error) {
2656
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
2738
+ await removePKCEVerifier(this.storage, this.storageKey, flowId)
2657
2739
  }
2658
2740
  return this._returnResult({ data: { user: null, session: null }, error })
2659
2741
  } else if ('phone' in credentials) {
@@ -2675,7 +2757,7 @@ export default class GoTrueClient {
2675
2757
  'You must provide either an email or phone number and a type'
2676
2758
  )
2677
2759
  } catch (error) {
2678
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
2760
+ await removePKCEVerifier(this.storage, this.storageKey, flowId)
2679
2761
  if (isAuthError(error)) {
2680
2762
  return this._returnResult({ data: { user: null, session: null }, error })
2681
2763
  }
@@ -2692,7 +2774,7 @@ export default class GoTrueClient {
2692
2774
  * to the client. If that storage is based on request cookies for example,
2693
2775
  * the values in it may not be authentic and therefore it's strongly advised
2694
2776
  * against using this method and its results in such circumstances. A warning
2695
- * will be emitted if this is detected. Use {@link #getUser()} instead.
2777
+ * will be emitted if this is detected. Use {@link GoTrueClient.getUser} instead.
2696
2778
  *
2697
2779
  * @category Auth
2698
2780
  *
@@ -2861,7 +2943,7 @@ export default class GoTrueClient {
2861
2943
  }
2862
2944
 
2863
2945
  /**
2864
- * Use instead of {@link #getSession} inside the library. Loads the session
2946
+ * Use instead of {@link GoTrueClient.getSession} inside the library. Loads the session
2865
2947
  * via `__loadSession` (which may trigger a refresh if the access token is
2866
2948
  * within the expiry margin) and runs `fn` with the result.
2867
2949
  */
@@ -2906,7 +2988,7 @@ export default class GoTrueClient {
2906
2988
  /**
2907
2989
  * NEVER USE DIRECTLY!
2908
2990
  *
2909
- * Always use {@link #_useSession}.
2991
+ * Always use `_useSession`.
2910
2992
  */
2911
2993
  private async __loadSession(): Promise<
2912
2994
  | {
@@ -3170,7 +3252,6 @@ export default class GoTrueClient {
3170
3252
  // session in the database, indicating the user is signed out.
3171
3253
 
3172
3254
  await this._removeSession()
3173
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
3174
3255
  }
3175
3256
 
3176
3257
  return this._returnResult({ data: { user: null }, error })
@@ -3318,6 +3399,7 @@ export default class GoTrueClient {
3318
3399
  emailRedirectTo?: string | undefined
3319
3400
  } = {}
3320
3401
  ): Promise<UserResponse> {
3402
+ let flowId: string | null = null
3321
3403
  try {
3322
3404
  return await this._useSession(async (result) => {
3323
3405
  const { data: sessionData, error: sessionError } = result
@@ -3331,15 +3413,12 @@ export default class GoTrueClient {
3331
3413
  let codeChallenge: string | null = null
3332
3414
  let codeChallengeMethod: string | null = null
3333
3415
  if (this.flowType === 'pkce' && attributes.email != null) {
3334
- ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
3335
- this.storage,
3336
- this.storageKey
3337
- )
3416
+ ;[codeChallenge, codeChallengeMethod, flowId] = await this._getCodeChallengeAndMethod()
3338
3417
  }
3339
3418
 
3340
3419
  const { data, error: userError } = await _request(this.fetch, 'PUT', `${this.url}/user`, {
3341
3420
  headers: this.headers,
3342
- redirectTo: options?.emailRedirectTo,
3421
+ redirectTo: this._maybeAppendFlowIdToRedirect(options?.emailRedirectTo, flowId),
3343
3422
  body: {
3344
3423
  ...attributes,
3345
3424
  code_challenge: codeChallenge,
@@ -3357,7 +3436,7 @@ export default class GoTrueClient {
3357
3436
  return this._returnResult({ data: { user: session.user }, error: null })
3358
3437
  })
3359
3438
  } catch (error) {
3360
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
3439
+ await removePKCEVerifier(this.storage, this.storageKey, flowId)
3361
3440
  if (isAuthError(error)) {
3362
3441
  return this._returnResult({ data: { user: null }, error })
3363
3442
  }
@@ -3789,11 +3868,14 @@ export default class GoTrueClient {
3789
3868
  if (callbackUrlType === 'pkce') {
3790
3869
  this._debug('#_initialize()', 'begin', 'is PKCE flow', true)
3791
3870
  if (!params.code) throw new AuthPKCEGrantCodeExchangeError('No code detected.')
3792
- const { data, error } = await this._exchangeCodeForSession(params.code)
3871
+ const { data, error } = await this._exchangeCodeForSession(params.code, {
3872
+ flowId: params[PKCE_FLOW_ID_PARAM],
3873
+ })
3793
3874
  if (error) throw error
3794
3875
 
3795
3876
  const url = new URL(window.location.href)
3796
3877
  url.searchParams.delete('code')
3878
+ url.searchParams.delete(PKCE_FLOW_ID_PARAM)
3797
3879
 
3798
3880
  window.history.replaceState(window.history.state, '', url.toString())
3799
3881
 
@@ -3897,12 +3979,24 @@ export default class GoTrueClient {
3897
3979
  * Checks if the current URL and backing storage contain parameters given by a PKCE flow
3898
3980
  */
3899
3981
  private async _isPKCECallback(params: { [parameter: string]: string }): Promise<boolean> {
3982
+ if (!params.code) {
3983
+ return false
3984
+ }
3985
+
3986
+ const flowId = validatePKCEFlowId(params[PKCE_FLOW_ID_PARAM])
3987
+ if (
3988
+ flowId &&
3989
+ (await getItemAsync(this.storage, pkceVerifierSlotKey(this.storageKey, flowId)))
3990
+ ) {
3991
+ return true
3992
+ }
3993
+
3900
3994
  const currentStorageContent = await getItemAsync(
3901
3995
  this.storage,
3902
3996
  `${this.storageKey}-code-verifier`
3903
3997
  )
3904
3998
 
3905
- return !!(params.code && currentStorageContent)
3999
+ return !!currentStorageContent
3906
4000
  }
3907
4001
 
3908
4002
  /**
@@ -3963,6 +4057,9 @@ export default class GoTrueClient {
3963
4057
  { scope }: SignOut = { scope: 'global' }
3964
4058
  ): Promise<{ error: AuthError | null }> {
3965
4059
  return await this._useSession(async (result) => {
4060
+ const removeCurrentSession = async () => {
4061
+ await this._removeSession()
4062
+ }
3966
4063
  const { data, error: sessionError } = result
3967
4064
  if (sessionError && !isAuthSessionMissingError(sessionError)) {
3968
4065
  return this._returnResult({ error: sessionError })
@@ -3980,13 +4077,15 @@ export default class GoTrueClient {
3980
4077
  isAuthSessionMissingError(error)
3981
4078
  )
3982
4079
  ) {
4080
+ if (scope !== 'others') {
4081
+ await removeCurrentSession()
4082
+ }
3983
4083
  return this._returnResult({ error })
3984
4084
  }
3985
4085
  }
3986
4086
  }
3987
4087
  if (scope !== 'others') {
3988
- await this._removeSession()
3989
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
4088
+ await removeCurrentSession()
3990
4089
  }
3991
4090
  return this._returnResult({ error: null })
3992
4091
  })
@@ -4247,7 +4346,18 @@ export default class GoTrueClient {
4247
4346
  } catch (err) {
4248
4347
  await this.stateChangeEmitters.get(id)?.callback('INITIAL_SESSION', null)
4249
4348
  this._debug('INITIAL_SESSION', 'callback id', id, 'error', err)
4250
- if (isAuthSessionMissingError(err)) {
4349
+ if (
4350
+ isAuthSessionMissingError(err) ||
4351
+ isAuthRetryableFetchError(err) ||
4352
+ (isAuthApiError(err) &&
4353
+ (err.code === 'refresh_token_not_found' ||
4354
+ err.code === 'refresh_token_already_used' ||
4355
+ err.code === 'session_expired'))
4356
+ ) {
4357
+ // A missing session, a transient/aborted network failure (e.g. a
4358
+ // superseded page navigation cancelling the in-flight request), or
4359
+ // a dead refresh token (e.g. stale SSR cookies) is not an
4360
+ // application error — warn rather than surface it raw.
4251
4361
  console.warn(err)
4252
4362
  } else {
4253
4363
  console.error(err)
@@ -4338,11 +4448,10 @@ export default class GoTrueClient {
4338
4448
  > {
4339
4449
  let codeChallenge: string | null = null
4340
4450
  let codeChallengeMethod: string | null = null
4451
+ let flowId: string | null = null
4341
4452
 
4342
4453
  if (this.flowType === 'pkce') {
4343
- ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
4344
- this.storage,
4345
- this.storageKey,
4454
+ ;[codeChallenge, codeChallengeMethod, flowId] = await this._getCodeChallengeAndMethod(
4346
4455
  true // isPasswordRecovery
4347
4456
  )
4348
4457
  }
@@ -4355,10 +4464,10 @@ export default class GoTrueClient {
4355
4464
  gotrue_meta_security: { captcha_token: options.captchaToken },
4356
4465
  },
4357
4466
  headers: this.headers,
4358
- redirectTo: options.redirectTo,
4467
+ redirectTo: this._maybeAppendFlowIdToRedirect(options.redirectTo, flowId),
4359
4468
  })
4360
4469
  } catch (error) {
4361
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
4470
+ await removePKCEVerifier(this.storage, this.storageKey, flowId)
4362
4471
  if (isAuthError(error)) {
4363
4472
  return this._returnResult({ data: null, error })
4364
4473
  }
@@ -4460,7 +4569,8 @@ export default class GoTrueClient {
4460
4569
  * {
4461
4570
  * data: {
4462
4571
  * provider: 'github',
4463
- * url: <PROVIDER_URL_TO_REDIRECT_TO>
4572
+ * url: <PROVIDER_URL_TO_REDIRECT_TO>,
4573
+ * flowId: <PKCE_FLOW_ID_OR_NULL>
4464
4574
  * },
4465
4575
  * error: null
4466
4576
  * }
@@ -4475,11 +4585,12 @@ export default class GoTrueClient {
4475
4585
  }
4476
4586
 
4477
4587
  private async linkIdentityOAuth(credentials: SignInWithOAuthCredentials): Promise<OAuthResponse> {
4588
+ let flowId: string | null = null
4478
4589
  try {
4479
4590
  const { data, error } = await this._useSession(async (result) => {
4480
4591
  const { data, error } = result
4481
4592
  if (error) throw error
4482
- const url: string = await this._getUrlForProvider(
4593
+ const { url, flowId: urlFlowId } = await this._getUrlForProvider(
4483
4594
  `${this.url}/user/identities/authorize`,
4484
4595
  credentials.provider,
4485
4596
  {
@@ -4489,6 +4600,7 @@ export default class GoTrueClient {
4489
4600
  skipBrowserRedirect: true,
4490
4601
  }
4491
4602
  )
4603
+ flowId = urlFlowId
4492
4604
  return await _request(this.fetch, 'GET', url, {
4493
4605
  headers: this.headers,
4494
4606
  jwt: data.session?.access_token ?? undefined,
@@ -4499,12 +4611,15 @@ export default class GoTrueClient {
4499
4611
  window.location.assign(data?.url)
4500
4612
  }
4501
4613
  return this._returnResult({
4502
- data: { provider: credentials.provider, url: data?.url },
4614
+ data: { provider: credentials.provider, url: data?.url, flowId },
4503
4615
  error: null,
4504
4616
  })
4505
4617
  } catch (error) {
4506
4618
  if (isAuthError(error)) {
4507
- return this._returnResult({ data: { provider: credentials.provider, url: null }, error })
4619
+ return this._returnResult({
4620
+ data: { provider: credentials.provider, url: null, flowId },
4621
+ error,
4622
+ })
4508
4623
  }
4509
4624
  throw error
4510
4625
  }
@@ -4552,7 +4667,7 @@ export default class GoTrueClient {
4552
4667
  }
4553
4668
  return this._returnResult({ data, error })
4554
4669
  } catch (error) {
4555
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
4670
+ await removePKCEVerifier(this.storage, this.storageKey, null)
4556
4671
  if (isAuthError(error)) {
4557
4672
  return this._returnResult({ data: { user: null, session: null }, error })
4558
4673
  }
@@ -4688,7 +4803,7 @@ export default class GoTrueClient {
4688
4803
  skipBrowserRedirect?: boolean
4689
4804
  }
4690
4805
  ) {
4691
- const url: string = await this._getUrlForProvider(`${this.url}/authorize`, provider, {
4806
+ const { url, flowId } = await this._getUrlForProvider(`${this.url}/authorize`, provider, {
4692
4807
  redirectTo: options.redirectTo,
4693
4808
  scopes: options.scopes,
4694
4809
  queryParams: options.queryParams,
@@ -4701,7 +4816,7 @@ export default class GoTrueClient {
4701
4816
  window.location.assign(url)
4702
4817
  }
4703
4818
 
4704
- return { data: { provider, url }, error: null }
4819
+ return { data: { provider, url, flowId }, error: null }
4705
4820
  }
4706
4821
 
4707
4822
  /**
@@ -4823,7 +4938,15 @@ export default class GoTrueClient {
4823
4938
  } catch (err) {
4824
4939
  this._debug(debugName, 'error', err)
4825
4940
 
4826
- console.error(err)
4941
+ if (isAuthRetryableFetchError(err)) {
4942
+ // Transient/aborted network failure during session recovery (e.g. a
4943
+ // superseded page navigation cancelling the in-flight request). Warn
4944
+ // rather than surface it raw; the session is untouched and recovery
4945
+ // will run again on the next load.
4946
+ console.warn(err)
4947
+ } else {
4948
+ console.error(err)
4949
+ }
4827
4950
  return
4828
4951
  } finally {
4829
4952
  this._debug(debugName, 'end')
@@ -5011,6 +5134,20 @@ export default class GoTrueClient {
5011
5134
  session: Session | null,
5012
5135
  broadcast = true
5013
5136
  ) {
5137
+ if (this._pendingInitNotifications !== null && broadcast) {
5138
+ // We're inside initialize() before initializePromise has resolved, and
5139
+ // this notification originates from the init chain (_recoverAndRefresh),
5140
+ // which always fires with broadcast = true. Enqueue instead of firing so
5141
+ // that callbacks can safely call getSession() / getUser() without
5142
+ // deadlocking on initializePromise.
5143
+ //
5144
+ // Notifications with broadcast = false come from the incoming
5145
+ // BroadcastChannel handler (a cross-tab event), not the init chain, so
5146
+ // they are fired immediately to preserve multi-tab ordering.
5147
+ this._pendingInitNotifications.push({ event, session, broadcast })
5148
+ return
5149
+ }
5150
+
5014
5151
  const debugName = `#_notifyAllSubscribers(${event})`
5015
5152
  this._debug(debugName, 'begin', session, `broadcast = ${broadcast}`)
5016
5153
 
@@ -5051,7 +5188,6 @@ export default class GoTrueClient {
5051
5188
  // _saveSession is always called whenever a new session has been acquired
5052
5189
  // so we can safely suppress the warning returned by future getSession calls
5053
5190
  this.suppressGetSessionWarning = true
5054
- await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
5055
5191
  // Create a shallow copy to work with, to avoid mutating the original session object if it's used elsewhere
5056
5192
  const sessionToProcess = { ...session }
5057
5193
 
@@ -5101,7 +5237,7 @@ export default class GoTrueClient {
5101
5237
  this.suppressGetSessionWarning = false
5102
5238
 
5103
5239
  await removeItemAsync(this.storage, this.storageKey)
5104
- await removeItemAsync(this.storage, this.storageKey + '-code-verifier')
5240
+ await removeAllPKCEVerifiers(this.storage, this.storageKey)
5105
5241
  await removeItemAsync(this.storage, this.storageKey + '-user')
5106
5242
 
5107
5243
  if (this.userStorage) {
@@ -5114,8 +5250,8 @@ export default class GoTrueClient {
5114
5250
  /**
5115
5251
  * Removes any registered visibilitychange callback.
5116
5252
  *
5117
- * {@see #startAutoRefresh}
5118
- * {@see #stopAutoRefresh}
5253
+ * {@link GoTrueClient.startAutoRefresh}
5254
+ * {@link GoTrueClient.stopAutoRefresh}
5119
5255
  */
5120
5256
  private _removeVisibilityChangedCallback() {
5121
5257
  this._debug('#_removeVisibilityChangedCallback()')
@@ -5133,7 +5269,7 @@ export default class GoTrueClient {
5133
5269
  }
5134
5270
 
5135
5271
  /**
5136
- * This is the private implementation of {@link #startAutoRefresh}. Use this
5272
+ * This is the private implementation of {@link GoTrueClient.startAutoRefresh}. Use this
5137
5273
  * within the library.
5138
5274
  */
5139
5275
  private async _startAutoRefresh() {
@@ -5179,7 +5315,7 @@ export default class GoTrueClient {
5179
5315
  }
5180
5316
 
5181
5317
  /**
5182
- * This is the private implementation of {@link #stopAutoRefresh}. Use this
5318
+ * This is the private implementation of {@link GoTrueClient.stopAutoRefresh}. Use this
5183
5319
  * within the library.
5184
5320
  */
5185
5321
  private async _stopAutoRefresh() {
@@ -5220,7 +5356,7 @@ export default class GoTrueClient {
5220
5356
  * platform's foreground indication mechanism and call these methods
5221
5357
  * appropriately to conserve resources.
5222
5358
  *
5223
- * {@see #stopAutoRefresh}
5359
+ * {@link GoTrueClient.stopAutoRefresh}
5224
5360
  *
5225
5361
  * @category Auth
5226
5362
  *
@@ -5255,7 +5391,7 @@ export default class GoTrueClient {
5255
5391
  * If you call this method any managed visibility change callback will be
5256
5392
  * removed and you must manage visibility changes on your own.
5257
5393
  *
5258
- * See {@link #startAutoRefresh} for more details.
5394
+ * See {@link GoTrueClient.startAutoRefresh} for more details.
5259
5395
  *
5260
5396
  * @category Auth
5261
5397
  *
@@ -5525,19 +5661,23 @@ export default class GoTrueClient {
5525
5661
  skipBrowserRedirect?: boolean
5526
5662
  }
5527
5663
  ) {
5664
+ let redirectTo = options?.redirectTo
5665
+ let codeChallenge: string | null = null
5666
+ let codeChallengeMethod: string | null = null
5667
+ let flowId: string | null = null
5668
+ if (this.flowType === 'pkce') {
5669
+ ;[codeChallenge, codeChallengeMethod, flowId] = await this._getCodeChallengeAndMethod()
5670
+ redirectTo = this._maybeAppendFlowIdToRedirect(redirectTo, flowId)
5671
+ }
5672
+
5528
5673
  const urlParams: string[] = [`provider=${encodeURIComponent(provider)}`]
5529
- if (options?.redirectTo) {
5530
- urlParams.push(`redirect_to=${encodeURIComponent(options.redirectTo)}`)
5674
+ if (redirectTo) {
5675
+ urlParams.push(`redirect_to=${encodeURIComponent(redirectTo)}`)
5531
5676
  }
5532
5677
  if (options?.scopes) {
5533
5678
  urlParams.push(`scopes=${encodeURIComponent(options.scopes)}`)
5534
5679
  }
5535
- if (this.flowType === 'pkce') {
5536
- const [codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
5537
- this.storage,
5538
- this.storageKey
5539
- )
5540
-
5680
+ if (codeChallenge != null && codeChallengeMethod != null) {
5541
5681
  const flowParams = new URLSearchParams({
5542
5682
  code_challenge: `${encodeURIComponent(codeChallenge)}`,
5543
5683
  code_challenge_method: `${encodeURIComponent(codeChallengeMethod)}`,
@@ -5552,7 +5692,44 @@ export default class GoTrueClient {
5552
5692
  urlParams.push(`skip_http_redirect=${options.skipBrowserRedirect}`)
5553
5693
  }
5554
5694
 
5555
- return `${url}?${urlParams.join('&')}`
5695
+ return { url: `${url}?${urlParams.join('&')}`, flowId }
5696
+ }
5697
+
5698
+ /**
5699
+ * Appends the reserved flow id parameter to a redirect URL so the callback
5700
+ * can be matched to the verifier stored for its flow. Opt-in via
5701
+ * `experimental.appendPkceFlowIdToRedirects`: redirect URLs are validated
5702
+ * against the project's allow list including the query string, so an extra
5703
+ * parameter can stop exact (non-wildcard) entries from matching.
5704
+ */
5705
+ private _maybeAppendFlowIdToRedirect(
5706
+ redirectTo: string | undefined,
5707
+ flowId: string | null
5708
+ ): string | undefined {
5709
+ if (!redirectTo || !flowId || !this.experimental.appendPkceFlowIdToRedirects) {
5710
+ return redirectTo ?? undefined
5711
+ }
5712
+ return appendFlowIdToRedirectTo(redirectTo, flowId)
5713
+ }
5714
+
5715
+ /**
5716
+ * Generates and stores a PKCE challenge/verifier pair for a new flow,
5717
+ * logging any pending verifier the bounded slot ring evicts.
5718
+ */
5719
+ private async _getCodeChallengeAndMethod(
5720
+ isPasswordRecovery = false
5721
+ ): Promise<[string, string, string]> {
5722
+ return getCodeChallengeAndMethod(
5723
+ this.storage,
5724
+ this.storageKey,
5725
+ isPasswordRecovery,
5726
+ (evictedFlowId) =>
5727
+ this._debug(
5728
+ '#_getCodeChallengeAndMethod()',
5729
+ 'evicted oldest pending PKCE verifier slot',
5730
+ evictedFlowId
5731
+ )
5732
+ )
5556
5733
  }
5557
5734
 
5558
5735
  private async _unenroll(params: MFAUnenrollParams): Promise<AuthMFAUnenrollResponse> {
@@ -5577,7 +5754,7 @@ export default class GoTrueClient {
5577
5754
  }
5578
5755
 
5579
5756
  /**
5580
- * {@see GoTrueMFAApi#enroll}
5757
+ * {@link GoTrueMFAApi#enroll}
5581
5758
  */
5582
5759
  private async _enroll(params: MFAEnrollTOTPParams): Promise<AuthMFAEnrollTOTPResponse>
5583
5760
  private async _enroll(params: MFAEnrollPhoneParams): Promise<AuthMFAEnrollPhoneResponse>
@@ -5624,7 +5801,7 @@ export default class GoTrueClient {
5624
5801
  }
5625
5802
 
5626
5803
  /**
5627
- * {@see GoTrueMFAApi#verify}
5804
+ * {@link GoTrueMFAApi#verify}
5628
5805
  */
5629
5806
  private async _verify(params: MFAVerifyTOTPParams): Promise<AuthMFAVerifyResponse>
5630
5807
  private async _verify(params: MFAVerifyPhoneParams): Promise<AuthMFAVerifyResponse>
@@ -5713,7 +5890,7 @@ export default class GoTrueClient {
5713
5890
  }
5714
5891
 
5715
5892
  /**
5716
- * {@see GoTrueMFAApi#challenge}
5893
+ * {@link GoTrueMFAApi#challenge}
5717
5894
  */
5718
5895
  private async _challenge(
5719
5896
  params: MFAChallengeTOTPParams
@@ -5808,7 +5985,7 @@ export default class GoTrueClient {
5808
5985
  }
5809
5986
 
5810
5987
  /**
5811
- * {@see GoTrueMFAApi#challengeAndVerify}
5988
+ * {@link GoTrueMFAApi#challengeAndVerify}
5812
5989
  */
5813
5990
  private async _challengeAndVerify(
5814
5991
  params: MFAChallengeAndVerifyParams
@@ -5828,7 +6005,7 @@ export default class GoTrueClient {
5828
6005
  }
5829
6006
 
5830
6007
  /**
5831
- * {@see GoTrueMFAApi#listFactors}
6008
+ * {@link GoTrueMFAApi#listFactors}
5832
6009
  */
5833
6010
  private async _listFactors(): Promise<AuthMFAListFactorsResponse> {
5834
6011
  const {
@@ -5861,7 +6038,7 @@ export default class GoTrueClient {
5861
6038
  }
5862
6039
 
5863
6040
  /**
5864
- * {@see GoTrueMFAApi#getAuthenticatorAssuranceLevel}
6041
+ * {@link GoTrueMFAApi#getAuthenticatorAssuranceLevel}
5865
6042
  */
5866
6043
  private async _getAuthenticatorAssuranceLevel(
5867
6044
  jwt?: string
@@ -6209,15 +6386,15 @@ export default class GoTrueClient {
6209
6386
  * Extracts the JWT claims present in the access token by first verifying the
6210
6387
  * JWT against the server's JSON Web Key Set endpoint
6211
6388
  * `/.well-known/jwks.json` which is often cached, resulting in significantly
6212
- * faster responses. Prefer this method over {@link #getUser} which always
6389
+ * faster responses. Prefer this method over {@link GoTrueClient.getUser} which always
6213
6390
  * sends a request to the Auth server for each JWT.
6214
6391
  *
6215
6392
  * If the project is not using an asymmetric JWT signing key (like ECC or
6216
- * RSA) it always sends a request to the Auth server (similar to {@link
6217
- * #getUser}) to verify the JWT.
6393
+ * RSA) it always sends a request to the Auth server (similar to
6394
+ * {@link GoTrueClient.getUser}) to verify the JWT.
6218
6395
  *
6219
6396
  * @param jwt An optional specific JWT you wish to verify, not the one you
6220
- * can obtain from {@link #getSession}.
6397
+ * can obtain from {@link GoTrueClient.getSession}.
6221
6398
  * @param options Various additional options that allow you to customize the
6222
6399
  * behavior of this method.
6223
6400
  *