@hookflo/tern 3.0.1 → 3.0.3
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
|
|
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,47 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
31
31
|
}
|
|
32
32
|
return values;
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
extractSignatures(request) {
|
|
35
35
|
const headerValue = request.headers.get(this.config.headerName);
|
|
36
36
|
if (!headerValue)
|
|
37
|
-
return
|
|
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]
|
|
44
|
+
return [sigMap[signatureKey], sigMap.signature, sigMap.v1].filter((value) => Boolean(value));
|
|
45
45
|
}
|
|
46
46
|
case "raw":
|
|
47
47
|
default:
|
|
48
48
|
if (this.config.customConfig?.signatureFormat?.includes("v1=")) {
|
|
49
|
-
const signatures = headerValue
|
|
49
|
+
const signatures = headerValue
|
|
50
|
+
.trim()
|
|
51
|
+
.split(/\s+/)
|
|
52
|
+
.map((entry) => entry.trim())
|
|
53
|
+
.filter(Boolean);
|
|
54
|
+
const normalized = [];
|
|
50
55
|
for (const sig of signatures) {
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
56
|
+
// Standard Webhooks format is usually "v1,<signature>"
|
|
57
|
+
if (sig.startsWith("v1,")) {
|
|
58
|
+
const [, value] = sig.split(",", 2);
|
|
59
|
+
if (value) {
|
|
60
|
+
normalized.push(value.trim());
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// Accept "v1=<signature>" variants used by some providers/docs.
|
|
65
|
+
if (sig.startsWith("v1=")) {
|
|
66
|
+
const [, value] = sig.split("=", 2);
|
|
67
|
+
if (value) {
|
|
68
|
+
normalized.push(value.trim());
|
|
69
|
+
}
|
|
54
70
|
}
|
|
55
71
|
}
|
|
56
|
-
return
|
|
72
|
+
return normalized;
|
|
57
73
|
}
|
|
58
|
-
return headerValue;
|
|
74
|
+
return [headerValue.trim()].filter(Boolean);
|
|
59
75
|
}
|
|
60
76
|
}
|
|
61
77
|
extractTimestamp(request) {
|
|
@@ -154,12 +170,13 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
154
170
|
const secretEncoding = this.config.customConfig?.secretEncoding || "base64";
|
|
155
171
|
let secretMaterial = this.secret;
|
|
156
172
|
if (secretEncoding === "base64") {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
173
|
+
let rawSecret = this.secret;
|
|
174
|
+
const lastUnderscore = rawSecret.lastIndexOf("_");
|
|
175
|
+
if (lastUnderscore !== -1) {
|
|
176
|
+
rawSecret = rawSecret.slice(lastUnderscore + 1);
|
|
177
|
+
}
|
|
178
|
+
secretMaterial = new Uint8Array(Buffer.from(rawSecret, "base64"));
|
|
161
179
|
}
|
|
162
|
-
// 'utf8', 'raw', or anything else → use secret as-is
|
|
163
180
|
const hmac = (0, crypto_1.createHmac)(algorithm, secretMaterial);
|
|
164
181
|
hmac.update(payload);
|
|
165
182
|
const expectedSignature = hmac.digest("base64");
|
|
@@ -239,8 +256,8 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
239
256
|
}
|
|
240
257
|
async verify(request) {
|
|
241
258
|
try {
|
|
242
|
-
const
|
|
243
|
-
if (
|
|
259
|
+
const signatures = this.extractSignatures(request);
|
|
260
|
+
if (signatures.length === 0) {
|
|
244
261
|
return {
|
|
245
262
|
isValid: false,
|
|
246
263
|
error: `Missing signature header: ${this.config.headerName}`,
|
|
@@ -270,14 +287,19 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
270
287
|
let isValid = false;
|
|
271
288
|
const algorithm = this.config.algorithm.replace("hmac-", "");
|
|
272
289
|
for (const payload of payloadCandidates) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
290
|
+
for (const signature of signatures) {
|
|
291
|
+
if (this.config.customConfig?.encoding === "base64") {
|
|
292
|
+
isValid = this.verifyHMACWithBase64(payload, signature, algorithm);
|
|
293
|
+
}
|
|
294
|
+
else if (this.config.headerFormat === "prefixed") {
|
|
295
|
+
isValid = this.verifyHMACWithPrefix(payload, signature, algorithm);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
isValid = this.verifyHMAC(payload, signature, algorithm);
|
|
299
|
+
}
|
|
300
|
+
if (isValid) {
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
281
303
|
}
|
|
282
304
|
if (isValid) {
|
|
283
305
|
break;
|
|
@@ -412,8 +434,8 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
|
|
|
412
434
|
}
|
|
413
435
|
async verify(request) {
|
|
414
436
|
try {
|
|
415
|
-
const
|
|
416
|
-
if (
|
|
437
|
+
const signatures = this.extractSignatures(request);
|
|
438
|
+
if (signatures.length === 0) {
|
|
417
439
|
return {
|
|
418
440
|
isValid: false,
|
|
419
441
|
error: `Missing signature header: ${this.config.headerName}`,
|
|
@@ -466,23 +488,28 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
|
|
|
466
488
|
// fal.ai signature is hex encoded (not base64)
|
|
467
489
|
// other ED25519 platforms use base64 by default
|
|
468
490
|
const signatureEncoding = this.platform === "falai" ? "hex" : "base64";
|
|
469
|
-
const signatureBytes = new Uint8Array(Buffer.from(signature, signatureEncoding));
|
|
470
491
|
const payloadBytes = new Uint8Array(Buffer.from(payload, "utf-8"));
|
|
471
492
|
// try all keys — succeeds if any key validates
|
|
472
493
|
// this handles key rotation gracefully
|
|
473
494
|
let isValid = false;
|
|
474
|
-
for (const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
495
|
+
for (const signature of signatures) {
|
|
496
|
+
const signatureBytes = new Uint8Array(Buffer.from(signature, signatureEncoding));
|
|
497
|
+
for (const pem of publicKeys) {
|
|
498
|
+
try {
|
|
499
|
+
const keyObject = (0, crypto_1.createPublicKey)(pem);
|
|
500
|
+
const valid = (0, crypto_1.verify)(null, payloadBytes, keyObject, signatureBytes);
|
|
501
|
+
if (valid) {
|
|
502
|
+
isValid = true;
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
// this key failed — try next
|
|
508
|
+
continue;
|
|
481
509
|
}
|
|
482
510
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
continue;
|
|
511
|
+
if (isValid) {
|
|
512
|
+
break;
|
|
486
513
|
}
|
|
487
514
|
}
|
|
488
515
|
if (!isValid) {
|
package/package.json
CHANGED