import { error } from '@sveltejs/kit'; export const MAX_JSON_BODY_BYTES = 64 * 1024; export function jsonOk(data: unknown, status = 200): Response { return new Response(JSON.stringify(data), { status, headers: { 'content-type': 'application/json' } }); } export function jsonError( status: number, message: string, headers?: Record ): Response { return new Response(JSON.stringify({ message }), { status, headers: { 'content-type': 'application/json', ...headers } }); } /** Read and parse a JSON request body with a size cap. Throws 413 for * oversized bodies and 400 for invalid JSON / wrong content type. */ export async function readJsonBody(request: Request): Promise { const contentType = request.headers.get('content-type') ?? ''; if (!contentType.includes('application/json')) { throw error(400, 'content-type must be application/json'); } const contentLength = Number(request.headers.get('content-length') ?? 0); if (contentLength > MAX_JSON_BODY_BYTES) { throw error(413, 'request body too large'); } const raw = await request.text(); if (raw.length === 0) { throw error(400, 'request body is required'); } if (raw.length > MAX_JSON_BODY_BYTES) { throw error(413, 'request body too large'); } try { return JSON.parse(raw); } catch { throw error(400, 'invalid JSON body'); } } /** Build a 429 response with Retry-After set in seconds. */ export function rateLimited(retryAfterMs: number): Response { const retryAfterSeconds = Math.max(1, Math.ceil(retryAfterMs / 1000)); return jsonError(429, 'rate limit exceeded', { 'retry-after': String(retryAfterSeconds) }); } export function clientIp(request: Request): string { const fwd = request.headers.get('x-forwarded-for'); if (fwd) { const first = fwd.split(',')[0]?.trim(); if (first) return first; } return request.headers.get('x-real-ip') ?? 'unknown'; }