import { timingSafeEqual } from 'node:crypto'; function authorized(request, token) { const supplied = new TextEncoder().encode(request.headers.get('authorization') || ''); const expected = new TextEncoder().encode(`Bearer ${token}`); return supplied.byteLength === expected.byteLength && timingSafeEqual(supplied, expected); } const reply = (data, status) => Response.json(data, { status, headers: { 'cache-control': 'no-store' } }); export default { async fetch(request, env) { if (new URL(request.url).pathname !== '/api/message') return reply({ error: 'Not found' }, 404); if (request.method !== 'POST') return new Response(null, { status: 405, headers: { allow: 'POST' } }); if (!env.ANTHROPIC_API_KEY || !env.DEMO_ACCESS_TOKEN || !env.ANTHROPIC_MODEL) return reply({ error: 'Configure Worker secrets and model first' }, 503); // This is a private, shell-client demo; never embed DEMO_ACCESS_TOKEN in frontend code. if (!authorized(request, env.DEMO_ACCESS_TOKEN)) return reply({ error: 'Unauthorized' }, 401); if (!request.headers.get('content-type')?.includes('application/json')) return reply({ error: 'Use application/json' }, 415); let size = 0; const reader = request.body?.getReader(); if (!reader) return reply({ error: 'Missing body' }, 400); const chunks = []; try { while (true) { const { done, value } = await reader.read(); if (done) break; size += value.byteLength; if (size > 8192) { await reader.cancel(); return reply({ error: 'Body too large' }, 413); } chunks.push(value); } } catch { return reply({ error: 'Could not read request body' }, 400); } finally { reader.releaseLock(); } let input; try { const bytes = new Uint8Array(size); let offset = 0; for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; } input = JSON.parse(new TextDecoder().decode(bytes)); } catch { return reply({ error: 'Invalid JSON' }, 400); } if (typeof input?.prompt !== 'string' || !input.prompt.trim() || input.prompt.length > 2000) return reply({ error: 'prompt must contain 1-2000 characters' }, 400); const headers = { 'content-type': 'application/json', 'x-api-key': env.ANTHROPIC_API_KEY, 'anthropic-version': '2023-06-01' }; if (env.ANTHROPIC_WORKSPACE_ID) headers['anthropic-workspace-id'] = env.ANTHROPIC_WORKSPACE_ID; try { const upstream = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers, signal: AbortSignal.timeout(30000), body: JSON.stringify({ model: env.ANTHROPIC_MODEL, max_tokens: 512, messages: [{ role: 'user', content: input.prompt }] }) }); const requestId = upstream.headers.get('request-id'); if (!upstream.ok) { await upstream.body?.cancel(); return reply({ error: 'Upstream API error', upstream_status: upstream.status, request_id: requestId }, 502); } return new Response(upstream.body, { status: 200, headers: { 'content-type': 'application/json', 'cache-control': 'no-store', ...(requestId ? { 'x-request-id': requestId } : {}) } }); } catch { return reply({ error: 'Upstream connection failed or timed out' }, 504); } } };