@supabase/auth-js 2.110.9 → 2.111.0

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 (42) hide show
  1. package/dist/main/GoTrueClient.d.ts +35 -2
  2. package/dist/main/GoTrueClient.d.ts.map +1 -1
  3. package/dist/main/GoTrueClient.js +129 -47
  4. package/dist/main/GoTrueClient.js.map +1 -1
  5. package/dist/main/lib/constants.d.ts +13 -0
  6. package/dist/main/lib/constants.d.ts.map +1 -1
  7. package/dist/main/lib/constants.js +14 -1
  8. package/dist/main/lib/constants.js.map +1 -1
  9. package/dist/main/lib/helpers.d.ts +50 -1
  10. package/dist/main/lib/helpers.d.ts.map +1 -1
  11. package/dist/main/lib/helpers.js +160 -4
  12. package/dist/main/lib/helpers.js.map +1 -1
  13. package/dist/main/lib/types.d.ts +38 -0
  14. package/dist/main/lib/types.d.ts.map +1 -1
  15. package/dist/main/lib/types.js.map +1 -1
  16. package/dist/main/lib/version.d.ts +1 -1
  17. package/dist/main/lib/version.js +1 -1
  18. package/dist/module/GoTrueClient.d.ts +35 -2
  19. package/dist/module/GoTrueClient.d.ts.map +1 -1
  20. package/dist/module/GoTrueClient.js +131 -49
  21. package/dist/module/GoTrueClient.js.map +1 -1
  22. package/dist/module/lib/constants.d.ts +13 -0
  23. package/dist/module/lib/constants.d.ts.map +1 -1
  24. package/dist/module/lib/constants.js +13 -0
  25. package/dist/module/lib/constants.js.map +1 -1
  26. package/dist/module/lib/helpers.d.ts +50 -1
  27. package/dist/module/lib/helpers.d.ts.map +1 -1
  28. package/dist/module/lib/helpers.js +152 -4
  29. package/dist/module/lib/helpers.js.map +1 -1
  30. package/dist/module/lib/types.d.ts +38 -0
  31. package/dist/module/lib/types.d.ts.map +1 -1
  32. package/dist/module/lib/types.js.map +1 -1
  33. package/dist/module/lib/version.d.ts +1 -1
  34. package/dist/module/lib/version.js +1 -1
  35. package/dist/tsconfig.module.tsbuildinfo +1 -1
  36. package/dist/tsconfig.tsbuildinfo +1 -1
  37. package/package.json +1 -1
  38. package/src/GoTrueClient.ts +169 -67
  39. package/src/lib/constants.ts +15 -0
  40. package/src/lib/helpers.ts +197 -5
  41. package/src/lib/types.ts +38 -0
  42. package/src/lib/version.ts +1 -1
@@ -1,4 +1,9 @@
1
- import { API_VERSION_HEADER_NAME, BASE64URL_REGEX } from './constants'
1
+ import {
2
+ API_VERSION_HEADER_NAME,
3
+ BASE64URL_REGEX,
4
+ PKCE_FLOW_ID_PARAM,
5
+ PKCE_MAX_CONCURRENT_FLOWS,
6
+ } from './constants'
2
7
  import { AuthInvalidJwtError } from './errors'
3
8
  import { base64UrlToUint8Array, stringFromBase64URL } from './base64url'
4
9
  import { JwtHeader, JwtPayload, SupportedStorage, User } from './types'
@@ -301,20 +306,207 @@ export async function generatePKCEChallenge(verifier: string) {
301
306
  return btoa(hashed).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
302
307
  }
303
308
 
