@supabase/auth-js 2.107.0-beta.1 → 2.107.0-canary.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 (47) hide show
  1. package/dist/main/GoTrueClient.d.ts +14 -68
  2. package/dist/main/GoTrueClient.d.ts.map +1 -1
  3. package/dist/main/GoTrueClient.js +116 -334
  4. package/dist/main/GoTrueClient.js.map +1 -1
  5. package/dist/main/lib/errors.d.ts +0 -24
  6. package/dist/main/lib/errors.d.ts.map +1 -1
  7. package/dist/main/lib/errors.js +1 -31
  8. package/dist/main/lib/errors.js.map +1 -1
  9. package/dist/main/lib/locks.d.ts +34 -28
  10. package/dist/main/lib/locks.d.ts.map +1 -1
  11. package/dist/main/lib/locks.js +34 -28
  12. package/dist/main/lib/locks.js.map +1 -1
  13. package/dist/main/lib/types.d.ts +27 -16
  14. package/dist/main/lib/types.d.ts.map +1 -1
  15. package/dist/main/lib/types.js.map +1 -1
  16. package/dist/main/lib/version.d.ts +1 -1
  17. package/dist/main/lib/version.d.ts.map +1 -1
  18. package/dist/main/lib/version.js +1 -1
  19. package/dist/main/lib/version.js.map +1 -1
  20. package/dist/module/GoTrueClient.d.ts +14 -68
  21. package/dist/module/GoTrueClient.d.ts.map +1 -1
  22. package/dist/module/GoTrueClient.js +118 -336
  23. package/dist/module/GoTrueClient.js.map +1 -1
  24. package/dist/module/lib/errors.d.ts +0 -24
  25. package/dist/module/lib/errors.d.ts.map +1 -1
  26. package/dist/module/lib/errors.js +0 -28
  27. package/dist/module/lib/errors.js.map +1 -1
  28. package/dist/module/lib/locks.d.ts +34 -28
  29. package/dist/module/lib/locks.d.ts.map +1 -1
  30. package/dist/module/lib/locks.js +34 -28
  31. package/dist/module/lib/locks.js.map +1 -1
  32. package/dist/module/lib/types.d.ts +27 -16
  33. package/dist/module/lib/types.d.ts.map +1 -1
  34. package/dist/module/lib/types.js.map +1 -1
  35. package/dist/module/lib/version.d.ts +1 -1
  36. package/dist/module/lib/version.d.ts.map +1 -1
  37. package/dist/module/lib/version.js +1 -1
  38. package/dist/module/lib/version.js.map +1 -1
  39. package/dist/tsconfig.module.tsbuildinfo +1 -1
  40. package/dist/tsconfig.tsbuildinfo +1 -1
  41. package/package.json +1 -1
  42. package/src/GoTrueClient.ts +147 -400
  43. package/src/lib/errors.ts +0 -32
  44. package/src/lib/locks.ts +34 -29
  45. package/src/lib/types.ts +27 -16
  46. package/src/lib/version.ts +1 -1
  47. package/migrations/lockless-coordination.md +0 -89
@@ -1,10 +1,10 @@
1
1
  import GoTrueAdminApi from './GoTrueAdminApi';
2
2
  import { AUTO_REFRESH_TICK_DURATION_MS, AUTO_REFRESH_TICK_THRESHOLD, DEFAULT_HEADERS, EXPIRY_MARGIN_MS, GOTRUE_URL, JWKS_TTL, STORAGE_KEY, } from './lib/constants';
3
- import { AuthImplicitGrantRedirectError, AuthInvalidCredentialsError, AuthInvalidJwtError, AuthInvalidTokenResponseError, AuthPKCECodeVerifierMissingError, AuthPKCEGrantCodeExchangeError, AuthRefreshDiscardedError, AuthSessionMissingError, AuthUnknownError, isAuthApiError, isAuthError, isAuthImplicitGrantRedirectError, isAuthRefreshDiscardedError, isAuthRetryableFetchError, isAuthSessionMissingError, } from './lib/errors';
3
+ import { AuthImplicitGrantRedirectError, AuthInvalidCredentialsError, AuthInvalidJwtError, AuthInvalidTokenResponseError, AuthPKCECodeVerifierMissingError, AuthPKCEGrantCodeExchangeError, AuthSessionMissingError, AuthUnknownError, isAuthApiError, isAuthError, isAuthImplicitGrantRedirectError, isAuthRetryableFetchError, isAuthSessionMissingError, } from './lib/errors';
4
4
  import { _request, _sessionResponse, _sessionResponsePassword, _ssoResponse, _userResponse, } from './lib/fetch';
5
5
  import { assertPasskeyExperimentalEnabled, decodeJWT, deepClone, Deferred, generateCallbackId, getAlgorithm, getCodeChallengeAndMethod, getItemAsync, insecureUserWarningProxy, isBrowser, parseParametersFromURL, removeItemAsync, resolveFetch, retryable, setItemAsync, sleep, supportsLocalStorage, userNotAvailableProxy, validateExp, } from './lib/helpers';
6
6
  import { memoryLocalStorageAdapter } from './lib/local-storage';
7
- import { LockAcquireTimeoutError } from './lib/locks';
7
+ import { LockAcquireTimeoutError, navigatorLock } from './lib/locks';
8
8
  import { polyfillGlobalThis } from './lib/polyfills';
9
9
  import { version } from './lib/version';
10
10
  import { bytesToBase64URL, stringToUint8Array } from './lib/base64url';
@@ -22,16 +22,10 @@ const DEFAULT_OPTIONS = {
22
22
  debug: false,
23
23
  hasCustomAuthorizationHeader: false,
24
24
  throwOnError: false,
25
- lockAcquireTimeout: 5000, // 5 seconds. Only used when a custom `lock` is supplied. TODO(v3): remove.
25
+ lockAcquireTimeout: 5000, // 5 seconds
26
26
  skipAutoInitialize: false,
27
27
  experimental: {},
28
28
  };
29
- /**
30
- * No-op lock used internally as a placeholder. Kept so older test setups that
31
- * inject this exact reference do not break; new code never sees it because
32
- * `this.lock` stays `null` when no custom lock is supplied (lockless path).
33
- * TODO(v3): remove with the legacy lock path.
34
- */
35
29
  async function lockNoOp(name, acquireTimeout, fn) {
36
30
  return await fn();
37
31
  }
