@supabase/auth-js 2.107.0-canary.0 → 2.107.0-canary.1
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 +68 -14
- package/dist/main/GoTrueClient.d.ts.map +1 -1
- package/dist/main/GoTrueClient.js +331 -107
- 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/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.js +1 -1
- package/dist/module/GoTrueClient.d.ts +68 -14
- package/dist/module/GoTrueClient.d.ts.map +1 -1
- package/dist/module/GoTrueClient.js +333 -109
- 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/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.js +1 -1
- package/dist/tsconfig.module.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/migrations/lockless-coordination.md +89 -0
- package/package.json +1 -1
- package/src/GoTrueClient.ts +397 -137
- package/src/lib/errors.ts +32 -0
- package/src/lib/locks.ts +29 -34
- package/src/lib/types.ts +16 -27
- package/src/lib/version.ts +1 -1
package/src/GoTrueClient.ts
CHANGED
|
@@ -16,11 +16,13 @@ import {
|
|
|
16
16
|
AuthInvalidTokenResponseError,
|
|
17
17
|
AuthPKCECodeVerifierMissingError,
|
|
18
18
|
AuthPKCEGrantCodeExchangeError,
|
|
19
|
+
AuthRefreshDiscardedError,
|
|
19
20
|
AuthSessionMissingError,
|
|
20
21
|
AuthUnknownError,
|
|
21
22
|
isAuthApiError,
|
|
22
23
|
isAuthError,
|
|
23
24
|
isAuthImplicitGrantRedirectError,
|
|
25
|
+
isAuthRefreshDiscardedError,
|
|
24
26
|
isAuthRetryableFetchError,
|
|
25
27
|
isAuthSessionMissingError,
|
|
26
28
|
} from './lib/errors'
|
|
@@ -195,11 +197,17 @@ const DEFAULT_OPTIONS: Omit<
|
|
|
195
197
|
debug: false,
|
|
196
198
|
hasCustomAuthorizationHeader: false,
|
|
197
199
|
throwOnError: false,
|
|
198
|
-
lockAcquireTimeout: 5000, // 5 seconds
|
|
200
|
+
lockAcquireTimeout: 5000, // 5 seconds. Only used when a custom `lock` is supplied. TODO(v3): remove.
|
|
199
201
|
skipAutoInitialize: false,
|
|
200
202
|
experimental: {},
|
|
201
203
|
}
|
|
202
204
|
|
|
205
|
+
/**
|
|
206
|
+
* No-op lock used internally as a placeholder. Kept so older test setups that
|
|
207
|
+
* inject this exact reference do not break; new code never sees it because
|
|
208
|
+
* `this.lock` stays `null` when no custom lock is supplied (lockless path).
|
|
209
|
+
* TODO(v3): remove with the legacy lock path.
|
|
210
|
+
*/
|
|
203
211
|
async function lockNoOp<R>(name: string, acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
|
|
204
212
|
return await fn()
|
|
205
213
|
}
|
|
@@ -280,6 +288,14 @@ export default class GoTrueClient {
|
|
|
280
288
|
protected autoRefreshTickTimeout: ReturnType<typeof setTimeout> | null = null
|
|
281
289
|
protected visibilityChangedCallback: (() => Promise<any>) | null = null
|
|
282
290
|
protected refreshingDeferred: Deferred<CallRefreshTokenResult> | null = null
|
|
291
|
+
/**
|
|
292
|
+
* Monotonic counter incremented at the top of `_removeSession`, before any
|
|
293
|
+
* `await`. The commit guard inside `_callRefreshToken` captures this value
|
|
294
|
+
* before `_saveSession` and re-checks it after, so a `signOut` that
|
|
295
|
+
* interleaves inside `_saveSession`'s storage-write awaits is still caught
|
|
296
|
+
* (the post-fetch storage snapshot alone misses that window).
|
|
297
|
+
*/
|
|
298
|
+
protected _sessionRemovalEpoch = 0
|
|
283
299
|
/**
|
|
284
300
|
* Keeps track of the async client initialization.
|
|
285
301
|
* When null or not yet resolved the auth state is `unknown`
|
|
@@ -297,10 +313,19 @@ export default class GoTrueClient {
|
|
|
297
313
|
protected hasCustomAuthorizationHeader = false
|
|
298
314
|
protected suppressGetSessionWarning = false
|
|
299
315
|
protected fetch: Fetch
|
|
300
|
-
|
|
316
|
+
/**
|
|
317
|
+
* Custom lock function passed via `settings.lock`. When non-null, every auth
|
|
318
|
+
* operation runs inside `_acquireLock`. When null (the default), the client
|
|
319
|
+
* uses its lockless coordination (refresh single-flight + commit guard).
|
|
320
|
+
* TODO(v3): remove along with the legacy lock path.
|
|
321
|
+
*/
|
|
322
|
+
protected lock: LockFunc | null = null
|
|
301
323
|
protected lockAcquired = false
|
|
302
324
|
protected pendingInLock: Promise<any>[] = []
|
|
303
325
|
protected throwOnError: boolean
|
|
326
|
+
/**
|
|
327
|
+
* Only consulted when a custom `lock` is supplied. TODO(v3): remove.
|
|
328
|
+
*/
|
|
304
329
|
protected lockAcquireTimeout: number
|
|
305
330
|
/**
|
|
306
331
|
* Opt-in flags for experimental features. Defaults to an empty object.
|
|
@@ -371,19 +396,23 @@ export default class GoTrueClient {
|
|
|
371
396
|
this.url = settings.url
|
|
372
397
|
this.headers = settings.headers
|
|
373
398
|
this.fetch = resolveFetch(settings.fetch)
|
|
374
|
-
this.lock = settings.lock || lockNoOp
|
|
375
399
|
this.detectSessionInUrl = settings.detectSessionInUrl
|
|
376
400
|
this.flowType = settings.flowType
|
|
377
401
|
this.hasCustomAuthorizationHeader = settings.hasCustomAuthorizationHeader
|
|
378
402
|
this.throwOnError = settings.throwOnError
|
|
403
|
+
|
|
404
|
+
// Always wire `lockAcquireTimeout` even on the lockless path: consumers
|
|
405
|
+
// (including supabase-js tests) read it off the client to verify option
|
|
406
|
+
// flow-through.
|
|
379
407
|
this.lockAcquireTimeout = settings.lockAcquireTimeout
|
|
380
408
|
|
|
381
|
-
|
|
409
|
+
// TODO(v3): remove. Legacy opt-in path preserved for backwards
|
|
410
|
+
// compatibility with callers passing a custom `lock` (typically React
|
|
411
|
+
// Native `processLock` or Node multi-process setups). When `settings.lock`
|
|
412
|
+
// is null the client uses its lockless coordination — no `navigator.locks`
|
|
413
|
+
// by default, no implicit `processLock`.
|
|
414
|
+
if (settings.lock != null) {
|
|
382
415
|
this.lock = settings.lock
|
|
383
|
-
} else if (this.persistSession && isBrowser() && globalThis?.navigator?.locks) {
|
|
384
|
-
this.lock = navigatorLock
|
|
385
|
-
} else {
|
|
386
|
-
this.lock = lockNoOp
|
|
387
416
|
}
|
|
388
417
|
|
|
389
418
|
if (!this.jwks) {
|
|
@@ -518,9 +547,13 @@ export default class GoTrueClient {
|
|
|
518
547
|
}
|
|
519
548
|
|
|
520
549
|
this.initializePromise = (async () => {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
550
|
+
if (this.lock != null) {
|
|
551
|
+
// TODO(v3): remove legacy lock path
|
|
552
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
553
|
+
return await this._initialize()
|
|
554
|
+
})
|
|
555
|
+
}
|
|
556
|
+
return await this._initialize()
|
|
524
557
|
})()
|
|
525
558
|
|
|
526
559
|
return await this.initializePromise
|
|
@@ -1405,9 +1438,14 @@ export default class GoTrueClient {
|
|
|
1405
1438
|
async exchangeCodeForSession(authCode: string): Promise<AuthTokenResponse> {
|
|
1406
1439
|
await this.initializePromise
|
|
1407
1440
|
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1441
|
+
if (this.lock != null) {
|
|
1442
|
+
// TODO(v3): remove legacy lock path
|
|
1443
|
+
return this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
1444
|
+
return this._exchangeCodeForSession(authCode)
|
|
1445
|
+
})
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
return this._exchangeCodeForSession(authCode)
|
|
1411
1449
|
}
|
|
1412
1450
|
|
|
1413
1451
|
/**
|
|
@@ -2444,9 +2482,14 @@ export default class GoTrueClient {
|
|
|
2444
2482
|
async reauthenticate(): Promise<AuthResponse> {
|
|
2445
2483
|
await this.initializePromise
|
|
2446
2484
|
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2485
|
+
if (this.lock != null) {
|
|
2486
|
+
// TODO(v3): remove legacy lock path
|
|
2487
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
2488
|
+
return await this._reauthenticate()
|
|
2489
|
+
})
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
return await this._reauthenticate()
|
|
2450
2493
|
}
|
|
2451
2494
|
|
|
2452
2495
|
private async _reauthenticate(): Promise<AuthResponse> {
|
|
@@ -2593,7 +2636,7 @@ export default class GoTrueClient {
|
|
|
2593
2636
|
* - If the session's access token is expired or is about to expire, this method will use the refresh token to refresh the session.
|
|
2594
2637
|
* - 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.
|
|
2595
2638
|
* - **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.
|
|
2596
|
-
* -
|
|
2639
|
+
* - 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.
|
|
2597
2640
|
*
|
|
2598
2641
|
* @example Get the session data
|
|
2599
2642
|
* ```js
|
|
@@ -2661,17 +2704,26 @@ export default class GoTrueClient {
|
|
|
2661
2704
|
async getSession() {
|
|
2662
2705
|
await this.initializePromise
|
|
2663
2706
|
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2707
|
+
if (this.lock != null) {
|
|
2708
|
+
// TODO(v3): remove legacy lock path
|
|
2709
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
2710
|
+
return this._useSession(async (result) => {
|
|
2711
|
+
return result
|
|
2712
|
+
})
|
|
2667
2713
|
})
|
|
2668
|
-
}
|
|
2714
|
+
}
|
|
2669
2715
|
|
|
2670
|
-
return result
|
|
2716
|
+
return await this._useSession(async (result) => {
|
|
2717
|
+
return result
|
|
2718
|
+
})
|
|
2671
2719
|
}
|
|
2672
2720
|
|
|
2673
2721
|
/**
|
|
2674
2722
|
* Acquires a global lock based on the storage key.
|
|
2723
|
+
*
|
|
2724
|
+
* TODO(v3): remove along with the legacy lock path. Only called when
|
|
2725
|
+
* `this.lock` is non-null (custom lock supplied via constructor). The
|
|
2726
|
+
* default lockless path bypasses this entirely.
|
|
2675
2727
|
*/
|
|
2676
2728
|
private async _acquireLock<R>(acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
|
|
2677
2729
|
this._debug('#_acquireLock', 'begin', acquireTimeout)
|
|
@@ -2700,7 +2752,7 @@ export default class GoTrueClient {
|
|
|
2700
2752
|
return result
|
|
2701
2753
|
}
|
|
2702
2754
|
|
|
2703
|
-
return await this.lock(`lock:${this.storageKey}`, acquireTimeout, async () => {
|
|
2755
|
+
return await this.lock!(`lock:${this.storageKey}`, acquireTimeout, async () => {
|
|
2704
2756
|
this._debug('#_acquireLock', 'lock acquired for storage key', this.storageKey)
|
|
2705
2757
|
|
|
2706
2758
|
try {
|
|
@@ -2742,10 +2794,9 @@ export default class GoTrueClient {
|
|
|
2742
2794
|
}
|
|
2743
2795
|
|
|
2744
2796
|
/**
|
|
2745
|
-
* Use instead of {@link #getSession} inside the library.
|
|
2746
|
-
*
|
|
2747
|
-
*
|
|
2748
|
-
* session at once across multiple tabs or processes.
|
|
2797
|
+
* Use instead of {@link #getSession} inside the library. Loads the session
|
|
2798
|
+
* via `__loadSession` (which may trigger a refresh if the access token is
|
|
2799
|
+
* within the expiry margin) and runs `fn` with the result.
|
|
2749
2800
|
*/
|
|
2750
2801
|
private async _useSession<R>(
|
|
2751
2802
|
fn: (
|
|
@@ -2773,7 +2824,10 @@ export default class GoTrueClient {
|
|
|
2773
2824
|
this._debug('#_useSession', 'begin')
|
|
2774
2825
|
|
|
2775
2826
|
try {
|
|
2776
|
-
//
|
|
2827
|
+
// Concurrent callers may both reach __loadSession; storage reads are
|
|
2828
|
+
// idempotent, and the only write path inside it (refresh) is
|
|
2829
|
+
// single-flighted downstream by `refreshingDeferred` in
|
|
2830
|
+
// `_callRefreshToken`. No serialization is needed at this layer.
|
|
2777
2831
|
const result = await this.__loadSession()
|
|
2778
2832
|
|
|
2779
2833
|
return await fn(result)
|
|
@@ -2809,7 +2863,8 @@ export default class GoTrueClient {
|
|
|
2809
2863
|
> {
|
|
2810
2864
|
this._debug('#__loadSession()', 'begin')
|
|
2811
2865
|
|
|
2812
|
-
if (!this.lockAcquired) {
|
|
2866
|
+
if (this.lock != null && !this.lockAcquired) {
|
|
2867
|
+
// TODO(v3): remove. Only meaningful on the legacy lock path.
|
|
2813
2868
|
this._debug('#__loadSession()', 'used outside of an acquired lock!', new Error().stack)
|
|
2814
2869
|
}
|
|
2815
2870
|
|
|
@@ -2976,9 +3031,15 @@ export default class GoTrueClient {
|
|
|
2976
3031
|
|
|
2977
3032
|
await this.initializePromise
|
|
2978
3033
|
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
3034
|
+
let result: UserResponse
|
|
3035
|
+
if (this.lock != null) {
|
|
3036
|
+
// TODO(v3): remove legacy lock path
|
|
3037
|
+
result = await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
3038
|
+
return await this._getUser()
|
|
3039
|
+
})
|
|
3040
|
+
} else {
|
|
3041
|
+
result = await this._getUser()
|
|
3042
|
+
}
|
|
2982
3043
|
|
|
2983
3044
|
if (result.data.user) {
|
|
2984
3045
|
this.suppressGetSessionWarning = true
|
|
@@ -3153,9 +3214,14 @@ export default class GoTrueClient {
|
|
|
3153
3214
|
): Promise<UserResponse> {
|
|
3154
3215
|
await this.initializePromise
|
|
3155
3216
|
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3217
|
+
if (this.lock != null) {
|
|
3218
|
+
// TODO(v3): remove legacy lock path
|
|
3219
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
3220
|
+
return await this._updateUser(attributes, options)
|
|
3221
|
+
})
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
return await this._updateUser(attributes, options)
|
|
3159
3225
|
}
|
|
3160
3226
|
|
|
3161
3227
|
protected async _updateUser(
|
|
@@ -3342,9 +3408,14 @@ export default class GoTrueClient {
|
|
|
3342
3408
|
}): Promise<AuthResponse> {
|
|
3343
3409
|
await this.initializePromise
|
|
3344
3410
|
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3411
|
+
if (this.lock != null) {
|
|
3412
|
+
// TODO(v3): remove legacy lock path
|
|
3413
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
3414
|
+
return await this._setSession(currentSession)
|
|
3415
|
+
})
|
|
3416
|
+
}
|
|
3417
|
+
|
|
3418
|
+
return await this._setSession(currentSession)
|
|
3348
3419
|
}
|
|
3349
3420
|
|
|
3350
3421
|
protected async _setSession(currentSession: {
|
|
@@ -3533,9 +3604,14 @@ export default class GoTrueClient {
|
|
|
3533
3604
|
async refreshSession(currentSession?: { refresh_token: string }): Promise<AuthResponse> {
|
|
3534
3605
|
await this.initializePromise
|
|
3535
3606
|
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3607
|
+
if (this.lock != null) {
|
|
3608
|
+
// TODO(v3): remove legacy lock path
|
|
3609
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
3610
|
+
return await this._refreshSession(currentSession)
|
|
3611
|
+
})
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
return await this._refreshSession(currentSession)
|
|
3539
3615
|
}
|
|
3540
3616
|
|
|
3541
3617
|
protected async _refreshSession(currentSession?: {
|
|
@@ -3785,9 +3861,14 @@ export default class GoTrueClient {
|
|
|
3785
3861
|
async signOut(options: SignOut = { scope: 'global' }): Promise<{ error: AuthError | null }> {
|
|
3786
3862
|
await this.initializePromise
|
|
3787
3863
|
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3864
|
+
if (this.lock != null) {
|
|
3865
|
+
// TODO(v3): remove legacy lock path
|
|
3866
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
3867
|
+
return await this._signOut(options)
|
|
3868
|
+
})
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
return await this._signOut(options)
|
|
3791
3872
|
}
|
|
3792
3873
|
|
|
3793
3874
|
protected async _signOut(
|
|
@@ -3834,16 +3915,19 @@ export default class GoTrueClient {
|
|
|
3834
3915
|
}
|
|
3835
3916
|
|
|
3836
3917
|
/**
|
|
3837
|
-
*
|
|
3838
|
-
*
|
|
3839
|
-
*
|
|
3840
|
-
*
|
|
3841
|
-
*
|
|
3842
|
-
*
|
|
3843
|
-
*
|
|
3918
|
+
* Receive a notification every time an auth event happens. Common reentry
|
|
3919
|
+
* patterns (`getUser`, `setSession`, reading the session from inside a
|
|
3920
|
+
* handler) complete normally. One hazard remains: calling `refreshSession`
|
|
3921
|
+
* (or anything that routes through `_callRefreshToken`) from inside a
|
|
3922
|
+
* `TOKEN_REFRESHED` handler. `refreshingDeferred` resolves only after
|
|
3923
|
+
* `_notifyAllSubscribers` returns, so the inner refresh dedupes onto the
|
|
3924
|
+
* outer's unresolved promise and the two wait on each other.
|
|
3844
3925
|
*
|
|
3845
3926
|
* @param callback A callback function to be invoked when an auth event happens.
|
|
3846
|
-
*
|
|
3927
|
+
*
|
|
3928
|
+
* @deprecated Async callbacks can deadlock when they trigger a nested
|
|
3929
|
+
* refresh from a `TOKEN_REFRESHED` event. Prefer the sync overload, or move
|
|
3930
|
+
* refresh-triggering work outside the callback.
|
|
3847
3931
|
*/
|
|
3848
3932
|
onAuthStateChange(callback: (event: AuthChangeEvent, session: Session | null) => Promise<void>): {
|
|
3849
3933
|
data: { subscription: Subscription }
|
|
@@ -3856,18 +3940,8 @@ export default class GoTrueClient {
|
|
|
3856
3940
|
* - Subscribes to important events occurring on the user's session.
|
|
3857
3941
|
* - Use on the frontend/client. It is less useful on the server.
|
|
3858
3942
|
* - 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.
|
|
3859
|
-
* -
|
|
3860
|
-
*
|
|
3861
|
-
* - Limit the number of `await` calls in `async` callbacks.
|
|
3862
|
-
* - 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:
|
|
3863
|
-
* ```js
|
|
3864
|
-
* supabase.auth.onAuthStateChange((event, session) => {
|
|
3865
|
-
* setTimeout(async () => {
|
|
3866
|
-
* // await on other Supabase function here
|
|
3867
|
-
* // this runs right after the callback has finished
|
|
3868
|
-
* }, 0)
|
|
3869
|
-
* })
|
|
3870
|
-
* ```
|
|
3943
|
+
* - Callbacks can be `async` and can safely call other Supabase auth methods (`getUser`, `setSession`, etc.) from inside the callback.
|
|
3944
|
+
* - Keep callbacks quick. Events are awaited in order, so a slow callback delays subsequent events to subscribers in this tab.
|
|
3871
3945
|
* - Emitted events:
|
|
3872
3946
|
* - `INITIAL_SESSION`
|
|
3873
3947
|
* - Emitted right after the Supabase client is constructed and the initial session from storage is loaded.
|
|
@@ -4058,9 +4132,14 @@ export default class GoTrueClient {
|
|
|
4058
4132
|
;(async () => {
|
|
4059
4133
|
await this.initializePromise
|
|
4060
4134
|
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4135
|
+
if (this.lock != null) {
|
|
4136
|
+
// TODO(v3): remove legacy lock path
|
|
4137
|
+
await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
4138
|
+
this._emitInitialSession(id)
|
|
4139
|
+
})
|
|
4140
|
+
} else {
|
|
4141
|
+
await this._emitInitialSession(id)
|
|
4142
|
+
}
|
|
4064
4143
|
})()
|
|
4065
4144
|
|
|
4066
4145
|
return { data: { subscription } }
|
|
@@ -4455,7 +4534,10 @@ export default class GoTrueClient {
|
|
|
4455
4534
|
* @param refreshToken A valid refresh token that was returned on login.
|
|
4456
4535
|
*/
|
|
4457
4536
|
private async _refreshAccessToken(refreshToken: string): Promise<AuthResponse> {
|
|
4458
|
-
|
|
4537
|
+
// Refresh tokens are long-lived bearer credentials; do NOT include any
|
|
4538
|
+
// fragment of the token in the debug tag, even when `debug: true` is
|
|
4539
|
+
// enabled (logs may be forwarded to third-party services).
|
|
4540
|
+
const debugName = `#_refreshAccessToken()`
|
|
4459
4541
|
this._debug(debugName, 'begin')
|
|
4460
4542
|
|
|
4461
4543
|
try {
|
|
@@ -4608,15 +4690,22 @@ export default class GoTrueClient {
|
|
|
4608
4690
|
const { error } = await this._callRefreshToken(currentSession.refresh_token)
|
|
4609
4691
|
|
|
4610
4692
|
if (error) {
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4693
|
+
// AuthRefreshDiscardedError means a concurrent signOut already
|
|
4694
|
+
// cleared storage and fired SIGNED_OUT. Don't run _removeSession
|
|
4695
|
+
// again here, or we'll emit a duplicate SIGNED_OUT.
|
|
4696
|
+
if (isAuthRefreshDiscardedError(error)) {
|
|
4697
|
+
this._debug(debugName, 'refresh discarded by commit guard', error)
|
|
4698
|
+
} else {
|
|
4699
|
+
console.error(error)
|
|
4700
|
+
|
|
4701
|
+
if (!isAuthRetryableFetchError(error)) {
|
|
4702
|
+
this._debug(
|
|
4703
|
+
debugName,
|
|
4704
|
+
'refresh failed with a non-retryable error, removing the session',
|
|
4705
|
+
error
|
|
4706
|
+
)
|
|
4707
|
+
await this._removeSession()
|
|
4708
|
+
}
|
|
4620
4709
|
}
|
|
4621
4710
|
}
|
|
4622
4711
|
}
|
|
@@ -4669,18 +4758,85 @@ export default class GoTrueClient {
|
|
|
4669
4758
|
return this.refreshingDeferred.promise
|
|
4670
4759
|
}
|
|
4671
4760
|
|
|
4672
|
-
|
|
4761
|
+
// Refresh tokens are long-lived bearer credentials; do NOT include any
|
|
4762
|
+
// fragment of the token in the debug tag, even when `debug: true` is
|
|
4763
|
+
// enabled (logs may be forwarded to third-party services).
|
|
4764
|
+
const debugName = `#_callRefreshToken()`
|
|
4673
4765
|
|
|
4674
4766
|
this._debug(debugName, 'begin')
|
|
4675
4767
|
|
|
4676
4768
|
try {
|
|
4677
4769
|
this.refreshingDeferred = new Deferred<CallRefreshTokenResult>()
|
|
4678
4770
|
|
|
4771
|
+
// Snapshot storage before the fetch. The commit guard discards the
|
|
4772
|
+
// rotated tokens only when a non-null pre-fetch snapshot changed under
|
|
4773
|
+
// us — typical case: a concurrent `signOut` ran `_removeSession`, or
|
|
4774
|
+
// another tab's refresh rewrote the slot. Callers passing
|
|
4775
|
+
// externally-sourced tokens (SSR cookie handoff, multi-account
|
|
4776
|
+
// switching, `setSession`/`refreshSession({ refresh_token })`) may
|
|
4777
|
+
// start from a null snapshot OR from a non-null snapshot whose
|
|
4778
|
+
// refresh_token differs from the one they're hydrating; in both
|
|
4779
|
+
// cases the guard fires only when storage was *modified between
|
|
4780
|
+
// snapshots*, not when the input token disagrees with what's stored.
|
|
4781
|
+
const storedAtStart = (await getItemAsync(this.storage, this.storageKey)) as Session | null
|
|
4782
|
+
|
|
4679
4783
|
const { data, error } = await this._refreshAccessToken(refreshToken)
|
|
4680
4784
|
if (error) throw error
|
|
4681
4785
|
if (!data.session) throw new AuthSessionMissingError()
|
|
4682
4786
|
|
|
4787
|
+
const storedAfter = (await getItemAsync(this.storage, this.storageKey)) as Session | null
|
|
4788
|
+
const storageChangedUnderUs =
|
|
4789
|
+
storedAtStart !== null &&
|
|
4790
|
+
(storedAfter === null || storedAfter.refresh_token !== storedAtStart.refresh_token)
|
|
4791
|
+
|
|
4792
|
+
if (storageChangedUnderUs) {
|
|
4793
|
+
this._debug(
|
|
4794
|
+
debugName,
|
|
4795
|
+
'commit guard: storage changed since refresh started, discarding rotated tokens',
|
|
4796
|
+
{
|
|
4797
|
+
// Presence indicators only — never log refresh token fragments,
|
|
4798
|
+
// even partial. Logs may be forwarded to third-party services.
|
|
4799
|
+
startedWith: 'present',
|
|
4800
|
+
nowHolds: storedAfter ? 'replaced' : 'cleared',
|
|
4801
|
+
}
|
|
4802
|
+
)
|
|
4803
|
+
const discarded: CallRefreshTokenResult = {
|
|
4804
|
+
data: null,
|
|
4805
|
+
error: new AuthRefreshDiscardedError(),
|
|
4806
|
+
}
|
|
4807
|
+
this.refreshingDeferred.resolve(discarded)
|
|
4808
|
+
return discarded
|
|
4809
|
+
}
|
|
4810
|
+
|
|
4811
|
+
// Second leg of the commit guard: close the TOCTOU window between the
|
|
4812
|
+
// synchronous `storageChangedUnderUs` check and the actual storage
|
|
4813
|
+
// writes inside `_saveSession`. A concurrent `signOut → _removeSession`
|
|
4814
|
+
// can land inside `_saveSession`'s `await setItemAsync(...)` yields and
|
|
4815
|
+
// clear storage just before we overwrite it. Capture the epoch BEFORE
|
|
4816
|
+
// the save and re-check after; if it advanced, undo the write directly
|
|
4817
|
+
// (do NOT call `_removeSession` — that would emit a duplicate
|
|
4818
|
+
// SIGNED_OUT for the concurrent signOut that already fired one).
|
|
4819
|
+
const epochBeforeSave = this._sessionRemovalEpoch
|
|
4820
|
+
|
|
4683
4821
|
await this._saveSession(data.session)
|
|
4822
|
+
|
|
4823
|
+
if (this._sessionRemovalEpoch !== epochBeforeSave) {
|
|
4824
|
+
this._debug(
|
|
4825
|
+
debugName,
|
|
4826
|
+
'commit guard (post-save): _removeSession ran during _saveSession, undoing write'
|
|
4827
|
+
)
|
|
4828
|
+
await removeItemAsync(this.storage, this.storageKey)
|
|
4829
|
+
if (this.userStorage) {
|
|
4830
|
+
await removeItemAsync(this.userStorage, this.storageKey + '-user')
|
|
4831
|
+
}
|
|
4832
|
+
const discarded: CallRefreshTokenResult = {
|
|
4833
|
+
data: null,
|
|
4834
|
+
error: new AuthRefreshDiscardedError(),
|
|
4835
|
+
}
|
|
4836
|
+
this.refreshingDeferred.resolve(discarded)
|
|
4837
|
+
return discarded
|
|
4838
|
+
}
|
|
4839
|
+
|
|
4684
4840
|
await this._notifyAllSubscribers('TOKEN_REFRESHED', data.session)
|
|
4685
4841
|
|
|
4686
4842
|
const result = { data: data.session, error: null }
|
|
@@ -4792,6 +4948,11 @@ export default class GoTrueClient {
|
|
|
4792
4948
|
}
|
|
4793
4949
|
|
|
4794
4950
|
private async _removeSession() {
|
|
4951
|
+
// Bump synchronously, BEFORE any `await`, so that `_callRefreshToken`'s
|
|
4952
|
+
// post-save check sees the increment whenever this method has started —
|
|
4953
|
+
// even if it hasn't finished. Pairs with the epoch check in
|
|
4954
|
+
// `_callRefreshToken`. See `_sessionRemovalEpoch` field doc.
|
|
4955
|
+
this._sessionRemovalEpoch += 1
|
|
4795
4956
|
this._debug('#_removeSession()')
|
|
4796
4957
|
|
|
4797
4958
|
this.suppressGetSessionWarning = false
|
|
@@ -4980,58 +5141,144 @@ export default class GoTrueClient {
|
|
|
4980
5141
|
await this._stopAutoRefresh()
|
|
4981
5142
|
}
|
|
4982
5143
|
|
|
5144
|
+
/**
|
|
5145
|
+
* Tears down the client's background work: stops the auto-refresh interval,
|
|
5146
|
+
* removes the `visibilitychange` listener, closes the cross-tab
|
|
5147
|
+
* `BroadcastChannel`, and clears registered `onAuthStateChange` subscribers.
|
|
5148
|
+
*
|
|
5149
|
+
* Call this from cleanup hooks when the client is being replaced before
|
|
5150
|
+
* its JS realm is destroyed. React Strict Mode and HMR are the common
|
|
5151
|
+
* cases. Any in-flight `fetch` calls continue to completion and may still
|
|
5152
|
+
* write to storage; dispose doesn't abort them or erase storage.
|
|
5153
|
+
*
|
|
5154
|
+
* Lifecycle caveat: because in-flight refreshes are not aborted, a
|
|
5155
|
+
* disposed instance can still persist a rotated session to storage after
|
|
5156
|
+
* `dispose()` returns. A subsequent `createClient` against the same
|
|
5157
|
+
* `storageKey` will pick up that session on its next read. If you need
|
|
5158
|
+
* strict isolation between client lifecycles, await any pending auth
|
|
5159
|
+
* operation before calling `dispose()` (or change the `storageKey` for
|
|
5160
|
+
* the replacement client).
|
|
5161
|
+
*
|
|
5162
|
+
* Safe to call repeatedly.
|
|
5163
|
+
*
|
|
5164
|
+
* @category Auth
|
|
5165
|
+
*
|
|
5166
|
+
* @example Cleanup on React unmount
|
|
5167
|
+
* ```ts
|
|
5168
|
+
* useEffect(() => {
|
|
5169
|
+
* const client = createClient(...)
|
|
5170
|
+
* return () => { client.auth.dispose() }
|
|
5171
|
+
* }, [])
|
|
5172
|
+
* ```
|
|
5173
|
+
*/
|
|
5174
|
+
async dispose(): Promise<void> {
|
|
5175
|
+
this._removeVisibilityChangedCallback()
|
|
5176
|
+
await this._stopAutoRefresh()
|
|
5177
|
+
this.broadcastChannel?.close()
|
|
5178
|
+
this.broadcastChannel = null
|
|
5179
|
+
this.stateChangeEmitters.clear()
|
|
5180
|
+
}
|
|
5181
|
+
|
|
4983
5182
|
/**
|
|
4984
5183
|
* Runs the auto refresh token tick.
|
|
4985
5184
|
*/
|
|
4986
5185
|
private async _autoRefreshTokenTick() {
|
|
4987
5186
|
this._debug('#_autoRefreshTokenTick()', 'begin')
|
|
4988
5187
|
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
5188
|
+
if (this.lock != null) {
|
|
5189
|
+
// TODO(v3): remove legacy lock path. Uses `_acquireLock(0, ...)` which
|
|
5190
|
+
// throws `LockAcquireTimeoutError` immediately if the lock is held —
|
|
5191
|
+
// that's the fail-fast skip path that lets the tick bail out instead
|
|
5192
|
+
// of queuing behind a long-running operation.
|
|
5193
|
+
try {
|
|
5194
|
+
await this._acquireLock(0, async () => {
|
|
4994
5195
|
try {
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5196
|
+
const now = Date.now()
|
|
5197
|
+
try {
|
|
5198
|
+
return await this._useSession(async (result) => {
|
|
5199
|
+
const {
|
|
5200
|
+
data: { session },
|
|
5201
|
+
} = result
|
|
5202
|
+
|
|
5203
|
+
if (!session || !session.refresh_token || !session.expires_at) {
|
|
5204
|
+
this._debug('#_autoRefreshTokenTick()', 'no session')
|
|
5205
|
+
return
|
|
5206
|
+
}
|
|
5004
5207
|
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
)
|
|
5208
|
+
const expiresInTicks = Math.floor(
|
|
5209
|
+
(session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS
|
|
5210
|
+
)
|
|
5009
5211
|
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5212
|
+
this._debug(
|
|
5213
|
+
'#_autoRefreshTokenTick()',
|
|
5214
|
+
`access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks`
|
|
5215
|
+
)
|
|
5014
5216
|
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5217
|
+
if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) {
|
|
5218
|
+
await this._callRefreshToken(session.refresh_token)
|
|
5219
|
+
}
|
|
5220
|
+
})
|
|
5221
|
+
} catch (e) {
|
|
5222
|
+
console.error(
|
|
5223
|
+
'Auto refresh tick failed with error. This is likely a transient error.',
|
|
5224
|
+
e
|
|
5225
|
+
)
|
|
5226
|
+
}
|
|
5227
|
+
} finally {
|
|
5228
|
+
this._debug('#_autoRefreshTokenTick()', 'end')
|
|
5024
5229
|
}
|
|
5025
|
-
}
|
|
5026
|
-
|
|
5230
|
+
})
|
|
5231
|
+
} catch (e) {
|
|
5232
|
+
if (e instanceof LockAcquireTimeoutError) {
|
|
5233
|
+
this._debug('auto refresh token tick lock not available')
|
|
5234
|
+
} else {
|
|
5235
|
+
throw e
|
|
5027
5236
|
}
|
|
5028
|
-
})
|
|
5029
|
-
} catch (e) {
|
|
5030
|
-
if (e instanceof LockAcquireTimeoutError) {
|
|
5031
|
-
this._debug('auto refresh token tick lock not available')
|
|
5032
|
-
} else {
|
|
5033
|
-
throw e
|
|
5034
5237
|
}
|
|
5238
|
+
return
|
|
5239
|
+
}
|
|
5240
|
+
|
|
5241
|
+
// Lockless default: skip if a refresh is already in flight.
|
|
5242
|
+
// `_callRefreshToken` also dedupes via the same field; this is just a
|
|
5243
|
+
// fast-path skip to avoid an unnecessary storage read.
|
|
5244
|
+
if (this.refreshingDeferred !== null) {
|
|
5245
|
+
this._debug('#_autoRefreshTokenTick()', 'refresh already in flight, skipping')
|
|
5246
|
+
return
|
|
5247
|
+
}
|
|
5248
|
+
|
|
5249
|
+
try {
|
|
5250
|
+
const now = Date.now()
|
|
5251
|
+
|
|
5252
|
+
try {
|
|
5253
|
+
await this._useSession(async (result) => {
|
|
5254
|
+
const {
|
|
5255
|
+
data: { session },
|
|
5256
|
+
} = result
|
|
5257
|
+
|
|
5258
|
+
if (!session || !session.refresh_token || !session.expires_at) {
|
|
5259
|
+
this._debug('#_autoRefreshTokenTick()', 'no session')
|
|
5260
|
+
return
|
|
5261
|
+
}
|
|
5262
|
+
|
|
5263
|
+
// session will expire in this many ticks (or has already expired if <= 0)
|
|
5264
|
+
const expiresInTicks = Math.floor(
|
|
5265
|
+
(session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS
|
|
5266
|
+
)
|
|
5267
|
+
|
|
5268
|
+
this._debug(
|
|
5269
|
+
'#_autoRefreshTokenTick()',
|
|
5270
|
+
`access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks`
|
|
5271
|
+
)
|
|
5272
|
+
|
|
5273
|
+
if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) {
|
|
5274
|
+
await this._callRefreshToken(session.refresh_token)
|
|
5275
|
+
}
|
|
5276
|
+
})
|
|
5277
|
+
} catch (e) {
|
|
5278
|
+
console.error('Auto refresh tick failed with error. This is likely a transient error.', e)
|
|
5279
|
+
}
|
|
5280
|
+
} finally {
|
|
5281
|
+
this._debug('#_autoRefreshTokenTick()', 'end')
|
|
5035
5282
|
}
|
|
5036
5283
|
}
|
|
5037
5284
|
|
|
@@ -5088,24 +5335,29 @@ export default class GoTrueClient {
|
|
|
5088
5335
|
if (!calledFromInitialize) {
|
|
5089
5336
|
// called when the visibility has changed, i.e. the browser
|
|
5090
5337
|
// transitioned from hidden -> visible so we need to see if the session
|
|
5091
|
-
// should be recovered
|
|
5092
|
-
// the lock first asynchronously
|
|
5338
|
+
// should be recovered
|
|
5093
5339
|
await this.initializePromise
|
|
5094
5340
|
|
|
5095
|
-
|
|
5341
|
+
if (this.lock != null) {
|
|
5342
|
+
// TODO(v3): remove legacy lock path
|
|
5343
|
+
await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
5344
|
+
if (document.visibilityState !== 'visible') {
|
|
5345
|
+
this._debug(
|
|
5346
|
+
methodName,
|
|
5347
|
+
'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting'
|
|
5348
|
+
)
|
|
5349
|
+
return
|
|
5350
|
+
}
|
|
5351
|
+
await this._recoverAndRefresh()
|
|
5352
|
+
})
|
|
5353
|
+
} else {
|
|
5096
5354
|
if (document.visibilityState !== 'visible') {
|
|
5097
|
-
this._debug(
|
|
5098
|
-
methodName,
|
|
5099
|
-
'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting'
|
|
5100
|
-
)
|
|
5101
|
-
|
|
5102
|
-
// visibility has changed while waiting for the lock, abort
|
|
5355
|
+
this._debug(methodName, 'visibilityState is no longer visible, skipping recovery')
|
|
5103
5356
|
return
|
|
5104
5357
|
}
|
|
5105
|
-
|
|
5106
5358
|
// recover the session
|
|
5107
5359
|
await this._recoverAndRefresh()
|
|
5108
|
-
}
|
|
5360
|
+
}
|
|
5109
5361
|
}
|
|
5110
5362
|
} else if (document.visibilityState === 'hidden') {
|
|
5111
5363
|
if (this.autoRefreshToken) {
|
|
@@ -5237,7 +5489,7 @@ export default class GoTrueClient {
|
|
|
5237
5489
|
params: MFAVerifyWebauthnParams<T>
|
|
5238
5490
|
): Promise<AuthMFAVerifyResponse>
|
|
5239
5491
|
private async _verify(params: MFAVerifyParams): Promise<AuthMFAVerifyResponse> {
|
|
5240
|
-
|
|
5492
|
+
const run = async (): Promise<AuthMFAVerifyResponse> => {
|
|
5241
5493
|
try {
|
|
5242
5494
|
return await this._useSession(async (result) => {
|
|
5243
5495
|
const { data: sessionData, error: sessionError } = result
|
|
@@ -5308,7 +5560,13 @@ export default class GoTrueClient {
|
|
|
5308
5560
|
}
|
|
5309
5561
|
throw error
|
|
5310
5562
|
}
|
|
5311
|
-
}
|
|
5563
|
+
}
|
|
5564
|
+
|
|
5565
|
+
if (this.lock != null) {
|
|
5566
|
+
// TODO(v3): remove legacy lock path
|
|
5567
|
+
return this._acquireLock(this.lockAcquireTimeout, run)
|
|
5568
|
+
}
|
|
5569
|
+
return run()
|
|
5312
5570
|
}
|
|
5313
5571
|
|
|
5314
5572
|
/**
|
|
@@ -5324,7 +5582,7 @@ export default class GoTrueClient {
|
|
|
5324
5582
|
params: MFAChallengeWebauthnParams
|
|
5325
5583
|
): Promise<Prettify<AuthMFAChallengeWebauthnResponse>>
|
|
5326
5584
|
private async _challenge(params: MFAChallengeParams): Promise<AuthMFAChallengeResponse> {
|
|
5327
|
-
|
|
5585
|
+
const run = async (): Promise<AuthMFAChallengeResponse> => {
|
|
5328
5586
|
try {
|
|
5329
5587
|
return await this._useSession(async (result) => {
|
|
5330
5588
|
const { data: sessionData, error: sessionError } = result
|
|
@@ -5397,7 +5655,13 @@ export default class GoTrueClient {
|
|
|
5397
5655
|
}
|
|
5398
5656
|
throw error
|
|
5399
5657
|
}
|
|
5400
|
-
}
|
|
5658
|
+
}
|
|
5659
|
+
|
|
5660
|
+
if (this.lock != null) {
|
|
5661
|
+
// TODO(v3): remove legacy lock path
|
|
5662
|
+
return this._acquireLock(this.lockAcquireTimeout, run)
|
|
5663
|
+
}
|
|
5664
|
+
return run()
|
|
5401
5665
|
}
|
|
5402
5666
|
|
|
5403
5667
|
/**
|
|
@@ -5406,9 +5670,6 @@ export default class GoTrueClient {
|
|
|
5406
5670
|
private async _challengeAndVerify(
|
|
5407
5671
|
params: MFAChallengeAndVerifyParams
|
|
5408
5672
|
): Promise<AuthMFAVerifyResponse> {
|
|
5409
|
-
// both _challenge and _verify independently acquire the lock, so no need
|
|
5410
|
-
// to acquire it here
|
|
5411
|
-
|
|
5412
5673
|
const { data: challengeData, error: challengeError } = await this._challenge({
|
|
5413
5674
|
factorId: params.factorId,
|
|
5414
5675
|
})
|
|
@@ -5427,7 +5688,6 @@ export default class GoTrueClient {
|
|
|
5427
5688
|
* {@see GoTrueMFAApi#listFactors}
|
|
5428
5689
|
*/
|
|
5429
5690
|
private async _listFactors(): Promise<AuthMFAListFactorsResponse> {
|
|
5430
|
-
// use #getUser instead of #_getUser as the former acquires a lock
|
|
5431
5691
|
const {
|
|
5432
5692
|
data: { user },
|
|
5433
5693
|
error: userError,
|