fix: harden oauth vault persistence
This commit is contained in:
parent
b69dd07dba
commit
91819929d9
@ -6,8 +6,6 @@ import type { OAuthClientInformationMixed, OAuthTokens } from '@modelcontextprot
|
|||||||
import type { ServerDefinition } from './config.js';
|
import type { ServerDefinition } from './config.js';
|
||||||
import { readJsonFile, writeJsonFile } from './fs-json.js';
|
import { readJsonFile, writeJsonFile } from './fs-json.js';
|
||||||
|
|
||||||
const VAULT_PATH = path.join(os.homedir(), '.mcporter', 'credentials.json');
|
|
||||||
|
|
||||||
type VaultKey = string;
|
type VaultKey = string;
|
||||||
|
|
||||||
export interface VaultEntry {
|
export interface VaultEntry {
|
||||||
@ -25,10 +23,14 @@ interface VaultFile {
|
|||||||
entries: Record<VaultKey, VaultEntry>;
|
entries: Record<VaultKey, VaultEntry>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function vaultPath(): string {
|
||||||
|
return path.join(os.homedir(), '.mcporter', 'credentials.json');
|
||||||
|
}
|
||||||
|
|
||||||
async function readVault(): Promise<VaultFile> {
|
async function readVault(): Promise<VaultFile> {
|
||||||
let shouldRewrite = false;
|
let shouldRewrite = false;
|
||||||
try {
|
try {
|
||||||
const existing = await readJsonFile<VaultFile>(VAULT_PATH);
|
const existing = await readJsonFile<VaultFile>(vaultPath());
|
||||||
if (existing && existing.version === 1 && existing.entries && typeof existing.entries === 'object') {
|
if (existing && existing.version === 1 && existing.entries && typeof existing.entries === 'object') {
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
@ -46,9 +48,10 @@ async function readVault(): Promise<VaultFile> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function writeVault(contents: VaultFile): Promise<void> {
|
async function writeVault(contents: VaultFile): Promise<void> {
|
||||||
const dir = path.dirname(VAULT_PATH);
|
const filePath = vaultPath();
|
||||||
|
const dir = path.dirname(filePath);
|
||||||
await fs.mkdir(dir, { recursive: true });
|
await fs.mkdir(dir, { recursive: true });
|
||||||
await writeJsonFile(VAULT_PATH, contents);
|
await writeJsonFile(filePath, contents);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function vaultKeyForDefinition(definition: ServerDefinition): VaultKey {
|
export function vaultKeyForDefinition(definition: ServerDefinition): VaultKey {
|
||||||
|
|||||||
@ -24,4 +24,27 @@ describe('CLI call error reporting', () => {
|
|||||||
logSpy.mockRestore();
|
logSpy.mockRestore();
|
||||||
errorSpy.mockRestore();
|
errorSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('emits structured http envelopes for non-auth transport failures', async () => {
|
||||||
|
const { handleCall } = await cliModulePromise;
|
||||||
|
const callTool = vi.fn().mockRejectedValue(new Error('SSE error: Non-200 status code (410)'));
|
||||||
|
const runtime = {
|
||||||
|
callTool,
|
||||||
|
close: vi.fn().mockResolvedValue(undefined),
|
||||||
|
} as unknown as Awaited<ReturnType<typeof import('../src/runtime.js')['createRuntime']>>;
|
||||||
|
|
||||||
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||||
|
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
await handleCall(runtime, ['deepwiki.read_wiki_structure', '--output', 'json']);
|
||||||
|
|
||||||
|
const payload = JSON.parse(logSpy.mock.calls.at(-1)?.[0] ?? '{}');
|
||||||
|
expect(payload.issue?.kind).toBe('http');
|
||||||
|
expect(payload.issue?.statusCode).toBe(410);
|
||||||
|
expect(payload.tool).toBe('read_wiki_structure');
|
||||||
|
expect(errorSpy.mock.calls.some((call) => call.join(' ').includes('responded with HTTP 410'))).toBe(true);
|
||||||
|
|
||||||
|
logSpy.mockRestore();
|
||||||
|
errorSpy.mockRestore();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -116,6 +116,14 @@ describe('printCallOutput format selection', () => {
|
|||||||
expect(String(logged)).toContain("type: 'json'");
|
expect(String(logged)).toContain("type: 'json'");
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'auto falls back to readable raw output for plain object payloads',
|
||||||
|
'auto',
|
||||||
|
{ result: 'Available pages for facebook/react' },
|
||||||
|
(logged: unknown) => {
|
||||||
|
expect(String(logged)).toContain("result: 'Available pages for facebook/react'");
|
||||||
|
},
|
||||||
|
],
|
||||||
] as const)('%s', (_name, format, raw, assertLogged) => {
|
] as const)('%s', (_name, format, raw, assertLogged) => {
|
||||||
const wrapped = createCallResult(raw);
|
const wrapped = createCallResult(raw);
|
||||||
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
|
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||||
|
|||||||
@ -14,5 +14,8 @@ export default defineConfig({
|
|||||||
// Quiet mode hides console output for passing tests so CLI fixture logs
|
// Quiet mode hides console output for passing tests so CLI fixture logs
|
||||||
// (e.g., the full `mcporter list` banners) don't overwhelm the reporter.
|
// (e.g., the full `mcporter list` banners) don't overwhelm the reporter.
|
||||||
...quietReporterOptions,
|
...quietReporterOptions,
|
||||||
|
// CLI-heavy suites import the full entrypoint in parallel and can exceed the
|
||||||
|
// default 5s timeout under local load even when behavior is correct.
|
||||||
|
testTimeout: 10_000,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user