@@ -85,7 +79,7 @@ class GoTrueClient {
85
79
  * ```
86
80
  */
87
81
  constructor(options) {
88
- var _a, _b, _c;
82
+ var _a, _b, _c, _d;
89
83
  /**
90
84
  * @experimental
91
85
  */
@@ -96,14 +90,6 @@ class GoTrueClient {
96
90
  this.autoRefreshTickTimeout = null;
97
91
  this.visibilityChangedCallback = null;
98
92
  this.refreshingDeferred = null;
99
- /**
100
- * Monotonic counter incremented at the top of `_removeSession`, before any
101
- * `await`. The commit guard inside `_callRefreshToken` captures this value
102
- * before `_saveSession` and re-checks it after, so a `signOut` that
103
- * interleaves inside `_saveSession`'s storage-write awaits is still caught
104
- * (the post-fetch storage snapshot alone misses that window).
105
- */
106
- this._sessionRemovalEpoch = 0;
107
93
  /**
108
94
  * Keeps track of the async client initialization.
109
95
  * When null or not yet resolved the auth state is `unknown`
@@ -114,13 +100,6 @@ class GoTrueClient {
114
100
  this.detectSessionInUrl = true;
115
101
  this.hasCustomAuthorizationHeader = false;
116
102
  this.suppressGetSessionWarning = false;
117
- /**
118
- * Custom lock function passed via `settings.lock`. When non-null, every auth
119
- * operation runs inside `_acquireLock`. When null (the default), the client
120
- * uses its lockless coordination (refresh single-flight + commit guard).
121
- * TODO(v3): remove along with the legacy lock path.
122
- */
123
- this.lock = null;
124
103
  this.lockAcquired = false;
125
104
  this.pendingInLock = [];
126
105
  /**
@@ -155,22 +134,21 @@ class GoTrueClient {
155
134
  this.url = settings.url;
156
135
  this.headers = settings.headers;
157
136
  this.fetch = resolveFetch(settings.fetch);
137
+ this.lock = settings.lock || lockNoOp;
158
138
  this.detectSessionInUrl = settings.detectSessionInUrl;
159
139
  this.flowType = settings.flowType;
160
140
  this.hasCustomAuthorizationHeader = settings.hasCustomAuthorizationHeader;
161
141
  this.throwOnError = settings.throwOnError;
162
- // Always wire `lockAcquireTimeout` even on the lockless path: consumers
163
- // (including supabase-js tests) read it off the client to verify option
164
- // flow-through.
165
142
  this.lockAcquireTimeout = settings.lockAcquireTimeout;
166
- // TODO(v3): remove. Legacy opt-in path preserved for backwards
167
- // compatibility with callers passing a custom `lock` (typically React
168
- // Native `processLock` or Node multi-process setups). When `settings.lock`
169
- // is null the client uses its lockless coordination — no `navigator.locks`
170
- // by default, no implicit `processLock`.
171
- if (settings.lock != null) {
143
+ if (settings.lock) {
172
144
  this.lock = settings.lock;
173
145
  }
146
+ else if (this.persistSession && isBrowser() && ((_c = globalThis === null || globalThis === void 0 ? void 0 : globalThis.navigator) === null || _c === void 0 ? void 0 : _c.locks)) {
147
+ this.lock = navigatorLock;
148
+ }
149
+ else {
150
+ this.lock = lockNoOp;
151
+ }
174
152
  if (!this.jwks) {
175
153
  this.jwks = { keys: [] };
176
154
  this.jwks_cached_at = Number.MIN_SAFE_INTEGER;
@@ -229,7 +207,7 @@ class GoTrueClient {
229
207
  catch (e) {
230
208
  console.error('Failed to create a new BroadcastChannel, multi-tab state changes will not be available', e);
231
209
  }
232
- (_c = this.broadcastChannel) === null || _c === void 0 ? void 0 : _c.addEventListener('message', async (event) => {
210
+ (_d = this.broadcastChannel) === null || _d === void 0 ? void 0 : _d.addEventListener('message', async (event) => {
233
211
  this._debug('received broadcast notification from other tab or client', event);
234
212
  try {
235
213
  await this._notifyAllSubscribers(event.data.event, event.data.session, false); // broadcast = false so we don't get an endless loop of messages
@@ -287,13 +265,9 @@ class GoTrueClient {
287
265
  return await this.initializePromise;
288
266
  }
289
267
  this.initializePromise = (async () => {
290
- if (this.lock != null) {
291
- // TODO(v3): remove legacy lock path
292
- return await this._acquireLock(this.lockAcquireTimeout, async () => {
293
- return await this._initialize();
294
- });
295
- }
296
- return await this._initialize();
268
+ return await this._acquireLock(this.lockAcquireTimeout, async () => {
269
+ return await this._initialize();
270
+ });
297
271
  })();
298
272
  return await this.initializePromise;
299
273
  }
@@ -1144,13 +1118,9 @@ class GoTrueClient {
1144
1118
  */
1145
1119
  async exchangeCodeForSession(authCode) {
1146
1120
  await this.initializePromise;
1147
- if (this.lock != null) {
1148
- // TODO(v3): remove legacy lock path
1149
- return this._acquireLock(this.lockAcquireTimeout, async () => {
1150
- return this._exchangeCodeForSession(authCode);
1151
- });
1152
- }
1153
- return this._exchangeCodeForSession(authCode);
1121
+ return this._acquireLock(this.lockAcquireTimeout, async () => {
1122
+ return this._exchangeCodeForSession(authCode);
1123
+ });
1154
1124
  }
1155
1125
  /**
1156
1126
  * Signs in a user by verifying a message signed by the user's private key.
@@ -2054,13 +2024,9 @@ class GoTrueClient {
2054
2024
  */
2055
2025
  async reauthenticate() {
2056
2026
  await this.initializePromise;
2057
- if (this.lock != null) {
2058
- // TODO(v3): remove legacy lock path
2059
- return await this._acquireLock(this.lockAcquireTimeout, async () => {
2060
- return await this._reauthenticate();
2061
- });
2062
- }
2063
- return await this._reauthenticate();
2027
+ return await this._acquireLock(this.lockAcquireTimeout, async () => {
2028
+ return await this._reauthenticate();
2029
+ });
2064
2030
  }
2065
2031
  async _reauthenticate() {
2066
2032
  try {
@@ -2203,7 +2169,7 @@ class GoTrueClient {
2203
2169
  * - If the session's access token is expired or is about to expire, this method will use the refresh token to refresh the session.
2204
2170
  * - 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.
2205
2171
  * - **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.
2206
- * - 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.
2172
+ * - 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.
2207
2173
  *
2208
2174
  * @example Get the session data
2209
2175
  * ```js
@@ -2270,24 +2236,15 @@ class GoTrueClient {
2270
2236
  */
2271
2237
  async getSession() {
2272
2238
  await this.initializePromise;
2273
- if (this.lock != null) {
2274
- // TODO(v3): remove legacy lock path
2275
- return await this._acquireLock(this.lockAcquireTimeout, async () => {
2276
- return this._useSession(async (result) => {
2277
- return result;
2278
- });
2239
+ const result = await this._acquireLock(this.lockAcquireTimeout, async () => {
2240
+ return this._useSession(async (result) => {
2241
+ return result;
2279
2242
  });
2280
- }
2281
- return await this._useSession(async (result) => {
2282
- return result;
2283
2243
  });
2244
+ return result;
2284
2245
  }
2285
2246
  /**
2286
2247
  * Acquires a global lock based on the storage key.
2287
- *
2288
- * TODO(v3): remove along with the legacy lock path. Only called when
2289
- * `this.lock` is non-null (custom lock supplied via constructor). The
2290
- * default lockless path bypasses this entirely.
2291
2248
  */
2292
2249
  async _acquireLock(acquireTimeout, fn) {
2293
2250
  this._debug('#_acquireLock', 'begin', acquireTimeout);
@@ -2343,17 +2300,15 @@ class GoTrueClient {
2343
2300
  }
2344
2301
  }
2345
2302
  /**
2346
- * Use instead of {@link #getSession} inside the library. Loads the session
2347
- * via `__loadSession` (which may trigger a refresh if the access token is
2348
- * within the expiry margin) and runs `fn` with the result.
2303
+ * Use instead of {@link #getSession} inside the library. It is
2304
+ * semantically usually what you want, as getting a session involves some
2305
+ * processing afterwards that requires only one client operating on the
2306
+ * session at once across multiple tabs or processes.
2349
2307
  */
2350
2308
  async _useSession(fn) {
2351
2309
  this._debug('#_useSession', 'begin');
2352
2310
  try {
2353
- // Concurrent callers may both reach __loadSession; storage reads are
2354
- // idempotent, and the only write path inside it (refresh) is
2355
- // single-flighted downstream by `refreshingDeferred` in
2356
- // `_callRefreshToken`. No serialization is needed at this layer.
2311
+ // the use of __loadSession here is the only correct use of the function!
2357
2312
  const result = await this.__loadSession();
2358
2313
  return await fn(result);
2359
2314
  }
@@ -2368,8 +2323,7 @@ class GoTrueClient {
2368
2323
  */
2369
2324
  async __loadSession() {
2370
2325
  this._debug('#__loadSession()', 'begin');
2371
- if (this.lock != null && !this.lockAcquired) {
2372
- // TODO(v3): remove. Only meaningful on the legacy lock path.
2326
+ if (!this.lockAcquired) {
2373
2327
  this._debug('#__loadSession()', 'used outside of an acquired lock!', new Error().stack);
2374
2328
  }
2375
2329
  try {
@@ -2512,16 +2466,9 @@ class GoTrueClient {
2512
2466
  return await this._getUser(jwt);
2513
2467
  }
2514
2468
  await this.initializePromise;
2515
- let result;
2516
- if (this.lock != null) {
2517
- // TODO(v3): remove legacy lock path
2518
- result = await this._acquireLock(this.lockAcquireTimeout, async () => {
2519
- return await this._getUser();
2520
- });
2521
- }
2522
- else {
2523
- result = await this._getUser();
2524
- }
2469
+ const result = await this._acquireLock(this.lockAcquireTimeout, async () => {
2470
+ return await this._getUser();
2471
+ });
2525
2472
  if (result.data.user) {
2526
2473
  this.suppressGetSessionWarning = true;
2527
2474
  }
@@ -2682,13 +2629,9 @@ class GoTrueClient {
2682
2629
  */
2683
2630
  async updateUser(attributes, options = {}) {
2684
2631
  await this.initializePromise;
2685
- if (this.lock != null) {
2686
- // TODO(v3): remove legacy lock path
2687
- return await this._acquireLock(this.lockAcquireTimeout, async () => {
2688
- return await this._updateUser(attributes, options);
2689
- });
2690
- }
2691
- return await this._updateUser(attributes, options);
2632
+ return await this._acquireLock(this.lockAcquireTimeout, async () => {
2633
+ return await this._updateUser(attributes, options);
2634
+ });
2692
2635
  }
2693
2636
  async _updateUser(attributes, options = {}) {
2694
2637
  try {
@@ -2857,13 +2800,9 @@ class GoTrueClient {
2857
2800
  */
2858
2801
  async setSession(currentSession) {
2859
2802
  await this.initializePromise;
2860
- if (this.lock != null) {
2861
- // TODO(v3): remove legacy lock path
2862
- return await this._acquireLock(this.lockAcquireTimeout, async () => {
2863
- return await this._setSession(currentSession);
2864
- });
2865
- }
2866
- return await this._setSession(currentSession);
2803
+ return await this._acquireLock(this.lockAcquireTimeout, async () => {
2804
+ return await this._setSession(currentSession);
2805
+ });
2867
2806
  }
2868
2807
  async _setSession(currentSession) {
2869
2808
  try {
@@ -3041,13 +2980,9 @@ class GoTrueClient {
3041
2980
  */
3042
2981
  async refreshSession(currentSession) {
3043
2982
  await this.initializePromise;
3044
- if (this.lock != null) {
3045
- // TODO(v3): remove legacy lock path
3046
- return await this._acquireLock(this.lockAcquireTimeout, async () => {
3047
- return await this._refreshSession(currentSession);
3048
- });
3049
- }
3050
- return await this._refreshSession(currentSession);
2983
+ return await this._acquireLock(this.lockAcquireTimeout, async () => {
2984
+ return await this._refreshSession(currentSession);
2985
+ });
3051
2986
  }
3052
2987
  async _refreshSession(currentSession) {
3053
2988
  try {
@@ -3185,7 +3120,7 @@ class GoTrueClient {
3185
3120
  if (typeof this.detectSessionInUrl === 'function') {
3186
3121
  return this.detectSessionInUrl(new URL(window.location.href), params);
3187
3122
  }
3188
- return Boolean(params.access_token || params.error_description);
3123
+ return Boolean(params.access_token || params.error || params.error_description || params.error_code);
3189
3124
  }
3190
3125
  /**
3191
3126
  * Checks if the current URL and backing storage contain parameters given by a PKCE flow
@@ -3237,13 +3172,9 @@ class GoTrueClient {
3237
3172
  */
3238
3173
  async signOut(options = { scope: 'global' }) {
3239
3174
  await this.initializePromise;
3240
- if (this.lock != null) {
3241
- // TODO(v3): remove legacy lock path
3242
- return await this._acquireLock(this.lockAcquireTimeout, async () => {
3243
- return await this._signOut(options);
3244
- });
3245
- }
3246
- return await this._signOut(options);
3175
+ return await this._acquireLock(this.lockAcquireTimeout, async () => {
3176
+ return await this._signOut(options);
3177
+ });
3247
3178
  }
3248
3179
  async _signOut({ scope } = { scope: 'global' }) {
3249
3180
  return await this._useSession(async (result) => {
@@ -3279,8 +3210,18 @@ class GoTrueClient {
3279
3210
  * - Subscribes to important events occurring on the user's session.
3280
3211
  * - Use on the frontend/client. It is less useful on the server.
3281
3212
  * - 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.
3282
- * - Callbacks can be `async` and can safely call other Supabase auth methods (`getUser`, `setSession`, etc.) from inside the callback.
3283
- * - Keep callbacks quick. Events are awaited in order, so a slow callback delays subsequent events to subscribers in this tab.
3213
+ * - **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.
3214
+ * - Avoid using `async` functions as callbacks.
3215
+ * - Limit the number of `await` calls in `async` callbacks.
3216
+ * - 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:
3217
+ * ```js
3218
+ * supabase.auth.onAuthStateChange((event, session) => {
3219
+ * setTimeout(async () => {
3220
+ * // await on other Supabase function here
3221
+ * // this runs right after the callback has finished
3222
+ * }, 0)
3223
+ * })
3224
+ * ```
3284
3225
  * - Emitted events:
3285
3226
  * - `INITIAL_SESSION`
3286
3227
  * - Emitted right after the Supabase client is constructed and the initial session from storage is loaded.
@@ -3463,15 +3404,9 @@ class GoTrueClient {
3463
3404
  this.stateChangeEmitters.set(id, subscription);
3464
3405
  (async () => {
3465
3406
  await this.initializePromise;
3466
- if (this.lock != null) {
3467
- // TODO(v3): remove legacy lock path
3468
- await this._acquireLock(this.lockAcquireTimeout, async () => {
3469
- this._emitInitialSession(id);
3470
- });
3471
- }
3472
- else {
3473
- await this._emitInitialSession(id);
3474
- }
3407
+ await this._acquireLock(this.lockAcquireTimeout, async () => {
3408
+ this._emitInitialSession(id);
3409
+ });
3475
3410
  })();
3476
3411
  return { data: { subscription } };
3477
3412
  }
@@ -3813,10 +3748,7 @@ class GoTrueClient {
3813
3748
  * @param refreshToken A valid refresh token that was returned on login.
3814
3749
  */
3815
3750
  async _refreshAccessToken(refreshToken) {
3816
- // Refresh tokens are long-lived bearer credentials; do NOT include any
3817
- // fragment of the token in the debug tag, even when `debug: true` is
3818
- // enabled (logs may be forwarded to third-party services).
3819
- const debugName = `#_refreshAccessToken()`;
3751
+ const debugName = `#_refreshAccessToken(${refreshToken.substring(0, 5)}...)`;
3820
3752
  this._debug(debugName, 'begin');
3821
3753
  try {
3822
3754
  const startedAt = Date.now();
@@ -3923,18 +3855,10 @@ class GoTrueClient {
3923
3855
  if (this.autoRefreshToken && currentSession.refresh_token) {
3924
3856
  const { error } = await this._callRefreshToken(currentSession.refresh_token);
3925
3857
  if (error) {
3926
- // AuthRefreshDiscardedError means a concurrent signOut already
3927
- // cleared storage and fired SIGNED_OUT. Don't run _removeSession
3928
- // again here, or we'll emit a duplicate SIGNED_OUT.
3929
- if (isAuthRefreshDiscardedError(error)) {
3930
- this._debug(debugName, 'refresh discarded by commit guard', error);
3931
- }
3932
- else {
3933
- console.error(error);
3934
- if (!isAuthRetryableFetchError(error)) {
3935
- this._debug(debugName, 'refresh failed with a non-retryable error, removing the session', error);
3936
- await this._removeSession();
3937
- }
3858
+ console.error(error);
3859
+ if (!isAuthRetryableFetchError(error)) {
3860
+ this._debug(debugName, 'refresh failed with a non-retryable error, removing the session', error);
3861
+ await this._removeSession();
3938
3862
  }
3939
3863
  }
3940
3864
  }
@@ -3983,69 +3907,16 @@ class GoTrueClient {
3983
3907
  if (this.refreshingDeferred) {
3984
3908
  return this.refreshingDeferred.promise;
3985
3909
  }
3986
- // Refresh tokens are long-lived bearer credentials; do NOT include any
3987
- // fragment of the token in the debug tag, even when `debug: true` is
3988
- // enabled (logs may be forwarded to third-party services).
3989
- const debugName = `#_callRefreshToken()`;
3910
+ const debugName = `#_callRefreshToken(${refreshToken.substring(0, 5)}...)`;
3990
3911
  this._debug(debugName, 'begin');
3991
3912
  try {
3992
3913
  this.refreshingDeferred = new Deferred();
3993
- // Snapshot storage before the fetch. The commit guard discards the
3994
- // rotated tokens only when a non-null pre-fetch snapshot changed under
3995
- // us — typical case: a concurrent `signOut` ran `_removeSession`, or
3996
- // another tab's refresh rewrote the slot. Callers passing
3997
- // externally-sourced tokens (SSR cookie handoff, multi-account
3998
- // switching, `setSession`/`refreshSession({ refresh_token })`) may
3999
- // start from a null snapshot OR from a non-null snapshot whose
4000
- // refresh_token differs from the one they're hydrating; in both
4001
- // cases the guard fires only when storage was *modified between
4002
- // snapshots*, not when the input token disagrees with what's stored.
4003
- const storedAtStart = (await getItemAsync(this.storage, this.storageKey));
4004
3914
  const { data, error } = await this._refreshAccessToken(refreshToken);
4005
3915
  if (error)
4006
3916
  throw error;
4007
3917
  if (!data.session)
4008
3918
  throw new AuthSessionMissingError();
4009
- const storedAfter = (await getItemAsync(this.storage, this.storageKey));
4010
- const storageChangedUnderUs = storedAtStart !== null &&
4011
- (storedAfter === null || storedAfter.refresh_token !== storedAtStart.refresh_token);
4012
- if (storageChangedUnderUs) {
4013
- this._debug(debugName, 'commit guard: storage changed since refresh started, discarding rotated tokens', {
4014
- // Presence indicators only — never log refresh token fragments,
4015
- // even partial. Logs may be forwarded to third-party services.
4016
- startedWith: 'present',
4017
- nowHolds: storedAfter ? 'replaced' : 'cleared',
4018
- });
4019
- const discarded = {
4020
- data: null,
4021
- error: new AuthRefreshDiscardedError(),
4022
- };
4023
- this.refreshingDeferred.resolve(discarded);
4024
- return discarded;
4025
- }
4026
- // Second leg of the commit guard: close the TOCTOU window between the
4027
- // synchronous `storageChangedUnderUs` check and the actual storage
4028
- // writes inside `_saveSession`. A concurrent `signOut → _removeSession`
4029
- // can land inside `_saveSession`'s `await setItemAsync(...)` yields and
4030
- // clear storage just before we overwrite it. Capture the epoch BEFORE
4031
- // the save and re-check after; if it advanced, undo the write directly
4032
- // (do NOT call `_removeSession` — that would emit a duplicate
4033
- // SIGNED_OUT for the concurrent signOut that already fired one).
4034
- const epochBeforeSave = this._sessionRemovalEpoch;
4035
3919
  await this._saveSession(data.session);
4036
- if (this._sessionRemovalEpoch !== epochBeforeSave) {
4037
- this._debug(debugName, 'commit guard (post-save): _removeSession ran during _saveSession, undoing write');
4038
- await removeItemAsync(this.storage, this.storageKey);
4039
- if (this.userStorage) {
4040
- await removeItemAsync(this.userStorage, this.storageKey + '-user');
4041
- }
4042
- const discarded = {
4043
- data: null,
4044
- error: new AuthRefreshDiscardedError(),
4045
- };
4046
- this.refreshingDeferred.resolve(discarded);
4047
- return discarded;
4048
- }
4049
3920
  await this._notifyAllSubscribers('TOKEN_REFRESHED', data.session);
4050
3921
  const result = { data: data.session, error: null };
4051
3922
  this.refreshingDeferred.resolve(result);
@@ -4139,11 +4010,6 @@ class GoTrueClient {
4139
4010
  }
4140
4011
  }
4141
4012
  async _removeSession() {
4142
- // Bump synchronously, BEFORE any `await`, so that `_callRefreshToken`'s
4143
- // post-save check sees the increment whenever this method has started —
4144
- // even if it hasn't finished. Pairs with the epoch check in
4145
- // `_callRefreshToken`. See `_sessionRemovalEpoch` field doc.
4146
- this._sessionRemovalEpoch += 1;
4147
4013
  this._debug('#_removeSession()');
4148
4014
  this.suppressGetSessionWarning = false;
4149
4015
  await removeItemAsync(this.storage, this.storageKey);
@@ -4314,122 +4180,47 @@ class GoTrueClient {
4314
4180
  this._removeVisibilityChangedCallback();
4315
4181
  await this._stopAutoRefresh();
4316
4182
  }
4317
- /**
4318
- * Tears down the client's background work: stops the auto-refresh interval,
4319
- * removes the `visibilitychange` listener, closes the cross-tab
4320
- * `BroadcastChannel`, and clears registered `onAuthStateChange` subscribers.
4321
- *
4322
- * Call this from cleanup hooks when the client is being replaced before
4323
- * its JS realm is destroyed. React Strict Mode and HMR are the common
4324
- * cases. Any in-flight `fetch` calls continue to completion and may still
4325
- * write to storage; dispose doesn't abort them or erase storage.
4326
- *
4327
- * Lifecycle caveat: because in-flight refreshes are not aborted, a
4328
- * disposed instance can still persist a rotated session to storage after
4329
- * `dispose()` returns. A subsequent `createClient` against the same
4330
- * `storageKey` will pick up that session on its next read. If you need
4331
- * strict isolation between client lifecycles, await any pending auth
4332
- * operation before calling `dispose()` (or change the `storageKey` for
4333
- * the replacement client).
4334
- *
4335
- * Safe to call repeatedly.
4336
- *
4337
- * @category Auth
4338
- *
4339
- * @example Cleanup on React unmount
4340
- * ```ts
4341
- * useEffect(() => {
4342
- * const client = createClient(...)
4343
- * return () => { client.auth.dispose() }
4344
- * }, [])
4345
- * ```
4346
- */
4347
- async dispose() {
4348
- var _a;
4349
- this._removeVisibilityChangedCallback();
4350
- await this._stopAutoRefresh();
4351
- (_a = this.broadcastChannel) === null || _a === void 0 ? void 0 : _a.close();
4352
- this.broadcastChannel = null;
4353
- this.stateChangeEmitters.clear();
4354
- }
4355
4183
  /**
4356
4184
  * Runs the auto refresh token tick.
4357
4185
  */
4358
4186
  async _autoRefreshTokenTick() {
4359
4187
  this._debug('#_autoRefreshTokenTick()', 'begin');
4360
- if (this.lock != null) {
4361
- // TODO(v3): remove legacy lock path. Uses `_acquireLock(0, ...)` which
4362
- // throws `LockAcquireTimeoutError` immediately if the lock is held —
4363
- // that's the fail-fast skip path that lets the tick bail out instead
4364
- // of queuing behind a long-running operation.
4365
- try {
4366
- await this._acquireLock(0, async () => {
4188
+ try {
4189
+ await this._acquireLock(0, async () => {
4190
+ try {
4191
+ const now = Date.now();
4367
4192
  try {
4368
- const now = Date.now();
4369
- try {
4370
- return await this._useSession(async (result) => {
4371
- const { data: { session }, } = result;
4372
- if (!session || !session.refresh_token || !session.expires_at) {
4373
- this._debug('#_autoRefreshTokenTick()', 'no session');
4374
- return;
4375
- }
4376
- const expiresInTicks = Math.floor((session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS);
4377
- this._debug('#_autoRefreshTokenTick()', `access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks`);
4378
- if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) {
4379
- await this._callRefreshToken(session.refresh_token);
4380
- }
4381
- });
4382
- }
4383
- catch (e) {
4384
- console.error('Auto refresh tick failed with error. This is likely a transient error.', e);
4385
- }
4193
+ return await this._useSession(async (result) => {
4194
+ const { data: { session }, } = result;
4195
+ if (!session || !session.refresh_token || !session.expires_at) {
4196
+ this._debug('#_autoRefreshTokenTick()', 'no session');
4197
+ return;
4198
+ }
4199
+ // session will expire in this many ticks (or has already expired if <= 0)
4200
+ const expiresInTicks = Math.floor((session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS);
4201
+ this._debug('#_autoRefreshTokenTick()', `access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks`);
4202
+ if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) {
4203
+ await this._callRefreshToken(session.refresh_token);
4204
+ }
4205
+ });
4386
4206
  }
4387
- finally {
4388
- this._debug('#_autoRefreshTokenTick()', 'end');
4207
+ catch (e) {
4208
+ console.error('Auto refresh tick failed with error. This is likely a transient error.', e);
4389
4209
  }
4390
- });
4391
- }
4392
- catch (e) {
4393
- if (e instanceof LockAcquireTimeoutError) {
4394
- this._debug('auto refresh token tick lock not available');
4395
4210
  }
4396
- else {
4397
- throw e;
4211
+ finally {
4212
+ this._debug('#_autoRefreshTokenTick()', 'end');
4398
4213
  }
4399
- }
4400
- return;
4401
- }
4402
- // Lockless default: skip if a refresh is already in flight.
4403
- // `_callRefreshToken` also dedupes via the same field; this is just a
4404
- // fast-path skip to avoid an unnecessary storage read.
4405
- if (this.refreshingDeferred !== null) {
4406
- this._debug('#_autoRefreshTokenTick()', 'refresh already in flight, skipping');
4407
- return;
4214
+ });
4408
4215
  }
4409
- try {
4410
- const now = Date.now();
4411
- try {
4412
- await this._useSession(async (result) => {
4413
- const { data: { session }, } = result;
4414
- if (!session || !session.refresh_token || !session.expires_at) {
4415
- this._debug('#_autoRefreshTokenTick()', 'no session');
4416
- return;
4417
- }
4418
- // session will expire in this many ticks (or has already expired if <= 0)
4419
- const expiresInTicks = Math.floor((session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS);
4420
- this._debug('#_autoRefreshTokenTick()', `access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks`);
4421
- if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) {
4422
- await this._callRefreshToken(session.refresh_token);
4423
- }
4424
- });
4216
+ catch (e) {
4217
+ if (e instanceof LockAcquireTimeoutError) {
4218
+ this._debug('auto refresh token tick lock not available');
4425
4219
  }
4426
- catch (e) {
4427
- console.error('Auto refresh tick failed with error. This is likely a transient error.', e);
4220
+ else {
4221
+ throw e;
4428
4222
  }
4429
4223
  }
4430
- finally {
4431
- this._debug('#_autoRefreshTokenTick()', 'end');
4432
- }
4433
4224
  }
4434
4225
  /**
4435
4226
  * Registers callbacks on the browser / platform, which in-turn run
@@ -4478,26 +4269,18 @@ class GoTrueClient {
4478
4269
  if (!calledFromInitialize) {
4479
4270
  // called when the visibility has changed, i.e. the browser
4480
4271
  // transitioned from hidden -> visible so we need to see if the session
4481
- // should be recovered
4272
+ // should be recovered immediately... but to do that we need to acquire
4273
+ // the lock first asynchronously
4482
4274
  await this.initializePromise;
4483
- if (this.lock != null) {
4484
- // TODO(v3): remove legacy lock path
4485
- await this._acquireLock(this.lockAcquireTimeout, async () => {
4486
- if (document.visibilityState !== 'visible') {
4487
- this._debug(methodName, 'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting');
4488
- return;
4489
- }
4490
- await this._recoverAndRefresh();
4491
- });
4492
- }
4493
- else {
4275
+ await this._acquireLock(this.lockAcquireTimeout, async () => {
4494
4276
  if (document.visibilityState !== 'visible') {
4495
- this._debug(methodName, 'visibilityState is no longer visible, skipping recovery');
4277
+ this._debug(methodName, 'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting');
4278
+ // visibility has changed while waiting for the lock, abort
4496
4279
  return;
4497
4280
  }
4498
4281
  // recover the session
4499
4282
  await this._recoverAndRefresh();
4500
- }
4283
+ });
4501
4284
  }
4502
4285
  }
4503
4286
  else if (document.visibilityState === 'hidden') {
@@ -4593,7 +4376,7 @@ class GoTrueClient {
4593
4376
  }
4594
4377
  }
4595
4378
  async _verify(params) {
4596
- const run = async () => {
4379
+ return this._acquireLock(this.lockAcquireTimeout, async () => {
4597
4380
  try {
4598
4381
  return await this._useSession(async (result) => {
4599
4382
  var _a;
@@ -4627,15 +4410,10 @@ class GoTrueClient {
4627
4410
  }
4628
4411
  throw error;
4629
4412
  }
4630
- };
4631
- if (this.lock != null) {
4632
- // TODO(v3): remove legacy lock path
4633
- return this._acquireLock(this.lockAcquireTimeout, run);
4634
- }
4635
- return run();
4413
+ });
4636
4414
  }
4637
4415
  async _challenge(params) {
4638
- const run = async () => {
4416
+ return this._acquireLock(this.lockAcquireTimeout, async () => {
4639
4417
  try {
4640
4418
  return await this._useSession(async (result) => {
4641
4419
  var _a;
@@ -4675,17 +4453,14 @@ class GoTrueClient {
4675
4453
  }
4676
4454
  throw error;
4677
4455
  }
4678
- };
4679
- if (this.lock != null) {
4680
- // TODO(v3): remove legacy lock path
4681
- return this._acquireLock(this.lockAcquireTimeout, run);
4682
- }
4683
- return run();
4456
+ });
4684
4457
  }
4685
4458
  /**
4686
4459
  * {@see GoTrueMFAApi#challengeAndVerify}
4687
4460
  */
4688
4461
  async _challengeAndVerify(params) {
4462
+ // both _challenge and _verify independently acquire the lock, so no need
4463
+ // to acquire it here
4689
4464
  const { data: challengeData, error: challengeError } = await this._challenge({
4690
4465
  factorId: params.factorId,
4691
4466
  });
@@ -4703,6 +4478,7 @@ class GoTrueClient {
4703
4478
  */
4704
4479
  async _listFactors() {
4705
4480
  var _a;
4481
+ // use #getUser instead of #_getUser as the former acquires a lock
4706
4482
  const { data: { user }, error: userError, } = await this.getUser();
4707
4483
  if (userError) {
4708
4484
  return { data: null, error: userError };
@@ -5051,8 +4827,14 @@ class GoTrueClient {
5051
4827
  }
5052
4828
  const { header, payload, signature, raw: { header: rawHeader, payload: rawPayload }, } = decodeJWT(token);
5053
4829
  if (!(options === null || options === void 0 ? void 0 : options.allowExpired)) {
5054
- // Reject expired JWTs should only happen if jwt argument was passed
5055
- validateExp(payload.exp);
4830
+ // Reject expired JWTs should only happen if jwt argument was passed.
4831
+ // Rethrow as AuthInvalidJwtError so the outer catch converts it to { data, error }.
4832
+ try {
4833
+ validateExp(payload.exp);
4834
+ }
4835
+ catch (e) {
4836
+ throw new AuthInvalidJwtError(e instanceof Error ? e.message : 'JWT validation failed');
4837
+ }
5056
4838
  }
5057
4839
  const signingKey = !header.alg ||
5058
4840
  header.alg.startsWith('HS') ||