@depup/supabase__auth-js 2.103.0-depup.0 → 2.110.8-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 +946 -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 +951 -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 +1117 -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,10 @@ 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) || (0, errors_1.isAuthRetryableFetchError)(err)) {
3610
+ // A missing session or a transient/aborted network failure (e.g. a
3611
+ // superseded page navigation cancelling the in-flight request) is
3612
+ // not an application error — warn rather than surface it raw.
3395
3613
  console.warn(err);
3396
3614
  }
3397
3615
  else {
@@ -3587,7 +3805,7 @@ class GoTrueClient {
3587
3805
  var _a;
3588
3806
  try {
3589
3807
  const { data, error } = await this._useSession(async (result) => {
3590
- var _a, _b, _c, _d, _e;
3808
+ var _a, _b, _c, _d, _f;
3591
3809
  const { data, error } = result;
3592
3810
  if (error)
3593
3811
  throw error;
@@ -3599,7 +3817,7 @@ class GoTrueClient {
3599
3817
  });
3600
3818
  return await (0, fetch_1._request)(this.fetch, 'GET', url, {
3601
3819
  headers: this.headers,
3602
- jwt: (_e = (_d = data.session) === null || _d === void 0 ? void 0 : _d.access_token) !== null && _e !== void 0 ? _e : undefined,
3820
+ jwt: (_f = (_d = data.session) === null || _d === void 0 ? void 0 : _d.access_token) !== null && _f !== void 0 ? _f : undefined,
3603
3821
  });
3604
3822
  });
3605
3823
  if (error)
@@ -3716,7 +3934,10 @@ class GoTrueClient {
3716
3934
  * @param refreshToken A valid refresh token that was returned on login.
3717
3935
  */
3718
3936
  async _refreshAccessToken(refreshToken) {
3719
- const debugName = `#_refreshAccessToken(${refreshToken.substring(0, 5)}...)`;
3937
+ // Refresh tokens are long-lived bearer credentials; do NOT include any
3938
+ // fragment of the token in the debug tag, even when `debug: true` is
3939
+ // enabled (logs may be forwarded to third-party services).
3940
+ const debugName = `#_refreshAccessToken()`;
3720
3941
  this._debug(debugName, 'begin');
3721
3942
  try {
3722
3943
  const startedAt = Date.now();
@@ -3823,10 +4044,17 @@ class GoTrueClient {
3823
4044
  if (this.autoRefreshToken && currentSession.refresh_token) {
3824
4045
  const { error } = await this._callRefreshToken(currentSession.refresh_token);
3825
4046
  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();
4047
+ // `_callRefreshToken` is the single source of truth for refresh
4048
+ // outcomes: it removes the session itself when the access token
4049
+ // is actually expired, and preserves it when the token is still
4050
+ // valid (proactive-preserve). Don't second-guess that here — a
4051
+ // local `_removeSession` would emit a duplicate `SIGNED_OUT` on
4052
+ // genuine failures and undo the proactive-preserve at init time.
4053
+ if ((0, errors_1.isAuthRefreshDiscardedError)(error)) {
4054
+ this._debug(debugName, 'refresh discarded by commit guard', error);
4055
+ }
4056
+ else {
4057
+ this._debug(debugName, 'refresh failed', error);
3830
4058
  }
3831
4059
  }
3832
4060
  }
@@ -3859,7 +4087,16 @@ class GoTrueClient {
3859
4087
  }
3860
4088
  catch (err) {
3861
4089
  this._debug(debugName, 'error', err);
3862
- console.error(err);
4090
+ if ((0, errors_1.isAuthRetryableFetchError)(err)) {
4091
+ // Transient/aborted network failure during session recovery (e.g. a
4092
+ // superseded page navigation cancelling the in-flight request). Warn
4093
+ // rather than surface it raw; the session is untouched and recovery
4094
+ // will run again on the next load.
4095
+ console.warn(err);
4096
+ }
4097
+ else {
4098
+ console.error(err);
4099
+ }
3863
4100
  return;
3864
4101
  }
3865
4102
  finally {
@@ -3875,18 +4112,91 @@ class GoTrueClient {
3875
4112
  if (this.refreshingDeferred) {
3876
4113
  return this.refreshingDeferred.promise;
3877
4114
  }
3878
- const debugName = `#_callRefreshToken(${refreshToken.substring(0, 5)}...)`;
4115
+ // Serial failure cooldown: callers passing the *same* refresh token
4116
+ // after a recent failure receive the cached result instead of firing
4117
+ // another `/token` request. This caps the proactive-refresh storm
4118
+ // where every `getSession()` call inside the 90s EXPIRY_MARGIN_MS
4119
+ // window kept re-firing against the same broken refresh token during
4120
+ // outages. Concurrent callers already share `refreshingDeferred`; this
4121
+ // cache covers serial callers spaced across cooldown windows.
4122
+ //
4123
+ // Token-keyed so callers with a fresh refresh token (rotation pickup
4124
+ // from another tab, explicit `setSession`/`refreshSession({ refresh_token })`,
4125
+ // multi-account switch) bypass the cache and attempt a real refresh.
4126
+ if (this.lastRefreshFailure &&
4127
+ this.lastRefreshFailure.refreshToken === refreshToken &&
4128
+ Date.now() < this.lastRefreshFailure.expiresAt) {
4129
+ this._debug('#_callRefreshToken()', 'returning cached failure (cooldown active)');
4130
+ return this.lastRefreshFailure.result;
4131
+ }
4132
+ // Refresh tokens are long-lived bearer credentials; do NOT include any
4133
+ // fragment of the token in the debug tag, even when `debug: true` is
4134
+ // enabled (logs may be forwarded to third-party services).
4135
+ const debugName = `#_callRefreshToken()`;
3879
4136
  this._debug(debugName, 'begin');
3880
4137
  try {
3881
4138
  this.refreshingDeferred = new helpers_1.Deferred();
4139
+ // Snapshot storage before the fetch. The commit guard discards the
4140
+ // rotated tokens only when a non-null pre-fetch snapshot changed under
4141
+ // us — typical case: a concurrent `signOut` ran `_removeSession`, or
4142
+ // another tab's refresh rewrote the slot. Callers passing
4143
+ // externally-sourced tokens (SSR cookie handoff, multi-account
4144
+ // switching, `setSession`/`refreshSession({ refresh_token })`) may
4145
+ // start from a null snapshot OR from a non-null snapshot whose
4146
+ // refresh_token differs from the one they're hydrating; in both
4147
+ // cases the guard fires only when storage was *modified between
4148
+ // snapshots*, not when the input token disagrees with what's stored.
4149
+ const storedAtStart = (await (0, helpers_1.getItemAsync)(this.storage, this.storageKey));
3882
4150
  const { data, error } = await this._refreshAccessToken(refreshToken);
3883
4151
  if (error)
3884
4152
  throw error;
3885
4153
  if (!data.session)
3886
4154
  throw new errors_1.AuthSessionMissingError();
4155
+ const storedAfter = (await (0, helpers_1.getItemAsync)(this.storage, this.storageKey));
4156
+ const storageChangedUnderUs = storedAtStart !== null &&
4157
+ (storedAfter === null || storedAfter.refresh_token !== storedAtStart.refresh_token);
4158
+ if (storageChangedUnderUs) {
4159
+ this._debug(debugName, 'commit guard: storage changed since refresh started, discarding rotated tokens', {
4160
+ // Presence indicators only — never log refresh token fragments,
4161
+ // even partial. Logs may be forwarded to third-party services.
4162
+ startedWith: 'present',
4163
+ nowHolds: storedAfter ? 'replaced' : 'cleared',
4164
+ });
4165
+ const discarded = {
4166
+ data: null,
4167
+ error: new errors_1.AuthRefreshDiscardedError(),
4168
+ };
4169
+ this.refreshingDeferred.resolve(discarded);
4170
+ return discarded;
4171
+ }
4172
+ // Second leg of the commit guard: close the TOCTOU window between the
4173
+ // synchronous `storageChangedUnderUs` check and the actual storage
4174
+ // writes inside `_saveSession`. A concurrent `signOut → _removeSession`
4175
+ // can land inside `_saveSession`'s `await setItemAsync(...)` yields and
4176
+ // clear storage just before we overwrite it. Capture the epoch BEFORE
4177
+ // the save and re-check after; if it advanced, undo the write directly
4178
+ // (do NOT call `_removeSession` — that would emit a duplicate
4179
+ // SIGNED_OUT for the concurrent signOut that already fired one).
4180
+ const epochBeforeSave = this._sessionRemovalEpoch;
3887
4181
  await this._saveSession(data.session);
4182
+ if (this._sessionRemovalEpoch !== epochBeforeSave) {
4183
+ this._debug(debugName, 'commit guard (post-save): _removeSession ran during _saveSession, undoing write');
4184
+ await (0, helpers_1.removeItemAsync)(this.storage, this.storageKey);
4185
+ if (this.userStorage) {
4186
+ await (0, helpers_1.removeItemAsync)(this.userStorage, this.storageKey + '-user');
4187
+ }
4188
+ const discarded = {
4189
+ data: null,
4190
+ error: new errors_1.AuthRefreshDiscardedError(),
4191
+ };
4192
+ this.refreshingDeferred.resolve(discarded);
4193
+ return discarded;
4194
+ }
3888
4195
  await this._notifyAllSubscribers('TOKEN_REFRESHED', data.session);
3889
4196
  const result = { data: data.session, error: null };
4197
+ // Refresh succeeded — clear any cached failure so the next caller
4198
+ // (including the auto-refresh ticker) attempts a real refresh again.
4199
+ this.lastRefreshFailure = null;
3890
4200
  this.refreshingDeferred.resolve(result);
3891
4201
  return result;
3892
4202
  }
@@ -3895,8 +4205,35 @@ class GoTrueClient {
3895
4205
  if ((0, errors_1.isAuthError)(error)) {
3896
4206
  const result = { data: null, error };
3897
4207
  if (!(0, errors_1.isAuthRetryableFetchError)(error)) {
3898
- await this._removeSession();
4208
+ // Proactive vs reactive distinction: a refresh fires whenever
4209
+ // the access token is within EXPIRY_MARGIN_MS of expiry. If the
4210
+ // access token is *still valid* at this moment, the refresh was
4211
+ // proactive and the existing session is still usable until its
4212
+ // real expiry — destroying it now would log out a user whose
4213
+ // access token works. If the access token has actually expired,
4214
+ // the refresh token is the only credential left and it just got
4215
+ // rejected — the session is genuinely dead. `__loadSession`
4216
+ // mirrors this distinction on the read path so callers see the
4217
+ // preserved session instead of `session: null`.
4218
+ const storedNow = (await (0, helpers_1.getItemAsync)(this.storage, this.storageKey));
4219
+ const accessTokenStillValid = !!((storedNow === null || storedNow === void 0 ? void 0 : storedNow.expires_at) && storedNow.expires_at * 1000 > Date.now());
4220
+ if (accessTokenStillValid) {
4221
+ this._debug(debugName, 'proactive refresh failed, access token still valid — preserving session');
4222
+ }
4223
+ else {
4224
+ await this._removeSession();
4225
+ }
3899
4226
  }
4227
+ // Cache the failure so serial callers (and the next auto-refresh
4228
+ // tick) passing the same refresh token within the cooldown window
4229
+ // receive it synchronously instead of firing another `/token`
4230
+ // call. Set after the optional `_removeSession` above (which
4231
+ // clears the cache as part of teardown) so the cache survives.
4232
+ this.lastRefreshFailure = {
4233
+ refreshToken,
4234
+ result,
4235
+ expiresAt: Date.now() + constants_1.REFRESH_FAILURE_COOLDOWN_MS,
4236
+ };
3900
4237
  (_a = this.refreshingDeferred) === null || _a === void 0 ? void 0 : _a.resolve(result);
3901
4238
  return result;
3902
4239
  }
@@ -3909,6 +4246,19 @@ class GoTrueClient {
3909
4246
  }
3910
4247
  }
3911
4248
  async _notifyAllSubscribers(event, session, broadcast = true) {
4249
+ if (this._pendingInitNotifications !== null && broadcast) {
4250
+ // We're inside initialize() before initializePromise has resolved, and
4251
+ // this notification originates from the init chain (_recoverAndRefresh),
4252
+ // which always fires with broadcast = true. Enqueue instead of firing so
4253
+ // that callbacks can safely call getSession() / getUser() without
4254
+ // deadlocking on initializePromise.
4255
+ //
4256
+ // Notifications with broadcast = false come from the incoming
4257
+ // BroadcastChannel handler (a cross-tab event), not the init chain, so
4258
+ // they are fired immediately to preserve multi-tab ordering.
4259
+ this._pendingInitNotifications.push({ event, session, broadcast });
4260
+ return;
4261
+ }
3912
4262
  const debugName = `#_notifyAllSubscribers(${event})`;
3913
4263
  this._debug(debugName, 'begin', session, `broadcast = ${broadcast}`);
3914
4264
  try {
@@ -3945,7 +4295,6 @@ class GoTrueClient {
3945
4295
  // _saveSession is always called whenever a new session has been acquired
3946
4296
  // so we can safely suppress the warning returned by future getSession calls
3947
4297
  this.suppressGetSessionWarning = true;
3948
- await (0, helpers_1.removeItemAsync)(this.storage, `${this.storageKey}-code-verifier`);
3949
4298
  // Create a shallow copy to work with, to avoid mutating the original session object if it's used elsewhere
3950
4299
  const sessionToProcess = Object.assign({}, session);
3951
4300
  const userIsProxy = sessionToProcess.user && sessionToProcess.user.__isUserNotAvailableProxy === true;
@@ -3978,7 +4327,15 @@ class GoTrueClient {
3978
4327
  }
3979
4328
  }
3980
4329
  async _removeSession() {
4330
+ // Bump synchronously, BEFORE any `await`, so that `_callRefreshToken`'s
4331
+ // post-save check sees the increment whenever this method has started —
4332
+ // even if it hasn't finished. Pairs with the epoch check in
4333
+ // `_callRefreshToken`. See `_sessionRemovalEpoch` field doc.
4334
+ this._sessionRemovalEpoch += 1;
3981
4335
  this._debug('#_removeSession()');
4336
+ // The session is gone — no point holding on to a cached refresh failure
4337
+ // for a token that no longer exists. Synchronous, before any `await`.
4338
+ this.lastRefreshFailure = null;
3982
4339
  this.suppressGetSessionWarning = false;
3983
4340
  await (0, helpers_1.removeItemAsync)(this.storage, this.storageKey);
3984
4341
  await (0, helpers_1.removeItemAsync)(this.storage, this.storageKey + '-code-verifier');
@@ -3991,8 +4348,8 @@ class GoTrueClient {
3991
4348
  /**
3992
4349
  * Removes any registered visibilitychange callback.
3993
4350
  *
3994
- * {@see #startAutoRefresh}
3995
- * {@see #stopAutoRefresh}
4351
+ * {@link GoTrueClient.startAutoRefresh}
4352
+ * {@link GoTrueClient.stopAutoRefresh}
3996
4353
  */
3997
4354
  _removeVisibilityChangedCallback() {
3998
4355
  this._debug('#_removeVisibilityChangedCallback()');
@@ -4008,7 +4365,7 @@ class GoTrueClient {
4008
4365
  }
4009
4366
  }
4010
4367
  /**
4011
- * This is the private implementation of {@link #startAutoRefresh}. Use this
4368
+ * This is the private implementation of {@link GoTrueClient.startAutoRefresh}. Use this
4012
4369
  * within the library.
4013
4370
  */
4014
4371
  async _startAutoRefresh() {
@@ -4050,7 +4407,7 @@ class GoTrueClient {
4050
4407
  }
4051
4408
  }
4052
4409
  /**
4053
- * This is the private implementation of {@link #stopAutoRefresh}. Use this
4410
+ * This is the private implementation of {@link GoTrueClient.stopAutoRefresh}. Use this
4054
4411
  * within the library.
4055
4412
  */
4056
4413
  async _stopAutoRefresh() {
@@ -4086,7 +4443,7 @@ class GoTrueClient {
4086
4443
  * platform's foreground indication mechanism and call these methods
4087
4444
  * appropriately to conserve resources.
4088
4445
  *
4089
- * {@see #stopAutoRefresh}
4446
+ * {@link GoTrueClient.stopAutoRefresh}
4090
4447
  *
4091
4448
  * @category Auth
4092
4449
  *
@@ -4120,7 +4477,7 @@ class GoTrueClient {
4120
4477
  * If you call this method any managed visibility change callback will be
4121
4478
  * removed and you must manage visibility changes on your own.
4122
4479
  *
4123
- * See {@link #startAutoRefresh} for more details.
4480
+ * See {@link GoTrueClient.startAutoRefresh} for more details.
4124
4481
  *
4125
4482
  * @category Auth
4126
4483
  *
@@ -4148,47 +4505,122 @@ class GoTrueClient {
4148
4505
  this._removeVisibilityChangedCallback();
4149
4506
  await this._stopAutoRefresh();
4150
4507
  }
4508
+ /**
4509
+ * Tears down the client's background work: stops the auto-refresh interval,
4510
+ * removes the `visibilitychange` listener, closes the cross-tab
4511
+ * `BroadcastChannel`, and clears registered `onAuthStateChange` subscribers.
4512
+ *
4513
+ * Call this from cleanup hooks when the client is being replaced before
4514
+ * its JS realm is destroyed. React Strict Mode and HMR are the common
4515
+ * cases. Any in-flight `fetch` calls continue to completion and may still
4516
+ * write to storage; dispose doesn't abort them or erase storage.
4517
+ *
4518
+ * Lifecycle caveat: because in-flight refreshes are not aborted, a
4519
+ * disposed instance can still persist a rotated session to storage after
4520
+ * `dispose()` returns. A subsequent `createClient` against the same
4521
+ * `storageKey` will pick up that session on its next read. If you need
4522
+ * strict isolation between client lifecycles, await any pending auth
4523
+ * operation before calling `dispose()` (or change the `storageKey` for
4524
+ * the replacement client).
4525
+ *
4526
+ * Safe to call repeatedly.
4527
+ *
4528
+ * @category Auth
4529
+ *
4530
+ * @example Cleanup on React unmount
4531
+ * ```ts
4532
+ * useEffect(() => {
4533
+ * const client = createClient(...)
4534
+ * return () => { client.auth.dispose() }
4535
+ * }, [])
4536
+ * ```
4537
+ */
4538
+ async dispose() {
4539
+ var _a;
4540
+ this._removeVisibilityChangedCallback();
4541
+ await this._stopAutoRefresh();
4542
+ (_a = this.broadcastChannel) === null || _a === void 0 ? void 0 : _a.close();
4543
+ this.broadcastChannel = null;
4544
+ this.stateChangeEmitters.clear();
4545
+ }
4151
4546
  /**
4152
4547
  * Runs the auto refresh token tick.
4153
4548
  */
4154
4549
  async _autoRefreshTokenTick() {
4155
4550
  this._debug('#_autoRefreshTokenTick()', 'begin');
4156
- try {
4157
- await this._acquireLock(0, async () => {
4158
- try {
4159
- const now = Date.now();
4551
+ if (this.lock != null) {
4552
+ // TODO(v3): remove legacy lock path. Uses `_acquireLock(0, ...)` which
4553
+ // throws `LockAcquireTimeoutError` immediately if the lock is held —
4554
+ // that's the fail-fast skip path that lets the tick bail out instead
4555
+ // of queuing behind a long-running operation.
4556
+ try {
4557
+ await this._acquireLock(0, async () => {
4160
4558
  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
- });
4559
+ const now = Date.now();
4560
+ try {
4561
+ return await this._useSession(async (result) => {
4562
+ const { data: { session }, } = result;
4563
+ if (!session || !session.refresh_token || !session.expires_at) {
4564
+ this._debug('#_autoRefreshTokenTick()', 'no session');
4565
+ return;
4566
+ }
4567
+ const expiresInTicks = Math.floor((session.expires_at * 1000 - now) / constants_1.AUTO_REFRESH_TICK_DURATION_MS);
4568
+ 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`);
4569
+ if (expiresInTicks <= constants_1.AUTO_REFRESH_TICK_THRESHOLD) {
4570
+ await this._callRefreshToken(session.refresh_token);
4571
+ }
4572
+ });
4573
+ }
4574
+ catch (e) {
4575
+ console.error('Auto refresh tick failed with error. This is likely a transient error.', e);
4576
+ }
4174
4577
  }
4175
- catch (e) {
4176
- console.error('Auto refresh tick failed with error. This is likely a transient error.', e);
4578
+ finally {
4579
+ this._debug('#_autoRefreshTokenTick()', 'end');
4177
4580
  }
4581
+ });
4582
+ }
4583
+ catch (e) {
4584
+ if (e instanceof locks_1.LockAcquireTimeoutError) {
4585
+ this._debug('auto refresh token tick lock not available');
4178
4586
  }
4179
- finally {
4180
- this._debug('#_autoRefreshTokenTick()', 'end');
4587
+ else {
4588
+ throw e;
4181
4589
  }
4182
- });
4590
+ }
4591
+ return;
4183
4592
  }
4184
- catch (e) {
4185
- if (e.isAcquireTimeout || e instanceof locks_1.LockAcquireTimeoutError) {
4186
- this._debug('auto refresh token tick lock not available');
4593
+ // Lockless default: skip if a refresh is already in flight.
4594
+ // `_callRefreshToken` also dedupes via the same field; this is just a
4595
+ // fast-path skip to avoid an unnecessary storage read.
4596
+ if (this.refreshingDeferred !== null) {
4597
+ this._debug('#_autoRefreshTokenTick()', 'refresh already in flight, skipping');
4598
+ return;
4599
+ }
4600
+ try {
4601
+ const now = Date.now();
4602
+ try {
4603
+ await this._useSession(async (result) => {
4604
+ const { data: { session }, } = result;
4605
+ if (!session || !session.refresh_token || !session.expires_at) {
4606
+ this._debug('#_autoRefreshTokenTick()', 'no session');
4607
+ return;
4608
+ }
4609
+ // session will expire in this many ticks (or has already expired if <= 0)
4610
+ const expiresInTicks = Math.floor((session.expires_at * 1000 - now) / constants_1.AUTO_REFRESH_TICK_DURATION_MS);
4611
+ 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`);
4612
+ if (expiresInTicks <= constants_1.AUTO_REFRESH_TICK_THRESHOLD) {
4613
+ await this._callRefreshToken(session.refresh_token);
4614
+ }
4615
+ });
4187
4616
  }
4188
- else {
4189
- throw e;
4617
+ catch (e) {
4618
+ console.error('Auto refresh tick failed with error. This is likely a transient error.', e);
4190
4619
  }
4191
4620
  }
4621
+ finally {
4622
+ this._debug('#_autoRefreshTokenTick()', 'end');
4623
+ }
4192
4624
  }
4193
4625
  /**
4194
4626
  * Registers callbacks on the browser / platform, which in-turn run
@@ -4237,18 +4669,26 @@ class GoTrueClient {
4237
4669
  if (!calledFromInitialize) {
4238
4670
  // called when the visibility has changed, i.e. the browser
4239
4671
  // 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
4672
+ // should be recovered
4242
4673
  await this.initializePromise;
4243
- await this._acquireLock(this.lockAcquireTimeout, async () => {
4674
+ if (this.lock != null) {
4675
+ // TODO(v3): remove legacy lock path
4676
+ await this._acquireLock(this.lockAcquireTimeout, async () => {
4677
+ if (document.visibilityState !== 'visible') {
4678
+ this._debug(methodName, 'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting');
4679
+ return;
4680
+ }
4681
+ await this._recoverAndRefresh();
4682
+ });
4683
+ }
4684
+ else {
4244
4685
  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
4686
+ this._debug(methodName, 'visibilityState is no longer visible, skipping recovery');
4247
4687
  return;
4248
4688
  }
4249
4689
  // recover the session
4250
4690
  await this._recoverAndRefresh();
4251
- });
4691
+ }
4252
4692
  }
4253
4693
  }
4254
4694
  else if (document.visibilityState === 'hidden') {
@@ -4344,7 +4784,7 @@ class GoTrueClient {
4344
4784
  }
4345
4785
  }
4346
4786
  async _verify(params) {
4347
- return this._acquireLock(this.lockAcquireTimeout, async () => {
4787
+ const run = async () => {
4348
4788
  try {
4349
4789
  return await this._useSession(async (result) => {
4350
4790
  var _a;
@@ -4378,10 +4818,15 @@ class GoTrueClient {
4378
4818
  }
4379
4819
  throw error;
4380
4820
  }
4381
- });
4821
+ };
4822
+ if (this.lock != null) {
4823
+ // TODO(v3): remove legacy lock path
4824
+ return this._acquireLock(this.lockAcquireTimeout, run);
4825
+ }
4826
+ return run();
4382
4827
  }
4383
4828
  async _challenge(params) {
4384
- return this._acquireLock(this.lockAcquireTimeout, async () => {
4829
+ const run = async () => {
4385
4830
  try {
4386
4831
  return await this._useSession(async (result) => {
4387
4832
  var _a;
@@ -4421,14 +4866,17 @@ class GoTrueClient {
4421
4866
  }
4422
4867
  throw error;
4423
4868
  }
4424
- });
4869
+ };
4870
+ if (this.lock != null) {
4871
+ // TODO(v3): remove legacy lock path
4872
+ return this._acquireLock(this.lockAcquireTimeout, run);
4873
+ }
4874
+ return run();
4425
4875
  }
4426
4876
  /**
4427
- * {@see GoTrueMFAApi#challengeAndVerify}
4877
+ * {@link GoTrueMFAApi#challengeAndVerify}
4428
4878
  */
4429
4879
  async _challengeAndVerify(params) {
4430
- // both _challenge and _verify independently acquire the lock, so no need
4431
- // to acquire it here
4432
4880
  const { data: challengeData, error: challengeError } = await this._challenge({
4433
4881
  factorId: params.factorId,
4434
4882
  });
@@ -4442,11 +4890,10 @@ class GoTrueClient {
4442
4890
  });
4443
4891
  }
4444
4892
  /**
4445
- * {@see GoTrueMFAApi#listFactors}
4893
+ * {@link GoTrueMFAApi#listFactors}
4446
4894
  */
4447
4895
  async _listFactors() {
4448
4896
  var _a;
4449
- // use #getUser instead of #_getUser as the former acquires a lock
4450
4897
  const { data: { user }, error: userError, } = await this.getUser();
4451
4898
  if (userError) {
4452
4899
  return { data: null, error: userError };
@@ -4471,7 +4918,7 @@ class GoTrueClient {
4471
4918
  };
4472
4919
  }
4473
4920
  /**
4474
- * {@see GoTrueMFAApi#getAuthenticatorAssuranceLevel}
4921
+ * {@link GoTrueMFAApi#getAuthenticatorAssuranceLevel}
4475
4922
  */
4476
4923
  async _getAuthenticatorAssuranceLevel(jwt) {
4477
4924
  var _a, _b, _c, _d;
@@ -4722,15 +5169,15 @@ class GoTrueClient {
4722
5169
  * Extracts the JWT claims present in the access token by first verifying the
4723
5170
  * JWT against the server's JSON Web Key Set endpoint
4724
5171
  * `/.well-known/jwks.json` which is often cached, resulting in significantly
4725
- * faster responses. Prefer this method over {@link #getUser} which always
5172
+ * faster responses. Prefer this method over {@link GoTrueClient.getUser} which always
4726
5173
  * sends a request to the Auth server for each JWT.
4727
5174
  *
4728
5175
  * 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.
5176
+ * RSA) it always sends a request to the Auth server (similar to
5177
+ * {@link GoTrueClient.getUser}) to verify the JWT.
4731
5178
  *
4732
5179
  * @param jwt An optional specific JWT you wish to verify, not the one you
4733
- * can obtain from {@link #getSession}.
5180
+ * can obtain from {@link GoTrueClient.getSession}.
4734
5181
  * @param options Various additional options that allow you to customize the
4735
5182
  * behavior of this method.
4736
5183
  *
@@ -4795,8 +5242,14 @@ class GoTrueClient {
4795
5242
  }
4796
5243
  const { header, payload, signature, raw: { header: rawHeader, payload: rawPayload }, } = (0, helpers_1.decodeJWT)(token);
4797
5244
  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);
5245
+ // Reject expired JWTs should only happen if jwt argument was passed.
5246
+ // Rethrow as AuthInvalidJwtError so the outer catch converts it to { data, error }.
5247
+ try {
5248
+ (0, helpers_1.validateExp)(payload.exp);
5249
+ }
5250
+ catch (e) {
5251
+ throw new errors_1.AuthInvalidJwtError(e instanceof Error ? e.message : 'JWT validation failed');
5252
+ }
4800
5253
  }
4801
5254
  const signingKey = !header.alg ||
4802
5255
  header.alg.startsWith('HS') ||
@@ -4847,6 +5300,335 @@ class GoTrueClient {
4847
5300
  throw error;
4848
5301
  }
4849
5302
  }
5303
+ // --- Passkey Methods ---
5304
+ /**
5305
+ * Sign in with a passkey. Handles the full WebAuthn ceremony:
5306
+ * 1. Fetches authentication challenge from server
5307
+ * 2. Prompts user via navigator.credentials.get()
5308
+ * 3. Verifies credential with server and creates session
5309
+ *
5310
+ * Requires `auth.experimental.passkey: true`.
5311
+ *
5312
+ * @category Auth
5313
+ */
5314
+ async signInWithPasskey(credentials) {
5315
+ var _a, _b, _c;
5316
+ (0, helpers_1.assertPasskeyExperimentalEnabled)(this.experimental);
5317
+ try {
5318
+ if (!(0, webauthn_1.browserSupportsWebAuthn)()) {
5319
+ return this._returnResult({
5320
+ data: null,
5321
+ error: new errors_1.AuthUnknownError('Browser does not support WebAuthn', null),
5322
+ });
5323
+ }
5324
+ // 1. Get challenge options from server
5325
+ const { data: options, error: optionsError } = await this._startPasskeyAuthentication({
5326
+ options: { captchaToken: (_a = credentials === null || credentials === void 0 ? void 0 : credentials.options) === null || _a === void 0 ? void 0 : _a.captchaToken },
5327
+ });
5328
+ if (optionsError || !options) {
5329
+ return this._returnResult({ data: null, error: optionsError });
5330
+ }
5331
+ // 2. Deserialize and prompt user via browser WebAuthn API
5332
+ const publicKeyOptions = (0, webauthn_1.deserializeCredentialRequestOptions)(options.options);
5333
+ 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();
5334
+ const { data: credential, error: credentialError } = await (0, webauthn_1.getCredential)({
5335
+ publicKey: publicKeyOptions,
5336
+ signal,
5337
+ });
5338
+ if (credentialError || !credential) {
5339
+ return this._returnResult({
5340
+ data: null,
5341
+ error: credentialError !== null && credentialError !== void 0 ? credentialError : new errors_1.AuthUnknownError('WebAuthn ceremony failed', null),
5342
+ });
5343
+ }
5344
+ // 3. Serialize and verify with server
5345
+ const serialized = (0, webauthn_1.serializeCredentialRequestResponse)(credential);
5346
+ return this._verifyPasskeyAuthentication({
5347
+ challengeId: options.challenge_id,
5348
+ credential: serialized,
5349
+ });
5350
+ }
5351
+ catch (error) {
5352
+ if ((0, errors_1.isAuthError)(error)) {
5353
+ return this._returnResult({ data: null, error });
5354
+ }
5355
+ throw error;
5356
+ }
5357
+ }
5358
+ /**
5359
+ * Register a passkey for the current authenticated user. Handles the full WebAuthn ceremony:
5360
+ * 1. Fetches registration challenge from server
5361
+ * 2. Prompts user via navigator.credentials.create()
5362
+ * 3. Verifies credential with server
5363
+ *
5364
+ * Requires an active session. Requires `auth.experimental.passkey: true`.
5365
+ *
5366
+ * @category Auth
5367
+ */
5368
+ async registerPasskey(credentials) {
5369
+ var _a, _b;
5370
+ (0, helpers_1.assertPasskeyExperimentalEnabled)(this.experimental);
5371
+ try {
5372
+ if (!(0, webauthn_1.browserSupportsWebAuthn)()) {
5373
+ return this._returnResult({
5374
+ data: null,
5375
+ error: new errors_1.AuthUnknownError('Browser does not support WebAuthn', null),
5376
+ });
5377
+ }
5378
+ // 1. Get challenge options from server
5379
+ const { data: options, error: optionsError } = await this._startPasskeyRegistration();
5380
+ if (optionsError || !options) {
5381
+ return this._returnResult({ data: null, error: optionsError });
5382
+ }
5383
+ // 2. Deserialize and prompt user via browser WebAuthn API
5384
+ const publicKeyOptions = (0, webauthn_1.deserializeCredentialCreationOptions)(options.options);
5385
+ 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();
5386
+ const { data: credential, error: credentialError } = await (0, webauthn_1.createCredential)({
5387
+ publicKey: publicKeyOptions,
5388
+ signal,
5389
+ });
5390
+ if (credentialError || !credential) {
5391
+ return this._returnResult({
5392
+ data: null,
5393
+ error: credentialError !== null && credentialError !== void 0 ? credentialError : new errors_1.AuthUnknownError('WebAuthn ceremony failed', null),
5394
+ });
5395
+ }
5396
+ // 3. Serialize and verify with server
5397
+ const serialized = (0, webauthn_1.serializeCredentialCreationResponse)(credential);
5398
+ return this._verifyPasskeyRegistration({
5399
+ challengeId: options.challenge_id,
5400
+ credential: serialized,
5401
+ });
5402
+ }
5403
+ catch (error) {
5404
+ if ((0, errors_1.isAuthError)(error)) {
5405
+ return this._returnResult({ data: null, error });
5406
+ }
5407
+ throw error;
5408
+ }
5409
+ }
5410
+ /**
5411
+ * Start passkey registration for the current authenticated user.
5412
+ * Returns WebAuthn credential creation options to pass to navigator.credentials.create().
5413
+ */
5414
+ async _startPasskeyRegistration() {
5415
+ (0, helpers_1.assertPasskeyExperimentalEnabled)(this.experimental);
5416
+ try {
5417
+ return await this._useSession(async (result) => {
5418
+ const { data: { session }, error: sessionError, } = result;
5419
+ if (sessionError) {
5420
+ return this._returnResult({ data: null, error: sessionError });
5421
+ }
5422
+ if (!session) {
5423
+ return this._returnResult({ data: null, error: new errors_1.AuthSessionMissingError() });
5424
+ }
5425
+ const { data, error } = await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/passkeys/registration/options`, {
5426
+ headers: this.headers,
5427
+ jwt: session.access_token,
5428
+ body: {},
5429
+ });
5430
+ if (error) {
5431
+ return this._returnResult({ data: null, error });
5432
+ }
5433
+ return this._returnResult({ data, error: null });
5434
+ });
5435
+ }
5436
+ catch (error) {
5437
+ if ((0, errors_1.isAuthError)(error)) {
5438
+ return this._returnResult({ data: null, error });
5439
+ }
5440
+ throw error;
5441
+ }
5442
+ }
5443
+ /**
5444
+ * Verify passkey registration with the credential response.
5445
+ * The credentialResponse should be the serialized output of navigator.credentials.create().
5446
+ */
5447
+ async _verifyPasskeyRegistration(params) {
5448
+ (0, helpers_1.assertPasskeyExperimentalEnabled)(this.experimental);
5449
+ try {
5450
+ return await this._useSession(async (result) => {
5451
+ const { data: { session }, error: sessionError, } = result;
5452
+ if (sessionError) {
5453
+ return this._returnResult({ data: null, error: sessionError });
5454
+ }
5455
+ if (!session) {
5456
+ return this._returnResult({ data: null, error: new errors_1.AuthSessionMissingError() });
5457
+ }
5458
+ const { data, error } = await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/passkeys/registration/verify`, {
5459
+ headers: this.headers,
5460
+ jwt: session.access_token,
5461
+ body: {
5462
+ challenge_id: params.challengeId,
5463
+ credential: params.credential,
5464
+ },
5465
+ });
5466
+ if (error) {
5467
+ return this._returnResult({ data: null, error });
5468
+ }
5469
+ return this._returnResult({ data, error: null });
5470
+ });
5471
+ }
5472
+ catch (error) {
5473
+ if ((0, errors_1.isAuthError)(error)) {
5474
+ return this._returnResult({ data: null, error });
5475
+ }
5476
+ throw error;
5477
+ }
5478
+ }
5479
+ /**
5480
+ * Start passkey authentication.
5481
+ * Returns WebAuthn credential request options to pass to navigator.credentials.get().
5482
+ */
5483
+ async _startPasskeyAuthentication(params) {
5484
+ var _a;
5485
+ (0, helpers_1.assertPasskeyExperimentalEnabled)(this.experimental);
5486
+ try {
5487
+ const { data, error } = await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/passkeys/authentication/options`, {
5488
+ headers: this.headers,
5489
+ body: {
5490
+ gotrue_meta_security: { captcha_token: (_a = params === null || params === void 0 ? void 0 : params.options) === null || _a === void 0 ? void 0 : _a.captchaToken },
5491
+ },
5492
+ });
5493
+ if (error) {
5494
+ return this._returnResult({ data: null, error });
5495
+ }
5496
+ return this._returnResult({ data, error: null });
5497
+ }
5498
+ catch (error) {
5499
+ if ((0, errors_1.isAuthError)(error)) {
5500
+ return this._returnResult({ data: null, error });
5501
+ }
5502
+ throw error;
5503
+ }
5504
+ }
5505
+ /**
5506
+ * Verify passkey authentication and create a session.
5507
+ * The credential should be the serialized output of navigator.credentials.get().
5508
+ */
5509
+ async _verifyPasskeyAuthentication(params) {
5510
+ (0, helpers_1.assertPasskeyExperimentalEnabled)(this.experimental);
5511
+ try {
5512
+ const { data, error } = await (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/passkeys/authentication/verify`, {
5513
+ headers: this.headers,
5514
+ body: {
5515
+ challenge_id: params.challengeId,
5516
+ credential: params.credential,
5517
+ },
5518
+ xform: fetch_1._sessionResponse,
5519
+ });
5520
+ if (error) {
5521
+ return this._returnResult({ data: null, error });
5522
+ }
5523
+ if (data.session) {
5524
+ await this._saveSession(data.session);
5525
+ await this._notifyAllSubscribers('SIGNED_IN', data.session);
5526
+ }
5527
+ return this._returnResult({ data, error: null });
5528
+ }
5529
+ catch (error) {
5530
+ if ((0, errors_1.isAuthError)(error)) {
5531
+ return this._returnResult({ data: null, error });
5532
+ }
5533
+ throw error;
5534
+ }
5535
+ }
5536
+ /**
5537
+ * List all passkeys for the current user.
5538
+ */
5539
+ async _listPasskeys() {
5540
+ (0, helpers_1.assertPasskeyExperimentalEnabled)(this.experimental);
5541
+ try {
5542
+ return await this._useSession(async (result) => {
5543
+ const { data: { session }, error: sessionError, } = result;
5544
+ if (sessionError) {
5545
+ return this._returnResult({ data: null, error: sessionError });
5546
+ }
5547
+ if (!session) {
5548
+ return this._returnResult({ data: null, error: new errors_1.AuthSessionMissingError() });
5549
+ }
5550
+ const { data, error } = await (0, fetch_1._request)(this.fetch, 'GET', `${this.url}/passkeys`, {
5551
+ headers: this.headers,
5552
+ jwt: session.access_token,
5553
+ xform: (data) => ({ data, error: null }),
5554
+ });
5555
+ if (error) {
5556
+ return this._returnResult({ data: null, error });
5557
+ }
5558
+ return this._returnResult({ data, error: null });
5559
+ });
5560
+ }
5561
+ catch (error) {
5562
+ if ((0, errors_1.isAuthError)(error)) {
5563
+ return this._returnResult({ data: null, error });
5564
+ }
5565
+ throw error;
5566
+ }
5567
+ }
5568
+ /**
5569
+ * Update a passkey.
5570
+ */
5571
+ async _updatePasskey(params) {
5572
+ (0, helpers_1.assertPasskeyExperimentalEnabled)(this.experimental);
5573
+ try {
5574
+ return await this._useSession(async (result) => {
5575
+ const { data: { session }, error: sessionError, } = result;
5576
+ if (sessionError) {
5577
+ return this._returnResult({ data: null, error: sessionError });
5578
+ }
5579
+ if (!session) {
5580
+ return this._returnResult({ data: null, error: new errors_1.AuthSessionMissingError() });
5581
+ }
5582
+ const { data, error } = await (0, fetch_1._request)(this.fetch, 'PATCH', `${this.url}/passkeys/${params.passkeyId}`, {
5583
+ headers: this.headers,
5584
+ jwt: session.access_token,
5585
+ body: { friendly_name: params.friendlyName },
5586
+ });
5587
+ if (error) {
5588
+ return this._returnResult({ data: null, error });
5589
+ }
5590
+ return this._returnResult({ data, error: null });
5591
+ });
5592
+ }
5593
+ catch (error) {
5594
+ if ((0, errors_1.isAuthError)(error)) {
5595
+ return this._returnResult({ data: null, error });
5596
+ }
5597
+ throw error;
5598
+ }
5599
+ }
5600
+ /**
5601
+ * Delete a passkey.
5602
+ */
5603
+ async _deletePasskey(params) {
5604
+ (0, helpers_1.assertPasskeyExperimentalEnabled)(this.experimental);
5605
+ try {
5606
+ return await this._useSession(async (result) => {
5607
+ const { data: { session }, error: sessionError, } = result;
5608
+ if (sessionError) {
5609
+ return this._returnResult({ data: null, error: sessionError });
5610
+ }
5611
+ if (!session) {
5612
+ return this._returnResult({ data: null, error: new errors_1.AuthSessionMissingError() });
5613
+ }
5614
+ const { error } = await (0, fetch_1._request)(this.fetch, 'DELETE', `${this.url}/passkeys/${params.passkeyId}`, {
5615
+ headers: this.headers,
5616
+ jwt: session.access_token,
5617
+ noResolveJson: true,
5618
+ });
5619
+ if (error) {
5620
+ return this._returnResult({ data: null, error });
5621
+ }
5622
+ return this._returnResult({ data: null, error: null });
5623
+ });
5624
+ }
5625
+ catch (error) {
5626
+ if ((0, errors_1.isAuthError)(error)) {
5627
+ return this._returnResult({ data: null, error });
5628
+ }
5629
+ throw error;
5630
+ }
5631
+ }
4850
5632
  }
4851
5633
  GoTrueClient.nextInstanceID = {};
4852
5634
  exports.default = GoTrueClient;