import { logger } from "~/services/logger.service"; import crypto from "crypto"; // Common webhook delivery types export type WebhookEventType = | "activity.created" | "integration.connected" | "integration.disconnected" | "mcp.connected" | "mcp.disconnected"; // Webhook target configuration export interface WebhookTarget { url: string; secret?: string | null; headers?: Record; accountId?: string; } // Delivery result export interface DeliveryResult { url: string; status: number; success: boolean; responseBody?: string; error?: string; } // Generic webhook delivery parameters export interface WebhookDeliveryParams { payload: any; // Can be any webhook payload structure targets: WebhookTarget[]; userAgent?: string; eventType: WebhookEventType; } /** * Common webhook delivery function that handles HTTP delivery logic */ export async function deliverWebhook(params: WebhookDeliveryParams): Promise<{ success: boolean; deliveryResults: DeliveryResult[]; summary: { total: number; successful: number; failed: number; }; }> { const { payload, targets, userAgent = "Core-Webhooks/1.0", eventType, } = params; const payloadString = JSON.stringify({ ...payload, accountId: payload.accountId, }); const deliveryResults: DeliveryResult[] = []; logger.log(`Delivering ${eventType} webhook to ${targets.length} targets`); // Send webhook to each target for (const target of targets) { const deliveryId = crypto.randomUUID(); try { // Prepare headers const headers: Record = { "Content-Type": "application/json", "User-Agent": userAgent, "X-Webhook-Delivery": deliveryId, "X-Webhook-Event": eventType, ...target.headers, }; // Add HMAC signature if secret is configured if (target.secret) { const signature = crypto .createHmac("sha256", target.secret) .update(payloadString) .digest("hex"); // Use different header names for different webhook types if (eventType === "activity.created") { headers["X-Hub-Signature-256"] = `sha256=${signature}`; } else { headers["X-Webhook-Secret"] = signature; } } // Make the HTTP request const response = await fetch(target.url, { method: "POST", headers, body: payloadString, signal: AbortSignal.timeout(30000), // 30 second timeout }); const responseBody = await response.text().catch(() => ""); const result: DeliveryResult = { url: target.url, status: response.status, success: response.ok, responseBody: responseBody.slice(0, 500), // Limit response body length error: response.ok ? undefined : `HTTP ${response.status}: ${response.statusText}`, }; deliveryResults.push(result); logger.log(`Webhook delivered to ${target.url}:`, { status: response.status, event: eventType, success: response.ok, }); } catch (error) { const result: DeliveryResult = { url: target.url, status: 0, success: false, error: error instanceof Error ? error.message : "Unknown error", }; deliveryResults.push(result); logger.error(`Failed to deliver webhook to ${target.url}:`, { error, event: eventType, }); } } const successfulDeliveries = deliveryResults.filter((r) => r.success).length; const totalDeliveries = deliveryResults.length; logger.log( `Webhook delivery completed: ${successfulDeliveries}/${totalDeliveries} successful`, { event: eventType, }, ); return { success: successfulDeliveries > 0, deliveryResults, summary: { total: totalDeliveries, successful: successfulDeliveries, failed: totalDeliveries - successfulDeliveries, }, }; } /** * Helper function to prepare webhook targets from basic URL/secret pairs */ export function prepareWebhookTargets( webhooks: Array<{ url: string; secret?: string | null; id: string }>, ): WebhookTarget[] { return webhooks.map((webhook) => ({ url: webhook.url, secret: webhook.secret, accountId: webhook.id, })); }