import { corsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts' import { requireUser, type AuthError } from '../_shared/auth.ts' import { createServiceRoleClient } from '../_shared/quota.ts' import { ContentReportRequestError, mapContentReportRpcError, parseContentReportReceipt, parseContentReportRequest, parseIdempotencyKey, } from '../_shared/content-report-contract.ts' const MAX_REQUEST_BYTES = 25_000 function jsonResponse(body: unknown, status: number): Response { return new Response(JSON.stringify(body), { status, headers: { ...corsHeaders, 'Content-Type': 'application/json', 'Cache-Control': 'no-store', Pragma: 'no-cache', }, }) } async function readRequestJson(req: Request): Promise { const contentType = req.headers.get('content-type')?.toLowerCase() ?? '' if (contentType.split(';', 1)[0].trim() !== 'application/json') { throw new ContentReportRequestError('content_type_must_be_json', 415) } const declaredLength = Number(req.headers.get('content-length')) if (Number.isFinite(declaredLength) && declaredLength > MAX_REQUEST_BYTES) { throw new ContentReportRequestError('request_too_large', 413) } const rawBody = await req.text() if (new TextEncoder().encode(rawBody).byteLength > MAX_REQUEST_BYTES) { throw new ContentReportRequestError('request_too_large', 413) } try { return JSON.parse(rawBody) as unknown } catch { throw new ContentReportRequestError('invalid_json') } } Deno.serve(async (req: Request) => { const preflight = handleCorsPreflightRequest(req) if (preflight) { const headers = new Headers(preflight.headers) headers.set('Cache-Control', 'no-store') return new Response(preflight.body, { status: preflight.status, headers }) } if (req.method !== 'POST') return jsonResponse({ error: 'method_not_allowed' }, 405) try { const user = await requireUser(req) const idempotencyKey = parseIdempotencyKey(req.headers.get('Idempotency-Key')) const body = parseContentReportRequest(await readRequestJson(req)) const serviceClient = createServiceRoleClient() const { data, error } = await serviceClient.rpc('submit_content_report_v1', { p_actor_id: user.id, p_idempotency_key: idempotencyKey, p_kind: body.kind, p_source_type: body.source.type, p_source_id: body.source.generationId, p_reason: body.reason, p_comment: body.comment ?? null, p_snapshot: body.snapshot, }) if (error) { const mapped = mapContentReportRpcError(error) if (mapped.status === 500) { console.error('content-report RPC failed', { code: error.code ?? 'unknown' }) } return jsonResponse({ error: mapped.code }, mapped.status) } const receipt = parseContentReportReceipt(data) return jsonResponse(receipt, receipt.idempotent ? 200 : 201) } catch (error) { if (error instanceof ContentReportRequestError) { return jsonResponse({ error: error.code }, error.status) } if (error && typeof error === 'object' && 'status' in error && 'message' in error) { const authError = error as AuthError return jsonResponse({ error: authError.message }, authError.status) } console.error('content-report failed', { code: 'internal_error' }) return jsonResponse({ error: 'internal_error' }, 500) } })