import { NextResponse } from "next/server"; import { db } from "@/lib/db"; import { withinRateLimit } from "@/lib/rate-limit"; import { clientIp } from "@/lib/security"; import { logEvent } from "@/lib/observability"; import { webhookSecretMatches } from "@/lib/webhook"; import { standardWebhookMatches } from "@/lib/webhook"; import { decrypt } from "@/lib/crypto"; import { queuePull } from "@/lib/sync"; export async function POST(request: Request, { params }: { params: Promise<{ sourceId: string; secret: string }> }) { const { sourceId, secret } = await params; const id = Number(sourceId); const source = db.prepare("SELECT id, webhook_secret_hash,webhook_mode,webhook_signing_secret_encrypted FROM sources WHERE id=? AND is_enabled=1").get(id) as { id: number; webhook_secret_hash: string | null; webhook_mode: string; webhook_signing_secret_encrypted: string | null } | undefined; if (!source || !webhookSecretMatches(secret, source.webhook_secret_hash)) return NextResponse.json({ error: "Not found" }, { status: 404 }); if (!withinRateLimit(`webhook:${id}:${clientIp(request)}`, 30, 60_000)) return NextResponse.json({ error: "Too many requests" }, { status: 429 }); const raw = await request.text(); if (source.webhook_mode === "signed") { const signingSecret = source.webhook_signing_secret_encrypted ? decrypt(source.webhook_signing_secret_encrypted) : ""; if (!standardWebhookMatches(signingSecret, request.headers.get("webhook-id"), request.headers.get("webhook-timestamp"), request.headers.get("webhook-signature"), raw)) return NextResponse.json({ error: "Invalid signature" }, { status: 401 }); } let payload: unknown = {}; try { payload = raw ? JSON.parse(raw) : {}; } catch { /* Memos payload is optional; a pull reconciles source state. */ } db.prepare("UPDATE sources SET last_webhook_at=CURRENT_TIMESTAMP WHERE id=?").run(id); const queued = queuePull(id, "webhook", payload); logEvent("info", "webhook_received", { sourceId: id, queued }); return NextResponse.json({ ok: true, queued }); }