2026-07-19 00:29:30 +08:00
import { NextResponse } from "next/server" ;
import { db } from "@/lib/db" ;
import { withinRateLimit } from "@/lib/rate-limit" ;
2026-07-19 03:58:03 +08:00
import { clientIp } from "@/lib/security" ;
import { logEvent } from "@/lib/observability" ;
2026-07-19 00:29:30 +08:00
import { webhookSecretMatches } from "@/lib/webhook" ;
2026-07-19 05:14:01 +08:00
import { standardWebhookMatches } from "@/lib/webhook" ;
import { decrypt } from "@/lib/crypto" ;
2026-07-19 01:12:16 +08:00
import { queuePull } from "@/lib/sync" ;
2026-07-19 00:29:30 +08:00
export async function POST ( request : Request , { params } : { params : Promise < { sourceId : string ; secret : string } > }) {
const { sourceId , secret } = await params ;
const id = Number ( sourceId );
2026-07-19 05:14:01 +08:00
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 ;
2026-07-19 00:29:30 +08:00
if ( ! source || ! webhookSecretMatches ( secret , source . webhook_secret_hash )) return NextResponse . json ({ error : "Not found" }, { status : 404 });
2026-07-19 03:58:03 +08:00
if ( ! withinRateLimit ( `webhook: ${ id } : ${ clientIp ( request ) } ` , 30 , 60 _000 )) return NextResponse . json ({ error : "Too many requests" }, { status : 429 });
2026-07-19 05:14:01 +08:00
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. */ }
2026-07-19 00:29:30 +08:00
db . prepare ( "UPDATE sources SET last_webhook_at=CURRENT_TIMESTAMP WHERE id=?" ). run ( id );
2026-07-19 01:12:16 +08:00
const queued = queuePull ( id , "webhook" , payload );
2026-07-19 03:58:03 +08:00
logEvent ( "info" , "webhook_received" , { sourceId : id , queued });
2026-07-19 01:12:16 +08:00
return NextResponse . json ({ ok : true , queued });
2026-07-19 00:29:30 +08:00
}