@supabase/auth-js 2.106.2-canary.1 → 2.107.0-beta.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/AGENTS.md +11 -0
- 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.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 +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.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 +4 -2
- 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?: {
|
|
@@ -3783,9 +3859,14 @@ export default class GoTrueClient {
|
|
|
3783
3859
|
async signOut(options: SignOut = { scope: 'global' }): Promise<{ error: AuthError | null }> {
|
|
3784
3860
|
await this.initializePromise
|
|
3785
3861
|
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3862
|
+
if (this.lock != null) {
|
|
3863
|
+
// TODO(v3): remove legacy lock path
|
|
3864
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
3865
|
+
return await this._signOut(options)
|
|
3866
|
+
})
|
|
3867
|
+
}
|
|
3868
|
+
|
|
3869
|
+
return await this._signOut(options)
|
|
3789
3870
|
}
|
|
3790
3871
|
|
|
3791
3872
|
protected async _signOut(
|
|
@@ -3832,16 +3913,19 @@ export default class GoTrueClient {
|
|
|
3832
3913
|
}
|
|
3833
3914
|
|
|
3834
3915
|
/**
|
|
3835
|
-
*
|
|
3836
|
-
*
|
|
3837
|
-
*
|
|
3838
|
-
*
|
|
3839
|
-
*
|
|
3840
|
-
*
|
|
3841
|
-
*
|
|
3916
|
+
* Receive a notification every time an auth event happens. Common reentry
|
|
3917
|
+
* patterns (`getUser`, `setSession`, reading the session from inside a
|
|
3918
|
+
* handler) complete normally. One hazard remains: calling `refreshSession`
|
|
3919
|
+
* (or anything that routes through `_callRefreshToken`) from inside a
|
|
3920
|
+
* `TOKEN_REFRESHED` handler. `refreshingDeferred` resolves only after
|
|
3921
|
+
* `_notifyAllSubscribers` returns, so the inner refresh dedupes onto the
|
|
3922
|
+
* outer's unresolved promise and the two wait on each other.
|
|
3842
3923
|
*
|
|
3843
3924
|
* @param callback A callback function to be invoked when an auth event happens.
|
|
3844
|
-
*
|
|
3925
|
+
*
|
|
3926
|
+
* @deprecated Async callbacks can deadlock when they trigger a nested
|
|
3927
|
+
* refresh from a `TOKEN_REFRESHED` event. Prefer the sync overload, or move
|
|
3928
|
+
* refresh-triggering work outside the callback.
|
|
3845
3929
|
*/
|
|
3846
3930
|
onAuthStateChange(callback: (event: AuthChangeEvent, session: Session | null) => Promise<void>): {
|
|
3847
3931
|
data: { subscription: Subscription }
|
|
@@ -3854,18 +3938,8 @@ export default class GoTrueClient {
|
|
|
3854
3938
|
* - Subscribes to important events occurring on the user's session.
|
|
3855
3939
|
* - Use on the frontend/client. It is less useful on the server.
|
|
3856
3940
|
* - 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.
|
|
3857
|
-
* -
|
|
3858
|
-
*
|
|
3859
|
-
* - Limit the number of `await` calls in `async` callbacks.
|
|
3860
|
-
* - 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:
|
|
3861
|
-
* ```js
|
|
3862
|
-
* supabase.auth.onAuthStateChange((event, session) => {
|
|
3863
|
-
* setTimeout(async () => {
|
|
3864
|
-
* // await on other Supabase function here
|
|
3865
|
-
* // this runs right after the callback has finished
|
|
3866
|
-
* }, 0)
|
|
3867
|
-
* })
|
|
3868
|
-
* ```
|
|
3941
|
+
* - Callbacks can be `async` and can safely call other Supabase auth methods (`getUser`, `setSession`, etc.) from inside the callback.
|
|
3942
|
+
* - Keep callbacks quick. Events are awaited in order, so a slow callback delays subsequent events to subscribers in this tab.
|
|
3869
3943
|
* - Emitted events:
|
|
3870
3944
|
* - `INITIAL_SESSION`
|
|
3871
3945
|
* - Emitted right after the Supabase client is constructed and the initial session from storage is loaded.
|
|
@@ -4056,9 +4130,14 @@ export default class GoTrueClient {
|
|
|
4056
4130
|
;(async () => {
|
|
4057
4131
|
await this.initializePromise
|
|
4058
4132
|
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4133
|
+
if (this.lock != null) {
|
|
4134
|
+
// TODO(v3): remove legacy lock path
|
|
4135
|
+
await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
4136
|
+
this._emitInitialSession(id)
|
|
4137
|
+
})
|
|
4138
|
+
} else {
|
|
4139
|
+
await this._emitInitialSession(id)
|
|
4140
|
+
}
|
|
4062
4141
|
})()
|
|
4063
4142
|
|
|
4064
4143
|
return { data: { subscription } }
|
|
@@ -4453,7 +4532,10 @@ export default class GoTrueClient {
|
|
|
4453
4532
|
* @param refreshToken A valid refresh token that was returned on login.
|
|
4454
4533
|
*/
|
|
4455
4534
|
private async _refreshAccessToken(refreshToken: string): Promise<AuthResponse> {
|
|
4456
|
-
|
|
4535
|
+
// Refresh tokens are long-lived bearer credentials; do NOT include any
|
|
4536
|
+
// fragment of the token in the debug tag, even when `debug: true` is
|
|
4537
|
+
// enabled (logs may be forwarded to third-party services).
|
|
4538
|
+
const debugName = `#_refreshAccessToken()`
|
|
4457
4539
|
this._debug(debugName, 'begin')
|
|
4458
4540
|
|
|
4459
4541
|
try {
|
|
@@ -4606,15 +4688,22 @@ export default class GoTrueClient {
|
|
|
4606
4688
|
const { error } = await this._callRefreshToken(currentSession.refresh_token)
|
|
4607
4689
|
|
|
4608
4690
|
if (error) {
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4691
|
+
// AuthRefreshDiscardedError means a concurrent signOut already
|
|
4692
|
+
// cleared storage and fired SIGNED_OUT. Don't run _removeSession
|
|
4693
|
+
// again here, or we'll emit a duplicate SIGNED_OUT.
|
|
4694
|
+
if (isAuthRefreshDiscardedError(error)) {
|
|
4695
|
+
this._debug(debugName, 'refresh discarded by commit guard', error)
|
|
4696
|
+
} else {
|
|
4697
|
+
console.error(error)
|
|
4698
|
+
|
|
4699
|
+
if (!isAuthRetryableFetchError(error)) {
|
|
4700
|
+
this._debug(
|
|
4701
|
+
debugName,
|
|
4702
|
+
'refresh failed with a non-retryable error, removing the session',
|
|
4703
|
+
error
|
|
4704
|
+
)
|
|
4705
|
+
await this._removeSession()
|
|
4706
|
+
}
|
|
4618
4707
|
}
|
|
4619
4708
|
}
|
|
4620
4709
|
}
|
|
@@ -4667,18 +4756,85 @@ export default class GoTrueClient {
|
|
|
4667
4756
|
return this.refreshingDeferred.promise
|
|
4668
4757
|
}
|
|
4669
4758
|
|
|
4670
|
-
|
|
4759
|
+
// Refresh tokens are long-lived bearer credentials; do NOT include any
|
|
4760
|
+
// fragment of the token in the debug tag, even when `debug: true` is
|
|
4761
|
+
// enabled (logs may be forwarded to third-party services).
|
|
4762
|
+
const debugName = `#_callRefreshToken()`
|
|
4671
4763
|
|
|
4672
4764
|
this._debug(debugName, 'begin')
|
|
4673
4765
|
|
|
4674
4766
|
try {
|
|
4675
4767
|
this.refreshingDeferred = new Deferred<CallRefreshTokenResult>()
|
|
4676
4768
|
|
|
4769
|
+
// Snapshot storage before the fetch. The commit guard discards the
|
|
4770
|
+
// rotated tokens only when a non-null pre-fetch snapshot changed under
|
|
4771
|
+
// us — typical case: a concurrent `signOut` ran `_removeSession`, or
|
|
4772
|
+
// another tab's refresh rewrote the slot. Callers passing
|
|
4773
|
+
// externally-sourced tokens (SSR cookie handoff, multi-account
|
|
4774
|
+
// switching, `setSession`/`refreshSession({ refresh_token })`) may
|
|
4775
|
+
// start from a null snapshot OR from a non-null snapshot whose
|
|
4776
|
+
// refresh_token differs from the one they're hydrating; in both
|
|
4777
|
+
// cases the guard fires only when storage was *modified between
|
|
4778
|
+
// snapshots*, not when the input token disagrees with what's stored.
|
|
4779
|
+
const storedAtStart = (await getItemAsync(this.storage, this.storageKey)) as Session | null
|
|
4780
|
+
|
|
4677
4781
|
const { data, error } = await this._refreshAccessToken(refreshToken)
|
|
4678
4782
|
if (error) throw error
|
|
4679
4783
|
if (!data.session) throw new AuthSessionMissingError()
|
|
4680
4784
|
|
|
4785
|
+
const storedAfter = (await getItemAsync(this.storage, this.storageKey)) as Session | null
|
|
4786
|
+
const storageChangedUnderUs =
|
|
4787
|
+
storedAtStart !== null &&
|
|
4788
|
+
(storedAfter === null || storedAfter.refresh_token !== storedAtStart.refresh_token)
|
|
4789
|
+
|
|
4790
|
+
if (storageChangedUnderUs) {
|
|
4791
|
+
this._debug(
|
|
4792
|
+
debugName,
|
|
4793
|
+
'commit guard: storage changed since refresh started, discarding rotated tokens',
|
|
4794
|
+
{
|
|
4795
|
+
// Presence indicators only — never log refresh token fragments,
|
|
4796
|
+
// even partial. Logs may be forwarded to third-party services.
|
|
4797
|
+
startedWith: 'present',
|
|
4798
|
+
nowHolds: storedAfter ? 'replaced' : 'cleared',
|
|
4799
|
+
}
|
|
4800
|
+
)
|
|
4801
|
+
const discarded: CallRefreshTokenResult = {
|
|
4802
|
+
data: null,
|
|
4803
|
+
error: new AuthRefreshDiscardedError(),
|
|
4804
|
+
}
|
|
4805
|
+
this.refreshingDeferred.resolve(discarded)
|
|
4806
|
+
return discarded
|
|
4807
|
+
}
|
|
4808
|
+
|
|
4809
|
+
// Second leg of the commit guard: close the TOCTOU window between the
|
|
4810
|
+
// synchronous `storageChangedUnderUs` check and the actual storage
|
|
4811
|
+
// writes inside `_saveSession`. A concurrent `signOut → _removeSession`
|
|
4812
|
+
// can land inside `_saveSession`'s `await setItemAsync(...)` yields and
|
|
4813
|
+
// clear storage just before we overwrite it. Capture the epoch BEFORE
|
|
4814
|
+
// the save and re-check after; if it advanced, undo the write directly
|
|
4815
|
+
// (do NOT call `_removeSession` — that would emit a duplicate
|
|
4816
|
+
// SIGNED_OUT for the concurrent signOut that already fired one).
|
|
4817
|
+
const epochBeforeSave = this._sessionRemovalEpoch
|
|
4818
|
+
|
|
4681
4819
|
await this._saveSession(data.session)
|
|
4820
|
+
|
|
4821
|
+
if (this._sessionRemovalEpoch !== epochBeforeSave) {
|
|
4822
|
+
this._debug(
|
|
4823
|
+
debugName,
|
|
4824
|
+
'commit guard (post-save): _removeSession ran during _saveSession, undoing write'
|
|
4825
|
+
)
|
|
4826
|
+
await removeItemAsync(this.storage, this.storageKey)
|
|
4827
|
+
if (this.userStorage) {
|
|
4828
|
+
await removeItemAsync(this.userStorage, this.storageKey + '-user')
|
|
4829
|
+
}
|
|
4830
|
+
const discarded: CallRefreshTokenResult = {
|
|
4831
|
+
data: null,
|
|
4832
|
+
error: new AuthRefreshDiscardedError(),
|
|
4833
|
+
}
|
|
4834
|
+
this.refreshingDeferred.resolve(discarded)
|
|
4835
|
+
return discarded
|
|
4836
|
+
}
|
|
4837
|
+
|
|
4682
4838
|
await this._notifyAllSubscribers('TOKEN_REFRESHED', data.session)
|
|
4683
4839
|
|
|
4684
4840
|
const result = { data: data.session, error: null }
|
|
@@ -4790,6 +4946,11 @@ export default class GoTrueClient {
|
|
|
4790
4946
|
}
|
|
4791
4947
|
|
|
4792
4948
|
private async _removeSession() {
|
|
4949
|
+
// Bump synchronously, BEFORE any `await`, so that `_callRefreshToken`'s
|
|
4950
|
+
// post-save check sees the increment whenever this method has started —
|
|
4951
|
+
// even if it hasn't finished. Pairs with the epoch check in
|
|
4952
|
+
// `_callRefreshToken`. See `_sessionRemovalEpoch` field doc.
|
|
4953
|
+
this._sessionRemovalEpoch += 1
|
|
4793
4954
|
this._debug('#_removeSession()')
|
|
4794
4955
|
|
|
4795
4956
|
this.suppressGetSessionWarning = false
|
|
@@ -4978,58 +5139,144 @@ export default class GoTrueClient {
|
|
|
4978
5139
|
await this._stopAutoRefresh()
|
|
4979
5140
|
}
|
|
4980
5141
|
|
|
5142
|
+
/**
|
|
5143
|
+
* Tears down the client's background work: stops the auto-refresh interval,
|
|
5144
|
+
* removes the `visibilitychange` listener, closes the cross-tab
|
|
5145
|
+
* `BroadcastChannel`, and clears registered `onAuthStateChange` subscribers.
|
|
5146
|
+
*
|
|
5147
|
+
* Call this from cleanup hooks when the client is being replaced before
|
|
5148
|
+
* its JS realm is destroyed. React Strict Mode and HMR are the common
|
|
5149
|
+
* cases. Any in-flight `fetch` calls continue to completion and may still
|
|
5150
|
+
* write to storage; dispose doesn't abort them or erase storage.
|
|
5151
|
+
*
|
|
5152
|
+
* Lifecycle caveat: because in-flight refreshes are not aborted, a
|
|
5153
|
+
* disposed instance can still persist a rotated session to storage after
|
|
5154
|
+
* `dispose()` returns. A subsequent `createClient` against the same
|
|
5155
|
+
* `storageKey` will pick up that session on its next read. If you need
|
|
5156
|
+
* strict isolation between client lifecycles, await any pending auth
|
|
5157
|
+
* operation before calling `dispose()` (or change the `storageKey` for
|
|
5158
|
+
* the replacement client).
|
|
5159
|
+
*
|
|
5160
|
+
* Safe to call repeatedly.
|
|
5161
|
+
*
|
|
5162
|
+
* @category Auth
|
|
5163
|
+
*
|
|
5164
|
+
* @example Cleanup on React unmount
|
|
5165
|
+
* ```ts
|
|
5166
|
+
* useEffect(() => {
|
|
5167
|
+
* const client = createClient(...)
|
|
5168
|
+
* return () => { client.auth.dispose() }
|
|
5169
|
+
* }, [])
|
|
5170
|
+
* ```
|
|
5171
|
+
*/
|
|
5172
|
+
async dispose(): Promise<void> {
|
|
5173
|
+
this._removeVisibilityChangedCallback()
|
|
5174
|
+
await this._stopAutoRefresh()
|
|
5175
|
+
this.broadcastChannel?.close()
|
|
5176
|
+
this.broadcastChannel = null
|
|
5177
|
+
this.stateChangeEmitters.clear()
|
|
5178
|
+
}
|
|
5179
|
+
|
|
4981
5180
|
/**
|
|
4982
5181
|
* Runs the auto refresh token tick.
|
|
4983
5182
|
*/
|
|
4984
5183
|
private async _autoRefreshTokenTick() {
|
|
4985
5184
|
this._debug('#_autoRefreshTokenTick()', 'begin')
|
|
4986
5185
|
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
5186
|
+
if (this.lock != null) {
|
|
5187
|
+
// TODO(v3): remove legacy lock path. Uses `_acquireLock(0, ...)` which
|
|
5188
|
+
// throws `LockAcquireTimeoutError` immediately if the lock is held —
|
|
5189
|
+
// that's the fail-fast skip path that lets the tick bail out instead
|
|
5190
|
+
// of queuing behind a long-running operation.
|
|
5191
|
+
try {
|
|
5192
|
+
await this._acquireLock(0, async () => {
|
|
4992
5193
|
try {
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5194
|
+
const now = Date.now()
|
|
5195
|
+
try {
|
|
5196
|
+
return await this._useSession(async (result) => {
|
|
5197
|
+
const {
|
|
5198
|
+
data: { session },
|
|
5199
|
+
} = result
|
|
5200
|
+
|
|
5201
|
+
if (!session || !session.refresh_token || !session.expires_at) {
|
|
5202
|
+
this._debug('#_autoRefreshTokenTick()', 'no session')
|
|
5203
|
+
return
|
|
5204
|
+
}
|
|
5002
5205
|
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
)
|
|
5206
|
+
const expiresInTicks = Math.floor(
|
|
5207
|
+
(session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS
|
|
5208
|
+
)
|
|
5007
5209
|
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5210
|
+
this._debug(
|
|
5211
|
+
'#_autoRefreshTokenTick()',
|
|
5212
|
+
`access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks`
|
|
5213
|
+
)
|
|
5012
5214
|
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5215
|
+
if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) {
|
|
5216
|
+
await this._callRefreshToken(session.refresh_token)
|
|
5217
|
+
}
|
|
5218
|
+
})
|
|
5219
|
+
} catch (e) {
|
|
5220
|
+
console.error(
|
|
5221
|
+
'Auto refresh tick failed with error. This is likely a transient error.',
|
|
5222
|
+
e
|
|
5223
|
+
)
|
|
5224
|
+
}
|
|
5225
|
+
} finally {
|
|
5226
|
+
this._debug('#_autoRefreshTokenTick()', 'end')
|
|
5022
5227
|
}
|
|
5023
|
-
}
|
|
5024
|
-
|
|
5228
|
+
})
|
|
5229
|
+
} catch (e) {
|
|
5230
|
+
if (e instanceof LockAcquireTimeoutError) {
|
|
5231
|
+
this._debug('auto refresh token tick lock not available')
|
|
5232
|
+
} else {
|
|
5233
|
+
throw e
|
|
5025
5234
|
}
|
|
5026
|
-
})
|
|
5027
|
-
} catch (e) {
|
|
5028
|
-
if (e instanceof LockAcquireTimeoutError) {
|
|
5029
|
-
this._debug('auto refresh token tick lock not available')
|
|
5030
|
-
} else {
|
|
5031
|
-
throw e
|
|
5032
5235
|
}
|
|
5236
|
+
return
|
|
5237
|
+
}
|
|
5238
|
+
|
|
5239
|
+
// Lockless default: skip if a refresh is already in flight.
|
|
5240
|
+
// `_callRefreshToken` also dedupes via the same field; this is just a
|
|
5241
|
+
// fast-path skip to avoid an unnecessary storage read.
|
|
5242
|
+
if (this.refreshingDeferred !== null) {
|
|
5243
|
+
this._debug('#_autoRefreshTokenTick()', 'refresh already in flight, skipping')
|
|
5244
|
+
return
|
|
5245
|
+
}
|
|
5246
|
+
|
|
5247
|
+
try {
|
|
5248
|
+
const now = Date.now()
|
|
5249
|
+
|
|
5250
|
+
try {
|
|
5251
|
+
await this._useSession(async (result) => {
|
|
5252
|
+
const {
|
|
5253
|
+
data: { session },
|
|
5254
|
+
} = result
|
|
5255
|
+
|
|
5256
|
+
if (!session || !session.refresh_token || !session.expires_at) {
|
|
5257
|
+
this._debug('#_autoRefreshTokenTick()', 'no session')
|
|
5258
|
+
return
|
|
5259
|
+
}
|
|
5260
|
+
|
|
5261
|
+
// session will expire in this many ticks (or has already expired if <= 0)
|
|
5262
|
+
const expiresInTicks = Math.floor(
|
|
5263
|
+
(session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS
|
|
5264
|
+
)
|
|
5265
|
+
|
|
5266
|
+
this._debug(
|
|
5267
|
+
'#_autoRefreshTokenTick()',
|
|
5268
|
+
`access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks`
|
|
5269
|
+
)
|
|
5270
|
+
|
|
5271
|
+
if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) {
|
|
5272
|
+
await this._callRefreshToken(session.refresh_token)
|
|
5273
|
+
}
|
|
5274
|
+
})
|
|
5275
|
+
} catch (e) {
|
|
5276
|
+
console.error('Auto refresh tick failed with error. This is likely a transient error.', e)
|
|
5277
|
+
}
|
|
5278
|
+
} finally {
|
|
5279
|
+
this._debug('#_autoRefreshTokenTick()', 'end')
|
|
5033
5280
|
}
|
|
5034
5281
|
}
|
|
5035
5282
|
|
|
@@ -5086,24 +5333,29 @@ export default class GoTrueClient {
|
|
|
5086
5333
|
if (!calledFromInitialize) {
|
|
5087
5334
|
// called when the visibility has changed, i.e. the browser
|
|
5088
5335
|
// transitioned from hidden -> visible so we need to see if the session
|
|
5089
|
-
// should be recovered
|
|
5090
|
-
// the lock first asynchronously
|
|
5336
|
+
// should be recovered
|
|
5091
5337
|
await this.initializePromise
|
|
5092
5338
|
|
|
5093
|
-
|
|
5339
|
+
if (this.lock != null) {
|
|
5340
|
+
// TODO(v3): remove legacy lock path
|
|
5341
|
+
await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
5342
|
+
if (document.visibilityState !== 'visible') {
|
|
5343
|
+
this._debug(
|
|
5344
|
+
methodName,
|
|
5345
|
+
'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting'
|
|
5346
|
+
)
|
|
5347
|
+
return
|
|
5348
|
+
}
|
|
5349
|
+
await this._recoverAndRefresh()
|
|
5350
|
+
})
|
|
5351
|
+
} else {
|
|
5094
5352
|
if (document.visibilityState !== 'visible') {
|
|
5095
|
-
this._debug(
|
|
5096
|
-
methodName,
|
|
5097
|
-
'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting'
|
|
5098
|
-
)
|
|
5099
|
-
|
|
5100
|
-
// visibility has changed while waiting for the lock, abort
|
|
5353
|
+
this._debug(methodName, 'visibilityState is no longer visible, skipping recovery')
|
|
5101
5354
|
return
|
|
5102
5355
|
}
|
|
5103
|
-
|
|
5104
5356
|
// recover the session
|
|
5105
5357
|
await this._recoverAndRefresh()
|
|
5106
|
-
}
|
|
5358
|
+
}
|
|
5107
5359
|
}
|
|
5108
5360
|
} else if (document.visibilityState === 'hidden') {
|
|
5109
5361
|
if (this.autoRefreshToken) {
|
|
@@ -5235,7 +5487,7 @@ export default class GoTrueClient {
|
|
|
5235
5487
|
params: MFAVerifyWebauthnParams<T>
|
|
5236
5488
|
): Promise<AuthMFAVerifyResponse>
|
|
5237
5489
|
private async _verify(params: MFAVerifyParams): Promise<AuthMFAVerifyResponse> {
|
|
5238
|
-
|
|
5490
|
+
const run = async (): Promise<AuthMFAVerifyResponse> => {
|
|
5239
5491
|
try {
|
|
5240
5492
|
return await this._useSession(async (result) => {
|
|
5241
5493
|
const { data: sessionData, error: sessionError } = result
|
|
@@ -5306,7 +5558,13 @@ export default class GoTrueClient {
|
|
|
5306
5558
|
}
|
|
5307
5559
|
throw error
|
|
5308
5560
|
}
|
|
5309
|
-
}
|
|
5561
|
+
}
|
|
5562
|
+
|
|
5563
|
+
if (this.lock != null) {
|
|
5564
|
+
// TODO(v3): remove legacy lock path
|
|
5565
|
+
return this._acquireLock(this.lockAcquireTimeout, run)
|
|
5566
|
+
}
|
|
5567
|
+
return run()
|
|
5310
5568
|
}
|
|
5311
5569
|
|
|
5312
5570
|
/**
|
|
@@ -5322,7 +5580,7 @@ export default class GoTrueClient {
|
|
|
5322
5580
|
params: MFAChallengeWebauthnParams
|
|
5323
5581
|
): Promise<Prettify<AuthMFAChallengeWebauthnResponse>>
|
|
5324
5582
|
private async _challenge(params: MFAChallengeParams): Promise<AuthMFAChallengeResponse> {
|
|
5325
|
-
|
|
5583
|
+
const run = async (): Promise<AuthMFAChallengeResponse> => {
|
|
5326
5584
|
try {
|
|
5327
5585
|
return await this._useSession(async (result) => {
|
|
5328
5586
|
const { data: sessionData, error: sessionError } = result
|
|
@@ -5395,7 +5653,13 @@ export default class GoTrueClient {
|
|
|
5395
5653
|
}
|
|
5396
5654
|
throw error
|
|
5397
5655
|
}
|
|
5398
|
-
}
|
|
5656
|
+
}
|
|
5657
|
+
|
|
5658
|
+
if (this.lock != null) {
|
|
5659
|
+
// TODO(v3): remove legacy lock path
|
|
5660
|
+
return this._acquireLock(this.lockAcquireTimeout, run)
|
|
5661
|
+
}
|
|
5662
|
+
return run()
|
|
5399
5663
|
}
|
|
5400
5664
|
|
|
5401
5665
|
/**
|
|
@@ -5404,9 +5668,6 @@ export default class GoTrueClient {
|
|
|
5404
5668
|
private async _challengeAndVerify(
|
|
5405
5669
|
params: MFAChallengeAndVerifyParams
|
|
5406
5670
|
): Promise<AuthMFAVerifyResponse> {
|
|
5407
|
-
// both _challenge and _verify independently acquire the lock, so no need
|
|
5408
|
-
// to acquire it here
|
|
5409
|
-
|
|
5410
5671
|
const { data: challengeData, error: challengeError } = await this._challenge({
|
|
5411
5672
|
factorId: params.factorId,
|
|
5412
5673
|
})
|
|
@@ -5425,7 +5686,6 @@ export default class GoTrueClient {
|
|
|
5425
5686
|
* {@see GoTrueMFAApi#listFactors}
|
|
5426
5687
|
*/
|
|
5427
5688
|
private async _listFactors(): Promise<AuthMFAListFactorsResponse> {
|
|
5428
|
-
// use #getUser instead of #_getUser as the former acquires a lock
|
|
5429
5689
|
const {
|
|
5430
5690
|
data: { user },
|
|
5431
5691
|
error: userError,
|