import { test } from "node:test"; import assert from "node:assert/strict"; import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { apply, grantFor, holdingsOf, keypair, onBinding, onServerChange, pull, registerServer, registered, ServerView, type Paths, type ServerChange, type ServerState, } from "../dist/node.js"; import { generateKeyPair, open, seal } from "../dist/seal.js"; const NOW = Date.now(); function node(name = "laptop"): { p: Paths; written: Record } { const root = mkdtempSync(join(tmpdir(), "cc-node-")); const p = { state: join(root, "state"), facts: join(root, "state", "facts.json"), settings: join(root, "state", "settings.json"), home: join(root, "home"), node: name }; mkdirSync(p.state, { recursive: true }); mkdirSync(join(p.home, ".claude"), { recursive: true }); writeFileSync(p.facts, JSON.stringify({ node: name, console: "http://127.0.0.1:4270/mcp" })); writeFileSync(p.settings, JSON.stringify({ role: "", mcp_servers: {} })); return { p, written: {} }; } const writer = (w: Record) => (name: string, content: string) => { w[name] = content; return `${name}: written`; }; const creds = (p: Paths) => JSON.parse(readFileSync(join(p.home, ".claude", ".credentials.json"), "utf8")); const grantFor_ = (p: Paths, licence: string, token: string, kind: "subscription" | "api-key" = "subscription", identity?: object) => ({ licence, kind, identity, sealed: seal(kind === "api-key" ? token : JSON.stringify({ accessToken: token, expiresAt: NOW + 3_600_000, refreshTokenExpiresAt: NOW + 86_400_000, subscriptionType: licence }), keypair(p).publicKey), }); test("a pull asks the seat with this node's key and applies what it answers", async () => { const { p, written } = node(); let asked: [string, Record] | null = null; const r = await pull(p, async (address, args) => { asked = [address, args]; return grantFor_(p, "personal", "at-1"); }, writer(written)); assert.equal(asked![0], "seat:anthropic-licence-manager.current"); assert.equal(asked![1].consumer, "laptop"); assert.match(String(asked![1].public_key), /BEGIN PUBLIC KEY/); assert.equal(r.applied, true); assert.equal(creds(p).claudeAiOauth.accessToken, "at-1"); assert.ok(written["managed-mcp.json"]); }); test("a switch replaces the old licence's grant whole and points the account at the new one", () => { const { p, written } = node(); writeFileSync(join(p.home, ".claude.json"), JSON.stringify({ oauthAccount: { accountUuid: "old" }, projects: { keep: 1 } })); apply(p, grantFor_(p, "personal", "at-1"), writer(written)); const r = apply(p, grantFor_(p, "work", "at-2", "subscription", { accountUuid: "new", emailAddress: "w@example.org" }), writer(written)); assert.equal(r.switched, true); assert.equal(creds(p).claudeAiOauth.accessToken, "at-2"); assert.equal(creds(p).claudeAiOauth.subscriptionType, "work", "the old licence's subscription type survived the switch"); const account = JSON.parse(readFileSync(join(p.home, ".claude.json"), "utf8")); assert.equal(account.oauthAccount.accountUuid, "new"); assert.deepEqual(account.projects, { keep: 1 }); }); test("switching to the API key adds the key-helper; switching away removes the key and the helper", () => { const { p, written } = node(); apply(p, grantFor_(p, "api", "sk-key", "api-key"), writer(written)); assert.ok(JSON.parse(written["managed-settings.json"]).apiKeyHelper); assert.ok(existsSync(join(p.state, "api-key"))); apply(p, grantFor_(p, "personal", "at-1"), writer(written)); assert.ok(!("apiKeyHelper" in JSON.parse(written["managed-settings.json"]))); assert.ok(!existsSync(join(p.state, "api-key")) && !existsSync(join(p.state, "api-key-helper"))); }); test("what a node holds is reported with fingerprints and its account, never a token", () => { const { p } = node(); writeFileSync(join(p.home, ".claude", ".credentials.json"), JSON.stringify({ claudeAiOauth: { accessToken: "at-secret", refreshToken: "rt-secret", expiresAt: NOW + 1000, refreshTokenExpiresAt: NOW + 9000 } })); writeFileSync(join(p.home, ".claude.json"), JSON.stringify({ oauthAccount: { accountUuid: "u-1", emailAddress: "a@example.org" } })); const h = holdingsOf(p); assert.equal(h.node, "laptop"); assert.equal(h.identity?.accountUuid, "u-1"); assert.equal(h.kind, "subscription"); assert.equal(h.refresh.present, true); assert.match(String(h.refresh.fingerprint), /^sha256:[0-9a-f]{16}$/); assert.equal(h.access?.expiresAt, NOW + 1000); assert.ok(h.changedAt); const text = JSON.stringify(h); assert.ok(!text.includes("at-secret") && !text.includes("rt-secret"), "a token is in the report"); assert.ok(!/token|secret|password/i.test(Object.keys(h).join(" ") + " " + Object.keys(h.refresh).join(" ")), "a field the runtime would refuse is in the report"); }); test("a node with nothing reports nothing held, and the grant answers only a waiting login", () => { const { p } = node(); const h = holdingsOf(p); assert.equal(h.kind, null); assert.equal(h.refresh.present, false); const manager = generateKeyPair(); assert.equal(grantFor(p, manager.publicKey), null); writeFileSync(join(p.home, ".claude", ".credentials.json"), JSON.stringify({ claudeAiOauth: { accessToken: "at", refreshToken: "rt-login", expiresAt: NOW } })); writeFileSync(join(p.home, ".claude.json"), JSON.stringify({ oauthAccount: { accountUuid: "u-9" } })); const g = grantFor(p, manager.publicKey)!; assert.equal(g.identity?.accountUuid, "u-9"); assert.equal(JSON.parse(open(g.sealed, manager.privateKey)).refreshToken, "rt-login"); assert.ok(!JSON.stringify(g).includes("rt-login"), "the refresh token crossed in the clear"); }); test("a newer generation in the bindings fetches the token once, by the seat's verb; an equal one asks nothing", async () => { const { p, written } = node(); const asked: string[] = []; const seatAsk = async (address: string) => { asked.push(address); return { ...grantFor_(p, "personal", "at-1"), generation: 3 }; }; await onBinding(p, { licence: "personal", kind: "subscription", generation: 3 }, seatAsk, writer(written)); assert.deepEqual(asked, ["seat:anthropic-licence-manager.current"]); assert.equal(creds(p).claudeAiOauth.accessToken, "at-1"); assert.equal(await onBinding(p, { licence: "personal", kind: "subscription", generation: 3 }, seatAsk, writer(written)), null); assert.equal(asked.length, 1, "an equal generation asked again"); assert.equal(holdingsOf(p).generation, 3); }); test("the token a node is handed replaces a login's grant and leaves no refresh token", () => { const { p, written } = node(); writeFileSync(join(p.home, ".claude", ".credentials.json"), JSON.stringify({ claudeAiOauth: { accessToken: "at-old", refreshToken: "rt-spent", expiresAt: NOW + 7_200_000 } })); const r = apply(p, grantFor_(p, "personal", "at-new"), writer(written)); assert.equal(r.applied, true); assert.equal(creds(p).claudeAiOauth.accessToken, "at-new"); assert.equal(creds(p).claudeAiOauth.refreshToken, undefined, "a refresh token survived the hand-over"); assert.equal(holdingsOf(p).refresh.present, false); }); /** The `servers` state as the bus holds it, shared by every node in a test, with each node's watch. */ function bus() { const kept = new Map>(); const watchers: ((c: ServerChange) => void)[] = []; const state: ServerState = { put: async (key, value) => { kept.set(key, value); watchers.forEach((w) => w({ key, op: "put", value })); return kept.size; }, delete: async (key) => { kept.delete(key); watchers.forEach((w) => w({ key, op: "delete" })); }, keys: async () => [...kept.keys()].sort(), }; /** A node joining: its view takes the current state, then every change. */ const join = (n: { p: Paths; written: Record }) => { const view = new ServerView(n.p); for (const [key, value] of kept) onServerChange(view, { key, op: "put", value }, n.p, writer(n.written)); watchers.push((c) => onServerChange(view, c, n.p, writer(n.written))); return view; }; return { state, join, kept }; } test("registering a server here puts it under this node's key, renders it, and asks about the other nodes", async () => { const n = node(); const b = bus(); const view = b.join(n); const r = await registerServer(n.p, { name: "search", entry: { type: "http", url: "https://s.example/mcp" } }, b.state, view, writer(n.written), async () => ["laptop", "server", "desktop"]); assert.equal(r.here, "changed"); assert.match(String(r.also), /server, desktop/); assert.deepEqual([...b.kept.keys()], ["laptop.search"]); assert.ok(JSON.parse(n.written["managed-mcp.json"]).mcpServers.search); }); test("registering for every node reaches the others through their watch, and a node joining later reads it", async () => { const a = node("laptop"), s = node("server"); const b = bus(); const va = b.join(a); b.join(s); await registerServer(a.p, { name: "docs", entry: { type: "stdio", command: "docs-mcp" }, nodes: "all" }, b.state, va, writer(a.written), async () => []); assert.deepEqual([...b.kept.keys()], ["all.docs"]); assert.deepEqual(registered(s.p).docs, { type: "stdio", command: "docs-mcp" }); assert.ok(JSON.parse(s.written["managed-mcp.json"]).mcpServers.docs); // The gap events left: a node assigned after the registration takes the whole current set at start. const late = node("desktop"); b.join(late); assert.deepEqual(registered(late.p).docs, { type: "stdio", command: "docs-mcp" }); // Unregistering is a delete, and every node's view drops it. await registerServer(a.p, { name: "docs", nodes: "all" }, b.state, va, writer(a.written), async () => []); assert.equal(registered(s.p).docs, undefined); assert.equal(registered(late.p).docs, undefined); }); test("a node's own registration overrides the one for every node; other nodes' keys leave this one alone", async () => { const a = node("laptop"), s = node("server"); const b = bus(); const va = b.join(a); const vs = b.join(s); await registerServer(a.p, { name: "x", entry: { type: "http", url: "https://all" }, nodes: "all" }, b.state, va, writer(a.written), async () => []); await registerServer(a.p, { name: "x", entry: { type: "http", url: "https://laptop" } }, b.state, va, writer(a.written), async () => []); assert.equal(registered(a.p).x.url, "https://laptop"); assert.equal(registered(s.p).x.url, "https://all"); await registerServer(a.p, { name: "only", entry: { type: "http", url: "https://o" }, nodes: ["server"] }, b.state, va, writer(a.written), async () => []); assert.equal(registered(a.p).only, undefined); assert.equal(registered(s.p).only.url, "https://o"); // Unregistering here leaves the every-node one applying, and says so. const r = await registerServer(a.p, { name: "x" }, b.state, va, writer(a.written), async () => []); assert.match(String(r.still), /still applies here/); assert.equal(registered(a.p).x.url, "https://all"); assert.equal(vs.effective().x.url, "https://all"); }); test("a bad entry is refused before anything is put; a repeated change changes nothing", async () => { const n = node(); const b = bus(); const view = b.join(n); const r = await registerServer(n.p, { name: "mesh", entry: { type: "http", url: "https://x" } }, b.state, view, writer(n.written), async () => []); assert.equal(r.registered, false); assert.equal(b.kept.size, 0); assert.equal(onServerChange(view, { key: "all.a", op: "put", value: { type: "http", url: "https://a" } }, n.p, writer(n.written)), "registered all.a"); assert.equal(onServerChange(view, { key: "all.a", op: "put", value: { type: "http", url: "https://a" } }, n.p, writer(n.written)), null); assert.equal(onServerChange(view, { key: "server.b", op: "put", value: { type: "http", url: "https://b" } }, n.p, writer(n.written)), null); });