mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-21 21:09:49 +08:00
fix(rde): keep session identity stable across access-token refreshes
Access-token-only client sessions (no refresh token) were keyed by the access-token string. The remote development environment dashboard re-mints its short-lived access token every ~30-60s, so every refresh produced a brand-new InternalSession object. Session-scoped caches (useUser/useConfig/ useTeams/...) are keyed by the session object, so each refresh cold- invalidated them, suspended the tree, and blanked the dashboard. Key access-only sessions by the token's stable refresh_token_id instead of the raw token string, and add InternalSession.updateAccessToken so a freshly minted token is pushed into the existing session in place rather than constructing a new one.
This commit is contained in:
parent
96273a9d65
commit
9da1aac3ad
@ -124,6 +124,14 @@ export class InternalSession {
|
||||
if (ofTokens.refreshToken) {
|
||||
return `refresh-${ofTokens.refreshToken}`;
|
||||
} else if (ofTokens.accessToken) {
|
||||
// Access-only sessions (no refresh token) are keyed by the underlying session's `refresh_token_id`, not the
|
||||
// access token string: access tokens get re-minted frequently, and keying by the raw token would spawn a new
|
||||
// session (and cold-invalidate every session-scoped cache) on each refresh. Falls back to the raw token if
|
||||
// the JWT can't be decoded.
|
||||
const refreshTokenId = decodeAccessTokenIfValid(ofTokens.accessToken)?.refresh_token_id;
|
||||
if (refreshTokenId) {
|
||||
return `access-session-${refreshTokenId}`;
|
||||
}
|
||||
return `access-${ofTokens.accessToken}`;
|
||||
} else {
|
||||
return "not-logged-in";
|
||||
@ -210,6 +218,21 @@ export class InternalSession {
|
||||
return accessToken ? { accessToken, refreshToken: this._refreshToken } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs a fresh access token into this session in place, keeping the session object (and therefore every
|
||||
* session-scoped cache) stable instead of constructing a new InternalSession. Caller must pass a token that
|
||||
* belongs to the same session. No-op if the session is invalid, the token can't be decoded, or it's unchanged;
|
||||
* never clears an existing token.
|
||||
*/
|
||||
updateAccessToken(accessToken: string | null) {
|
||||
if (this._knownToBeInvalid.get()) return;
|
||||
if (!accessToken) return;
|
||||
const newAccessToken = AccessToken.createIfValid(accessToken);
|
||||
if (!newAccessToken) return;
|
||||
if (this._accessToken.get()?.token === newAccessToken.token) return;
|
||||
this._accessToken.set(newAccessToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually mark the access token as expired, even if the date on its payload may still be valid.
|
||||
*
|
||||
|
||||
@ -1547,10 +1547,14 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
||||
const tokenStore = this._getOrCreateTokenStore(await this._createCookieHelper());
|
||||
tokenStore.set(tokens);
|
||||
|
||||
// Pre-fetch the current user for the new session so the cache is already
|
||||
// populated when useUser() re-renders, avoiding a stale-cache render cycle.
|
||||
const newSession = this._getSessionFromTokenStore(tokenStore);
|
||||
this._currentUserCache.getOrWait([newSession], "write-only").catch(() => {});
|
||||
// If these tokens resolve to a session we already have (eg. the RDE dashboard re-installing a freshly minted
|
||||
// access token for the same access-only session), push the new token into it in place; constructing a new
|
||||
// session here would cold-invalidate every session-scoped cache and suspend the UI on each refresh.
|
||||
const session = this._getSessionFromTokenStore(tokenStore);
|
||||
session.updateAccessToken(tokens.accessToken);
|
||||
|
||||
// Pre-fetch the current user so the cache is warm when useUser() re-renders (write-only, so it never suspends).
|
||||
this._currentUserCache.getOrWait([session], "write-only").catch(() => {});
|
||||
}
|
||||
|
||||
protected _getTokenStoreInitForFreshTokens(tokens: { accessToken: string | null, refreshToken: string }): TokenStoreInit | undefined {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user