// The only file that talks to Anthropic — the vendor half of the refreshable-grant adapter // (novox/hq ADR 0050). Isolated exactly as cloudflare-dns isolates its registrar call, so the // vendor is swappable and the one place a token endpoint is reached is auditable. // // Two endpoints, and they are different hosts (port-map "don't-map" #1): the TOKEN host mints a new // access token from the refresh token; the USAGE host reports utilisation against an access token. /** The token endpoint, overridable so the lab can point the whole flow at a stub without a vendor. */ export function tokenEndpoint(env = process.env): string { return env.MESH_ANTHROPIC_TOKEN_ENDPOINT ?? "https://platform.claude.com/v1/oauth/token"; } /** The usage endpoint, likewise overridable for the lab. */ export function usageEndpoint(env = process.env): string { return env.MESH_ANTHROPIC_USAGE_ENDPOINT ?? "https://api.anthropic.com/api/oauth/usage"; } // The OAuth client id is a hard-won constant, ported byte-exact from the mature implementation: a // metadata URL in its place yields 400. It is not a secret (it identifies the public Claude Code // client), so it lives in code. const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"; /** The vendor's token response, snake_case as the wire has it. */ export interface RefreshedGrant { readonly access_token?: string; readonly refresh_token?: string; readonly expires_in?: number; readonly refresh_token_expires_in?: number; readonly scopes?: string[]; readonly subscription_type?: string; } /** * Exchange a refresh token for a fresh grant. Returns null on any non-ok response, surfacing the * OAuth error body (invalid_grant/invalid_client/…) — the difference between "the token is dead" and * "the endpoint was unreachable", which a bare status hides. */ export async function refreshGrant( refreshToken: string, env = process.env, ): Promise { const resp = await fetch(tokenEndpoint(env), { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: CLIENT_ID, }), }); if (!resp.ok) { const body = await resp.text().catch(() => ""); console.error( `[anthropic-manager] token refresh failed: ${resp.status} ${resp.statusText} — ${body.slice(0, 400)}`, ); return null; } return (await resp.json()) as RefreshedGrant; } /** The vendor's usage response — utilisation percentages against several windows. */ export interface UsageLimits { readonly five_hour?: { utilization: number; resets_at?: string }; readonly seven_day?: { utilization: number; resets_at?: string }; readonly seven_day_sonnet?: { utilization: number; resets_at?: string }; readonly seven_day_opus?: { utilization: number; resets_at?: string }; readonly extra_usage?: { utilization: number }; readonly [key: string]: unknown; } /** * Read utilisation for an access token. Never refreshes here (a 401 is just reported): a second * refresh source racing the first is the fault the mature implementation warns against. */ export async function readUsage(accessToken: string, env = process.env): Promise { const resp = await fetch(usageEndpoint(env), { headers: { authorization: `Bearer ${accessToken}` }, }); if (!resp.ok) { const body = await resp.text().catch(() => ""); console.error(`[anthropic-manager] usage endpoint returned ${resp.status}: ${body.slice(0, 200)}`); return null; } return (await resp.json()) as UsageLimits; } /** The licence-grain reading ADR 0054 fixes, flattened from the vendor's windows. */ export interface UsageReading { readonly sessionPct: number | null; readonly sessionResetsAt: string | null; readonly weeklyPct: number | null; readonly sonnetPct: number | null; readonly extraPct: number | null; readonly raw: UsageLimits; } export function flattenUsage(u: UsageLimits): UsageReading { return { sessionPct: u.five_hour?.utilization ?? null, sessionResetsAt: u.five_hour?.resets_at ?? null, weeklyPct: u.seven_day?.utilization ?? null, sonnetPct: u.seven_day_sonnet?.utilization ?? null, extraPct: u.extra_usage?.utilization ?? null, raw: u, }; } /** The access-token-only grant a holder is delivered — the port-map credential-file shape's fields. */ export interface AccessGrant { readonly accessToken: string; readonly expiresAt: number | null; readonly refreshTokenExpiresAt: number | null; readonly scopes: string[] | null; readonly subscriptionType: string | null; } /** * Turn a vendor refresh into what the manager submits: the access-token-only grant for holders, and * the rotated refresh token if the vendor sent one. Never clobbers a good grant from an empty * response — no access_token means the caller keeps what it had. */ export function grantFromRefresh(r: RefreshedGrant, nowMs: number): { access: AccessGrant; rotatedRefresh: string | null } | null { if (!r.access_token) return null; return { access: { accessToken: r.access_token, expiresAt: typeof r.expires_in === "number" ? nowMs + r.expires_in * 1000 : null, refreshTokenExpiresAt: typeof r.refresh_token_expires_in === "number" ? nowMs + r.refresh_token_expires_in * 1000 : null, scopes: r.scopes ?? null, subscriptionType: r.subscription_type ?? null, }, rotatedRefresh: r.refresh_token ?? null, }; }