fix: harden oauth vault persistence

This commit is contained in:
Peter Steinberger 2026-03-29 09:53:17 +09:00
parent b69dd07dba
commit 91819929d9
No known key found for this signature in database
4 changed files with 42 additions and 5 deletions

View File

@ -6,8 +6,6 @@ import type { OAuthClientInformationMixed, OAuthTokens } from '@modelcontextprot
import type { ServerDefinition } from './config.js';
import { readJsonFile, writeJsonFile } from './fs-json.js';
const VAULT_PATH = path.join(os.homedir(), '.mcporter', 'credentials.json');
type VaultKey = string;
export interface VaultEntry {
@ -25,10 +23,14 @@ interface VaultFile {
entries: Record<VaultKey, VaultEntry>;
}
function vaultPath(): string {
return path.join(os.homedir(), '.mcporter', 'credentials.json');
}
async function readVault(): Promise<VaultFile> {
let shouldRewrite = false;
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') {
return existing;
}
@ -46,9 +48,10 @@ async function readVault(): Promise<VaultFile> {
}
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 writeJsonFile(VAULT_PATH, contents);
await writeJsonFile(filePath, contents);
}
export function vaultKeyForDefinition(definition: ServerDefinition): VaultKey {

View File

@ -24,4 +24,27 @@ describe('CLI call error reporting', () => {
logSpy.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();
});
});

View File

@ -116,6 +116,14 @@ describe('printCallOutput format selection', () => {
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) => {
const wrapped = createCallResult(raw);
const log = vi.spyOn(console, 'log').mockImplementation(() => {});

View File

@ -14,5 +14,8 @@ export default defineConfig({
// Quiet mode hides console output for passing tests so CLI fixture logs
// (e.g., the full `mcporter list` banners) don't overwhelm the reporter.
...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,
},
});