@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
|
@@ -25,10 +25,16 @@ const DEFAULT_OPTIONS = {
|
|
|
25
25
|
debug: false,
|
|
26
26
|
hasCustomAuthorizationHeader: false,
|
|
27
27
|
throwOnError: false,
|
|
28
|
-
lockAcquireTimeout: 5000, // 5 seconds
|
|
28
|
+
lockAcquireTimeout: 5000, // 5 seconds. Only used when a custom `lock` is supplied. TODO(v3): remove.
|
|
29
29
|
skipAutoInitialize: false,
|
|
30
30
|
experimental: {},
|
|
31
31
|
};
|
|
32
|
+
/**
|
|
33
|
+
* No-op lock used internally as a placeholder. Kept so older test setups that
|
|
34
|
+
* inject this exact reference do not break; new code never sees it because
|
|
35
|
+
* `this.lock` stays `null` when no custom lock is supplied (lockless path).
|
|
36
|
+
* TODO(v3): remove with the legacy lock path.
|
|
37
|
+
*/
|
|
32
38
|
async function lockNoOp(name, acquireTimeout, fn) {
|
|
33
39
|
return await fn();
|
|
34
40
|
}
|
|
@@ -82,7 +88,7 @@ class GoTrueClient {
|
|
|
82
88
|
* ```
|
|
83
89
|
*/
|
|
84
90
|
constructor(options) {
|
|
85
|
-
var _a, _b, _c
|
|
91
|
+
var _a, _b, _c;
|
|
86
92
|
/**
|
|
87
93
|
* @experimental
|
|
88
94
|
*/
|
|
@@ -93,6 +99,14 @@ class GoTrueClient {
|
|
|
93
99
|
this.autoRefreshTickTimeout = null;
|
|
94
100
|
this.visibilityChangedCallback = null;
|
|
95
101
|
this.refreshingDeferred = null;
|
|
102
|
+
/**
|
|
103
|
+
* Monotonic counter incremented at the top of `_removeSession`, before any
|
|
104
|
+
* `await`. The commit guard inside `_callRefreshToken` captures this value
|
|
105
|
+
* before `_saveSession` and re-checks it after, so a `signOut` that
|
|
106
|
+
* interleaves inside `_saveSession`'s storage-write awaits is still caught
|
|
107
|
+
* (the post-fetch storage snapshot alone misses that window).
|
|
108
|
+
*/
|
|
109
|
+
this._sessionRemovalEpoch = 0;
|
|
96
110
|
/**
|
|
97
111
|
* Keeps track of the async client initialization.
|
|
98
112
|
* When null or not yet resolved the auth state is `unknown`
|
|
@@ -103,6 +117,13 @@ class GoTrueClient {
|
|
|
103
117
|
this.detectSessionInUrl = true;
|
|
104
118
|
this.hasCustomAuthorizationHeader = false;
|
|
105
119
|
this.suppressGetSessionWarning = false;
|
|
120
|
+
/**
|
|
121
|
+
* Custom lock function passed via `settings.lock`. When non-null, every auth
|
|
122
|
+
* operation runs inside `_acquireLock`. When null (the default), the client
|
|
123
|
+
* uses its lockless coordination (refresh single-flight + commit guard).
|
|
124
|
+
* TODO(v3): remove along with the legacy lock path.
|
|
125
|
+
*/
|
|
126
|
+
this.lock = null;
|
|
106
127
|
this.lockAcquired = false;
|
|
107
128
|
this.pendingInLock = [];
|
|
108
129
|
/**
|
|
@@ -137,21 +158,22 @@ class GoTrueClient {
|
|
|
137
158
|
this.url = settings.url;
|
|
138
159
|
this.headers = settings.headers;
|
|
139
160
|
this.fetch = (0, helpers_1.resolveFetch)(settings.fetch);
|
|
140
|
-
this.lock = settings.lock || lockNoOp;
|
|
141
161
|
this.detectSessionInUrl = settings.detectSessionInUrl;
|
|
142
162
|
this.flowType = settings.flowType;
|
|
143
163
|
this.hasCustomAuthorizationHeader = settings.hasCustomAuthorizationHeader;
|
|
144
164
|
this.throwOnError = settings.throwOnError;
|
|
165
|
+
// Always wire `lockAcquireTimeout` even on the lockless path: consumers
|
|
166
|
+
// (including supabase-js tests) read it off the client to verify option
|
|
167
|
+
// flow-through.
|
|
145
168
|
this.lockAcquireTimeout = settings.lockAcquireTimeout;
|
|
146
|
-
|
|
169
|
+
// TODO(v3): remove. Legacy opt-in path preserved for backwards
|
|
170
|
+
// compatibility with callers passing a custom `lock` (typically React
|
|
171
|
+
// Native `processLock` or Node multi-process setups). When `settings.lock`
|
|
172
|
+
// is null the client uses its lockless coordination — no `navigator.locks`
|
|
173
|
+
// by default, no implicit `processLock`.
|
|
174
|
+
if (settings.lock != null) {
|
|
147
175
|
this.lock = settings.lock;
|
|
148
176
|
}
|
|
149
|
-
else if (this.persistSession && (0, helpers_1.isBrowser)() && ((_c = globalThis === null || globalThis === void 0 ? void 0 : globalThis.navigator) === null || _c === void 0 ? void 0 : _c.locks)) {
|
|
150
|
-
this.lock = locks_1.navigatorLock;
|
|
151
|
-
}
|
|
152
|
-
else {
|
|
153
|
-
this.lock = lockNoOp;
|
|
154
|
-
}
|
|
155
177
|
if (!this.jwks) {
|
|
156
178
|
this.jwks = { keys: [] };
|
|
157
179
|
this.jwks_cached_at = Number.MIN_SAFE_INTEGER;
|
|
@@ -210,7 +232,7 @@ class GoTrueClient {
|
|
|
210
232
|
catch (e) {
|
|
211
233
|
console.error('Failed to create a new BroadcastChannel, multi-tab state changes will not be available', e);
|
|
212
234
|
}
|
|
213
|
-
(
|
|
235
|
+
(_c = this.broadcastChannel) === null || _c === void 0 ? void 0 : _c.addEventListener('message', async (event) => {
|
|
214
236
|
this._debug('received broadcast notification from other tab or client', event);
|
|
215
237
|
try {
|
|
216
238
|
await this._notifyAllSubscribers(event.data.event, event.data.session, false); // broadcast = false so we don't get an endless loop of messages
|
|
@@ -268,9 +290,13 @@ class GoTrueClient {
|
|
|
268
290
|
return await this.initializePromise;
|
|
269
291
|
}
|
|
270
292
|
this.initializePromise = (async () => {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
293
|
+
if (this.lock != null) {
|
|
294
|
+
// TODO(v3): remove legacy lock path
|
|
295
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
296
|
+
return await this._initialize();
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return await this._initialize();
|
|
274
300
|
})();
|
|
275
301
|
return await this.initializePromise;
|
|
276
302
|
}
|
|
@@ -806,6 +832,21 @@ class GoTrueClient {
|
|
|
806
832
|
* password: 'some-password',
|
|
807
833
|
* })
|
|
808
834
|
* ```
|
|
835
|
+
*
|
|
836
|
+
* @exampleDescription Handling errors
|
|
837
|
+
* 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.
|
|
838
|
+
*
|
|
839
|
+
* @example Handling errors
|
|
840
|
+
* ```js
|
|
841
|
+
* const { data, error } = await supabase.auth.signInWithPassword({
|
|
842
|
+
* email: 'example@email.com',
|
|
843
|
+
* password: 'example-password',
|
|
844
|
+
* })
|
|
845
|
+
* if (error) {
|
|
846
|
+
* console.error(error)
|
|
847
|
+
* return
|
|
848
|
+
* }
|
|
849
|
+
* ```
|
|
809
850
|
*/
|
|
810
851
|
async signInWithPassword(credentials) {
|
|
811
852
|
try {
|
|
@@ -1121,9 +1162,13 @@ class GoTrueClient {
|
|
|
1121
1162
|
*/
|
|
1122
1163
|
async exchangeCodeForSession(authCode) {
|
|
1123
1164
|
await this.initializePromise;
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1165
|
+
if (this.lock != null) {
|
|
1166
|
+
// TODO(v3): remove legacy lock path
|
|
1167
|
+
return this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
1168
|
+
return this._exchangeCodeForSession(authCode);
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
return this._exchangeCodeForSession(authCode);
|
|
1127
1172
|
}
|
|
1128
1173
|
/**
|
|
1129
1174
|
* Signs in a user by verifying a message signed by the user's private key.
|
|
@@ -2027,9 +2072,13 @@ class GoTrueClient {
|
|
|
2027
2072
|
*/
|
|
2028
2073
|
async reauthenticate() {
|
|
2029
2074
|
await this.initializePromise;
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2075
|
+
if (this.lock != null) {
|
|
2076
|
+
// TODO(v3): remove legacy lock path
|
|
2077
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
2078
|
+
return await this._reauthenticate();
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
return await this._reauthenticate();
|
|
2033
2082
|
}
|
|
2034
2083
|
async _reauthenticate() {
|
|
2035
2084
|
try {
|
|
@@ -2172,7 +2221,7 @@ class GoTrueClient {
|
|
|
2172
2221
|
* - If the session's access token is expired or is about to expire, this method will use the refresh token to refresh the session.
|
|
2173
2222
|
* - 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.
|
|
2174
2223
|
* - **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.
|
|
2175
|
-
* -
|
|
2224
|
+
* - 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.
|
|
2176
2225
|
*
|
|
2177
2226
|
* @example Get the session data
|
|
2178
2227
|
* ```js
|
|
@@ -2239,15 +2288,24 @@ class GoTrueClient {
|
|
|
2239
2288
|
*/
|
|
2240
2289
|
async getSession() {
|
|
2241
2290
|
await this.initializePromise;
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2291
|
+
if (this.lock != null) {
|
|
2292
|
+
// TODO(v3): remove legacy lock path
|
|
2293
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
2294
|
+
return this._useSession(async (result) => {
|
|
2295
|
+
return result;
|
|
2296
|
+
});
|
|
2245
2297
|
});
|
|
2298
|
+
}
|
|
2299
|
+
return await this._useSession(async (result) => {
|
|
2300
|
+
return result;
|
|
2246
2301
|
});
|
|
2247
|
-
return result;
|
|
2248
2302
|
}
|
|
2249
2303
|
/**
|
|
2250
2304
|
* Acquires a global lock based on the storage key.
|
|
2305
|
+
*
|
|
2306
|
+
* TODO(v3): remove along with the legacy lock path. Only called when
|
|
2307
|
+
* `this.lock` is non-null (custom lock supplied via constructor). The
|
|
2308
|
+
* default lockless path bypasses this entirely.
|
|
2251
2309
|
*/
|
|
2252
2310
|
async _acquireLock(acquireTimeout, fn) {
|
|
2253
2311
|
this._debug('#_acquireLock', 'begin', acquireTimeout);
|
|
@@ -2303,15 +2361,17 @@ class GoTrueClient {
|
|
|
2303
2361
|
}
|
|
2304
2362
|
}
|
|
2305
2363
|
/**
|
|
2306
|
-
* Use instead of {@link #getSession} inside the library.
|
|
2307
|
-
*
|
|
2308
|
-
*
|
|
2309
|
-
* session at once across multiple tabs or processes.
|
|
2364
|
+
* Use instead of {@link #getSession} inside the library. Loads the session
|
|
2365
|
+
* via `__loadSession` (which may trigger a refresh if the access token is
|
|
2366
|
+
* within the expiry margin) and runs `fn` with the result.
|
|
2310
2367
|
*/
|
|
2311
2368
|
async _useSession(fn) {
|
|
2312
2369
|
this._debug('#_useSession', 'begin');
|
|
2313
2370
|
try {
|
|
2314
|
-
//
|
|
2371
|
+
// Concurrent callers may both reach __loadSession; storage reads are
|
|
2372
|
+
// idempotent, and the only write path inside it (refresh) is
|
|
2373
|
+
// single-flighted downstream by `refreshingDeferred` in
|
|
2374
|
+
// `_callRefreshToken`. No serialization is needed at this layer.
|
|
2315
2375
|
const result = await this.__loadSession();
|
|
2316
2376
|
return await fn(result);
|
|
2317
2377
|
}
|
|
@@ -2326,7 +2386,8 @@ class GoTrueClient {
|
|
|
2326
2386
|
*/
|
|
2327
2387
|
async __loadSession() {
|
|
2328
2388
|
this._debug('#__loadSession()', 'begin');
|
|
2329
|
-
if (!this.lockAcquired) {
|
|
2389
|
+
if (this.lock != null && !this.lockAcquired) {
|
|
2390
|
+
// TODO(v3): remove. Only meaningful on the legacy lock path.
|
|
2330
2391
|
this._debug('#__loadSession()', 'used outside of an acquired lock!', new Error().stack);
|
|
2331
2392
|
}
|
|
2332
2393
|
try {
|
|
@@ -2469,9 +2530,16 @@ class GoTrueClient {
|
|
|
2469
2530
|
return await this._getUser(jwt);
|
|
2470
2531
|
}
|
|
2471
2532
|
await this.initializePromise;
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2533
|
+
let result;
|
|
2534
|
+
if (this.lock != null) {
|
|
2535
|
+
// TODO(v3): remove legacy lock path
|
|
2536
|
+
result = await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
2537
|
+
return await this._getUser();
|
|
2538
|
+
});
|
|
2539
|
+
}
|
|
2540
|
+
else {
|
|
2541
|
+
result = await this._getUser();
|
|
2542
|
+
}
|
|
2475
2543
|
if (result.data.user) {
|
|
2476
2544
|
this.suppressGetSessionWarning = true;
|
|
2477
2545
|
}
|
|
@@ -2632,9 +2700,13 @@ class GoTrueClient {
|
|
|
2632
2700
|
*/
|
|
2633
2701
|
async updateUser(attributes, options = {}) {
|
|
2634
2702
|
await this.initializePromise;
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2703
|
+
if (this.lock != null) {
|
|
2704
|
+
// TODO(v3): remove legacy lock path
|
|
2705
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
2706
|
+
return await this._updateUser(attributes, options);
|
|
2707
|
+
});
|
|
2708
|
+
}
|
|
2709
|
+
return await this._updateUser(attributes, options);
|
|
2638
2710
|
}
|
|
2639
2711
|
async _updateUser(attributes, options = {}) {
|
|
2640
2712
|
try {
|
|
@@ -2803,9 +2875,13 @@ class GoTrueClient {
|
|
|
2803
2875
|
*/
|
|
2804
2876
|
async setSession(currentSession) {
|
|
2805
2877
|
await this.initializePromise;
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2878
|
+
if (this.lock != null) {
|
|
2879
|
+
// TODO(v3): remove legacy lock path
|
|
2880
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
2881
|
+
return await this._setSession(currentSession);
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2884
|
+
return await this._setSession(currentSession);
|
|
2809
2885
|
}
|
|
2810
2886
|
async _setSession(currentSession) {
|
|
2811
2887
|
try {
|
|
@@ -2983,9 +3059,13 @@ class GoTrueClient {
|
|
|
2983
3059
|
*/
|
|
2984
3060
|
async refreshSession(currentSession) {
|
|
2985
3061
|
await this.initializePromise;
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
3062
|
+
if (this.lock != null) {
|
|
3063
|
+
// TODO(v3): remove legacy lock path
|
|
3064
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
3065
|
+
return await this._refreshSession(currentSession);
|
|
3066
|
+
});
|
|
3067
|
+
}
|
|
3068
|
+
return await this._refreshSession(currentSession);
|
|
2989
3069
|
}
|
|
2990
3070
|
async _refreshSession(currentSession) {
|
|
2991
3071
|
try {
|
|
@@ -3123,7 +3203,7 @@ class GoTrueClient {
|
|
|
3123
3203
|
if (typeof this.detectSessionInUrl === 'function') {
|
|
3124
3204
|
return this.detectSessionInUrl(new URL(window.location.href), params);
|
|
3125
3205
|
}
|
|
3126
|
-
return Boolean(params.access_token || params.error_description);
|
|
3206
|
+
return Boolean(params.access_token || params.error || params.error_description || params.error_code);
|
|
3127
3207
|
}
|
|
3128
3208
|
/**
|
|
3129
3209
|
* Checks if the current URL and backing storage contain parameters given by a PKCE flow
|
|
@@ -3175,9 +3255,13 @@ class GoTrueClient {
|
|
|
3175
3255
|
*/
|
|
3176
3256
|
async signOut(options = { scope: 'global' }) {
|
|
3177
3257
|
await this.initializePromise;
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3258
|
+
if (this.lock != null) {
|
|
3259
|
+
// TODO(v3): remove legacy lock path
|
|
3260
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
3261
|
+
return await this._signOut(options);
|
|
3262
|
+
});
|
|
3263
|
+
}
|
|
3264
|
+
return await this._signOut(options);
|
|
3181
3265
|
}
|
|
3182
3266
|
async _signOut({ scope } = { scope: 'global' }) {
|
|
3183
3267
|
return await this._useSession(async (result) => {
|
|
@@ -3213,18 +3297,8 @@ class GoTrueClient {
|
|
|
3213
3297
|
* - Subscribes to important events occurring on the user's session.
|
|
3214
3298
|
* - Use on the frontend/client. It is less useful on the server.
|
|
3215
3299
|
* - 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.
|
|
3216
|
-
* -
|
|
3217
|
-
*
|
|
3218
|
-
* - Limit the number of `await` calls in `async` callbacks.
|
|
3219
|
-
* - 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:
|
|
3220
|
-
* ```js
|
|
3221
|
-
* supabase.auth.onAuthStateChange((event, session) => {
|
|
3222
|
-
* setTimeout(async () => {
|
|
3223
|
-
* // await on other Supabase function here
|
|
3224
|
-
* // this runs right after the callback has finished
|
|
3225
|
-
* }, 0)
|
|
3226
|
-
* })
|
|
3227
|
-
* ```
|
|
3300
|
+
* - Callbacks can be `async` and can safely call other Supabase auth methods (`getUser`, `setSession`, etc.) from inside the callback.
|
|
3301
|
+
* - Keep callbacks quick. Events are awaited in order, so a slow callback delays subsequent events to subscribers in this tab.
|
|
3228
3302
|
* - Emitted events:
|
|
3229
3303
|
* - `INITIAL_SESSION`
|
|
3230
3304
|
* - Emitted right after the Supabase client is constructed and the initial session from storage is loaded.
|
|
@@ -3407,9 +3481,15 @@ class GoTrueClient {
|
|
|
3407
3481
|
this.stateChangeEmitters.set(id, subscription);
|
|
3408
3482
|
(async () => {
|
|
3409
3483
|
await this.initializePromise;
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3484
|
+
if (this.lock != null) {
|
|
3485
|
+
// TODO(v3): remove legacy lock path
|
|
3486
|
+
await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
3487
|
+
this._emitInitialSession(id);
|
|
3488
|
+
});
|
|
3489
|
+
}
|
|
3490
|
+
else {
|
|
3491
|
+
await this._emitInitialSession(id);
|
|
3492
|
+
}
|
|
3413
3493
|
})();
|
|
3414
3494
|
return { data: { subscription } };
|
|
3415
3495
|
}
|
|
@@ -3751,7 +3831,10 @@ class GoTrueClient {
|
|
|
3751
3831
|
* @param refreshToken A valid refresh token that was returned on login.
|
|
3752
3832
|
*/
|
|
3753
3833
|
async _refreshAccessToken(refreshToken) {
|
|
3754
|
-
|
|
3834
|
+
// Refresh tokens are long-lived bearer credentials; do NOT include any
|
|
3835
|
+
// fragment of the token in the debug tag, even when `debug: true` is
|
|
3836
|
+
// enabled (logs may be forwarded to third-party services).
|
|
3837
|
+
const debugName = `#_refreshAccessToken()`;
|
|
3755
3838
|
this._debug(debugName, 'begin');
|
|
3756
3839
|
try {
|
|
3757
3840
|
const startedAt = Date.now();
|
|
@@ -3858,10 +3941,18 @@ class GoTrueClient {
|
|
|
3858
3941
|
if (this.autoRefreshToken && currentSession.refresh_token) {
|
|
3859
3942
|
const { error } = await this._callRefreshToken(currentSession.refresh_token);
|
|
3860
3943
|
if (error) {
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3944
|
+
// AuthRefreshDiscardedError means a concurrent signOut already
|
|
3945
|
+
// cleared storage and fired SIGNED_OUT. Don't run _removeSession
|
|
3946
|
+
// again here, or we'll emit a duplicate SIGNED_OUT.
|
|
3947
|
+
if ((0, errors_1.isAuthRefreshDiscardedError)(error)) {
|
|
3948
|
+
this._debug(debugName, 'refresh discarded by commit guard', error);
|
|
3949
|
+
}
|
|
3950
|
+
else {
|
|
3951
|
+
console.error(error);
|
|
3952
|
+
if (!(0, errors_1.isAuthRetryableFetchError)(error)) {
|
|
3953
|
+
this._debug(debugName, 'refresh failed with a non-retryable error, removing the session', error);
|
|
3954
|
+
await this._removeSession();
|
|
3955
|
+
}
|
|
3865
3956
|
}
|
|
3866
3957
|
}
|
|
3867
3958
|
}
|
|
@@ -3910,16 +4001,69 @@ class GoTrueClient {
|
|
|
3910
4001
|
if (this.refreshingDeferred) {
|
|
3911
4002
|
return this.refreshingDeferred.promise;
|
|
3912
4003
|
}
|
|
3913
|
-
|
|
4004
|
+
// Refresh tokens are long-lived bearer credentials; do NOT include any
|
|
4005
|
+
// fragment of the token in the debug tag, even when `debug: true` is
|
|
4006
|
+
// enabled (logs may be forwarded to third-party services).
|
|
4007
|
+
const debugName = `#_callRefreshToken()`;
|
|
3914
4008
|
this._debug(debugName, 'begin');
|
|
3915
4009
|
try {
|
|
3916
4010
|
this.refreshingDeferred = new helpers_1.Deferred();
|
|
4011
|
+
// Snapshot storage before the fetch. The commit guard discards the
|
|
4012
|
+
// rotated tokens only when a non-null pre-fetch snapshot changed under
|
|
4013
|
+
// us — typical case: a concurrent `signOut` ran `_removeSession`, or
|
|
4014
|
+
// another tab's refresh rewrote the slot. Callers passing
|
|
4015
|
+
// externally-sourced tokens (SSR cookie handoff, multi-account
|
|
4016
|
+
// switching, `setSession`/`refreshSession({ refresh_token })`) may
|
|
4017
|
+
// start from a null snapshot OR from a non-null snapshot whose
|
|
4018
|
+
// refresh_token differs from the one they're hydrating; in both
|
|
4019
|
+
// cases the guard fires only when storage was *modified between
|
|
4020
|
+
// snapshots*, not when the input token disagrees with what's stored.
|
|
4021
|
+
const storedAtStart = (await (0, helpers_1.getItemAsync)(this.storage, this.storageKey));
|
|
3917
4022
|
const { data, error } = await this._refreshAccessToken(refreshToken);
|
|
3918
4023
|
if (error)
|
|
3919
4024
|
throw error;
|
|
3920
4025
|
if (!data.session)
|
|
3921
4026
|
throw new errors_1.AuthSessionMissingError();
|
|
4027
|
+
const storedAfter = (await (0, helpers_1.getItemAsync)(this.storage, this.storageKey));
|
|
4028
|
+
const storageChangedUnderUs = storedAtStart !== null &&
|
|
4029
|
+
(storedAfter === null || storedAfter.refresh_token !== storedAtStart.refresh_token);
|
|
4030
|
+
if (storageChangedUnderUs) {
|
|
4031
|
+
this._debug(debugName, 'commit guard: storage changed since refresh started, discarding rotated tokens', {
|
|
4032
|
+
// Presence indicators only — never log refresh token fragments,
|
|
4033
|
+
// even partial. Logs may be forwarded to third-party services.
|
|
4034
|
+
startedWith: 'present',
|
|
4035
|
+
nowHolds: storedAfter ? 'replaced' : 'cleared',
|
|
4036
|
+
});
|
|
4037
|
+
const discarded = {
|
|
4038
|
+
data: null,
|
|
4039
|
+
error: new errors_1.AuthRefreshDiscardedError(),
|
|
4040
|
+
};
|
|
4041
|
+
this.refreshingDeferred.resolve(discarded);
|
|
4042
|
+
return discarded;
|
|
4043
|
+
}
|
|
4044
|
+
// Second leg of the commit guard: close the TOCTOU window between the
|
|
4045
|
+
// synchronous `storageChangedUnderUs` check and the actual storage
|
|
4046
|
+
// writes inside `_saveSession`. A concurrent `signOut → _removeSession`
|
|
4047
|
+
// can land inside `_saveSession`'s `await setItemAsync(...)` yields and
|
|
4048
|
+
// clear storage just before we overwrite it. Capture the epoch BEFORE
|
|
4049
|
+
// the save and re-check after; if it advanced, undo the write directly
|
|
4050
|
+
// (do NOT call `_removeSession` — that would emit a duplicate
|
|
4051
|
+
// SIGNED_OUT for the concurrent signOut that already fired one).
|
|
4052
|
+
const epochBeforeSave = this._sessionRemovalEpoch;
|
|
3922
4053
|
await this._saveSession(data.session);
|
|
4054
|
+
if (this._sessionRemovalEpoch !== epochBeforeSave) {
|
|
4055
|
+
this._debug(debugName, 'commit guard (post-save): _removeSession ran during _saveSession, undoing write');
|
|
4056
|
+
await (0, helpers_1.removeItemAsync)(this.storage, this.storageKey);
|
|
4057
|
+
if (this.userStorage) {
|
|
4058
|
+
await (0, helpers_1.removeItemAsync)(this.userStorage, this.storageKey + '-user');
|
|
4059
|
+
}
|
|
4060
|
+
const discarded = {
|
|
4061
|
+
data: null,
|
|
4062
|
+
error: new errors_1.AuthRefreshDiscardedError(),
|
|
4063
|
+
};
|
|
4064
|
+
this.refreshingDeferred.resolve(discarded);
|
|
4065
|
+
return discarded;
|
|
4066
|
+
}
|
|
3923
4067
|
await this._notifyAllSubscribers('TOKEN_REFRESHED', data.session);
|
|
3924
4068
|
const result = { data: data.session, error: null };
|
|
3925
4069
|
this.refreshingDeferred.resolve(result);
|
|
@@ -4013,6 +4157,11 @@ class GoTrueClient {
|
|
|
4013
4157
|
}
|
|
4014
4158
|
}
|
|
4015
4159
|
async _removeSession() {
|
|
4160
|
+
// Bump synchronously, BEFORE any `await`, so that `_callRefreshToken`'s
|
|
4161
|
+
// post-save check sees the increment whenever this method has started —
|
|
4162
|
+
// even if it hasn't finished. Pairs with the epoch check in
|
|
4163
|
+
// `_callRefreshToken`. See `_sessionRemovalEpoch` field doc.
|
|
4164
|
+
this._sessionRemovalEpoch += 1;
|
|
4016
4165
|
this._debug('#_removeSession()');
|
|
4017
4166
|
this.suppressGetSessionWarning = false;
|
|
4018
4167
|
await (0, helpers_1.removeItemAsync)(this.storage, this.storageKey);
|
|
@@ -4183,47 +4332,122 @@ class GoTrueClient {
|
|
|
4183
4332
|
this._removeVisibilityChangedCallback();
|
|
4184
4333
|
await this._stopAutoRefresh();
|
|
4185
4334
|
}
|
|
4335
|
+
/**
|
|
4336
|
+
* Tears down the client's background work: stops the auto-refresh interval,
|
|
4337
|
+
* removes the `visibilitychange` listener, closes the cross-tab
|
|
4338
|
+
* `BroadcastChannel`, and clears registered `onAuthStateChange` subscribers.
|
|
4339
|
+
*
|
|
4340
|
+
* Call this from cleanup hooks when the client is being replaced before
|
|
4341
|
+
* its JS realm is destroyed. React Strict Mode and HMR are the common
|
|
4342
|
+
* cases. Any in-flight `fetch` calls continue to completion and may still
|
|
4343
|
+
* write to storage; dispose doesn't abort them or erase storage.
|
|
4344
|
+
*
|
|
4345
|
+
* Lifecycle caveat: because in-flight refreshes are not aborted, a
|
|
4346
|
+
* disposed instance can still persist a rotated session to storage after
|
|
4347
|
+
* `dispose()` returns. A subsequent `createClient` against the same
|
|
4348
|
+
* `storageKey` will pick up that session on its next read. If you need
|
|
4349
|
+
* strict isolation between client lifecycles, await any pending auth
|
|
4350
|
+
* operation before calling `dispose()` (or change the `storageKey` for
|
|
4351
|
+
* the replacement client).
|
|
4352
|
+
*
|
|
4353
|
+
* Safe to call repeatedly.
|
|
4354
|
+
*
|
|
4355
|
+
* @category Auth
|
|
4356
|
+
*
|
|
4357
|
+
* @example Cleanup on React unmount
|
|
4358
|
+
* ```ts
|
|
4359
|
+
* useEffect(() => {
|
|
4360
|
+
* const client = createClient(...)
|
|
4361
|
+
* return () => { client.auth.dispose() }
|
|
4362
|
+
* }, [])
|
|
4363
|
+
* ```
|
|
4364
|
+
*/
|
|
4365
|
+
async dispose() {
|
|
4366
|
+
var _a;
|
|
4367
|
+
this._removeVisibilityChangedCallback();
|
|
4368
|
+
await this._stopAutoRefresh();
|
|
4369
|
+
(_a = this.broadcastChannel) === null || _a === void 0 ? void 0 : _a.close();
|
|
4370
|
+
this.broadcastChannel = null;
|
|
4371
|
+
this.stateChangeEmitters.clear();
|
|
4372
|
+
}
|
|
4186
4373
|
/**
|
|
4187
4374
|
* Runs the auto refresh token tick.
|
|
4188
4375
|
*/
|
|
4189
4376
|
async _autoRefreshTokenTick() {
|
|
4190
4377
|
this._debug('#_autoRefreshTokenTick()', 'begin');
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4378
|
+
if (this.lock != null) {
|
|
4379
|
+
// TODO(v3): remove legacy lock path. Uses `_acquireLock(0, ...)` which
|
|
4380
|
+
// throws `LockAcquireTimeoutError` immediately if the lock is held —
|
|
4381
|
+
// that's the fail-fast skip path that lets the tick bail out instead
|
|
4382
|
+
// of queuing behind a long-running operation.
|
|
4383
|
+
try {
|
|
4384
|
+
await this._acquireLock(0, async () => {
|
|
4195
4385
|
try {
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4386
|
+
const now = Date.now();
|
|
4387
|
+
try {
|
|
4388
|
+
return await this._useSession(async (result) => {
|
|
4389
|
+
const { data: { session }, } = result;
|
|
4390
|
+
if (!session || !session.refresh_token || !session.expires_at) {
|
|
4391
|
+
this._debug('#_autoRefreshTokenTick()', 'no session');
|
|
4392
|
+
return;
|
|
4393
|
+
}
|
|
4394
|
+
const expiresInTicks = Math.floor((session.expires_at * 1000 - now) / constants_1.AUTO_REFRESH_TICK_DURATION_MS);
|
|
4395
|
+
this._debug('#_autoRefreshTokenTick()', `access token expires in ${expiresInTicks} ticks, a tick lasts ${constants_1.AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${constants_1.AUTO_REFRESH_TICK_THRESHOLD} ticks`);
|
|
4396
|
+
if (expiresInTicks <= constants_1.AUTO_REFRESH_TICK_THRESHOLD) {
|
|
4397
|
+
await this._callRefreshToken(session.refresh_token);
|
|
4398
|
+
}
|
|
4399
|
+
});
|
|
4400
|
+
}
|
|
4401
|
+
catch (e) {
|
|
4402
|
+
console.error('Auto refresh tick failed with error. This is likely a transient error.', e);
|
|
4403
|
+
}
|
|
4209
4404
|
}
|
|
4210
|
-
|
|
4211
|
-
|
|
4405
|
+
finally {
|
|
4406
|
+
this._debug('#_autoRefreshTokenTick()', 'end');
|
|
4212
4407
|
}
|
|
4408
|
+
});
|
|
4409
|
+
}
|
|
4410
|
+
catch (e) {
|
|
4411
|
+
if (e instanceof locks_1.LockAcquireTimeoutError) {
|
|
4412
|
+
this._debug('auto refresh token tick lock not available');
|
|
4213
4413
|
}
|
|
4214
|
-
|
|
4215
|
-
|
|
4414
|
+
else {
|
|
4415
|
+
throw e;
|
|
4216
4416
|
}
|
|
4217
|
-
}
|
|
4417
|
+
}
|
|
4418
|
+
return;
|
|
4218
4419
|
}
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4420
|
+
// Lockless default: skip if a refresh is already in flight.
|
|
4421
|
+
// `_callRefreshToken` also dedupes via the same field; this is just a
|
|
4422
|
+
// fast-path skip to avoid an unnecessary storage read.
|
|
4423
|
+
if (this.refreshingDeferred !== null) {
|
|
4424
|
+
this._debug('#_autoRefreshTokenTick()', 'refresh already in flight, skipping');
|
|
4425
|
+
return;
|
|
4426
|
+
}
|
|
4427
|
+
try {
|
|
4428
|
+
const now = Date.now();
|
|
4429
|
+
try {
|
|
4430
|
+
await this._useSession(async (result) => {
|
|
4431
|
+
const { data: { session }, } = result;
|
|
4432
|
+
if (!session || !session.refresh_token || !session.expires_at) {
|
|
4433
|
+
this._debug('#_autoRefreshTokenTick()', 'no session');
|
|
4434
|
+
return;
|
|
4435
|
+
}
|
|
4436
|
+
// session will expire in this many ticks (or has already expired if <= 0)
|
|
4437
|
+
const expiresInTicks = Math.floor((session.expires_at * 1000 - now) / constants_1.AUTO_REFRESH_TICK_DURATION_MS);
|
|
4438
|
+
this._debug('#_autoRefreshTokenTick()', `access token expires in ${expiresInTicks} ticks, a tick lasts ${constants_1.AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${constants_1.AUTO_REFRESH_TICK_THRESHOLD} ticks`);
|
|
4439
|
+
if (expiresInTicks <= constants_1.AUTO_REFRESH_TICK_THRESHOLD) {
|
|
4440
|
+
await this._callRefreshToken(session.refresh_token);
|
|
4441
|
+
}
|
|
4442
|
+
});
|
|
4222
4443
|
}
|
|
4223
|
-
|
|
4224
|
-
|
|
4444
|
+
catch (e) {
|
|
4445
|
+
console.error('Auto refresh tick failed with error. This is likely a transient error.', e);
|
|
4225
4446
|
}
|
|
4226
4447
|
}
|
|
4448
|
+
finally {
|
|
4449
|
+
this._debug('#_autoRefreshTokenTick()', 'end');
|
|
4450
|
+
}
|
|
4227
4451
|
}
|
|
4228
4452
|
/**
|
|
4229
4453
|
* Registers callbacks on the browser / platform, which in-turn run
|
|
@@ -4272,18 +4496,26 @@ class GoTrueClient {
|
|
|
4272
4496
|
if (!calledFromInitialize) {
|
|
4273
4497
|
// called when the visibility has changed, i.e. the browser
|
|
4274
4498
|
// transitioned from hidden -> visible so we need to see if the session
|
|
4275
|
-
// should be recovered
|
|
4276
|
-
// the lock first asynchronously
|
|
4499
|
+
// should be recovered
|
|
4277
4500
|
await this.initializePromise;
|
|
4278
|
-
|
|
4501
|
+
if (this.lock != null) {
|
|
4502
|
+
// TODO(v3): remove legacy lock path
|
|
4503
|
+
await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
4504
|
+
if (document.visibilityState !== 'visible') {
|
|
4505
|
+
this._debug(methodName, 'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting');
|
|
4506
|
+
return;
|
|
4507
|
+
}
|
|
4508
|
+
await this._recoverAndRefresh();
|
|
4509
|
+
});
|
|
4510
|
+
}
|
|
4511
|
+
else {
|
|
4279
4512
|
if (document.visibilityState !== 'visible') {
|
|
4280
|
-
this._debug(methodName, '
|
|
4281
|
-
// visibility has changed while waiting for the lock, abort
|
|
4513
|
+
this._debug(methodName, 'visibilityState is no longer visible, skipping recovery');
|
|
4282
4514
|
return;
|
|
4283
4515
|
}
|
|
4284
4516
|
// recover the session
|
|
4285
4517
|
await this._recoverAndRefresh();
|
|
4286
|
-
}
|
|
4518
|
+
}
|
|
4287
4519
|
}
|
|
4288
4520
|
}
|
|
4289
4521
|
else if (document.visibilityState === 'hidden') {
|
|
@@ -4379,7 +4611,7 @@ class GoTrueClient {
|
|
|
4379
4611
|
}
|
|
4380
4612
|
}
|
|
4381
4613
|
async _verify(params) {
|
|
4382
|
-
|
|
4614
|
+
const run = async () => {
|
|
4383
4615
|
try {
|
|
4384
4616
|
return await this._useSession(async (result) => {
|
|
4385
4617
|
var _a;
|
|
@@ -4413,10 +4645,15 @@ class GoTrueClient {
|
|
|
4413
4645
|
}
|
|
4414
4646
|
throw error;
|
|
4415
4647
|
}
|
|
4416
|
-
}
|
|
4648
|
+
};
|
|
4649
|
+
if (this.lock != null) {
|
|
4650
|
+
// TODO(v3): remove legacy lock path
|
|
4651
|
+
return this._acquireLock(this.lockAcquireTimeout, run);
|
|
4652
|
+
}
|
|
4653
|
+
return run();
|
|
4417
4654
|
}
|
|
4418
4655
|
async _challenge(params) {
|
|
4419
|
-
|
|
4656
|
+
const run = async () => {
|
|
4420
4657
|
try {
|
|
4421
4658
|
return await this._useSession(async (result) => {
|
|
4422
4659
|
var _a;
|
|
@@ -4456,14 +4693,17 @@ class GoTrueClient {
|
|
|
4456
4693
|
}
|
|
4457
4694
|
throw error;
|
|
4458
4695
|
}
|
|
4459
|
-
}
|
|
4696
|
+
};
|
|
4697
|
+
if (this.lock != null) {
|
|
4698
|
+
// TODO(v3): remove legacy lock path
|
|
4699
|
+
return this._acquireLock(this.lockAcquireTimeout, run);
|
|
4700
|
+
}
|
|
4701
|
+
return run();
|
|
4460
4702
|
}
|
|
4461
4703
|
/**
|
|
4462
4704
|
* {@see GoTrueMFAApi#challengeAndVerify}
|
|
4463
4705
|
*/
|
|
4464
4706
|
async _challengeAndVerify(params) {
|
|
4465
|
-
// both _challenge and _verify independently acquire the lock, so no need
|
|
4466
|
-
// to acquire it here
|
|
4467
4707
|
const { data: challengeData, error: challengeError } = await this._challenge({
|
|
4468
4708
|
factorId: params.factorId,
|
|
4469
4709
|
});
|
|
@@ -4481,7 +4721,6 @@ class GoTrueClient {
|
|
|
4481
4721
|
*/
|
|
4482
4722
|
async _listFactors() {
|
|
4483
4723
|
var _a;
|
|
4484
|
-
// use #getUser instead of #_getUser as the former acquires a lock
|
|
4485
4724
|
const { data: { user }, error: userError, } = await this.getUser();
|
|
4486
4725
|
if (userError) {
|
|
4487
4726
|
return { data: null, error: userError };
|
|
@@ -4830,8 +5069,14 @@ class GoTrueClient {
|
|
|
4830
5069
|
}
|
|
4831
5070
|
const { header, payload, signature, raw: { header: rawHeader, payload: rawPayload }, } = (0, helpers_1.decodeJWT)(token);
|
|
4832
5071
|
if (!(options === null || options === void 0 ? void 0 : options.allowExpired)) {
|
|
4833
|
-
// Reject expired JWTs should only happen if jwt argument was passed
|
|
4834
|
-
|
|
5072
|
+
// Reject expired JWTs should only happen if jwt argument was passed.
|
|
5073
|
+
// Rethrow as AuthInvalidJwtError so the outer catch converts it to { data, error }.
|
|
5074
|
+
try {
|
|
5075
|
+
(0, helpers_1.validateExp)(payload.exp);
|
|
5076
|
+
}
|
|
5077
|
+
catch (e) {
|
|
5078
|
+
throw new errors_1.AuthInvalidJwtError(e instanceof Error ? e.message : 'JWT validation failed');
|
|
5079
|
+
}
|
|
4835
5080
|
}
|
|
4836
5081
|
const signingKey = !header.alg ||
|
|
4837
5082
|
header.alg.startsWith('HS') ||
|