inspector/clients/web/src/utils/oauthClearKey.ts
cliffhall b2bbfcb409 fix: key the in-flight clear guard on the OAuth storage key too (#2217)
Copilot: `runClear`'s double-click guard deduped by `server.id`, which is
the same identity mismatch this PR fixes, one layer up. Two catalog
entries against one URL share one credential blob, one grant and one
revocation, so an id-keyed guard let exactly the race it exists to
prevent through between them — concurrent store writes, concurrent RFC
7009 requests, and two contradictory teardown toasts.

The guard now keys on `oauthClearKey`, a pure helper that resolves the
OAuth storage key and falls back to the entry id for a config that has
none (stdio), where there is no shared state to collide over. The id
fallback keeps such entries distinct from each other rather than
collapsing them onto one key, and both forms are prefixed so a
user-supplied id can never impersonate a URL.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGbw6TezNWnXZSCSp7sqb4
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-03 20:48:45 -04:00

23 lines
1.1 KiB
TypeScript

import { getOAuthServerUrl } from "@inspector/core/mcp/config.js";
import type { MCPServerConfig } from "@inspector/core/mcp/types.js";
/**
* The identity a *clear* of stored OAuth state acts on.
*
* Persisted OAuth state is keyed by the server URL, and `core/mcp/serverList`
* enforces no URL uniqueness — so two catalog entries with distinct ids can
* point at one URL and therefore share one credential blob, one live grant and
* one revocation. Anything that dedupes, locks or compares *clears* has to key
* on that shared identity rather than on the entry id, which cannot see it
* (#2217).
*
* A config with no OAuth server URL (stdio, and anything else
* `getOAuthServerUrl` declines) has no shared blob to collide over, so it falls
* back to the entry id — which keeps every such entry distinct from every
* other, rather than collapsing them all onto one shared key.
*/
export function oauthClearKey(config: MCPServerConfig, id: string): string {
const url = getOAuthServerUrl(config);
// Prefixed so a URL-keyed entry can never collide with an id-keyed one.
return url !== undefined ? `url:${url}` : `id:${id}`;
}