fix: chunk OAuth credentials for platform keyrings
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
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";
|
||||
|
||||
class MemoryEntry {
|
||||
constructor(private readonly values: Map<string, string>, private readonly account: string) {}
|
||||
|
||||
async getPassword(): Promise<string | undefined> {
|
||||
return this.values.get(this.account);
|
||||
}
|
||||
|
||||
async setPassword(password: string): Promise<void> {
|
||||
if (password.length > 1000) throw new Error("credential too large");
|
||||
this.values.set(this.account, password);
|
||||
}
|
||||
|
||||
async deleteCredential(): Promise<boolean> {
|
||||
return this.values.delete(this.account);
|
||||
}
|
||||
}
|
||||
|
||||
function memoryStore(values = new Map<string, string>()): 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<string, string>();
|
||||
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<string, string>();
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user