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
48 lines
2.0 KiB
TypeScript
48 lines
2.0 KiB
TypeScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { generateKeyPairSync } from "node:crypto";
|
|
|
|
import { sealAtRest, openAtRest, type Envelope } from "../atrest.ts";
|
|
|
|
/** A node key pair as the mesh records it: raw 32-byte X25519 keys, standard base64. */
|
|
function nodeKeys(): { pub: string; priv: string } {
|
|
const kp = generateKeyPairSync("x25519");
|
|
const pub = (kp.publicKey.export({ format: "jwk" }) as { x: string }).x;
|
|
const priv = (kp.privateKey.export({ format: "jwk" }) as { d: string }).d;
|
|
// JWK is base64url; the mesh records standard base64 of the same 32 bytes.
|
|
const std = (b64url: string) => Buffer.from(b64url, "base64url").toString("base64");
|
|
return { pub: std(pub), priv: std(priv) };
|
|
}
|
|
|
|
test("the manager seals a refresh token and reads it back with its own key", () => {
|
|
const { pub, priv } = nodeKeys();
|
|
const env = sealAtRest("rt-the-refresh-token", pub);
|
|
assert.equal(env.managerKey, pub);
|
|
// Nothing in the envelope is the refresh token in the clear.
|
|
assert.doesNotMatch(env.token, /rt-the-refresh-token/);
|
|
assert.doesNotMatch(env.wrappedKey, /rt-the-refresh-token/);
|
|
assert.equal(openAtRest(env, pub, priv), "rt-the-refresh-token");
|
|
});
|
|
|
|
test("a node that is not the manager cannot open the envelope", () => {
|
|
const manager = nodeKeys();
|
|
const other = nodeKeys();
|
|
const env = sealAtRest("rt-secret", manager.pub);
|
|
assert.throws(() => openAtRest(env, other.pub, other.priv));
|
|
});
|
|
|
|
test("two seals of the same token look nothing alike", () => {
|
|
const { pub } = nodeKeys();
|
|
const a = sealAtRest("rt-secret", pub);
|
|
const b = sealAtRest("rt-secret", pub);
|
|
assert.notEqual(a.token, b.token);
|
|
assert.notEqual(a.wrappedKey, b.wrappedKey);
|
|
});
|
|
|
|
test("a tampered envelope is refused, not silently mis-opened", () => {
|
|
const { pub, priv } = nodeKeys();
|
|
const env = sealAtRest("rt-secret", pub);
|
|
const flipped: Envelope = { ...env, token: Buffer.from(env.token, "base64").reverse().toString("base64") };
|
|
assert.throws(() => openAtRest(flipped, pub, priv));
|
|
});
|