Review follow-ups on the RemoteOAuthStorage web migration: - useEmaIdpLoginState.logout(): add a .catch to the now-async clearEmaIdpSession call so a storage failure no longer surfaces as an unhandled rejection; leave loginState unchanged on failure (the session was not actually cleared). Add a test asserting no unhandled rejection. - createOAuthStore: attach a no-op catch to the eager persistLoadPromise so a hydration read that fails before any load() awaits it does not become an unhandled rejection; real awaiters still observe the rejection via the shared reference. - storage-remote.test: replace `as unknown as typeof fetch` double-casts with the typed vi.fn<typeof fetch>() pattern the same PR established. - remoteOAuthStorage: correct the doc comment to describe the cache-of-one accurately instead of implying a keyed map. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
59 lines
2 KiB
TypeScript
59 lines
2 KiB
TypeScript
import { RemoteOAuthStorage } from "@inspector/core/auth/remote/storage-remote.js";
|
|
|
|
export interface WebRemoteOAuthStorageOptions {
|
|
baseUrl: string;
|
|
authToken?: string;
|
|
}
|
|
|
|
let cached: { cacheKey: string; storage: RemoteOAuthStorage } | undefined;
|
|
|
|
function buildCacheKey(options: WebRemoteOAuthStorageOptions): string {
|
|
return `${options.baseUrl}\0${options.authToken ?? ""}`;
|
|
}
|
|
|
|
const defaultFetch: typeof fetch = (...args) => globalThis.fetch(...args);
|
|
|
|
/**
|
|
* Shared web OAuth store: `RemoteOAuthStorage` → `GET/POST /api/storage/oauth`
|
|
* → `~/.mcp-inspector/storage/oauth.json` (same file as CLI/TUI).
|
|
*
|
|
* Caches a single instance keyed by `{ baseUrl, authToken }`: repeat calls with
|
|
* the same key return that instance, so connect, EMA IdP session, and per-server
|
|
* clear all mutate the same in-memory view. A call with a different key replaces
|
|
* the cached instance (cache of one). That is sufficient because the web app
|
|
* uses a stable origin + page-lifetime API token, so the key never changes
|
|
* within a session.
|
|
*/
|
|
export function getRemoteOAuthStorage(
|
|
options: WebRemoteOAuthStorageOptions,
|
|
): RemoteOAuthStorage {
|
|
const cacheKey = buildCacheKey(options);
|
|
if (cached?.cacheKey === cacheKey) {
|
|
return cached.storage;
|
|
}
|
|
cached = {
|
|
cacheKey,
|
|
storage: new RemoteOAuthStorage({
|
|
baseUrl: options.baseUrl,
|
|
authToken: options.authToken,
|
|
fetchFn: defaultFetch,
|
|
}),
|
|
};
|
|
return cached.storage;
|
|
}
|
|
|
|
/** Current origin + optional API token (see `getAuthToken()` in App.tsx). */
|
|
export function getWebRemoteOAuthStorage(
|
|
authToken?: string,
|
|
): RemoteOAuthStorage {
|
|
if (typeof window === "undefined") {
|
|
throw new Error("getWebRemoteOAuthStorage requires a browser environment");
|
|
}
|
|
const baseUrl = `${window.location.protocol}//${window.location.host}`;
|
|
return getRemoteOAuthStorage({ baseUrl, authToken });
|
|
}
|
|
|
|
/** @internal Vitest isolation */
|
|
export function resetWebRemoteOAuthStorageCacheForTests(): void {
|
|
cached = undefined;
|
|
}
|