Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Fix

- **#112** — Recorded OAuth-host verification against compiled cli.js v2.1.154 (ALIGNMENT Class A); usage-probe and default request model now derive from `models.json` (ADR 0003 SPOT) instead of hardcoded IDs.

### Security

- **#109 P0** — `/health` no longer advertises `PROXY_ANONYMOUS_KEY` to remote callers by default. The `anonymousKey` field is now gated behind a new `PROXY_ADVERTISE_ANON_KEY=1` opt-in env var; localhost callers are always exempt. This prevents any LAN-reachable device from harvesting a working bearer credential from the unauthenticated `/health` endpoint.
Expand Down
10 changes: 8 additions & 2 deletions server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1222,6 +1222,12 @@ function streamStringAsSSE(res, id, model, content) {

let usageCache = { data: null, fetchedAt: 0 };
const USAGE_CACHE_TTL = 5 * 60 * 1000; // 5 min
// ALIGNMENT (Class A — OAuth bearer machinery). Verified against the compiled cli.js
// (claude.exe v2.1.154) on 2026-05-31 via `strings`: both OAUTH_CLIENT_ID and
// OAUTH_TOKEN_URL appear in the binary byte-for-byte; the legacy host
// console.anthropic.com/v1/oauth is absent (0 hits). Re-verify on cli.js major bumps
// using the compiled-binary protocol (strings on the Mach-O/ELF; no live OAuth probe —
// a refresh-token grant would rotate the operator's real credentials). (issue #112)
const OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
const OAUTH_TOKEN_URL = "https://platform.claude.com/v1/oauth/token";

Expand Down Expand Up @@ -1329,7 +1335,7 @@ async function fetchUsageFromApi() {
// Minimal /v1/messages request — we only need the response headers.
// Mirrors Claude Code cli.js vE4: headers anthropic-ratelimit-unified-{5h,7d}-{utilization,reset}.
const body = JSON.stringify({
model: "claude-haiku-4-5-20251001",
model: modelsConfig.aliases.haiku,
max_tokens: 1,
messages: [{ role: "user", content: "." }],
});
Expand Down Expand Up @@ -1670,7 +1676,7 @@ async function handleChatCompletions(req, res) {
try { parsed = JSON.parse(body); } catch { return jsonResponse(res, 400, { error: "Invalid JSON" }); }

const messages = parsed.messages || parsed.input || [{ role: "user", content: parsed.prompt || "" }];
const model = parsed.model || "claude-sonnet-4-6";
const model = parsed.model || modelsConfig.aliases.sonnet;
const stream = parsed.stream;

// Validate model against known models
Expand Down
21 changes: 21 additions & 0 deletions test-features.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1654,6 +1654,27 @@ test("sanitizeError: multiple paths all stripped", () => {
assert.ok(result.includes("[path]"), `expected [path] in: ${result}`);
});

// ── models.json SPOT wiring (issue #112) ────────────────────────────────────
// Asserts that the alias values used by server.mjs (usage probe + default model)
// match the expected IDs. A future alias rename that silently breaks these
// code paths is caught here.
import { readFileSync as spotReadFileSync } from "node:fs";
import { fileURLToPath as spotFileURLToPath } from "node:url";
import { dirname as spotDirname, join as spotJoin } from "node:path";

console.log("\nmodels.json SPOT aliases (issue #112):");

const _spotDir = spotDirname(spotFileURLToPath(import.meta.url));
const _spotModels = JSON.parse(spotReadFileSync(spotJoin(_spotDir, "models.json"), "utf8"));

test("models.json aliases.haiku === 'claude-haiku-4-5-20251001' (usage-probe SPOT)", () => {
assert.equal(_spotModels.aliases.haiku, "claude-haiku-4-5-20251001");
});

test("models.json aliases.sonnet === 'claude-sonnet-4-6' (default-request-model SPOT)", () => {
assert.equal(_spotModels.aliases.sonnet, "claude-sonnet-4-6");
});

// ── Cleanup ──
closeDb();

Expand Down
Loading