import { join } from 'node:path'; import { homedir } from 'node:os'; const PROXY_PORT = Number(process.env.ANTHROPIC_PROXY_PORT ?? '5051'); const ANTHROPIC_API_BASE = 'https://api.anthropic.com'; const CREDENTIALS_PATH = join(homedir(), '.claude', '.credentials.json'); // Generate a random proxy secret at startup — shared with Claude Code spawns // Prefix must match a real Anthropic API key format (sk-ant-api03-*) so Claude Code accepts it export const proxySecret = `sk-ant-api03-${crypto.randomUUID()}`; type CredentialsFile = { claudeAiOauth?: { accessToken?: string; }; }; async function readOAuthToken(): Promise { try { const file = Bun.file(CREDENTIALS_PATH); if (!(await file.exists())) return null; const data = (await file.json()) as CredentialsFile; return data.claudeAiOauth?.accessToken?.trim() || null; } catch { return null; } } export function startAnthropicProxy() { Bun.serve({ port: PROXY_PORT, hostname: '127.0.0.1', idleTimeout: 0, async fetch(req) { const url = new URL(req.url); // Validate proxy secret const incomingKey = req.headers.get('x-api-key'); if (incomingKey !== proxySecret) { return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401, headers: { 'Content-Type': 'application/json' }, }); } // Read fresh OAuth token const token = await readOAuthToken(); if (!token) { return new Response(JSON.stringify({ error: 'No OAuth token available' }), { status: 502, headers: { 'Content-Type': 'application/json' }, }); } // Build upstream URL const upstream = `${ANTHROPIC_API_BASE}${url.pathname}${url.search}`; // Clone headers, replace proxy secret with real OAuth token const headers = new Headers(req.headers); headers.set('x-api-key', token); headers.delete('host'); // Read request body fully before forwarding — avoids stream-in-stream issues const body = req.body ? await req.arrayBuffer() : null; // Forward request const upstreamRes = await fetch(upstream, { method: req.method, headers, body, }); // Build clean response headers const resHeaders = new Headers(); for (const [key, value] of upstreamRes.headers) { const lower = key.toLowerCase(); // Skip hop-by-hop and encoding headers that may have been consumed by fetch if (lower === 'content-encoding' || lower === 'content-length' || lower === 'transfer-encoding') continue; resHeaders.set(key, value); } return new Response(upstreamRes.body, { status: upstreamRes.status, statusText: upstreamRes.statusText, headers: resHeaders, }); }, }); console.log(`[anthropic-proxy] listening on 127.0.0.1:${PROXY_PORT}`); }