@depup/supabase__auth-js 2.103.0-depup.0 → 2.110.9-depup.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 (106) hide show
  1. package/AGENTS.md +11 -0
  2. package/README.md +2 -2
  3. package/changes.json +1 -1
  4. package/dist/main/GoTrueAdminApi.d.ts +45 -8
  5. package/dist/main/GoTrueAdminApi.d.ts.map +1 -1
  6. package/dist/main/GoTrueAdminApi.js +67 -7
  7. package/dist/main/GoTrueAdminApi.js.map +1 -1
  8. package/dist/main/GoTrueClient.d.ts +239 -42
  9. package/dist/main/GoTrueClient.d.ts.map +1 -1
  10. package/dist/main/GoTrueClient.js +952 -164
  11. package/dist/main/GoTrueClient.js.map +1 -1
  12. package/dist/main/lib/constants.d.ts +8 -0
  13. package/dist/main/lib/constants.d.ts.map +1 -1
  14. package/dist/main/lib/constants.js +9 -1
  15. package/dist/main/lib/constants.js.map +1 -1
  16. package/dist/main/lib/errors.d.ts +42 -3
  17. package/dist/main/lib/errors.d.ts.map +1 -1
  18. package/dist/main/lib/errors.js +44 -13
  19. package/dist/main/lib/errors.js.map +1 -1
  20. package/dist/main/lib/fetch.d.ts +28 -8
  21. package/dist/main/lib/fetch.d.ts.map +1 -1
  22. package/dist/main/lib/fetch.js +28 -6
  23. package/dist/main/lib/fetch.js.map +1 -1
  24. package/dist/main/lib/helpers.d.ts +4 -1
  25. package/dist/main/lib/helpers.d.ts.map +1 -1
  26. package/dist/main/lib/helpers.js +15 -4
  27. package/dist/main/lib/helpers.js.map +1 -1
  28. package/dist/main/lib/locks.d.ts +29 -35
  29. package/dist/main/lib/locks.d.ts.map +1 -1
  30. package/dist/main/lib/locks.js +37 -38
  31. package/dist/main/lib/locks.js.map +1 -1
  32. package/dist/main/lib/types.d.ts +305 -64
  33. package/dist/main/lib/types.d.ts.map +1 -1
  34. package/dist/main/lib/types.js.map +1 -1
  35. package/dist/main/lib/version.d.ts +1 -1
  36. package/dist/main/lib/version.js +1 -1
  37. package/dist/main/lib/webauthn.d.ts +8 -0
  38. package/dist/main/lib/webauthn.d.ts.map +1 -1
  39. package/dist/main/lib/webauthn.dom.d.ts +3 -3
  40. package/dist/main/lib/webauthn.dom.d.ts.map +1 -1
  41. package/dist/main/lib/webauthn.errors.d.ts +5 -0
  42. package/dist/main/lib/webauthn.errors.d.ts.map +1 -1
  43. package/dist/main/lib/webauthn.errors.js +7 -0
  44. package/dist/main/lib/webauthn.errors.js.map +1 -1
  45. package/dist/main/lib/webauthn.js +1 -0
  46. package/dist/main/lib/webauthn.js.map +1 -1
  47. package/dist/module/GoTrueAdminApi.d.ts +45 -8
  48. package/dist/module/GoTrueAdminApi.d.ts.map +1 -1
  49. package/dist/module/GoTrueAdminApi.js +68 -8
  50. package/dist/module/GoTrueAdminApi.js.map +1 -1
  51. package/dist/module/GoTrueClient.d.ts +239 -42
  52. package/dist/module/GoTrueClient.d.ts.map +1 -1
  53. package/dist/module/GoTrueClient.js +957 -169
  54. package/dist/module/GoTrueClient.js.map +1 -1
  55. package/dist/module/lib/constants.d.ts +8 -0
  56. package/dist/module/lib/constants.d.ts.map +1 -1
  57. package/dist/module/lib/constants.js +8 -0
  58. package/dist/module/lib/constants.js.map +1 -1
  59. package/dist/module/lib/errors.d.ts +42 -3
  60. package/dist/module/lib/errors.d.ts.map +1 -1
  61. package/dist/module/lib/errors.js +41 -12
  62. package/dist/module/lib/errors.js.map +1 -1
  63. package/dist/module/lib/fetch.d.ts +28 -8
  64. package/dist/module/lib/fetch.d.ts.map +1 -1
  65. package/dist/module/lib/fetch.js +28 -6
  66. package/dist/module/lib/fetch.js.map +1 -1
  67. package/dist/module/lib/helpers.d.ts +4 -1
  68. package/dist/module/lib/helpers.d.ts.map +1 -1
  69. package/dist/module/lib/helpers.js +14 -4
  70. package/dist/module/lib/helpers.js.map +1 -1
  71. package/dist/module/lib/locks.d.ts +29 -35
  72. package/dist/module/lib/locks.d.ts.map +1 -1
  73. package/dist/module/lib/locks.js +37 -38
  74. package/dist/module/lib/locks.js.map +1 -1
  75. package/dist/module/lib/types.d.ts +305 -64
  76. package/dist/module/lib/types.d.ts.map +1 -1
  77. package/dist/module/lib/types.js.map +1 -1
  78. package/dist/module/lib/version.d.ts +1 -1
  79. package/dist/module/lib/version.js +1 -1
  80. package/dist/module/lib/webauthn.d.ts +8 -0
  81. package/dist/module/lib/webauthn.d.ts.map +1 -1
  82. package/dist/module/lib/webauthn.dom.d.ts +3 -3
  83. package/dist/module/lib/webauthn.dom.d.ts.map +1 -1
  84. package/dist/module/lib/webauthn.errors.d.ts +5 -0
  85. package/dist/module/lib/webauthn.errors.d.ts.map +1 -1
  86. package/dist/module/lib/webauthn.errors.js +7 -0
  87. package/dist/module/lib/webauthn.errors.js.map +1 -1
  88. package/dist/module/lib/webauthn.js +1 -1
  89. package/dist/module/lib/webauthn.js.map +1 -1
  90. package/dist/tsconfig.module.tsbuildinfo +1 -1
  91. package/dist/tsconfig.tsbuildinfo +1 -1
  92. package/migrations/README.md +25 -0
  93. package/migrations/lockless-coordination.md +155 -0
  94. package/package.json +25 -12
  95. package/src/GoTrueAdminApi.ts +99 -7
  96. package/src/GoTrueClient.ts +1125 -181
  97. package/src/lib/constants.ts +9 -0
  98. package/src/lib/errors.ts +76 -9
  99. package/src/lib/fetch.ts +66 -24
  100. package/src/lib/helpers.ts +19 -6
  101. package/src/lib/locks.ts +44 -42
  102. package/src/lib/types.ts +374 -82
  103. package/src/lib/version.ts +1 -1
  104. package/src/lib/webauthn.dom.ts +3 -3
  105. package/src/lib/webauthn.errors.ts +12 -0
  106. package/src/lib/webauthn.ts +1 -1
@@ -25,9 +25,16 @@ const DEFAULT_OPTIONS = {
25
25
  debug: false,
26
26
  hasCustomAuthorizationHeader: false,
27
27
  throwOnError: false,
28
- lockAcquireTimeout: 5000, // 5 seconds
28
+ lockAcquireTimeout: 5000, // 5 seconds. Only used when a custom `lock` is supplied. TODO(v3): remove.
29
29
  skipAutoInitialize: false,
30
+ experimental: {},
30
31
  };
32
+ /**
33
+ * No-op lock used internally as a placeholder. Kept so older test setups that
34
+ * inject this exact reference do not break; new code never sees it because
35
+ * `this.lock` stays `null` when no custom lock is supplied (lockless path).
36
+ * TODO(v3): remove with the legacy lock path.
37
+ */
31
38
  async function lockNoOp(name, acquireTimeout, fn) {
32
39
  return await fn();
33
40
  }
