// The mis-binding guard (novox/hq ADR 0050, port map §identity). Account identity is NOT in the // token or any API — it lives in a sibling CLI state file, `~/.claude.json` → // `oauthAccount.accountUuid`. The guard compares the account the CLI is actually logged in as to the // account the licence was recorded against, and FAILS CLOSED: an absent file or an unrecorded licence // account refuses rather than guesses, because delivering an access token to the wrong account is the // exact fault this exists to catch. // // **Partial first cut, FLAGGED.** Reading the sibling file is implemented; the licence's recorded // account uuid is not yet plumbed from the control plane to the consumer (the bound `model.json` does // not carry it today). So `check` returns `licence-not-adopted` when no expected uuid is supplied, // which is the fail-closed answer, and the wiring of the expected uuid is a TODO below. import { readFileSync } from "node:fs"; export type IdentityVerdict = | { state: "verified"; accountUuid: string } | { state: "no-identity-file" } | { state: "licence-not-adopted" } | { state: "wrong-account"; found: string; expected: string }; interface ClaudeJson { oauthAccount?: { accountUuid?: string; emailAddress?: string; organizationUuid?: string }; } /** Read `oauthAccount.accountUuid` from `~/.claude.json`, or null if the file or field is absent. */ export function readAccountUuid(path: string): string | null { try { const raw = JSON.parse(readFileSync(path, "utf8")) as ClaudeJson; return raw.oauthAccount?.accountUuid ?? null; } catch { return null; } } /** * Compare the CLI's logged-in account to the one the licence was recorded against. Pure over its * inputs so the fail-closed logic is tested without a filesystem. * * TODO(novox/hq ADR 0050, Phase C): plumb `expected` — the licence's recorded account uuid — from the * control plane into the consumer's bound `model.json`, then adopt-on-first-sight or refuse per the * port map's five states. Until then only the two safe verdicts are reachable: verified when an * expected uuid is provided and matches, refuse otherwise. */ export function check(found: string | null, expected: string | null): IdentityVerdict { if (found === null) return { state: "no-identity-file" }; if (!expected) return { state: "licence-not-adopted" }; if (found === expected) return { state: "verified", accountUuid: found }; return { state: "wrong-account", found, expected }; }