From 93b1c6b9423d75d8698cb2062e42c70c779ad229 Mon Sep 17 00:00:00 2001 From: ZacLou Date: Sun, 6 Sep 2026 02:08:18 +0800 Subject: [PATCH 1/4] feat(dedup): add generateIdempotencyKey and key registry (#612) - generateIdempotencyKey: deterministic SHA-256 hex key from invoiceId, payer, amount - isKnownKey / registerKey / clearKeys: in-memory Set-backed registry - Exported from index.ts alongside existing Deduplicator - Unit tests: determinism, input sensitivity, nonce variation, registry lifecycle Closes Stellar-split/split-sdk#612 --- src/dedup.ts | 37 +++++++++++++++ src/index.ts | 8 +++- test/dedup.test.ts | 115 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 test/dedup.test.ts diff --git a/src/dedup.ts b/src/dedup.ts index 485edcd..4357afd 100644 --- a/src/dedup.ts +++ b/src/dedup.ts @@ -1,3 +1,5 @@ +import { createHash } from "crypto"; + export class Deduplicator { private _inflight = new Map>(); private _hits = 0; @@ -24,3 +26,38 @@ export class Deduplicator { return { deduped: this._hits, total: this._hits + this._misses }; } } + +// In-memory key registry for idempotency tracking (#612) +const _knownKeys = new Set(); + +/** + * Generates a deterministic idempotency key from payment parameters. + * The key is a SHA-256 hex digest of `"{invoiceId}:{payer}:{amount}"` + * with an optional `:{nonce}` suffix when provided. + */ +export function generateIdempotencyKey(params: { + invoiceId: string; + payer: string; + amount: bigint; + nonce?: string; +}): string { + const payload = params.nonce + ? `${params.invoiceId}:${params.payer}:${params.amount}:${params.nonce}` + : `${params.invoiceId}:${params.payer}:${params.amount}`; + return createHash("sha256").update(payload).digest("hex"); +} + +/** Returns true if the key has already been registered. */ +export function isKnownKey(key: string): boolean { + return _knownKeys.has(key); +} + +/** Registers a key as known (idempotent). */ +export function registerKey(key: string): void { + _knownKeys.add(key); +} + +/** Clears the in-memory key registry. Intended for test teardown. */ +export function clearKeys(): void { + _knownKeys.clear(); +} diff --git a/src/index.ts b/src/index.ts index ebf7e30..a68bda2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -277,7 +277,13 @@ export { buildRevealTransactionFromStorage, } from "./confidential.js"; -export { Deduplicator } from "./dedup.js"; +export { + Deduplicator, + generateIdempotencyKey, + isKnownKey, + registerKey, + clearKeys, +} from "./dedup.js"; export { TxQueue } from "./queue.js"; diff --git a/test/dedup.test.ts b/test/dedup.test.ts new file mode 100644 index 0000000..29c0d42 --- /dev/null +++ b/test/dedup.test.ts @@ -0,0 +1,115 @@ +import { + generateIdempotencyKey, + isKnownKey, + registerKey, + clearKeys, +} from "../src/dedup.js"; + +describe("generateIdempotencyKey", () => { + afterEach(() => { + clearKeys(); + }); + + it("produces the same key for identical inputs", () => { + const params = { + invoiceId: "inv-123", + payer: "GABC123", + amount: 1000n, + }; + const key1 = generateIdempotencyKey(params); + const key2 = generateIdempotencyKey(params); + expect(key1).toBe(key2); + expect(key1).toMatch(/^[a-f0-9]{64}$/); + }); + + it("produces different keys for different amounts", () => { + const key1 = generateIdempotencyKey({ + invoiceId: "inv-123", + payer: "GABC123", + amount: 1000n, + }); + const key2 = generateIdempotencyKey({ + invoiceId: "inv-123", + payer: "GABC123", + amount: 2000n, + }); + expect(key1).not.toBe(key2); + }); + + it("produces different keys for different payers", () => { + const key1 = generateIdempotencyKey({ + invoiceId: "inv-123", + payer: "GABC123", + amount: 1000n, + }); + const key2 = generateIdempotencyKey({ + invoiceId: "inv-123", + payer: "GDEF456", + amount: 1000n, + }); + expect(key1).not.toBe(key2); + }); + + it("produces different keys for different invoiceIds", () => { + const key1 = generateIdempotencyKey({ + invoiceId: "inv-123", + payer: "GABC123", + amount: 1000n, + }); + const key2 = generateIdempotencyKey({ + invoiceId: "inv-456", + payer: "GABC123", + amount: 1000n, + }); + expect(key1).not.toBe(key2); + }); + + it("changes the key when a nonce is provided", () => { + const base = generateIdempotencyKey({ + invoiceId: "inv-123", + payer: "GABC123", + amount: 1000n, + }); + const withNonce = generateIdempotencyKey({ + invoiceId: "inv-123", + payer: "GABC123", + amount: 1000n, + nonce: "abc", + }); + expect(withNonce).not.toBe(base); + expect(withNonce).toMatch(/^[a-f0-9]{64}$/); + }); + + it("produces the same key for the same nonce", () => { + const params = { + invoiceId: "inv-123", + payer: "GABC123", + amount: 1000n, + nonce: "xyz", + }; + expect(generateIdempotencyKey(params)).toBe(generateIdempotencyKey(params)); + }); +}); + +describe("key registry", () => { + afterEach(() => { + clearKeys(); + }); + + it("returns false for unknown keys", () => { + expect(isKnownKey("unknown")).toBe(false); + }); + + it("returns true after registering a key", () => { + registerKey("my-key"); + expect(isKnownKey("my-key")).toBe(true); + }); + + it("clears all keys", () => { + registerKey("a"); + registerKey("b"); + clearKeys(); + expect(isKnownKey("a")).toBe(false); + expect(isKnownKey("b")).toBe(false); + }); +}); From dc9dfb8741d5e9859a8a753b2b08f1ee19355189 Mon Sep 17 00:00:00 2001 From: ZacLou Date: Sun, 6 Sep 2026 02:12:12 +0800 Subject: [PATCH 2/4] feat(search): add searchByMemo for memo-content lookup (#614) - searchByMemo(invoices, query, opts?) filters invoices by memo substring - case-insensitive by default; opts.caseSensitive=true for exact case - empty query returns all invoices unchanged - invoices with undefined/null memo are skipped without error - 6 unit tests covering all acceptance criteria Closes Stellar-split/split-sdk#614 --- src/index.ts | 1 + src/search.ts | 26 +++++++++++++++++- test/searchByMemo.test.ts | 56 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 test/searchByMemo.test.ts diff --git a/src/index.ts b/src/index.ts index a68bda2..58a11ba 100644 --- a/src/index.ts +++ b/src/index.ts @@ -285,6 +285,7 @@ export { clearKeys, } from "./dedup.js"; +export { searchByMemo } from "./search.js"; export { TxQueue } from "./queue.js"; export { replayEvents } from "./events.js"; diff --git a/src/search.ts b/src/search.ts index 8d84608..f197dec 100644 --- a/src/search.ts +++ b/src/search.ts @@ -45,4 +45,28 @@ export async function searchInvoices( } catch (error) { throw new SearchFailedError(error instanceof Error ? error.message : String(error)); } -} \ No newline at end of file +} +import type { Invoice } from "./types.js"; + +/** + * Search a local array of invoices by memo content. + * + * @param invoices - Array of invoices to search + * @param query - Substring to match against `invoice.memo` + * @param opts - Optional flags (caseSensitive defaults to false) + * @returns Invoices whose memo contains the query substring + */ +export function searchByMemo( + invoices: Invoice[], + query: string, + opts?: { caseSensitive?: boolean } +): Invoice[] { + if (!query) return invoices; + + const target = opts?.caseSensitive ? query : query.toLowerCase(); + return invoices.filter((invoice) => { + if (invoice.memo == null) return false; + const memo = opts?.caseSensitive ? invoice.memo : invoice.memo.toLowerCase(); + return memo.includes(target); + }); +} diff --git a/test/searchByMemo.test.ts b/test/searchByMemo.test.ts new file mode 100644 index 0000000..87f1064 --- /dev/null +++ b/test/searchByMemo.test.ts @@ -0,0 +1,56 @@ +import { searchByMemo } from "../src/search.js"; +import type { Invoice } from "../src/types.js"; + +function makeInvoice(memo?: string): Invoice { + return { + id: "1", + creator: "GABC", + recipients: [], + token: "USDC", + deadline: 0, + memo, + } as Invoice; +} + +describe("searchByMemo", () => { + const invoices = [ + makeInvoice("split:INV-001"), + makeInvoice("SPLIT:inv-002"), + makeInvoice("payment for project alpha"), + makeInvoice(), + makeInvoice(""), + ]; + + it("returns all invoices when query is empty", () => { + expect(searchByMemo(invoices, "")).toHaveLength(5); + }); + + it("finds invoices by substring (case-insensitive default)", () => { + const results = searchByMemo(invoices, "split"); + expect(results).toHaveLength(2); + expect(results.map((i) => i.memo)).toContain("split:INV-001"); + expect(results.map((i) => i.memo)).toContain("SPLIT:inv-002"); + }); + + it("is case-sensitive when opts.caseSensitive is true", () => { + const results = searchByMemo(invoices, "split", { caseSensitive: true }); + expect(results).toHaveLength(1); + expect(results[0].memo).toBe("split:INV-001"); + }); + + it("skips invoices with undefined or null memo", () => { + const results = searchByMemo(invoices, "project"); + expect(results).toHaveLength(1); + expect(results[0].memo).toBe("payment for project alpha"); + }); + + it("matches partial strings", () => { + const results = searchByMemo(invoices, "alpha"); + expect(results).toHaveLength(1); + expect(results[0].memo).toBe("payment for project alpha"); + }); + + it("returns empty array when no matches", () => { + expect(searchByMemo(invoices, "nonexistent")).toHaveLength(0); + }); +}); From 2c23e471522a0760c2daf81da5d351a8dded33b5 Mon Sep 17 00:00:00 2001 From: ZacLou Date: Sun, 6 Sep 2026 02:33:46 +0800 Subject: [PATCH 3/4] feat(webhooks): export verifyWebhookSignature as standalone with error class (#617) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - verifyWebhookSignature(payload, signature, secret): boolean — HMAC-SHA256 with timing-safe comparison - verifyWebhookSignatureOrThrow: wrapper that throws WebhookVerificationError - WebhookVerificationError: typed error for consumers who prefer throwing - Exported from src/index.ts alongside existing webhookMiddleware exports - 7 unit tests covering valid/invalid signatures, tampered payloads, malformed input, and error class Closes Stellar-split/split-sdk#617 --- src/index.ts | 11 ++++- src/webhooks/verify.ts | 51 ++++++++++++++++------ test/webhookVerify.test.ts | 87 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 15 deletions(-) create mode 100644 test/webhookVerify.test.ts diff --git a/src/index.ts b/src/index.ts index 58a11ba..6018467 100644 --- a/src/index.ts +++ b/src/index.ts @@ -589,7 +589,8 @@ export type { WebhookRecord, WebhookReplayStore } from "./webhookReplay.js"; export { createWebhookMiddleware, generateWebhookSignature, - verifyWebhookSignature, + // verifyWebhookSignature moved to ./webhooks/verify.js + parseWebhookPayload, isValidEventType, isWebhookRequest, @@ -614,6 +615,14 @@ export type { InvoiceCancelledData, InvoiceExpiredData, } from "./webhookMiddleware.js"; + +// Standalone webhook signature verifier (#617) +export { + verifyWebhookSignature, + verifyWebhookSignatureOrThrow, + WebhookVerificationError, +} from "./webhooks/verify.js"; + // --------------------------------------------------------------------------- // Lazy factories for heavy modules // --------------------------------------------------------------------------- diff --git a/src/webhooks/verify.ts b/src/webhooks/verify.ts index 88cab67..4dbc168 100644 --- a/src/webhooks/verify.ts +++ b/src/webhooks/verify.ts @@ -10,30 +10,53 @@ import { createHmac, timingSafeEqual } from "crypto"; const HEX_PATTERN = /^[0-9a-f]+$/i; +/** Thrown by {@link verifyWebhookSignatureOrThrow} when the signature does not match. */ +export class WebhookVerificationError extends Error { + constructor(message = "Webhook signature verification failed") { + super(message); + this.name = "WebhookVerificationError"; + } +} + /** - * Verifies the `X-Stellar-Split-Signature` header against the raw request - * body using a timing-safe comparison. + * Verifies an HMAC-SHA256 webhook signature in constant time. * - * @param secret - The shared HMAC secret configured for the webhook. - * @param rawBody - The exact, unparsed request body bytes as received. - * @param signatureHeader - The hex-encoded signature from the request header. - * @returns `true` only when the computed digest matches the header value. + * @param payload - The raw request body (exact bytes as received). + * @param signature - The hex-encoded signature to verify against. + * @param secret - The shared HMAC secret. + * @returns `true` when the computed digest matches the provided signature. + * Returns `false` (never throws) on malformed input or mismatch. */ export function verifyWebhookSignature( - secret: string, - rawBody: string, - signatureHeader: string + payload: string, + signature: string, + secret: string ): boolean { - if (!HEX_PATTERN.test(signatureHeader) || signatureHeader.length % 2 !== 0) { + if (!HEX_PATTERN.test(signature)) { return false; } - const expected = createHmac("sha256", secret).update(rawBody).digest(); - const provided = Buffer.from(signatureHeader, "hex"); + const expected = createHmac("sha256", secret).update(payload).digest("hex"); + const expectedBuf = Buffer.from(expected, "utf-8"); + const providedBuf = Buffer.from(signature, "utf-8"); - if (expected.length !== provided.length) { + if (expectedBuf.length !== providedBuf.length) { return false; } - return timingSafeEqual(expected, provided); + return timingSafeEqual(expectedBuf, providedBuf); +} + +/** + * Wrapper around {@link verifyWebhookSignature} that throws + * {@link WebhookVerificationError} instead of returning `false`. + */ +export function verifyWebhookSignatureOrThrow( + payload: string, + signature: string, + secret: string +): void { + if (!verifyWebhookSignature(payload, signature, secret)) { + throw new WebhookVerificationError(); + } } diff --git a/test/webhookVerify.test.ts b/test/webhookVerify.test.ts new file mode 100644 index 0000000..38b2053 --- /dev/null +++ b/test/webhookVerify.test.ts @@ -0,0 +1,87 @@ +import { + verifyWebhookSignature, + verifyWebhookSignatureOrThrow, + WebhookVerificationError, +} from "../src/webhooks/verify.js"; + +describe("verifyWebhookSignature", () => { + const secret = "my-secret-key"; + const payload = '{"event":"invoice.paid","data":{"id":"123"}}'; + + it("returns true for a valid signature", () => { + const crypto = require("crypto"); + const expected = crypto + .createHmac("sha256", secret) + .update(payload) + .digest("hex"); + expect(verifyWebhookSignature(payload, expected, secret)).toBe(true); + }); + + it("returns false for a wrong secret", () => { + const crypto = require("crypto"); + const expected = crypto + .createHmac("sha256", secret) + .update(payload) + .digest("hex"); + expect(verifyWebhookSignature(payload, expected, "wrong-secret")).toBe( + false, + ); + }); + + it("returns false for a tampered payload", () => { + const crypto = require("crypto"); + const expected = crypto + .createHmac("sha256", secret) + .update(payload) + .digest("hex"); + expect( + verifyWebhookSignature(payload + "x", expected, secret), + ).toBe(false); + }); + + it("returns false for a malformed signature (non-hex)", () => { + expect(verifyWebhookSignature(payload, "not-hex!", secret)).toBe(false); + }); + + it("returns false when signature lengths differ", () => { + expect(verifyWebhookSignature(payload, "abcd", secret)).toBe(false); + }); + + it("never throws", () => { + expect(() => + verifyWebhookSignature(payload, "bad-sig", secret), + ).not.toThrow(); + }); +}); + +describe("verifyWebhookSignatureOrThrow", () => { + const secret = "my-secret-key"; + const payload = "test-payload"; + + it("does not throw for a valid signature", () => { + const crypto = require("crypto"); + const sig = crypto + .createHmac("sha256", secret) + .update(payload) + .digest("hex"); + expect(() => verifyWebhookSignatureOrThrow(payload, sig, secret)).not.toThrow(); + }); + + it("throws WebhookVerificationError for an invalid signature", () => { + expect(() => + verifyWebhookSignatureOrThrow(payload, "bad-sig", secret), + ).toThrow(WebhookVerificationError); + }); +}); + +describe("WebhookVerificationError", () => { + it("has the correct name", () => { + const err = new WebhookVerificationError(); + expect(err.name).toBe("WebhookVerificationError"); + }); + + it("accepts a custom message", () => { + const err = new WebhookVerificationError("custom msg"); + expect(err.message).toBe("custom msg"); + }); +}); From 9dd54dc5730d4bfae469d1b83769abee90913484 Mon Sep 17 00:00:00 2001 From: ZacLou Date: Sun, 6 Sep 2026 05:16:10 +0800 Subject: [PATCH 4/4] feat(webhooks): export verifyWebhookSignature with correct signature, add WebhookVerificationError (#617) --- src/webhooks/verify.ts | 54 ++++++++++++++---------- test/webhookVerify.test.ts | 85 ++++++++++++++++---------------------- 2 files changed, 67 insertions(+), 72 deletions(-) diff --git a/src/webhooks/verify.ts b/src/webhooks/verify.ts index 4dbc168..79547ac 100644 --- a/src/webhooks/verify.ts +++ b/src/webhooks/verify.ts @@ -10,46 +10,56 @@ import { createHmac, timingSafeEqual } from "crypto"; const HEX_PATTERN = /^[0-9a-f]+$/i; -/** Thrown by {@link verifyWebhookSignatureOrThrow} when the signature does not match. */ -export class WebhookVerificationError extends Error { - constructor(message = "Webhook signature verification failed") { - super(message); - this.name = "WebhookVerificationError"; - } -} - /** - * Verifies an HMAC-SHA256 webhook signature in constant time. + * Verifies a webhook payload against its HMAC-SHA256 signature. * - * @param payload - The raw request body (exact bytes as received). - * @param signature - The hex-encoded signature to verify against. - * @param secret - The shared HMAC secret. - * @returns `true` when the computed digest matches the provided signature. - * Returns `false` (never throws) on malformed input or mismatch. + * @param payload - The raw request body / payload string. + * @param signature - The hex-encoded HMAC-SHA256 signature to verify. + * @param secret - The shared secret key. + * @returns `true` when the signature is valid, `false` otherwise. + * Never throws — malformed inputs return `false`. */ export function verifyWebhookSignature( payload: string, signature: string, secret: string ): boolean { - if (!HEX_PATTERN.test(signature)) { + if (!HEX_PATTERN.test(signature) || signature.length % 2 !== 0) { return false; } - const expected = createHmac("sha256", secret).update(payload).digest("hex"); - const expectedBuf = Buffer.from(expected, "utf-8"); - const providedBuf = Buffer.from(signature, "utf-8"); + const expected = createHmac("sha256", secret).update(payload).digest(); + const provided = Buffer.from(signature, "hex"); - if (expectedBuf.length !== providedBuf.length) { + if (expected.length !== provided.length) { return false; } - return timingSafeEqual(expectedBuf, providedBuf); + return timingSafeEqual(expected, provided); +} + +/** + * Thrown when a webhook signature fails verification. + * + * Wraps {@link verifyWebhookSignature} for consumers who prefer a throwing + * interface rather than checking a boolean return value. + */ +export class WebhookVerificationError extends Error { + constructor() { + super("Webhook signature verification failed"); + this.name = "WebhookVerificationError"; + Object.setPrototypeOf(this, new.target.prototype); + } } /** - * Wrapper around {@link verifyWebhookSignature} that throws - * {@link WebhookVerificationError} instead of returning `false`. + * Verifies a webhook payload and throws {@link WebhookVerificationError} + * when the signature is invalid. + * + * @param payload - The raw request body / payload string. + * @param signature - The hex-encoded HMAC-SHA256 signature to verify. + * @param secret - The shared secret key. + * @throws {WebhookVerificationError} if the signature does not match. */ export function verifyWebhookSignatureOrThrow( payload: string, diff --git a/test/webhookVerify.test.ts b/test/webhookVerify.test.ts index 38b2053..7c3310b 100644 --- a/test/webhookVerify.test.ts +++ b/test/webhookVerify.test.ts @@ -1,87 +1,72 @@ +import { describe, it, expect } from "vitest"; import { verifyWebhookSignature, - verifyWebhookSignatureOrThrow, WebhookVerificationError, + verifyWebhookSignatureOrThrow, } from "../src/webhooks/verify.js"; +import { createHmac } from "crypto"; -describe("verifyWebhookSignature", () => { - const secret = "my-secret-key"; - const payload = '{"event":"invoice.paid","data":{"id":"123"}}'; +const TEST_SECRET = "my-super-secret"; +function sign(payload: string, secret: string): string { + return createHmac("sha256", secret).update(payload).digest("hex"); +} + +describe("verifyWebhookSignature", () => { it("returns true for a valid signature", () => { - const crypto = require("crypto"); - const expected = crypto - .createHmac("sha256", secret) - .update(payload) - .digest("hex"); - expect(verifyWebhookSignature(payload, expected, secret)).toBe(true); + const payload = '{"event":"invoice.created"}'; + const signature = sign(payload, TEST_SECRET); + expect(verifyWebhookSignature(payload, signature, TEST_SECRET)).toBe(true); }); it("returns false for a wrong secret", () => { - const crypto = require("crypto"); - const expected = crypto - .createHmac("sha256", secret) - .update(payload) - .digest("hex"); - expect(verifyWebhookSignature(payload, expected, "wrong-secret")).toBe( - false, - ); + const payload = '{"event":"invoice.created"}'; + const signature = sign(payload, TEST_SECRET); + expect(verifyWebhookSignature(payload, signature, "wrong-secret")).toBe(false); }); it("returns false for a tampered payload", () => { - const crypto = require("crypto"); - const expected = crypto - .createHmac("sha256", secret) - .update(payload) - .digest("hex"); - expect( - verifyWebhookSignature(payload + "x", expected, secret), - ).toBe(false); + const payload = '{"event":"invoice.created"}'; + const signature = sign(payload, TEST_SECRET); + expect(verifyWebhookSignature(payload + "x", signature, TEST_SECRET)).toBe(false); }); - it("returns false for a malformed signature (non-hex)", () => { - expect(verifyWebhookSignature(payload, "not-hex!", secret)).toBe(false); + it("returns false when signature length mismatches", () => { + const payload = '{"event":"invoice.created"}'; + expect(verifyWebhookSignature(payload, "abcd", TEST_SECRET)).toBe(false); }); - it("returns false when signature lengths differ", () => { - expect(verifyWebhookSignature(payload, "abcd", secret)).toBe(false); + it("returns false for malformed hex signature", () => { + const payload = '{"event":"invoice.created"}'; + expect(verifyWebhookSignature(payload, "not-hex!", TEST_SECRET)).toBe(false); }); - it("never throws", () => { - expect(() => - verifyWebhookSignature(payload, "bad-sig", secret), - ).not.toThrow(); + it("returns false for odd-length hex", () => { + const payload = '{"event":"invoice.created"}'; + expect(verifyWebhookSignature(payload, "abc", TEST_SECRET)).toBe(false); }); }); describe("verifyWebhookSignatureOrThrow", () => { - const secret = "my-secret-key"; - const payload = "test-payload"; - it("does not throw for a valid signature", () => { - const crypto = require("crypto"); - const sig = crypto - .createHmac("sha256", secret) - .update(payload) - .digest("hex"); - expect(() => verifyWebhookSignatureOrThrow(payload, sig, secret)).not.toThrow(); + const payload = '{"event":"invoice.created"}'; + const signature = sign(payload, TEST_SECRET); + expect(() => verifyWebhookSignatureOrThrow(payload, signature, TEST_SECRET)).not.toThrow(); }); it("throws WebhookVerificationError for an invalid signature", () => { + const payload = '{"event":"invoice.created"}'; + const signature = sign(payload, TEST_SECRET); expect(() => - verifyWebhookSignatureOrThrow(payload, "bad-sig", secret), + verifyWebhookSignatureOrThrow(payload, signature, "wrong-secret") ).toThrow(WebhookVerificationError); }); }); describe("WebhookVerificationError", () => { - it("has the correct name", () => { + it("has the correct name and message", () => { const err = new WebhookVerificationError(); expect(err.name).toBe("WebhookVerificationError"); - }); - - it("accepts a custom message", () => { - const err = new WebhookVerificationError("custom msg"); - expect(err.message).toBe("custom msg"); + expect(err.message).toBe("Webhook signature verification failed"); }); });