stack/packages/template
BilalG1 76fc62e98b
fix(rde): stop the RDE dashboard blanking on every access-token refresh (#1566)
## Problem

In the Remote Development Environment (`hexclave dev`), the dashboard
suspends and goes **blank for a moment every ~30–60 seconds**, then
repopulates. It never happens on a plain `pnpm dev` dashboard.

## Root cause

The RDE auth gate
([`remote-development-environment-auth-gate.tsx`](../blob/dev/apps/dashboard/src/app/remote-development-environment-auth-gate.tsx))
keeps the browser signed in by re-installing a freshly minted
**access-token-only** session (`signInWithTokens({ accessToken,
refreshToken: "" })`) on a timer (capped by
`RDE_ACCESS_TOKEN_MAX_AGE_MS`).

`InternalSession.calculateSessionKey` keyed access-only sessions by the
**access-token string**:

```ts
} else if (ofTokens.accessToken) {
  return `access-${ofTokens.accessToken}`;   // 👈 changes on every refresh
}
```

So each refresh = a new key = a brand-new `InternalSession` object.
Every session-scoped cache (`useUser` / `useConfig` / `useTeams` /
`useOwnedProjects`, via `createCacheBySession`) is keyed by the
**session object**, so a new object means a cold cache → pending promise
→ `React.use()` suspends → the whole tree falls back to its (empty)
Suspense boundary.

It only *shows* in RDE because the backend is remote: the post-swap
refetch has real network latency, so the blank is visible for hundreds
of ms. On localhost the same swap is a sub-frame flicker. (A background
`refresh()`/write-only is stale-preserving and does **not** suspend —
only a new session dependency does.)

## Fix

Two changes in the SDK source (`packages/shared` + `packages/template`;
the `js`/`next`/`react`/`tanstack-start` copies are generated):

1. **Stable key for access-only sessions.** Key by the token's
`refresh_token_id` (decoded from the JWT) instead of the raw token
string. Every access token minted for the same session shares that id,
so the session identity — and therefore every cache — stays stable
across refreshes. Falls back to the raw token if the JWT can't be
decoded. Refresh-keyed and not-logged-in paths are untouched.

2. **In-place token update.** New
`InternalSession.updateAccessToken(token)`, called from
`_signInToAccountWithTokens`, pushes the fresh token into the reused
session object instead of constructing a new one (no-op when the session
is invalid / the token is unchanged / null).

Net effect: re-minting the RDE access token reuses the same
`InternalSession`, caches stay warm, nothing suspends, no blank.

## Why this is safe

- Session reuse is scoped **per token store** —
`_sessionsByTokenStoreAndSessionKey` is a `WeakMap` keyed by the
token-store object first, then the session key — so server-side
per-request sessions remain isolated regardless of how coarse the key
is. No cross-user/cross-session mixup.
- `refresh_token_id` is a per-session UUID; the coarser key only merges
*the same session's* successive access tokens, which is the intent.
- This keying convention matches the existing `refresh-${refreshToken}`
path (key by the unique token value).

## Testing

- **Reproduced live** via a temporary harness driven through a real
browser: forcing the session-identity swap triggered the exact
cold-cache refetch storm (`useUser`/`useTeams`/`useOwnedProjects`
refetching) that produces the blank.
- **Unit-verified** the new behavior against the built `shared` dist:
two access tokens sharing a `refresh_token_id` → same session key;
different ids → different keys; opaque token → fallback; refresh-keyed
unchanged; `updateAccessToken` swaps the token in place and is a correct
no-op when invalid/unchanged/null.
- **Typecheck + lint clean** across `shared`, `template`, `next`,
`react`, `tanstack-start`, and the dashboard.
- Reviewed by independent passes for correctness, security/blast-radius,
and simplification — no actionable findings.

## How to verify in the real RDE path

Set `RDE_ACCESS_TOKEN_MAX_AGE_MS` to e.g. `5000` in the auth gate and
run `hexclave dev`: before this change the dashboard blanks every few
seconds; after, it stays populated.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Stops the RDE dashboard from going blank every 30–60s by keeping session
identity stable during access‑token refreshes. Session-scoped caches
stay warm, so the UI no longer suspends.

- **Bug Fixes**
- Key access-only sessions by the JWT `refresh_token_id` (stable across
re‑mints); fall back to the raw token if undecodable. Implemented in
`packages/shared`; tests in `packages/shared/src/sessions.test.ts`.
- Add `InternalSession.updateAccessToken()` and use it in
`_signInToAccountWithTokens` to update the token in place only when the
incoming pair resolves to the same `sessionKey` (rejects
foreign/null/unchanged/undecodable; covers access‑only and
refresh‑backed sessions). Prefetch the current user via
`runAsynchronously` in write‑only mode. Implemented in `packages/shared`
and `packages/template`.

<sup>Written for commit fdaf2f28be.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1566?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

* **Bug Fixes**
* Improved session stability and token management to enhance
authentication reliability.
* Strengthened session validation to prevent stale token-related issues.

* **Tests**
* Added comprehensive test coverage for session and token handling
mechanisms.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-10 11:13:33 -07:00
..
scripts Fix Vite environment variables 2026-06-05 16:42:11 -07:00
src fix(rde): stop the RDE dashboard blanking on every access-token refresh (#1566) 2026-06-10 11:13:33 -07:00
.env Vanilla JS client library (#408) 2025-02-08 02:31:03 +01:00
.eslintrc.cjs Fix Vite environment variables 2026-06-05 16:42:11 -07:00
.gitignore Vanilla JS client library (#408) 2025-02-08 02:31:03 +01:00
components.json Vanilla JS client library (#408) 2025-02-08 02:31:03 +01:00
LICENSE Vanilla JS client library (#408) 2025-02-08 02:31:03 +01:00
next-env.d.ts Vanilla JS client library (#408) 2025-02-08 02:31:03 +01:00
package-template.json chore: update package versions 2026-06-10 18:10:23 +00:00
package.json chore: update package versions 2026-06-10 18:10:23 +00:00
postcss.config.js Vanilla JS client library (#408) 2025-02-08 02:31:03 +01:00
quetzal.config.json Vanilla JS client library (#408) 2025-02-08 02:31:03 +01:00
README.md Update package README 2026-05-28 09:59:54 -07:00
tailwind.config.js feat(hexclave): PR 3 — native @hexclave/* source rename + delete dual-publish wiring (#1482) 2026-05-29 15:21:59 -07:00
tsconfig.json In-source unit tests (#429) 2025-02-14 11:47:52 -08:00
tsdown.config.ts Fix build 2026-02-27 00:48:07 -08:00
vitest.config.ts feat(hexclave): PR 3 — native @hexclave/* source rename + delete dual-publish wiring (#1482) 2026-05-29 15:21:59 -07:00

This package has been renamed.

Stack Auth is now Hexclave! The new packages are:

  • @hexclave/next
  • @hexclave/react
  • @hexclave/js
  • @hexclave/cli

See the migration guide for more information.