Web: migrate auth store to shared /store API (RemoteOAuthStorage parity with TUI/CLI)#1592
Conversation
|
@claude review |
|
Claude finished @cliffhall's task in 3m 10s —— View job Review: Web OAuth store migration to shared
|
…palive/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
|
Thanks for the thorough review. Addressed all five in 1. 2. 3. 4. Memo key omits 5.
|
|
@claude review |
|
Claude finished @cliffhall's task in 2m 2s —— View job Re-review: fixes from
|
Round-2 @claude review of #1592 caught a seventh member of the clear family that the first pass missed: clearClientInformation still wrote to the store synchronously, carrying the same pre-hydration clobber risk the clearAfterHydration helper was written for (a clear before hydration lands persists a near-empty blob, overwriting the whole on-disk oauth.json). Wrap its mutation the same way and add a clear-before-hydration test proving a sibling token (and thus every other server's blob) survives. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
|
Good catch on the seventh clear-family member. Fixed in
|
|
@claude review |
|
Claude finished @cliffhall's task in 1m 16s —— View job Re-review:
|
| Write | Guard |
|---|---|
saveClientInformation (L157), savePreregistered… (L168), saveTokens (L203), saveCodeVerifier (L226), saveScope (L260), saveIdpSession (L317) |
await this.hydrated ✓ |
clearClientInformation (L183), clearTokens (L211), clearScope (L265), clear (L295), clearIdpSession (L322), clearEnterpriseManagedResourceServers (L327) |
clearAfterHydration ✓ |
Every mutation is now hydration-safe — no unguarded synchronous writes remain. This resolves the last item; the PR looks good to merge from my side. 👍
One optional doc nit (non-blocking, pre-existing — not from this commit)
getClientRegistrationKind (oauth-storage.ts:144-149) is the other synchronous getter besides getScope, and it relies on the same "a preceding awaited read flushed hydration" invariant: its only caller, buildOAuthConnectionState (connection-state.ts:94), reads it after awaiting getClientInformation/getServerMetadata (L86-89). That's safe today, but unlike getScope (which you tightened with an excellent invariant docstring in 8c10b8eb) it carries no such note — so a future refactor reading it without a preceding awaited storage read would silently get pre-hydration undefined on the callback path. Worth a one-line comment mirroring the getScope one, but purely a nicety. Fix this →
· branch v2/1548-web-remote-oauth-storage
… getScope Round-3 @claude review of #1592 (optional nit): getClientRegistrationKind is the other synchronous getter that, like getScope, is safe only because its sole caller (buildOAuthConnectionState) awaits getClientInformation/ getServerMetadata first, flushing hydration before this runs on the post-redirect callback path. Add a docstring mirroring getScope's so a future refactor doesn't read it without a preceding awaited storage read. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
|
Thanks — glad the seventh clear member and the invariant docs all check out. Addressed the optional nit too in |
|
@claude review |
|
Claude finished @cliffhall's task in 1m 38s —— View job Re-review (
|
|
📋 Smoke test & AGENTS.md audit for this PR: #1548 (comment) (posted on the linked issue #1548). |
|
My plan was to remove Zustand at the same time, which should eliminate all of the hydration gymnastics entirely (with just a nice clean async read/write). |
…ase change; tree unchanged)
Closes #1548
Wave 1 of #1579. Migrates the web client's OAuth storage from
BrowserOAuthStorage(sessionStorage) toRemoteOAuthStorage, so it reads/writes the same~/.mcp-inspector/storage/oauth.jsonthe TUI/CLI use — giving web ⇄ TUI ⇄ CLI parity. Re-implemented deliberately from the reference in #1510, adapted to currentv2/main(which has since gained EMA +connection-state).What changed
Core (shared with TUI/CLI):
OAuthStorageBasenow drives a single async hydration and exposesready()/getHydrationError(). Every read on the post-redirect callback path (getClientInformation,getTokens,getCodeVerifier,getServerMetadata,getIdpSession) and every save awaits hydration first, so a late hydration merge can't clobber a value written before it landed.getCodeVerifierandgetServerMetadatabecome async (interface +BaseOAuthClientProvider+ EMAidpOidc/connection-statecallers updated to await).skipHydration: trueso the constructor's explicitrehydrate()is the sole hydration (no auto-hydration to race it).store.tsaddsnormalizeServerUrlso a token saved underhttps://Example.com/mcpis found when the CLI later asks forhttps://example.com/mcp/;getServerStatefalls back to the raw key for pre-normalization blobs.remote-storageadapter POSTs withkeepalive: true(survives the OAuth authorize redirect), surfaces Zustand's otherwise-swallowed persist write failures, and emits richer read/write error messages.generateOAuthStatethrows ifcrypto.getRandomValuesis unavailable instead of silently degrading toMath.random.NodeOAuthStoragehonorsMCP_INSPECTOR_OAUTH_STATE_PATHfor isolated fixtures.Web:
lib/remoteOAuthStorageaccessor (one instance per{baseUrl, authToken}) wiresRemoteOAuthStorageintoenvironmentFactory(the connection path), the EMA IdP hook, and the per-server "clear OAuth" action, so all three share one in-memory view ofoauth.json.clearServerOAuthStatenow takes the injected storage instead of reaching for the sessionStorage singleton.Tests
oauth-storage.test.ts(async-hydration gating, hydration-error fallback,normalizeServerUrl), keepalive + persist-failure adapter coverage, agetBrowserOAuthStoragesingleton test, thegenerateOAuthStatethrow path, and aremoteOAuthStoragememoization test.normalizeServerUrlfor raw-key blob assertions; explicitrehydrate()where a raw store is inspected).Validation
clients/web:validate(2458 unit),test:coverage(3259 tests, per-file gate green, EXIT 0),test:integration(801) all pass.validategreen for web / cli (110) / tui (237) / launcher (5) — core changes don't break the other clients.🤖 Generated with Claude Code
https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX