From 7631c6288e2ab1705d1900bb052e03489bc53972 Mon Sep 17 00:00:00 2001 From: paoloanzn Date: Wed, 1 Apr 2026 15:27:09 +0200 Subject: [PATCH] feat: compute and inject cch hash in fetch wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The buildFetch wrapper now intercepts requests to /v1/messages that contain the cch=00000 placeholder, computes xxHash64 over the serialized body, and replaces the placeholder with the 5-char hex digest before sending. This replicates the request signing that was previously done in Bun's native HTTP stack. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/services/api/client.ts | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/services/api/client.ts b/src/services/api/client.ts index d0720df..bf4b262 100644 --- a/src/services/api/client.ts +++ b/src/services/api/client.ts @@ -1,5 +1,10 @@ import Anthropic, { type ClientOptions } from '@anthropic-ai/sdk' import { randomUUID } from 'crypto' +import { + computeCch, + hasCchPlaceholder, + replaceCchPlaceholder, +} from 'src/utils/cch.js' import type { GoogleAuth } from 'google-auth-library' import { checkAndRefreshOAuthTokenIfNeeded, @@ -383,15 +388,14 @@ function buildFetch( // and unknown headers risk rejection by strict proxies (inc-4029 class). const injectClientRequestId = getAPIProvider() === 'firstParty' && isFirstPartyAnthropicBaseUrl() - return (input, init) => { + return async (input, init) => { // eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins const headers = new Headers(init?.headers) - // Generate a client-side request ID so timeouts (which return no server - // request ID) can still be correlated with server logs by the API team. - // Callers that want to track the ID themselves can pre-set the header. if (injectClientRequestId && !headers.has(CLIENT_REQUEST_ID_HEADER)) { headers.set(CLIENT_REQUEST_ID_HEADER, randomUUID()) } + + let body = init?.body try { // eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins const url = input instanceof Request ? input.url : String(input) @@ -399,9 +403,20 @@ function buildFetch( logForDebugging( `[API REQUEST] ${new URL(url).pathname}${id ? ` ${CLIENT_REQUEST_ID_HEADER}=${id}` : ''} source=${source ?? 'unknown'}`, ) + + if ( + url.includes('/v1/messages') && + headers.has('anthropic-version') && + typeof body === 'string' && + hasCchPlaceholder(body) + ) { + const cch = await computeCch(body) + body = replaceCchPlaceholder(body, cch) + logForDebugging(`[CCH] signed request cch=${cch}`) + } } catch { // never let logging crash the fetch } - return inner(input, { ...init, headers }) + return inner(input, { ...init, headers, body }) } }