abuse.ts57 lines · main
1import { Hono } from 'hono';
2import { z } from 'zod';
3
4import { ipKey, rateLimit } from '../middleware/rate-limit.js';
5import { ABUSE_SEVERITY, createAbuseReport } from '../services/abuse.js';
6import { hashIp } from '../services/audit.js';
7import type { AppEnv } from '../types/app-env.js';
8
9/**
10 * Public abuse-report intake. Anonymous (no auth) by design — anyone on
11 * the internet can flag a deployed briven app. Rate-limited by IP to
12 * keep the endpoint from being a spam vector against itself; the admin
13 * triage queue (in routes/admin.ts) is where reports get reviewed.
14 */
15export const abuseRouter = new Hono<AppEnv>();
16
17const createSchema = z.object({
18 targetUrl: z.string().url().max(500),
19 reason: z.string().min(1).max(2000),
20 severity: z.enum(ABUSE_SEVERITY),
21 reporterContact: z.string().max(200).nullable().optional(),
22});
23
24// Tight cap — abuse-report submissions should be bursty rare events,
25// not a sustained stream from any one origin. cf-connecting-ip pinning
26// is enforced by the rateLimit middleware itself outside dev.
27abuseRouter.post(
28 '/v1/abuse-reports',
29 rateLimit({
30 scope: 'abuse-report',
31 limit: 5,
32 windowMs: 60_000,
33 key: ipKey,
34 }),
35 async (c) => {
36 const body = await c.req.json().catch(() => null);
37 const parsed = createSchema.safeParse(body);
38 if (!parsed.success) {
39 return c.json(
40 { code: 'validation_failed', message: 'invalid request body', issues: parsed.error.issues },
41 400,
42 );
43 }
44 const fwd = c.req.raw.headers.get('cf-connecting-ip') ?? c.req.raw.headers.get('x-forwarded-for');
45 const ip = fwd ? fwd.split(',')[0]!.trim() : null;
46 const { reportId } = await createAbuseReport({
47 targetUrl: parsed.data.targetUrl,
48 reason: parsed.data.reason,
49 severity: parsed.data.severity,
50 reporterContact: parsed.data.reporterContact ?? null,
51 ipHash: hashIp(ip),
52 userAgent: c.req.header('user-agent') ?? null,
53 });
54 // 202 — we've accepted the report for triage but haven't acted yet.
55 return c.json({ reportId, status: 'open' }, 202);
56 },
57);