fix(auth): make clear* hydration-safe, migrate raw-key blobs, doc keepalive/memo caveats

Address round-1 @claude review of #1592:

- OAuthStorageBase.clear* no longer mutate the pre-hydration (empty) store —
  a clear issued before hydration is deferred until it lands, so it merges
  onto the real persisted state instead of (a) being resurrected by the late
  rehydrate merge or (b) persisting a near-empty blob that clobbers every
  other server's on-disk credential. Already-hydrated clears stay synchronous.
- store.setServerState migrates a pre-normalization raw-key blob onto the
  canonical key on first partial write (was: shadowed it with a fresh
  canonical entry, orphaning the raw blob's other fields). clearServerState
  now also drops the raw-key orphan.
- Document the keepalive 64KB combined-body ceiling on the remote-storage
  write, the getRemoteOAuthStorage memo key intentionally omitting fetchFn,
  and the real ordering invariant behind the sync getScope.
- Tests for the clear-before-hydration and raw-key-migration paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
This commit is contained in:
cliffhall 2026-07-01 19:20:49 -04:00
commit 8c10b8eb8e

View file

@ -72,6 +72,15 @@ export function createRemoteStorageAdapter(
// full-page navigation (the OAuth authorize redirect): without it the
// browser may abort the request mid-flight and the just-saved
// codeVerifier/clientInformation never reaches disk.
//
// Caveat: the fetch spec caps the *combined* body size of all in-flight
// keepalive requests at 64 KB. `value` here is the whole store blob
// (every server's tokens + metadata), so a user with a very large
// number of authorized servers could in theory exceed that and have the
// browser reject this write. That ceiling is well above realistic
// inspector usage; if it ever bites, the fix is per-server stores rather
// than dropping `keepalive` (which would reintroduce the redirect-abort
// bug this guards against). The catch below surfaces such a rejection.
const res = await fetchFn(url, {
method: "POST",
headers,