import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { WEBHOOK_EVENTS } from "../lib/webhook-dispatcher.js"; export const webhookEventEnum = z.enum(WEBHOOK_EVENTS as unknown as [string, ...string[]]); export const createWebhookInputSchema = z.object({ name: z.string().min(1).max(200), url: z.string().url(), secret: z.string().optional(), events: z.array(webhookEventEnum).min(1), isActive: z.boolean().default(true), }); export const updateWebhookInputSchema = z.object({ name: z.string().min(1).max(200).optional(), url: z.string().url().optional(), secret: z.string().nullish(), events: z.array(webhookEventEnum).min(1).optional(), isActive: z.boolean().optional(), }); type WebhookRecord = { id: string; name: string; url: string; secret?: string | null; events?: string[]; isActive?: boolean; }; type WebhookDb = { webhook: { findUnique: (args: { where: { id: string } }) => Promise; }; }; export function buildWebhookCreateData( input: z.infer, ) { return { name: input.name, url: input.url, ...(input.secret !== undefined ? { secret: input.secret } : {}), events: input.events, isActive: input.isActive, }; } export function buildWebhookUpdateData( input: z.infer, ) { return { ...(input.name !== undefined ? { name: input.name } : {}), ...(input.url !== undefined ? { url: input.url } : {}), ...(input.secret !== undefined ? { secret: input.secret } : {}), ...(input.events !== undefined ? { events: input.events } : {}), ...(input.isActive !== undefined ? { isActive: input.isActive } : {}), }; } export async function loadWebhookOrThrow( db: WebhookDb, id: string, ) { const webhook = await db.webhook.findUnique({ where: { id } }); if (!webhook) { throw new TRPCError({ code: "NOT_FOUND", message: "Webhook not found" }); } return webhook; } export async function buildWebhookTestRequest( webhook: Pick, timestamp = new Date().toISOString(), ) { const payload = { event: "webhook.test", timestamp, payload: { webhookId: webhook.id, webhookName: webhook.name, message: "This is a test payload from CapaKraken.", }, }; const body = JSON.stringify(payload); const headers: Record = { "Content-Type": "application/json", "X-Webhook-Event": "webhook.test", }; if (webhook.secret) { const { createHmac } = await import("node:crypto"); headers["X-Webhook-Signature"] = createHmac("sha256", webhook.secret) .update(body) .digest("hex"); } return { body, headers }; } export async function sendWebhookTestRequest( webhook: Pick, options: { fetchImpl?: typeof fetch; timeoutMs?: number; timestamp?: string; } = {}, ): Promise<{ success: boolean; statusCode: number; statusText: string }> { const fetchImpl = options.fetchImpl ?? fetch; const timeoutMs = options.timeoutMs ?? 5_000; const { body, headers } = await buildWebhookTestRequest(webhook, options.timestamp); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetchImpl(webhook.url, { method: "POST", headers, body, signal: controller.signal, }); return { success: response.ok, statusCode: response.status, statusText: response.statusText, }; } catch (error) { return { success: false, statusCode: 0, statusText: error instanceof Error ? error.message : "Unknown error", }; } finally { clearTimeout(timeout); } }