Removed Zustand storage, cleanup up auth storage IO interfaces, implementations, and usage.
This commit is contained in:
parent
512f45d357
commit
3c65bdb4c6
57 changed files with 863 additions and 1008 deletions
|
|
@ -22,7 +22,7 @@ inspector/
|
||||||
│ ├── tui/ # TUI client (Ink + React, tsup bundle)
|
│ ├── tui/ # TUI client (Ink + React, tsup bundle)
|
||||||
│ ├── launcher/ # Shared launcher (relative imports into sibling build/ outputs)
|
│ ├── launcher/ # Shared launcher (relative imports into sibling build/ outputs)
|
||||||
├── core/ # Shared core code (no package.json — consumed via the `@inspector/core` vite alias)
|
├── core/ # Shared core code (no package.json — consumed via the `@inspector/core` vite alias)
|
||||||
│ ├── auth/ # OAuth: state machine, providers, discovery, storage;
|
│ ├── auth/ # OAuth: providers, discovery, OAuthStorage + persist backends;
|
||||||
│ │ # mid-session recovery (challenge.ts WWW-Authenticate
|
│ │ # mid-session recovery (challenge.ts WWW-Authenticate
|
||||||
│ │ # parsing, scopes.ts SEP-2350 scope union, oauthUx.ts
|
│ │ # parsing, scopes.ts SEP-2350 scope union, oauthUx.ts
|
||||||
│ │ # shared copy, mcpAuth.ts force-reauthorization)
|
│ │ # shared copy, mcpAuth.ts force-reauthorization)
|
||||||
|
|
@ -41,9 +41,9 @@ inspector/
|
||||||
│ │ ├── node/ # Node stdio transport factory
|
│ │ ├── node/ # Node stdio transport factory
|
||||||
│ │ ├── remote/ # Browser HTTP/SSE transport + remote logger/fetch
|
│ │ ├── remote/ # Browser HTTP/SSE transport + remote logger/fetch
|
||||||
│ │ │ └── node/ # Hono-based remote server backend (used by remote/ above)
|
│ │ │ └── node/ # Hono-based remote server backend (used by remote/ above)
|
||||||
│ │ └── state/ # Zustand-style state stores consumed by core/react/
|
│ │ └── state/ # InspectorClient state stores consumed by core/react/
|
||||||
│ ├── react/ # React hooks over the state stores
|
│ ├── react/ # React hooks over the state stores
|
||||||
│ └── storage/ # File and remote storage adapters (Zustand middleware)
|
│ └── storage/ # File I/O helpers (store-io.ts) used by OAuth persist backends
|
||||||
├── test-servers/ # Composable MCP test servers + fixtures used by integration tests.
|
├── test-servers/ # Composable MCP test servers + fixtures used by integration tests.
|
||||||
│ ├── src/ # TypeScript sources.
|
│ ├── src/ # TypeScript sources.
|
||||||
│ ├── build/ # Built JS (gitignored). Produced by `npm run test-servers:build`
|
│ ├── build/ # Built JS (gitignored). Produced by `npm run test-servers:build`
|
||||||
|
|
|
||||||
32
clients/cli/package-lock.json
generated
32
clients/cli/package-lock.json
generated
|
|
@ -15,8 +15,7 @@
|
||||||
"atomically": "^2.1.1",
|
"atomically": "^2.1.1",
|
||||||
"commander": "^13.1.0",
|
"commander": "^13.1.0",
|
||||||
"pino": "^9.14.0",
|
"pino": "^9.14.0",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6"
|
||||||
"zustand": "^5.0.13"
|
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"mcp-inspector-cli": "build/index.js"
|
"mcp-inspector-cli": "build/index.js"
|
||||||
|
|
@ -5675,35 +5674,6 @@
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^3.25.28 || ^4"
|
"zod": "^3.25.28 || ^4"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"node_modules/zustand": {
|
|
||||||
"version": "5.0.14",
|
|
||||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz",
|
|
||||||
"integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.20.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": ">=18.0.0",
|
|
||||||
"immer": ">=9.0.6",
|
|
||||||
"react": ">=18.0.0",
|
|
||||||
"use-sync-external-store": ">=1.2.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"immer": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"use-sync-external-store": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,7 @@
|
||||||
"atomically": "^2.1.1",
|
"atomically": "^2.1.1",
|
||||||
"commander": "^13.1.0",
|
"commander": "^13.1.0",
|
||||||
"pino": "^9.14.0",
|
"pino": "^9.14.0",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6"
|
||||||
"zustand": "^5.0.13"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.4",
|
"@eslint/js": "^9.39.4",
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export default defineConfig({
|
||||||
platform: 'node',
|
platform: 'node',
|
||||||
// Bundle core source; leave npm deps external.
|
// Bundle core source; leave npm deps external.
|
||||||
noExternal: [/^@inspector\/core/],
|
noExternal: [/^@inspector\/core/],
|
||||||
external: ['@napi-rs/keyring', '@modelcontextprotocol/sdk', 'commander', 'pino', 'zustand'],
|
external: ['@napi-rs/keyring', '@modelcontextprotocol/sdk', 'commander', 'pino'],
|
||||||
esbuildOptions(options) {
|
esbuildOptions(options) {
|
||||||
options.alias = {
|
options.alias = {
|
||||||
'@inspector/core': path.join(repoRoot, 'core'),
|
'@inspector/core': path.join(repoRoot, 'core'),
|
||||||
|
|
|
||||||
32
clients/tui/package-lock.json
generated
32
clients/tui/package-lock.json
generated
|
|
@ -20,8 +20,7 @@
|
||||||
"open": "^10.2.0",
|
"open": "^10.2.0",
|
||||||
"pino": "^9.14.0",
|
"pino": "^9.14.0",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6"
|
||||||
"zustand": "^5.0.13"
|
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"mcp-inspector-tui": "build/index.js"
|
"mcp-inspector-tui": "build/index.js"
|
||||||
|
|
@ -7510,35 +7509,6 @@
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^3.25.0 || ^4.0.0"
|
"zod": "^3.25.0 || ^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"node_modules/zustand": {
|
|
||||||
"version": "5.0.14",
|
|
||||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz",
|
|
||||||
"integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.20.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": ">=18.0.0",
|
|
||||||
"immer": ">=9.0.6",
|
|
||||||
"react": ">=18.0.0",
|
|
||||||
"use-sync-external-store": ">=1.2.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"immer": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"use-sync-external-store": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,7 @@
|
||||||
"open": "^10.2.0",
|
"open": "^10.2.0",
|
||||||
"pino": "^9.14.0",
|
"pino": "^9.14.0",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6"
|
||||||
"zustand": "^5.0.13"
|
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"ink-select-input": "^6.2.0"
|
"ink-select-input": "^6.2.0"
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ export default defineConfig({
|
||||||
'open',
|
'open',
|
||||||
'commander',
|
'commander',
|
||||||
'pino',
|
'pino',
|
||||||
'zustand',
|
|
||||||
'@modelcontextprotocol/sdk',
|
'@modelcontextprotocol/sdk',
|
||||||
'@napi-rs/keyring',
|
'@napi-rs/keyring',
|
||||||
],
|
],
|
||||||
|
|
|
||||||
34
clients/web/package-lock.json
generated
34
clients/web/package-lock.json
generated
|
|
@ -34,8 +34,7 @@
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-syntax-highlighter": "^16.1.1",
|
"react-syntax-highlighter": "^16.1.1",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6"
|
||||||
"zustand": "^5.0.13"
|
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"mcp-inspector-web": "build/index.js"
|
"mcp-inspector-web": "build/index.js"
|
||||||
|
|
@ -9952,7 +9951,7 @@
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
|
@ -10359,35 +10358,6 @@
|
||||||
"zod": "^3.25.0 || ^4.0.0"
|
"zod": "^3.25.0 || ^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zustand": {
|
|
||||||
"version": "5.0.13",
|
|
||||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.13.tgz",
|
|
||||||
"integrity": "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.20.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": ">=18.0.0",
|
|
||||||
"immer": ">=9.0.6",
|
|
||||||
"react": ">=18.0.0",
|
|
||||||
"use-sync-external-store": ">=1.2.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"immer": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"use-sync-external-store": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/zwitch": {
|
"node_modules/zwitch": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,7 @@
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-syntax-highlighter": "^16.1.1",
|
"react-syntax-highlighter": "^16.1.1",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6"
|
||||||
"zustand": "^5.0.13"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@chromatic-com/storybook": "^5.0.1",
|
"@chromatic-com/storybook": "^5.0.1",
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ describe("ensureCimdClientRegistration", () => {
|
||||||
storage = {
|
storage = {
|
||||||
getClientInformation: vi.fn(async () => undefined),
|
getClientInformation: vi.fn(async () => undefined),
|
||||||
saveClientInformation: vi.fn(async () => {}),
|
saveClientInformation: vi.fn(async () => {}),
|
||||||
getScope: vi.fn(() => undefined),
|
getScope: vi.fn().mockResolvedValue(undefined),
|
||||||
getTokens: vi.fn(async () => undefined),
|
getTokens: vi.fn(async () => undefined),
|
||||||
saveTokens: vi.fn(async () => {}),
|
saveTokens: vi.fn(async () => {}),
|
||||||
clear: vi.fn(),
|
clear: vi.fn(),
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,13 @@ function createStorage(
|
||||||
tokens: Awaited<ReturnType<OAuthStorage["getTokens"]>>;
|
tokens: Awaited<ReturnType<OAuthStorage["getTokens"]>>;
|
||||||
preregistered: Awaited<ReturnType<OAuthStorage["getClientInformation"]>>;
|
preregistered: Awaited<ReturnType<OAuthStorage["getClientInformation"]>>;
|
||||||
dynamic: Awaited<ReturnType<OAuthStorage["getClientInformation"]>>;
|
dynamic: Awaited<ReturnType<OAuthStorage["getClientInformation"]>>;
|
||||||
registrationKind: ReturnType<OAuthStorage["getClientRegistrationKind"]>;
|
registrationKind: Awaited<
|
||||||
|
ReturnType<OAuthStorage["getClientRegistrationKind"]>
|
||||||
|
>;
|
||||||
scope: string | undefined;
|
scope: string | undefined;
|
||||||
serverMetadata: ReturnType<OAuthStorage["getServerMetadata"]>;
|
serverMetadata: Awaited<ReturnType<OAuthStorage["getServerMetadata"]>>;
|
||||||
idpSession: Awaited<ReturnType<OAuthStorage["getIdpSession"]>>;
|
idpSession: Awaited<ReturnType<OAuthStorage["getIdpSession"]>>;
|
||||||
idpMetadata: ReturnType<OAuthStorage["getServerMetadata"]>;
|
idpMetadata: Awaited<ReturnType<OAuthStorage["getServerMetadata"]>>;
|
||||||
}> = {},
|
}> = {},
|
||||||
): OAuthStorage {
|
): OAuthStorage {
|
||||||
return {
|
return {
|
||||||
|
|
@ -27,15 +29,17 @@ function createStorage(
|
||||||
getClientInformation: vi.fn(async (_url, isPreregistered) =>
|
getClientInformation: vi.fn(async (_url, isPreregistered) =>
|
||||||
isPreregistered ? overrides.preregistered : overrides.dynamic,
|
isPreregistered ? overrides.preregistered : overrides.dynamic,
|
||||||
),
|
),
|
||||||
getScope: vi.fn().mockReturnValue(overrides.scope),
|
getScope: vi.fn().mockResolvedValue(overrides.scope),
|
||||||
getServerMetadata: vi.fn((url: string) => {
|
getServerMetadata: vi.fn(async (url: string) => {
|
||||||
if (url.startsWith("ema-idp:")) {
|
if (url.startsWith("ema-idp:")) {
|
||||||
return overrides.idpMetadata ?? null;
|
return overrides.idpMetadata ?? null;
|
||||||
}
|
}
|
||||||
return overrides.serverMetadata ?? null;
|
return overrides.serverMetadata ?? null;
|
||||||
}),
|
}),
|
||||||
getIdpSession: vi.fn().mockResolvedValue(overrides.idpSession),
|
getIdpSession: vi.fn().mockResolvedValue(overrides.idpSession),
|
||||||
getClientRegistrationKind: vi.fn(() => overrides.registrationKind),
|
getClientRegistrationKind: vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(overrides.registrationKind),
|
||||||
saveClientInformation: vi.fn(),
|
saveClientInformation: vi.fn(),
|
||||||
savePreregisteredClientInformation: vi.fn(),
|
savePreregisteredClientInformation: vi.fn(),
|
||||||
saveTokens: vi.fn(),
|
saveTokens: vi.fn(),
|
||||||
|
|
|
||||||
|
|
@ -49,19 +49,21 @@ function createMemoryStorage(
|
||||||
idpSessions[issuer] = { ...idpSessions[issuer], ...updates };
|
idpSessions[issuer] = { ...idpSessions[issuer], ...updates };
|
||||||
}),
|
}),
|
||||||
clearIdpSession: vi.fn(),
|
clearIdpSession: vi.fn(),
|
||||||
getServerMetadata: vi.fn((key: string) => metadataByKey[key]),
|
getServerMetadata: vi.fn(async (key: string) => metadataByKey[key] ?? null),
|
||||||
saveServerMetadata: vi.fn((key: string, metadata: unknown) => {
|
saveServerMetadata: vi.fn(async (key: string, metadata: unknown) => {
|
||||||
metadataByKey[key] = metadata;
|
metadataByKey[key] = metadata;
|
||||||
}),
|
}),
|
||||||
getClientInformation: vi.fn((key: string) => clientInfoByKey[key]),
|
getClientInformation: vi.fn(async (key: string) => clientInfoByKey[key]),
|
||||||
savePreregisteredClientInformation: vi.fn((key: string, info: unknown) => {
|
savePreregisteredClientInformation: vi.fn(
|
||||||
clientInfoByKey[key] = info;
|
async (key: string, info: unknown) => {
|
||||||
}),
|
clientInfoByKey[key] = info;
|
||||||
getCodeVerifier: vi.fn((key: string) => codeVerifierByKey[key]),
|
},
|
||||||
saveCodeVerifier: vi.fn((key: string, verifier: string) => {
|
),
|
||||||
|
getCodeVerifier: vi.fn(async (key: string) => codeVerifierByKey[key]),
|
||||||
|
saveCodeVerifier: vi.fn(async (key: string, verifier: string) => {
|
||||||
codeVerifierByKey[key] = verifier;
|
codeVerifierByKey[key] = verifier;
|
||||||
}),
|
}),
|
||||||
clearCodeVerifier: vi.fn((key: string) => {
|
clearCodeVerifier: vi.fn(async (key: string) => {
|
||||||
delete codeVerifierByKey[key];
|
delete codeVerifierByKey[key];
|
||||||
}),
|
}),
|
||||||
getTokens: vi.fn(async (url: string) => savedTokens[url] as never),
|
getTokens: vi.fn(async (url: string) => savedTokens[url] as never),
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ describe("idpOidc refresh", () => {
|
||||||
saveIdpSession: vi.fn(async (issuer: string, updates) => {
|
saveIdpSession: vi.fn(async (issuer: string, updates) => {
|
||||||
idpSessions[issuer] = { ...idpSessions[issuer], ...updates };
|
idpSessions[issuer] = { ...idpSessions[issuer], ...updates };
|
||||||
}),
|
}),
|
||||||
getServerMetadata: vi.fn(() => minimalOAuthAsMetadata(IDP_ISSUER)),
|
getServerMetadata: vi.fn(async () => minimalOAuthAsMetadata(IDP_ISSUER)),
|
||||||
clearIdpSession: vi.fn(),
|
clearIdpSession: vi.fn(),
|
||||||
} as unknown as OAuthStorage;
|
} as unknown as OAuthStorage;
|
||||||
});
|
});
|
||||||
|
|
@ -266,7 +266,7 @@ describe("startIdpOidcAuthorization", () => {
|
||||||
Object.keys(saved).forEach((k) => delete saved[k]);
|
Object.keys(saved).forEach((k) => delete saved[k]);
|
||||||
storage = {
|
storage = {
|
||||||
load: vi.fn().mockResolvedValue(undefined),
|
load: vi.fn().mockResolvedValue(undefined),
|
||||||
getServerMetadata: vi.fn(() => null),
|
getServerMetadata: vi.fn(async () => null),
|
||||||
saveCodeVerifier: vi.fn(async (key: string, value: string) => {
|
saveCodeVerifier: vi.fn(async (key: string, value: string) => {
|
||||||
saved[`cv:${key}`] = value;
|
saved[`cv:${key}`] = value;
|
||||||
}),
|
}),
|
||||||
|
|
@ -335,13 +335,13 @@ describe("completeIdpOidcAuthorization", () => {
|
||||||
function buildStorage(overrides: Partial<OAuthStorage> = {}): OAuthStorage {
|
function buildStorage(overrides: Partial<OAuthStorage> = {}): OAuthStorage {
|
||||||
return {
|
return {
|
||||||
load: vi.fn().mockResolvedValue(undefined),
|
load: vi.fn().mockResolvedValue(undefined),
|
||||||
getServerMetadata: vi.fn(() => minimalOAuthAsMetadata(IDP_ISSUER)),
|
getServerMetadata: vi.fn(async () => minimalOAuthAsMetadata(IDP_ISSUER)),
|
||||||
getClientInformation: vi.fn(async () => ({
|
getClientInformation: vi.fn(async () => ({
|
||||||
client_id: "idp-client",
|
client_id: "idp-client",
|
||||||
client_secret: "idp-secret",
|
client_secret: "idp-secret",
|
||||||
token_endpoint_auth_method: "client_secret_post",
|
token_endpoint_auth_method: "client_secret_post",
|
||||||
})),
|
})),
|
||||||
getCodeVerifier: vi.fn(() => "verifier-123"),
|
getCodeVerifier: vi.fn(async () => "verifier-123"),
|
||||||
clearCodeVerifier: vi.fn((key: string) => {
|
clearCodeVerifier: vi.fn((key: string) => {
|
||||||
clearedVerifiers.push(key);
|
clearedVerifiers.push(key);
|
||||||
}),
|
}),
|
||||||
|
|
@ -474,7 +474,7 @@ describe("completeIdpOidcAuthorization", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws when the PKCE verifier is missing", async () => {
|
it("throws when the PKCE verifier is missing", async () => {
|
||||||
storage = buildStorage({ getCodeVerifier: vi.fn(() => undefined) });
|
storage = buildStorage({ getCodeVerifier: vi.fn(async () => undefined) });
|
||||||
await expect(
|
await expect(
|
||||||
completeIdpOidcAuthorization({
|
completeIdpOidcAuthorization({
|
||||||
idp,
|
idp,
|
||||||
|
|
|
||||||
54
clients/web/src/test/core/auth/oauth-persist.test.ts
Normal file
54
clients/web/src/test/core/auth/oauth-persist.test.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import {
|
||||||
|
parseOAuthPersistBlob,
|
||||||
|
serializeOAuthPersistBlob,
|
||||||
|
} from "@inspector/core/auth/oauth-persist.js";
|
||||||
|
|
||||||
|
describe("parseOAuthPersistBlob", () => {
|
||||||
|
it("returns null for empty input", () => {
|
||||||
|
expect(parseOAuthPersistBlob(null)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reads plain JSON with servers and idpSessions", () => {
|
||||||
|
const snapshot = {
|
||||||
|
servers: {
|
||||||
|
"http://example.com": { codeVerifier: "v1" },
|
||||||
|
},
|
||||||
|
idpSessions: {
|
||||||
|
"https://idp.example": { idToken: "token" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(parseOAuthPersistBlob(JSON.stringify(snapshot))).toEqual(snapshot);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("promotes legacy persist envelope state to the top level", () => {
|
||||||
|
const legacy = {
|
||||||
|
state: {
|
||||||
|
servers: {
|
||||||
|
"http://example.com": {
|
||||||
|
tokens: { access_token: "t", token_type: "Bearer" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
idpSessions: {},
|
||||||
|
},
|
||||||
|
version: 0,
|
||||||
|
};
|
||||||
|
expect(parseOAuthPersistBlob(JSON.stringify(legacy))).toEqual({
|
||||||
|
servers: legacy.state.servers,
|
||||||
|
idpSessions: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("serializeOAuthPersistBlob", () => {
|
||||||
|
it("writes plain JSON without a state/version envelope", () => {
|
||||||
|
const snapshot = {
|
||||||
|
servers: { "http://example.com": { scope: "read" } },
|
||||||
|
idpSessions: {},
|
||||||
|
};
|
||||||
|
const raw = serializeOAuthPersistBlob(snapshot);
|
||||||
|
expect(JSON.parse(raw)).toEqual(snapshot);
|
||||||
|
expect(raw).not.toContain('"version"');
|
||||||
|
expect(raw).not.toMatch(/"state"\s*:/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -214,7 +214,7 @@ describe("OAuthNavigation", () => {
|
||||||
function makeStorage(): OAuthStorage {
|
function makeStorage(): OAuthStorage {
|
||||||
return {
|
return {
|
||||||
load: vi.fn().mockResolvedValue(undefined),
|
load: vi.fn().mockResolvedValue(undefined),
|
||||||
getScope: vi.fn(() => undefined),
|
getScope: vi.fn().mockResolvedValue(undefined),
|
||||||
getClientInformation: vi.fn(async () => undefined),
|
getClientInformation: vi.fn(async () => undefined),
|
||||||
saveClientInformation: vi.fn(async () => undefined),
|
saveClientInformation: vi.fn(async () => undefined),
|
||||||
savePreregisteredClientInformation: vi.fn(async () => undefined),
|
savePreregisteredClientInformation: vi.fn(async () => undefined),
|
||||||
|
|
@ -222,9 +222,9 @@ describe("OAuthNavigation", () => {
|
||||||
getTokens: vi.fn(async () => undefined),
|
getTokens: vi.fn(async () => undefined),
|
||||||
saveTokens: vi.fn(async () => undefined),
|
saveTokens: vi.fn(async () => undefined),
|
||||||
saveCodeVerifier: vi.fn(async () => undefined),
|
saveCodeVerifier: vi.fn(async () => undefined),
|
||||||
getCodeVerifier: vi.fn(() => undefined),
|
getCodeVerifier: vi.fn().mockResolvedValue(undefined),
|
||||||
clear: vi.fn(),
|
clear: vi.fn(),
|
||||||
getServerMetadata: vi.fn(() => null),
|
getServerMetadata: vi.fn().mockResolvedValue(null),
|
||||||
saveServerMetadata: vi.fn(async () => undefined),
|
saveServerMetadata: vi.fn(async () => undefined),
|
||||||
} as unknown as OAuthStorage;
|
} as unknown as OAuthStorage;
|
||||||
}
|
}
|
||||||
|
|
@ -272,27 +272,28 @@ describe("OAuthNavigation", () => {
|
||||||
expect(await provider.clientInformation()).toEqual({ client_id: "dyn" });
|
expect(await provider.clientInformation()).toEqual({ client_id: "dyn" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("codeVerifier() throws when none is saved", () => {
|
it("codeVerifier() throws when none is saved", async () => {
|
||||||
const storage = makeStorage();
|
const storage = makeStorage();
|
||||||
const provider = makeProvider(storage);
|
const provider = makeProvider(storage);
|
||||||
|
|
||||||
expect(() => provider.codeVerifier()).toThrow(
|
await expect(provider.codeVerifier()).rejects.toThrow(
|
||||||
/No code verifier saved for session/,
|
/No code verifier saved for session/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("codeVerifier() returns the saved verifier", () => {
|
it("codeVerifier() returns the saved verifier", async () => {
|
||||||
const storage = makeStorage();
|
const storage = makeStorage();
|
||||||
vi.mocked(storage.getCodeVerifier).mockReturnValue("cv-1");
|
vi.mocked(storage.getCodeVerifier).mockResolvedValue("cv-1");
|
||||||
const provider = makeProvider(storage);
|
const provider = makeProvider(storage);
|
||||||
|
|
||||||
expect(provider.codeVerifier()).toBe("cv-1");
|
expect(await provider.codeVerifier()).toBe("cv-1");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clientMetadata reflects the stored scope when present", () => {
|
it("clientMetadata reflects the stored scope when present", async () => {
|
||||||
const storage = makeStorage();
|
const storage = makeStorage();
|
||||||
vi.mocked(storage.getScope).mockReturnValue("read write");
|
vi.mocked(storage.getScope).mockResolvedValue("read write");
|
||||||
const provider = makeProvider(storage);
|
const provider = makeProvider(storage);
|
||||||
|
await provider.prepareForAuth();
|
||||||
|
|
||||||
expect(provider.clientMetadata.scope).toBe("read write");
|
expect(provider.clientMetadata.scope).toBe("read write");
|
||||||
});
|
});
|
||||||
|
|
@ -380,7 +381,7 @@ describe("OAuthNavigation", () => {
|
||||||
expect(storage.saveCodeVerifier).toHaveBeenCalledWith(SERVER, "cv");
|
expect(storage.saveCodeVerifier).toHaveBeenCalledWith(SERVER, "cv");
|
||||||
expect(storage.saveServerMetadata).toHaveBeenCalled();
|
expect(storage.saveServerMetadata).toHaveBeenCalled();
|
||||||
expect(await provider.tokens()).toBeUndefined();
|
expect(await provider.tokens()).toBeUndefined();
|
||||||
expect(provider.getServerMetadata()).toBeNull();
|
expect(await provider.getServerMetadata()).toBeNull();
|
||||||
const state = await provider.state();
|
const state = await provider.state();
|
||||||
expect(typeof state).toBe("string");
|
expect(typeof state).toBe("string");
|
||||||
expect(state.length).toBeGreaterThan(0);
|
expect(state.length).toBeGreaterThan(0);
|
||||||
|
|
|
||||||
|
|
@ -91,16 +91,11 @@ describe("BrowserOAuthStorage", () => {
|
||||||
client_secret: "preregistered-secret",
|
client_secret: "preregistered-secret",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use the storage API instead of manually setting sessionStorage
|
|
||||||
// since BrowserOAuthStorage now uses Zustand with a different storage format
|
|
||||||
await storage.savePreregisteredClientInformation(
|
await storage.savePreregisteredClientInformation(
|
||||||
testServerUrl,
|
testServerUrl,
|
||||||
preregisteredInfo,
|
preregisteredInfo,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Wait for Zustand to persist
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
||||||
|
|
||||||
const result = await storage.getClientInformation(testServerUrl, true);
|
const result = await storage.getClientInformation(testServerUrl, true);
|
||||||
|
|
||||||
expect(result).toEqual(preregisteredInfo);
|
expect(result).toEqual(preregisteredInfo);
|
||||||
|
|
@ -139,14 +134,18 @@ describe("BrowserOAuthStorage", () => {
|
||||||
const result = await storage.getClientInformation(testServerUrl);
|
const result = await storage.getClientInformation(testServerUrl);
|
||||||
|
|
||||||
expect(result).toEqual(secondInfo);
|
expect(result).toEqual(secondInfo);
|
||||||
expect(storage.getClientRegistrationKind(testServerUrl)).toBe("cimd");
|
expect(await storage.getClientRegistrationKind(testServerUrl)).toBe(
|
||||||
|
"cimd",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("savePreregisteredClientInformation sets static registration kind", async () => {
|
it("savePreregisteredClientInformation sets static registration kind", async () => {
|
||||||
await storage.savePreregisteredClientInformation(testServerUrl, {
|
await storage.savePreregisteredClientInformation(testServerUrl, {
|
||||||
client_id: "static-id",
|
client_id: "static-id",
|
||||||
});
|
});
|
||||||
expect(storage.getClientRegistrationKind(testServerUrl)).toBe("static");
|
expect(await storage.getClientRegistrationKind(testServerUrl)).toBe(
|
||||||
|
"static",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -333,16 +332,16 @@ describe("BrowserOAuthStorage", () => {
|
||||||
|
|
||||||
it("clearCodeVerifier removes only the PKCE verifier", async () => {
|
it("clearCodeVerifier removes only the PKCE verifier", async () => {
|
||||||
await storage.saveCodeVerifier(testServerUrl, "verifier");
|
await storage.saveCodeVerifier(testServerUrl, "verifier");
|
||||||
expect(storage.getCodeVerifier(testServerUrl)).toBe("verifier");
|
expect(await storage.getCodeVerifier(testServerUrl)).toBe("verifier");
|
||||||
await storage.clearCodeVerifier(testServerUrl);
|
await storage.clearCodeVerifier(testServerUrl);
|
||||||
expect(storage.getCodeVerifier(testServerUrl)).toBeUndefined();
|
expect(await storage.getCodeVerifier(testServerUrl)).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clearScope removes only the scope", async () => {
|
it("clearScope removes only the scope", async () => {
|
||||||
await storage.saveScope(testServerUrl, "read");
|
await storage.saveScope(testServerUrl, "read");
|
||||||
expect(storage.getScope(testServerUrl)).toBe("read");
|
expect(await storage.getScope(testServerUrl)).toBe("read");
|
||||||
await storage.clearScope(testServerUrl);
|
await storage.clearScope(testServerUrl);
|
||||||
expect(storage.getScope(testServerUrl)).toBeUndefined();
|
expect(await storage.getScope(testServerUrl)).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clearServerMetadata removes only the cached metadata", async () => {
|
it("clearServerMetadata removes only the cached metadata", async () => {
|
||||||
|
|
@ -353,9 +352,9 @@ describe("BrowserOAuthStorage", () => {
|
||||||
response_types_supported: ["code"],
|
response_types_supported: ["code"],
|
||||||
};
|
};
|
||||||
await storage.saveServerMetadata(testServerUrl, metadata);
|
await storage.saveServerMetadata(testServerUrl, metadata);
|
||||||
expect(storage.getServerMetadata(testServerUrl)).toEqual(metadata);
|
expect(await storage.getServerMetadata(testServerUrl)).toEqual(metadata);
|
||||||
await storage.clearServerMetadata(testServerUrl);
|
await storage.clearServerMetadata(testServerUrl);
|
||||||
expect(storage.getServerMetadata(testServerUrl)).toBeNull();
|
expect(await storage.getServerMetadata(testServerUrl)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||||
import { RemoteOAuthStorage } from "@inspector/core/auth/remote/storage-remote.js";
|
import { RemoteOAuthStorage } from "@inspector/core/auth/remote/storage-remote.js";
|
||||||
import {
|
|
||||||
type OAuthStore,
|
|
||||||
waitForOAuthStorePersistLoad,
|
|
||||||
} from "@inspector/core/auth/store.js";
|
|
||||||
|
|
||||||
const NOOP_FETCH = vi.fn(
|
const NOOP_FETCH = vi.fn(
|
||||||
async () =>
|
async () =>
|
||||||
|
|
@ -76,16 +72,16 @@ describe("RemoteOAuthStorage (unit, mocked fetch)", () => {
|
||||||
|
|
||||||
it("codeVerifier round-trip and clearCodeVerifier", async () => {
|
it("codeVerifier round-trip and clearCodeVerifier", async () => {
|
||||||
await storage.saveCodeVerifier(serverUrl, "verifier");
|
await storage.saveCodeVerifier(serverUrl, "verifier");
|
||||||
expect(storage.getCodeVerifier(serverUrl)).toBe("verifier");
|
expect(await storage.getCodeVerifier(serverUrl)).toBe("verifier");
|
||||||
await storage.clearCodeVerifier(serverUrl);
|
await storage.clearCodeVerifier(serverUrl);
|
||||||
expect(storage.getCodeVerifier(serverUrl)).toBeUndefined();
|
expect(await storage.getCodeVerifier(serverUrl)).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("scope round-trip and clearScope", async () => {
|
it("scope round-trip and clearScope", async () => {
|
||||||
await storage.saveScope(serverUrl, "read write");
|
await storage.saveScope(serverUrl, "read write");
|
||||||
expect(storage.getScope(serverUrl)).toBe("read write");
|
expect(await storage.getScope(serverUrl)).toBe("read write");
|
||||||
await storage.clearScope(serverUrl);
|
await storage.clearScope(serverUrl);
|
||||||
expect(storage.getScope(serverUrl)).toBeUndefined();
|
expect(await storage.getScope(serverUrl)).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("serverMetadata round-trip and clearServerMetadata", async () => {
|
it("serverMetadata round-trip and clearServerMetadata", async () => {
|
||||||
|
|
@ -96,9 +92,9 @@ describe("RemoteOAuthStorage (unit, mocked fetch)", () => {
|
||||||
response_types_supported: ["code"],
|
response_types_supported: ["code"],
|
||||||
};
|
};
|
||||||
await storage.saveServerMetadata(serverUrl, md);
|
await storage.saveServerMetadata(serverUrl, md);
|
||||||
expect(storage.getServerMetadata(serverUrl)).toEqual(md);
|
expect(await storage.getServerMetadata(serverUrl)).toEqual(md);
|
||||||
await storage.clearServerMetadata(serverUrl);
|
await storage.clearServerMetadata(serverUrl);
|
||||||
expect(storage.getServerMetadata(serverUrl)).toBeNull();
|
expect(await storage.getServerMetadata(serverUrl)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clear() wipes all state for a server", async () => {
|
it("clear() wipes all state for a server", async () => {
|
||||||
|
|
@ -125,13 +121,7 @@ describe("RemoteOAuthStorage (unit, mocked fetch)", () => {
|
||||||
expect(s).toBeInstanceOf(RemoteOAuthStorage);
|
expect(s).toBeInstanceOf(RemoteOAuthStorage);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("waitForOAuthStorePersistLoad resolves for an unregistered store handle", async () => {
|
it("getCodeVerifier loads remote state automatically when not preloaded", async () => {
|
||||||
await expect(
|
|
||||||
waitForOAuthStorePersistLoad({} as OAuthStore),
|
|
||||||
).resolves.toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("load() waits for remote GET before sync reads see persisted state", async () => {
|
|
||||||
const persisted = {
|
const persisted = {
|
||||||
state: {
|
state: {
|
||||||
servers: {
|
servers: {
|
||||||
|
|
@ -155,15 +145,10 @@ describe("RemoteOAuthStorage (unit, mocked fetch)", () => {
|
||||||
fetchFn: delayedFetch,
|
fetchFn: delayedFetch,
|
||||||
});
|
});
|
||||||
|
|
||||||
const loadPromise = delayedStorage.load();
|
const getPromise = delayedStorage.getCodeVerifier(serverUrl);
|
||||||
expect(delayedStorage.getCodeVerifier(serverUrl)).toBeUndefined();
|
|
||||||
|
|
||||||
resolveFetch(new Response(JSON.stringify(persisted), { status: 200 }));
|
resolveFetch(new Response(JSON.stringify(persisted), { status: 200 }));
|
||||||
await loadPromise;
|
|
||||||
|
|
||||||
expect(delayedStorage.getCodeVerifier(serverUrl)).toBe(
|
expect(await getPromise).toBe("persisted-verifier");
|
||||||
"persisted-verifier",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("load() rejects when remote GET fails instead of hanging", async () => {
|
it("load() rejects when remote GET fails instead of hanging", async () => {
|
||||||
|
|
|
||||||
|
|
@ -36,15 +36,15 @@ function createMockParams(
|
||||||
|
|
||||||
const storage = {
|
const storage = {
|
||||||
load: vi.fn().mockResolvedValue(undefined),
|
load: vi.fn().mockResolvedValue(undefined),
|
||||||
getScope: vi.fn().mockReturnValue(undefined),
|
getScope: vi.fn().mockResolvedValue(undefined),
|
||||||
getClientInformation: vi.fn().mockResolvedValue(undefined),
|
getClientInformation: vi.fn().mockResolvedValue(undefined),
|
||||||
getClientRegistrationKind: vi.fn().mockReturnValue(undefined),
|
getClientRegistrationKind: vi.fn().mockResolvedValue(undefined),
|
||||||
saveClientInformation: vi.fn().mockResolvedValue(undefined),
|
saveClientInformation: vi.fn().mockResolvedValue(undefined),
|
||||||
savePreregisteredClientInformation: vi.fn().mockResolvedValue(undefined),
|
savePreregisteredClientInformation: vi.fn().mockResolvedValue(undefined),
|
||||||
saveScope: vi.fn().mockResolvedValue(undefined),
|
saveScope: vi.fn().mockResolvedValue(undefined),
|
||||||
getTokens: vi.fn().mockResolvedValue(undefined),
|
getTokens: vi.fn().mockResolvedValue(undefined),
|
||||||
saveTokens: vi.fn().mockResolvedValue(undefined),
|
saveTokens: vi.fn().mockResolvedValue(undefined),
|
||||||
getCodeVerifier: vi.fn().mockReturnValue("verifier"),
|
getCodeVerifier: vi.fn().mockResolvedValue("verifier"),
|
||||||
saveCodeVerifier: vi.fn().mockResolvedValue(undefined),
|
saveCodeVerifier: vi.fn().mockResolvedValue(undefined),
|
||||||
clear: vi.fn().mockResolvedValue(undefined),
|
clear: vi.fn().mockResolvedValue(undefined),
|
||||||
clearClientInformation: vi.fn(),
|
clearClientInformation: vi.fn(),
|
||||||
|
|
@ -52,7 +52,7 @@ function createMockParams(
|
||||||
clearCodeVerifier: vi.fn(),
|
clearCodeVerifier: vi.fn(),
|
||||||
clearScope: vi.fn(),
|
clearScope: vi.fn(),
|
||||||
clearServerMetadata: vi.fn(),
|
clearServerMetadata: vi.fn(),
|
||||||
getServerMetadata: vi.fn().mockReturnValue(null),
|
getServerMetadata: vi.fn().mockResolvedValue(null),
|
||||||
saveServerMetadata: vi.fn().mockResolvedValue(undefined),
|
saveServerMetadata: vi.fn().mockResolvedValue(undefined),
|
||||||
getIdpSession: vi.fn().mockResolvedValue(undefined),
|
getIdpSession: vi.fn().mockResolvedValue(undefined),
|
||||||
saveIdpSession: vi.fn().mockResolvedValue(undefined),
|
saveIdpSession: vi.fn().mockResolvedValue(undefined),
|
||||||
|
|
@ -428,7 +428,7 @@ describe("OAuthManager", () => {
|
||||||
const params = createMockParams({ onBeforeOAuthRedirect });
|
const params = createMockParams({ onBeforeOAuthRedirect });
|
||||||
// A configured scope exercises the saveScope branch in createOAuthProvider.
|
// A configured scope exercises the saveScope branch in createOAuthProvider.
|
||||||
params.initialConfig.scope = "read write";
|
params.initialConfig.scope = "read write";
|
||||||
storageOf(params).getScope.mockReturnValue(undefined);
|
storageOf(params).getScope.mockResolvedValue(undefined);
|
||||||
storageOf(params).getClientInformation.mockResolvedValue({
|
storageOf(params).getClientInformation.mockResolvedValue({
|
||||||
client_id: "cid",
|
client_id: "cid",
|
||||||
});
|
});
|
||||||
|
|
@ -466,7 +466,9 @@ describe("OAuthManager", () => {
|
||||||
mockedMcpAuth.mockResolvedValue("REDIRECT");
|
mockedMcpAuth.mockResolvedValue("REDIRECT");
|
||||||
const params = createMockParams();
|
const params = createMockParams();
|
||||||
params.initialConfig.scope = "mcp tools:read";
|
params.initialConfig.scope = "mcp tools:read";
|
||||||
storageOf(params).getScope.mockReturnValue("mcp tools:read weather:read");
|
storageOf(params).getScope.mockResolvedValue(
|
||||||
|
"mcp tools:read weather:read",
|
||||||
|
);
|
||||||
storageOf(params).getClientInformation.mockResolvedValue({
|
storageOf(params).getClientInformation.mockResolvedValue({
|
||||||
client_id: "cid",
|
client_id: "cid",
|
||||||
});
|
});
|
||||||
|
|
@ -586,7 +588,7 @@ describe("OAuthManager", () => {
|
||||||
.mockResolvedValueOnce("REDIRECT")
|
.mockResolvedValueOnce("REDIRECT")
|
||||||
.mockResolvedValueOnce("AUTHORIZED");
|
.mockResolvedValueOnce("AUTHORIZED");
|
||||||
const params = createMockParams();
|
const params = createMockParams();
|
||||||
storageOf(params).getScope.mockReturnValue("mcp");
|
storageOf(params).getScope.mockResolvedValue("mcp");
|
||||||
storageOf(params).getTokens.mockResolvedValue({
|
storageOf(params).getTokens.mockResolvedValue({
|
||||||
access_token: "access",
|
access_token: "access",
|
||||||
refresh_token: "refresh",
|
refresh_token: "refresh",
|
||||||
|
|
@ -629,7 +631,7 @@ describe("OAuthManager", () => {
|
||||||
it("persists granted scope when AS down-scopes the token response", async () => {
|
it("persists granted scope when AS down-scopes the token response", async () => {
|
||||||
mockedMcpAuth.mockResolvedValue("AUTHORIZED");
|
mockedMcpAuth.mockResolvedValue("AUTHORIZED");
|
||||||
const params = createMockParams();
|
const params = createMockParams();
|
||||||
storageOf(params).getScope.mockReturnValue("mcp");
|
storageOf(params).getScope.mockResolvedValue("mcp");
|
||||||
storageOf(params).getTokens.mockResolvedValue({
|
storageOf(params).getTokens.mockResolvedValue({
|
||||||
access_token: "access",
|
access_token: "access",
|
||||||
token_type: "Bearer",
|
token_type: "Bearer",
|
||||||
|
|
@ -984,7 +986,9 @@ describe("OAuthManager", () => {
|
||||||
token_type: "Bearer",
|
token_type: "Bearer",
|
||||||
scope: "mcp tools:read tools:write",
|
scope: "mcp tools:read tools:write",
|
||||||
});
|
});
|
||||||
storageOf(params).getScope.mockReturnValue("mcp tools:read tools:write");
|
storageOf(params).getScope.mockResolvedValue(
|
||||||
|
"mcp tools:read tools:write",
|
||||||
|
);
|
||||||
const manager = new OAuthManager(params);
|
const manager = new OAuthManager(params);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
|
|
@ -1002,7 +1006,7 @@ describe("OAuthManager", () => {
|
||||||
token_type: "Bearer",
|
token_type: "Bearer",
|
||||||
scope: "mcp tools:read",
|
scope: "mcp tools:read",
|
||||||
});
|
});
|
||||||
storageOf(params).getScope.mockReturnValue("mcp tools:read");
|
storageOf(params).getScope.mockResolvedValue("mcp tools:read");
|
||||||
const manager = new OAuthManager(params);
|
const manager = new OAuthManager(params);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
|
|
@ -1020,7 +1024,7 @@ describe("OAuthManager", () => {
|
||||||
token_type: "Bearer",
|
token_type: "Bearer",
|
||||||
scope: "mcp",
|
scope: "mcp",
|
||||||
});
|
});
|
||||||
storageOf(params).getScope.mockReturnValue("mcp weather:read");
|
storageOf(params).getScope.mockResolvedValue("mcp weather:read");
|
||||||
const manager = new OAuthManager(params);
|
const manager = new OAuthManager(params);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
|
|
@ -1037,7 +1041,7 @@ describe("OAuthManager", () => {
|
||||||
access_token: "tok",
|
access_token: "tok",
|
||||||
token_type: "Bearer",
|
token_type: "Bearer",
|
||||||
});
|
});
|
||||||
storageOf(params).getScope.mockReturnValue(undefined);
|
storageOf(params).getScope.mockResolvedValue(undefined);
|
||||||
const manager = new OAuthManager(params);
|
const manager = new OAuthManager(params);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
|
|
@ -1054,7 +1058,9 @@ describe("OAuthManager", () => {
|
||||||
token_type: "Bearer",
|
token_type: "Bearer",
|
||||||
scope: "mcp tools:read tools:write",
|
scope: "mcp tools:read tools:write",
|
||||||
});
|
});
|
||||||
storageOf(params).getScope.mockReturnValue("mcp tools:read tools:write");
|
storageOf(params).getScope.mockResolvedValue(
|
||||||
|
"mcp tools:read tools:write",
|
||||||
|
);
|
||||||
const manager = new OAuthManager(params);
|
const manager = new OAuthManager(params);
|
||||||
|
|
||||||
const outcome = await manager.handleAuthChallenge({
|
const outcome = await manager.handleAuthChallenge({
|
||||||
|
|
@ -1136,7 +1142,7 @@ describe("OAuthManager", () => {
|
||||||
const params = createMockParams();
|
const params = createMockParams();
|
||||||
const manager = new OAuthManager(params);
|
const manager = new OAuthManager(params);
|
||||||
manager.setOAuthConfig({ scope: "catalog:scope" });
|
manager.setOAuthConfig({ scope: "catalog:scope" });
|
||||||
storageOf(params).getScope.mockReturnValue("stored union scope");
|
storageOf(params).getScope.mockResolvedValue("stored union scope");
|
||||||
const captureSpy = vi
|
const captureSpy = vi
|
||||||
.spyOn(
|
.spyOn(
|
||||||
(await import("@inspector/core/auth/providers.js"))
|
(await import("@inspector/core/auth/providers.js"))
|
||||||
|
|
@ -1164,7 +1170,7 @@ describe("OAuthManager", () => {
|
||||||
token_type: "Bearer",
|
token_type: "Bearer",
|
||||||
scope: "mcp tools:read",
|
scope: "mcp tools:read",
|
||||||
});
|
});
|
||||||
storageOf(params).getScope.mockReturnValue("mcp tools:read");
|
storageOf(params).getScope.mockResolvedValue("mcp tools:read");
|
||||||
const manager = new OAuthManager(params);
|
const manager = new OAuthManager(params);
|
||||||
|
|
||||||
const outcome = await manager.handleAuthChallenge({
|
const outcome = await manager.handleAuthChallenge({
|
||||||
|
|
@ -1201,7 +1207,7 @@ describe("OAuthManager", () => {
|
||||||
);
|
);
|
||||||
mockedMcpAuth.mockResolvedValue("REDIRECT");
|
mockedMcpAuth.mockResolvedValue("REDIRECT");
|
||||||
const params = createMockParams();
|
const params = createMockParams();
|
||||||
storageOf(params).getScope.mockReturnValue("mcp tools:read");
|
storageOf(params).getScope.mockResolvedValue("mcp tools:read");
|
||||||
const manager = new OAuthManager(params);
|
const manager = new OAuthManager(params);
|
||||||
const captureSpy = vi
|
const captureSpy = vi
|
||||||
.spyOn(
|
.spyOn(
|
||||||
|
|
@ -1234,7 +1240,7 @@ describe("OAuthManager", () => {
|
||||||
);
|
);
|
||||||
mockedMcpAuth.mockResolvedValue("REDIRECT");
|
mockedMcpAuth.mockResolvedValue("REDIRECT");
|
||||||
const params = createMockParams();
|
const params = createMockParams();
|
||||||
storageOf(params).getScope.mockReturnValue("mcp tools:read");
|
storageOf(params).getScope.mockResolvedValue("mcp tools:read");
|
||||||
storageOf(params).getTokens.mockResolvedValue({
|
storageOf(params).getTokens.mockResolvedValue({
|
||||||
access_token: "access",
|
access_token: "access",
|
||||||
refresh_token: "refresh",
|
refresh_token: "refresh",
|
||||||
|
|
@ -1322,7 +1328,7 @@ describe("OAuthManager", () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
storageOf(params).getScope.mockReturnValue("mcp");
|
storageOf(params).getScope.mockResolvedValue("mcp");
|
||||||
storageOf(params).getTokens.mockResolvedValue({
|
storageOf(params).getTokens.mockResolvedValue({
|
||||||
access_token: "tok",
|
access_token: "tok",
|
||||||
token_type: "Bearer",
|
token_type: "Bearer",
|
||||||
|
|
@ -1360,7 +1366,7 @@ describe("OAuthManager", () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
storageOf(params).getScope.mockReturnValue("mcp tools:read");
|
storageOf(params).getScope.mockResolvedValue("mcp tools:read");
|
||||||
storageOf(params).getTokens.mockResolvedValue({
|
storageOf(params).getTokens.mockResolvedValue({
|
||||||
access_token: "tok",
|
access_token: "tok",
|
||||||
token_type: "Bearer",
|
token_type: "Bearer",
|
||||||
|
|
@ -1453,7 +1459,7 @@ describe("OAuthManager", () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
storageOf(params).getScope.mockReturnValue("mcp");
|
storageOf(params).getScope.mockResolvedValue("mcp");
|
||||||
storageOf(params).getTokens.mockResolvedValue({
|
storageOf(params).getTokens.mockResolvedValue({
|
||||||
access_token: "tok",
|
access_token: "tok",
|
||||||
token_type: "Bearer",
|
token_type: "Bearer",
|
||||||
|
|
@ -1501,7 +1507,7 @@ describe("OAuthManager", () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
storageOf(params).getScope.mockReturnValue("mcp tools:read");
|
storageOf(params).getScope.mockResolvedValue("mcp tools:read");
|
||||||
storageOf(params).getTokens.mockResolvedValue({
|
storageOf(params).getTokens.mockResolvedValue({
|
||||||
access_token: "old",
|
access_token: "old",
|
||||||
token_type: "Bearer",
|
token_type: "Bearer",
|
||||||
|
|
@ -1635,7 +1641,7 @@ describe("OAuthManager", () => {
|
||||||
token_type: "Bearer",
|
token_type: "Bearer",
|
||||||
scope: "mcp tools:read weather:read",
|
scope: "mcp tools:read weather:read",
|
||||||
};
|
};
|
||||||
storageOf(params).getScope.mockReturnValue("mcp tools:read");
|
storageOf(params).getScope.mockResolvedValue("mcp tools:read");
|
||||||
storageOf(params)
|
storageOf(params)
|
||||||
.getTokens.mockResolvedValueOnce(insufficientTokens)
|
.getTokens.mockResolvedValueOnce(insufficientTokens)
|
||||||
.mockResolvedValue(sufficientTokens);
|
.mockResolvedValue(sufficientTokens);
|
||||||
|
|
@ -1665,7 +1671,7 @@ describe("OAuthManager", () => {
|
||||||
token_type: "Bearer",
|
token_type: "Bearer",
|
||||||
scope: "mcp tools:read weather:read",
|
scope: "mcp tools:read weather:read",
|
||||||
};
|
};
|
||||||
storageOf(params).getScope.mockReturnValue("mcp tools:read");
|
storageOf(params).getScope.mockResolvedValue("mcp tools:read");
|
||||||
storageOf(params)
|
storageOf(params)
|
||||||
.getTokens.mockResolvedValueOnce(insufficientTokens)
|
.getTokens.mockResolvedValueOnce(insufficientTokens)
|
||||||
.mockResolvedValueOnce(insufficientTokens)
|
.mockResolvedValueOnce(insufficientTokens)
|
||||||
|
|
@ -1801,7 +1807,7 @@ describe("OAuthManager", () => {
|
||||||
.mockResolvedValueOnce("AUTHORIZED")
|
.mockResolvedValueOnce("AUTHORIZED")
|
||||||
.mockResolvedValueOnce("REDIRECT");
|
.mockResolvedValueOnce("REDIRECT");
|
||||||
const params = createMockParams();
|
const params = createMockParams();
|
||||||
storageOf(params).getScope.mockReturnValue("mcp tools:read");
|
storageOf(params).getScope.mockResolvedValue("mcp tools:read");
|
||||||
storageOf(params).getTokens.mockResolvedValue({
|
storageOf(params).getTokens.mockResolvedValue({
|
||||||
access_token: "access",
|
access_token: "access",
|
||||||
token_type: "Bearer",
|
token_type: "Bearer",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||||
import {
|
import {
|
||||||
NodeOAuthStorage,
|
NodeOAuthStorage,
|
||||||
getOAuthStore,
|
getStateFilePath,
|
||||||
|
resetNodeOAuthStorageCache,
|
||||||
clearAllOAuthClientState,
|
clearAllOAuthClientState,
|
||||||
} from "@inspector/core/auth/node/storage-node.js";
|
} from "@inspector/core/auth/node/storage-node.js";
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -20,44 +21,27 @@ const testStatePath = path.join(
|
||||||
`mcp-inspector-oauth-${process.pid}-storage-node.json`,
|
`mcp-inspector-oauth-${process.pid}-storage-node.json`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
async function resetTestStorage(stateFilePath: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await fs.unlink(stateFilePath);
|
||||||
|
} catch {
|
||||||
|
// Ignore if file doesn't exist
|
||||||
|
}
|
||||||
|
resetNodeOAuthStorageCache(stateFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
describe("NodeOAuthStorage", () => {
|
describe("NodeOAuthStorage", () => {
|
||||||
let storage: NodeOAuthStorage;
|
let storage: NodeOAuthStorage;
|
||||||
const testServerUrl = "http://localhost:3000";
|
const testServerUrl = "http://localhost:3000";
|
||||||
const stateFilePath = testStatePath;
|
const stateFilePath = testStatePath;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// Clean up any existing state file
|
await resetTestStorage(stateFilePath);
|
||||||
try {
|
|
||||||
await fs.unlink(stateFilePath);
|
|
||||||
} catch {
|
|
||||||
// Ignore if file doesn't exist
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset store state by clearing all servers
|
|
||||||
const store = getOAuthStore(testStatePath);
|
|
||||||
const state = store.getState();
|
|
||||||
// Clear all server states
|
|
||||||
Object.keys(state.servers).forEach((url) => {
|
|
||||||
state.clearServerState(url);
|
|
||||||
});
|
|
||||||
|
|
||||||
storage = new NodeOAuthStorage(testStatePath);
|
storage = new NodeOAuthStorage(testStatePath);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
// Clean up state file after each test
|
await resetTestStorage(stateFilePath);
|
||||||
try {
|
|
||||||
await fs.unlink(stateFilePath);
|
|
||||||
} catch {
|
|
||||||
// Ignore if file doesn't exist
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset store state
|
|
||||||
const store = getOAuthStore(testStatePath);
|
|
||||||
const state = store.getState();
|
|
||||||
Object.keys(state.servers).forEach((url) => {
|
|
||||||
state.clearServerState(url);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getClientInformation", () => {
|
describe("getClientInformation", () => {
|
||||||
|
|
@ -88,11 +72,10 @@ describe("NodeOAuthStorage", () => {
|
||||||
client_secret: "preregistered-secret",
|
client_secret: "preregistered-secret",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store as preregistered by directly setting it in the store
|
await storage.savePreregisteredClientInformation(
|
||||||
const store = getOAuthStore(testStatePath);
|
testServerUrl,
|
||||||
store.getState().setServerState(testServerUrl, {
|
preregisteredInfo,
|
||||||
preregisteredClientInformation: preregisteredInfo,
|
);
|
||||||
});
|
|
||||||
|
|
||||||
const result = await storage.getClientInformation(testServerUrl, true);
|
const result = await storage.getClientInformation(testServerUrl, true);
|
||||||
|
|
||||||
|
|
@ -341,16 +324,16 @@ describe("NodeOAuthStorage", () => {
|
||||||
|
|
||||||
it("clearCodeVerifier removes only the PKCE verifier", async () => {
|
it("clearCodeVerifier removes only the PKCE verifier", async () => {
|
||||||
await storage.saveCodeVerifier(testServerUrl, "verifier");
|
await storage.saveCodeVerifier(testServerUrl, "verifier");
|
||||||
expect(storage.getCodeVerifier(testServerUrl)).toBe("verifier");
|
expect(await storage.getCodeVerifier(testServerUrl)).toBe("verifier");
|
||||||
await storage.clearCodeVerifier(testServerUrl);
|
await storage.clearCodeVerifier(testServerUrl);
|
||||||
expect(storage.getCodeVerifier(testServerUrl)).toBeUndefined();
|
expect(await storage.getCodeVerifier(testServerUrl)).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clearScope removes only the scope", async () => {
|
it("clearScope removes only the scope", async () => {
|
||||||
await storage.saveScope(testServerUrl, "read write");
|
await storage.saveScope(testServerUrl, "read write");
|
||||||
expect(storage.getScope(testServerUrl)).toBe("read write");
|
expect(await storage.getScope(testServerUrl)).toBe("read write");
|
||||||
await storage.clearScope(testServerUrl);
|
await storage.clearScope(testServerUrl);
|
||||||
expect(storage.getScope(testServerUrl)).toBeUndefined();
|
expect(await storage.getScope(testServerUrl)).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clearServerMetadata removes only the cached metadata", async () => {
|
it("clearServerMetadata removes only the cached metadata", async () => {
|
||||||
|
|
@ -361,9 +344,9 @@ describe("NodeOAuthStorage", () => {
|
||||||
response_types_supported: ["code"],
|
response_types_supported: ["code"],
|
||||||
};
|
};
|
||||||
await storage.saveServerMetadata(testServerUrl, metadata);
|
await storage.saveServerMetadata(testServerUrl, metadata);
|
||||||
expect(storage.getServerMetadata(testServerUrl)).toEqual(metadata);
|
expect(await storage.getServerMetadata(testServerUrl)).toEqual(metadata);
|
||||||
await storage.clearServerMetadata(testServerUrl);
|
await storage.clearServerMetadata(testServerUrl);
|
||||||
expect(storage.getServerMetadata(testServerUrl)).toBeNull();
|
expect(await storage.getServerMetadata(testServerUrl)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -437,83 +420,52 @@ describe("NodeOAuthStorage", () => {
|
||||||
expect(result2).toEqual(clientInfo2);
|
expect(result2).toEqual(clientInfo2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe("OAuth Store (Zustand)", () => {
|
it("reuses in-memory state for the same file path", async () => {
|
||||||
const stateFilePath = testStatePath;
|
const serverUrl = "http://localhost:3000";
|
||||||
|
const tokens: OAuthTokens = {
|
||||||
|
access_token: "shared-memory-token",
|
||||||
|
token_type: "Bearer",
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
await storage.saveTokens(serverUrl, tokens);
|
||||||
try {
|
|
||||||
await fs.unlink(stateFilePath);
|
const otherView = new NodeOAuthStorage(testStatePath);
|
||||||
} catch {
|
expect(await otherView.getTokens(serverUrl)).toEqual(tokens);
|
||||||
// Ignore if file doesn't exist
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
it("persists state to file on save", async () => {
|
||||||
try {
|
|
||||||
await fs.unlink(stateFilePath);
|
|
||||||
} catch {
|
|
||||||
// Ignore if file doesn't exist
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create a new store", () => {
|
|
||||||
const store = getOAuthStore(testStatePath);
|
|
||||||
expect(store).toBeDefined();
|
|
||||||
expect(store.getState).toBeDefined();
|
|
||||||
expect(store.setState).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return the same store instance via getOAuthStore", () => {
|
|
||||||
const store1 = getOAuthStore(testStatePath);
|
|
||||||
const store2 = getOAuthStore(testStatePath);
|
|
||||||
expect(store1).toBe(store2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should persist state to file", async () => {
|
|
||||||
// Use a unique path so no other test (e.g. "should overwrite..." with second-id) can
|
|
||||||
// write to the same file; Zustand persist is async and can race with shared paths.
|
|
||||||
const persistTestPath = path.join(
|
const persistTestPath = path.join(
|
||||||
os.tmpdir(),
|
os.tmpdir(),
|
||||||
`mcp-inspector-oauth-persist-${Date.now()}-${Math.random().toString(36).slice(2)}.json`,
|
`mcp-inspector-oauth-persist-${Date.now()}-${Math.random().toString(36).slice(2)}.json`,
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
if (process.env.DEBUG_PERSIST_PATH === "1") {
|
resetNodeOAuthStorageCache(persistTestPath);
|
||||||
console.error("[storage-node.test] state file path:", persistTestPath);
|
const fileStorage = new NodeOAuthStorage(persistTestPath);
|
||||||
}
|
|
||||||
const store = getOAuthStore(persistTestPath);
|
|
||||||
const serverUrl = "http://localhost:3000";
|
const serverUrl = "http://localhost:3000";
|
||||||
const clientInfo: OAuthClientInformation = {
|
const clientInfo: OAuthClientInformation = {
|
||||||
client_id: "test-client-id",
|
client_id: "test-client-id",
|
||||||
};
|
};
|
||||||
|
|
||||||
store.getState().setServerState(serverUrl, {
|
await fileStorage.saveClientInformation(serverUrl, clientInfo, {
|
||||||
clientInformation: clientInfo,
|
registrationKind: "dcr",
|
||||||
});
|
});
|
||||||
|
|
||||||
type StateShape = {
|
type StateShape = {
|
||||||
state: {
|
servers: Record<string, { clientInformation?: OAuthClientInformation }>;
|
||||||
servers: Record<
|
|
||||||
string,
|
|
||||||
{ clientInformation?: OAuthClientInformation }
|
|
||||||
>;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
// Persistence is fire-and-forget; await the write rather than polling.
|
|
||||||
await flushStoreFileWrites(persistTestPath);
|
await flushStoreFileWrites(persistTestPath);
|
||||||
const parsed = JSON.parse(
|
const parsed = JSON.parse(
|
||||||
await fs.readFile(persistTestPath, "utf-8"),
|
await fs.readFile(persistTestPath, "utf-8"),
|
||||||
) as StateShape;
|
) as StateShape;
|
||||||
expect(parsed.state.servers[serverUrl]?.clientInformation).toEqual(
|
expect(parsed.servers[serverUrl]?.clientInformation).toEqual(clientInfo);
|
||||||
clientInfo,
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
await fs.unlink(persistTestPath);
|
await fs.unlink(persistTestPath);
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
|
resetNodeOAuthStorageCache(persistTestPath);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -572,9 +524,7 @@ describe("NodeOAuthStorage with custom storagePath", () => {
|
||||||
await storage.saveTokens(testServerUrl, tokens);
|
await storage.saveTokens(testServerUrl, tokens);
|
||||||
|
|
||||||
type StateShape = {
|
type StateShape = {
|
||||||
state: {
|
servers: Record<string, { tokens?: { access_token?: string } }>;
|
||||||
servers: Record<string, { tokens?: { access_token?: string } }>;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
// Persistence is fire-and-forget; await the write rather than polling.
|
// Persistence is fire-and-forget; await the write rather than polling.
|
||||||
await flushStoreFileWrites(customPath);
|
await flushStoreFileWrites(customPath);
|
||||||
|
|
@ -582,7 +532,7 @@ describe("NodeOAuthStorage with custom storagePath", () => {
|
||||||
await fs.readFile(customPath, "utf-8"),
|
await fs.readFile(customPath, "utf-8"),
|
||||||
) as StateShape;
|
) as StateShape;
|
||||||
|
|
||||||
expect(parsed.state.servers[testServerUrl]?.tokens?.access_token).toBe(
|
expect(parsed.servers[testServerUrl]?.tokens?.access_token).toBe(
|
||||||
tokens.access_token,
|
tokens.access_token,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -599,33 +549,34 @@ describe("NodeOAuthStorage with custom storagePath", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clearAllOAuthClientState clears every server in the default store", async () => {
|
it("clearAllOAuthClientState clears every server in the default store", async () => {
|
||||||
const defaultStore = getOAuthStore();
|
resetNodeOAuthStorageCache();
|
||||||
defaultStore.getState().setServerState("http://server-a.test", {
|
const storage = new NodeOAuthStorage();
|
||||||
tokens: { access_token: "a", token_type: "Bearer" },
|
await storage.saveTokens("http://server-a.test", {
|
||||||
|
access_token: "a",
|
||||||
|
token_type: "Bearer",
|
||||||
});
|
});
|
||||||
defaultStore.getState().setServerState("http://server-b.test", {
|
await storage.saveTokens("http://server-b.test", {
|
||||||
tokens: { access_token: "b", token_type: "Bearer" },
|
access_token: "b",
|
||||||
|
token_type: "Bearer",
|
||||||
});
|
});
|
||||||
clearAllOAuthClientState();
|
await flushStoreFileWrites(getStateFilePath());
|
||||||
const all = defaultStore.getState();
|
|
||||||
expect(all.getServerState("http://server-a.test").tokens).toBeUndefined();
|
await clearAllOAuthClientState();
|
||||||
expect(all.getServerState("http://server-b.test").tokens).toBeUndefined();
|
|
||||||
|
expect(await storage.getTokens("http://server-a.test")).toBeUndefined();
|
||||||
|
expect(await storage.getTokens("http://server-b.test")).toBeUndefined();
|
||||||
|
|
||||||
|
resetNodeOAuthStorageCache();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clearAllOAuthClientState tolerates a store whose servers map is absent", () => {
|
it("clearAllOAuthClientState tolerates persisted state without a servers map", async () => {
|
||||||
// Exercises the `state.servers ?? {}` nullish fallback: when the persisted
|
const filePath = getStateFilePath();
|
||||||
// state lacks a `servers` map, the iteration must default to {} (no throw).
|
resetNodeOAuthStorageCache();
|
||||||
const defaultStore = getOAuthStore();
|
await fs.writeFile(filePath, JSON.stringify({ idpSessions: {} }), "utf-8");
|
||||||
const clearServerState = vi.fn();
|
|
||||||
vi.spyOn(defaultStore, "getState").mockReturnValue({
|
|
||||||
servers: undefined,
|
|
||||||
clearServerState,
|
|
||||||
} as unknown as ReturnType<typeof defaultStore.getState>);
|
|
||||||
|
|
||||||
expect(() => clearAllOAuthClientState()).not.toThrow();
|
await expect(clearAllOAuthClientState()).resolves.toBeUndefined();
|
||||||
expect(clearServerState).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
vi.mocked(defaultStore.getState).mockRestore();
|
resetNodeOAuthStorageCache();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should isolate state from default store", async () => {
|
it("should isolate state from default store", async () => {
|
||||||
|
|
@ -635,12 +586,14 @@ describe("NodeOAuthStorage with custom storagePath", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
resetNodeOAuthStorageCache();
|
||||||
const defaultStorage = new NodeOAuthStorage();
|
const defaultStorage = new NodeOAuthStorage();
|
||||||
await defaultStorage.saveTokens(testServerUrl, {
|
await defaultStorage.saveTokens(testServerUrl, {
|
||||||
access_token: "default-token",
|
access_token: "default-token",
|
||||||
token_type: "Bearer",
|
token_type: "Bearer",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
resetNodeOAuthStorageCache(customPath);
|
||||||
const customStorage = new NodeOAuthStorage(customPath);
|
const customStorage = new NodeOAuthStorage(customPath);
|
||||||
await customStorage.saveTokens(testServerUrl, {
|
await customStorage.saveTokens(testServerUrl, {
|
||||||
access_token: "custom-token",
|
access_token: "custom-token",
|
||||||
|
|
@ -653,13 +606,15 @@ describe("NodeOAuthStorage with custom storagePath", () => {
|
||||||
const fromDefault = await defaultStorage.getTokens(testServerUrl);
|
const fromDefault = await defaultStorage.getTokens(testServerUrl);
|
||||||
expect(fromDefault?.access_token).toBe("default-token");
|
expect(fromDefault?.access_token).toBe("default-token");
|
||||||
|
|
||||||
getOAuthStore().getState().clearServerState(testServerUrl);
|
await defaultStorage.clear(testServerUrl);
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
await fs.unlink(customPath);
|
await fs.unlink(customPath);
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
|
resetNodeOAuthStorageCache(customPath);
|
||||||
|
resetNodeOAuthStorageCache();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ describe("InspectorClient EMA E2E", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
clearAllOAuthClientState();
|
await clearAllOAuthClientState();
|
||||||
vi.spyOn(console, "log").mockImplementation(() => {});
|
vi.spyOn(console, "log").mockImplementation(() => {});
|
||||||
|
|
||||||
const keys = await createEmaMockKeyMaterial();
|
const keys = await createEmaMockKeyMaterial();
|
||||||
|
|
@ -199,14 +199,12 @@ describe("InspectorClient EMA E2E", () => {
|
||||||
await flushStoreFileWrites(oauthTestStatePath);
|
await flushStoreFileWrites(oauthTestStatePath);
|
||||||
|
|
||||||
const raw = JSON.parse(await fs.readFile(oauthTestStatePath, "utf-8")) as {
|
const raw = JSON.parse(await fs.readFile(oauthTestStatePath, "utf-8")) as {
|
||||||
state: {
|
servers: Record<
|
||||||
servers: Record<
|
string,
|
||||||
string,
|
{ tokens?: { access_token?: string }; enterpriseManaged?: boolean }
|
||||||
{ tokens?: { access_token?: string }; enterpriseManaged?: boolean }
|
>;
|
||||||
>;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
const entry = raw.state.servers[mcpUrl];
|
const entry = raw.servers[mcpUrl];
|
||||||
expect(entry?.tokens?.access_token).toBeDefined();
|
expect(entry?.tokens?.access_token).toBeDefined();
|
||||||
expect(entry?.enterpriseManaged).toBe(true);
|
expect(entry?.enterpriseManaged).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue