@supabase/auth-js 2.107.0-beta.0 → 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.
- package/dist/main/GoTrueClient.d.ts +14 -68
- package/dist/main/GoTrueClient.d.ts.map +1 -1
- package/dist/main/GoTrueClient.js +116 -334
- package/dist/main/GoTrueClient.js.map +1 -1
- package/dist/main/lib/errors.d.ts +0 -24
- package/dist/main/lib/errors.d.ts.map +1 -1
- package/dist/main/lib/errors.js +1 -31
- package/dist/main/lib/errors.js.map +1 -1
- package/dist/main/lib/locks.d.ts +34 -28
- package/dist/main/lib/locks.d.ts.map +1 -1
- package/dist/main/lib/locks.js +34 -28
- package/dist/main/lib/locks.js.map +1 -1
- package/dist/main/lib/types.d.ts +27 -16
- 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 +14 -68
- package/dist/module/GoTrueClient.d.ts.map +1 -1
- package/dist/module/GoTrueClient.js +118 -336
- package/dist/module/GoTrueClient.js.map +1 -1
- package/dist/module/lib/errors.d.ts +0 -24
- package/dist/module/lib/errors.d.ts.map +1 -1
- package/dist/module/lib/errors.js +0 -28
- package/dist/module/lib/errors.js.map +1 -1
- package/dist/module/lib/locks.d.ts +34 -28
- package/dist/module/lib/locks.d.ts.map +1 -1
- package/dist/module/lib/locks.js +34 -28
- package/dist/module/lib/locks.js.map +1 -1
- package/dist/module/lib/types.d.ts +27 -16
- 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/package.json +1 -1
- package/src/GoTrueClient.ts +147 -400
- package/src/lib/errors.ts +0 -32
- package/src/lib/locks.ts +34 -29
- package/src/lib/types.ts +27 -16
- package/src/lib/version.ts +1 -1
- 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,
|
|
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
|
|
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
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
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
|
-
* -
|
|
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
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
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.
|
|
2347
|
-
*
|
|
2348
|
-
*
|
|
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
|
-
//
|
|
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 (
|
|
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
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
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
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
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
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
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
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
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
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
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
|
-
* -
|
|
3283
|
-
*
|
|
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
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
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
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
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
|
-
|
|
4388
|
-
|
|
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
|
-
|
|
4397
|
-
|
|
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
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
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
|
-
|
|
4427
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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') ||
|