The refactor routed the browser's RemoteOAuthStorage through `oauth-persist.ts`, which value-imported the Node-only `store-io.js` (`atomically` -> `stubborn-fs` -> `node:process.getuid`, plus `node:fs/promises`). That dragged Node built-ins into the browser bundle and the web app crashed to a blank page at runtime — a regression from v2/main, where browser OAuth storage used a Zustand JSON adapter with no Node imports. Unit/integration tests run in node/happy-dom and `smoke:web` only checks the served HTML, so none of them execute the app in a real browser and the break slipped through. Fix, following the existing `store-id.ts` precedent: - Split the pure JSON (de)serializers into Node-free `core/storage/store-serialize.ts`; `store-io.ts` re-exports them so existing importers are unaffected. - Move the Node-only `createFileOAuthPersistBackend` into `core/auth/node/oauth-persist-file.ts`; update `storage-node.ts` and the storage adapters test to import from there. - `oauth-persist.ts` now imports only the Node-free serializers, so the browser bundle no longer pulls in `node:fs`/`atomically`. Tests: - oauth-persist-browser-safe.test.ts: source-level guard that `oauth-persist.ts` never imports store-io/node:/atomically. - Direct unit tests for the isomorphic remote + session backends (restoring oauth-persist.ts coverage after the file backend moved out). - store.test.ts: OAuthMemoryStore unit coverage incl. replace() fallbacks. - storage.test.ts: clearAllOAuthClientState null-snapshot (no file) path. Verified end-to-end in a real browser (Playwright): DCR OAuth flow completes, tokens persist via the remote store, and after a full page reload the server reconnects with no re-authentication. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
84 lines
2.7 KiB
TypeScript
84 lines
2.7 KiB
TypeScript
import { OAuthStorageBase } from "../oauth-storage.js";
|
|
import { OAuthMemoryStore } from "../store.js";
|
|
import { createFileOAuthPersistBackend } from "./oauth-persist-file.js";
|
|
import {
|
|
getDefaultStorageDir,
|
|
getStoreFilePath,
|
|
} from "../../storage/store-io.js";
|
|
|
|
/** Default path: ~/.mcp-inspector/storage/oauth.json */
|
|
const DEFAULT_STATE_PATH = getStoreFilePath(getDefaultStorageDir(), "oauth");
|
|
|
|
/**
|
|
* Get path to OAuth state file.
|
|
* @param customPath - Optional custom path (full path to state file). Default: ~/.mcp-inspector/storage/oauth.json
|
|
*/
|
|
export function getStateFilePath(customPath?: string): string {
|
|
return customPath ?? DEFAULT_STATE_PATH;
|
|
}
|
|
|
|
const memoryCache = new Map<string, OAuthMemoryStore>();
|
|
const storageCache = new Map<string, NodeOAuthStorage>();
|
|
|
|
function getSharedMemory(stateFilePath?: string): OAuthMemoryStore {
|
|
const key = getStateFilePath(stateFilePath);
|
|
let memory = memoryCache.get(key);
|
|
if (!memory) {
|
|
memory = new OAuthMemoryStore();
|
|
memoryCache.set(key, memory);
|
|
}
|
|
return memory;
|
|
}
|
|
|
|
/**
|
|
* Drop cached in-memory and {@link NodeOAuthStorage} instances for a path.
|
|
* @internal Test isolation only.
|
|
*/
|
|
export function resetNodeOAuthStorageCache(stateFilePath?: string): void {
|
|
const key = getStateFilePath(stateFilePath);
|
|
memoryCache.delete(key);
|
|
storageCache.delete(key);
|
|
}
|
|
|
|
/**
|
|
* Clear all OAuth client state (all servers) in the default store.
|
|
* Useful for test isolation in E2E OAuth tests.
|
|
* Use a custom-path store and clear per serverUrl if you need to clear non-default storage.
|
|
*/
|
|
export async function clearAllOAuthClientState(): Promise<void> {
|
|
const storage = getNodeOAuthStorage();
|
|
const filePath = getStateFilePath();
|
|
const snapshot = await createFileOAuthPersistBackend({ filePath }).read();
|
|
const urls = Object.keys(snapshot?.servers ?? {});
|
|
for (const url of urls) {
|
|
await storage.clear(url);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Node.js storage implementation with file-based persistence.
|
|
* For InspectorClient, CLI, and TUI.
|
|
*/
|
|
export class NodeOAuthStorage extends OAuthStorageBase {
|
|
/**
|
|
* @param storagePath - Optional path to state file. Default: ~/.mcp-inspector/storage/oauth.json
|
|
*/
|
|
constructor(storagePath?: string) {
|
|
const filePath = getStateFilePath(storagePath);
|
|
super(
|
|
getSharedMemory(storagePath),
|
|
createFileOAuthPersistBackend({ filePath }),
|
|
);
|
|
}
|
|
}
|
|
|
|
/** Cached NodeOAuthStorage instances keyed by resolved file path. */
|
|
function getNodeOAuthStorage(storagePath?: string): NodeOAuthStorage {
|
|
const key = getStateFilePath(storagePath);
|
|
let storage = storageCache.get(key);
|
|
if (!storage) {
|
|
storage = new NodeOAuthStorage(storagePath);
|
|
storageCache.set(key, storage);
|
|
}
|
|
return storage;
|
|
}
|