309
+ const PKCE_FLOW_ID_PATTERN = /^[a-zA-Z0-9_-]{8,64}$/
310
+
311
+ /**
312
+ * Returns the flow id if it is a plausible flow id, `null` otherwise. Flow
313
+ * ids can arrive via URL parameters, so anything outside the expected shape
314
+ * is discarded before it is used to build a storage key.
315
+ */
316
+ export function validatePKCEFlowId(flowId: unknown): string | null {
317
+ return typeof flowId === 'string' && PKCE_FLOW_ID_PATTERN.test(flowId) ? flowId : null
318
+ }
319
+
320
+ export function generatePKCEFlowId(): string {
321
+ if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
322
+ const bytes = new Uint8Array(16)
323
+ crypto.getRandomValues(bytes)
324
+ return Array.from(bytes, dec2hex).join('')
325
+ }
326
+ let flowId = ''
327
+ for (let i = 0; i < 32; i++) {
328
+ flowId += Math.floor(Math.random() * 16).toString(16)
329
+ }
330
+ return flowId
331
+ }
332
+
333
+ // Slot keys deliberately end in `-code-verifier`: @supabase/ssr's server
334
+ // cookie adapter only persists writes immediately for keys with that suffix
335
+ // (no auth event fires when a verifier is stored). They also contain no dot,
336
+ // because @supabase/ssr chunks oversized cookies as `<key>.<number>` and a
337
+ // dot-delimited key could be mistaken for a chunk of the fixed
338
+ // `-code-verifier` cookie and clobbered by its chunk management.
339
+ export const pkceVerifierSlotKey = (storageKey: string, flowId: string) =>
340
+ `${storageKey}-flow-${flowId}-code-verifier`
341
+
342
+ const pkceFlowIndexKey = (storageKey: string) => `${storageKey}-flows-code-verifier`
343
+
344
+ /**
345
+ * Storage adapters cannot enumerate keys, so the ids of pending verifier
346
+ * slots are tracked in an index entry, oldest first. Index entries pass
347
+ * through the same validation as URL-provided flow ids: with cookie-based
348
+ * storage the index contents are no more trustworthy than a URL parameter.
349
+ */
350
+ async function getPKCEFlowIndex(storage: SupportedStorage, storageKey: string): Promise<string[]> {
351
+ const index = await getItemAsync(storage, pkceFlowIndexKey(storageKey))
352
+ return Array.isArray(index)
353
+ ? index.filter((id): id is string => validatePKCEFlowId(id) !== null)
354
+ : []
355
+ }
356
+
357
+ /**
358
+ * The index is read-modify-write without a lock: two concurrent starts (e.g.
359
+ * two tabs) can lose one index update. The losing flow still works — its slot
360
+ * is addressed directly by key — but its entry is missing from the index, so
361
+ * it escapes both ring eviction and removeAllPKCEVerifiers: the orphaned slot
362
+ * persists for the storage medium's lifetime (up to the cookie max age in
363
+ * cookie storage) and repeated races accumulate one orphan each. Accepted
364
+ * trade-off: locking every flow start is far more intrusive than the leak.
365
+ */
366
+ export async function storePKCEVerifier(
367
+ storage: SupportedStorage,
368
+ storageKey: string,
369
+ flowId: string,
370
+ verifier: string,
371
+ onEvictFlow?: (evictedFlowId: string) => void
372
+ ): Promise<void> {
373
+ await setItemAsync(storage, pkceVerifierSlotKey(storageKey, flowId), verifier)
374
+
375
+ const index = (await getPKCEFlowIndex(storage, storageKey)).filter((id) => id !== flowId)
376
+ index.push(flowId)
377
+ while (index.length > PKCE_MAX_CONCURRENT_FLOWS) {
378
+ const evicted = index.shift()!
379
+ await removeItemAsync(storage, pkceVerifierSlotKey(storageKey, evicted))
380
+ onEvictFlow?.(evicted)
381
+ }
382
+ await setItemAsync(storage, pkceFlowIndexKey(storageKey), index)
383
+
384
+ // Deprecation-window dual write: exchanges that cannot identify their flow
385
+ // (older SDK versions, redirects without the flow id parameter) read the
386
+ // fixed key, which mirrors the most recently started flow.
387
+ await setItemAsync(storage, `${storageKey}-code-verifier`, verifier)
388
+ }
389
+
390
+ /**
391
+ * Looks up the verifier for `flowId`. When a flow id is given, only that slot
392
+ * is consulted — deliberately no fallback to the fixed legacy key: submitting
393
+ * another flow's verifier would burn the single-use auth code, and the
394
+ * subsequent cleanup would delete a pending flow's only fallback. The legacy
395
+ * key is read only when no flow id is available at all.
396
+ */
397
+ export async function retrievePKCEVerifier(
398
+ storage: SupportedStorage,
399
+ storageKey: string,
400
+ flowId: string | null
401
+ ): Promise<{ verifier: string | null; flowId: string | null }> {
402
+ if (flowId) {
403
+ const verifier = await getItemAsync(storage, pkceVerifierSlotKey(storageKey, flowId))
404
+ return { verifier: typeof verifier === 'string' ? verifier : null, flowId }
405
+ }
406
+ const verifier = await getItemAsync(storage, `${storageKey}-code-verifier`)
407
+ return { verifier: typeof verifier === 'string' ? verifier : null, flowId: null }
408
+ }
409
+
410
+ /**
411
+ * Removes a single flow's verifier. Never clears other flows' slots: with a
412
+ * `flowId` only that slot is deleted (plus the legacy fixed key when it holds
413
+ * the same verifier); without one, only the legacy fixed key is deleted.
414
+ */
415
+ export async function removePKCEVerifier(
416
+ storage: SupportedStorage,
417
+ storageKey: string,
418
+ flowId: string | null
419
+ ): Promise<void> {
420
+ const legacyKey = `${storageKey}-code-verifier`
421
+ if (!flowId) {
422
+ await removeItemAsync(storage, legacyKey)
423
+ return
424
+ }
425
+
426
+ const slotKey = pkceVerifierSlotKey(storageKey, flowId)
427
+ const slotValue = await getItemAsync(storage, slotKey)
428
+ await removeItemAsync(storage, slotKey)
429
+
430
+ // Skip the index rewrite when the flow was never indexed (e.g. a failed
431
+ // exchange for an absent slot): on cookie storage every write is a full
432
+ // Set-Cookie cycle.
433
+ const index = await getPKCEFlowIndex(storage, storageKey)
434
+ const remaining = index.filter((id) => id !== flowId)
435
+ if (remaining.length !== index.length) {
436
+ if (remaining.length > 0) {
437
+ await setItemAsync(storage, pkceFlowIndexKey(storageKey), remaining)
438
+ } else {
439
+ await removeItemAsync(storage, pkceFlowIndexKey(storageKey))
440
+ }
441
+ }
442
+
443
+ if (slotValue != null && slotValue === (await getItemAsync(storage, legacyKey))) {
444
+ await removeItemAsync(storage, legacyKey)
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Removes every pending verifier: all slots in the index, the index itself
450
+ * and the fixed legacy key. Used on session teardown (sign-out, invalid
451
+ * session) — matches the pre-slot behavior where tearing down the session
452
+ * deleted the only verifier, and prevents long-lived stale verifier cookies.
453
+ */
454
+ export async function removeAllPKCEVerifiers(
455
+ storage: SupportedStorage,
456
+ storageKey: string
457
+ ): Promise<void> {
458
+ const index = await getPKCEFlowIndex(storage, storageKey)
459
+ for (const flowId of index) {
460
+ await removeItemAsync(storage, pkceVerifierSlotKey(storageKey, flowId))
461
+ }
462
+ await removeItemAsync(storage, pkceFlowIndexKey(storageKey))
463
+ await removeItemAsync(storage, `${storageKey}-code-verifier`)
464
+ }
465
+
466
+ /**
467
+ * Appends the reserved flow id parameter to a `redirectTo` URL, replacing any
468
+ * existing occurrence. String-based (no URL round-trip) so custom schemes
469
+ * (native deep links) and the exact encoding of the app's own parameters
470
+ * survive untouched; an existing fragment stays at the end of the URL.
471
+ */
472
+ export function appendFlowIdToRedirectTo(redirectTo: string, flowId: string): string {
473
+ const hashIndex = redirectTo.indexOf('#')
474
+ let base = hashIndex === -1 ? redirectTo : redirectTo.slice(0, hashIndex)
475
+ const fragment = hashIndex === -1 ? '' : redirectTo.slice(hashIndex)
476
+
477
+ const queryIndex = base.indexOf('?')
478
+ if (queryIndex !== -1) {
479
+ const path = base.slice(0, queryIndex)
480
+ const remaining = base
481
+ .slice(queryIndex + 1)
482
+ .split('&')
483
+ .filter(
484
+ (pair) =>
485
+ pair !== '' && pair !== PKCE_FLOW_ID_PARAM && !pair.startsWith(`${PKCE_FLOW_ID_PARAM}=`)
486
+ )
487
+ base = remaining.length > 0 ? `${path}?${remaining.join('&')}` : path
488
+ }
489
+
490
+ const separator = base.includes('?') ? '&' : '?'
491
+ return `${base}${separator}${PKCE_FLOW_ID_PARAM}=${encodeURIComponent(flowId)}${fragment}`
492
+ }
493
+
304
494
  export async function getCodeChallengeAndMethod(
305
495
  storage: SupportedStorage,
306
496
  storageKey: string,
307
- isPasswordRecovery = false
308
- ) {
497
+ isPasswordRecovery = false,
498
+ onEvictFlow?: (evictedFlowId: string) => void
499
+ ): Promise<[string, string, string]> {
309
500
  const codeVerifier = generatePKCEVerifier()
310
501
  let storedCodeVerifier = codeVerifier
311
502
  if (isPasswordRecovery) {
312
503
  storedCodeVerifier += '/recovery'
313
504
  }
314
- await setItemAsync(storage, `${storageKey}-code-verifier`, storedCodeVerifier)
505
+ const flowId = generatePKCEFlowId()
506
+ await storePKCEVerifier(storage, storageKey, flowId, storedCodeVerifier, onEvictFlow)
315
507
  const codeChallenge = await generatePKCEChallenge(codeVerifier)
316
508
  const codeChallengeMethod = codeVerifier === codeChallenge ? 'plain' : 's256'
317
- return [codeChallenge, codeChallengeMethod]
509
+ return [codeChallenge, codeChallengeMethod, flowId]
318
510
  }
319
511
 
320
512
  /** Parses the API version which is 2YYY-MM-DD. */
package/src/lib/types.ts CHANGED
@@ -190,6 +190,30 @@ export type ExperimentalFeatureFlags = {
190
190
  * disabled throws a descriptive error at call time.
191
191
  */
192
192
  passkey?: boolean
193
+ /**
194
+ * Appends a reserved `sb_flow_id` query parameter to `redirectTo` URLs on
195
+ * PKCE flows. The parameter round-trips through the auth server back to
196
+ * your callback URL, where the client uses it to select the code verifier
197
+ * created by that specific flow — so multiple sign-in flows (e.g. two OAuth
198
+ * providers started in different tabs) can be in flight at the same time
199
+ * without overwriting each other.
200
+ *
201
+ * Before enabling, make sure your [redirect URL allow
202
+ * list](https://supabase.com/docs/guides/auth/redirect-urls) tolerates the
203
+ * extra query parameter: allow-list entries are matched against the full
204
+ * URL including the query string, so an exact entry (no wildcard) stops
205
+ * matching once the parameter is appended and the redirect falls back to
206
+ * your Site URL. Redirects to the Site URL's own origin always pass.
207
+ *
208
+ * Defaults to `false`. Without it, concurrent flows still keep separate
209
+ * verifiers in storage, but no flow id travels through the redirect: to
210
+ * match a callback to its verifier you must carry the `flowId` returned by
211
+ * `signInWithOAuth` (or `linkIdentity`) through your own channel and pass
212
+ * it to `exchangeCodeForSession`. Flows that offer no way to obtain the
213
+ * flow id (email OTP, password recovery, sign-up confirmation) can only be
214
+ * correlated via this flag.
215
+ */
216
+ appendPkceFlowIdToRedirects?: boolean
193
217
  }
194
218
 
195
219
  const WeakPasswordReasons = ['length', 'characters', 'pwned'] as const
@@ -275,6 +299,19 @@ export type OAuthResponse =
275
299
  data: {
276
300
  provider: Provider
277
301
  url: string
302
+ /**
303
+ * Identifier of the PKCE flow started by this call, usable as the
304
+ * `flowId` option of {@link GoTrueClient#exchangeCodeForSession} to
305
+ * select this flow's code verifier when several flows are in flight.
306
+ * `null` on the implicit flow. The id is a selector for a verifier
307
+ * kept in storage — it is not a secret and never contains the
308
+ * verifier itself.
309
+ *
310
+ * Always set at runtime; optional in the type so existing code that
311
+ * constructs `OAuthResponse` values (e.g. test mocks) keeps
312
+ * compiling.
313
+ */
314
+ flowId?: string | null
278
315
  }
279
316
  error: null
280
317
  }
@@ -282,6 +319,7 @@ export type OAuthResponse =
282
319
  data: {
283
320
  provider: Provider
284
321
  url: null
322
+ flowId?: string | null
285
323
  }
286
324
  error: AuthError
287
325
  }
@@ -4,4 +4,4 @@
4
4
  // - Debugging and support (identifying which version is running)
5
5
  // - Telemetry and logging (version reporting in errors/analytics)
6
6
  // - Ensuring build artifacts match the published package version
7
- export const version = '2.110.9'
7
+ export const version = '2.111.0'