@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
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
|
|
@@ -1083,6 +1116,21 @@ export default class GoTrueClient {
|
|
|
1083
1116
|
* password: 'some-password',
|
|
1084
1117
|
* })
|
|
1085
1118
|
* ```
|
|
1119
|
+
*
|
|
1120
|
+
* @exampleDescription Handling errors
|
|
1121
|
+
* 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.
|
|
1122
|
+
*
|
|
1123
|
+
* @example Handling errors
|
|
1124
|
+
* ```js
|
|
1125
|
+
* const { data, error } = await supabase.auth.signInWithPassword({
|
|
1126
|
+
* email: 'example@email.com',
|
|
1127
|
+
* password: 'example-password',
|
|
1128
|
+
* })
|
|
1129
|
+
* if (error) {
|
|
1130
|
+
* console.error(error)
|
|
1131
|
+
* return
|
|
1132
|
+
* }
|
|
1133
|
+
* ```
|
|
1086
1134
|
*/
|
|
1087
1135
|
async signInWithPassword(
|
|
1088
1136
|
credentials: SignInWithPasswordCredentials
|
|
@@ -1405,9 +1453,14 @@ export default class GoTrueClient {
|
|
|
1405
1453
|
async exchangeCodeForSession(authCode: string): Promise<AuthTokenResponse> {
|
|
1406
1454
|
await this.initializePromise
|
|
1407
1455
|
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1456
|
+
if (this.lock != null) {
|
|
1457
|
+
// TODO(v3): remove legacy lock path
|
|
1458
|
+
return this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
1459
|
+
return this._exchangeCodeForSession(authCode)
|
|
1460
|
+
})
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
return this._exchangeCodeForSession(authCode)
|
|
1411
1464
|
}
|
|
1412
1465
|
|
|
1413
1466
|
/**
|
|
@@ -2305,7 +2358,7 @@ export default class GoTrueClient {
|
|
|
2305
2358
|
}
|
|
2306
2359
|
|
|
2307
2360
|
const session: Session | null = data.session
|
|
2308
|
-
const user: User = data.user
|
|
2361
|
+
const user: User | null = data.user
|
|
2309
2362
|
|
|
2310
2363
|
if (session?.access_token) {
|
|
2311
2364
|
await this._saveSession(session as Session)
|
|
@@ -2444,9 +2497,14 @@ export default class GoTrueClient {
|
|
|
2444
2497
|
async reauthenticate(): Promise<AuthResponse> {
|
|
2445
2498
|
await this.initializePromise
|
|
2446
2499
|
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2500
|
+
if (this.lock != null) {
|
|
2501
|
+
// TODO(v3): remove legacy lock path
|
|
2502
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
2503
|
+
return await this._reauthenticate()
|
|
2504
|
+
})
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
return await this._reauthenticate()
|
|
2450
2508
|
}
|
|
2451
2509
|
|
|
2452
2510
|
private async _reauthenticate(): Promise<AuthResponse> {
|
|
@@ -2593,7 +2651,7 @@ export default class GoTrueClient {
|
|
|
2593
2651
|
* - 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
2652
|
* - 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
2653
|
* - **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
|
-
* -
|
|
2654
|
+
* - 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
2655
|
*
|
|
2598
2656
|
* @example Get the session data
|
|
2599
2657
|
* ```js
|
|
@@ -2661,17 +2719,26 @@ export default class GoTrueClient {
|
|
|
2661
2719
|
async getSession() {
|
|
2662
2720
|
await this.initializePromise
|
|
2663
2721
|
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2722
|
+
if (this.lock != null) {
|
|
2723
|
+
// TODO(v3): remove legacy lock path
|
|
2724
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
2725
|
+
return this._useSession(async (result) => {
|
|
2726
|
+
return result
|
|
2727
|
+
})
|
|
2667
2728
|
})
|
|
2668
|
-
}
|
|
2729
|
+
}
|
|
2669
2730
|
|
|
2670
|
-
return result
|
|
2731
|
+
return await this._useSession(async (result) => {
|
|
2732
|
+
return result
|
|
2733
|
+
})
|
|
2671
2734
|
}
|
|
2672
2735
|
|
|
2673
2736
|
/**
|
|
2674
2737
|
* Acquires a global lock based on the storage key.
|
|
2738
|
+
*
|
|
2739
|
+
* TODO(v3): remove along with the legacy lock path. Only called when
|
|
2740
|
+
* `this.lock` is non-null (custom lock supplied via constructor). The
|
|
2741
|
+
* default lockless path bypasses this entirely.
|
|
2675
2742
|
*/
|
|
2676
2743
|
private async _acquireLock<R>(acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
|
|
2677
2744
|
this._debug('#_acquireLock', 'begin', acquireTimeout)
|
|
@@ -2700,7 +2767,7 @@ export default class GoTrueClient {
|
|
|
2700
2767
|
return result
|
|
2701
2768
|
}
|
|
2702
2769
|
|
|
2703
|
-
return await this.lock(`lock:${this.storageKey}`, acquireTimeout, async () => {
|
|
2770
|
+
return await this.lock!(`lock:${this.storageKey}`, acquireTimeout, async () => {
|
|
2704
2771
|
this._debug('#_acquireLock', 'lock acquired for storage key', this.storageKey)
|
|
2705
2772
|
|
|
2706
2773
|
try {
|
|
@@ -2742,10 +2809,9 @@ export default class GoTrueClient {
|
|
|
2742
2809
|
}
|
|
2743
2810
|
|
|
2744
2811
|
/**
|
|
2745
|
-
* Use instead of {@link #getSession} inside the library.
|
|
2746
|
-
*
|
|
2747
|
-
*
|
|
2748
|
-
* session at once across multiple tabs or processes.
|
|
2812
|
+
* Use instead of {@link #getSession} inside the library. Loads the session
|
|
2813
|
+
* via `__loadSession` (which may trigger a refresh if the access token is
|
|
2814
|
+
* within the expiry margin) and runs `fn` with the result.
|
|
2749
2815
|
*/
|
|
2750
2816
|
private async _useSession<R>(
|
|
2751
2817
|
fn: (
|
|
@@ -2773,7 +2839,10 @@ export default class GoTrueClient {
|
|
|
2773
2839
|
this._debug('#_useSession', 'begin')
|
|
2774
2840
|
|
|
2775
2841
|
try {
|
|
2776
|
-
//
|
|
2842
|
+
// Concurrent callers may both reach __loadSession; storage reads are
|
|
2843
|
+
// idempotent, and the only write path inside it (refresh) is
|
|
2844
|
+
// single-flighted downstream by `refreshingDeferred` in
|
|
2845
|
+
// `_callRefreshToken`. No serialization is needed at this layer.
|
|
2777
2846
|
const result = await this.__loadSession()
|
|
2778
2847
|
|
|
2779
2848
|
return await fn(result)
|
|
@@ -2809,7 +2878,8 @@ export default class GoTrueClient {
|
|
|
2809
2878
|
> {
|
|
2810
2879
|
this._debug('#__loadSession()', 'begin')
|
|
2811
2880
|
|
|
2812
|
-
if (!this.lockAcquired) {
|
|
2881
|
+
if (this.lock != null && !this.lockAcquired) {
|
|
2882
|
+
// TODO(v3): remove. Only meaningful on the legacy lock path.
|
|
2813
2883
|
this._debug('#__loadSession()', 'used outside of an acquired lock!', new Error().stack)
|
|
2814
2884
|
}
|
|
2815
2885
|
|
|
@@ -2976,9 +3046,15 @@ export default class GoTrueClient {
|
|
|
2976
3046
|
|
|
2977
3047
|
await this.initializePromise
|
|
2978
3048
|
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
3049
|
+
let result: UserResponse
|
|
3050
|
+
if (this.lock != null) {
|
|
3051
|
+
// TODO(v3): remove legacy lock path
|
|
3052
|
+
result = await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
3053
|
+
return await this._getUser()
|
|
3054
|
+
})
|
|
3055
|
+
} else {
|
|
3056
|
+
result = await this._getUser()
|
|
3057
|
+
}
|
|
2982
3058
|
|
|
2983
3059
|
if (result.data.user) {
|
|
2984
3060
|
this.suppressGetSessionWarning = true
|
|
@@ -3153,9 +3229,14 @@ export default class GoTrueClient {
|
|
|
3153
3229
|
): Promise<UserResponse> {
|
|
3154
3230
|
await this.initializePromise
|
|
3155
3231
|
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3232
|
+
if (this.lock != null) {
|
|
3233
|
+
// TODO(v3): remove legacy lock path
|
|
3234
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
3235
|
+
return await this._updateUser(attributes, options)
|
|
3236
|
+
})
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
return await this._updateUser(attributes, options)
|
|
3159
3240
|
}
|
|
3160
3241
|
|
|
3161
3242
|
protected async _updateUser(
|
|
@@ -3342,9 +3423,14 @@ export default class GoTrueClient {
|
|
|
3342
3423
|
}): Promise<AuthResponse> {
|
|
3343
3424
|
await this.initializePromise
|
|
3344
3425
|
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3426
|
+
if (this.lock != null) {
|
|
3427
|
+
// TODO(v3): remove legacy lock path
|
|
3428
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
3429
|
+
return await this._setSession(currentSession)
|
|
3430
|
+
})
|
|
3431
|
+
}
|
|
3432
|
+
|
|
3433
|
+
return await this._setSession(currentSession)
|
|
3348
3434
|
}
|
|
3349
3435
|
|
|
3350
3436
|
protected async _setSession(currentSession: {
|
|
@@ -3533,9 +3619,14 @@ export default class GoTrueClient {
|
|
|
3533
3619
|
async refreshSession(currentSession?: { refresh_token: string }): Promise<AuthResponse> {
|
|
3534
3620
|
await this.initializePromise
|
|
3535
3621
|
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3622
|
+
if (this.lock != null) {
|
|
3623
|
+
// TODO(v3): remove legacy lock path
|
|
3624
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
3625
|
+
return await this._refreshSession(currentSession)
|
|
3626
|
+
})
|
|
3627
|
+
}
|
|
3628
|
+
|
|
3629
|
+
return await this._refreshSession(currentSession)
|
|
3539
3630
|
}
|
|
3540
3631
|
|
|
3541
3632
|
protected async _refreshSession(currentSession?: {
|
|
@@ -3724,7 +3815,9 @@ export default class GoTrueClient {
|
|
|
3724
3815
|
if (typeof this.detectSessionInUrl === 'function') {
|
|
3725
3816
|
return this.detectSessionInUrl(new URL(window.location.href), params)
|
|
3726
3817
|
}
|
|
3727
|
-
return Boolean(
|
|
3818
|
+
return Boolean(
|
|
3819
|
+
params.access_token || params.error || params.error_description || params.error_code
|
|
3820
|
+
)
|
|
3728
3821
|
}
|
|
3729
3822
|
|
|
3730
3823
|
/**
|
|
@@ -3783,9 +3876,14 @@ export default class GoTrueClient {
|
|
|
3783
3876
|
async signOut(options: SignOut = { scope: 'global' }): Promise<{ error: AuthError | null }> {
|
|
3784
3877
|
await this.initializePromise
|
|
3785
3878
|
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3879
|
+
if (this.lock != null) {
|
|
3880
|
+
// TODO(v3): remove legacy lock path
|
|
3881
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
3882
|
+
return await this._signOut(options)
|
|
3883
|
+
})
|
|
3884
|
+
}
|
|
3885
|
+
|
|
3886
|
+
return await this._signOut(options)
|
|
3789
3887
|
}
|
|
3790
3888
|
|
|
3791
3889
|
protected async _signOut(
|
|
@@ -3832,16 +3930,19 @@ export default class GoTrueClient {
|
|
|
3832
3930
|
}
|
|
3833
3931
|
|
|
3834
3932
|
/**
|
|
3835
|
-
*
|
|
3836
|
-
*
|
|
3837
|
-
*
|
|
3838
|
-
*
|
|
3839
|
-
*
|
|
3840
|
-
*
|
|
3841
|
-
*
|
|
3933
|
+
* Receive a notification every time an auth event happens. Common reentry
|
|
3934
|
+
* patterns (`getUser`, `setSession`, reading the session from inside a
|
|
3935
|
+
* handler) complete normally. One hazard remains: calling `refreshSession`
|
|
3936
|
+
* (or anything that routes through `_callRefreshToken`) from inside a
|
|
3937
|
+
* `TOKEN_REFRESHED` handler. `refreshingDeferred` resolves only after
|
|
3938
|
+
* `_notifyAllSubscribers` returns, so the inner refresh dedupes onto the
|
|
3939
|
+
* outer's unresolved promise and the two wait on each other.
|
|
3842
3940
|
*
|
|
3843
3941
|
* @param callback A callback function to be invoked when an auth event happens.
|
|
3844
|
-
*
|
|
3942
|
+
*
|
|
3943
|
+
* @deprecated Async callbacks can deadlock when they trigger a nested
|
|
3944
|
+
* refresh from a `TOKEN_REFRESHED` event. Prefer the sync overload, or move
|
|
3945
|
+
* refresh-triggering work outside the callback.
|
|
3845
3946
|
*/
|
|
3846
3947
|
onAuthStateChange(callback: (event: AuthChangeEvent, session: Session | null) => Promise<void>): {
|
|
3847
3948
|
data: { subscription: Subscription }
|
|
@@ -3854,18 +3955,8 @@ export default class GoTrueClient {
|
|
|
3854
3955
|
* - Subscribes to important events occurring on the user's session.
|
|
3855
3956
|
* - Use on the frontend/client. It is less useful on the server.
|
|
3856
3957
|
* - 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
|
-
* ```
|
|
3958
|
+
* - Callbacks can be `async` and can safely call other Supabase auth methods (`getUser`, `setSession`, etc.) from inside the callback.
|
|
3959
|
+
* - Keep callbacks quick. Events are awaited in order, so a slow callback delays subsequent events to subscribers in this tab.
|
|
3869
3960
|
* - Emitted events:
|
|
3870
3961
|
* - `INITIAL_SESSION`
|
|
3871
3962
|
* - Emitted right after the Supabase client is constructed and the initial session from storage is loaded.
|
|
@@ -4056,9 +4147,14 @@ export default class GoTrueClient {
|
|
|
4056
4147
|
;(async () => {
|
|
4057
4148
|
await this.initializePromise
|
|
4058
4149
|
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4150
|
+
if (this.lock != null) {
|
|
4151
|
+
// TODO(v3): remove legacy lock path
|
|
4152
|
+
await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
4153
|
+
this._emitInitialSession(id)
|
|
4154
|
+
})
|
|
4155
|
+
} else {
|
|
4156
|
+
await this._emitInitialSession(id)
|
|
4157
|
+
}
|
|
4062
4158
|
})()
|
|
4063
4159
|
|
|
4064
4160
|
return { data: { subscription } }
|
|
@@ -4453,7 +4549,10 @@ export default class GoTrueClient {
|
|
|
4453
4549
|
* @param refreshToken A valid refresh token that was returned on login.
|
|
4454
4550
|
*/
|
|
4455
4551
|
private async _refreshAccessToken(refreshToken: string): Promise<AuthResponse> {
|
|
4456
|
-
|
|
4552
|
+
// Refresh tokens are long-lived bearer credentials; do NOT include any
|
|
4553
|
+
// fragment of the token in the debug tag, even when `debug: true` is
|
|
4554
|
+
// enabled (logs may be forwarded to third-party services).
|
|
4555
|
+
const debugName = `#_refreshAccessToken()`
|
|
4457
4556
|
this._debug(debugName, 'begin')
|
|
4458
4557
|
|
|
4459
4558
|
try {
|
|
@@ -4606,15 +4705,22 @@ export default class GoTrueClient {
|
|
|
4606
4705
|
const { error } = await this._callRefreshToken(currentSession.refresh_token)
|
|
4607
4706
|
|
|
4608
4707
|
if (error) {
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4708
|
+
// AuthRefreshDiscardedError means a concurrent signOut already
|
|
4709
|
+
// cleared storage and fired SIGNED_OUT. Don't run _removeSession
|
|
4710
|
+
// again here, or we'll emit a duplicate SIGNED_OUT.
|
|
4711
|
+
if (isAuthRefreshDiscardedError(error)) {
|
|
4712
|
+
this._debug(debugName, 'refresh discarded by commit guard', error)
|
|
4713
|
+
} else {
|
|
4714
|
+
console.error(error)
|
|
4715
|
+
|
|
4716
|
+
if (!isAuthRetryableFetchError(error)) {
|
|
4717
|
+
this._debug(
|
|
4718
|
+
debugName,
|
|
4719
|
+
'refresh failed with a non-retryable error, removing the session',
|
|
4720
|
+
error
|
|
4721
|
+
)
|
|
4722
|
+
await this._removeSession()
|
|
4723
|
+
}
|
|
4618
4724
|
}
|
|
4619
4725
|
}
|
|
4620
4726
|
}
|
|
@@ -4667,18 +4773,85 @@ export default class GoTrueClient {
|
|
|
4667
4773
|
return this.refreshingDeferred.promise
|
|
4668
4774
|
}
|
|
4669
4775
|
|
|
4670
|
-
|
|
4776
|
+
// Refresh tokens are long-lived bearer credentials; do NOT include any
|
|
4777
|
+
// fragment of the token in the debug tag, even when `debug: true` is
|
|
4778
|
+
// enabled (logs may be forwarded to third-party services).
|
|
4779
|
+
const debugName = `#_callRefreshToken()`
|
|
4671
4780
|
|
|
4672
4781
|
this._debug(debugName, 'begin')
|
|
4673
4782
|
|
|
4674
4783
|
try {
|
|
4675
4784
|
this.refreshingDeferred = new Deferred<CallRefreshTokenResult>()
|
|
4676
4785
|
|
|
4786
|
+
// Snapshot storage before the fetch. The commit guard discards the
|
|
4787
|
+
// rotated tokens only when a non-null pre-fetch snapshot changed under
|
|
4788
|
+
// us — typical case: a concurrent `signOut` ran `_removeSession`, or
|
|
4789
|
+
// another tab's refresh rewrote the slot. Callers passing
|
|
4790
|
+
// externally-sourced tokens (SSR cookie handoff, multi-account
|
|
4791
|
+
// switching, `setSession`/`refreshSession({ refresh_token })`) may
|
|
4792
|
+
// start from a null snapshot OR from a non-null snapshot whose
|
|
4793
|
+
// refresh_token differs from the one they're hydrating; in both
|
|
4794
|
+
// cases the guard fires only when storage was *modified between
|
|
4795
|
+
// snapshots*, not when the input token disagrees with what's stored.
|
|
4796
|
+
const storedAtStart = (await getItemAsync(this.storage, this.storageKey)) as Session | null
|
|
4797
|
+
|
|
4677
4798
|
const { data, error } = await this._refreshAccessToken(refreshToken)
|
|
4678
4799
|
if (error) throw error
|
|
4679
4800
|
if (!data.session) throw new AuthSessionMissingError()
|
|
4680
4801
|
|
|
4802
|
+
const storedAfter = (await getItemAsync(this.storage, this.storageKey)) as Session | null
|
|
4803
|
+
const storageChangedUnderUs =
|
|
4804
|
+
storedAtStart !== null &&
|
|
4805
|
+
(storedAfter === null || storedAfter.refresh_token !== storedAtStart.refresh_token)
|
|
4806
|
+
|
|
4807
|
+
if (storageChangedUnderUs) {
|
|
4808
|
+
this._debug(
|
|
4809
|
+
debugName,
|
|
4810
|
+
'commit guard: storage changed since refresh started, discarding rotated tokens',
|
|
4811
|
+
{
|
|
4812
|
+
// Presence indicators only — never log refresh token fragments,
|
|
4813
|
+
// even partial. Logs may be forwarded to third-party services.
|
|
4814
|
+
startedWith: 'present',
|
|
4815
|
+
nowHolds: storedAfter ? 'replaced' : 'cleared',
|
|
4816
|
+
}
|
|
4817
|
+
)
|
|
4818
|
+
const discarded: CallRefreshTokenResult = {
|
|
4819
|
+
data: null,
|
|
4820
|
+
error: new AuthRefreshDiscardedError(),
|
|
4821
|
+
}
|
|
4822
|
+
this.refreshingDeferred.resolve(discarded)
|
|
4823
|
+
return discarded
|
|
4824
|
+
}
|
|
4825
|
+
|
|
4826
|
+
// Second leg of the commit guard: close the TOCTOU window between the
|
|
4827
|
+
// synchronous `storageChangedUnderUs` check and the actual storage
|
|
4828
|
+
// writes inside `_saveSession`. A concurrent `signOut → _removeSession`
|
|
4829
|
+
// can land inside `_saveSession`'s `await setItemAsync(...)` yields and
|
|
4830
|
+
// clear storage just before we overwrite it. Capture the epoch BEFORE
|
|
4831
|
+
// the save and re-check after; if it advanced, undo the write directly
|
|
4832
|
+
// (do NOT call `_removeSession` — that would emit a duplicate
|
|
4833
|
+
// SIGNED_OUT for the concurrent signOut that already fired one).
|
|
4834
|
+
const epochBeforeSave = this._sessionRemovalEpoch
|
|
4835
|
+
|
|
4681
4836
|
await this._saveSession(data.session)
|
|
4837
|
+
|
|
4838
|
+
if (this._sessionRemovalEpoch !== epochBeforeSave) {
|
|
4839
|
+
this._debug(
|
|
4840
|
+
debugName,
|
|
4841
|
+
'commit guard (post-save): _removeSession ran during _saveSession, undoing write'
|
|
4842
|
+
)
|
|
4843
|
+
await removeItemAsync(this.storage, this.storageKey)
|
|
4844
|
+
if (this.userStorage) {
|
|
4845
|
+
await removeItemAsync(this.userStorage, this.storageKey + '-user')
|
|
4846
|
+
}
|
|
4847
|
+
const discarded: CallRefreshTokenResult = {
|
|
4848
|
+
data: null,
|
|
4849
|
+
error: new AuthRefreshDiscardedError(),
|
|
4850
|
+
}
|
|
4851
|
+
this.refreshingDeferred.resolve(discarded)
|
|
4852
|
+
return discarded
|
|
4853
|
+
}
|
|
4854
|
+
|
|
4682
4855
|
await this._notifyAllSubscribers('TOKEN_REFRESHED', data.session)
|
|
4683
4856
|
|
|
4684
4857
|
const result = { data: data.session, error: null }
|
|
@@ -4790,6 +4963,11 @@ export default class GoTrueClient {
|
|
|
4790
4963
|
}
|
|
4791
4964
|
|
|
4792
4965
|
private async _removeSession() {
|
|
4966
|
+
// Bump synchronously, BEFORE any `await`, so that `_callRefreshToken`'s
|
|
4967
|
+
// post-save check sees the increment whenever this method has started —
|
|
4968
|
+
// even if it hasn't finished. Pairs with the epoch check in
|
|
4969
|
+
// `_callRefreshToken`. See `_sessionRemovalEpoch` field doc.
|
|
4970
|
+
this._sessionRemovalEpoch += 1
|
|
4793
4971
|
this._debug('#_removeSession()')
|
|
4794
4972
|
|
|
4795
4973
|
this.suppressGetSessionWarning = false
|
|
@@ -4978,58 +5156,144 @@ export default class GoTrueClient {
|
|
|
4978
5156
|
await this._stopAutoRefresh()
|
|
4979
5157
|
}
|
|
4980
5158
|
|
|
5159
|
+
/**
|
|
5160
|
+
* Tears down the client's background work: stops the auto-refresh interval,
|
|
5161
|
+
* removes the `visibilitychange` listener, closes the cross-tab
|
|
5162
|
+
* `BroadcastChannel`, and clears registered `onAuthStateChange` subscribers.
|
|
5163
|
+
*
|
|
5164
|
+
* Call this from cleanup hooks when the client is being replaced before
|
|
5165
|
+
* its JS realm is destroyed. React Strict Mode and HMR are the common
|
|
5166
|
+
* cases. Any in-flight `fetch` calls continue to completion and may still
|
|
5167
|
+
* write to storage; dispose doesn't abort them or erase storage.
|
|
5168
|
+
*
|
|
5169
|
+
* Lifecycle caveat: because in-flight refreshes are not aborted, a
|
|
5170
|
+
* disposed instance can still persist a rotated session to storage after
|
|
5171
|
+
* `dispose()` returns. A subsequent `createClient` against the same
|
|
5172
|
+
* `storageKey` will pick up that session on its next read. If you need
|
|
5173
|
+
* strict isolation between client lifecycles, await any pending auth
|
|
5174
|
+
* operation before calling `dispose()` (or change the `storageKey` for
|
|
5175
|
+
* the replacement client).
|
|
5176
|
+
*
|
|
5177
|
+
* Safe to call repeatedly.
|
|
5178
|
+
*
|
|
5179
|
+
* @category Auth
|
|
5180
|
+
*
|
|
5181
|
+
* @example Cleanup on React unmount
|
|
5182
|
+
* ```ts
|
|
5183
|
+
* useEffect(() => {
|
|
5184
|
+
* const client = createClient(...)
|
|
5185
|
+
* return () => { client.auth.dispose() }
|
|
5186
|
+
* }, [])
|
|
5187
|
+
* ```
|
|
5188
|
+
*/
|
|
5189
|
+
async dispose(): Promise<void> {
|
|
5190
|
+
this._removeVisibilityChangedCallback()
|
|
5191
|
+
await this._stopAutoRefresh()
|
|
5192
|
+
this.broadcastChannel?.close()
|
|
5193
|
+
this.broadcastChannel = null
|
|
5194
|
+
this.stateChangeEmitters.clear()
|
|
5195
|
+
}
|
|
5196
|
+
|
|
4981
5197
|
/**
|
|
4982
5198
|
* Runs the auto refresh token tick.
|
|
4983
5199
|
*/
|
|
4984
5200
|
private async _autoRefreshTokenTick() {
|
|
4985
5201
|
this._debug('#_autoRefreshTokenTick()', 'begin')
|
|
4986
5202
|
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
5203
|
+
if (this.lock != null) {
|
|
5204
|
+
// TODO(v3): remove legacy lock path. Uses `_acquireLock(0, ...)` which
|
|
5205
|
+
// throws `LockAcquireTimeoutError` immediately if the lock is held —
|
|
5206
|
+
// that's the fail-fast skip path that lets the tick bail out instead
|
|
5207
|
+
// of queuing behind a long-running operation.
|
|
5208
|
+
try {
|
|
5209
|
+
await this._acquireLock(0, async () => {
|
|
4992
5210
|
try {
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5211
|
+
const now = Date.now()
|
|
5212
|
+
try {
|
|
5213
|
+
return await this._useSession(async (result) => {
|
|
5214
|
+
const {
|
|
5215
|
+
data: { session },
|
|
5216
|
+
} = result
|
|
5217
|
+
|
|
5218
|
+
if (!session || !session.refresh_token || !session.expires_at) {
|
|
5219
|
+
this._debug('#_autoRefreshTokenTick()', 'no session')
|
|
5220
|
+
return
|
|
5221
|
+
}
|
|
5002
5222
|
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
)
|
|
5223
|
+
const expiresInTicks = Math.floor(
|
|
5224
|
+
(session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS
|
|
5225
|
+
)
|
|
5007
5226
|
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5227
|
+
this._debug(
|
|
5228
|
+
'#_autoRefreshTokenTick()',
|
|
5229
|
+
`access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks`
|
|
5230
|
+
)
|
|
5012
5231
|
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5232
|
+
if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) {
|
|
5233
|
+
await this._callRefreshToken(session.refresh_token)
|
|
5234
|
+
}
|
|
5235
|
+
})
|
|
5236
|
+
} catch (e) {
|
|
5237
|
+
console.error(
|
|
5238
|
+
'Auto refresh tick failed with error. This is likely a transient error.',
|
|
5239
|
+
e
|
|
5240
|
+
)
|
|
5241
|
+
}
|
|
5242
|
+
} finally {
|
|
5243
|
+
this._debug('#_autoRefreshTokenTick()', 'end')
|
|
5022
5244
|
}
|
|
5023
|
-
}
|
|
5024
|
-
|
|
5245
|
+
})
|
|
5246
|
+
} catch (e) {
|
|
5247
|
+
if (e instanceof LockAcquireTimeoutError) {
|
|
5248
|
+
this._debug('auto refresh token tick lock not available')
|
|
5249
|
+
} else {
|
|
5250
|
+
throw e
|
|
5025
5251
|
}
|
|
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
5252
|
}
|
|
5253
|
+
return
|
|
5254
|
+
}
|
|
5255
|
+
|
|
5256
|
+
// Lockless default: skip if a refresh is already in flight.
|
|
5257
|
+
// `_callRefreshToken` also dedupes via the same field; this is just a
|
|
5258
|
+
// fast-path skip to avoid an unnecessary storage read.
|
|
5259
|
+
if (this.refreshingDeferred !== null) {
|
|
5260
|
+
this._debug('#_autoRefreshTokenTick()', 'refresh already in flight, skipping')
|
|
5261
|
+
return
|
|
5262
|
+
}
|
|
5263
|
+
|
|
5264
|
+
try {
|
|
5265
|
+
const now = Date.now()
|
|
5266
|
+
|
|
5267
|
+
try {
|
|
5268
|
+
await this._useSession(async (result) => {
|
|
5269
|
+
const {
|
|
5270
|
+
data: { session },
|
|
5271
|
+
} = result
|
|
5272
|
+
|
|
5273
|
+
if (!session || !session.refresh_token || !session.expires_at) {
|
|
5274
|
+
this._debug('#_autoRefreshTokenTick()', 'no session')
|
|
5275
|
+
return
|
|
5276
|
+
}
|
|
5277
|
+
|
|
5278
|
+
// session will expire in this many ticks (or has already expired if <= 0)
|
|
5279
|
+
const expiresInTicks = Math.floor(
|
|
5280
|
+
(session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS
|
|
5281
|
+
)
|
|
5282
|
+
|
|
5283
|
+
this._debug(
|
|
5284
|
+
'#_autoRefreshTokenTick()',
|
|
5285
|
+
`access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks`
|
|
5286
|
+
)
|
|
5287
|
+
|
|
5288
|
+
if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) {
|
|
5289
|
+
await this._callRefreshToken(session.refresh_token)
|
|
5290
|
+
}
|
|
5291
|
+
})
|
|
5292
|
+
} catch (e) {
|
|
5293
|
+
console.error('Auto refresh tick failed with error. This is likely a transient error.', e)
|
|
5294
|
+
}
|
|
5295
|
+
} finally {
|
|
5296
|
+
this._debug('#_autoRefreshTokenTick()', 'end')
|
|
5033
5297
|
}
|
|
5034
5298
|
}
|
|
5035
5299
|
|
|
@@ -5086,24 +5350,29 @@ export default class GoTrueClient {
|
|
|
5086
5350
|
if (!calledFromInitialize) {
|
|
5087
5351
|
// called when the visibility has changed, i.e. the browser
|
|
5088
5352
|
// transitioned from hidden -> visible so we need to see if the session
|
|
5089
|
-
// should be recovered
|
|
5090
|
-
// the lock first asynchronously
|
|
5353
|
+
// should be recovered
|
|
5091
5354
|
await this.initializePromise
|
|
5092
5355
|
|
|
5093
|
-
|
|
5356
|
+
if (this.lock != null) {
|
|
5357
|
+
// TODO(v3): remove legacy lock path
|
|
5358
|
+
await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
5359
|
+
if (document.visibilityState !== 'visible') {
|
|
5360
|
+
this._debug(
|
|
5361
|
+
methodName,
|
|
5362
|
+
'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting'
|
|
5363
|
+
)
|
|
5364
|
+
return
|
|
5365
|
+
}
|
|
5366
|
+
await this._recoverAndRefresh()
|
|
5367
|
+
})
|
|
5368
|
+
} else {
|
|
5094
5369
|
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
|
|
5370
|
+
this._debug(methodName, 'visibilityState is no longer visible, skipping recovery')
|
|
5101
5371
|
return
|
|
5102
5372
|
}
|
|
5103
|
-
|
|
5104
5373
|
// recover the session
|
|
5105
5374
|
await this._recoverAndRefresh()
|
|
5106
|
-
}
|
|
5375
|
+
}
|
|
5107
5376
|
}
|
|
5108
5377
|
} else if (document.visibilityState === 'hidden') {
|
|
5109
5378
|
if (this.autoRefreshToken) {
|
|
@@ -5235,7 +5504,7 @@ export default class GoTrueClient {
|
|
|
5235
5504
|
params: MFAVerifyWebauthnParams<T>
|
|
5236
5505
|
): Promise<AuthMFAVerifyResponse>
|
|
5237
5506
|
private async _verify(params: MFAVerifyParams): Promise<AuthMFAVerifyResponse> {
|
|
5238
|
-
|
|
5507
|
+
const run = async (): Promise<AuthMFAVerifyResponse> => {
|
|
5239
5508
|
try {
|
|
5240
5509
|
return await this._useSession(async (result) => {
|
|
5241
5510
|
const { data: sessionData, error: sessionError } = result
|
|
@@ -5306,7 +5575,13 @@ export default class GoTrueClient {
|
|
|
5306
5575
|
}
|
|
5307
5576
|
throw error
|
|
5308
5577
|
}
|
|
5309
|
-
}
|
|
5578
|
+
}
|
|
5579
|
+
|
|
5580
|
+
if (this.lock != null) {
|
|
5581
|
+
// TODO(v3): remove legacy lock path
|
|
5582
|
+
return this._acquireLock(this.lockAcquireTimeout, run)
|
|
5583
|
+
}
|
|
5584
|
+
return run()
|
|
5310
5585
|
}
|
|
5311
5586
|
|
|
5312
5587
|
/**
|
|
@@ -5322,7 +5597,7 @@ export default class GoTrueClient {
|
|
|
5322
5597
|
params: MFAChallengeWebauthnParams
|
|
5323
5598
|
): Promise<Prettify<AuthMFAChallengeWebauthnResponse>>
|
|
5324
5599
|
private async _challenge(params: MFAChallengeParams): Promise<AuthMFAChallengeResponse> {
|
|
5325
|
-
|
|
5600
|
+
const run = async (): Promise<AuthMFAChallengeResponse> => {
|
|
5326
5601
|
try {
|
|
5327
5602
|
return await this._useSession(async (result) => {
|
|
5328
5603
|
const { data: sessionData, error: sessionError } = result
|
|
@@ -5395,7 +5670,13 @@ export default class GoTrueClient {
|
|
|
5395
5670
|
}
|
|
5396
5671
|
throw error
|
|
5397
5672
|
}
|
|
5398
|
-
}
|
|
5673
|
+
}
|
|
5674
|
+
|
|
5675
|
+
if (this.lock != null) {
|
|
5676
|
+
// TODO(v3): remove legacy lock path
|
|
5677
|
+
return this._acquireLock(this.lockAcquireTimeout, run)
|
|
5678
|
+
}
|
|
5679
|
+
return run()
|
|
5399
5680
|
}
|
|
5400
5681
|
|
|
5401
5682
|
/**
|
|
@@ -5404,9 +5685,6 @@ export default class GoTrueClient {
|
|
|
5404
5685
|
private async _challengeAndVerify(
|
|
5405
5686
|
params: MFAChallengeAndVerifyParams
|
|
5406
5687
|
): Promise<AuthMFAVerifyResponse> {
|
|
5407
|
-
// both _challenge and _verify independently acquire the lock, so no need
|
|
5408
|
-
// to acquire it here
|
|
5409
|
-
|
|
5410
5688
|
const { data: challengeData, error: challengeError } = await this._challenge({
|
|
5411
5689
|
factorId: params.factorId,
|
|
5412
5690
|
})
|
|
@@ -5425,7 +5703,6 @@ export default class GoTrueClient {
|
|
|
5425
5703
|
* {@see GoTrueMFAApi#listFactors}
|
|
5426
5704
|
*/
|
|
5427
5705
|
private async _listFactors(): Promise<AuthMFAListFactorsResponse> {
|
|
5428
|
-
// use #getUser instead of #_getUser as the former acquires a lock
|
|
5429
5706
|
const {
|
|
5430
5707
|
data: { user },
|
|
5431
5708
|
error: userError,
|
|
@@ -5905,8 +6182,13 @@ export default class GoTrueClient {
|
|
|
5905
6182
|
} = decodeJWT(token)
|
|
5906
6183
|
|
|
5907
6184
|
if (!options?.allowExpired) {
|
|
5908
|
-
// Reject expired JWTs should only happen if jwt argument was passed
|
|
5909
|
-
|
|
6185
|
+
// Reject expired JWTs should only happen if jwt argument was passed.
|
|
6186
|
+
// Rethrow as AuthInvalidJwtError so the outer catch converts it to { data, error }.
|
|
6187
|
+
try {
|
|
6188
|
+
validateExp(payload.exp)
|
|
6189
|
+
} catch (e) {
|
|
6190
|
+
throw new AuthInvalidJwtError(e instanceof Error ? e.message : 'JWT validation failed')
|
|
6191
|
+
}
|
|
5910
6192
|
}
|
|
5911
6193
|
|
|
5912
6194
|
const signingKey =
|