@supabase/gotrue-js 2.108.1 → 2.108.2-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 +19 -0
- package/dist/main/GoTrueClient.d.ts.map +1 -1
- package/dist/main/GoTrueClient.js +97 -8
- package/dist/main/GoTrueClient.js.map +1 -1
- package/dist/main/lib/constants.d.ts +8 -0
- package/dist/main/lib/constants.d.ts.map +1 -1
- package/dist/main/lib/constants.js +9 -1
- package/dist/main/lib/constants.js.map +1 -1
- package/dist/main/lib/fetch.d.ts.map +1 -1
- package/dist/main/lib/fetch.js +5 -3
- package/dist/main/lib/fetch.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 +19 -0
- package/dist/module/GoTrueClient.d.ts.map +1 -1
- package/dist/module/GoTrueClient.js +98 -9
- package/dist/module/GoTrueClient.js.map +1 -1
- package/dist/module/lib/constants.d.ts +8 -0
- package/dist/module/lib/constants.d.ts.map +1 -1
- package/dist/module/lib/constants.js +8 -0
- package/dist/module/lib/constants.js.map +1 -1
- package/dist/module/lib/fetch.d.ts.map +1 -1
- package/dist/module/lib/fetch.js +5 -3
- package/dist/module/lib/fetch.js.map +1 -1
- package/dist/module/lib/version.d.ts +1 -1
- package/dist/module/lib/version.d.ts.map +1 -1
- package/dist/module/lib/version.js +1 -1
- package/dist/module/lib/version.js.map +1 -1
- package/dist/tsconfig.module.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/GoTrueClient.ts +116 -13
- package/src/lib/constants.ts +9 -0
- package/src/lib/fetch.ts +5 -3
- package/src/lib/version.ts +1 -1
package/package.json
CHANGED
package/src/GoTrueClient.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
EXPIRY_MARGIN_MS,
|
|
7
7
|
GOTRUE_URL,
|
|
8
8
|
JWKS_TTL,
|
|
9
|
+
REFRESH_FAILURE_COOLDOWN_MS,
|
|
9
10
|
STORAGE_KEY,
|
|
10
11
|
} from './lib/constants'
|
|
11
12
|
import {
|
|
@@ -288,6 +289,25 @@ export default class GoTrueClient {
|
|
|
288
289
|
protected autoRefreshTickTimeout: ReturnType<typeof setTimeout> | null = null
|
|
289
290
|
protected visibilityChangedCallback: (() => Promise<any>) | null = null
|
|
290
291
|
protected refreshingDeferred: Deferred<CallRefreshTokenResult> | null = null
|
|
292
|
+
/**
|
|
293
|
+
* Cache of the most recent refresh failure, keyed by the refresh token
|
|
294
|
+
* that failed. Serial callers passing the *same* token within
|
|
295
|
+
* `REFRESH_FAILURE_COOLDOWN_MS` (including subsequent auto-refresh ticks)
|
|
296
|
+
* receive this cached result instead of firing another `/token` request.
|
|
297
|
+
* Callers passing a *different* token (token rotation pickup, explicit
|
|
298
|
+
* `setSession`/`refreshSession({ refresh_token })`, multi-account switch)
|
|
299
|
+
* bypass the cache and attempt a fresh refresh as they should.
|
|
300
|
+
* Cleared on any successful refresh (locally or via BroadcastChannel from
|
|
301
|
+
* another tab) and on `_removeSession`.
|
|
302
|
+
*
|
|
303
|
+
* Pairs with `refreshingDeferred`: concurrent callers share the in-flight
|
|
304
|
+
* promise, serial callers within the cooldown share the failure result.
|
|
305
|
+
*/
|
|
306
|
+
protected lastRefreshFailure: {
|
|
307
|
+
refreshToken: string
|
|
308
|
+
result: CallRefreshTokenResult
|
|
309
|
+
expiresAt: number
|
|
310
|
+
} | null = null
|
|
291
311
|
/**
|
|
292
312
|
* Monotonic counter incremented at the top of `_removeSession`, before any
|
|
293
313
|
* `await`. The commit guard inside `_callRefreshToken` captures this value
|
|
@@ -482,6 +502,13 @@ export default class GoTrueClient {
|
|
|
482
502
|
this.broadcastChannel?.addEventListener('message', async (event) => {
|
|
483
503
|
this._debug('received broadcast notification from other tab or client', event)
|
|
484
504
|
|
|
505
|
+
// Another tab successfully refreshed or signed in — any cached
|
|
506
|
+
// failure in this tab is stale and should not block the next
|
|
507
|
+
// refresh attempt.
|
|
508
|
+
if (event.data.event === 'TOKEN_REFRESHED' || event.data.event === 'SIGNED_IN') {
|
|
509
|
+
this.lastRefreshFailure = null
|
|
510
|
+
}
|
|
511
|
+
|
|
485
512
|
try {
|
|
486
513
|
await this._notifyAllSubscribers(event.data.event, event.data.session, false) // broadcast = false so we don't get an endless loop of messages
|
|
487
514
|
} catch (error) {
|
|
@@ -2979,6 +3006,27 @@ export default class GoTrueClient {
|
|
|
2979
3006
|
|
|
2980
3007
|
const { data: session, error } = await this._callRefreshToken(currentSession.refresh_token)
|
|
2981
3008
|
if (error) {
|
|
3009
|
+
// Proactive-preserve mirror: `_callRefreshToken` keeps the session
|
|
3010
|
+
// in storage when refresh fails non-retryably but the access token
|
|
3011
|
+
// is still inside its real expiry window. Hand the caller the
|
|
3012
|
+
// still-valid session instead of translating the refresh error
|
|
3013
|
+
// into `session: null`. If the access token has actually expired,
|
|
3014
|
+
// the session is genuinely dead and the error stands. Explicit
|
|
3015
|
+
// refresh entry points (`refreshSession`, `setSession`)
|
|
3016
|
+
// intentionally bypass this fallback — they want to know the
|
|
3017
|
+
// refresh failed.
|
|
3018
|
+
const accessTokenStillValid = !!(
|
|
3019
|
+
currentSession.expires_at && currentSession.expires_at * 1000 > Date.now()
|
|
3020
|
+
)
|
|
3021
|
+
if (accessTokenStillValid) {
|
|
3022
|
+
// Race guard: a concurrent `signOut` may have cleared storage
|
|
3023
|
+
// during the refresh attempt. Don't hand back a session that no
|
|
3024
|
+
// longer exists on disk.
|
|
3025
|
+
const stillStored = (await getItemAsync(this.storage, this.storageKey)) as Session | null
|
|
3026
|
+
if (stillStored && stillStored.refresh_token === currentSession.refresh_token) {
|
|
3027
|
+
return this._returnResult({ data: { session: currentSession }, error: null })
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
2982
3030
|
return this._returnResult({ data: { session: null }, error })
|
|
2983
3031
|
}
|
|
2984
3032
|
|
|
@@ -4730,22 +4778,16 @@ export default class GoTrueClient {
|
|
|
4730
4778
|
const { error } = await this._callRefreshToken(currentSession.refresh_token)
|
|
4731
4779
|
|
|
4732
4780
|
if (error) {
|
|
4733
|
-
//
|
|
4734
|
-
//
|
|
4735
|
-
//
|
|
4781
|
+
// `_callRefreshToken` is the single source of truth for refresh
|
|
4782
|
+
// outcomes: it removes the session itself when the access token
|
|
4783
|
+
// is actually expired, and preserves it when the token is still
|
|
4784
|
+
// valid (proactive-preserve). Don't second-guess that here — a
|
|
4785
|
+
// local `_removeSession` would emit a duplicate `SIGNED_OUT` on
|
|
4786
|
+
// genuine failures and undo the proactive-preserve at init time.
|
|
4736
4787
|
if (isAuthRefreshDiscardedError(error)) {
|
|
4737
4788
|
this._debug(debugName, 'refresh discarded by commit guard', error)
|
|
4738
4789
|
} else {
|
|
4739
4790
|
this._debug(debugName, 'refresh failed', error)
|
|
4740
|
-
|
|
4741
|
-
if (!isAuthRetryableFetchError(error)) {
|
|
4742
|
-
this._debug(
|
|
4743
|
-
debugName,
|
|
4744
|
-
'refresh failed with a non-retryable error, removing the session',
|
|
4745
|
-
error
|
|
4746
|
-
)
|
|
4747
|
-
await this._removeSession()
|
|
4748
|
-
}
|
|
4749
4791
|
}
|
|
4750
4792
|
}
|
|
4751
4793
|
}
|
|
@@ -4798,6 +4840,26 @@ export default class GoTrueClient {
|
|
|
4798
4840
|
return this.refreshingDeferred.promise
|
|
4799
4841
|
}
|
|
4800
4842
|
|
|
4843
|
+
// Serial failure cooldown: callers passing the *same* refresh token
|
|
4844
|
+
// after a recent failure receive the cached result instead of firing
|
|
4845
|
+
// another `/token` request. This caps the proactive-refresh storm
|
|
4846
|
+
// where every `getSession()` call inside the 90s EXPIRY_MARGIN_MS
|
|
4847
|
+
// window kept re-firing against the same broken refresh token during
|
|
4848
|
+
// outages. Concurrent callers already share `refreshingDeferred`; this
|
|
4849
|
+
// cache covers serial callers spaced across cooldown windows.
|
|
4850
|
+
//
|
|
4851
|
+
// Token-keyed so callers with a fresh refresh token (rotation pickup
|
|
4852
|
+
// from another tab, explicit `setSession`/`refreshSession({ refresh_token })`,
|
|
4853
|
+
// multi-account switch) bypass the cache and attempt a real refresh.
|
|
4854
|
+
if (
|
|
4855
|
+
this.lastRefreshFailure &&
|
|
4856
|
+
this.lastRefreshFailure.refreshToken === refreshToken &&
|
|
4857
|
+
Date.now() < this.lastRefreshFailure.expiresAt
|
|
4858
|
+
) {
|
|
4859
|
+
this._debug('#_callRefreshToken()', 'returning cached failure (cooldown active)')
|
|
4860
|
+
return this.lastRefreshFailure.result
|
|
4861
|
+
}
|
|
4862
|
+
|
|
4801
4863
|
// Refresh tokens are long-lived bearer credentials; do NOT include any
|
|
4802
4864
|
// fragment of the token in the debug tag, even when `debug: true` is
|
|
4803
4865
|
// enabled (logs may be forwarded to third-party services).
|
|
@@ -4881,6 +4943,10 @@ export default class GoTrueClient {
|
|
|
4881
4943
|
|
|
4882
4944
|
const result = { data: data.session, error: null }
|
|
4883
4945
|
|
|
4946
|
+
// Refresh succeeded — clear any cached failure so the next caller
|
|
4947
|
+
// (including the auto-refresh ticker) attempts a real refresh again.
|
|
4948
|
+
this.lastRefreshFailure = null
|
|
4949
|
+
|
|
4884
4950
|
this.refreshingDeferred.resolve(result)
|
|
4885
4951
|
|
|
4886
4952
|
return result
|
|
@@ -4891,7 +4957,40 @@ export default class GoTrueClient {
|
|
|
4891
4957
|
const result = { data: null, error }
|
|
4892
4958
|
|
|
4893
4959
|
if (!isAuthRetryableFetchError(error)) {
|
|
4894
|
-
|
|
4960
|
+
// Proactive vs reactive distinction: a refresh fires whenever
|
|
4961
|
+
// the access token is within EXPIRY_MARGIN_MS of expiry. If the
|
|
4962
|
+
// access token is *still valid* at this moment, the refresh was
|
|
4963
|
+
// proactive and the existing session is still usable until its
|
|
4964
|
+
// real expiry — destroying it now would log out a user whose
|
|
4965
|
+
// access token works. If the access token has actually expired,
|
|
4966
|
+
// the refresh token is the only credential left and it just got
|
|
4967
|
+
// rejected — the session is genuinely dead. `__loadSession`
|
|
4968
|
+
// mirrors this distinction on the read path so callers see the
|
|
4969
|
+
// preserved session instead of `session: null`.
|
|
4970
|
+
const storedNow = (await getItemAsync(this.storage, this.storageKey)) as Session | null
|
|
4971
|
+
const accessTokenStillValid = !!(
|
|
4972
|
+
storedNow?.expires_at && storedNow.expires_at * 1000 > Date.now()
|
|
4973
|
+
)
|
|
4974
|
+
|
|
4975
|
+
if (accessTokenStillValid) {
|
|
4976
|
+
this._debug(
|
|
4977
|
+
debugName,
|
|
4978
|
+
'proactive refresh failed, access token still valid — preserving session'
|
|
4979
|
+
)
|
|
4980
|
+
} else {
|
|
4981
|
+
await this._removeSession()
|
|
4982
|
+
}
|
|
4983
|
+
}
|
|
4984
|
+
|
|
4985
|
+
// Cache the failure so serial callers (and the next auto-refresh
|
|
4986
|
+
// tick) passing the same refresh token within the cooldown window
|
|
4987
|
+
// receive it synchronously instead of firing another `/token`
|
|
4988
|
+
// call. Set after the optional `_removeSession` above (which
|
|
4989
|
+
// clears the cache as part of teardown) so the cache survives.
|
|
4990
|
+
this.lastRefreshFailure = {
|
|
4991
|
+
refreshToken,
|
|
4992
|
+
result,
|
|
4993
|
+
expiresAt: Date.now() + REFRESH_FAILURE_COOLDOWN_MS,
|
|
4895
4994
|
}
|
|
4896
4995
|
|
|
4897
4996
|
this.refreshingDeferred?.resolve(result)
|
|
@@ -4995,6 +5094,10 @@ export default class GoTrueClient {
|
|
|
4995
5094
|
this._sessionRemovalEpoch += 1
|
|
4996
5095
|
this._debug('#_removeSession()')
|
|
4997
5096
|
|
|
5097
|
+
// The session is gone — no point holding on to a cached refresh failure
|
|
5098
|
+
// for a token that no longer exists. Synchronous, before any `await`.
|
|
5099
|
+
this.lastRefreshFailure = null
|
|
5100
|
+
|
|
4998
5101
|
this.suppressGetSessionWarning = false
|
|
4999
5102
|
|
|
5000
5103
|
await removeItemAsync(this.storage, this.storageKey)
|
package/src/lib/constants.ts
CHANGED
|
@@ -12,6 +12,15 @@ export const AUTO_REFRESH_TICK_THRESHOLD = 3
|
|
|
12
12
|
*/
|
|
13
13
|
export const EXPIRY_MARGIN_MS = AUTO_REFRESH_TICK_THRESHOLD * AUTO_REFRESH_TICK_DURATION_MS
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* After a refresh fails, serial callers (including the next auto-refresh
|
|
17
|
+
* tick) within this window receive the cached failure instead of firing
|
|
18
|
+
* another /token request. Two ticks: each outage burns at most one /token
|
|
19
|
+
* call per cooldown window. Cleared on any successful refresh (locally or
|
|
20
|
+
* via BroadcastChannel from another tab) and on `_removeSession`.
|
|
21
|
+
*/
|
|
22
|
+
export const REFRESH_FAILURE_COOLDOWN_MS = 2 * AUTO_REFRESH_TICK_DURATION_MS
|
|
23
|
+
|
|
15
24
|
export const GOTRUE_URL = 'http://localhost:9999'
|
|
16
25
|
export const STORAGE_KEY = 'supabase.auth.token'
|
|
17
26
|
export const AUDIENCE = ''
|
package/src/lib/fetch.ts
CHANGED
|
@@ -69,10 +69,12 @@ const _getErrorMessage = (err: unknown): string => {
|
|
|
69
69
|
return JSON.stringify(err)
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
// 502, 503, 504: Standard server/gateway errors
|
|
73
|
-
// 520-
|
|
72
|
+
// 500, 501, 502, 503, 504: Standard server/gateway errors
|
|
73
|
+
// 520-529, 530: Cloudflare-specific error codes (web server down, connection timed out, etc.)
|
|
74
74
|
// These are infrastructure errors and should not cause session invalidation.
|
|
75
|
-
const NETWORK_ERROR_CODES = [
|
|
75
|
+
const NETWORK_ERROR_CODES = [
|
|
76
|
+
500, 501, 502, 503, 504, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530,
|
|
77
|
+
]
|
|
76
78
|
|
|
77
79
|
export async function handleError(error: unknown) {
|
|
78
80
|
if (!looksLikeFetchResponse(error)) {
|
package/src/lib/version.ts
CHANGED
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
// - Debugging and support (identifying which version is running)
|
|
5
5
|
// - Telemetry and logging (version reporting in errors/analytics)
|
|
6
6
|
// - Ensuring build artifacts match the published package version
|
|
7
|
-
export const version = '2.108.1'
|
|
7
|
+
export const version = '2.108.2-canary.1'
|