Phase C of vendor-agnostic model-access (ADR 0050/0054). Two TypeScript runtime modules: - anthropic-manager: the refresh token is sealed at rest to the manager node's own key (atrest.ts, envelope encryption over X25519) and opened ONLY on the manager node. adopt seals the first envelope; refresh opens it, calls the Anthropic OAuth token endpoint, re-seals a rotated refresh token, and hands the control plane only the access token plus the opaque envelope. Also polls licence-grain usage (ADR 0054). - anthropic-consumer: writes the delivered access token to ~/.claude/.credentials.json, access-token-only, atomically (the refresh token is never delivered); reports session-grain usage from the CLI transcripts; a fail-closed identity guard (expected-uuid plumbing is a flagged TODO). Both run as scheduled containers (ADR 0053). Pure logic covered by node --test fixtures (at-rest round-trip, credential strip, transcript sum, refresh merge). Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
33 lines
1.6 KiB
TypeScript
33 lines
1.6 KiB
TypeScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
import { applyGrant, deliver } from "../credentials.ts";
|
|
|
|
test("applyGrant strips the refresh token a full grant on disk left behind", () => {
|
|
const local = { claudeAiOauth: { accessToken: "at-old", refreshToken: "rt-must-not-survive" } };
|
|
const next = applyGrant(local, { accessToken: "at-new", expiresAt: 123 });
|
|
assert.equal(next.claudeAiOauth!.accessToken, "at-new");
|
|
assert.equal(next.claudeAiOauth!.expiresAt, 123);
|
|
assert.ok(!("refreshToken" in next.claudeAiOauth!), "a node held onto a refresh token");
|
|
});
|
|
|
|
test("deliver writes the port-map shape, access-token-only, and never a refresh token", () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "anthropic-consumer-"));
|
|
const path = join(dir, ".credentials.json");
|
|
// A prior interactive login left a full grant on disk.
|
|
writeFileSync(path, JSON.stringify({ claudeAiOauth: { accessToken: "at-old", refreshToken: "rt-login" } }));
|
|
|
|
deliver(path, { accessToken: "at-delivered", expiresAt: 999, subscriptionType: "max" });
|
|
|
|
const raw = readFileSync(path, "utf8");
|
|
const creds = JSON.parse(raw);
|
|
assert.equal(creds.claudeAiOauth.accessToken, "at-delivered");
|
|
assert.equal(creds.claudeAiOauth.expiresAt, 999);
|
|
assert.equal(creds.claudeAiOauth.subscriptionType, "max");
|
|
assert.doesNotMatch(raw, /rt-login/, "the refresh token is still on disk");
|
|
assert.ok(!("refreshToken" in creds.claudeAiOauth));
|
|
});
|