// functions/api/chat.js // Cloudflare Pages Function for chat assistant. // // ⚠️ PASTE YOUR ANTHROPIC API KEY ON THE LINE BELOW (replace the placeholder text): const ANTHROPIC_API_KEY = "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"; // ⚠️ Repo MUST stay private. Anyone with read access to this file can use your key. const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', }; // Simple in-memory rate limiter const rateLimitStore = new Map(); const RATE_LIMIT_MAX = 30; const RATE_LIMIT_WINDOW_MS = 60 * 60 * 1000; function checkRateLimit(ip) { const now = Date.now(); const record = rateLimitStore.get(ip); if (!record || now - record.start > RATE_LIMIT_WINDOW_MS) { rateLimitStore.set(ip, { count: 1, start: now }); return { allowed: true }; } if (record.count >= RATE_LIMIT_MAX) { const resetIn = Math.ceil((record.start + RATE_LIMIT_WINDOW_MS - now) / 60000); return { allowed: false, resetIn }; } record.count++; return { allowed: true }; } export async function onRequest(context) { const { request } = context; if (request.method === 'OPTIONS') { return new Response(null, { status: 204, headers: corsHeaders }); } if (request.method !== 'POST') { return new Response( JSON.stringify({ error: 'Method not allowed' }), { status: 405, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } const ip = request.headers.get('cf-connecting-ip') || request.headers.get('x-forwarded-for')?.split(',')[0] || 'unknown'; const limit = checkRateLimit(ip); if (!limit.allowed) { return new Response( JSON.stringify({ error: `Whoa, slow down! You've hit the message limit. Try again in ${limit.resetIn} minute(s).`, }), { status: 429, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } let body; try { body = await request.json(); } catch { return new Response( JSON.stringify({ error: 'Invalid JSON' }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } const { messages, system, model, max_tokens } = body; if (!Array.isArray(messages) || messages.length === 0) { return new Response( JSON.stringify({ error: 'Missing messages' }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } const trimmedMessages = messages.slice(-20); if (!ANTHROPIC_API_KEY || ANTHROPIC_API_KEY === "PASTE_YOUR_KEY_HERE") { return new Response( JSON.stringify({ error: 'Server not configured. API key not set in chat.js.' }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } try { const anthropicRes = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': ANTHROPIC_API_KEY, 'anthropic-version': '2023-06-01', }, body: JSON.stringify({ model: model || 'claude-haiku-4-5-20251001', max_tokens: max_tokens || 1024, system: system || 'You are a helpful, friendly AI assistant.', messages: trimmedMessages, }), }); if (!anthropicRes.ok) { const errText = await anthropicRes.text(); console.error('Anthropic error:', anthropicRes.status, errText); return new Response( JSON.stringify({ error: `AI service error (${anthropicRes.status})` }), { status: anthropicRes.status, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } const data = await anthropicRes.json(); return new Response(JSON.stringify(data), { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); } catch (err) { console.error('Function error:', err); return new Response( JSON.stringify({ error: 'Internal server error' }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } }