From 88c4f0eda69ea7956448f06ae6f4fe460dcf5ea5 Mon Sep 17 00:00:00 2001 From: arkptz Date: Tue, 14 Apr 2026 15:06:25 +0300 Subject: [PATCH 1/7] feat: add pure TypeScript xxHash64 implementation --- src/xxhash64.test.ts | 69 ++++++++++++++++++++++ src/xxhash64.ts | 136 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 src/xxhash64.test.ts create mode 100644 src/xxhash64.ts diff --git a/src/xxhash64.test.ts b/src/xxhash64.test.ts new file mode 100644 index 0000000..0f5bfb1 --- /dev/null +++ b/src/xxhash64.test.ts @@ -0,0 +1,69 @@ +import assert from "node:assert/strict" +import { describe, it } from "node:test" +import { xxhash64, computeCchHash, CCH_SEED } from "./xxhash64.ts" + +describe("xxhash64", () => { + it("returns known spec vector for empty input with seed 0", () => { + const result = xxhash64(new Uint8Array(0), 0n) + assert.equal(result, 0xef46db3751d8e999n) + }) + + it("returns a bigint for short input (<32 bytes)", () => { + const input = new TextEncoder().encode("hello") + const result = xxhash64(input, 0n) + assert.equal(typeof result, "bigint") + assert.ok(result >= 0n, "hash should be non-negative") + assert.ok(result <= 0xffffffffffffffffn, "hash should fit in 64 bits") + }) + + it("exercises 4-lane accumulator for long input (>32 bytes)", () => { + // 120 chars to ensure we hit the 32-byte stripe loop multiple times + const longStr = "The quick brown fox jumps over the lazy dog. " + + "Pack my box with five dozen liquor jugs. Sphinx of black quartz, judge my vow!" + const input = new TextEncoder().encode(longStr) + assert.ok(input.length > 32, "input must exceed 32 bytes") + const result = xxhash64(input, 0n) + assert.equal(typeof result, "bigint") + assert.ok(result >= 0n) + assert.ok(result <= 0xffffffffffffffffn) + }) + + it("produces different hashes for different seeds", () => { + const input = new TextEncoder().encode("test") + const h1 = xxhash64(input, 0n) + const h2 = xxhash64(input, 1n) + assert.notEqual(h1, h2, "different seeds should produce different hashes") + }) +}) + +describe("computeCchHash", () => { + it("returns a 5-char lowercase hex string", () => { + const body = new TextEncoder().encode("{\"prompt\":\"hello\",\"cch\":\"00000\"}") + const result = computeCchHash(body) + assert.match(result, /^[0-9a-f]{5}$/, "must be 5-char hex") + }) + + it("is deterministic (same input produces same output)", () => { + const body = new TextEncoder().encode("{\"prompt\":\"hello\",\"cch\":\"00000\"}") + const r1 = computeCchHash(body) + const r2 = computeCchHash(body) + assert.equal(r1, r2, "same input must produce same hash") + }) + + it("uses CCH_SEED consistently", () => { + const body = new TextEncoder().encode("{\"model\":\"claude\",\"cch\":\"00000\"}") + const fromComputeCch = computeCchHash(body) + // Manually compute using xxhash64 with CCH_SEED + const hash = xxhash64(body, CCH_SEED) + const expected = (hash & 0xfffffn).toString(16).padStart(5, "0") + assert.equal(fromComputeCch, expected, "computeCchHash must use CCH_SEED internally") + }) + + it("produces different hashes for different inputs", () => { + const body1 = new TextEncoder().encode("{\"a\":1,\"cch\":\"00000\"}") + const body2 = new TextEncoder().encode("{\"b\":2,\"cch\":\"00000\"}") + const r1 = computeCchHash(body1) + const r2 = computeCchHash(body2) + assert.notEqual(r1, r2, "different inputs should produce different hashes") + }) +}) diff --git a/src/xxhash64.ts b/src/xxhash64.ts new file mode 100644 index 0000000..539907c --- /dev/null +++ b/src/xxhash64.ts @@ -0,0 +1,136 @@ +/** + * Pure TypeScript xxHash64 implementation using BigInt. + * Used to compute the cch attestation hash for Claude Code billing headers. + * + * Reference: https://github.com/Cyan4973/xxHash/blob/dev/doc/xxhash_spec.md + */ + +const PRIME1 = 0x9e3779b185ebca87n +const PRIME2 = 0xc2b2ae3d27d4eb4fn +const PRIME3 = 0x165667b19e3779f9n +const PRIME4 = 0x85ebca77c2b2ae63n +const PRIME5 = 0x27d4eb2f165667c5n +const M = 0xffffffffffffffffn // 64-bit mask + +function mul(a: bigint, b: bigint): bigint { + return (a * b) & M +} + +function add(a: bigint, b: bigint): bigint { + return (a + b) & M +} + +function rotl(v: bigint, n: number): bigint { + return ((v << BigInt(n)) | (v >> BigInt(64 - n))) & M +} + +function u64le(buf: Uint8Array, i: number): bigint { + return ( + BigInt(buf[i]) | + (BigInt(buf[i + 1]) << 8n) | + (BigInt(buf[i + 2]) << 16n) | + (BigInt(buf[i + 3]) << 24n) | + (BigInt(buf[i + 4]) << 32n) | + (BigInt(buf[i + 5]) << 40n) | + (BigInt(buf[i + 6]) << 48n) | + (BigInt(buf[i + 7]) << 56n) + ) +} + +function u32le(buf: Uint8Array, i: number): bigint { + return ( + BigInt(buf[i]) | + (BigInt(buf[i + 1]) << 8n) | + (BigInt(buf[i + 2]) << 16n) | + (BigInt(buf[i + 3]) << 24n) + ) +} + +function round(acc: bigint, lane: bigint): bigint { + acc = add(acc, mul(lane, PRIME2)) + acc = rotl(acc, 31) + return mul(acc, PRIME1) +} + +function mergeAccumulator(h: bigint, acc: bigint): bigint { + const val = round(0n, acc) + h = (h ^ val) & M + return add(mul(h, PRIME1), PRIME4) +} + +function avalanche(h: bigint): bigint { + h = mul(h ^ (h >> 33n), PRIME2) + h = mul(h ^ (h >> 29n), PRIME3) + return (h ^ (h >> 32n)) & M +} + +export function xxhash64(input: Uint8Array, seed: bigint): bigint { + const len = input.length + let h: bigint + let off = 0 + + if (len >= 32) { + let v1 = add(add(seed, PRIME1), PRIME2) + let v2 = add(seed, PRIME2) + let v3 = seed & M + let v4 = (seed - PRIME1) & M + + const limit = len - 32 + while (off <= limit) { + v1 = round(v1, u64le(input, off)) + v2 = round(v2, u64le(input, off + 8)) + v3 = round(v3, u64le(input, off + 16)) + v4 = round(v4, u64le(input, off + 24)) + off += 32 + } + + h = add( + add(rotl(v1, 1), rotl(v2, 7)), + add(rotl(v3, 12), rotl(v4, 18)), + ) + h = mergeAccumulator(h, v1) + h = mergeAccumulator(h, v2) + h = mergeAccumulator(h, v3) + h = mergeAccumulator(h, v4) + } else { + h = add(seed, PRIME5) + } + + h = add(h, BigInt(len)) + + // Remaining 8-byte blocks + while (off + 8 <= len) { + const k = round(0n, u64le(input, off)) + h = add(mul(rotl((h ^ k) & M, 27), PRIME1), PRIME4) + off += 8 + } + + // Remaining 4-byte block + if (off + 4 <= len) { + h = add(mul(rotl((h ^ mul(u32le(input, off), PRIME1)) & M, 23), PRIME2), PRIME3) + off += 4 + } + + // Remaining bytes + while (off < len) { + h = mul(rotl((h ^ mul(BigInt(input[off]), PRIME5)) & M, 11), PRIME1) + off++ + } + + return avalanche(h) +} + +/** The seed baked into Claude Code's custom Bun binary (v2.1.37) */ +export const CCH_SEED = 0x6e52736ac806831en + +/** + * Compute the cch attestation hash for a request body. + * The body must contain the placeholder "cch=00000". + * + * @returns 5-character zero-padded lowercase hex string + */ +export function computeCchHash(bodyBytes: Uint8Array): string { + const hash = xxhash64(bodyBytes, CCH_SEED) + const truncated = hash & 0xfffffn + return truncated.toString(16).padStart(5, "0") +} From 3dd09fbfefe321e4b9a26379959ed0e96be2b49a Mon Sep 17 00:00:00 2001 From: arkptz Date: Tue, 14 Apr 2026 15:10:54 +0300 Subject: [PATCH 2/7] refactor(signing): emit cch placeholder for xxHash64 body hashing --- src/index.ts | 1 - src/signing.test.ts | 23 ++++------------------- src/signing.ts | 18 +++++++----------- 3 files changed, 11 insertions(+), 31 deletions(-) diff --git a/src/index.ts b/src/index.ts index 3edda7f..35f80bf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,7 +48,6 @@ export { export { isEnable1mContext, type PluginSettings } from "./plugin-config.ts" export { buildBillingHeaderValue, - computeCch, computeVersionSuffix, extractFirstUserMessageText, } from "./signing.ts" diff --git a/src/signing.test.ts b/src/signing.test.ts index bf545c1..7f5f822 100644 --- a/src/signing.test.ts +++ b/src/signing.test.ts @@ -2,7 +2,6 @@ import assert from "node:assert/strict" import { describe, it } from "node:test" import { extractFirstUserMessageText, - computeCch, computeVersionSuffix, buildBillingHeaderValue, } from "./signing.ts" @@ -65,20 +64,6 @@ describe("signing", () => { }) }) - describe("computeCch", () => { - it("matches test vector: 'hey' → fa690", () => { - assert.equal(computeCch("hey"), "fa690") - }) - - it("matches test vector: empty string → e3b0c", () => { - assert.equal(computeCch(""), "e3b0c") - }) - - it("matches test vector: long message", () => { - assert.equal(computeCch("Hello, how are you doing today?"), "852db") - }) - }) - describe("computeVersionSuffix", () => { it("matches test vector: 'hey' + v2.1.37 → 0d9", () => { assert.equal(computeVersionSuffix("hey", "2.1.37"), "0d9") @@ -121,7 +106,7 @@ describe("signing", () => { ) assert.equal( result, - "x-anthropic-billing-header: cc_version=2.1.90.b39; cc_entrypoint=cli; cch=fa690;", + "x-anthropic-billing-header: cc_version=2.1.90.b39; cc_entrypoint=cli; cch=00000;", ) }) @@ -141,13 +126,13 @@ describe("signing", () => { ) assert.equal( result, - "x-anthropic-billing-header: cc_version=2.1.90.b39; cc_entrypoint=cli; cch=fa690;", + "x-anthropic-billing-header: cc_version=2.1.90.b39; cc_entrypoint=cli; cch=00000;", ) }) - it("handles missing user message (hashes empty string)", () => { + it("handles missing user message (uses placeholder cch)", () => { const result = buildBillingHeaderValue([], "2.1.90", "cli") - assert.ok(result.includes("cch=e3b0c")) + assert.ok(result.includes("cch=00000")) }) it("uses provided entrypoint", () => { diff --git a/src/signing.ts b/src/signing.ts index 0870522..851efeb 100644 --- a/src/signing.ts +++ b/src/signing.ts @@ -26,13 +26,6 @@ export function extractFirstUserMessageText(messages: Message[]): string { return "" } -/** - * Compute cch: first 5 hex characters of SHA-256(messageText). - */ -export function computeCch(messageText: string): string { - return createHash("sha256").update(messageText).digest("hex").slice(0, 5) -} - /** * Compute the 3-char version suffix. * Samples characters at indices 4, 7, 20 from the message text (padding @@ -51,8 +44,12 @@ export function computeVersionSuffix( } /** - * Build the complete billing header string for insertion into system[0]. - * Format: x-anthropic-billing-header: cc_version=V.S; cc_entrypoint=E; cch=H; + * Build the billing header string with cch=00000 placeholder. + * Format matches Claude Code exactly: + * x-anthropic-billing-header: cc_version=V.S; cc_entrypoint=E; cch=00000; + * + * The placeholder is later replaced with the real xxHash64-based cch + * by computeCchHash() after the full body is serialized. */ export function buildBillingHeaderValue( messages: Message[], @@ -61,11 +58,10 @@ export function buildBillingHeaderValue( ): string { const text = extractFirstUserMessageText(messages) const suffix = computeVersionSuffix(text, version) - const cch = computeCch(text) return ( `x-anthropic-billing-header: ` + `cc_version=${version}.${suffix}; ` + `cc_entrypoint=${entrypoint}; ` + - `cch=${cch};` + `cch=00000;` ) } From 1c22d1587c297601a3e8b6b9ad54fb2a8a1c4c46 Mon Sep 17 00:00:00 2001 From: arkptz Date: Tue, 14 Apr 2026 15:16:17 +0300 Subject: [PATCH 3/7] refactor: remove debug dump and bump ccVersion to 2.1.97 --- src/model-config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model-config.ts b/src/model-config.ts index 1827ce8..5e67b87 100644 --- a/src/model-config.ts +++ b/src/model-config.ts @@ -12,7 +12,7 @@ export interface ModelConfig { } export const config: ModelConfig = { - ccVersion: "2.1.90", + ccVersion: "2.1.97", baseBetas: [ "claude-code-20250219", "oauth-2025-04-20", From 0e68dfee3e3a934223f65daa78098385e699ea0e Mon Sep 17 00:00:00 2001 From: arkptz Date: Tue, 14 Apr 2026 15:16:52 +0300 Subject: [PATCH 4/7] chore: add cch extraction and prompt test scripts --- package.json | 3 +- scripts/extract-cch.ts | 179 +++++++++++++++++++++++++++++++++++++++++ scripts/test-prompt.ts | 107 ++++++++++++++++++++++++ 3 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 scripts/extract-cch.ts create mode 100644 scripts/test-prompt.ts diff --git a/package.json b/package.json index 83a21a7..f7663d7 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,8 @@ "lint": "oxlint && oxfmt --check .", "lint:fix": "oxlint --fix . && oxfmt --write .", "format": "oxfmt --write .", - "prepublishOnly": "pnpm run build && pnpm test" + "prepublishOnly": "pnpm run build && pnpm test", + "test:prompt": "pnpm run build && node --experimental-strip-types scripts/test-prompt.ts" }, "devDependencies": { "@opencode-ai/plugin": "latest", diff --git a/scripts/extract-cch.ts b/scripts/extract-cch.ts new file mode 100644 index 0000000..727bb97 --- /dev/null +++ b/scripts/extract-cch.ts @@ -0,0 +1,179 @@ +/** + * Extract the cch value from a real Claude CLI request and compare + * with our xxHash64 implementation to verify seed correctness. + * + * Usage: pnpm run build && node --experimental-strip-types scripts/extract-cch.ts + */ +import { createServer, type IncomingMessage, type ServerResponse } from "node:http" +import { request as httpsRequest } from "node:https" +import { spawn } from "node:child_process" +import { writeFileSync } from "node:fs" +import { computeCchHash } from "../dist/xxhash64.js" + +const PORT = 18899 +const TIMEOUT_MS = 30_000 + +console.log("Starting intercept proxy on port", PORT) +console.log("Will capture one request from Claude CLI and compare cch...\n") + +const server = createServer((req: IncomingMessage, res: ServerResponse) => { + // Handle health checks — respond 200 and wait for the real request + if (req.method === "HEAD" || !req.url?.includes("/v1/messages")) { + console.log(`>>> ${req.method} ${req.url} (skipping, not /v1/messages)`) + res.writeHead(200) + res.end() + return + } + + const chunks: Buffer[] = [] + req.on("data", (chunk: Buffer) => chunks.push(chunk)) + req.on("end", () => { + console.log(`\n>>> ${req.method} ${req.url}`) + const bodyStr = Buffer.concat(chunks).toString() + + // Extract the cch from the billing header in the body + const cchMatch = bodyStr.match(/cch=([0-9a-f]{5})/) + const realCch = cchMatch ? cchMatch[1] : null + + console.log("=== CAPTURED REQUEST ===") + console.log("Body length:", bodyStr.length) + console.log("Real cch from Claude CLI:", realCch) + + // Now compute what our implementation produces + // Replace the real cch with 00000 placeholder to simulate what Bun hashes + const bodyWithPlaceholder = bodyStr.replace( + /cch=[0-9a-f]{5}/, + "cch=00000", + ) + const encoder = new TextEncoder() + const fullBodyHash = computeCchHash(encoder.encode(bodyWithPlaceholder)) + console.log("Our xxHash64 (full body):", fullBodyHash) + console.log("Match (full body):", fullBodyHash === realCch ? "YES ✓" : "NO ✗") + + // Also try hashing with system stripped to billing-only + try { + const parsed = JSON.parse(bodyWithPlaceholder) as { + system?: Array<{ type?: string; text?: string }> + } + if (Array.isArray(parsed.system)) { + const billingOnly = parsed.system.filter( + (e) => + typeof e.text === "string" && + e.text.startsWith("x-anthropic-billing-header"), + ) + const strippedBody = { ...parsed, system: billingOnly } + const strippedHash = computeCchHash( + encoder.encode(JSON.stringify(strippedBody)), + ) + console.log("Our xxHash64 (billing-only system):", strippedHash) + console.log( + "Match (billing-only):", + strippedHash === realCch ? "YES ✓" : "NO ✗", + ) + + // Also try with no system at all + const noSystem = { ...parsed } + delete (noSystem as Record).system + const noSystemHash = computeCchHash( + encoder.encode(JSON.stringify(noSystem)), + ) + console.log("Our xxHash64 (no system):", noSystemHash) + console.log( + "Match (no system):", + noSystemHash === realCch ? "YES ✓" : "NO ✗", + ) + } + } catch { + console.log("Failed to parse body for stripped test") + } + + // Save body with placeholder for seed brute-forcing + writeFileSync("/tmp/claude-body-placeholder.json", bodyWithPlaceholder, "utf-8") + writeFileSync("/tmp/claude-cch-real.txt", realCch ?? "", "utf-8") + console.log("\nSaved body to /tmp/claude-body-placeholder.json") + console.log("Saved real cch to /tmp/claude-cch-real.txt") + + // Extract version info + const versionMatch = bodyStr.match( + /cc_version=([^;]+)/, + ) + const entrypointMatch = bodyStr.match( + /cc_entrypoint=([^;]+)/, + ) + console.log("\nBilling header details:") + console.log(" cc_version:", versionMatch?.[1]) + console.log(" cc_entrypoint:", entrypointMatch?.[1]) + + // Extract system block count + try { + const p = JSON.parse(bodyStr) as { + system?: unknown[] + } + console.log(" system block count:", p.system?.length) + if (Array.isArray(p.system)) { + for (let i = 0; i < p.system.length; i++) { + const entry = p.system[i] as { text?: string; cache_control?: unknown } + const text = typeof entry.text === "string" ? entry.text.slice(0, 80) : "?" + const cc = entry.cache_control ? JSON.stringify(entry.cache_control) : "none" + console.log(` system[${i}]: cache_control=${cc} text="${text}..."`) + } + } + } catch {} + + // Forward to real API so claude doesn't error + const proxyOpts = { + hostname: "api.anthropic.com", + path: req.url, + method: req.method, + headers: { ...req.headers, host: "api.anthropic.com" }, + } + const proxy = httpsRequest(proxyOpts, (proxyRes) => { + res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers) + proxyRes.pipe(res) + proxyRes.on("end", () => { + server.close() + process.exit(0) + }) + }) + proxy.on("error", () => { + res.writeHead(502) + res.end() + server.close() + process.exit(1) + }) + proxy.write(bodyStr) + proxy.end() + }) +}) + +const timer = setTimeout(() => { + console.log("Timeout - no request captured") + server.close() + process.exit(1) +}, TIMEOUT_MS) + +server.listen(PORT, () => { + const child = spawn("claude", ["-p", "say hi", "--model", "claude-haiku-4-5"], { + env: { + ...process.env, + ANTHROPIC_API_KEY: "", + ANTHROPIC_BASE_URL: `http://localhost:${PORT}`, + TERM: "dumb", + }, + stdio: "ignore", + }) + + child.on("error", (err) => { + console.log("Claude CLI error:", err.message) + clearTimeout(timer) + server.close() + process.exit(1) + }) + + child.on("close", () => { + setTimeout(() => { + clearTimeout(timer) + server.close() + }, 3000) + }) +}) diff --git a/scripts/test-prompt.ts b/scripts/test-prompt.ts new file mode 100644 index 0000000..b709b57 --- /dev/null +++ b/scripts/test-prompt.ts @@ -0,0 +1,107 @@ +import { + getCachedCredentials, + initAccounts, + setActiveAccountSource, +} from "../dist/credentials.js" +import { buildRequestHeaders, fetchWithRetry } from "../dist/index.js" +import { readAllClaudeAccounts } from "../dist/keychain.js" +import { transformBody, transformResponseStream } from "../dist/transforms.js" +import { buildBillingHeaderValue } from "../dist/signing.js" +import { config } from "../dist/model-config.js" + +const API_URL = "https://api.anthropic.com/v1/messages" +const SYSTEM_IDENTITY = + "You are Claude Code, Anthropic's official CLI for Claude." + +const prompt = process.argv[2] +if (!prompt) { + console.error("Usage: pnpm run test:prompt 'your prompt here' [model]") + process.exit(1) +} + +const modelId = process.argv[3] ?? "claude-sonnet-4-6" + +// Init credentials +const accounts = readAllClaudeAccounts() +if (accounts.length === 0) { + console.error("No Claude Code credentials found. Run `claude` to authenticate.") + process.exit(1) +} +initAccounts(accounts) +setActiveAccountSource(accounts[0].source) + +const creds = getCachedCredentials() +if (!creds) { + console.error("Credentials expired. Run `claude` to refresh.") + process.exit(1) +} + +// Build messages +const messages = [{ role: "user", content: prompt }] + +const version = process.env.ANTHROPIC_CLI_VERSION ?? config.ccVersion +const billingHeader = buildBillingHeaderValue(messages, version, "cli") + +const requestBody = { + model: modelId, + max_tokens: 4096, + stream: true, + system: [ + { type: "text", text: billingHeader }, + { type: "text", text: SYSTEM_IDENTITY }, + ], + messages, +} + +const body = transformBody(JSON.stringify(requestBody)) + +const init: RequestInit = { method: "POST", body } +const headers = buildRequestHeaders( + new URL(API_URL), + init, + creds.accessToken, + modelId, +) +headers.set("content-type", "application/json") + +// Send request and stream response +const response = await fetchWithRetry(API_URL, { ...init, body, headers }) + +if (!response.ok) { + const errorBody = await response.text() + console.error(`API error ${response.status}:`, errorBody) + process.exit(1) +} + +const transformed = transformResponseStream(response) +const reader = transformed.body!.getReader() +const decoder = new TextDecoder() + +let fullText = "" + +while (true) { + const { done, value } = await reader.read() + if (done) break + + const chunk = decoder.decode(value, { stream: true }) + // Parse SSE events + for (const line of chunk.split("\n")) { + if (!line.startsWith("data: ")) continue + const data = line.slice(6) + if (data === "[DONE]") continue + try { + const event = JSON.parse(data) as { + type?: string + delta?: { type?: string; text?: string } + } + if (event.type === "content_block_delta" && event.delta?.text) { + process.stdout.write(event.delta.text) + fullText += event.delta.text + } + } catch { + // skip non-JSON lines + } + } +} + +if (fullText) console.log() From 974949840c35a6d053f29a669dce3382978e95f1 Mon Sep 17 00:00:00 2001 From: arkptz Date: Tue, 14 Apr 2026 15:20:29 +0300 Subject: [PATCH 5/7] feat(transforms): compute cch via xxHash64 of full serialized body --- src/transforms.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/transforms.ts b/src/transforms.ts index acf5b00..a147a22 100644 --- a/src/transforms.ts +++ b/src/transforms.ts @@ -1,5 +1,7 @@ import { buildBillingHeaderValue } from "./signing.ts" import { config, getModelOverride } from "./model-config.ts" +import { log } from "./logger.ts" +import { computeCchHash } from "./xxhash64.ts" const TOOL_PREFIX = "mcp_" @@ -239,8 +241,22 @@ export function transformBody( parsed.messages = repairToolPairs(parsed.messages) } - return JSON.stringify(parsed) - } catch { + // --- Compute cch via xxHash64 --- + // Hash the FULL serialized body with cch=00000 placeholder in place, + // then replace the placeholder with the computed hash. + const serialized = JSON.stringify(parsed) + const encoder = new TextEncoder() + const cch = computeCchHash(encoder.encode(serialized)) + const final = serialized.replace("cch=00000", `cch=${cch}`) + + log("transform_cch", { cch, bodyLength: serialized.length }) + + return final + } catch (err) { + log("transform_body_error", { + error: err instanceof Error ? err.message : String(err), + stack: err instanceof Error ? err.stack?.slice(0, 500) : undefined, + }) return body } } From 9cf89cbe4b14d90afd30e1610d6aae1757204085 Mon Sep 17 00:00:00 2001 From: arkptz Date: Tue, 14 Apr 2026 15:26:07 +0300 Subject: [PATCH 6/7] test: update cch-related test assertions for xxHash64 Replace hardcoded cch=fa690 SHA-256 test vectors with structural checks (5-char hex, not placeholder) since cch is now computed via xxHash64. Add xxhash64.ts to SOURCE_FILES in index.test.ts so temp-dir copies include the new module. --- src/index.test.ts | 1 + src/transforms.test.ts | 14 ++++++-------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/index.test.ts b/src/index.test.ts index 84756d2..b36a100 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -127,6 +127,7 @@ const SOURCE_FILES = [ "transforms.ts", "credentials.ts", "logger.ts", + "xxhash64.ts", ] as const async function copySourceFiles(tempDir: string): Promise { diff --git a/src/transforms.test.ts b/src/transforms.test.ts index 763a5b5..d68dc9d 100644 --- a/src/transforms.test.ts +++ b/src/transforms.test.ts @@ -106,10 +106,9 @@ describe("transforms", () => { } assert.ok(parsed.system[0].text.startsWith("x-anthropic-billing-header:")) - assert.ok( - parsed.system[0].text.includes("cch=fa690"), - `Expected cch=fa690 for 'hey', got: ${parsed.system[0].text}`, - ) + const cchMatch = parsed.system[0].text.match(/cch=([0-9a-f]{5})/) + assert.ok(cchMatch, "billing header should contain cch=XXXXX") + assert.notEqual(cchMatch![1], "00000", "cch should not be the placeholder") }) it("transformBody billing header has no cache_control", () => { @@ -234,10 +233,9 @@ describe("transforms", () => { 1, "Should have exactly one billing header", ) - assert.ok( - billingEntries[0].text.includes("cch=fa690"), - `Expected computed cch, got: ${billingEntries[0].text}`, - ) + const cchMatch = billingEntries[0].text.match(/cch=([0-9a-f]{5})/) + assert.ok(cchMatch, "billing header should contain computed cch") + assert.notEqual(cchMatch![1], "00000", "cch should not be the placeholder") // "prompt" should be relocated to user message assert.ok(parsed.messages[0].content.includes("prompt")) }) From 5653a5b055dd3e242b71e75f6562d647ac2ba79e Mon Sep 17 00:00:00 2001 From: arkptz Date: Tue, 14 Apr 2026 15:30:42 +0300 Subject: [PATCH 7/7] style: fix formatting --- scripts/extract-cch.ts | 62 ++++++++++++++++++++++++++---------------- scripts/test-prompt.ts | 4 ++- src/xxhash64.test.ts | 19 ++++++++----- src/xxhash64.ts | 10 +++---- 4 files changed, 58 insertions(+), 37 deletions(-) diff --git a/scripts/extract-cch.ts b/scripts/extract-cch.ts index 727bb97..6d969e3 100644 --- a/scripts/extract-cch.ts +++ b/scripts/extract-cch.ts @@ -4,7 +4,11 @@ * * Usage: pnpm run build && node --experimental-strip-types scripts/extract-cch.ts */ -import { createServer, type IncomingMessage, type ServerResponse } from "node:http" +import { + createServer, + type IncomingMessage, + type ServerResponse, +} from "node:http" import { request as httpsRequest } from "node:https" import { spawn } from "node:child_process" import { writeFileSync } from "node:fs" @@ -41,14 +45,14 @@ const server = createServer((req: IncomingMessage, res: ServerResponse) => { // Now compute what our implementation produces // Replace the real cch with 00000 placeholder to simulate what Bun hashes - const bodyWithPlaceholder = bodyStr.replace( - /cch=[0-9a-f]{5}/, - "cch=00000", - ) + const bodyWithPlaceholder = bodyStr.replace(/cch=[0-9a-f]{5}/, "cch=00000") const encoder = new TextEncoder() const fullBodyHash = computeCchHash(encoder.encode(bodyWithPlaceholder)) console.log("Our xxHash64 (full body):", fullBodyHash) - console.log("Match (full body):", fullBodyHash === realCch ? "YES ✓" : "NO ✗") + console.log( + "Match (full body):", + fullBodyHash === realCch ? "YES ✓" : "NO ✗", + ) // Also try hashing with system stripped to billing-only try { @@ -88,18 +92,18 @@ const server = createServer((req: IncomingMessage, res: ServerResponse) => { } // Save body with placeholder for seed brute-forcing - writeFileSync("/tmp/claude-body-placeholder.json", bodyWithPlaceholder, "utf-8") + writeFileSync( + "/tmp/claude-body-placeholder.json", + bodyWithPlaceholder, + "utf-8", + ) writeFileSync("/tmp/claude-cch-real.txt", realCch ?? "", "utf-8") console.log("\nSaved body to /tmp/claude-body-placeholder.json") console.log("Saved real cch to /tmp/claude-cch-real.txt") // Extract version info - const versionMatch = bodyStr.match( - /cc_version=([^;]+)/, - ) - const entrypointMatch = bodyStr.match( - /cc_entrypoint=([^;]+)/, - ) + const versionMatch = bodyStr.match(/cc_version=([^;]+)/) + const entrypointMatch = bodyStr.match(/cc_entrypoint=([^;]+)/) console.log("\nBilling header details:") console.log(" cc_version:", versionMatch?.[1]) console.log(" cc_entrypoint:", entrypointMatch?.[1]) @@ -112,9 +116,15 @@ const server = createServer((req: IncomingMessage, res: ServerResponse) => { console.log(" system block count:", p.system?.length) if (Array.isArray(p.system)) { for (let i = 0; i < p.system.length; i++) { - const entry = p.system[i] as { text?: string; cache_control?: unknown } - const text = typeof entry.text === "string" ? entry.text.slice(0, 80) : "?" - const cc = entry.cache_control ? JSON.stringify(entry.cache_control) : "none" + const entry = p.system[i] as { + text?: string + cache_control?: unknown + } + const text = + typeof entry.text === "string" ? entry.text.slice(0, 80) : "?" + const cc = entry.cache_control + ? JSON.stringify(entry.cache_control) + : "none" console.log(` system[${i}]: cache_control=${cc} text="${text}..."`) } } @@ -153,15 +163,19 @@ const timer = setTimeout(() => { }, TIMEOUT_MS) server.listen(PORT, () => { - const child = spawn("claude", ["-p", "say hi", "--model", "claude-haiku-4-5"], { - env: { - ...process.env, - ANTHROPIC_API_KEY: "", - ANTHROPIC_BASE_URL: `http://localhost:${PORT}`, - TERM: "dumb", + const child = spawn( + "claude", + ["-p", "say hi", "--model", "claude-haiku-4-5"], + { + env: { + ...process.env, + ANTHROPIC_API_KEY: "", + ANTHROPIC_BASE_URL: `http://localhost:${PORT}`, + TERM: "dumb", + }, + stdio: "ignore", }, - stdio: "ignore", - }) + ) child.on("error", (err) => { console.log("Claude CLI error:", err.message) diff --git a/scripts/test-prompt.ts b/scripts/test-prompt.ts index b709b57..adb15da 100644 --- a/scripts/test-prompt.ts +++ b/scripts/test-prompt.ts @@ -24,7 +24,9 @@ const modelId = process.argv[3] ?? "claude-sonnet-4-6" // Init credentials const accounts = readAllClaudeAccounts() if (accounts.length === 0) { - console.error("No Claude Code credentials found. Run `claude` to authenticate.") + console.error( + "No Claude Code credentials found. Run `claude` to authenticate.", + ) process.exit(1) } initAccounts(accounts) diff --git a/src/xxhash64.test.ts b/src/xxhash64.test.ts index 0f5bfb1..d49cdbc 100644 --- a/src/xxhash64.test.ts +++ b/src/xxhash64.test.ts @@ -18,7 +18,8 @@ describe("xxhash64", () => { it("exercises 4-lane accumulator for long input (>32 bytes)", () => { // 120 chars to ensure we hit the 32-byte stripe loop multiple times - const longStr = "The quick brown fox jumps over the lazy dog. " + + const longStr = + "The quick brown fox jumps over the lazy dog. " + "Pack my box with five dozen liquor jugs. Sphinx of black quartz, judge my vow!" const input = new TextEncoder().encode(longStr) assert.ok(input.length > 32, "input must exceed 32 bytes") @@ -38,30 +39,34 @@ describe("xxhash64", () => { describe("computeCchHash", () => { it("returns a 5-char lowercase hex string", () => { - const body = new TextEncoder().encode("{\"prompt\":\"hello\",\"cch\":\"00000\"}") + const body = new TextEncoder().encode('{"prompt":"hello","cch":"00000"}') const result = computeCchHash(body) assert.match(result, /^[0-9a-f]{5}$/, "must be 5-char hex") }) it("is deterministic (same input produces same output)", () => { - const body = new TextEncoder().encode("{\"prompt\":\"hello\",\"cch\":\"00000\"}") + const body = new TextEncoder().encode('{"prompt":"hello","cch":"00000"}') const r1 = computeCchHash(body) const r2 = computeCchHash(body) assert.equal(r1, r2, "same input must produce same hash") }) it("uses CCH_SEED consistently", () => { - const body = new TextEncoder().encode("{\"model\":\"claude\",\"cch\":\"00000\"}") + const body = new TextEncoder().encode('{"model":"claude","cch":"00000"}') const fromComputeCch = computeCchHash(body) // Manually compute using xxhash64 with CCH_SEED const hash = xxhash64(body, CCH_SEED) const expected = (hash & 0xfffffn).toString(16).padStart(5, "0") - assert.equal(fromComputeCch, expected, "computeCchHash must use CCH_SEED internally") + assert.equal( + fromComputeCch, + expected, + "computeCchHash must use CCH_SEED internally", + ) }) it("produces different hashes for different inputs", () => { - const body1 = new TextEncoder().encode("{\"a\":1,\"cch\":\"00000\"}") - const body2 = new TextEncoder().encode("{\"b\":2,\"cch\":\"00000\"}") + const body1 = new TextEncoder().encode('{"a":1,"cch":"00000"}') + const body2 = new TextEncoder().encode('{"b":2,"cch":"00000"}') const r1 = computeCchHash(body1) const r2 = computeCchHash(body2) assert.notEqual(r1, r2, "different inputs should produce different hashes") diff --git a/src/xxhash64.ts b/src/xxhash64.ts index 539907c..f78c0a8 100644 --- a/src/xxhash64.ts +++ b/src/xxhash64.ts @@ -84,10 +84,7 @@ export function xxhash64(input: Uint8Array, seed: bigint): bigint { off += 32 } - h = add( - add(rotl(v1, 1), rotl(v2, 7)), - add(rotl(v3, 12), rotl(v4, 18)), - ) + h = add(add(rotl(v1, 1), rotl(v2, 7)), add(rotl(v3, 12), rotl(v4, 18))) h = mergeAccumulator(h, v1) h = mergeAccumulator(h, v2) h = mergeAccumulator(h, v3) @@ -107,7 +104,10 @@ export function xxhash64(input: Uint8Array, seed: bigint): bigint { // Remaining 4-byte block if (off + 4 <= len) { - h = add(mul(rotl((h ^ mul(u32le(input, off), PRIME1)) & M, 23), PRIME2), PRIME3) + h = add( + mul(rotl((h ^ mul(u32le(input, off), PRIME1)) & M, 23), PRIME2), + PRIME3, + ) off += 4 }