@hookflo/tern 3.0.0 → 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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
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
|
|
76
|
+
return normalized;
|
|
57
77
|
}
|
|
58
|
-
return headerValue;
|
|
78
|
+
return [headerValue.trim()].filter(Boolean);
|
|
59
79
|
}
|
|
60
80
|
}
|
|
61
81
|
extractTimestamp(request) {
|
|
@@ -114,10 +134,18 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
114
134
|
const customFormat = this.config.customConfig.payloadFormat;
|
|
115
135
|
if (customFormat.includes("{id}") && customFormat.includes("{timestamp}")) {
|
|
116
136
|
const id = request.headers.get(this.config.customConfig.idHeader || "x-webhook-id");
|
|
117
|
-
const timestamp = request.headers.get(this.config.timestampHeader ||
|
|
137
|
+
const timestamp = request.headers.get(this.config.timestampHeader ||
|
|
138
|
+
this.config.customConfig?.timestampHeader ||
|
|
139
|
+
"x-webhook-timestamp");
|
|
140
|
+
// if either is missing payload will be malformed — fail explicitly
|
|
141
|
+
if (!id || !timestamp) {
|
|
142
|
+
throw new Error(`Missing required headers for payload construction: ${!id ? this.config.customConfig.idHeader || "x-webhook-id" : ""} ${!timestamp
|
|
143
|
+
? this.config.timestampHeader || "x-webhook-timestamp"
|
|
144
|
+
: ""}`.trim());
|
|
145
|
+
}
|
|
118
146
|
return customFormat
|
|
119
|
-
.replace("{id}", id || "")
|
|
120
|
-
.replace("{timestamp}", timestamp || "")
|
|
147
|
+
.replace("{id}", id.trim() || "")
|
|
148
|
+
.replace("{timestamp}", timestamp.trim() || "")
|
|
121
149
|
.replace("{body}", rawBody);
|
|
122
150
|
}
|
|
123
151
|
if (customFormat.includes("{timestamp}") &&
|
|
@@ -187,7 +215,9 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
187
215
|
case "workos":
|
|
188
216
|
case "sentry":
|
|
189
217
|
case "sanity":
|
|
190
|
-
metadata.id = request.headers.get(this.config.idHeader ||
|
|
218
|
+
metadata.id = request.headers.get(this.config.idHeader ||
|
|
219
|
+
this.config.customConfig?.idHeader ||
|
|
220
|
+
"webhook-id");
|
|
191
221
|
break;
|
|
192
222
|
default:
|
|
193
223
|
if (this.config.idHeader) {
|
|
@@ -201,7 +231,10 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
201
231
|
exports.AlgorithmBasedVerifier = AlgorithmBasedVerifier;
|
|
202
232
|
class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
203
233
|
resolveSentryPayloadCandidates(rawBody, request) {
|
|
204
|
-
const candidates = [
|
|
234
|
+
const candidates = [
|
|
235
|
+
this.formatPayload(rawBody, request),
|
|
236
|
+
rawBody,
|
|
237
|
+
];
|
|
205
238
|
if (this.config.payloadFormat === "json-stringified") {
|
|
206
239
|
try {
|
|
207
240
|
const parsed = JSON.parse(rawBody);
|
|
@@ -226,8 +259,8 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
226
259
|
}
|
|
227
260
|
async verify(request) {
|
|
228
261
|
try {
|
|
229
|
-
const
|
|
230
|
-
if (
|
|
262
|
+
const signatures = this.extractSignatures(request);
|
|
263
|
+
if (signatures.length === 0) {
|
|
231
264
|
return {
|
|
232
265
|
isValid: false,
|
|
233
266
|
error: `Missing signature header: ${this.config.headerName}`,
|
|
@@ -257,14 +290,19 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
257
290
|
let isValid = false;
|
|
258
291
|
const algorithm = this.config.algorithm.replace("hmac-", "");
|
|
259
292
|
for (const payload of payloadCandidates) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
+
}
|
|
268
306
|
}
|
|
269
307
|
if (isValid) {
|
|
270
308
|
break;
|
|
@@ -399,8 +437,8 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
|
|
|
399
437
|
}
|
|
400
438
|
async verify(request) {
|
|
401
439
|
try {
|
|
402
|
-
const
|
|
403
|
-
if (
|
|
440
|
+
const signatures = this.extractSignatures(request);
|
|
441
|
+
if (signatures.length === 0) {
|
|
404
442
|
return {
|
|
405
443
|
isValid: false,
|
|
406
444
|
error: `Missing signature header: ${this.config.headerName}`,
|
|
@@ -453,23 +491,28 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
|
|
|
453
491
|
// fal.ai signature is hex encoded (not base64)
|
|
454
492
|
// other ED25519 platforms use base64 by default
|
|
455
493
|
const signatureEncoding = this.platform === "falai" ? "hex" : "base64";
|
|
456
|
-
const signatureBytes = new Uint8Array(Buffer.from(signature, signatureEncoding));
|
|
457
494
|
const payloadBytes = new Uint8Array(Buffer.from(payload, "utf-8"));
|
|
458
495
|
// try all keys — succeeds if any key validates
|
|
459
496
|
// this handles key rotation gracefully
|
|
460
497
|
let isValid = false;
|
|
461
|
-
for (const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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;
|
|
468
512
|
}
|
|
469
513
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
continue;
|
|
514
|
+
if (isValid) {
|
|
515
|
+
break;
|
|
473
516
|
}
|
|
474
517
|
}
|
|
475
518
|
if (!isValid) {
|
package/package.json
CHANGED