25 lines
2.0 KiB
TypeScript
25 lines
2.0 KiB
TypeScript
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 });
|
|
}
|