@hookflo/tern 3.0.1 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ export declare abstract class AlgorithmBasedVerifier extends WebhookVerifier {
6
6
  constructor(secret: string, config: SignatureConfig, platform: WebhookPlatform, toleranceInSeconds?: number);
7
7
  abstract verify(request: Request): Promise<WebhookVerificationResult>;
8
8
  protected parseDelimitedHeader(headerValue: string): Record<string, string>;
9
- protected extractSignature(request: Request): string | null;
9
+ protected extractSignatures(request: Request): string[];
10
10
  protected extractTimestamp(request: Request): number | null;
11
11
  protected extractTimestampFromSignature(request: Request): number | null;
12
12
  protected formatPayload(rawBody: string, request: Request): string;
@@ -31,31 +31,51 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
31
31
  }
32
32
  return values;
33
33
  }
34
- extractSignature(request) {
34
+ extractSignatures(request) {
35
35
  const headerValue = request.headers.get(this.config.headerName);
36
36
  if (!headerValue)
37
- return null;
37
+ return [];
38
38
  switch (this.config.headerFormat) {
39
39
  case "prefixed":
40
- return headerValue.trim();
40
+ return [headerValue.trim()].filter(Boolean);
41
41
  case "comma-separated": {
42
42
  const sigMap = this.parseDelimitedHeader(headerValue);
43
43
  const signatureKey = this.config.customConfig?.signatureKey || "v1";
44
- return sigMap[signatureKey] || sigMap.signature || sigMap.v1 || null;
44
+ return [
45
+ sigMap[signatureKey],
46
+ sigMap.signature,
47
+ sigMap.v1,
48
+ ].filter((value) => Boolean(value));
45
49
  }
46
50
  case "raw":
47
51
  default:
48
52
  if (this.config.customConfig?.signatureFormat?.includes("v1=")) {
49
- const signatures = headerValue.split(" ");
53
+ const signatures = headerValue
54
+ .trim()
55
+ .split(/\s+/)
56
+ .map((entry) => entry.trim())
57
+ .filter(Boolean);
58
+ const normalized = [];
50
59
  for (const sig of signatures) {
51
- const [version, signature] = sig.split(",");
52
- if (version === "v1") {
53
- return signature;
60
+ // Standard Webhooks format is usually "v1,<signature>"
61
+ if (sig.startsWith("v1,")) {
62
+ const [, value] = sig.split(",", 2);
63
+ if (value) {
64
+ normalized.push(value.trim());
65
+ }
66
+ continue;
67
+ }
68
+ // Accept "v1=<signature>" variants used by some providers/docs.
69
+ if (sig.startsWith("v1=")) {
70
+ const [, value] = sig.split("=", 2);
71
+ if (value) {
72
+ normalized.push(value.trim());
73
+ }
54
74
  }
55
75
  }
56
- return null;
76
+ return normalized;
57
77
  }
58
- return headerValue;
78
+ return [headerValue.trim()].filter(Boolean);
59
79
  }
60
80
  }
61
81
  extractTimestamp(request) {
@@ -239,8 +259,8 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
239
259
  }
240
260
  async verify(request) {
241
261
  try {
242
- const signature = this.extractSignature(request);
243
- if (!signature) {
262
+ const signatures = this.extractSignatures(request);
263
+ if (signatures.length === 0) {
244
264
  return {
245
265
  isValid: false,
246
266
  error: `Missing signature header: ${this.config.headerName}`,
@@ -270,14 +290,19 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
270
290
  let isValid = false;
271
291
  const algorithm = this.config.algorithm.replace("hmac-", "");
272
292
  for (const payload of payloadCandidates) {
273
- if (this.config.customConfig?.encoding === "base64") {
274
- isValid = this.verifyHMACWithBase64(payload, signature, algorithm);
275
- }
276
- else if (this.config.headerFormat === "prefixed") {
277
- isValid = this.verifyHMACWithPrefix(payload, signature, algorithm);
278
- }
279
- else {
280
- isValid = this.verifyHMAC(payload, signature, algorithm);
293
+ for (const signature of signatures) {
294
+ if (this.config.customConfig?.encoding === "base64") {
295
+ isValid = this.verifyHMACWithBase64(payload, signature, algorithm);
296
+ }
297
+ else if (this.config.headerFormat === "prefixed") {
298
+ isValid = this.verifyHMACWithPrefix(payload, signature, algorithm);
299
+ }
300
+ else {
301
+ isValid = this.verifyHMAC(payload, signature, algorithm);
302
+ }
303
+ if (isValid) {
304
+ break;
305
+ }
281
306
  }
282
307
  if (isValid) {
283
308
  break;
@@ -412,8 +437,8 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
412
437
  }
413
438
  async verify(request) {
414
439
  try {
415
- const signature = this.extractSignature(request);
416
- if (!signature) {
440
+ const signatures = this.extractSignatures(request);
441
+ if (signatures.length === 0) {
417
442
  return {
418
443
  isValid: false,
419
444
  error: `Missing signature header: ${this.config.headerName}`,
@@ -466,23 +491,28 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
466
491
  // fal.ai signature is hex encoded (not base64)
467
492
  // other ED25519 platforms use base64 by default
468
493
  const signatureEncoding = this.platform === "falai" ? "hex" : "base64";
469
- const signatureBytes = new Uint8Array(Buffer.from(signature, signatureEncoding));
470
494
  const payloadBytes = new Uint8Array(Buffer.from(payload, "utf-8"));
471
495
  // try all keys — succeeds if any key validates
472
496
  // this handles key rotation gracefully
473
497
  let isValid = false;
474
- for (const pem of publicKeys) {
475
- try {
476
- const keyObject = (0, crypto_1.createPublicKey)(pem);
477
- const valid = (0, crypto_1.verify)(null, payloadBytes, keyObject, signatureBytes);
478
- if (valid) {
479
- isValid = true;
480
- break;
498
+ for (const signature of signatures) {
499
+ const signatureBytes = new Uint8Array(Buffer.from(signature, signatureEncoding));
500
+ for (const pem of publicKeys) {
501
+ try {
502
+ const keyObject = (0, crypto_1.createPublicKey)(pem);
503
+ const valid = (0, crypto_1.verify)(null, payloadBytes, keyObject, signatureBytes);
504
+ if (valid) {
505
+ isValid = true;
506
+ break;
507
+ }
508
+ }
509
+ catch {
510
+ // this key failed — try next
511
+ continue;
481
512
  }
482
513
  }
483
- catch {
484
- // this key failed — try next
485
- continue;
514
+ if (isValid) {
515
+ break;
486
516
  }
487
517
  }
488
518
  if (!isValid) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hookflo/tern",
3
- "version": "3.0.1",
3
+ "version": "3.0.2",
4
4
  "description": "A robust, scalable webhook verification framework supporting multiple platforms and signature algorithms",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",