From 1e5e33fd013f06592fb230c3d790ef578758e37d Mon Sep 17 00:00:00 2001 From: Eric Viana Date: Mon, 3 Aug 2026 19:52:03 -0300 Subject: [PATCH] chore(deps): bump svix to 1.99.1 and cover webhook signature verification The Snyk PR could not pass CI because it edits package.json without regenerating bun.lock, and lint installs with --frozen-lockfile. Claude-Session: https://claude.ai/code/session_01F1stiNzuNtJXoXtiW9ZCbs --- bun.lock | 4 +-- package.json | 4 +-- src/client.test.ts | 61 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index 4e37ecf..391272c 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "@blindpay/node", "dependencies": { - "svix": "1.94.0", + "svix": "1.99.1", }, "devDependencies": { "@biomejs/biome": "2.2.2", @@ -536,7 +536,7 @@ "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "svix": ["svix@1.94.0", "", { "dependencies": { "standardwebhooks": "1.0.0" } }, "sha512-FuP5nJez1P/SeK+4zOBI8RrNhtrzMTBSvwuD4yGGsn+Af2yQazid6OwPu3fpZpMifOJxrJua0su/dYB5fe4v9Q=="], + "svix": ["svix@1.99.1", "", { "dependencies": { "standardwebhooks": "1.0.0" } }, "sha512-JfpvbAg5iT5NagDAfOq3e6Ci6NbOMbMm6C3DnfDBB60m2JDK+Z2hXPE9XNAmutb53DCdPoih1V70IbklZnX09A=="], "term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="], diff --git a/package.json b/package.json index 7ac9d15..d8ef605 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@blindpay/node", - "version": "5.0.0", + "version": "5.0.1", "description": "Official Node.js SDK for Blindpay API - Stablecoin API for global payments", "keywords": [ "blindpay", @@ -63,6 +63,6 @@ }, "packageManager": "bun@1.2.18", "dependencies": { - "svix": "1.94.0" + "svix": "1.99.1" } } diff --git a/src/client.test.ts b/src/client.test.ts index b439f80..ab5acf5 100644 --- a/src/client.test.ts +++ b/src/client.test.ts @@ -1,3 +1,4 @@ +import { Webhook } from "svix"; import { afterEach, describe, expect, it } from "vitest"; import { BlindPay } from "./client"; @@ -15,3 +16,63 @@ describe("BlindPay client", () => { expect(data).toBeNull(); }); }); + +describe("verifyWebhookSignature", () => { + const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"; + const payload = JSON.stringify({ type: "payout.complete", data: { id: "po_000000000000" } }); + const id = "msg_000000000000"; + const timestamp = new Date(); + + const blindpay = new BlindPay({ apiKey: "test-key", instanceId: "in_000000000000" }); + const sign = (body: string) => new Webhook(secret).sign(id, timestamp, body); + const headers = (signature: string) => ({ + id, + timestamp: Math.floor(timestamp.getTime() / 1000).toString(), + signature, + }); + + it("accepts a signature produced for the same payload", () => { + expect( + blindpay.verifyWebhookSignature({ secret, headers: headers(sign(payload)), payload }) + ).toBe(true); + }); + + it("rejects a signature produced for a different payload", () => { + expect( + blindpay.verifyWebhookSignature({ + secret, + headers: headers(sign(payload)), + payload: `${payload} `, + }) + ).toBe(false); + }); + + it("rejects a signature made with a different secret", () => { + const other = new Webhook("whsec_TWFjaGluZUxlYXJuaW5nUm9ja3NOb3RSZWFsbHk=").sign( + id, + timestamp, + payload + ); + + expect(blindpay.verifyWebhookSignature({ secret, headers: headers(other), payload })).toBe( + false + ); + }); + + it("rejects a timestamp outside the tolerated window", () => { + const stale = new Date(timestamp.getTime() - 60 * 60 * 1000); + const signature = new Webhook(secret).sign(id, stale, payload); + + expect( + blindpay.verifyWebhookSignature({ + secret, + headers: { + id, + timestamp: Math.floor(stale.getTime() / 1000).toString(), + signature, + }, + payload, + }) + ).toBe(false); + }); +});