import { Hono } from 'hono'; import { bodyLimit } from 'hono/body-limit'; import { validateLead, type LeadResponse } from '@kvarta/shared/lead'; import type { Db } from './db/index.js'; import { saveLead } from './leads.js'; import { notifyLater, type Notifier } from './notifier.js'; export type AppDeps = { db: Db; now: () => Date; notifier: Notifier; version?: string; }; export const MAX_BODY_BYTES = 16 * 1024; const USER_AGENT_MAX = 500; export function createApp(deps: AppDeps) { const app = new Hono(); const reply = (body: LeadResponse, status: 200 | 201 | 400 | 413 | 415 | 500) => Response.json(body, { status }); app.onError((err) => { console.error(err); return reply({ ok: false, error: 'server' }, 500); }); app.get('/api/health', (c) => c.json({ ok: true, version: deps.version ?? 'dev' })); app.post( '/api/lead', async (c, next) => { const type = c.req.header('content-type')?.split(';')[0]?.trim().toLowerCase(); if (type !== 'application/json') return reply({ ok: false, error: 'bad_request' }, 415); await next(); }, bodyLimit({ maxSize: MAX_BODY_BYTES, onError: () => reply({ ok: false, error: 'bad_request' }, 413) }), async (c) => { let body: unknown; try { body = await c.req.json(); } catch { return reply({ ok: false, error: 'bad_request' }, 400); } const result = validateLead(body); if (!result.ok) return reply({ ok: false, error: 'validation', fieldErrors: result.fieldErrors }, 400); const userAgent = c.req.header('user-agent')?.slice(0, USER_AGENT_MAX) ?? null; const saved = saveLead(deps.db, result.data, { now: deps.now(), userAgent }); if (saved.kind === 'unchanged') return reply({ ok: true, duplicate: true, clarified: false }, 200); notifyLater(deps.notifier, saved.event); if (saved.kind === 'clarified') return reply({ ok: true, duplicate: true, clarified: true }, 200); return reply({ ok: true, duplicate: false }, 201); }, ); return app; }