@supabase/gotrue-js 2.105.4 → 2.107.0-canary.5
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.
- package/AGENTS.md +11 -0
- package/README.md +19 -19
- package/dist/main/GoTrueClient.d.ts +83 -14
- package/dist/main/GoTrueClient.d.ts.map +1 -1
- package/dist/main/GoTrueClient.js +355 -110
- package/dist/main/GoTrueClient.js.map +1 -1
- package/dist/main/lib/errors.d.ts +24 -0
- package/dist/main/lib/errors.d.ts.map +1 -1
- package/dist/main/lib/errors.js +31 -1
- package/dist/main/lib/errors.js.map +1 -1
- package/dist/main/lib/fetch.d.ts.map +1 -1
- package/dist/main/lib/fetch.js +3 -1
- package/dist/main/lib/fetch.js.map +1 -1
- package/dist/main/lib/locks.d.ts +28 -34
- package/dist/main/lib/locks.d.ts.map +1 -1
- package/dist/main/lib/locks.js +28 -34
- package/dist/main/lib/locks.js.map +1 -1
- package/dist/main/lib/types.d.ts +16 -27
- package/dist/main/lib/types.d.ts.map +1 -1
- package/dist/main/lib/types.js.map +1 -1
- package/dist/main/lib/version.d.ts +1 -1
- package/dist/main/lib/version.d.ts.map +1 -1
- package/dist/main/lib/version.js +1 -1
- package/dist/main/lib/version.js.map +1 -1
- package/dist/module/GoTrueClient.d.ts +83 -14
- package/dist/module/GoTrueClient.d.ts.map +1 -1
- package/dist/module/GoTrueClient.js +357 -112
- package/dist/module/GoTrueClient.js.map +1 -1
- package/dist/module/lib/errors.d.ts +24 -0
- package/dist/module/lib/errors.d.ts.map +1 -1
- package/dist/module/lib/errors.js +28 -0
- package/dist/module/lib/errors.js.map +1 -1
- package/dist/module/lib/fetch.d.ts.map +1 -1
- package/dist/module/lib/fetch.js +3 -1
- package/dist/module/lib/fetch.js.map +1 -1
- package/dist/module/lib/locks.d.ts +28 -34
- package/dist/module/lib/locks.d.ts.map +1 -1
- package/dist/module/lib/locks.js +28 -34
- package/dist/module/lib/locks.js.map +1 -1
- package/dist/module/lib/types.d.ts +16 -27
- package/dist/module/lib/types.d.ts.map +1 -1
- package/dist/module/lib/types.js.map +1 -1
- package/dist/module/lib/version.d.ts +1 -1
- package/dist/module/lib/version.d.ts.map +1 -1
- package/dist/module/lib/version.js +1 -1
- package/dist/module/lib/version.js.map +1 -1
- package/dist/tsconfig.module.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/migrations/README.md +25 -0
- package/migrations/lockless-coordination.md +89 -0
- package/package.json +24 -11
- package/src/GoTrueClient.ts +423 -141
- package/src/lib/errors.ts +32 -0
- package/src/lib/fetch.ts +3 -1
- package/src/lib/locks.ts +29 -34
- package/src/lib/types.ts +16 -27
- package/src/lib/version.ts +1 -1
|
@@ -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, AuthSessionMissingError, AuthUnknownError, isAuthApiError, isAuthError, isAuthImplicitGrantRedirectError, isAuthRetryableFetchError, isAuthSessionMissingError, } from './lib/errors';
|
|
3
|
+
import { AuthImplicitGrantRedirectError, AuthInvalidCredentialsError, AuthInvalidJwtError, AuthInvalidTokenResponseError, AuthPKCECodeVerifierMissingError, AuthPKCEGrantCodeExchangeError, AuthRefreshDiscardedError, AuthSessionMissingError, AuthUnknownError, isAuthApiError, isAuthError, isAuthImplicitGrantRedirectError, isAuthRefreshDiscardedError, 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
|
|
7
|
+
import { LockAcquireTimeoutError } 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,10 +22,16 @@ const DEFAULT_OPTIONS = {
|
|
|
22
22
|
debug: false,
|
|
23
23
|
hasCustomAuthorizationHeader: false,
|
|
24
24
|
throwOnError: false,
|
|
25
|
-
lockAcquireTimeout: 5000, // 5 seconds
|
|
25
|
+
lockAcquireTimeout: 5000, // 5 seconds. Only used when a custom `lock` is supplied. TODO(v3): remove.
|
|
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
|
+
*/
|
|
29
35
|
async function lockNoOp(name, acquireTimeout, fn) {
|
|
30
36
|
return await fn();
|
|
31
37
|
}
|
|
@@ -79,7 +85,7 @@ class GoTrueClient {
|
|
|
79
85
|
* ```
|
|
80
86
|
*/
|
|
81
87
|
constructor(options) {
|
|
82
|
-
var _a, _b, _c
|
|
88
|
+
var _a, _b, _c;
|
|
83
89
|
/**
|
|
84
90
|
* @experimental
|
|
85
91
|
*/
|
|
@@ -90,6 +96,14 @@ class GoTrueClient {
|
|
|
90
96
|
this.autoRefreshTickTimeout = null;
|
|
91
97
|
this.visibilityChangedCallback = null;
|
|
92
98
|
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;
|
|
93
107
|
/**
|
|
94
108
|
* Keeps track of the async client initialization.
|
|
95
109
|
* When null or not yet resolved the auth state is `unknown`
|
|
@@ -100,6 +114,13 @@ class GoTrueClient {
|
|
|
100
114
|
this.detectSessionInUrl = true;
|
|
101
115
|
this.hasCustomAuthorizationHeader = false;
|
|
102
116
|
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;
|
|
103
124
|
this.lockAcquired = false;
|
|
104
125
|
this.pendingInLock = [];
|
|
105
126
|
/**
|
|
@@ -134,21 +155,22 @@ class GoTrueClient {
|
|
|
134
155
|
this.url = settings.url;
|
|
135
156
|
this.headers = settings.headers;
|
|
136
157
|
this.fetch = resolveFetch(settings.fetch);
|
|
137
|
-
this.lock = settings.lock || lockNoOp;
|
|
138
158
|
this.detectSessionInUrl = settings.detectSessionInUrl;
|
|
139
159
|
this.flowType = settings.flowType;
|
|
140
160
|
this.hasCustomAuthorizationHeader = settings.hasCustomAuthorizationHeader;
|
|
141
161
|
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.
|
|
142
165
|
this.lockAcquireTimeout = settings.lockAcquireTimeout;
|
|
143
|
-
|
|
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) {
|
|
144
172
|
this.lock = settings.lock;
|
|
145
173
|
}
|
|
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
|
-
}
|
|
152
174
|
if (!this.jwks) {
|
|
153
175
|
this.jwks = { keys: [] };
|
|
154
176
|
this.jwks_cached_at = Number.MIN_SAFE_INTEGER;
|
|
@@ -207,7 +229,7 @@ class GoTrueClient {
|
|
|
207
229
|
catch (e) {
|
|
208
230
|
console.error('Failed to create a new BroadcastChannel, multi-tab state changes will not be available', e);
|
|
209
231
|
}
|
|
210
|
-
(
|
|
232
|
+
(_c = this.broadcastChannel) === null || _c === void 0 ? void 0 : _c.addEventListener('message', async (event) => {
|
|
211
233
|
this._debug('received broadcast notification from other tab or client', event);
|
|
212
234
|
try {
|
|
213
235
|
await this._notifyAllSubscribers(event.data.event, event.data.session, false); // broadcast = false so we don't get an endless loop of messages
|
|
@@ -265,9 +287,13 @@ class GoTrueClient {
|
|
|
265
287
|
return await this.initializePromise;
|
|
266
288
|
}
|
|
267
289
|
this.initializePromise = (async () => {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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();
|
|
271
297
|
})();
|
|
272
298
|
return await this.initializePromise;
|
|
273
299
|
}
|
|
@@ -803,6 +829,21 @@ class GoTrueClient {
|
|
|
803
829
|
* password: 'some-password',
|
|
804
830
|
* })
|
|
805
831
|
* ```
|
|
832
|
+
*
|
|
833
|
+
* @exampleDescription Handling errors
|
|
834
|
+
* 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.
|
|
835
|
+
*
|
|
836
|
+
* @example Handling errors
|
|
837
|
+
* ```js
|
|
838
|
+
* const { data, error } = await supabase.auth.signInWithPassword({
|
|
839
|
+
* email: 'example@email.com',
|
|
840
|
+
* password: 'example-password',
|
|
841
|
+
* })
|
|
842
|
+
* if (error) {
|
|
843
|
+
* console.error(error)
|
|
844
|
+
* return
|
|
845
|
+
* }
|
|
846
|
+
* ```
|
|
806
847
|
*/
|
|
807
848
|
async signInWithPassword(credentials) {
|
|
808
849
|
try {
|
|
@@ -1118,9 +1159,13 @@ class GoTrueClient {
|
|
|
1118
1159
|
*/
|
|
1119
1160
|
async exchangeCodeForSession(authCode) {
|
|
1120
1161
|
await this.initializePromise;
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1162
|
+
if (this.lock != null) {
|
|
1163
|
+
// TODO(v3): remove legacy lock path
|
|
1164
|
+
return this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
1165
|
+
return this._exchangeCodeForSession(authCode);
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
return this._exchangeCodeForSession(authCode);
|
|
1124
1169
|
}
|
|
1125
1170
|
/**
|
|
1126
1171
|
* Signs in a user by verifying a message signed by the user's private key.
|
|
@@ -2024,9 +2069,13 @@ class GoTrueClient {
|
|
|
2024
2069
|
*/
|
|
2025
2070
|
async reauthenticate() {
|
|
2026
2071
|
await this.initializePromise;
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2072
|
+
if (this.lock != null) {
|
|
2073
|
+
// TODO(v3): remove legacy lock path
|
|
2074
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
2075
|
+
return await this._reauthenticate();
|
|
2076
|
+
});
|
|
2077
|
+
}
|
|
2078
|
+
return await this._reauthenticate();
|
|
2030
2079
|
}
|
|
2031
2080
|
async _reauthenticate() {
|
|
2032
2081
|
try {
|
|
@@ -2169,7 +2218,7 @@ class GoTrueClient {
|
|
|
2169
2218
|
* - If the session's access token is expired or is about to expire, this method will use the refresh token to refresh the session.
|
|
2170
2219
|
* - 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.
|
|
2171
2220
|
* - **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.
|
|
2172
|
-
* -
|
|
2221
|
+
* - 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.
|
|
2173
2222
|
*
|
|
2174
2223
|
* @example Get the session data
|
|
2175
2224
|
* ```js
|
|
@@ -2236,15 +2285,24 @@ class GoTrueClient {
|
|
|
2236
2285
|
*/
|
|
2237
2286
|
async getSession() {
|
|
2238
2287
|
await this.initializePromise;
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2288
|
+
if (this.lock != null) {
|
|
2289
|
+
// TODO(v3): remove legacy lock path
|
|
2290
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
2291
|
+
return this._useSession(async (result) => {
|
|
2292
|
+
return result;
|
|
2293
|
+
});
|
|
2242
2294
|
});
|
|
2295
|
+
}
|
|
2296
|
+
return await this._useSession(async (result) => {
|
|
2297
|
+
return result;
|
|
2243
2298
|
});
|
|
2244
|
-
return result;
|
|
2245
2299
|
}
|
|
2246
2300
|
/**
|
|
2247
2301
|
* Acquires a global lock based on the storage key.
|
|
2302
|
+
*
|
|
2303
|
+
* TODO(v3): remove along with the legacy lock path. Only called when
|
|
2304
|
+
* `this.lock` is non-null (custom lock supplied via constructor). The
|
|
2305
|
+
* default lockless path bypasses this entirely.
|
|
2248
2306
|
*/
|
|
2249
2307
|
async _acquireLock(acquireTimeout, fn) {
|
|
2250
2308
|
this._debug('#_acquireLock', 'begin', acquireTimeout);
|
|
@@ -2300,15 +2358,17 @@ class GoTrueClient {
|
|
|
2300
2358
|
}
|
|
2301
2359
|
}
|
|
2302
2360
|
/**
|
|
2303
|
-
* Use instead of {@link #getSession} inside the library.
|
|
2304
|
-
*
|
|
2305
|
-
*
|
|
2306
|
-
* session at once across multiple tabs or processes.
|
|
2361
|
+
* Use instead of {@link #getSession} inside the library. Loads the session
|
|
2362
|
+
* via `__loadSession` (which may trigger a refresh if the access token is
|
|
2363
|
+
* within the expiry margin) and runs `fn` with the result.
|
|
2307
2364
|
*/
|
|
2308
2365
|
async _useSession(fn) {
|
|
2309
2366
|
this._debug('#_useSession', 'begin');
|
|
2310
2367
|
try {
|
|
2311
|
-
//
|
|
2368
|
+
// Concurrent callers may both reach __loadSession; storage reads are
|
|
2369
|
+
// idempotent, and the only write path inside it (refresh) is
|
|
2370
|
+
// single-flighted downstream by `refreshingDeferred` in
|
|
2371
|
+
// `_callRefreshToken`. No serialization is needed at this layer.
|
|
2312
2372
|
const result = await this.__loadSession();
|
|
2313
2373
|
return await fn(result);
|
|
2314
2374
|
}
|
|
@@ -2323,7 +2383,8 @@ class GoTrueClient {
|
|
|
2323
2383
|
*/
|
|
2324
2384
|
async __loadSession() {
|
|
2325
2385
|
this._debug('#__loadSession()', 'begin');
|
|
2326
|
-
if (!this.lockAcquired) {
|
|
2386
|
+
if (this.lock != null && !this.lockAcquired) {
|
|
2387
|
+
// TODO(v3): remove. Only meaningful on the legacy lock path.
|
|
2327
2388
|
this._debug('#__loadSession()', 'used outside of an acquired lock!', new Error().stack);
|
|
2328
2389
|
}
|
|
2329
2390
|
try {
|
|
@@ -2466,9 +2527,16 @@ class GoTrueClient {
|
|
|
2466
2527
|
return await this._getUser(jwt);
|
|
2467
2528
|
}
|
|
2468
2529
|
await this.initializePromise;
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2530
|
+
let result;
|
|
2531
|
+
if (this.lock != null) {
|
|
2532
|
+
// TODO(v3): remove legacy lock path
|
|
2533
|
+
result = await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
2534
|
+
return await this._getUser();
|
|
2535
|
+
});
|
|
2536
|
+
}
|
|
2537
|
+
else {
|
|
2538
|
+
result = await this._getUser();
|
|
2539
|
+
}
|
|
2472
2540
|
if (result.data.user) {
|
|
2473
2541
|
this.suppressGetSessionWarning = true;
|
|
2474
2542
|
}
|
|
@@ -2629,9 +2697,13 @@ class GoTrueClient {
|
|
|
2629
2697
|
*/
|
|
2630
2698
|
async updateUser(attributes, options = {}) {
|
|
2631
2699
|
await this.initializePromise;
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2700
|
+
if (this.lock != null) {
|
|
2701
|
+
// TODO(v3): remove legacy lock path
|
|
2702
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
2703
|
+
return await this._updateUser(attributes, options);
|
|
2704
|
+
});
|
|
2705
|
+
}
|
|
2706
|
+
return await this._updateUser(attributes, options);
|
|
2635
2707
|
}
|
|
2636
2708
|
async _updateUser(attributes, options = {}) {
|
|
2637
2709
|
try {
|
|
@@ -2800,9 +2872,13 @@ class GoTrueClient {
|
|
|
2800
2872
|
*/
|
|
2801
2873
|
async setSession(currentSession) {
|
|
2802
2874
|
await this.initializePromise;
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2875
|
+
if (this.lock != null) {
|
|
2876
|
+
// TODO(v3): remove legacy lock path
|
|
2877
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
2878
|
+
return await this._setSession(currentSession);
|
|
2879
|
+
});
|
|
2880
|
+
}
|
|
2881
|
+
return await this._setSession(currentSession);
|
|
2806
2882
|
}
|
|
2807
2883
|
async _setSession(currentSession) {
|
|
2808
2884
|
try {
|
|
@@ -2980,9 +3056,13 @@ class GoTrueClient {
|
|
|
2980
3056
|
*/
|
|
2981
3057
|
async refreshSession(currentSession) {
|
|
2982
3058
|
await this.initializePromise;
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
3059
|
+
if (this.lock != null) {
|
|
3060
|
+
// TODO(v3): remove legacy lock path
|
|
3061
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
3062
|
+
return await this._refreshSession(currentSession);
|
|
3063
|
+
});
|
|
3064
|
+
}
|
|
3065
|
+
return await this._refreshSession(currentSession);
|
|
2986
3066
|
}
|
|
2987
3067
|
async _refreshSession(currentSession) {
|
|
2988
3068
|
try {
|
|
@@ -3120,7 +3200,7 @@ class GoTrueClient {
|
|
|
3120
3200
|
if (typeof this.detectSessionInUrl === 'function') {
|
|
3121
3201
|
return this.detectSessionInUrl(new URL(window.location.href), params);
|
|
3122
3202
|
}
|
|
3123
|
-
return Boolean(params.access_token || params.error_description);
|
|
3203
|
+
return Boolean(params.access_token || params.error || params.error_description || params.error_code);
|
|
3124
3204
|
}
|
|
3125
3205
|
/**
|
|
3126
3206
|
* Checks if the current URL and backing storage contain parameters given by a PKCE flow
|
|
@@ -3172,9 +3252,13 @@ class GoTrueClient {
|
|
|
3172
3252
|
*/
|
|
3173
3253
|
async signOut(options = { scope: 'global' }) {
|
|
3174
3254
|
await this.initializePromise;
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3255
|
+
if (this.lock != null) {
|
|
3256
|
+
// TODO(v3): remove legacy lock path
|
|
3257
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
3258
|
+
return await this._signOut(options);
|
|
3259
|
+
});
|
|
3260
|
+
}
|
|
3261
|
+
return await this._signOut(options);
|
|
3178
3262
|
}
|
|
3179
3263
|
async _signOut({ scope } = { scope: 'global' }) {
|
|
3180
3264
|
return await this._useSession(async (result) => {
|
|
@@ -3210,18 +3294,8 @@ class GoTrueClient {
|
|
|
3210
3294
|
* - Subscribes to important events occurring on the user's session.
|
|
3211
3295
|
* - Use on the frontend/client. It is less useful on the server.
|
|
3212
3296
|
* - 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.
|
|
3213
|
-
* -
|
|
3214
|
-
*
|
|
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
|
-
* ```
|
|
3297
|
+
* - Callbacks can be `async` and can safely call other Supabase auth methods (`getUser`, `setSession`, etc.) from inside the callback.
|
|
3298
|
+
* - Keep callbacks quick. Events are awaited in order, so a slow callback delays subsequent events to subscribers in this tab.
|
|
3225
3299
|
* - Emitted events:
|
|
3226
3300
|
* - `INITIAL_SESSION`
|
|
3227
3301
|
* - Emitted right after the Supabase client is constructed and the initial session from storage is loaded.
|
|
@@ -3404,9 +3478,15 @@ class GoTrueClient {
|
|
|
3404
3478
|
this.stateChangeEmitters.set(id, subscription);
|
|
3405
3479
|
(async () => {
|
|
3406
3480
|
await this.initializePromise;
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3481
|
+
if (this.lock != null) {
|
|
3482
|
+
// TODO(v3): remove legacy lock path
|
|
3483
|
+
await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
3484
|
+
this._emitInitialSession(id);
|
|
3485
|
+
});
|
|
3486
|
+
}
|
|
3487
|
+
else {
|
|
3488
|
+
await this._emitInitialSession(id);
|
|
3489
|
+
}
|
|
3410
3490
|
})();
|
|
3411
3491
|
return { data: { subscription } };
|
|
3412
3492
|
}
|
|
@@ -3748,7 +3828,10 @@ class GoTrueClient {
|
|
|
3748
3828
|
* @param refreshToken A valid refresh token that was returned on login.
|
|
3749
3829
|
*/
|
|
3750
3830
|
async _refreshAccessToken(refreshToken) {
|
|
3751
|
-
|
|
3831
|
+
// Refresh tokens are long-lived bearer credentials; do NOT include any
|
|
3832
|
+
// fragment of the token in the debug tag, even when `debug: true` is
|
|
3833
|
+
// enabled (logs may be forwarded to third-party services).
|
|
3834
|
+
const debugName = `#_refreshAccessToken()`;
|
|
3752
3835
|
this._debug(debugName, 'begin');
|
|
3753
3836
|
try {
|
|
3754
3837
|
const startedAt = Date.now();
|
|
@@ -3855,10 +3938,18 @@ class GoTrueClient {
|
|
|
3855
3938
|
if (this.autoRefreshToken && currentSession.refresh_token) {
|
|
3856
3939
|
const { error } = await this._callRefreshToken(currentSession.refresh_token);
|
|
3857
3940
|
if (error) {
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3941
|
+
// AuthRefreshDiscardedError means a concurrent signOut already
|
|
3942
|
+
// cleared storage and fired SIGNED_OUT. Don't run _removeSession
|
|
3943
|
+
// again here, or we'll emit a duplicate SIGNED_OUT.
|
|
3944
|
+
if (isAuthRefreshDiscardedError(error)) {
|
|
3945
|
+
this._debug(debugName, 'refresh discarded by commit guard', error);
|
|
3946
|
+
}
|
|
3947
|
+
else {
|
|
3948
|
+
console.error(error);
|
|
3949
|
+
if (!isAuthRetryableFetchError(error)) {
|
|
3950
|
+
this._debug(debugName, 'refresh failed with a non-retryable error, removing the session', error);
|
|
3951
|
+
await this._removeSession();
|
|
3952
|
+
}
|
|
3862
3953
|
}
|
|
3863
3954
|
}
|
|
3864
3955
|
}
|
|
@@ -3907,16 +3998,69 @@ class GoTrueClient {
|
|
|
3907
3998
|
if (this.refreshingDeferred) {
|
|
3908
3999
|
return this.refreshingDeferred.promise;
|
|
3909
4000
|
}
|
|
3910
|
-
|
|
4001
|
+
// Refresh tokens are long-lived bearer credentials; do NOT include any
|
|
4002
|
+
// fragment of the token in the debug tag, even when `debug: true` is
|
|
4003
|
+
// enabled (logs may be forwarded to third-party services).
|
|
4004
|
+
const debugName = `#_callRefreshToken()`;
|
|
3911
4005
|
this._debug(debugName, 'begin');
|
|
3912
4006
|
try {
|
|
3913
4007
|
this.refreshingDeferred = new Deferred();
|
|
4008
|
+
// Snapshot storage before the fetch. The commit guard discards the
|
|
4009
|
+
// rotated tokens only when a non-null pre-fetch snapshot changed under
|
|
4010
|
+
// us — typical case: a concurrent `signOut` ran `_removeSession`, or
|
|
4011
|
+
// another tab's refresh rewrote the slot. Callers passing
|
|
4012
|
+
// externally-sourced tokens (SSR cookie handoff, multi-account
|
|
4013
|
+
// switching, `setSession`/`refreshSession({ refresh_token })`) may
|
|
4014
|
+
// start from a null snapshot OR from a non-null snapshot whose
|
|
4015
|
+
// refresh_token differs from the one they're hydrating; in both
|
|
4016
|
+
// cases the guard fires only when storage was *modified between
|
|
4017
|
+
// snapshots*, not when the input token disagrees with what's stored.
|
|
4018
|
+
const storedAtStart = (await getItemAsync(this.storage, this.storageKey));
|
|
3914
4019
|
const { data, error } = await this._refreshAccessToken(refreshToken);
|
|
3915
4020
|
if (error)
|
|
3916
4021
|
throw error;
|
|
3917
4022
|
if (!data.session)
|
|
3918
4023
|
throw new AuthSessionMissingError();
|
|
4024
|
+
const storedAfter = (await getItemAsync(this.storage, this.storageKey));
|
|
4025
|
+
const storageChangedUnderUs = storedAtStart !== null &&
|
|
4026
|
+
(storedAfter === null || storedAfter.refresh_token !== storedAtStart.refresh_token);
|
|
4027
|
+
if (storageChangedUnderUs) {
|
|
4028
|
+
this._debug(debugName, 'commit guard: storage changed since refresh started, discarding rotated tokens', {
|
|
4029
|
+
// Presence indicators only — never log refresh token fragments,
|
|
4030
|
+
// even partial. Logs may be forwarded to third-party services.
|
|
4031
|
+
startedWith: 'present',
|
|
4032
|
+
nowHolds: storedAfter ? 'replaced' : 'cleared',
|
|
4033
|
+
});
|
|
4034
|
+
const discarded = {
|
|
4035
|
+
data: null,
|
|
4036
|
+
error: new AuthRefreshDiscardedError(),
|
|
4037
|
+
};
|
|
4038
|
+
this.refreshingDeferred.resolve(discarded);
|
|
4039
|
+
return discarded;
|
|
4040
|
+
}
|
|
4041
|
+
// Second leg of the commit guard: close the TOCTOU window between the
|
|
4042
|
+
// synchronous `storageChangedUnderUs` check and the actual storage
|
|
4043
|
+
// writes inside `_saveSession`. A concurrent `signOut → _removeSession`
|
|
4044
|
+
// can land inside `_saveSession`'s `await setItemAsync(...)` yields and
|
|
4045
|
+
// clear storage just before we overwrite it. Capture the epoch BEFORE
|
|
4046
|
+
// the save and re-check after; if it advanced, undo the write directly
|
|
4047
|
+
// (do NOT call `_removeSession` — that would emit a duplicate
|
|
4048
|
+
// SIGNED_OUT for the concurrent signOut that already fired one).
|
|
4049
|
+
const epochBeforeSave = this._sessionRemovalEpoch;
|
|
3919
4050
|
await this._saveSession(data.session);
|
|
4051
|
+
if (this._sessionRemovalEpoch !== epochBeforeSave) {
|
|
4052
|
+
this._debug(debugName, 'commit guard (post-save): _removeSession ran during _saveSession, undoing write');
|
|
4053
|
+
await removeItemAsync(this.storage, this.storageKey);
|
|
4054
|
+
if (this.userStorage) {
|
|
4055
|
+
await removeItemAsync(this.userStorage, this.storageKey + '-user');
|
|
4056
|
+
}
|
|
4057
|
+
const discarded = {
|
|
4058
|
+
data: null,
|
|
4059
|
+
error: new AuthRefreshDiscardedError(),
|
|
4060
|
+
};
|
|
4061
|
+
this.refreshingDeferred.resolve(discarded);
|
|
4062
|
+
return discarded;
|
|
4063
|
+
}
|
|
3920
4064
|
await this._notifyAllSubscribers('TOKEN_REFRESHED', data.session);
|
|
3921
4065
|
const result = { data: data.session, error: null };
|
|
3922
4066
|
this.refreshingDeferred.resolve(result);
|
|
@@ -4010,6 +4154,11 @@ class GoTrueClient {
|
|
|
4010
4154
|
}
|
|
4011
4155
|
}
|
|
4012
4156
|
async _removeSession() {
|
|
4157
|
+
// Bump synchronously, BEFORE any `await`, so that `_callRefreshToken`'s
|
|
4158
|
+
// post-save check sees the increment whenever this method has started —
|
|
4159
|
+
// even if it hasn't finished. Pairs with the epoch check in
|
|
4160
|
+
// `_callRefreshToken`. See `_sessionRemovalEpoch` field doc.
|
|
4161
|
+
this._sessionRemovalEpoch += 1;
|
|
4013
4162
|
this._debug('#_removeSession()');
|
|
4014
4163
|
this.suppressGetSessionWarning = false;
|
|
4015
4164
|
await removeItemAsync(this.storage, this.storageKey);
|
|
@@ -4180,47 +4329,122 @@ class GoTrueClient {
|
|
|
4180
4329
|
this._removeVisibilityChangedCallback();
|
|
4181
4330
|
await this._stopAutoRefresh();
|
|
4182
4331
|
}
|
|
4332
|
+
/**
|
|
4333
|
+
* Tears down the client's background work: stops the auto-refresh interval,
|
|
4334
|
+
* removes the `visibilitychange` listener, closes the cross-tab
|
|
4335
|
+
* `BroadcastChannel`, and clears registered `onAuthStateChange` subscribers.
|
|
4336
|
+
*
|
|
4337
|
+
* Call this from cleanup hooks when the client is being replaced before
|
|
4338
|
+
* its JS realm is destroyed. React Strict Mode and HMR are the common
|
|
4339
|
+
* cases. Any in-flight `fetch` calls continue to completion and may still
|
|
4340
|
+
* write to storage; dispose doesn't abort them or erase storage.
|
|
4341
|
+
*
|
|
4342
|
+
* Lifecycle caveat: because in-flight refreshes are not aborted, a
|
|
4343
|
+
* disposed instance can still persist a rotated session to storage after
|
|
4344
|
+
* `dispose()` returns. A subsequent `createClient` against the same
|
|
4345
|
+
* `storageKey` will pick up that session on its next read. If you need
|
|
4346
|
+
* strict isolation between client lifecycles, await any pending auth
|
|
4347
|
+
* operation before calling `dispose()` (or change the `storageKey` for
|
|
4348
|
+
* the replacement client).
|
|
4349
|
+
*
|
|
4350
|
+
* Safe to call repeatedly.
|
|
4351
|
+
*
|
|
4352
|
+
* @category Auth
|
|
4353
|
+
*
|
|
4354
|
+
* @example Cleanup on React unmount
|
|
4355
|
+
* ```ts
|
|
4356
|
+
* useEffect(() => {
|
|
4357
|
+
* const client = createClient(...)
|
|
4358
|
+
* return () => { client.auth.dispose() }
|
|
4359
|
+
* }, [])
|
|
4360
|
+
* ```
|
|
4361
|
+
*/
|
|
4362
|
+
async dispose() {
|
|
4363
|
+
var _a;
|
|
4364
|
+
this._removeVisibilityChangedCallback();
|
|
4365
|
+
await this._stopAutoRefresh();
|
|
4366
|
+
(_a = this.broadcastChannel) === null || _a === void 0 ? void 0 : _a.close();
|
|
4367
|
+
this.broadcastChannel = null;
|
|
4368
|
+
this.stateChangeEmitters.clear();
|
|
4369
|
+
}
|
|
4183
4370
|
/**
|
|
4184
4371
|
* Runs the auto refresh token tick.
|
|
4185
4372
|
*/
|
|
4186
4373
|
async _autoRefreshTokenTick() {
|
|
4187
4374
|
this._debug('#_autoRefreshTokenTick()', 'begin');
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4375
|
+
if (this.lock != null) {
|
|
4376
|
+
// TODO(v3): remove legacy lock path. Uses `_acquireLock(0, ...)` which
|
|
4377
|
+
// throws `LockAcquireTimeoutError` immediately if the lock is held —
|
|
4378
|
+
// that's the fail-fast skip path that lets the tick bail out instead
|
|
4379
|
+
// of queuing behind a long-running operation.
|
|
4380
|
+
try {
|
|
4381
|
+
await this._acquireLock(0, async () => {
|
|
4192
4382
|
try {
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4383
|
+
const now = Date.now();
|
|
4384
|
+
try {
|
|
4385
|
+
return await this._useSession(async (result) => {
|
|
4386
|
+
const { data: { session }, } = result;
|
|
4387
|
+
if (!session || !session.refresh_token || !session.expires_at) {
|
|
4388
|
+
this._debug('#_autoRefreshTokenTick()', 'no session');
|
|
4389
|
+
return;
|
|
4390
|
+
}
|
|
4391
|
+
const expiresInTicks = Math.floor((session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS);
|
|
4392
|
+
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`);
|
|
4393
|
+
if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) {
|
|
4394
|
+
await this._callRefreshToken(session.refresh_token);
|
|
4395
|
+
}
|
|
4396
|
+
});
|
|
4397
|
+
}
|
|
4398
|
+
catch (e) {
|
|
4399
|
+
console.error('Auto refresh tick failed with error. This is likely a transient error.', e);
|
|
4400
|
+
}
|
|
4206
4401
|
}
|
|
4207
|
-
|
|
4208
|
-
|
|
4402
|
+
finally {
|
|
4403
|
+
this._debug('#_autoRefreshTokenTick()', 'end');
|
|
4209
4404
|
}
|
|
4405
|
+
});
|
|
4406
|
+
}
|
|
4407
|
+
catch (e) {
|
|
4408
|
+
if (e instanceof LockAcquireTimeoutError) {
|
|
4409
|
+
this._debug('auto refresh token tick lock not available');
|
|
4210
4410
|
}
|
|
4211
|
-
|
|
4212
|
-
|
|
4411
|
+
else {
|
|
4412
|
+
throw e;
|
|
4213
4413
|
}
|
|
4214
|
-
}
|
|
4414
|
+
}
|
|
4415
|
+
return;
|
|
4215
4416
|
}
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4417
|
+
// Lockless default: skip if a refresh is already in flight.
|
|
4418
|
+
// `_callRefreshToken` also dedupes via the same field; this is just a
|
|
4419
|
+
// fast-path skip to avoid an unnecessary storage read.
|
|
4420
|
+
if (this.refreshingDeferred !== null) {
|
|
4421
|
+
this._debug('#_autoRefreshTokenTick()', 'refresh already in flight, skipping');
|
|
4422
|
+
return;
|
|
4423
|
+
}
|
|
4424
|
+
try {
|
|
4425
|
+
const now = Date.now();
|
|
4426
|
+
try {
|
|
4427
|
+
await this._useSession(async (result) => {
|
|
4428
|
+
const { data: { session }, } = result;
|
|
4429
|
+
if (!session || !session.refresh_token || !session.expires_at) {
|
|
4430
|
+
this._debug('#_autoRefreshTokenTick()', 'no session');
|
|
4431
|
+
return;
|
|
4432
|
+
}
|
|
4433
|
+
// session will expire in this many ticks (or has already expired if <= 0)
|
|
4434
|
+
const expiresInTicks = Math.floor((session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS);
|
|
4435
|
+
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`);
|
|
4436
|
+
if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) {
|
|
4437
|
+
await this._callRefreshToken(session.refresh_token);
|
|
4438
|
+
}
|
|
4439
|
+
});
|
|
4219
4440
|
}
|
|
4220
|
-
|
|
4221
|
-
|
|
4441
|
+
catch (e) {
|
|
4442
|
+
console.error('Auto refresh tick failed with error. This is likely a transient error.', e);
|
|
4222
4443
|
}
|
|
4223
4444
|
}
|
|
4445
|
+
finally {
|
|
4446
|
+
this._debug('#_autoRefreshTokenTick()', 'end');
|
|
4447
|
+
}
|
|
4224
4448
|
}
|
|
4225
4449
|
/**
|
|
4226
4450
|
* Registers callbacks on the browser / platform, which in-turn run
|
|
@@ -4269,18 +4493,26 @@ class GoTrueClient {
|
|
|
4269
4493
|
if (!calledFromInitialize) {
|
|
4270
4494
|
// called when the visibility has changed, i.e. the browser
|
|
4271
4495
|
// transitioned from hidden -> visible so we need to see if the session
|
|
4272
|
-
// should be recovered
|
|
4273
|
-
// the lock first asynchronously
|
|
4496
|
+
// should be recovered
|
|
4274
4497
|
await this.initializePromise;
|
|
4275
|
-
|
|
4498
|
+
if (this.lock != null) {
|
|
4499
|
+
// TODO(v3): remove legacy lock path
|
|
4500
|
+
await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
4501
|
+
if (document.visibilityState !== 'visible') {
|
|
4502
|
+
this._debug(methodName, 'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting');
|
|
4503
|
+
return;
|
|
4504
|
+
}
|
|
4505
|
+
await this._recoverAndRefresh();
|
|
4506
|
+
});
|
|
4507
|
+
}
|
|
4508
|
+
else {
|
|
4276
4509
|
if (document.visibilityState !== 'visible') {
|
|
4277
|
-
this._debug(methodName, '
|
|
4278
|
-
// visibility has changed while waiting for the lock, abort
|
|
4510
|
+
this._debug(methodName, 'visibilityState is no longer visible, skipping recovery');
|
|
4279
4511
|
return;
|
|
4280
4512
|
}
|
|
4281
4513
|
// recover the session
|
|
4282
4514
|
await this._recoverAndRefresh();
|
|
4283
|
-
}
|
|
4515
|
+
}
|
|
4284
4516
|
}
|
|
4285
4517
|
}
|
|
4286
4518
|
else if (document.visibilityState === 'hidden') {
|
|
@@ -4376,7 +4608,7 @@ class GoTrueClient {
|
|
|
4376
4608
|
}
|
|
4377
4609
|
}
|
|
4378
4610
|
async _verify(params) {
|
|
4379
|
-
|
|
4611
|
+
const run = async () => {
|
|
4380
4612
|
try {
|
|
4381
4613
|
return await this._useSession(async (result) => {
|
|
4382
4614
|
var _a;
|
|
@@ -4410,10 +4642,15 @@ class GoTrueClient {
|
|
|
4410
4642
|
}
|
|
4411
4643
|
throw error;
|
|
4412
4644
|
}
|
|
4413
|
-
}
|
|
4645
|
+
};
|
|
4646
|
+
if (this.lock != null) {
|
|
4647
|
+
// TODO(v3): remove legacy lock path
|
|
4648
|
+
return this._acquireLock(this.lockAcquireTimeout, run);
|
|
4649
|
+
}
|
|
4650
|
+
return run();
|
|
4414
4651
|
}
|
|
4415
4652
|
async _challenge(params) {
|
|
4416
|
-
|
|
4653
|
+
const run = async () => {
|
|
4417
4654
|
try {
|
|
4418
4655
|
return await this._useSession(async (result) => {
|
|
4419
4656
|
var _a;
|
|
@@ -4453,14 +4690,17 @@ class GoTrueClient {
|
|
|
4453
4690
|
}
|
|
4454
4691
|
throw error;
|
|
4455
4692
|
}
|
|
4456
|
-
}
|
|
4693
|
+
};
|
|
4694
|
+
if (this.lock != null) {
|
|
4695
|
+
// TODO(v3): remove legacy lock path
|
|
4696
|
+
return this._acquireLock(this.lockAcquireTimeout, run);
|
|
4697
|
+
}
|
|
4698
|
+
return run();
|
|
4457
4699
|
}
|
|
4458
4700
|
/**
|
|
4459
4701
|
* {@see GoTrueMFAApi#challengeAndVerify}
|
|
4460
4702
|
*/
|
|
4461
4703
|
async _challengeAndVerify(params) {
|
|
4462
|
-
// both _challenge and _verify independently acquire the lock, so no need
|
|
4463
|
-
// to acquire it here
|
|
4464
4704
|
const { data: challengeData, error: challengeError } = await this._challenge({
|
|
4465
4705
|
factorId: params.factorId,
|
|
4466
4706
|
});
|
|
@@ -4478,7 +4718,6 @@ class GoTrueClient {
|
|
|
4478
4718
|
*/
|
|
4479
4719
|
async _listFactors() {
|
|
4480
4720
|
var _a;
|
|
4481
|
-
// use #getUser instead of #_getUser as the former acquires a lock
|
|
4482
4721
|
const { data: { user }, error: userError, } = await this.getUser();
|
|
4483
4722
|
if (userError) {
|
|
4484
4723
|
return { data: null, error: userError };
|
|
@@ -4827,8 +5066,14 @@ class GoTrueClient {
|
|
|
4827
5066
|
}
|
|
4828
5067
|
const { header, payload, signature, raw: { header: rawHeader, payload: rawPayload }, } = decodeJWT(token);
|
|
4829
5068
|
if (!(options === null || options === void 0 ? void 0 : options.allowExpired)) {
|
|
4830
|
-
// Reject expired JWTs should only happen if jwt argument was passed
|
|
4831
|
-
|
|
5069
|
+
// Reject expired JWTs should only happen if jwt argument was passed.
|
|
5070
|
+
// Rethrow as AuthInvalidJwtError so the outer catch converts it to { data, error }.
|
|
5071
|
+
try {
|
|
5072
|
+
validateExp(payload.exp);
|
|
5073
|
+
}
|
|
5074
|
+
catch (e) {
|
|
5075
|
+
throw new AuthInvalidJwtError(e instanceof Error ? e.message : 'JWT validation failed');
|
|
5076
|
+
}
|
|
4832
5077
|
}
|
|
4833
5078
|
const signingKey = !header.alg ||
|
|
4834
5079
|
header.alg.startsWith('HS') ||
|