@supabase/auth-js 2.117.0 → 2.117.1-canary.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.
@@ -8,8 +8,8 @@
8
8
  ## What changed
9
9
 
10
10
  - **Default coordination is lockless.** A client constructed without a `lock` option no longer acquires `navigator.locks` or any in-process lock. In-tab concurrent refreshes are deduplicated via the pre-existing single-flight (`refreshingDeferred`); cross-tab refresh races are resolved by GoTrue's server-side parent-of-active mechanism on the v1 refresh-token path.
11
- - **Commit guard inside `_callRefreshToken`.** The client snapshots storage before the rotated-token fetch and re-reads after. If a non-null pre-fetch snapshot was cleared between the two reads (typical case: a concurrent `signOut` ran `_removeSession`), the rotated tokens are discarded instead of being written back over the cleared storage. The discarded result resolves with `{ data: null, error: new AuthRefreshDiscardedError() }`.
12
- - **New `AuthRefreshDiscardedError`** (with `isAuthRefreshDiscardedError` type guard). Surfaces through `refreshSession()` and `getSession()` results when the commit guard fires. Distinct from `AuthRetryableFetchError` (transient network) and `AuthApiError` (server rejection).
11
+ - **Commit guard inside `_callRefreshToken`.** The client snapshots storage before the rotated-token fetch and re-reads after. If a non-null pre-fetch snapshot was cleared or replaced between the two reads (a concurrent `signOut` ran `_removeSession`, or another tab's refresh rotated first), the rotated tokens are discarded instead of being written back over storage. The discarded result resolves with `{ data: null, error: new AuthRefreshDiscardedError() }`.
12
+ - **New `AuthRefreshDiscardedError`** (with `isAuthRefreshDiscardedError` type guard). Surfaces through `refreshSession()` and `setSession()` results when the commit guard fires. `getSession()` surfaces it only when storage was cleared mid-flight; when another caller's rotation replaced storage, `getSession()` returns the session now in storage instead. Distinct from `AuthRetryableFetchError` (transient network) and `AuthApiError` (server rejection).
13
13
  - **New `client.auth.dispose()`.** Tears down the auto-refresh interval, the `visibilitychange` listener, the `BroadcastChannel`, and registered `onAuthStateChange` subscribers. Idempotent. Designed for React Strict Mode and HMR cleanup hooks. In-flight `fetch` calls are not aborted — they run to completion.
14
14
  - **`lock` and `lockAcquireTimeout` options.** Accepted and honored when supplied (legacy opt-in path); both are `@deprecated` and will be removed in v3.
15
15
 
@@ -39,7 +39,7 @@ useEffect(() => {
39
39
 
40
40
  ### `AuthRefreshDiscardedError`
41
41
 
42
- Returned from `refreshSession()` / `getSession()` when the commit guard discards a successfully-rotated session.
42
+ Returned from `refreshSession()` / `setSession()` when the commit guard discards a successfully-rotated session. `getSession()` returns it only when storage was cleared mid-flight (concurrent `signOut`); when another tab's refresh replaced storage, `getSession()` hands back that session instead.
43
43
 
44
44
  ```ts
45
45
  import { isAuthRefreshDiscardedError } from '@supabase/auth-js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supabase/auth-js",
3
- "version": "2.117.0",
3
+ "version": "2.117.1-canary.0",
4
4
  "private": false,
5
5
  "description": "Official SDK for Supabase Auth",
6
6
  "keywords": [
@@ -3075,60 +3075,36 @@ export default class GoTrueClient {
3075
3075
  )
3076
3076
 
3077
3077
  if (!hasExpired) {
3078
- if (this.userStorage) {
3079
- const maybeUser: { user?: User | null } | null = (await getItemAsync(
3080
- this.userStorage,
3081
- this.storageKey + '-user'
3082
- )) as any
3083
-
3084
- if (maybeUser?.user) {
3085
- currentSession.user = maybeUser.user
3086
- } else {
3087
- currentSession.user = userNotAvailableProxy()
3088
- }
3089
- }
3090
-
3091
- // Wrap the user object with a warning proxy on the server
3092
- // This warns when properties of the user are accessed, not when session.user itself is accessed
3093
- if (
3094
- this.storage.isServer &&
3095
- currentSession.user &&
3096
- !(currentSession.user as any).__isUserNotAvailableProxy
3097
- ) {
3098
- const suppressWarningRef = { value: this.suppressGetSessionWarning }
3099
- currentSession.user = insecureUserWarningProxy(currentSession.user, suppressWarningRef)
3100
-
3101
- // Update the client-level suppression flag when the proxy suppresses the warning
3102
- if (suppressWarningRef.value) {
3103
- this.suppressGetSessionWarning = true
3104
- }
3105
- }
3106
-
3107
- return { data: { session: currentSession }, error: null }
3078
+ return { data: { session: await this._hydrateSessionUser(currentSession) }, error: null }
3108
3079
  }
3109
3080
 
3110
3081
  const { data: session, error } = await this._callRefreshToken(currentSession.refresh_token)
3111
3082
  if (error) {
3112
- // Proactive-preserve mirror: `_callRefreshToken` keeps the session
3113
- // in storage when refresh fails non-retryably but the access token
3114
- // is still inside its real expiry window. Hand the caller the
3115
- // still-valid session instead of translating the refresh error
3116
- // into `session: null`. If the access token has actually expired,
3117
- // the session is genuinely dead and the error stands. Explicit
3118
- // refresh entry points (`refreshSession`, `setSession`)
3119
- // intentionally bypass this fallback — they want to know the
3120
- // refresh failed.
3121
- const accessTokenStillValid = !!(
3122
- currentSession.expires_at && currentSession.expires_at * 1000 > Date.now()
3123
- )
3124
- if (accessTokenStillValid) {
3125
- // Race guard: a concurrent `signOut` may have cleared storage
3126
- // during the refresh attempt. Don't hand back a session that no
3127
- // longer exists on disk.
3128
- const stillStored = (await getItemAsync(this.storage, this.storageKey)) as Session | null
3129
- if (stillStored && stillStored.refresh_token === currentSession.refresh_token) {
3130
- return this._returnResult({ data: { session: currentSession }, error: null })
3131
- }
3083
+ // A failed refresh does not mean there is no session. Storage is the
3084
+ // source of truth at this point, because either:
3085
+ // - `_callRefreshToken` kept the original session in storage since
3086
+ // the access token is still inside its real expiry window (a
3087
+ // proactive refresh that failed non-retryably), or
3088
+ // - another caller, in this tab or a different one, committed its
3089
+ // own rotated session while this refresh was in flight, so this
3090
+ // one was discarded by the commit guard or rejected by the server
3091
+ // as already used.
3092
+ // Empty storage (concurrent `signOut`) or an expired stored session
3093
+ // means the session is genuinely gone and the error stands. Callers
3094
+ // that pass an explicit token (`setSession`,
3095
+ // `refreshSession({ refresh_token })`) call `_callRefreshToken`
3096
+ // directly and surface the failure instead.
3097
+ const stored = (await getItemAsync(this.storage, this.storageKey)) as Session | null
3098
+ if (
3099
+ stored &&
3100
+ this._isValidSession(stored) &&
3101
+ stored.expires_at &&
3102
+ stored.expires_at * 1000 > Date.now()
3103
+ ) {
3104
+ return this._returnResult({
3105
+ data: { session: await this._hydrateSessionUser(stored) },
3106
+ error: null,
3107
+ })
3132
3108
  }
3133
3109
  return this._returnResult({ data: { session: null }, error })
3134
3110
  }
@@ -3139,6 +3115,35 @@ export default class GoTrueClient {
3139
3115
  }
3140
3116
  }
3141
3117
 
3118
+ /**
3119
+ * Completes a session read back from storage so it matches what callers of
3120
+ * `getSession()` expect: fills in `session.user` from `userStorage` when the
3121
+ * client keeps the user in split storage, and wraps the user in the
3122
+ * insecure-access warning proxy on the server.
3123
+ */
3124
+ private async _hydrateSessionUser(session: Session): Promise<Session> {
3125
+ if (this.userStorage) {
3126
+ const maybeUser: { user?: User | null } | null = (await getItemAsync(
3127
+ this.userStorage,
3128
+ this.storageKey + '-user'
3129
+ )) as any
3130
+
3131
+ session.user = maybeUser?.user ? maybeUser.user : userNotAvailableProxy()
3132
+ }
3133
+
3134
+ // Warns when properties of the user are accessed, not when session.user itself is accessed.
3135
+ if (this.storage.isServer && session.user && !(session.user as any).__isUserNotAvailableProxy) {
3136
+ const suppressWarningRef = { value: this.suppressGetSessionWarning }
3137
+ session.user = insecureUserWarningProxy(session.user, suppressWarningRef)
3138
+
3139
+ if (suppressWarningRef.value) {
3140
+ this.suppressGetSessionWarning = true
3141
+ }
3142
+ }
3143
+
3144
+ return session
3145
+ }
3146
+
3142
3147
  /**
3143
3148
  * Gets the current user details if there is an existing session. This method
3144
3149
  * performs a network request to the Supabase Auth server, so the returned
@@ -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.117.0'
7
+ export const version = '2.117.1-canary.0'