const CORS_HEADERS = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', }; const KV_KEY = 'messages'; const MAX_MESSAGES = 500; const MAX_NAME_LENGTH = 30; const MAX_MESSAGE_LENGTH = 500; function jsonResponse(data, status = 200) { return new Response(JSON.stringify(data), { status, headers: { 'Content-Type': 'application/json', ...CORS_HEADERS }, }); } function generateId() { return crypto.randomUUID(); } function formatDate() { return new Date().toISOString(); } function sanitize(str) { return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } async function hashIP(ip) { const encoder = new TextEncoder(); const data = encoder.encode(ip + '_guestbook_salt'); const hash = await crypto.subtle.digest('SHA-256', data); return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('').slice(0, 16); } async function getMessages(env) { const raw = await env.GUESTBOOK.get(KV_KEY); return raw ? JSON.parse(raw) : []; } async function saveMessages(env, messages) { await env.GUESTBOOK.put(KV_KEY, JSON.stringify(messages)); } export default { async fetch(request, env) { if (request.method === 'OPTIONS') { return new Response(null, { headers: CORS_HEADERS }); } const url = new URL(request.url); const path = url.pathname; if (request.method === 'GET' && path === '/messages') { const messages = await getMessages(env); const sanitized = messages.map(m => ({ ...m, likes: Array.isArray(m.likes) ? m.likes.length : (typeof m.likes === 'number' ? m.likes : 0), })); return jsonResponse(sanitized); } if (request.method === 'POST' && path === '/messages') { const body = await request.json().catch(() => null); if (!body || !body.message || !body.message.trim()) { return jsonResponse({ error: 'Message is required' }, 400); } const name = sanitize((body.name || 'Anonymous').trim().slice(0, MAX_NAME_LENGTH)); const message = sanitize(body.message.trim().slice(0, MAX_MESSAGE_LENGTH)); const messages = await getMessages(env); const entry = { id: generateId(), name, message, date: formatDate(), likes: [], replies: [], }; messages.unshift(entry); if (messages.length > MAX_MESSAGES) messages.length = MAX_MESSAGES; await saveMessages(env, messages); return jsonResponse(entry, 201); } const likeMatch = path.match(/^\/messages\/([^/]+)\/like$/); if (request.method === 'POST' && likeMatch) { const messageId = likeMatch[1]; const ip = request.headers.get('CF-Connecting-IP') || 'unknown'; const ipHash = await hashIP(ip); const messages = await getMessages(env); const msg = messages.find(m => m.id === messageId); if (!msg) return jsonResponse({ error: 'Message not found' }, 404); if (!Array.isArray(msg.likes)) { const oldCount = typeof msg.likes === 'number' ? msg.likes : 0; msg.likes = []; for (let i = 0; i < oldCount; i++) msg.likes.push('legacy_' + i); } const alreadyLiked = msg.likes.includes(ipHash); if (!alreadyLiked) { msg.likes.push(ipHash); } await saveMessages(env, messages); return jsonResponse({ likes: msg.likes.length, liked: true }); } return jsonResponse({ error: 'Not found' }, 404); }, };