/** * 覆盖系统凭据存储适配器的令牌分块、轮换清理与旧格式兼容行为。 * * @packageDocumentation */ import assert from "node:assert/strict"; import test from "node:test"; import type { OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js"; import { KeyringTokenStore } from "../src/credentials.js"; import { credentialAccount, resolveClientOptions } from "../src/config.js"; import { logoutRemote } from "../src/remote.js"; class MemoryEntry { constructor(private readonly values: Map, private readonly account: string) {} async getPassword(): Promise { return this.values.get(this.account); } async setPassword(password: string): Promise { if (password.length > 1000) throw new Error("credential too large"); this.values.set(this.account, password); } async deleteCredential(): Promise { return this.values.delete(this.account); } } function memoryStore(values = new Map()): KeyringTokenStore { return new KeyringTokenStore(async (account) => new MemoryEntry(values, account)); } test("keyring token store chunks large OAuth credentials and removes every chunk", async () => { const values = new Map(); const store = memoryStore(values); const tokens: OAuthTokens = { access_token: "a".repeat(3400), refresh_token: "r".repeat(2400), token_type: "Bearer", }; await store.set("account", tokens); assert.ok(values.size > 2); assert.deepEqual(await store.get("account"), tokens); assert.ok([...values.values()].every((value) => value.length <= 1000)); await store.delete("account"); assert.equal(values.size, 0); }); test("keyring token store replaces rotated credentials and cleans old chunks", async () => { const values = new Map(); const store = memoryStore(values); await store.set("account", { access_token: "a".repeat(3000), refresh_token: "old", token_type: "Bearer" }); const oldAccounts = [...values.keys()].filter((account) => account !== "account"); await store.set("account", { access_token: "b".repeat(3000), refresh_token: "new", token_type: "Bearer" }); assert.equal((await store.get("account"))?.refresh_token, "new"); assert.ok(oldAccounts.every((account) => !values.has(account))); }); test("keyring token store still reads and removes legacy single-entry credentials", async () => { const tokens: OAuthTokens = { access_token: "legacy", token_type: "Bearer" }; const values = new Map([["account", JSON.stringify(tokens)]]); const store = memoryStore(values); assert.deepEqual(await store.get("account"), tokens); await store.delete("account"); assert.equal(values.size, 0); }); for (const damage of ["invalid-json", "missing-token-fields", "missing-chunk"] as const) { test(`local-only logout removes ${damage} credentials without remote requests`, async () => { const options = resolveClientOptions({ url: "https://example.test/mcp", env: {} }); const account = credentialAccount(options); const values = new Map(); const store = memoryStore(values); if (damage === "missing-chunk") { await store.set(account, { access_token: "a".repeat(3000), token_type: "Bearer" }); values.delete([...values.keys()].find((key) => key !== account)!); } else { values.set(account, damage === "invalid-json" ? "{broken" : "{}"); } values.set("other-account", "untouched"); await assert.rejects(store.get(account), /credential is invalid/); const discovery = new Map([[account, "cached"], ["other-account", "untouched"]]); const result = await logoutRemote(options, store, { async delete(key: string) { discovery.delete(key); }, } as never, true, async () => { throw new Error("unexpected network request"); }); assert.deepEqual(result, { hadCredential: true, revoked: false }); assert.deepEqual([...values], [["other-account", "untouched"]]); assert.deepEqual([...discovery], [["other-account", "untouched"]]); assert.equal(await store.get(account), undefined); assert.deepEqual(await logoutRemote(options, store, { async delete(key: string) { discovery.delete(key); }, } as never, true), { hadCredential: false, revoked: false }); }); } test("local-only logout reports credential store failures without claiming success", async () => { const store = new KeyringTokenStore(async () => { throw new Error("access denied"); }); await assert.rejects(logoutRemote(resolveClientOptions({ env: {} }), store, { async delete() { assert.fail("discovery must be retained when credential deletion fails"); }, } as never, true), /credential store is unavailable.*access denied/); }); test("keyring null for an absent Windows credential is treated as missing", async () => { const store = new KeyringTokenStore(async () => ({ async getPassword() { return null; }, async setPassword() { assert.fail("unexpected credential write"); }, async deleteCredential() { return false; }, })); assert.equal(await store.get("account"), undefined); assert.equal(await store.delete("account"), false); });