@@ -61,13 +68,21 @@ class GoTrueClient {
61
68
  /**
62
69
  * Create a new client for use in the browser.
63
70
  *
64
- * @example
71
+ * @example Using supabase-js (recommended)
72
+ * ```ts
73
+ * import { createClient } from '@supabase/supabase-js'
74
+ *
75
+ * const supabase = createClient('https://xyzcompany.supabase.co', 'your-publishable-key')
76
+ * const { data, error } = await supabase.auth.getUser()
77
+ * ```
78
+ *
79
+ * @example Standalone import for bundle-sensitive environments
65
80
  * ```ts
66
81
  * import { GoTrueClient } from '@supabase/auth-js'
67
82
  *
68
83
  * const auth = new GoTrueClient({
69
84
  * url: 'https://xyzcompany.supabase.co/auth/v1',
70
- * headers: { apikey: 'public-anon-key' },
85
+ * headers: { apikey: 'your-publishable-key' },
71
86
  * storageKey: 'supabase-auth',
72
87
  * })
73
88
  * ```
@@ -84,6 +99,29 @@ class GoTrueClient {
84
99
  this.autoRefreshTickTimeout = null;
85
100
  this.visibilityChangedCallback = null;
86
101
  this.refreshingDeferred = null;
102
+ /**
103
+ * Cache of the most recent refresh failure, keyed by the refresh token
104
+ * that failed. Serial callers passing the *same* token within
105
+ * `REFRESH_FAILURE_COOLDOWN_MS` (including subsequent auto-refresh ticks)
106
+ * receive this cached result instead of firing another `/token` request.
107
+ * Callers passing a *different* token (token rotation pickup, explicit
108
+ * `setSession`/`refreshSession({ refresh_token })`, multi-account switch)
109
+ * bypass the cache and attempt a fresh refresh as they should.
110
+ * Cleared on any successful refresh (locally or via BroadcastChannel from
111
+ * another tab) and on `_removeSession`.
112
+ *
113
+ * Pairs with `refreshingDeferred`: concurrent callers share the in-flight
114
+ * promise, serial callers within the cooldown share the failure result.
115
+ */
116
+ this.lastRefreshFailure = null;
117
+ /**
118
+ * Monotonic counter incremented at the top of `_removeSession`, before any
119
+ * `await`. The commit guard inside `_callRefreshToken` captures this value
120
+ * before `_saveSession` and re-checks it after, so a `signOut` that
121
+ * interleaves inside `_saveSession`'s storage-write awaits is still caught
122
+ * (the post-fetch storage snapshot alone misses that window).
123
+ */
124
+ this._sessionRemovalEpoch = 0;
87
125
  /**
88
126
  * Keeps track of the async client initialization.
89
127
  * When null or not yet resolved the auth state is `unknown`
@@ -91,9 +129,29 @@ class GoTrueClient {
91
129
  * Keep extra care to never reject or throw uncaught errors
92
130
  */
93
131
  this.initializePromise = null;
132
+ /**
133
+ * Non-null only while `initialize()` is running. While open,
134
+ * `_notifyAllSubscribers` enqueues init-chain notifications (those fired with
135
+ * `broadcast = true`, i.e. from `_recoverAndRefresh`) into this array instead
136
+ * of firing directly, so that `initializePromise` is guaranteed to be
137
+ * resolved before any subscriber callback runs. Callbacks that call
138
+ * `getSession()` / `getUser()` etc. would otherwise deadlock because those
139
+ * methods await `initializePromise`. Notifications from the incoming
140
+ * BroadcastChannel handler (`broadcast = false`) are not enqueued — they fire
141
+ * immediately. Flushed (in order) by `initialize()` after `initializePromise`
142
+ * settles.
143
+ */
144
+ this._pendingInitNotifications = null;
94
145
  this.detectSessionInUrl = true;
95
146
  this.hasCustomAuthorizationHeader = false;
96
147
  this.suppressGetSessionWarning = false;
148
+ /**
149
+ * Custom lock function passed via `settings.lock`. When non-null, every auth
150
+ * operation runs inside `_acquireLock`. When null (the default), the client
151
+ * uses its lockless coordination (refresh single-flight + commit guard).
152
+ * TODO(v3): remove along with the legacy lock path.
153
+ */
154
+ this.lock = null;
97
155
  this.lockAcquired = false;
98
156
  this.pendingInLock = [];
99
157
  /**
@@ -118,29 +176,32 @@ class GoTrueClient {
118
176
  }
119
177
  this.persistSession = settings.persistSession;
120
178
  this.autoRefreshToken = settings.autoRefreshToken;
179
+ this.experimental = (_b = settings.experimental) !== null && _b !== void 0 ? _b : {};
121
180
  this.admin = new GoTrueAdminApi_1.default({
122
181
  url: settings.url,
123
182
  headers: settings.headers,
124
183
  fetch: settings.fetch,
184
+ experimental: this.experimental,
125
185
  });
126
186
  this.url = settings.url;
127
187
  this.headers = settings.headers;
128
188
  this.fetch = (0, helpers_1.resolveFetch)(settings.fetch);
129
- this.lock = settings.lock || lockNoOp;
130
189
  this.detectSessionInUrl = settings.detectSessionInUrl;
131
190
  this.flowType = settings.flowType;
132
191
  this.hasCustomAuthorizationHeader = settings.hasCustomAuthorizationHeader;
133
192
  this.throwOnError = settings.throwOnError;
193
+ // Always wire `lockAcquireTimeout` even on the lockless path: consumers
194
+ // (including supabase-js tests) read it off the client to verify option
195
+ // flow-through.
134
196
  this.lockAcquireTimeout = settings.lockAcquireTimeout;
135
- if (settings.lock) {
197
+ // TODO(v3): remove. Legacy opt-in path preserved for backwards
198
+ // compatibility with callers passing a custom `lock` (typically React
199
+ // Native `processLock` or Node multi-process setups). When `settings.lock`
200
+ // is null the client uses its lockless coordination — no `navigator.locks`
201
+ // by default, no implicit `processLock`.
202
+ if (settings.lock != null) {
136
203
  this.lock = settings.lock;
137
204
  }
138
- else if (this.persistSession && (0, helpers_1.isBrowser)() && ((_b = globalThis === null || globalThis === void 0 ? void 0 : globalThis.navigator) === null || _b === void 0 ? void 0 : _b.locks)) {
139
- this.lock = locks_1.navigatorLock;
140
- }
141
- else {
142
- this.lock = lockNoOp;
143
- }
144
205
  if (!this.jwks) {
145
206
  this.jwks = { keys: [] };
146
207
  this.jwks_cached_at = Number.MIN_SAFE_INTEGER;
@@ -162,6 +223,15 @@ class GoTrueClient {
162
223
  listGrants: this._listOAuthGrants.bind(this),
163
224
  revokeGrant: this._revokeOAuthGrant.bind(this),
164
225
  };
226
+ this.passkey = {
227
+ startRegistration: this._startPasskeyRegistration.bind(this),
228
+ verifyRegistration: this._verifyPasskeyRegistration.bind(this),
229
+ startAuthentication: this._startPasskeyAuthentication.bind(this),
230
+ verifyAuthentication: this._verifyPasskeyAuthentication.bind(this),
231
+ list: this._listPasskeys.bind(this),
232
+ update: this._updatePasskey.bind(this),
233
+ delete: this._deletePasskey.bind(this),
234
+ };
165
235
  if (this.persistSession) {
166
236
  if (settings.storage) {
167
237
  this.storage = settings.storage;
@@ -192,6 +262,12 @@ class GoTrueClient {
192
262
  }
193
263
  (_c = this.broadcastChannel) === null || _c === void 0 ? void 0 : _c.addEventListener('message', async (event) => {
194
264
  this._debug('received broadcast notification from other tab or client', event);
265
+ // Another tab successfully refreshed or signed in — any cached
266
+ // failure in this tab is stale and should not block the next
267
+ // refresh attempt.
268
+ if (event.data.event === 'TOKEN_REFRESHED' || event.data.event === 'SIGNED_IN') {
269
+ this.lastRefreshFailure = null;
270
+ }
195
271
  try {
196
272
  await this._notifyAllSubscribers(event.data.event, event.data.session, false); // broadcast = false so we don't get an endless loop of messages
197
273
  }
@@ -237,22 +313,55 @@ class GoTrueClient {
237
313
  return this;
238
314
  }
239
315
  /**
240
- * Initializes the client session either from the url or from storage.
241
- * This method is automatically called when instantiating the client, but should also be called
242
- * manually when checking for an error from an auth redirect (oauth, magiclink, password recovery, etc).
316
+ * Initialize the auth client by loading the session from storage or
317
+ * detecting it from the URL after an OAuth, magic-link, or password-recovery
318
+ * redirect.
319
+ *
320
+ * **Most callers do not need to invoke this directly.** The client calls it
321
+ * automatically during construction, and to react to sign-in events (including
322
+ * post-redirect events) you should subscribe to `onAuthStateChange` rather
323
+ * than awaiting `initialize()`.
324
+ *
325
+ * You only need to call it manually when you have opted out of the automatic
326
+ * call by passing `skipAutoInitialize: true` — for example, in an SSR context
327
+ * where you need to control initialization timing. In that case, awaiting
328
+ * `initialize()` returns the resolved session result (or any error encountered
329
+ * while detecting it from the URL).
243
330
  *
244
331
  * @category Auth
245
332
  */
246
333
  async initialize() {
334
+ var _a;
247
335
  if (this.initializePromise) {
248
336
  return await this.initializePromise;
249
337
  }
338
+ // Open the notification queue before _initialize() runs so that every
339
+ // _notifyAllSubscribers call inside the init chain enqueues instead of
340
+ // firing. Without this, a callback receiving SIGNED_IN (or TOKEN_REFRESHED
341
+ // / SIGNED_OUT) during _recoverAndRefresh would deadlock if it called
342
+ // getSession() / getUser() — those methods await initializePromise, which
343
+ // can only resolve after the callback returns, which can only return after
344
+ // getSession() resolves. The queue is flushed below after initializePromise
345
+ // has settled, so callbacks run with a fully resolved initializePromise.
346
+ this._pendingInitNotifications = [];
250
347
  this.initializePromise = (async () => {
251
- return await this._acquireLock(this.lockAcquireTimeout, async () => {
252
- return await this._initialize();
253
- });
348
+ if (this.lock != null) {
349
+ // TODO(v3): remove legacy lock path
350
+ return await this._acquireLock(this.lockAcquireTimeout, async () => {
351
+ return await this._initialize();
352
+ });
353
+ }
354
+ return await this._initialize();
254
355
  })();
255
- return await this.initializePromise;
356
+ const result = await this.initializePromise;
357
+ // initializePromise is now resolved — flush queued notifications in order.
358
+ // Callbacks can safely call getSession() / getUser() / signOut() etc.
359
+ const queue = (_a = this._pendingInitNotifications) !== null && _a !== void 0 ? _a : [];
360
+ this._pendingInitNotifications = null;
361
+ for (const n of queue) {
362
+ await this._notifyAllSubscribers(n.event, n.session, n.broadcast);
363
+ }
364
+ return result;
256
365
  }
257
366
  /**
258
367
  * IMPORTANT:
@@ -786,6 +895,21 @@ class GoTrueClient {
786
895
  * password: 'some-password',
787
896
  * })
788
897
  * ```
898
+ *
899
+ * @exampleDescription Handling errors
900
+ * Log the full `error` object so fields like `code`, `status`, and `name` aren't hidden. The `error.code` (e.g. `'invalid_credentials'`, `'email_not_confirmed'`) is often more useful for branching than `error.message`, and the full object surfaces both.
901
+ *
902
+ * @example Handling errors
903
+ * ```js
904
+ * const { data, error } = await supabase.auth.signInWithPassword({
905
+ * email: 'example@email.com',
906
+ * password: 'example-password',
907
+ * })
908
+ * if (error) {
909
+ * console.error(error)
910
+ * return
911
+ * }
912
+ * ```
789
913
  */
790
914
  async signInWithPassword(credentials) {
791
915
  try {
@@ -1101,9 +1225,13 @@ class GoTrueClient {
1101
1225
  */
1102
1226
  async exchangeCodeForSession(authCode) {
1103
1227
  await this.initializePromise;
1104
- return this._acquireLock(this.lockAcquireTimeout, async () => {
1105
- return this._exchangeCodeForSession(authCode);
1106
- });
1228
+ if (this.lock != null) {
1229
+ // TODO(v3): remove legacy lock path
1230
+ return this._acquireLock(this.lockAcquireTimeout, async () => {
1231
+ return this._exchangeCodeForSession(authCode);
1232
+ });
1233
+ }
1234
+ return this._exchangeCodeForSession(authCode);
1107
1235
  }
1108
1236
  /**
1109
1237
  * Signs in a user by verifying a message signed by the user's private key.
@@ -1205,7 +1333,7 @@ class GoTrueClient {
1205
1333
  }
1206
1334
  }
1207
1335
  async signInWithEthereum(credentials) {
1208
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
1336
+ var _a, _b, _c, _d, _f, _g, _h, _j, _k, _l, _m;
1209
1337
  // TODO: flatten type
1210
1338
  let message;
1211
1339
  let signature;
@@ -1265,11 +1393,11 @@ class GoTrueClient {
1265
1393
  version: '1',
1266
1394
  chainId: chainId,
1267
1395
  nonce: (_c = options === null || options === void 0 ? void 0 : options.signInWithEthereum) === null || _c === void 0 ? void 0 : _c.nonce,
1268
- issuedAt: (_e = (_d = options === null || options === void 0 ? void 0 : options.signInWithEthereum) === null || _d === void 0 ? void 0 : _d.issuedAt) !== null && _e !== void 0 ? _e : new Date(),
1269
- expirationTime: (_f = options === null || options === void 0 ? void 0 : options.signInWithEthereum) === null || _f === void 0 ? void 0 : _f.expirationTime,
1270
- notBefore: (_g = options === null || options === void 0 ? void 0 : options.signInWithEthereum) === null || _g === void 0 ? void 0 : _g.notBefore,
1271
- requestId: (_h = options === null || options === void 0 ? void 0 : options.signInWithEthereum) === null || _h === void 0 ? void 0 : _h.requestId,
1272
- resources: (_j = options === null || options === void 0 ? void 0 : options.signInWithEthereum) === null || _j === void 0 ? void 0 : _j.resources,
1396
+ issuedAt: (_f = (_d = options === null || options === void 0 ? void 0 : options.signInWithEthereum) === null || _d === void 0 ? void 0 : _d.issuedAt) !== null && _f !== void 0 ? _f : new Date(),
1397
+ expirationTime: (_g = options === null || options === void 0 ? void 0 : options.signInWithEthereum) === null || _g === void 0 ? void 0 : _g.expirationTime,
1398
+ notBefore: (_h = options === null || options === void 0 ? void 0 : options.signInWithEthereum) === null || _h === void 0 ? void 0 : _h.notBefore,
1399
+ requestId: (_j = options === null || options === void 0 ? void 0 : options.signInWithEthereum) === null || _j === void 0 ? void 0 : _j.requestId,
1400
+ resources: (_k = options === null || options === void 0 ? void 0 : options.signInWithEthereum) === null || _k === void 0 ? void 0 : _k.resources,
1273
1401
  };
1274
1402
  message = (0, ethereum_1.createSiweMessage)(siweMessage);
1275
1403
  // Sign message
@@ -1282,8 +1410,8 @@ class GoTrueClient {
1282
1410
  const { data, error } = await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/token?grant_type=web3`, {
1283
1411
  headers: this.headers,
1284
1412
  body: Object.assign({ chain: 'ethereum', message,
1285
- signature }, (((_k = credentials.options) === null || _k === void 0 ? void 0 : _k.captchaToken)
1286
- ? { gotrue_meta_security: { captcha_token: (_l = credentials.options) === null || _l === void 0 ? void 0 : _l.captchaToken } }
1413
+ signature }, (((_l = credentials.options) === null || _l === void 0 ? void 0 : _l.captchaToken)
1414
+ ? { gotrue_meta_security: { captcha_token: (_m = credentials.options) === null || _m === void 0 ? void 0 : _m.captchaToken } }
1287
1415
  : null)),
1288
1416
  xform: fetch_1._sessionResponse,
1289
1417
  });
@@ -1308,7 +1436,7 @@ class GoTrueClient {
1308
1436
  }
1309
1437
  }
1310
1438
  async signInWithSolana(credentials) {
1311
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
1439
+ var _a, _b, _c, _d, _f, _g, _h, _j, _k, _l, _m, _o;
1312
1440
  let message;
1313
1441
  let signature;
1314
1442
  if ('message' in credentials) {
@@ -1393,17 +1521,17 @@ class GoTrueClient {
1393
1521
  ...(((_d = options === null || options === void 0 ? void 0 : options.signInWithSolana) === null || _d === void 0 ? void 0 : _d.notBefore)
1394
1522
  ? [`Not Before: ${options.signInWithSolana.notBefore}`]
1395
1523
  : []),
1396
- ...(((_e = options === null || options === void 0 ? void 0 : options.signInWithSolana) === null || _e === void 0 ? void 0 : _e.expirationTime)
1524
+ ...(((_f = options === null || options === void 0 ? void 0 : options.signInWithSolana) === null || _f === void 0 ? void 0 : _f.expirationTime)
1397
1525
  ? [`Expiration Time: ${options.signInWithSolana.expirationTime}`]
1398
1526
  : []),
1399
- ...(((_f = options === null || options === void 0 ? void 0 : options.signInWithSolana) === null || _f === void 0 ? void 0 : _f.chainId)
1527
+ ...(((_g = options === null || options === void 0 ? void 0 : options.signInWithSolana) === null || _g === void 0 ? void 0 : _g.chainId)
1400
1528
  ? [`Chain ID: ${options.signInWithSolana.chainId}`]
1401
1529
  : []),
1402
- ...(((_g = options === null || options === void 0 ? void 0 : options.signInWithSolana) === null || _g === void 0 ? void 0 : _g.nonce) ? [`Nonce: ${options.signInWithSolana.nonce}`] : []),
1403
- ...(((_h = options === null || options === void 0 ? void 0 : options.signInWithSolana) === null || _h === void 0 ? void 0 : _h.requestId)
1530
+ ...(((_h = options === null || options === void 0 ? void 0 : options.signInWithSolana) === null || _h === void 0 ? void 0 : _h.nonce) ? [`Nonce: ${options.signInWithSolana.nonce}`] : []),
1531
+ ...(((_j = options === null || options === void 0 ? void 0 : options.signInWithSolana) === null || _j === void 0 ? void 0 : _j.requestId)
1404
1532
  ? [`Request ID: ${options.signInWithSolana.requestId}`]
1405
1533
  : []),
1406
- ...(((_k = (_j = options === null || options === void 0 ? void 0 : options.signInWithSolana) === null || _j === void 0 ? void 0 : _j.resources) === null || _k === void 0 ? void 0 : _k.length)
1534
+ ...(((_l = (_k = options === null || options === void 0 ? void 0 : options.signInWithSolana) === null || _k === void 0 ? void 0 : _k.resources) === null || _l === void 0 ? void 0 : _l.length)
1407
1535
  ? [
1408
1536
  'Resources',
1409
1537
  ...options.signInWithSolana.resources.map((resource) => `- ${resource}`),
@@ -1420,8 +1548,8 @@ class GoTrueClient {
1420
1548
  try {
1421
1549
  const { data, error } = await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/token?grant_type=web3`, {
1422
1550
  headers: this.headers,
1423
- body: Object.assign({ chain: 'solana', message, signature: (0, base64url_1.bytesToBase64URL)(signature) }, (((_l = credentials.options) === null || _l === void 0 ? void 0 : _l.captchaToken)
1424
- ? { gotrue_meta_security: { captcha_token: (_m = credentials.options) === null || _m === void 0 ? void 0 : _m.captchaToken } }
1551
+ body: Object.assign({ chain: 'solana', message, signature: (0, base64url_1.bytesToBase64URL)(signature) }, (((_m = credentials.options) === null || _m === void 0 ? void 0 : _m.captchaToken)
1552
+ ? { gotrue_meta_security: { captcha_token: (_o = credentials.options) === null || _o === void 0 ? void 0 : _o.captchaToken } }
1425
1553
  : null)),
1426
1554
  xform: fetch_1._sessionResponse,
1427
1555
  });
@@ -1473,7 +1601,7 @@ class GoTrueClient {
1473
1601
  }
1474
1602
  if (data.session) {
1475
1603
  await this._saveSession(data.session);
1476
- await this._notifyAllSubscribers('SIGNED_IN', data.session);
1604
+ await this._notifyAllSubscribers(redirectType === 'recovery' ? 'PASSWORD_RECOVERY' : 'SIGNED_IN', data.session);
1477
1605
  }
1478
1606
  return this._returnResult({ data: Object.assign(Object.assign({}, data), { redirectType: redirectType !== null && redirectType !== void 0 ? redirectType : null }), error });
1479
1607
  }
@@ -1676,7 +1804,7 @@ class GoTrueClient {
1676
1804
  * ```
1677
1805
  */
1678
1806
  async signInWithOtp(credentials) {
1679
- var _a, _b, _c, _d, _e;
1807
+ var _a, _b, _c, _d, _f;
1680
1808
  try {
1681
1809
  if ('email' in credentials) {
1682
1810
  const { email, options } = credentials;
@@ -1709,7 +1837,7 @@ class GoTrueClient {
1709
1837
  data: (_c = options === null || options === void 0 ? void 0 : options.data) !== null && _c !== void 0 ? _c : {},
1710
1838
  create_user: (_d = options === null || options === void 0 ? void 0 : options.shouldCreateUser) !== null && _d !== void 0 ? _d : true,
1711
1839
  gotrue_meta_security: { captcha_token: options === null || options === void 0 ? void 0 : options.captchaToken },
1712
- channel: (_e = options === null || options === void 0 ? void 0 : options.channel) !== null && _e !== void 0 ? _e : 'sms',
1840
+ channel: (_f = options === null || options === void 0 ? void 0 : options.channel) !== null && _f !== void 0 ? _f : 'sms',
1713
1841
  },
1714
1842
  });
1715
1843
  return this._returnResult({
@@ -1955,7 +2083,7 @@ class GoTrueClient {
1955
2083
  * ```
1956
2084
  */
1957
2085
  async signInWithSSO(params) {
1958
- var _a, _b, _c, _d, _e;
2086
+ var _a, _b, _c, _d, _f;
1959
2087
  try {
1960
2088
  let codeChallenge = null;
1961
2089
  let codeChallengeMethod = null;
@@ -1971,7 +2099,7 @@ class GoTrueClient {
1971
2099
  xform: fetch_1._ssoResponse,
1972
2100
  });
1973
2101
  // Automatically redirect in browser unless skipBrowserRedirect is true
1974
- if (((_d = result.data) === null || _d === void 0 ? void 0 : _d.url) && (0, helpers_1.isBrowser)() && !((_e = params.options) === null || _e === void 0 ? void 0 : _e.skipBrowserRedirect)) {
2102
+ if (((_d = result.data) === null || _d === void 0 ? void 0 : _d.url) && (0, helpers_1.isBrowser)() && !((_f = params.options) === null || _f === void 0 ? void 0 : _f.skipBrowserRedirect)) {
1975
2103
  window.location.assign(result.data.url);
1976
2104
  }
1977
2105
  return this._returnResult(result);
@@ -2007,9 +2135,13 @@ class GoTrueClient {
2007
2135
  */
2008
2136
  async reauthenticate() {
2009
2137
  await this.initializePromise;
2010
- return await this._acquireLock(this.lockAcquireTimeout, async () => {
2011
- return await this._reauthenticate();
2012
- });
2138
+ if (this.lock != null) {
2139
+ // TODO(v3): remove legacy lock path
2140
+ return await this._acquireLock(this.lockAcquireTimeout, async () => {
2141
+ return await this._reauthenticate();
2142
+ });
2143
+ }
2144
+ return await this._reauthenticate();
2013
2145
  }
2014
2146
  async _reauthenticate() {
2015
2147
  try {
@@ -2097,15 +2229,26 @@ class GoTrueClient {
2097
2229
  const endpoint = `${this.url}/resend`;
2098
2230
  if ('email' in credentials) {
2099
2231
  const { email, type, options } = credentials;
2232
+ let codeChallenge = null;
2233
+ let codeChallengeMethod = null;
2234
+ if (this.flowType === 'pkce') {
2235
+ ;
2236
+ [codeChallenge, codeChallengeMethod] = await (0, helpers_1.getCodeChallengeAndMethod)(this.storage, this.storageKey);
2237
+ }
2100
2238
  const { error } = await (0, fetch_1._request)(this.fetch, 'POST', endpoint, {
2101
2239
  headers: this.headers,
2102
2240
  body: {
2103
2241
  email,
2104
2242
  type,
2105
2243
  gotrue_meta_security: { captcha_token: options === null || options === void 0 ? void 0 : options.captchaToken },
2244
+ code_challenge: codeChallenge,
2245
+ code_challenge_method: codeChallengeMethod,
2106
2246
  },
2107
2247
  redirectTo: options === null || options === void 0 ? void 0 : options.emailRedirectTo,
2108
2248
  });
2249
+ if (error) {
2250
+ await (0, helpers_1.removeItemAsync)(this.storage, `${this.storageKey}-code-verifier`);
2251
+ }
2109
2252
  return this._returnResult({ data: { user: null, session: null }, error });
2110
2253
  }
2111
2254
  else if ('phone' in credentials) {
@@ -2126,6 +2269,7 @@ class GoTrueClient {
2126
2269
  throw new errors_1.AuthInvalidCredentialsError('You must provide either an email or phone number and a type');
2127
2270
  }
2128
2271
  catch (error) {
2272
+ await (0, helpers_1.removeItemAsync)(this.storage, `${this.storageKey}-code-verifier`);
2129
2273
  if ((0, errors_1.isAuthError)(error)) {
2130
2274
  return this._returnResult({ data: { user: null, session: null }, error });
2131
2275
  }
@@ -2141,7 +2285,7 @@ class GoTrueClient {
2141
2285
  * to the client. If that storage is based on request cookies for example,
2142
2286
  * the values in it may not be authentic and therefore it's strongly advised
2143
2287
  * against using this method and its results in such circumstances. A warning
2144
- * will be emitted if this is detected. Use {@link #getUser()} instead.
2288
+ * will be emitted if this is detected. Use {@link GoTrueClient.getUser} instead.
2145
2289
  *
2146
2290
  * @category Auth
2147
2291
  *
@@ -2152,7 +2296,7 @@ class GoTrueClient {
2152
2296
  * - If the session's access token is expired or is about to expire, this method will use the refresh token to refresh the session.
2153
2297
  * - When using in a browser, or you've called `startAutoRefresh()` in your environment (React Native, etc.) this function always returns a valid access token without refreshing the session itself, as this is done in the background. This function returns very fast.
2154
2298
  * - **IMPORTANT SECURITY NOTICE:** If using an insecure storage medium, such as cookies or request headers, the user object returned by this function **must not be trusted**. Always verify the JWT using `getClaims()` or your own JWT verification library to securely establish the user's identity and access. You can also use `getUser()` to fetch the user object directly from the Auth server for this purpose.
2155
- * - When using in a browser, this function is synchronized across all tabs using the [LockManager](https://developer.mozilla.org/en-US/docs/Web/API/LockManager) API. In other environments make sure you've defined a proper `lock` property, if necessary, to make sure there are no race conditions while the session is being refreshed.
2299
+ * - Cross-tab refresh races are handled by the GoTrue server (the rotated token from the first tab is returned to subsequent tabs via the parent-of-active mechanism), so no client-side serialization is needed.
2156
2300
  *
2157
2301
  * @example Get the session data
2158
2302
  * ```js
@@ -2219,15 +2363,24 @@ class GoTrueClient {
2219
2363
  */
2220
2364
  async getSession() {
2221
2365
  await this.initializePromise;
2222
- const result = await this._acquireLock(this.lockAcquireTimeout, async () => {
2223
- return this._useSession(async (result) => {
2224
- return result;
2366
+ if (this.lock != null) {
2367
+ // TODO(v3): remove legacy lock path
2368
+ return await this._acquireLock(this.lockAcquireTimeout, async () => {
2369
+ return this._useSession(async (result) => {
2370
+ return result;
2371
+ });
2225
2372
  });
2373
+ }
2374
+ return await this._useSession(async (result) => {
2375
+ return result;
2226
2376
  });
2227
- return result;
2228
2377
  }
2229
2378
  /**
2230
2379
  * Acquires a global lock based on the storage key.
2380
+ *
2381
+ * TODO(v3): remove along with the legacy lock path. Only called when
2382
+ * `this.lock` is non-null (custom lock supplied via constructor). The
2383
+ * default lockless path bypasses this entirely.
2231
2384
  */
2232
2385
  async _acquireLock(acquireTimeout, fn) {
2233
2386
  this._debug('#_acquireLock', 'begin', acquireTimeout);
@@ -2244,7 +2397,7 @@ class GoTrueClient {
2244
2397
  try {
2245
2398
  await result;
2246
2399
  }
2247
- catch (e) {
2400
+ catch (_e) {
2248
2401
  // we just care if it finished
2249
2402
  }
2250
2403
  })());
@@ -2283,15 +2436,17 @@ class GoTrueClient {
2283
2436
  }
2284
2437
  }
2285
2438
  /**
2286
- * Use instead of {@link #getSession} inside the library. It is
2287
- * semantically usually what you want, as getting a session involves some
2288
- * processing afterwards that requires only one client operating on the
2289
- * session at once across multiple tabs or processes.
2439
+ * Use instead of {@link GoTrueClient.getSession} inside the library. Loads the session
2440
+ * via `__loadSession` (which may trigger a refresh if the access token is
2441
+ * within the expiry margin) and runs `fn` with the result.
2290
2442
  */
2291
2443
  async _useSession(fn) {
2292
2444
  this._debug('#_useSession', 'begin');
2293
2445
  try {
2294
- // the use of __loadSession here is the only correct use of the function!
2446
+ // Concurrent callers may both reach __loadSession; storage reads are
2447
+ // idempotent, and the only write path inside it (refresh) is
2448
+ // single-flighted downstream by `refreshingDeferred` in
2449
+ // `_callRefreshToken`. No serialization is needed at this layer.
2295
2450
  const result = await this.__loadSession();
2296
2451
  return await fn(result);
2297
2452
  }
@@ -2302,11 +2457,12 @@ class GoTrueClient {
2302
2457
  /**
2303
2458
  * NEVER USE DIRECTLY!
2304
2459
  *
2305
- * Always use {@link #_useSession}.
2460
+ * Always use `_useSession`.
2306
2461
  */
2307
2462
  async __loadSession() {
2308
2463
  this._debug('#__loadSession()', 'begin');
2309
- if (!this.lockAcquired) {
2464
+ if (this.lock != null && !this.lockAcquired) {
2465
+ // TODO(v3): remove. Only meaningful on the legacy lock path.
2310
2466
  this._debug('#__loadSession()', 'used outside of an acquired lock!', new Error().stack);
2311
2467
  }
2312
2468
  try {
@@ -2360,6 +2516,25 @@ class GoTrueClient {
2360
2516
  }
2361
2517
  const { data: session, error } = await this._callRefreshToken(currentSession.refresh_token);
2362
2518
  if (error) {
2519
+ // Proactive-preserve mirror: `_callRefreshToken` keeps the session
2520
+ // in storage when refresh fails non-retryably but the access token
2521
+ // is still inside its real expiry window. Hand the caller the
2522
+ // still-valid session instead of translating the refresh error
2523
+ // into `session: null`. If the access token has actually expired,
2524
+ // the session is genuinely dead and the error stands. Explicit
2525
+ // refresh entry points (`refreshSession`, `setSession`)
2526
+ // intentionally bypass this fallback — they want to know the
2527
+ // refresh failed.
2528
+ const accessTokenStillValid = !!(currentSession.expires_at && currentSession.expires_at * 1000 > Date.now());
2529
+ if (accessTokenStillValid) {
2530
+ // Race guard: a concurrent `signOut` may have cleared storage
2531
+ // during the refresh attempt. Don't hand back a session that no
2532
+ // longer exists on disk.
2533
+ const stillStored = (await (0, helpers_1.getItemAsync)(this.storage, this.storageKey));
2534
+ if (stillStored && stillStored.refresh_token === currentSession.refresh_token) {
2535
+ return this._returnResult({ data: { session: currentSession }, error: null });
2536
+ }
2537
+ }
2363
2538
  return this._returnResult({ data: { session: null }, error });
2364
2539
  }
2365
2540
  return this._returnResult({ data: { session }, error: null });
@@ -2449,9 +2624,16 @@ class GoTrueClient {
2449
2624
  return await this._getUser(jwt);
2450
2625
  }
2451
2626
  await this.initializePromise;
2452
- const result = await this._acquireLock(this.lockAcquireTimeout, async () => {
2453
- return await this._getUser();
2454
- });
2627
+ let result;
2628
+ if (this.lock != null) {
2629
+ // TODO(v3): remove legacy lock path
2630
+ result = await this._acquireLock(this.lockAcquireTimeout, async () => {
2631
+ return await this._getUser();
2632
+ });
2633
+ }
2634
+ else {
2635
+ result = await this._getUser();
2636
+ }
2455
2637
  if (result.data.user) {
2456
2638
  this.suppressGetSessionWarning = true;
2457
2639
  }
@@ -2612,9 +2794,13 @@ class GoTrueClient {
2612
2794
  */
2613
2795
  async updateUser(attributes, options = {}) {
2614
2796
  await this.initializePromise;
2615
- return await this._acquireLock(this.lockAcquireTimeout, async () => {
2616
- return await this._updateUser(attributes, options);
2617
- });
2797
+ if (this.lock != null) {
2798
+ // TODO(v3): remove legacy lock path
2799
+ return await this._acquireLock(this.lockAcquireTimeout, async () => {
2800
+ return await this._updateUser(attributes, options);
2801
+ });
2802
+ }
2803
+ return await this._updateUser(attributes, options);
2618
2804
  }
2619
2805
  async _updateUser(attributes, options = {}) {
2620
2806
  try {
@@ -2783,9 +2969,13 @@ class GoTrueClient {
2783
2969
  */
2784
2970
  async setSession(currentSession) {
2785
2971
  await this.initializePromise;
2786
- return await this._acquireLock(this.lockAcquireTimeout, async () => {
2787
- return await this._setSession(currentSession);
2788
- });
2972
+ if (this.lock != null) {
2973
+ // TODO(v3): remove legacy lock path
2974
+ return await this._acquireLock(this.lockAcquireTimeout, async () => {
2975
+ return await this._setSession(currentSession);
2976
+ });
2977
+ }
2978
+ return await this._setSession(currentSession);
2789
2979
  }
2790
2980
  async _setSession(currentSession) {
2791
2981
  try {
@@ -2963,9 +3153,13 @@ class GoTrueClient {
2963
3153
  */
2964
3154
  async refreshSession(currentSession) {
2965
3155
  await this.initializePromise;
2966
- return await this._acquireLock(this.lockAcquireTimeout, async () => {
2967
- return await this._refreshSession(currentSession);
2968
- });
3156
+ if (this.lock != null) {
3157
+ // TODO(v3): remove legacy lock path
3158
+ return await this._acquireLock(this.lockAcquireTimeout, async () => {
3159
+ return await this._refreshSession(currentSession);
3160
+ });
3161
+ }
3162
+ return await this._refreshSession(currentSession);
2969
3163
  }
2970
3164
  async _refreshSession(currentSession) {
2971
3165
  try {
@@ -3002,6 +3196,7 @@ class GoTrueClient {
3002
3196
  * Gets the session data from a URL string
3003
3197
  */
3004
3198
  async _getSessionFromURL(params, callbackUrlType) {
3199
+ var _a;
3005
3200
  try {
3006
3201
  if (!(0, helpers_1.isBrowser)())
3007
3202
  throw new errors_1.AuthImplicitGrantRedirectError('No browser detected.');
@@ -3040,7 +3235,10 @@ class GoTrueClient {
3040
3235
  const url = new URL(window.location.href);
3041
3236
  url.searchParams.delete('code');
3042
3237
  window.history.replaceState(window.history.state, '', url.toString());
3043
- return { data: { session: data.session, redirectType: null }, error: null };
3238
+ return {
3239
+ data: { session: data.session, redirectType: (_a = data.redirectType) !== null && _a !== void 0 ? _a : null },
3240
+ error: null,
3241
+ };
3044
3242
  }
3045
3243
  const { provider_token, provider_refresh_token, access_token, refresh_token, expires_in, expires_at, token_type, } = params;
3046
3244
  if (!access_token || !expires_in || !refresh_token || !token_type) {
@@ -3099,7 +3297,7 @@ class GoTrueClient {
3099
3297
  if (typeof this.detectSessionInUrl === 'function') {
3100
3298
  return this.detectSessionInUrl(new URL(window.location.href), params);
3101
3299
  }
3102
- return Boolean(params.access_token || params.error_description);
3300
+ return Boolean(params.access_token || params.error || params.error_description || params.error_code);
3103
3301
  }
3104
3302
  /**
3105
3303
  * Checks if the current URL and backing storage contain parameters given by a PKCE flow
@@ -3116,37 +3314,56 @@ class GoTrueClient {
3116
3314
  *
3117
3315
  * If using `others` scope, no `SIGNED_OUT` event is fired!
3118
3316
  *
3317
+ * **Warning:** the default `scope` is `'global'`. This signs the user out of
3318
+ * **every device they are currently signed in on**, not just the current
3319
+ * tab/session. If you only want to sign the user out of the current session
3320
+ * (the behavior most other auth libraries default to), pass
3321
+ * `{ scope: 'local' }` explicitly.
3322
+ *
3119
3323
  * @category Auth
3120
3324
  *
3121
3325
  * @remarks
3122
3326
  * - In order to use the `signOut()` method, the user needs to be signed in first.
3123
- * - By default, `signOut()` uses the global scope, which signs out all other sessions that the user is logged into as well. Customize this behavior by passing a scope parameter.
3327
+ * - By default, `signOut()` uses the **global** scope, which signs out the user
3328
+ * on every device they are signed in on (not just the current one). Pass
3329
+ * `{ scope: 'local' }` to only sign out the current session. This is
3330
+ * usually what apps want on a "Sign out" button, especially when users
3331
+ * sign in from multiple devices and do not expect signing out of one to
3332
+ * terminate the others.
3124
3333
  * - Since Supabase Auth uses JWTs for authentication, the access token JWT will be valid until it's expired. When the user signs out, Supabase revokes the refresh token and deletes the JWT from the client-side. This does not revoke the JWT and it will still be valid until it expires.
3125
3334
  *
3126
- * @example Sign out (all sessions)
3335
+ * @example Sign out of every device (global – default)
3127
3336
  * ```js
3128
3337
  * const { error } = await supabase.auth.signOut()
3129
3338
  * ```
3130
3339
  *
3131
- * @example Sign out (current session)
3340
+ * @example Sign out only the current session (recommended for most apps)
3132
3341
  * ```js
3133
3342
  * const { error } = await supabase.auth.signOut({ scope: 'local' })
3134
3343
  * ```
3135
3344
  *
3136
- * @example Sign out (other sessions)
3345
+ * @example Sign out of all other sessions, keep the current one
3137
3346
  * ```js
3138
3347
  * const { error } = await supabase.auth.signOut({ scope: 'others' })
3139
3348
  * ```
3140
3349
  */
3141
3350
  async signOut(options = { scope: 'global' }) {
3142
3351
  await this.initializePromise;
3143
- return await this._acquireLock(this.lockAcquireTimeout, async () => {
3144
- return await this._signOut(options);
3145
- });
3352
+ if (this.lock != null) {
3353
+ // TODO(v3): remove legacy lock path
3354
+ return await this._acquireLock(this.lockAcquireTimeout, async () => {
3355
+ return await this._signOut(options);
3356
+ });
3357
+ }
3358
+ return await this._signOut(options);
3146
3359
  }
3147
3360
  async _signOut({ scope } = { scope: 'global' }) {
3148
3361
  return await this._useSession(async (result) => {
3149
3362
  var _a;
3363
+ const removeCurrentSession = async () => {
3364
+ await this._removeSession();
3365
+ await (0, helpers_1.removeItemAsync)(this.storage, `${this.storageKey}-code-verifier`);
3366
+ };
3150
3367
  const { data, error: sessionError } = result;
3151
3368
  if (sessionError && !(0, errors_1.isAuthSessionMissingError)(sessionError)) {
3152
3369
  return this._returnResult({ error: sessionError });
@@ -3160,13 +3377,15 @@ class GoTrueClient {
3160
3377
  if (!(((0, errors_1.isAuthApiError)(error) &&
3161
3378
  (error.status === 404 || error.status === 401 || error.status === 403)) ||
3162
3379
  (0, errors_1.isAuthSessionMissingError)(error))) {
3380
+ if (scope !== 'others') {
3381
+ await removeCurrentSession();
3382
+ }
3163
3383
  return this._returnResult({ error });
3164
3384
  }
3165
3385
  }
3166
3386
  }
3167
3387
  if (scope !== 'others') {
3168
- await this._removeSession();
3169
- await (0, helpers_1.removeItemAsync)(this.storage, `${this.storageKey}-code-verifier`);
3388
+ await removeCurrentSession();
3170
3389
  }
3171
3390
  return this._returnResult({ error: null });
3172
3391
  });
@@ -3178,18 +3397,8 @@ class GoTrueClient {
3178
3397
  * - Subscribes to important events occurring on the user's session.
3179
3398
  * - Use on the frontend/client. It is less useful on the server.
3180
3399
  * - Events are emitted across tabs to keep your application's UI up-to-date. Some events can fire very frequently, based on the number of tabs open. Use a quick and efficient callback function, and defer or debounce as many operations as you can to be performed outside of the callback.
3181
- * - **Important:** A callback can be an `async` function and it runs synchronously during the processing of the changes causing the event. You can easily create a dead-lock by using `await` on a call to another method of the Supabase library.
3182
- * - Avoid using `async` functions as callbacks.
3183
- * - Limit the number of `await` calls in `async` callbacks.
3184
- * - Do not use other Supabase functions in the callback function. If you must, dispatch the functions once the callback has finished executing. Use this as a quick way to achieve this:
3185
- * ```js
3186
- * supabase.auth.onAuthStateChange((event, session) => {
3187
- * setTimeout(async () => {
3188
- * // await on other Supabase function here
3189
- * // this runs right after the callback has finished
3190
- * }, 0)
3191
- * })
3192
- * ```
3400
+ * - Callbacks can be `async` and can safely call other Supabase auth methods (`getUser`, `setSession`, etc.) from inside the callback.
3401
+ * - Keep callbacks quick. Events are awaited in order, so a slow callback delays subsequent events to subscribers in this tab.
3193
3402
  * - Emitted events:
3194
3403
  * - `INITIAL_SESSION`
3195
3404
  * - Emitted right after the Supabase client is constructed and the initial session from storage is loaded.
@@ -3372,9 +3581,15 @@ class GoTrueClient {
3372
3581
  this.stateChangeEmitters.set(id, subscription);
3373
3582
  (async () => {
3374
3583
  await this.initializePromise;
3375
- await this._acquireLock(this.lockAcquireTimeout, async () => {
3376
- this._emitInitialSession(id);
3377
- });
3584
+ if (this.lock != null) {
3585
+ // TODO(v3): remove legacy lock path
3586
+ await this._acquireLock(this.lockAcquireTimeout, async () => {
3587
+ this._emitInitialSession(id);
3588
+ });
3589
+ }
3590
+ else {
3591
+ await this._emitInitialSession(id);
3592
+ }
3378
3593
  })();
3379
3594
  return { data: { subscription } };
3380
3595
  }
@@ -3391,7 +3606,16 @@ class GoTrueClient {
3391
3606
  catch (err) {
3392
3607
  await ((_b = this.stateChangeEmitters.get(id)) === null || _b === void 0 ? void 0 : _b.callback('INITIAL_SESSION', null));
3393
3608
  this._debug('INITIAL_SESSION', 'callback id', id, 'error', err);
3394
- if ((0, errors_1.isAuthSessionMissingError)(err)) {
3609
+ if ((0, errors_1.isAuthSessionMissingError)(err) ||
3610
+ (0, errors_1.isAuthRetryableFetchError)(err) ||
3611
+ ((0, errors_1.isAuthApiError)(err) &&
3612
+ (err.code === 'refresh_token_not_found' ||
3613
+ err.code === 'refresh_token_already_used' ||
3614
+ err.code === 'session_expired'))) {
3615
+ // A missing session, a transient/aborted network failure (e.g. a
3616
+ // superseded page navigation cancelling the in-flight request), or
3617
+ // a dead refresh token (e.g. stale SSR cookies) is not an
3618
+ // application error — warn rather than surface it raw.
3395
3619
  console.warn(err);
3396
3620
  }
3397
3621
  else {
@@ -3587,7 +3811,7 @@ class GoTrueClient {
3587
3811
  var _a;
3588
3812
  try {
3589
3813
  const { data, error } = await this._useSession(async (result) => {
3590
- var _a, _b, _c, _d, _e;
3814
+ var _a, _b, _c, _d, _f;
3591
3815
  const { data, error } = result;
3592
3816
  if (error)
3593
3817
  throw error;
@@ -3599,7 +3823,7 @@ class GoTrueClient {
3599
3823
  });
3600
3824
  return await (0, fetch_1._request)(this.fetch, 'GET', url, {
3601
3825
  headers: this.headers,
3602
- jwt: (_e = (_d = data.session) === null || _d === void 0 ? void 0 : _d.access_token) !== null && _e !== void 0 ? _e : undefined,
3826
+ jwt: (_f = (_d = data.session) === null || _d === void 0 ? void 0 : _d.access_token) !== null && _f !== void 0 ? _f : undefined,
3603
3827
  });
3604
3828
  });
3605
3829
  if (error)
@@ -3716,7 +3940,10 @@ class GoTrueClient {
3716
3940
  * @param refreshToken A valid refresh token that was returned on login.
3717
3941
  */
3718
3942
  async _refreshAccessToken(refreshToken) {
3719
- const debugName = `#_refreshAccessToken(${refreshToken.substring(0, 5)}...)`;
3943
+ // Refresh tokens are long-lived bearer credentials; do NOT include any
3944
+ // fragment of the token in the debug tag, even when `debug: true` is
3945
+ // enabled (logs may be forwarded to third-party services).
3946
+ const debugName = `#_refreshAccessToken()`;
3720
3947
  this._debug(debugName, 'begin');
3721
3948
  try {
3722
3949
  const startedAt = Date.now();
@@ -3823,10 +4050,17 @@ class GoTrueClient {
3823
4050
  if (this.autoRefreshToken && currentSession.refresh_token) {
3824
4051
  const { error } = await this._callRefreshToken(currentSession.refresh_token);
3825
4052
  if (error) {
3826
- console.error(error);
3827
- if (!(0, errors_1.isAuthRetryableFetchError)(error)) {
3828
- this._debug(debugName, 'refresh failed with a non-retryable error, removing the session', error);
3829
- await this._removeSession();
4053
+ // `_callRefreshToken` is the single source of truth for refresh
4054
+ // outcomes: it removes the session itself when the access token
4055
+ // is actually expired, and preserves it when the token is still
4056
+ // valid (proactive-preserve). Don't second-guess that here — a
4057
+ // local `_removeSession` would emit a duplicate `SIGNED_OUT` on
4058
+ // genuine failures and undo the proactive-preserve at init time.
4059
+ if ((0, errors_1.isAuthRefreshDiscardedError)(error)) {
4060
+ this._debug(debugName, 'refresh discarded by commit guard', error);
4061
+ }
4062
+ else {
4063
+ this._debug(debugName, 'refresh failed', error);
3830
4064
  }
3831
4065
  }
3832
4066
  }
@@ -3859,7 +4093,16 @@ class GoTrueClient {
3859
4093
  }
3860
4094
  catch (err) {
3861
4095
  this._debug(debugName, 'error', err);
3862
- console.error(err);
4096
+ if ((0, errors_1.isAuthRetryableFetchError)(err)) {
4097
+ // Transient/aborted network failure during session recovery (e.g. a
4098
+ // superseded page navigation cancelling the in-flight request). Warn
4099
+ // rather than surface it raw; the session is untouched and recovery
4100
+ // will run again on the next load.
4101
+ console.warn(err);
4102
+ }
4103
+ else {
4104
+ console.error(err);
4105
+ }
3863
4106
  return;
3864
4107
  }
3865
4108
  finally {
@@ -3875,18 +4118,91 @@ class GoTrueClient {
3875
4118
  if (this.refreshingDeferred) {
3876
4119
  return this.refreshingDeferred.promise;
3877
4120
  }
3878
- const debugName = `#_callRefreshToken(${refreshToken.substring(0, 5)}...)`;
4121
+ // Serial failure cooldown: callers passing the *same* refresh token
4122
+ // after a recent failure receive the cached result instead of firing
4123
+ // another `/token` request. This caps the proactive-refresh storm
4124
+ // where every `getSession()` call inside the 90s EXPIRY_MARGIN_MS
4125
+ // window kept re-firing against the same broken refresh token during
4126
+ // outages. Concurrent callers already share `refreshingDeferred`; this
4127
+ // cache covers serial callers spaced across cooldown windows.
4128
+ //
4129
+ // Token-keyed so callers with a fresh refresh token (rotation pickup
4130
+ // from another tab, explicit `setSession`/`refreshSession({ refresh_token })`,
4131
+ // multi-account switch) bypass the cache and attempt a real refresh.
4132
+ if (this.lastRefreshFailure &&
4133
+ this.lastRefreshFailure.refreshToken === refreshToken &&
4134
+ Date.now() < this.lastRefreshFailure.expiresAt) {
4135
+ this._debug('#_callRefreshToken()', 'returning cached failure (cooldown active)');
4136
+ return this.lastRefreshFailure.result;
4137
+ }
4138
+ // Refresh tokens are long-lived bearer credentials; do NOT include any
4139
+ // fragment of the token in the debug tag, even when `debug: true` is
4140
+ // enabled (logs may be forwarded to third-party services).
4141
+ const debugName = `#_callRefreshToken()`;
3879
4142
  this._debug(debugName, 'begin');
3880
4143
  try {
3881
4144
  this.refreshingDeferred = new helpers_1.Deferred();
4145
+ // Snapshot storage before the fetch. The commit guard discards the
4146
+ // rotated tokens only when a non-null pre-fetch snapshot changed under
4147
+ // us — typical case: a concurrent `signOut` ran `_removeSession`, or
4148
+ // another tab's refresh rewrote the slot. Callers passing
4149
+ // externally-sourced tokens (SSR cookie handoff, multi-account
4150
+ // switching, `setSession`/`refreshSession({ refresh_token })`) may
4151
+ // start from a null snapshot OR from a non-null snapshot whose
4152
+ // refresh_token differs from the one they're hydrating; in both
4153
+ // cases the guard fires only when storage was *modified between
4154
+ // snapshots*, not when the input token disagrees with what's stored.
4155
+ const storedAtStart = (await (0, helpers_1.getItemAsync)(this.storage, this.storageKey));
3882
4156
  const { data, error } = await this._refreshAccessToken(refreshToken);
3883
4157
  if (error)
3884
4158
  throw error;
3885
4159
  if (!data.session)
3886
4160
  throw new errors_1.AuthSessionMissingError();
4161
+ const storedAfter = (await (0, helpers_1.getItemAsync)(this.storage, this.storageKey));
4162
+ const storageChangedUnderUs = storedAtStart !== null &&
4163
+ (storedAfter === null || storedAfter.refresh_token !== storedAtStart.refresh_token);
4164
+ if (storageChangedUnderUs) {
4165
+ this._debug(debugName, 'commit guard: storage changed since refresh started, discarding rotated tokens', {
4166
+ // Presence indicators only — never log refresh token fragments,
4167
+ // even partial. Logs may be forwarded to third-party services.
4168
+ startedWith: 'present',
4169
+ nowHolds: storedAfter ? 'replaced' : 'cleared',
4170
+ });
4171
+ const discarded = {
4172
+ data: null,
4173
+ error: new errors_1.AuthRefreshDiscardedError(),
4174
+ };
4175
+ this.refreshingDeferred.resolve(discarded);
4176
+ return discarded;
4177
+ }
4178
+ // Second leg of the commit guard: close the TOCTOU window between the
4179
+ // synchronous `storageChangedUnderUs` check and the actual storage
4180
+ // writes inside `_saveSession`. A concurrent `signOut → _removeSession`
4181
+ // can land inside `_saveSession`'s `await setItemAsync(...)` yields and
4182
+ // clear storage just before we overwrite it. Capture the epoch BEFORE
4183
+ // the save and re-check after; if it advanced, undo the write directly
4184
+ // (do NOT call `_removeSession` — that would emit a duplicate
4185
+ // SIGNED_OUT for the concurrent signOut that already fired one).
4186
+ const epochBeforeSave = this._sessionRemovalEpoch;
3887
4187
  await this._saveSession(data.session);
4188
+ if (this._sessionRemovalEpoch !== epochBeforeSave) {
4189
+ this._debug(debugName, 'commit guard (post-save): _removeSession ran during _saveSession, undoing write');
4190
+ await (0, helpers_1.removeItemAsync)(this.storage, this.storageKey);
4191
+ if (this.userStorage) {
4192
+ await (0, helpers_1.removeItemAsync)(this.userStorage, this.storageKey + '-user');
4193
+ }
4194
+ const discarded = {
4195
+ data: null,
4196
+ error: new errors_1.AuthRefreshDiscardedError(),
4197
+ };
4198
+ this.refreshingDeferred.resolve(discarded);
4199
+ return discarded;
4200
+ }
3888
4201
  await this._notifyAllSubscribers('TOKEN_REFRESHED', data.session);
3889
4202
  const result = { data: data.session, error: null };
4203
+ // Refresh succeeded — clear any cached failure so the next caller
4204
+ // (including the auto-refresh ticker) attempts a real refresh again.
4205
+ this.lastRefreshFailure = null;
3890
4206
  this.refreshingDeferred.resolve(result);
3891
4207
  return result;
3892
4208
  }
@@ -3895,8 +4211,35 @@ class GoTrueClient {
3895
4211
  if ((0, errors_1.isAuthError)(error)) {
3896
4212
  const result = { data: null, error };
3897
4213
  if (!(0, errors_1.isAuthRetryableFetchError)(error)) {
3898
- await this._removeSession();
4214
+ // Proactive vs reactive distinction: a refresh fires whenever
4215
+ // the access token is within EXPIRY_MARGIN_MS of expiry. If the
4216
+ // access token is *still valid* at this moment, the refresh was
4217
+ // proactive and the existing session is still usable until its
4218
+ // real expiry — destroying it now would log out a user whose
4219
+ // access token works. If the access token has actually expired,
4220
+ // the refresh token is the only credential left and it just got
4221
+ // rejected — the session is genuinely dead. `__loadSession`
4222
+ // mirrors this distinction on the read path so callers see the
4223
+ // preserved session instead of `session: null`.
4224
+ const storedNow = (await (0, helpers_1.getItemAsync)(this.storage, this.storageKey));
4225
+ const accessTokenStillValid = !!((storedNow === null || storedNow === void 0 ? void 0 : storedNow.expires_at) && storedNow.expires_at * 1000 > Date.now());
4226
+ if (accessTokenStillValid) {
4227
+ this._debug(debugName, 'proactive refresh failed, access token still valid — preserving session');
4228
+ }
4229
+ else {
4230
+ await this._removeSession();
4231
+ }
3899
4232
  }
4233
+ // Cache the failure so serial callers (and the next auto-refresh
4234
+ // tick) passing the same refresh token within the cooldown window
4235
+ // receive it synchronously instead of firing another `/token`
4236
+ // call. Set after the optional `_removeSession` above (which
4237
+ // clears the cache as part of teardown) so the cache survives.
4238
+ this.lastRefreshFailure = {
4239
+ refreshToken,
4240
+ result,
4241
+ expiresAt: Date.now() + constants_1.REFRESH_FAILURE_COOLDOWN_MS,
4242
+ };
3900
4243
  (_a = this.refreshingDeferred) === null || _a === void 0 ? void 0 : _a.resolve(result);
3901
4244
  return result;
3902
4245
  }
@@ -3909,6 +4252,19 @@ class GoTrueClient {
3909
4252
  }
3910
4253
  }
3911
4254
  async _notifyAllSubscribers(event, session, broadcast = true) {
4255
+ if (this._pendingInitNotifications !== null && broadcast) {
4256
+ // We're inside initialize() before initializePromise has resolved, and
4257
+ // this notification originates from the init chain (_recoverAndRefresh),
4258
+ // which always fires with broadcast = true. Enqueue instead of firing so
4259
+ // that callbacks can safely call getSession() / getUser() without
4260
+ // deadlocking on initializePromise.
4261
+ //
4262
+ // Notifications with broadcast = false come from the incoming
4263
+ // BroadcastChannel handler (a cross-tab event), not the init chain, so
4264
+ // they are fired immediately to preserve multi-tab ordering.
4265
+ this._pendingInitNotifications.push({ event, session, broadcast });
4266
+ return;
4267
+ }
3912
4268
  const debugName = `#_notifyAllSubscribers(${event})`;
3913
4269
  this._debug(debugName, 'begin', session, `broadcast = ${broadcast}`);
3914
4270
  try {
@@ -3945,7 +4301,6 @@ class GoTrueClient {
3945
4301
  // _saveSession is always called whenever a new session has been acquired
3946
4302
  // so we can safely suppress the warning returned by future getSession calls
3947
4303
  this.suppressGetSessionWarning = true;
3948
- await (0, helpers_1.removeItemAsync)(this.storage, `${this.storageKey}-code-verifier`);
3949
4304
  // Create a shallow copy to work with, to avoid mutating the original session object if it's used elsewhere
3950
4305
  const sessionToProcess = Object.assign({}, session);
3951
4306
  const userIsProxy = sessionToProcess.user && sessionToProcess.user.__isUserNotAvailableProxy === true;
@@ -3978,7 +4333,15 @@ class GoTrueClient {
3978
4333
  }
3979
4334
  }
3980
4335
  async _removeSession() {
4336
+ // Bump synchronously, BEFORE any `await`, so that `_callRefreshToken`'s
4337
+ // post-save check sees the increment whenever this method has started —
4338
+ // even if it hasn't finished. Pairs with the epoch check in
4339
+ // `_callRefreshToken`. See `_sessionRemovalEpoch` field doc.
4340
+ this._sessionRemovalEpoch += 1;
3981
4341
  this._debug('#_removeSession()');
4342
+ // The session is gone — no point holding on to a cached refresh failure
4343
+ // for a token that no longer exists. Synchronous, before any `await`.
4344
+ this.lastRefreshFailure = null;
3982
4345
  this.suppressGetSessionWarning = false;
3983
4346
  await (0, helpers_1.removeItemAsync)(this.storage, this.storageKey);
3984
4347
  await (0, helpers_1.removeItemAsync)(this.storage, this.storageKey + '-code-verifier');
@@ -3991,8 +4354,8 @@ class GoTrueClient {
3991
4354
  /**
3992
4355
  * Removes any registered visibilitychange callback.
3993
4356
  *
3994
- * {@see #startAutoRefresh}
3995
- * {@see #stopAutoRefresh}
4357
+ * {@link GoTrueClient.startAutoRefresh}
4358
+ * {@link GoTrueClient.stopAutoRefresh}
3996
4359
  */
3997
4360
  _removeVisibilityChangedCallback() {
3998
4361
  this._debug('#_removeVisibilityChangedCallback()');
@@ -4008,7 +4371,7 @@ class GoTrueClient {
4008
4371
  }
4009
4372
  }
4010
4373
  /**
4011
- * This is the private implementation of {@link #startAutoRefresh}. Use this
4374
+ * This is the private implementation of {@link GoTrueClient.startAutoRefresh}. Use this
4012
4375
  * within the library.
4013
4376
  */
4014
4377
  async _startAutoRefresh() {
@@ -4050,7 +4413,7 @@ class GoTrueClient {
4050
4413
  }
4051
4414
  }
4052
4415
  /**
4053
- * This is the private implementation of {@link #stopAutoRefresh}. Use this
4416
+ * This is the private implementation of {@link GoTrueClient.stopAutoRefresh}. Use this
4054
4417
  * within the library.
4055
4418
  */
4056
4419
  async _stopAutoRefresh() {
@@ -4086,7 +4449,7 @@ class GoTrueClient {
4086
4449
  * platform's foreground indication mechanism and call these methods
4087
4450
  * appropriately to conserve resources.
4088
4451
  *
4089
- * {@see #stopAutoRefresh}
4452
+ * {@link GoTrueClient.stopAutoRefresh}
4090
4453
  *
4091
4454
  * @category Auth
4092
4455
  *
@@ -4120,7 +4483,7 @@ class GoTrueClient {
4120
4483
  * If you call this method any managed visibility change callback will be
4121
4484
  * removed and you must manage visibility changes on your own.
4122
4485
  *
4123
- * See {@link #startAutoRefresh} for more details.
4486
+ * See {@link GoTrueClient.startAutoRefresh} for more details.
4124
4487
  *
4125
4488
  * @category Auth
4126
4489
  *
@@ -4148,47 +4511,122 @@ class GoTrueClient {
4148
4511
  this._removeVisibilityChangedCallback();
4149
4512
  await this._stopAutoRefresh();
4150
4513
  }
4514
+ /**
4515
+ * Tears down the client's background work: stops the auto-refresh interval,
4516
+ * removes the `visibilitychange` listener, closes the cross-tab
4517
+ * `BroadcastChannel`, and clears registered `onAuthStateChange` subscribers.
4518
+ *
4519
+ * Call this from cleanup hooks when the client is being replaced before
4520
+ * its JS realm is destroyed. React Strict Mode and HMR are the common
4521
+ * cases. Any in-flight `fetch` calls continue to completion and may still
4522
+ * write to storage; dispose doesn't abort them or erase storage.
4523
+ *
4524
+ * Lifecycle caveat: because in-flight refreshes are not aborted, a
4525
+ * disposed instance can still persist a rotated session to storage after
4526
+ * `dispose()` returns. A subsequent `createClient` against the same
4527
+ * `storageKey` will pick up that session on its next read. If you need
4528
+ * strict isolation between client lifecycles, await any pending auth
4529
+ * operation before calling `dispose()` (or change the `storageKey` for
4530
+ * the replacement client).
4531
+ *
4532
+ * Safe to call repeatedly.
4533
+ *
4534
+ * @category Auth
4535
+ *
4536
+ * @example Cleanup on React unmount
4537
+ * ```ts
4538
+ * useEffect(() => {
4539
+ * const client = createClient(...)
4540
+ * return () => { client.auth.dispose() }
4541
+ * }, [])
4542
+ * ```
4543
+ */
4544
+ async dispose() {
4545
+ var _a;
4546
+ this._removeVisibilityChangedCallback();
4547
+ await this._stopAutoRefresh();
4548
+ (_a = this.broadcastChannel) === null || _a === void 0 ? void 0 : _a.close();
4549
+ this.broadcastChannel = null;
4550
+ this.stateChangeEmitters.clear();
4551
+ }
4151
4552
  /**
4152
4553
  * Runs the auto refresh token tick.
4153
4554
  */
4154
4555
  async _autoRefreshTokenTick() {
4155
4556
  this._debug('#_autoRefreshTokenTick()', 'begin');
4156
- try {
4157
- await this._acquireLock(0, async () => {
4158
- try {
4159
- const now = Date.now();
4557
+ if (this.lock != null) {
4558
+ // TODO(v3): remove legacy lock path. Uses `_acquireLock(0, ...)` which
4559
+ // throws `LockAcquireTimeoutError` immediately if the lock is held —
4560
+ // that's the fail-fast skip path that lets the tick bail out instead
4561
+ // of queuing behind a long-running operation.
4562
+ try {
4563
+ await this._acquireLock(0, async () => {
4160
4564
  try {
4161
- return await this._useSession(async (result) => {
4162
- const { data: { session }, } = result;
4163
- if (!session || !session.refresh_token || !session.expires_at) {
4164
- this._debug('#_autoRefreshTokenTick()', 'no session');
4165
- return;
4166
- }
4167
- // session will expire in this many ticks (or has already expired if <= 0)
4168
- const expiresInTicks = Math.floor((session.expires_at * 1000 - now) / constants_1.AUTO_REFRESH_TICK_DURATION_MS);
4169
- this._debug('#_autoRefreshTokenTick()', `access token expires in ${expiresInTicks} ticks, a tick lasts ${constants_1.AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${constants_1.AUTO_REFRESH_TICK_THRESHOLD} ticks`);
4170
- if (expiresInTicks <= constants_1.AUTO_REFRESH_TICK_THRESHOLD) {
4171
- await this._callRefreshToken(session.refresh_token);
4172
- }
4173
- });
4565
+ const now = Date.now();
4566
+ try {
4567
+ return await this._useSession(async (result) => {
4568
+ const { data: { session }, } = result;
4569
+ if (!session || !session.refresh_token || !session.expires_at) {
4570
+ this._debug('#_autoRefreshTokenTick()', 'no session');
4571
+ return;
4572
+ }
4573
+ const expiresInTicks = Math.floor((session.expires_at * 1000 - now) / constants_1.AUTO_REFRESH_TICK_DURATION_MS);
4574
+ this._debug('#_autoRefreshTokenTick()', `access token expires in ${expiresInTicks} ticks, a tick lasts ${constants_1.AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${constants_1.AUTO_REFRESH_TICK_THRESHOLD} ticks`);
4575
+ if (expiresInTicks <= constants_1.AUTO_REFRESH_TICK_THRESHOLD) {
4576
+ await this._callRefreshToken(session.refresh_token);
4577
+ }
4578
+ });
4579
+ }
4580
+ catch (e) {
4581
+ console.error('Auto refresh tick failed with error. This is likely a transient error.', e);
4582
+ }
4174
4583
  }
4175
- catch (e) {
4176
- console.error('Auto refresh tick failed with error. This is likely a transient error.', e);
4584
+ finally {
4585
+ this._debug('#_autoRefreshTokenTick()', 'end');
4177
4586
  }
4587
+ });
4588
+ }
4589
+ catch (e) {
4590
+ if (e instanceof locks_1.LockAcquireTimeoutError) {
4591
+ this._debug('auto refresh token tick lock not available');
4178
4592
  }
4179
- finally {
4180
- this._debug('#_autoRefreshTokenTick()', 'end');
4593
+ else {
4594
+ throw e;
4181
4595
  }
4182
- });
4596
+ }
4597
+ return;
4183
4598
  }
4184
- catch (e) {
4185
- if (e.isAcquireTimeout || e instanceof locks_1.LockAcquireTimeoutError) {
4186
- this._debug('auto refresh token tick lock not available');
4599
+ // Lockless default: skip if a refresh is already in flight.
4600
+ // `_callRefreshToken` also dedupes via the same field; this is just a
4601
+ // fast-path skip to avoid an unnecessary storage read.
4602
+ if (this.refreshingDeferred !== null) {
4603
+ this._debug('#_autoRefreshTokenTick()', 'refresh already in flight, skipping');
4604
+ return;
4605
+ }
4606
+ try {
4607
+ const now = Date.now();
4608
+ try {
4609
+ await this._useSession(async (result) => {
4610
+ const { data: { session }, } = result;
4611
+ if (!session || !session.refresh_token || !session.expires_at) {
4612
+ this._debug('#_autoRefreshTokenTick()', 'no session');
4613
+ return;
4614
+ }
4615
+ // session will expire in this many ticks (or has already expired if <= 0)
4616
+ const expiresInTicks = Math.floor((session.expires_at * 1000 - now) / constants_1.AUTO_REFRESH_TICK_DURATION_MS);
4617
+ this._debug('#_autoRefreshTokenTick()', `access token expires in ${expiresInTicks} ticks, a tick lasts ${constants_1.AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${constants_1.AUTO_REFRESH_TICK_THRESHOLD} ticks`);
4618
+ if (expiresInTicks <= constants_1.AUTO_REFRESH_TICK_THRESHOLD) {
4619
+ await this._callRefreshToken(session.refresh_token);
4620
+ }
4621
+ });
4187
4622
  }
4188
- else {
4189
- throw e;
4623
+ catch (e) {
4624
+ console.error('Auto refresh tick failed with error. This is likely a transient error.', e);
4190
4625
  }
4191
4626
  }
4627
+ finally {
4628
+ this._debug('#_autoRefreshTokenTick()', 'end');
4629
+ }
4192
4630
  }
4193
4631
  /**
4194
4632
  * Registers callbacks on the browser / platform, which in-turn run
@@ -4237,18 +4675,26 @@ class GoTrueClient {
4237
4675
  if (!calledFromInitialize) {
4238
4676
  // called when the visibility has changed, i.e. the browser
4239
4677
  // transitioned from hidden -> visible so we need to see if the session
4240
- // should be recovered immediately... but to do that we need to acquire
4241
- // the lock first asynchronously
4678
+ // should be recovered
4242
4679
  await this.initializePromise;
4243
- await this._acquireLock(this.lockAcquireTimeout, async () => {
4680
+ if (this.lock != null) {
4681
+ // TODO(v3): remove legacy lock path
4682
+ await this._acquireLock(this.lockAcquireTimeout, async () => {
4683
+ if (document.visibilityState !== 'visible') {
4684
+ this._debug(methodName, 'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting');
4685
+ return;
4686
+ }
4687
+ await this._recoverAndRefresh();
4688
+ });
4689
+ }
4690
+ else {
4244
4691
  if (document.visibilityState !== 'visible') {
4245
- this._debug(methodName, 'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting');
4246
- // visibility has changed while waiting for the lock, abort
4692
+ this._debug(methodName, 'visibilityState is no longer visible, skipping recovery');
4247
4693
  return;
4248
4694
  }
4249
4695
  // recover the session
4250
4696
  await this._recoverAndRefresh();
4251
- });
4697
+ }
4252
4698
  }
4253
4699
  }
4254
4700
  else if (document.visibilityState === 'hidden') {
@@ -4344,7 +4790,7 @@ class GoTrueClient {
4344
4790
  }
4345
4791
  }
4346
4792
  async _verify(params) {
4347
- return this._acquireLock(this.lockAcquireTimeout, async () => {
4793
+ const run = async () => {
4348
4794
  try {
4349
4795
  return await this._useSession(async (result) => {
4350
4796
  var _a;
@@ -4378,10 +4824,15 @@ class GoTrueClient {
4378
4824
  }
4379
4825
  throw error;
4380
4826
  }
4381
- });
4827
+ };
4828
+ if (this.lock != null) {
4829
+ // TODO(v3): remove legacy lock path
4830
+ return this._acquireLock(this.lockAcquireTimeout, run);
4831
+ }
4832
+ return run();
4382
4833
  }
4383
4834
  async _challenge(params) {
4384
- return this._acquireLock(this.lockAcquireTimeout, async () => {
4835
+ const run = async () => {
4385
4836
  try {
4386
4837
  return await this._useSession(async (result) => {
4387
4838
  var _a;
@@ -4421,14 +4872,17 @@ class GoTrueClient {
4421
4872
  }
4422
4873
  throw error;
4423
4874
  }
4424
- });
4875
+ };
4876
+ if (this.lock != null) {
4877
+ // TODO(v3): remove legacy lock path
4878
+ return this._acquireLock(this.lockAcquireTimeout, run);
4879
+ }
4880
+ return run();
4425
4881
  }
4426
4882
  /**
4427
- * {@see GoTrueMFAApi#challengeAndVerify}
4883
+ * {@link GoTrueMFAApi#challengeAndVerify}
4428
4884
  */
4429
4885
  async _challengeAndVerify(params) {
4430
- // both _challenge and _verify independently acquire the lock, so no need
4431
- // to acquire it here
4432
4886
  const { data: challengeData, error: challengeError } = await this._challenge({
4433
4887
  factorId: params.factorId,
4434
4888
  });
@@ -4442,11 +4896,10 @@ class GoTrueClient {
4442
4896
  });
4443
4897
  }
4444
4898
  /**
4445
- * {@see GoTrueMFAApi#listFactors}
4899
+ * {@link GoTrueMFAApi#listFactors}
4446
4900
  */
4447
4901
  async _listFactors() {
4448
4902
  var _a;
4449
- // use #getUser instead of #_getUser as the former acquires a lock
4450
4903
  const { data: { user }, error: userError, } = await this.getUser();
4451
4904
  if (userError) {
4452
4905
  return { data: null, error: userError };
@@ -4471,7 +4924,7 @@ class GoTrueClient {
4471
4924
  };
4472
4925
  }
4473
4926
  /**
4474
- * {@see GoTrueMFAApi#getAuthenticatorAssuranceLevel}
4927
+ * {@link GoTrueMFAApi#getAuthenticatorAssuranceLevel}
4475
4928
  */
4476
4929
  async _getAuthenticatorAssuranceLevel(jwt) {
4477
4930
  var _a, _b, _c, _d;
@@ -4722,15 +5175,15 @@ class GoTrueClient {
4722
5175
  * Extracts the JWT claims present in the access token by first verifying the
4723
5176
  * JWT against the server's JSON Web Key Set endpoint
4724
5177
  * `/.well-known/jwks.json` which is often cached, resulting in significantly
4725
- * faster responses. Prefer this method over {@link #getUser} which always
5178
+ * faster responses. Prefer this method over {@link GoTrueClient.getUser} which always
4726
5179
  * sends a request to the Auth server for each JWT.
4727
5180
  *
4728
5181
  * If the project is not using an asymmetric JWT signing key (like ECC or
4729
- * RSA) it always sends a request to the Auth server (similar to {@link
4730
- * #getUser}) to verify the JWT.
5182
+ * RSA) it always sends a request to the Auth server (similar to
5183
+ * {@link GoTrueClient.getUser}) to verify the JWT.
4731
5184
  *
4732
5185
  * @param jwt An optional specific JWT you wish to verify, not the one you
4733
- * can obtain from {@link #getSession}.
5186
+ * can obtain from {@link GoTrueClient.getSession}.
4734
5187
  * @param options Various additional options that allow you to customize the
4735
5188
  * behavior of this method.
4736
5189
  *
@@ -4795,8 +5248,14 @@ class GoTrueClient {
4795
5248
  }
4796
5249
  const { header, payload, signature, raw: { header: rawHeader, payload: rawPayload }, } = (0, helpers_1.decodeJWT)(token);
4797
5250
  if (!(options === null || options === void 0 ? void 0 : options.allowExpired)) {
4798
- // Reject expired JWTs should only happen if jwt argument was passed
4799
- (0, helpers_1.validateExp)(payload.exp);
5251
+ // Reject expired JWTs should only happen if jwt argument was passed.
5252
+ // Rethrow as AuthInvalidJwtError so the outer catch converts it to { data, error }.
5253
+ try {
5254
+ (0, helpers_1.validateExp)(payload.exp);
5255
+ }
5256
+ catch (e) {
5257
+ throw new errors_1.AuthInvalidJwtError(e instanceof Error ? e.message : 'JWT validation failed');
5258
+ }
4800
5259
  }
4801
5260
  const signingKey = !header.alg ||
4802
5261
  header.alg.startsWith('HS') ||
@@ -4847,6 +5306,335 @@ class GoTrueClient {
4847
5306
  throw error;
4848
5307
  }
4849
5308
  }
5309
+ // --- Passkey Methods ---
5310
+ /**
5311
+ * Sign in with a passkey. Handles the full WebAuthn ceremony:
5312
+ * 1. Fetches authentication challenge from server
5313
+ * 2. Prompts user via navigator.credentials.get()
5314
+ * 3. Verifies credential with server and creates session
5315
+ *
5316
+ * Requires `auth.experimental.passkey: true`.
5317
+ *
5318
+ * @category Auth
5319
+ */
5320
+ async signInWithPasskey(credentials) {
5321
+ var _a, _b, _c;
5322
+ (0, helpers_1.assertPasskeyExperimentalEnabled)(this.experimental);
5323
+ try {
5324
+ if (!(0, webauthn_1.browserSupportsWebAuthn)()) {
5325
+ return this._returnResult({
5326
+ data: null,
5327
+ error: new errors_1.AuthUnknownError('Browser does not support WebAuthn', null),
5328
+ });
5329
+ }
5330
+ // 1. Get challenge options from server
5331
+ const { data: options, error: optionsError } = await this._startPasskeyAuthentication({
5332
+ options: { captchaToken: (_a = credentials === null || credentials === void 0 ? void 0 : credentials.options) === null || _a === void 0 ? void 0 : _a.captchaToken },
5333
+ });
5334
+ if (optionsError || !options) {
5335
+ return this._returnResult({ data: null, error: optionsError });
5336
+ }
5337
+ // 2. Deserialize and prompt user via browser WebAuthn API
5338
+ const publicKeyOptions = (0, webauthn_1.deserializeCredentialRequestOptions)(options.options);
5339
+ const signal = (_c = (_b = credentials === null || credentials === void 0 ? void 0 : credentials.options) === null || _b === void 0 ? void 0 : _b.signal) !== null && _c !== void 0 ? _c : webauthn_1.webAuthnAbortService.createNewAbortSignal();
5340
+ const { data: credential, error: credentialError } = await (0, webauthn_1.getCredential)({
5341
+ publicKey: publicKeyOptions,
5342
+ signal,
5343
+ });
5344
+ if (credentialError || !credential) {
5345
+ return this._returnResult({
5346
+ data: null,
5347
+ error: credentialError !== null && credentialError !== void 0 ? credentialError : new errors_1.AuthUnknownError('WebAuthn ceremony failed', null),
5348
+ });
5349
+ }
5350
+ // 3. Serialize and verify with server
5351
+ const serialized = (0, webauthn_1.serializeCredentialRequestResponse)(credential);
5352
+ return this._verifyPasskeyAuthentication({
5353
+ challengeId: options.challenge_id,
5354
+ credential: serialized,
5355
+ });
5356
+ }
5357
+ catch (error) {
5358
+ if ((0, errors_1.isAuthError)(error)) {
5359
+ return this._returnResult({ data: null, error });
5360
+ }
5361
+ throw error;
5362
+ }
5363
+ }
5364
+ /**
5365
+ * Register a passkey for the current authenticated user. Handles the full WebAuthn ceremony:
5366
+ * 1. Fetches registration challenge from server
5367
+ * 2. Prompts user via navigator.credentials.create()
5368
+ * 3. Verifies credential with server
5369
+ *
5370
+ * Requires an active session. Requires `auth.experimental.passkey: true`.
5371
+ *
5372
+ * @category Auth
5373
+ */
5374
+ async registerPasskey(credentials) {
5375
+ var _a, _b;
5376
+ (0, helpers_1.assertPasskeyExperimentalEnabled)(this.experimental);
5377
+ try {
5378
+ if (!(0, webauthn_1.browserSupportsWebAuthn)()) {
5379
+ return this._returnResult({
5380
+ data: null,
5381
+ error: new errors_1.AuthUnknownError('Browser does not support WebAuthn', null),
5382
+ });
5383
+ }
5384
+ // 1. Get challenge options from server
5385
+ const { data: options, error: optionsError } = await this._startPasskeyRegistration();
5386
+ if (optionsError || !options) {
5387
+ return this._returnResult({ data: null, error: optionsError });
5388
+ }
5389
+ // 2. Deserialize and prompt user via browser WebAuthn API
5390
+ const publicKeyOptions = (0, webauthn_1.deserializeCredentialCreationOptions)(options.options);
5391
+ const signal = (_b = (_a = credentials === null || credentials === void 0 ? void 0 : credentials.options) === null || _a === void 0 ? void 0 : _a.signal) !== null && _b !== void 0 ? _b : webauthn_1.webAuthnAbortService.createNewAbortSignal();
5392
+ const { data: credential, error: credentialError } = await (0, webauthn_1.createCredential)({
5393
+ publicKey: publicKeyOptions,
5394
+ signal,
5395
+ });
5396
+ if (credentialError || !credential) {
5397
+ return this._returnResult({
5398
+ data: null,
5399
+ error: credentialError !== null && credentialError !== void 0 ? credentialError : new errors_1.AuthUnknownError('WebAuthn ceremony failed', null),
5400
+ });
5401
+ }
5402
+ // 3. Serialize and verify with server
5403
+ const serialized = (0, webauthn_1.serializeCredentialCreationResponse)(credential);
5404
+ return this._verifyPasskeyRegistration({
5405
+ challengeId: options.challenge_id,
5406
+ credential: serialized,
5407
+ });
5408
+ }
5409
+ catch (error) {
5410
+ if ((0, errors_1.isAuthError)(error)) {
5411
+ return this._returnResult({ data: null, error });
5412
+ }
5413
+ throw error;
5414
+ }
5415
+ }
5416
+ /**
5417
+ * Start passkey registration for the current authenticated user.
5418
+ * Returns WebAuthn credential creation options to pass to navigator.credentials.create().
5419
+ */
5420
+ async _startPasskeyRegistration() {
5421
+ (0, helpers_1.assertPasskeyExperimentalEnabled)(this.experimental);
5422
+ try {
5423
+ return await this._useSession(async (result) => {
5424
+ const { data: { session }, error: sessionError, } = result;
5425
+ if (sessionError) {
5426
+ return this._returnResult({ data: null, error: sessionError });
5427
+ }
5428
+ if (!session) {
5429
+ return this._returnResult({ data: null, error: new errors_1.AuthSessionMissingError() });
5430
+ }
5431
+ const { data, error } = await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/passkeys/registration/options`, {
5432
+ headers: this.headers,
5433
+ jwt: session.access_token,
5434
+ body: {},
5435
+ });
5436
+ if (error) {
5437
+ return this._returnResult({ data: null, error });
5438
+ }
5439
+ return this._returnResult({ data, error: null });
5440
+ });
5441
+ }
5442
+ catch (error) {
5443
+ if ((0, errors_1.isAuthError)(error)) {
5444
+ return this._returnResult({ data: null, error });
5445
+ }
5446
+ throw error;
5447
+ }
5448
+ }
5449
+ /**
5450
+ * Verify passkey registration with the credential response.
5451
+ * The credentialResponse should be the serialized output of navigator.credentials.create().
5452
+ */
5453
+ async _verifyPasskeyRegistration(params) {
5454
+ (0, helpers_1.assertPasskeyExperimentalEnabled)(this.experimental);
5455
+ try {
5456
+ return await this._useSession(async (result) => {
5457
+ const { data: { session }, error: sessionError, } = result;
5458
+ if (sessionError) {
5459
+ return this._returnResult({ data: null, error: sessionError });
5460
+ }
5461
+ if (!session) {
5462
+ return this._returnResult({ data: null, error: new errors_1.AuthSessionMissingError() });
5463
+ }
5464
+ const { data, error } = await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/passkeys/registration/verify`, {
5465
+ headers: this.headers,
5466
+ jwt: session.access_token,
5467
+ body: {
5468
+ challenge_id: params.challengeId,
5469
+ credential: params.credential,
5470
+ },
5471
+ });
5472
+ if (error) {
5473
+ return this._returnResult({ data: null, error });
5474
+ }
5475
+ return this._returnResult({ data, error: null });
5476
+ });
5477
+ }
5478
+ catch (error) {
5479
+ if ((0, errors_1.isAuthError)(error)) {
5480
+ return this._returnResult({ data: null, error });
5481
+ }
5482
+ throw error;
5483
+ }
5484
+ }
5485
+ /**
5486
+ * Start passkey authentication.
5487
+ * Returns WebAuthn credential request options to pass to navigator.credentials.get().
5488
+ */
5489
+ async _startPasskeyAuthentication(params) {
5490
+ var _a;
5491
+ (0, helpers_1.assertPasskeyExperimentalEnabled)(this.experimental);
5492
+ try {
5493
+ const { data, error } = await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/passkeys/authentication/options`, {
5494
+ headers: this.headers,
5495
+ body: {
5496
+ gotrue_meta_security: { captcha_token: (_a = params === null || params === void 0 ? void 0 : params.options) === null || _a === void 0 ? void 0 : _a.captchaToken },
5497
+ },
5498
+ });
5499
+ if (error) {
5500
+ return this._returnResult({ data: null, error });
5501
+ }
5502
+ return this._returnResult({ data, error: null });
5503
+ }
5504
+ catch (error) {
5505
+ if ((0, errors_1.isAuthError)(error)) {
5506
+ return this._returnResult({ data: null, error });
5507
+ }
5508
+ throw error;
5509
+ }
5510
+ }
5511
+ /**
5512
+ * Verify passkey authentication and create a session.
5513
+ * The credential should be the serialized output of navigator.credentials.get().
5514
+ */
5515
+ async _verifyPasskeyAuthentication(params) {
5516
+ (0, helpers_1.assertPasskeyExperimentalEnabled)(this.experimental);
5517
+ try {
5518
+ const { data, error } = await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/passkeys/authentication/verify`, {
5519
+ headers: this.headers,
5520
+ body: {
5521
+ challenge_id: params.challengeId,
5522
+ credential: params.credential,
5523
+ },
5524
+ xform: fetch_1._sessionResponse,
5525
+ });
5526
+ if (error) {
5527
+ return this._returnResult({ data: null, error });
5528
+ }
5529
+ if (data.session) {
5530
+ await this._saveSession(data.session);
5531
+ await this._notifyAllSubscribers('SIGNED_IN', data.session);
5532
+ }
5533
+ return this._returnResult({ data, error: null });
5534
+ }
5535
+ catch (error) {
5536
+ if ((0, errors_1.isAuthError)(error)) {
5537
+ return this._returnResult({ data: null, error });
5538
+ }
5539
+ throw error;
5540
+ }
5541
+ }
5542
+ /**
5543
+ * List all passkeys for the current user.
5544
+ */
5545
+ async _listPasskeys() {
5546
+ (0, helpers_1.assertPasskeyExperimentalEnabled)(this.experimental);
5547
+ try {
5548
+ return await this._useSession(async (result) => {
5549
+ const { data: { session }, error: sessionError, } = result;
5550
+ if (sessionError) {
5551
+ return this._returnResult({ data: null, error: sessionError });
5552
+ }
5553
+ if (!session) {
5554
+ return this._returnResult({ data: null, error: new errors_1.AuthSessionMissingError() });
5555
+ }
5556
+ const { data, error } = await (0, fetch_1._request)(this.fetch, 'GET', `${this.url}/passkeys`, {
5557
+ headers: this.headers,
5558
+ jwt: session.access_token,
5559
+ xform: (data) => ({ data, error: null }),
5560
+ });
5561
+ if (error) {
5562
+ return this._returnResult({ data: null, error });
5563
+ }
5564
+ return this._returnResult({ data, error: null });
5565
+ });
5566
+ }
5567
+ catch (error) {
5568
+ if ((0, errors_1.isAuthError)(error)) {
5569
+ return this._returnResult({ data: null, error });
5570
+ }
5571
+ throw error;
5572
+ }
5573
+ }
5574
+ /**
5575
+ * Update a passkey.
5576
+ */
5577
+ async _updatePasskey(params) {
5578
+ (0, helpers_1.assertPasskeyExperimentalEnabled)(this.experimental);
5579
+ try {
5580
+ return await this._useSession(async (result) => {
5581
+ const { data: { session }, error: sessionError, } = result;
5582
+ if (sessionError) {
5583
+ return this._returnResult({ data: null, error: sessionError });
5584
+ }
5585
+ if (!session) {
5586
+ return this._returnResult({ data: null, error: new errors_1.AuthSessionMissingError() });
5587
+ }
5588
+ const { data, error } = await (0, fetch_1._request)(this.fetch, 'PATCH', `${this.url}/passkeys/${params.passkeyId}`, {
5589
+ headers: this.headers,
5590
+ jwt: session.access_token,
5591
+ body: { friendly_name: params.friendlyName },
5592
+ });
5593
+ if (error) {
5594
+ return this._returnResult({ data: null, error });
5595
+ }
5596
+ return this._returnResult({ data, error: null });
5597
+ });
5598
+ }
5599
+ catch (error) {
5600
+ if ((0, errors_1.isAuthError)(error)) {
5601
+ return this._returnResult({ data: null, error });
5602
+ }
5603
+ throw error;
5604
+ }
5605
+ }
5606
+ /**
5607
+ * Delete a passkey.
5608
+ */
5609
+ async _deletePasskey(params) {
5610
+ (0, helpers_1.assertPasskeyExperimentalEnabled)(this.experimental);
5611
+ try {
5612
+ return await this._useSession(async (result) => {
5613
+ const { data: { session }, error: sessionError, } = result;
5614
+ if (sessionError) {
5615
+ return this._returnResult({ data: null, error: sessionError });
5616
+ }
5617
+ if (!session) {
5618
+ return this._returnResult({ data: null, error: new errors_1.AuthSessionMissingError() });
5619
+ }
5620
+ const { error } = await (0, fetch_1._request)(this.fetch, 'DELETE', `${this.url}/passkeys/${params.passkeyId}`, {
5621
+ headers: this.headers,
5622
+ jwt: session.access_token,
5623
+ noResolveJson: true,
5624
+ });
5625
+ if (error) {
5626
+ return this._returnResult({ data: null, error });
5627
+ }
5628
+ return this._returnResult({ data: null, error: null });
5629
+ });
5630
+ }
5631
+ catch (error) {
5632
+ if ((0, errors_1.isAuthError)(error)) {
5633
+ return this._returnResult({ data: null, error });
5634
+ }
5635
+ throw error;
5636
+ }
5637
+ }
4850
5638
  }
4851
5639
  GoTrueClient.nextInstanceID = {};
4852
5640
  exports.default = GoTrueClient;