import { normalizeBug, normalizeCrash, normalizeEvents, readJsonObject, } from "./input"; import { type DiagnosticsEnv, runRetention, storeBug, storeCrash, storeEvents, } from "./storage"; const DEFAULT_MAX_BODY_BYTES = 262_144; const HARD_MAX_BODY_BYTES = 1_048_576; const DEFAULT_MAX_EVENTS = 50; export default { async fetch(request: Request, env: DiagnosticsEnv, _ctx: ExecutionContext): Promise { const requestId = crypto.randomUUID(); const url = new URL(request.url); try { if (request.method === "GET" && url.pathname === "/live") { return json({ ok: true, service: "vnidrop-diagnostics", schema: 1 }, 200, requestId); } if (request.method === "GET" && url.pathname === "/health") { if (await sourceRateLimited(request, url.pathname, env)) { return json({ error: "rate_limited" }, 429, requestId, { "retry-after": "60" }); } const authError = await authorize(request, env); if (authError) return json({ error: authError.error }, authError.status, requestId); return await readiness(env, requestId); } if (!isIngestPath(url.pathname)) { return json({ error: "not_found" }, 404, requestId); } if (request.method === "OPTIONS") { return new Response(null, { status: 204, headers: responseHeaders(requestId, { allow: "POST, OPTIONS" }), }); } if (request.method !== "POST") { return json( { error: "method_not_allowed" }, 405, requestId, { allow: "POST, OPTIONS" }, ); } if (await sourceRateLimited(request, url.pathname, env)) { return json({ error: "rate_limited" }, 429, requestId, { "retry-after": "60" }); } const authError = await authorize(request, env); if (authError) return json({ error: authError.error }, authError.status, requestId); if (await installRateLimited(request, url.pathname, env)) { return json({ error: "rate_limited" }, 429, requestId, { "retry-after": "60" }); } const maxBodyBytes = boundedPositiveInt( env.MAX_BODY_BYTES, DEFAULT_MAX_BODY_BYTES, 1, HARD_MAX_BODY_BYTES, ); const parsed = await readJsonObject(request, maxBodyBytes); if (!parsed.ok) return json({ error: parsed.error }, parsed.status, requestId); switch (url.pathname) { case "/v1/events": { const maxEvents = boundedPositiveInt(env.MAX_EVENTS_PER_BATCH, DEFAULT_MAX_EVENTS, 1, 100); const normalized = normalizeEvents(parsed.value, maxEvents); if (!normalized.ok) { return json({ error: normalized.error }, normalized.status, requestId); } const result = await storeEvents(normalized.value, env); return json( { ok: true, id: result.id, stored: result.stored, duplicate: result.duplicate, }, 202, requestId, ); } case "/v1/crashes": { const normalized = normalizeCrash(parsed.value); if (!normalized.ok) { return json({ error: normalized.error }, normalized.status, requestId); } const result = await storeCrash(normalized.value, env); return json( { ok: true, id: result.id, fingerprint: result.fingerprint, duplicate: result.duplicate, }, 202, requestId, ); } case "/v1/bugs": { const normalized = normalizeBug(parsed.value); if (!normalized.ok) { return json({ error: normalized.error }, normalized.status, requestId); } const result = await storeBug(normalized.value, env); return json( { ok: true, id: result.id, duplicate: result.duplicate }, 202, requestId, ); } } } catch (error) { console.error( JSON.stringify({ message: "diagnostics request failed", requestId, method: request.method, path: url.pathname, error: error instanceof Error ? error.message : String(error), }), ); return json({ error: "internal" }, 500, requestId); } }, scheduled( controller: ScheduledController, env: DiagnosticsEnv, ctx: ExecutionContext, ): void { ctx.waitUntil( runRetention(env).catch((error) => { console.error( JSON.stringify({ message: "diagnostics retention failed", scheduledTime: controller.scheduledTime, error: error instanceof Error ? error.message : String(error), }), ); }), ); }, } satisfies ExportedHandler; async function readiness(env: DiagnosticsEnv, requestId: string): Promise { if (!env.INGEST_KEY) { return json({ ok: false, error: "server_misconfigured" }, 503, requestId); } try { await env.DB.batch([ env.DB.prepare( "SELECT id, received_at, install_id, payload_json FROM event_batches LIMIT 1", ), env.DB.prepare( "SELECT id, occurred_at, stack_r2_key, breadcrumbs_json FROM crashes LIMIT 1", ), env.DB.prepare( "SELECT id, occurred_at, logs_r2_key, device_json FROM bugs LIMIT 1", ), ]); return json({ ok: true, service: "vnidrop-diagnostics", schema: 1 }, 200, requestId); } catch (error) { console.error( JSON.stringify({ message: "diagnostics readiness check failed", requestId, error: error instanceof Error ? error.message : String(error), }), ); return json({ ok: false, error: "dependency_unavailable" }, 503, requestId); } } async function authorize( request: Request, env: DiagnosticsEnv, ): Promise<{ status: 401 | 503; error: "unauthorized" | "server_misconfigured" } | null> { const expected = env.INGEST_KEY; if (!expected) return { status: 503, error: "server_misconfigured" }; const provided = request.headers.get("x-vnidrop-key") ?? ""; if (!(await timingSafeEqual(provided, expected))) { return { status: 401, error: "unauthorized" }; } return null; } async function sourceRateLimited( request: Request, path: string, env: DiagnosticsEnv, ): Promise { const source = request.headers.get("cf-connecting-ip")?.slice(0, 64) || "unknown"; const result = await env.SOURCE_RATE_LIMITER.limit({ key: `${source}:${path}` }); return !result.success; } async function installRateLimited( request: Request, path: string, env: DiagnosticsEnv, ): Promise { const source = request.headers.get("cf-connecting-ip")?.slice(0, 64) || "unknown"; const installId = request.headers.get("x-vnidrop-install-id")?.trim().slice(0, 80) || source; const result = await env.INSTALL_RATE_LIMITER.limit({ key: `${installId}:${path}` }); return !result.success; } function isIngestPath(path: string): path is "/v1/events" | "/v1/crashes" | "/v1/bugs" { return path === "/v1/events" || path === "/v1/crashes" || path === "/v1/bugs"; } async function timingSafeEqual(provided: string, expected: string): Promise { const encoder = new TextEncoder(); const [providedHash, expectedHash] = await Promise.all([ crypto.subtle.digest("SHA-256", encoder.encode(provided)), crypto.subtle.digest("SHA-256", encoder.encode(expected)), ]); return crypto.subtle.timingSafeEqual(providedHash, expectedHash); } function json( body: unknown, status: number, requestId: string, extraHeaders?: Readonly>, ): Response { return new Response(JSON.stringify(body), { status, headers: responseHeaders(requestId, { "content-type": "application/json; charset=utf-8", ...extraHeaders, }), }); } function responseHeaders( requestId: string, extraHeaders?: Readonly>, ): Headers { const headers = new Headers(extraHeaders); headers.set("cache-control", "no-store"); headers.set("x-content-type-options", "nosniff"); headers.set("x-request-id", requestId); return headers; } function boundedPositiveInt( raw: string | undefined, fallback: number, minimum: number, maximum: number, ): number { const parsed = Number(raw); if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) return fallback; return parsed; }