inspector/core/auth/store.ts
Bob Dickinson 13dafe457b
Guided-auth removal, OAuth smoke testing, and TUI/CLI support (#1531)
* refactor(web): update OAuth E2E tests to use new authentication method

- Replaced `runGuidedAuth` with `authenticate` in the InspectorClient OAuth end-to-end tests to support quick authentication with CIMD pre-registration.
- Updated comments to reflect the change in authentication method and its support for HTTP test metadata URLs.
- Ensured that the tests continue to validate the expected authorization URL after the change.

Additionally, imported `ensureCimdClientRegistration` in the OAuthManager to streamline client registration during the authentication process.

* Guided-auth removal, OAuth smoke testing, and TUI/CLI support

* Added CLI OAuth runner flags tests

* Added clarifying comment on port 0 binding

* README notes on TUI oauth callback port, prettier fixes for CLI tests

* CIMD validation uses inline like EMA IdP

* Fixed TUI test failures from merge

* test: bring 5 OAuth files over the 90 per-file coverage gate

The guided-auth-removal work added/touched several files that fell below
the repo's >=90 per-file coverage gate (npm run coverage). CI doesn't run
the gate (it's local-only), so validate + test:integration passed while
these gaps slipped through. Bring them all back over the line:

- core/auth/node/runner-oauth-callback.ts: test bad-URL/non-http-scheme
  throws, no-port->80 default, and IPv6 hostname bracketing. Annotate the
  provably-dead guards (empty host, out-of-range port, String(err)
  fallback, pathname "/" fallback) -- all rejected by new URL() upstream --
  with justified v8 ignore comments instead of lowering the gate.
- core/auth/cimd.ts: test the AS-doesn't-support-CIMD early return.
- core/react/useClientSettingsDraft.ts: test the functional-updater path,
  the stale-onChange-after-close prev===null guard, and the
  timer-fires-after-close value===null guard.
- ClientSettingsForm.tsx: test the CIMD checkbox toggle and CIMD URL clear.
- clients/tui/src/utils/oauthDisplay.ts: test static/dcr registration
  kinds, the expired IdP session, and the configured-scope/empty-scope
  branches of formatScopes.

npm run coverage now passes (exit 0) across web, cli, tui, launcher.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: cliffhall <cliff@futurescale.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 15:35:51 -04:00

124 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* OAuth store factory using Zustand.
* Creates a store with any storage adapter (file, remote, sessionStorage).
*/
import { createStore } from "zustand/vanilla";
import { persist, createJSONStorage } from "zustand/middleware";
import type {
OAuthClientInformation,
OAuthTokens,
OAuthMetadata,
} from "@modelcontextprotocol/sdk/shared/auth.js";
import type { IdpSessionState, OAuthClientRegistrationKind } from "./storage.js";
/**
* OAuth state for a single server
*/
export interface ServerOAuthState {
clientInformation?: OAuthClientInformation;
/** Set when {@link clientInformation} is saved — DCR vs CIMD. */
clientRegistrationKind?: OAuthClientRegistrationKind;
preregisteredClientInformation?: OAuthClientInformation;
tokens?: OAuthTokens;
codeVerifier?: string;
scope?: string;
serverMetadata?: OAuthMetadata;
/** Set when resource tokens were minted via EMA (legs 2–3). */
enterpriseManaged?: boolean;
}
/**
* Zustand store state (all servers)
*/
export interface OAuthStoreState {
servers: Record<string, ServerOAuthState>;
idpSessions: Record<string, IdpSessionState>;
getServerState: (serverUrl: string) => ServerOAuthState;
setServerState: (serverUrl: string, state: Partial<ServerOAuthState>) => void;
clearServerState: (serverUrl: string) => void;
getIdpSession: (issuer: string) => IdpSessionState;
setIdpSession: (issuer: string, updates: Partial<IdpSessionState>) => void;
clearIdpSession: (issuer: string) => void;
clearEnterpriseManagedResourceServers: () => void;
}
/**
* Creates a Zustand store for OAuth state with the given storage adapter.
* The storage adapter handles persistence (file, remote HTTP, sessionStorage, etc.).
*
* @param storage - Zustand storage adapter (from createJSONStorage)
* @returns Zustand store instance
*/
export function createOAuthStore(
storage: ReturnType<typeof createJSONStorage>,
) {
return createStore<OAuthStoreState>()(
persist(
(set, get) => ({
servers: {},
idpSessions: {},
getServerState: (serverUrl: string) => {
return get().servers[serverUrl] || {};
},
setServerState: (
serverUrl: string,
updates: Partial<ServerOAuthState>,
) => {
set((state) => ({
servers: {
...state.servers,
[serverUrl]: {
...state.servers[serverUrl],
...updates,
},
},
}));
},
clearServerState: (serverUrl: string) => {
set((state) => {
const rest = { ...state.servers };
delete rest[serverUrl];
return { servers: rest };
});
},
getIdpSession: (issuer: string) => {
return get().idpSessions[issuer] || {};
},
setIdpSession: (issuer: string, updates: Partial<IdpSessionState>) => {
set((state) => ({
idpSessions: {
...state.idpSessions,
[issuer]: {
...state.idpSessions[issuer],
...updates,
},
},
}));
},
clearIdpSession: (issuer: string) => {
set((state) => {
const rest = { ...state.idpSessions };
delete rest[issuer];
return { idpSessions: rest };
});
},
clearEnterpriseManagedResourceServers: () => {
set((state) => {
const rest = { ...state.servers };
for (const [url, serverState] of Object.entries(state.servers)) {
if (serverState.enterpriseManaged === true) {
delete rest[url];
}
}
return { servers: rest };
});
},
}),
{
name: "mcp-inspector-oauth",
storage,
},
),
);
}