import crypto from "crypto"; import { db } from "@epicure/db"; import { webhooks, webhookDeliveries } from "@epicure/db"; import { eq, and } from "@epicure/db"; export type WebhookEvent = | "recipe.created" | "recipe.updated" | "recipe.published" | "recipe.deleted" | "meal_plan.updated" | "shopping_list.completed" | "comment.added"; export async function dispatchWebhook(userId: string, event: WebhookEvent, payload: object) { const hooks = await db .select() .from(webhooks) .where(and(eq(webhooks.userId, userId), eq(webhooks.active, true))); const filtered = hooks.filter((h) => h.events.length === 0 || h.events.includes(event)); await Promise.allSettled( filtered.map(async (hook) => { const body = JSON.stringify({ event, payload, timestamp: new Date().toISOString() }); const sig = crypto.createHmac("sha256", hook.secret).update(body).digest("hex"); let statusCode = 0; let success = false; try { const res = await fetch(hook.url, { method: "POST", headers: { "Content-Type": "application/json", "X-Epicure-Signature": `sha256=${sig}`, "X-Epicure-Event": event, }, body, signal: AbortSignal.timeout(10000), }); statusCode = res.status; success = res.ok; } catch { // delivery failed; statusCode stays 0, success stays false } await db.insert(webhookDeliveries).values({ id: crypto.randomUUID(), webhookId: hook.id, event, payload: payload as Record, statusCode, success, attempts: 1, createdAt: new Date(), }); }) ); }