@hookflo/tern 2.2.7 → 2.2.10
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.
- package/README.md +14 -14
- package/dist/adapters/express.d.ts +3 -3
- package/dist/adapters/express.js +7 -2
- package/dist/adapters/shared.d.ts +2 -2
- package/dist/adapters/shared.js +55 -25
- package/dist/adapters/testExpress.d.ts +1 -0
- package/dist/adapters/testExpress.js +84 -0
- package/dist/platforms/algorithms.js +5 -6
- package/dist/verifiers/algorithms.d.ts +23 -3
- package/dist/verifiers/algorithms.js +222 -126
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -193,7 +193,7 @@ const result = await WebhookVerificationService.verify(request, stripeConfig);
|
|
|
193
193
|
|
|
194
194
|
## Supported Platforms
|
|
195
195
|
|
|
196
|
-
### Stripe
|
|
196
|
+
### Stripe OK Tested
|
|
197
197
|
- **Signature Format**: `t={timestamp},v1={signature}`
|
|
198
198
|
- **Algorithm**: HMAC-SHA256
|
|
199
199
|
- **Payload Format**: `{timestamp}.{body}`
|
|
@@ -209,20 +209,20 @@ const result = await WebhookVerificationService.verify(request, stripeConfig);
|
|
|
209
209
|
- **Payload Format**: `{id}.{timestamp}.{body}`
|
|
210
210
|
|
|
211
211
|
### Other Platforms
|
|
212
|
-
- **Dodo Payments**: HMAC-SHA256
|
|
213
|
-
- **Paddle**: HMAC-SHA256
|
|
214
|
-
- **Razorpay**: HMAC-SHA256
|
|
215
|
-
- **Lemon Squeezy**: HMAC-SHA256
|
|
216
|
-
- **Auth0**: HMAC-SHA256
|
|
217
|
-
- **WorkOS**: HMAC-SHA256 (`workos-signature`, `t/v1`)
|
|
218
|
-
- **WooCommerce**: HMAC-SHA256 (base64 signature)
|
|
219
|
-
- **ReplicateAI**: HMAC-SHA256 (Standard Webhooks style)
|
|
212
|
+
- **Dodo Payments**: HMAC-SHA256 OK Tested
|
|
213
|
+
- **Paddle**: HMAC-SHA256 OK Tested
|
|
214
|
+
- **Razorpay**: HMAC-SHA256 Pending
|
|
215
|
+
- **Lemon Squeezy**: HMAC-SHA256 OK Tested
|
|
216
|
+
- **Auth0**: HMAC-SHA256 Pending
|
|
217
|
+
- **WorkOS**: HMAC-SHA256 (`workos-signature`, `t/v1`) OK Tested
|
|
218
|
+
- **WooCommerce**: HMAC-SHA256 (base64 signature) Pending
|
|
219
|
+
- **ReplicateAI**: HMAC-SHA256 (Standard Webhooks style) OK Tested
|
|
220
220
|
- **fal.ai**: ED25519 (`x-fal-webhook-signature`)
|
|
221
|
-
- **Shopify**: HMAC-SHA256 (base64 signature)
|
|
222
|
-
- **Vercel**: HMAC-SHA256
|
|
223
|
-
- **Polar**: HMAC-SHA256
|
|
224
|
-
- **Supabase**: Token-based authentication
|
|
225
|
-
- **GitLab**: Token-based authentication
|
|
221
|
+
- **Shopify**: HMAC-SHA256 (base64 signature) OK Tested
|
|
222
|
+
- **Vercel**: HMAC-SHA256 Pending
|
|
223
|
+
- **Polar**: HMAC-SHA256 OK Tested
|
|
224
|
+
- **Supabase**: Token-based authentication Pending
|
|
225
|
+
- **GitLab**: Token-based authentication OK Tested
|
|
226
226
|
|
|
227
227
|
## Custom Platform Configuration
|
|
228
228
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { WebhookPlatform, WebhookVerificationResult, NormalizeOptions } from
|
|
2
|
-
import { MinimalNodeRequest } from
|
|
1
|
+
import { WebhookPlatform, WebhookVerificationResult, NormalizeOptions } from "../types";
|
|
2
|
+
import { MinimalNodeRequest } from "./shared";
|
|
3
3
|
export interface ExpressLikeResponse {
|
|
4
4
|
status: (code: number) => ExpressLikeResponse;
|
|
5
5
|
json: (payload: unknown) => unknown;
|
|
@@ -7,7 +7,7 @@ export interface ExpressLikeResponse {
|
|
|
7
7
|
export interface ExpressLikeRequest extends MinimalNodeRequest {
|
|
8
8
|
webhook?: WebhookVerificationResult;
|
|
9
9
|
}
|
|
10
|
-
export type ExpressLikeNext = () => void;
|
|
10
|
+
export type ExpressLikeNext = (err?: unknown) => void;
|
|
11
11
|
export interface ExpressWebhookMiddlewareOptions {
|
|
12
12
|
platform: WebhookPlatform;
|
|
13
13
|
secret: string;
|
package/dist/adapters/express.js
CHANGED
|
@@ -9,7 +9,12 @@ function createWebhookMiddleware(options) {
|
|
|
9
9
|
const webRequest = await (0, shared_1.toWebRequest)(req);
|
|
10
10
|
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(webRequest, options.platform, options.secret, options.toleranceInSeconds, options.normalize);
|
|
11
11
|
if (!result.isValid) {
|
|
12
|
-
res.status(400).json({
|
|
12
|
+
res.status(400).json({
|
|
13
|
+
error: result.error,
|
|
14
|
+
errorCode: result.errorCode,
|
|
15
|
+
platform: result.platform,
|
|
16
|
+
metadata: result.metadata,
|
|
17
|
+
});
|
|
13
18
|
return;
|
|
14
19
|
}
|
|
15
20
|
req.webhook = result;
|
|
@@ -17,7 +22,7 @@ function createWebhookMiddleware(options) {
|
|
|
17
22
|
}
|
|
18
23
|
catch (error) {
|
|
19
24
|
options.onError?.(error);
|
|
20
|
-
|
|
25
|
+
next(error);
|
|
21
26
|
}
|
|
22
27
|
};
|
|
23
28
|
}
|
|
@@ -6,8 +6,8 @@ export interface MinimalNodeRequest {
|
|
|
6
6
|
get?: (name: string) => string | undefined;
|
|
7
7
|
originalUrl?: string;
|
|
8
8
|
url?: string;
|
|
9
|
-
on?: (event: string, cb: (chunk?:
|
|
9
|
+
on?: (event: string, cb: (chunk?: unknown) => void) => void;
|
|
10
10
|
}
|
|
11
|
-
export declare function extractRawBody(request: MinimalNodeRequest): Promise<
|
|
11
|
+
export declare function extractRawBody(request: MinimalNodeRequest): Promise<Uint8Array>;
|
|
12
12
|
export declare function toHeadersInit(headers: Record<string, string | string[] | undefined>): HeadersInit;
|
|
13
13
|
export declare function toWebRequest(request: MinimalNodeRequest): Promise<Request>;
|
package/dist/adapters/shared.js
CHANGED
|
@@ -6,39 +6,65 @@ exports.toWebRequest = toWebRequest;
|
|
|
6
6
|
function getHeaderValue(headers, name) {
|
|
7
7
|
const value = headers[name.toLowerCase()] ?? headers[name];
|
|
8
8
|
if (Array.isArray(value)) {
|
|
9
|
-
return value.join(
|
|
9
|
+
return value.join(",");
|
|
10
10
|
}
|
|
11
11
|
return value;
|
|
12
12
|
}
|
|
13
|
-
async function
|
|
14
|
-
if (!request.on)
|
|
15
|
-
return
|
|
16
|
-
}
|
|
13
|
+
async function readIncomingMessageBodyAsBuffer(request) {
|
|
14
|
+
if (!request.on)
|
|
15
|
+
return new Uint8Array(0);
|
|
17
16
|
const chunks = [];
|
|
18
17
|
return new Promise((resolve, reject) => {
|
|
19
|
-
request.on?.(
|
|
20
|
-
if (
|
|
18
|
+
request.on?.("data", (chunk) => {
|
|
19
|
+
if (chunk instanceof Uint8Array) {
|
|
21
20
|
chunks.push(chunk);
|
|
22
|
-
return;
|
|
23
21
|
}
|
|
24
|
-
|
|
22
|
+
else if (typeof chunk === "string") {
|
|
23
|
+
chunks.push(new TextEncoder().encode(chunk));
|
|
24
|
+
}
|
|
25
|
+
else if (chunk != null) {
|
|
26
|
+
try {
|
|
27
|
+
chunks.push(new TextEncoder().encode(String(chunk)));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// skip unreadable chunk silently
|
|
31
|
+
}
|
|
32
|
+
}
|
|
25
33
|
});
|
|
26
|
-
request.on?.(
|
|
27
|
-
|
|
34
|
+
request.on?.("end", () => {
|
|
35
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.byteLength, 0);
|
|
36
|
+
const result = new Uint8Array(totalLength);
|
|
37
|
+
let offset = 0;
|
|
38
|
+
for (const chunk of chunks) {
|
|
39
|
+
result.set(chunk, offset);
|
|
40
|
+
offset += chunk.byteLength;
|
|
41
|
+
}
|
|
42
|
+
resolve(result);
|
|
43
|
+
});
|
|
44
|
+
request.on?.("error", (err) => reject(err));
|
|
28
45
|
});
|
|
29
46
|
}
|
|
30
47
|
async function extractRawBody(request) {
|
|
31
48
|
const body = request.body;
|
|
32
|
-
if (
|
|
49
|
+
if (body instanceof Uint8Array) {
|
|
33
50
|
return body;
|
|
34
51
|
}
|
|
35
|
-
if (
|
|
36
|
-
return
|
|
52
|
+
if (typeof body === "string") {
|
|
53
|
+
return new TextEncoder().encode(body);
|
|
37
54
|
}
|
|
38
|
-
if (body && typeof body ===
|
|
39
|
-
|
|
55
|
+
if (body !== null && body !== undefined && typeof body === "object") {
|
|
56
|
+
console.warn("[Tern] Warning: request body is already parsed as JSON. " +
|
|
57
|
+
"Signature verification may fail. " +
|
|
58
|
+
'Add express.raw({ type: "*/*" }) before Tern on webhook routes, ' +
|
|
59
|
+
"or register webhook routes before app.use(express.json()).");
|
|
60
|
+
return new TextEncoder().encode(JSON.stringify(body));
|
|
40
61
|
}
|
|
41
|
-
|
|
62
|
+
// nothing ran — read raw stream directly
|
|
63
|
+
return readIncomingMessageBodyAsBuffer(request);
|
|
64
|
+
}
|
|
65
|
+
// converts Uint8Array to a plain ArrayBuffer that all TS versions accept as BodyInit
|
|
66
|
+
function toArrayBuffer(uint8) {
|
|
67
|
+
return uint8.buffer.slice(uint8.byteOffset, uint8.byteOffset + uint8.byteLength);
|
|
42
68
|
}
|
|
43
69
|
function toHeadersInit(headers) {
|
|
44
70
|
const normalized = new Headers();
|
|
@@ -47,7 +73,7 @@ function toHeadersInit(headers) {
|
|
|
47
73
|
continue;
|
|
48
74
|
}
|
|
49
75
|
if (Array.isArray(value)) {
|
|
50
|
-
normalized.set(key, value.join(
|
|
76
|
+
normalized.set(key, value.join(","));
|
|
51
77
|
continue;
|
|
52
78
|
}
|
|
53
79
|
normalized.set(key, value);
|
|
@@ -55,13 +81,17 @@ function toHeadersInit(headers) {
|
|
|
55
81
|
return normalized;
|
|
56
82
|
}
|
|
57
83
|
async function toWebRequest(request) {
|
|
58
|
-
const protocol = request.protocol ||
|
|
59
|
-
const host = request.get?.(
|
|
60
|
-
|
|
84
|
+
const protocol = request.protocol || "https";
|
|
85
|
+
const host = request.get?.("host") ||
|
|
86
|
+
getHeaderValue(request.headers, "host") ||
|
|
87
|
+
"localhost";
|
|
88
|
+
const path = request.originalUrl || request.url || "/";
|
|
61
89
|
const rawBody = await extractRawBody(request);
|
|
62
|
-
|
|
63
|
-
method: request.method ||
|
|
90
|
+
const init = {
|
|
91
|
+
method: request.method || "POST",
|
|
64
92
|
headers: toHeadersInit(request.headers),
|
|
65
|
-
body: rawBody,
|
|
66
|
-
|
|
93
|
+
body: toArrayBuffer(rawBody),
|
|
94
|
+
duplex: "half",
|
|
95
|
+
};
|
|
96
|
+
return new Request(`${protocol}://${host}${path}`, init);
|
|
67
97
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
7
|
+
const shared_1 = require("./shared");
|
|
8
|
+
const __1 = __importDefault(require(".."));
|
|
9
|
+
const secret = "whsec_test_secret";
|
|
10
|
+
const payload = JSON.stringify({ type: "payment_intent.succeeded", data: { amount: 2000 } }) +
|
|
11
|
+
"\n";
|
|
12
|
+
function buildStripeSignature(body, secret) {
|
|
13
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
14
|
+
const signed = `${timestamp}.${body}`;
|
|
15
|
+
const hmac = crypto_1.default
|
|
16
|
+
.createHmac("sha256", secret.replace("whsec_", ""))
|
|
17
|
+
.update(signed)
|
|
18
|
+
.digest("hex");
|
|
19
|
+
return `t=${timestamp},v1=${hmac}`;
|
|
20
|
+
}
|
|
21
|
+
// ── Test 1: raw stream — no middleware ran ───────────────────
|
|
22
|
+
async function testRawStream() {
|
|
23
|
+
const sig = buildStripeSignature(payload, secret); // ✓ fixed: was missing secret arg
|
|
24
|
+
const chunks = [Buffer.from(payload, "utf8")];
|
|
25
|
+
const req = {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: {
|
|
28
|
+
"stripe-signature": sig,
|
|
29
|
+
"content-type": "application/json",
|
|
30
|
+
},
|
|
31
|
+
body: undefined,
|
|
32
|
+
on: (event, cb) => {
|
|
33
|
+
if (event === "data") {
|
|
34
|
+
for (const chunk of chunks)
|
|
35
|
+
cb(chunk);
|
|
36
|
+
}
|
|
37
|
+
if (event === "end") {
|
|
38
|
+
cb();
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
const webReq = await (0, shared_1.toWebRequest)(req);
|
|
43
|
+
const result = await __1.default.verifyWithPlatformConfig(webReq, "stripe", secret);
|
|
44
|
+
console.log("Test 1 Raw stream (no middleware):", result.isValid ? "✓ PASS" : `✗ FAIL — ${result.error}`);
|
|
45
|
+
}
|
|
46
|
+
// ── Test 2: Buffer body — express.raw() ran ──────────────────
|
|
47
|
+
async function testBufferBody() {
|
|
48
|
+
const sig = buildStripeSignature(payload, secret);
|
|
49
|
+
const req = {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: {
|
|
52
|
+
"content-type": "application/json",
|
|
53
|
+
"stripe-signature": sig,
|
|
54
|
+
},
|
|
55
|
+
body: Buffer.from(payload, "utf8"),
|
|
56
|
+
};
|
|
57
|
+
const webReq = await (0, shared_1.toWebRequest)(req);
|
|
58
|
+
const result = await __1.default.verifyWithPlatformConfig(webReq, "stripe", secret);
|
|
59
|
+
console.log("Test 2 Buffer body (express.raw):", result.isValid ? "✓ PASS" : `✗ FAIL — ${result.error}`);
|
|
60
|
+
}
|
|
61
|
+
// ── Test 3: parsed object — express.json() ran ───────────────
|
|
62
|
+
async function testParsedObject() {
|
|
63
|
+
// signature computed against raw payload including \n
|
|
64
|
+
const sig = buildStripeSignature(payload, secret);
|
|
65
|
+
const req = {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: {
|
|
68
|
+
"content-type": "application/json",
|
|
69
|
+
"stripe-signature": sig,
|
|
70
|
+
},
|
|
71
|
+
body: JSON.parse(payload), // \n gone, bytes lost
|
|
72
|
+
};
|
|
73
|
+
const webReq = await (0, shared_1.toWebRequest)(req);
|
|
74
|
+
const result = await __1.default.verifyWithPlatformConfig(webReq, "stripe", secret);
|
|
75
|
+
// should FAIL — \n was in original signature but lost after JSON.parse
|
|
76
|
+
console.log("Test 3 Parsed object (express.json):", !result.isValid ? "✓ FAILS AS EXPECTED" : "✗ SHOULD HAVE FAILED");
|
|
77
|
+
}
|
|
78
|
+
async function run() {
|
|
79
|
+
await testRawStream(); // no middleware → should PASS
|
|
80
|
+
await testBufferBody(); // express.raw() → should PASS
|
|
81
|
+
await testParsedObject(); // express.json() → should FAIL as expected
|
|
82
|
+
console.log("\nDone. Test 3 failing is correct behavior.");
|
|
83
|
+
}
|
|
84
|
+
run();
|
|
@@ -119,12 +119,12 @@ exports.platformAlgorithmConfigs = {
|
|
|
119
119
|
platform: "supabase",
|
|
120
120
|
signatureConfig: {
|
|
121
121
|
algorithm: "custom",
|
|
122
|
-
headerName: "x-
|
|
122
|
+
headerName: "x-supabase-token",
|
|
123
123
|
headerFormat: "raw",
|
|
124
124
|
payloadFormat: "raw",
|
|
125
125
|
customConfig: {
|
|
126
126
|
type: "token-based",
|
|
127
|
-
idHeader: "x-
|
|
127
|
+
idHeader: "x-supabase-token",
|
|
128
128
|
},
|
|
129
129
|
},
|
|
130
130
|
description: "Supabase webhooks use token-based authentication",
|
|
@@ -244,14 +244,13 @@ exports.platformAlgorithmConfigs = {
|
|
|
244
244
|
headerFormat: "raw",
|
|
245
245
|
payloadFormat: "custom",
|
|
246
246
|
customConfig: {
|
|
247
|
-
requestIdHeader: "x-fal-request-id",
|
|
248
|
-
userIdHeader: "x-fal-user-id",
|
|
247
|
+
requestIdHeader: "x-fal-webhook-request-id",
|
|
248
|
+
userIdHeader: "x-fal-webhook-user-id",
|
|
249
249
|
timestampHeader: "x-fal-webhook-timestamp",
|
|
250
|
-
kidHeader: "x-fal-webhook-key-id",
|
|
251
250
|
jwksUrl: "https://rest.alpha.fal.ai/.well-known/jwks.json",
|
|
252
251
|
},
|
|
253
252
|
},
|
|
254
|
-
description: "fal.ai webhooks use ED25519 with
|
|
253
|
+
description: "fal.ai webhooks use ED25519 with JWKS key verification. No secret required — pass empty string.",
|
|
255
254
|
},
|
|
256
255
|
custom: {
|
|
257
256
|
platform: "custom",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { WebhookVerifier } from
|
|
2
|
-
import { WebhookVerificationResult, SignatureConfig, WebhookPlatform } from
|
|
1
|
+
import { WebhookVerifier } from "./base";
|
|
2
|
+
import { WebhookVerificationResult, SignatureConfig, WebhookPlatform } from "../types";
|
|
3
3
|
export declare abstract class AlgorithmBasedVerifier extends WebhookVerifier {
|
|
4
4
|
protected config: SignatureConfig;
|
|
5
5
|
protected platform: WebhookPlatform;
|
|
@@ -20,7 +20,27 @@ export declare class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
20
20
|
verify(request: Request): Promise<WebhookVerificationResult>;
|
|
21
21
|
}
|
|
22
22
|
export declare class Ed25519Verifier extends AlgorithmBasedVerifier {
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Fetch all public keys from JWKS endpoint.
|
|
25
|
+
* Returns array of PEM strings — tries all keys during verification
|
|
26
|
+
* so key rotation works transparently.
|
|
27
|
+
* Caches for 24 hours per fal.ai docs recommendation.
|
|
28
|
+
*/
|
|
29
|
+
private fetchJWKSKeys;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve public keys to use for verification.
|
|
32
|
+
* Priority:
|
|
33
|
+
* 1. Explicit publicKey in config
|
|
34
|
+
* 2. Non-empty secret passed directly (user-provided PEM)
|
|
35
|
+
* 3. JWKS URL in config (fal.ai — fetches all keys, tries each)
|
|
36
|
+
*/
|
|
37
|
+
private resolvePublicKeys;
|
|
38
|
+
/**
|
|
39
|
+
* Build fal.ai specific payload string.
|
|
40
|
+
*
|
|
41
|
+
* Per fal.ai docs, message is newline-separated (NOT dot-separated):
|
|
42
|
+
* {request-id}\n{user-id}\n{timestamp}\n{sha256hex(body)}
|
|
43
|
+
*/
|
|
24
44
|
private buildFalPayload;
|
|
25
45
|
verify(request: Request): Promise<WebhookVerificationResult>;
|
|
26
46
|
}
|
|
@@ -4,7 +4,9 @@ exports.HMACSHA512Verifier = exports.HMACSHA1Verifier = exports.HMACSHA256Verifi
|
|
|
4
4
|
exports.createAlgorithmVerifier = createAlgorithmVerifier;
|
|
5
5
|
const crypto_1 = require("crypto");
|
|
6
6
|
const base_1 = require("./base");
|
|
7
|
+
// Cache stores array of PEM keys per JWKS URL
|
|
7
8
|
const ed25519KeyCache = new Map();
|
|
9
|
+
const JWKS_CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours per fal.ai docs
|
|
8
10
|
class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
9
11
|
constructor(secret, config, platform, toleranceInSeconds = 300) {
|
|
10
12
|
super(secret, toleranceInSeconds);
|
|
@@ -18,7 +20,7 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
18
20
|
const trimmed = part.trim();
|
|
19
21
|
if (!trimmed)
|
|
20
22
|
continue;
|
|
21
|
-
const equalIndex = trimmed.indexOf(
|
|
23
|
+
const equalIndex = trimmed.indexOf("=");
|
|
22
24
|
if (equalIndex === -1)
|
|
23
25
|
continue;
|
|
24
26
|
const key = trimmed.slice(0, equalIndex).trim();
|
|
@@ -34,20 +36,20 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
34
36
|
if (!headerValue)
|
|
35
37
|
return null;
|
|
36
38
|
switch (this.config.headerFormat) {
|
|
37
|
-
case
|
|
38
|
-
return headerValue;
|
|
39
|
-
case
|
|
39
|
+
case "prefixed":
|
|
40
|
+
return headerValue.trim();
|
|
41
|
+
case "comma-separated": {
|
|
40
42
|
const sigMap = this.parseDelimitedHeader(headerValue);
|
|
41
|
-
const signatureKey = this.config.customConfig?.signatureKey ||
|
|
43
|
+
const signatureKey = this.config.customConfig?.signatureKey || "v1";
|
|
42
44
|
return sigMap[signatureKey] || sigMap.signature || sigMap.v1 || null;
|
|
43
45
|
}
|
|
44
|
-
case
|
|
46
|
+
case "raw":
|
|
45
47
|
default:
|
|
46
|
-
if (this.config.customConfig?.signatureFormat?.includes(
|
|
47
|
-
const signatures = headerValue.split(
|
|
48
|
+
if (this.config.customConfig?.signatureFormat?.includes("v1=")) {
|
|
49
|
+
const signatures = headerValue.split(" ");
|
|
48
50
|
for (const sig of signatures) {
|
|
49
|
-
const [version, signature] = sig.split(
|
|
50
|
-
if (version ===
|
|
51
|
+
const [version, signature] = sig.split(",");
|
|
52
|
+
if (version === "v1") {
|
|
51
53
|
return signature;
|
|
52
54
|
}
|
|
53
55
|
}
|
|
@@ -63,37 +65,37 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
63
65
|
if (!timestampHeader)
|
|
64
66
|
return null;
|
|
65
67
|
switch (this.config.timestampFormat) {
|
|
66
|
-
case
|
|
68
|
+
case "unix":
|
|
67
69
|
return parseInt(timestampHeader, 10);
|
|
68
|
-
case
|
|
70
|
+
case "iso":
|
|
69
71
|
return Math.floor(new Date(timestampHeader).getTime() / 1000);
|
|
70
|
-
case
|
|
72
|
+
case "custom":
|
|
71
73
|
return parseInt(timestampHeader, 10);
|
|
72
74
|
default:
|
|
73
75
|
return parseInt(timestampHeader, 10);
|
|
74
76
|
}
|
|
75
77
|
}
|
|
76
78
|
extractTimestampFromSignature(request) {
|
|
77
|
-
if (this.config.headerFormat !==
|
|
79
|
+
if (this.config.headerFormat !== "comma-separated") {
|
|
78
80
|
return null;
|
|
79
81
|
}
|
|
80
82
|
const headerValue = request.headers.get(this.config.headerName);
|
|
81
83
|
if (!headerValue)
|
|
82
84
|
return null;
|
|
83
85
|
const sigMap = this.parseDelimitedHeader(headerValue);
|
|
84
|
-
const timestampKey = this.config.customConfig?.timestampKey ||
|
|
86
|
+
const timestampKey = this.config.customConfig?.timestampKey || "t";
|
|
85
87
|
return sigMap[timestampKey] ? parseInt(sigMap[timestampKey], 10) : null;
|
|
86
88
|
}
|
|
87
89
|
formatPayload(rawBody, request) {
|
|
88
90
|
switch (this.config.payloadFormat) {
|
|
89
|
-
case
|
|
90
|
-
const timestamp = this.extractTimestampFromSignature(request)
|
|
91
|
-
|
|
91
|
+
case "timestamped": {
|
|
92
|
+
const timestamp = this.extractTimestampFromSignature(request) ||
|
|
93
|
+
this.extractTimestamp(request);
|
|
92
94
|
return timestamp ? `${timestamp}.${rawBody}` : rawBody;
|
|
93
95
|
}
|
|
94
|
-
case
|
|
96
|
+
case "custom":
|
|
95
97
|
return this.formatCustomPayload(rawBody, request);
|
|
96
|
-
case
|
|
98
|
+
case "raw":
|
|
97
99
|
default:
|
|
98
100
|
return rawBody;
|
|
99
101
|
}
|
|
@@ -103,78 +105,80 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
103
105
|
return rawBody;
|
|
104
106
|
}
|
|
105
107
|
const customFormat = this.config.customConfig.payloadFormat;
|
|
106
|
-
if (customFormat.includes(
|
|
107
|
-
const id = request.headers.get(this.config.customConfig.idHeader ||
|
|
108
|
-
const timestamp = request.headers.get(this.config.timestampHeader ||
|
|
108
|
+
if (customFormat.includes("{id}") && customFormat.includes("{timestamp}")) {
|
|
109
|
+
const id = request.headers.get(this.config.customConfig.idHeader || "x-webhook-id");
|
|
110
|
+
const timestamp = request.headers.get(this.config.timestampHeader || "x-webhook-timestamp");
|
|
109
111
|
return customFormat
|
|
110
|
-
.replace(
|
|
111
|
-
.replace(
|
|
112
|
-
.replace(
|
|
112
|
+
.replace("{id}", id || "")
|
|
113
|
+
.replace("{timestamp}", timestamp || "")
|
|
114
|
+
.replace("{body}", rawBody);
|
|
113
115
|
}
|
|
114
|
-
if (customFormat.includes(
|
|
115
|
-
|
|
116
|
-
const timestamp = this.extractTimestampFromSignature(request)
|
|
117
|
-
|
|
116
|
+
if (customFormat.includes("{timestamp}") &&
|
|
117
|
+
customFormat.includes("{body}")) {
|
|
118
|
+
const timestamp = this.extractTimestampFromSignature(request) ||
|
|
119
|
+
this.extractTimestamp(request);
|
|
118
120
|
return customFormat
|
|
119
|
-
.replace(
|
|
120
|
-
.replace(
|
|
121
|
+
.replace("{timestamp}", timestamp?.toString() || "")
|
|
122
|
+
.replace("{body}", rawBody);
|
|
121
123
|
}
|
|
122
124
|
return rawBody;
|
|
123
125
|
}
|
|
124
|
-
verifyHMAC(payload, signature, algorithm =
|
|
126
|
+
verifyHMAC(payload, signature, algorithm = "sha256") {
|
|
125
127
|
const hmac = (0, crypto_1.createHmac)(algorithm, this.secret);
|
|
126
128
|
hmac.update(payload);
|
|
127
|
-
const expectedSignature = hmac.digest(
|
|
129
|
+
const expectedSignature = hmac.digest("hex");
|
|
128
130
|
return this.safeCompare(signature, expectedSignature);
|
|
129
131
|
}
|
|
130
|
-
verifyHMACWithPrefix(payload, signature, algorithm =
|
|
132
|
+
verifyHMACWithPrefix(payload, signature, algorithm = "sha256") {
|
|
131
133
|
const hmac = (0, crypto_1.createHmac)(algorithm, this.secret);
|
|
132
134
|
hmac.update(payload);
|
|
133
|
-
const expectedSignature = `${this.config.prefix ||
|
|
135
|
+
const expectedSignature = `${this.config.prefix || ""}${hmac.digest("hex")}`;
|
|
134
136
|
return this.safeCompare(signature, expectedSignature);
|
|
135
137
|
}
|
|
136
|
-
verifyHMACWithBase64(payload, signature, algorithm =
|
|
137
|
-
const secretEncoding = this.config.customConfig?.secretEncoding ||
|
|
138
|
+
verifyHMACWithBase64(payload, signature, algorithm = "sha256") {
|
|
139
|
+
const secretEncoding = this.config.customConfig?.secretEncoding || "base64";
|
|
138
140
|
let secretMaterial = this.secret;
|
|
139
|
-
if (secretEncoding ===
|
|
140
|
-
const base64Secret = this.secret.includes(
|
|
141
|
-
? this.secret.split(
|
|
141
|
+
if (secretEncoding === "base64") {
|
|
142
|
+
const base64Secret = this.secret.includes("_")
|
|
143
|
+
? this.secret.split("_").slice(1).join("_")
|
|
142
144
|
: this.secret;
|
|
143
|
-
secretMaterial = new Uint8Array(Buffer.from(base64Secret,
|
|
145
|
+
secretMaterial = new Uint8Array(Buffer.from(base64Secret, "base64"));
|
|
144
146
|
}
|
|
147
|
+
// 'utf8', 'raw', or anything else → use secret as-is
|
|
145
148
|
const hmac = (0, crypto_1.createHmac)(algorithm, secretMaterial);
|
|
146
149
|
hmac.update(payload);
|
|
147
|
-
const expectedSignature = hmac.digest(
|
|
150
|
+
const expectedSignature = hmac.digest("base64");
|
|
148
151
|
return this.safeCompare(signature, expectedSignature);
|
|
149
152
|
}
|
|
150
153
|
extractMetadata(request) {
|
|
151
154
|
const metadata = {
|
|
152
155
|
algorithm: this.config.algorithm,
|
|
153
156
|
};
|
|
154
|
-
const timestamp = this.extractTimestamp(request) ||
|
|
157
|
+
const timestamp = this.extractTimestamp(request) ||
|
|
158
|
+
this.extractTimestampFromSignature(request);
|
|
155
159
|
if (timestamp) {
|
|
156
160
|
metadata.timestamp = timestamp.toString();
|
|
157
161
|
}
|
|
158
162
|
switch (this.platform) {
|
|
159
|
-
case
|
|
160
|
-
metadata.event = request.headers.get(
|
|
161
|
-
metadata.delivery = request.headers.get(
|
|
163
|
+
case "github":
|
|
164
|
+
metadata.event = request.headers.get("x-github-event");
|
|
165
|
+
metadata.delivery = request.headers.get("x-github-delivery");
|
|
162
166
|
break;
|
|
163
|
-
case
|
|
167
|
+
case "stripe": {
|
|
164
168
|
const headerValue = request.headers.get(this.config.headerName);
|
|
165
|
-
if (headerValue && this.config.headerFormat ===
|
|
169
|
+
if (headerValue && this.config.headerFormat === "comma-separated") {
|
|
166
170
|
const sigMap = this.parseDelimitedHeader(headerValue);
|
|
167
171
|
metadata.id = sigMap.id;
|
|
168
172
|
}
|
|
169
173
|
break;
|
|
170
174
|
}
|
|
171
|
-
case
|
|
172
|
-
case
|
|
173
|
-
case
|
|
174
|
-
metadata.id = request.headers.get(this.config.customConfig?.idHeader ||
|
|
175
|
+
case "clerk":
|
|
176
|
+
case "dodopayments":
|
|
177
|
+
case "replicateai":
|
|
178
|
+
metadata.id = request.headers.get(this.config.customConfig?.idHeader || "webhook-id");
|
|
175
179
|
break;
|
|
176
|
-
case
|
|
177
|
-
metadata.id = request.headers.get(this.config.customConfig?.idHeader ||
|
|
180
|
+
case "workos":
|
|
181
|
+
metadata.id = request.headers.get(this.config.customConfig?.idHeader || "webhook-id");
|
|
178
182
|
break;
|
|
179
183
|
default:
|
|
180
184
|
break;
|
|
@@ -191,13 +195,13 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
191
195
|
return {
|
|
192
196
|
isValid: false,
|
|
193
197
|
error: `Missing signature header: ${this.config.headerName}`,
|
|
194
|
-
errorCode:
|
|
198
|
+
errorCode: "MISSING_SIGNATURE",
|
|
195
199
|
platform: this.platform,
|
|
196
200
|
};
|
|
197
201
|
}
|
|
198
202
|
const rawBody = await request.text();
|
|
199
203
|
let timestamp = null;
|
|
200
|
-
if (this.config.headerFormat ===
|
|
204
|
+
if (this.config.headerFormat === "comma-separated") {
|
|
201
205
|
timestamp = this.extractTimestampFromSignature(request);
|
|
202
206
|
}
|
|
203
207
|
else {
|
|
@@ -206,18 +210,18 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
206
210
|
if (timestamp && !this.isTimestampValid(timestamp)) {
|
|
207
211
|
return {
|
|
208
212
|
isValid: false,
|
|
209
|
-
error:
|
|
210
|
-
errorCode:
|
|
213
|
+
error: "Webhook timestamp expired",
|
|
214
|
+
errorCode: "TIMESTAMP_EXPIRED",
|
|
211
215
|
platform: this.platform,
|
|
212
216
|
};
|
|
213
217
|
}
|
|
214
218
|
const payload = this.formatPayload(rawBody, request);
|
|
215
219
|
let isValid = false;
|
|
216
|
-
const algorithm = this.config.algorithm.replace(
|
|
217
|
-
if (this.config.customConfig?.encoding ===
|
|
220
|
+
const algorithm = this.config.algorithm.replace("hmac-", "");
|
|
221
|
+
if (this.config.customConfig?.encoding === "base64") {
|
|
218
222
|
isValid = this.verifyHMACWithBase64(payload, signature, algorithm);
|
|
219
223
|
}
|
|
220
|
-
else if (this.config.headerFormat ===
|
|
224
|
+
else if (this.config.headerFormat === "prefixed") {
|
|
221
225
|
isValid = this.verifyHMACWithPrefix(payload, signature, algorithm);
|
|
222
226
|
}
|
|
223
227
|
else {
|
|
@@ -226,8 +230,8 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
226
230
|
if (!isValid) {
|
|
227
231
|
return {
|
|
228
232
|
isValid: false,
|
|
229
|
-
error:
|
|
230
|
-
errorCode:
|
|
233
|
+
error: "Invalid signature",
|
|
234
|
+
errorCode: "INVALID_SIGNATURE",
|
|
231
235
|
platform: this.platform,
|
|
232
236
|
};
|
|
233
237
|
}
|
|
@@ -238,19 +242,18 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
238
242
|
catch (e) {
|
|
239
243
|
parsedPayload = rawBody;
|
|
240
244
|
}
|
|
241
|
-
const metadata = this.extractMetadata(request);
|
|
242
245
|
return {
|
|
243
246
|
isValid: true,
|
|
244
247
|
platform: this.platform,
|
|
245
248
|
payload: parsedPayload,
|
|
246
|
-
metadata,
|
|
249
|
+
metadata: this.extractMetadata(request),
|
|
247
250
|
};
|
|
248
251
|
}
|
|
249
252
|
catch (error) {
|
|
250
253
|
return {
|
|
251
254
|
isValid: false,
|
|
252
255
|
error: `${this.platform} verification error: ${error.message}`,
|
|
253
|
-
errorCode:
|
|
256
|
+
errorCode: "VERIFICATION_ERROR",
|
|
254
257
|
platform: this.platform,
|
|
255
258
|
};
|
|
256
259
|
}
|
|
@@ -258,47 +261,91 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
258
261
|
}
|
|
259
262
|
exports.GenericHMACVerifier = GenericHMACVerifier;
|
|
260
263
|
class Ed25519Verifier extends AlgorithmBasedVerifier {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
264
|
+
/**
|
|
265
|
+
* Fetch all public keys from JWKS endpoint.
|
|
266
|
+
* Returns array of PEM strings — tries all keys during verification
|
|
267
|
+
* so key rotation works transparently.
|
|
268
|
+
* Caches for 24 hours per fal.ai docs recommendation.
|
|
269
|
+
*/
|
|
270
|
+
async fetchJWKSKeys(jwksUrl) {
|
|
271
|
+
const cached = ed25519KeyCache.get(jwksUrl);
|
|
272
|
+
if (cached && Date.now() < cached.expiresAt) {
|
|
273
|
+
return cached.pems;
|
|
265
274
|
}
|
|
266
|
-
|
|
267
|
-
|
|
275
|
+
const response = await fetch(jwksUrl);
|
|
276
|
+
if (!response.ok) {
|
|
277
|
+
throw new Error(`Failed to fetch JWKS from ${jwksUrl}: ${response.status}`);
|
|
268
278
|
}
|
|
269
|
-
const
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
return null;
|
|
279
|
+
const body = (await response.json());
|
|
280
|
+
const keys = body.keys || [];
|
|
281
|
+
if (keys.length === 0) {
|
|
282
|
+
throw new Error("No keys found in JWKS response");
|
|
274
283
|
}
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
284
|
+
const pems = [];
|
|
285
|
+
for (const key of keys) {
|
|
286
|
+
try {
|
|
287
|
+
const keyObject = (0, crypto_1.createPublicKey)({ key, format: "jwk" });
|
|
288
|
+
const pem = keyObject
|
|
289
|
+
.export({ type: "spki", format: "pem" })
|
|
290
|
+
.toString();
|
|
291
|
+
pems.push(pem);
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
// skip malformed keys, continue with rest
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
278
297
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
return null;
|
|
298
|
+
if (pems.length === 0) {
|
|
299
|
+
throw new Error("No valid ED25519 keys found in JWKS");
|
|
282
300
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
301
|
+
ed25519KeyCache.set(jwksUrl, {
|
|
302
|
+
pems,
|
|
303
|
+
expiresAt: Date.now() + JWKS_CACHE_TTL,
|
|
304
|
+
});
|
|
305
|
+
return pems;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Resolve public keys to use for verification.
|
|
309
|
+
* Priority:
|
|
310
|
+
* 1. Explicit publicKey in config
|
|
311
|
+
* 2. Non-empty secret passed directly (user-provided PEM)
|
|
312
|
+
* 3. JWKS URL in config (fal.ai — fetches all keys, tries each)
|
|
313
|
+
*/
|
|
314
|
+
async resolvePublicKeys(request) {
|
|
315
|
+
// 1. explicit public key in config
|
|
316
|
+
const configPublicKey = this.config.customConfig?.publicKey;
|
|
317
|
+
if (configPublicKey) {
|
|
318
|
+
return [configPublicKey];
|
|
287
319
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
320
|
+
// 2. non-empty secret = user passed PEM directly
|
|
321
|
+
// empty string = fal.ai pattern (no shared secret, use JWKS)
|
|
322
|
+
if (this.secret &&
|
|
323
|
+
this.secret.trim().length > 0 &&
|
|
324
|
+
this.platform !== "falai") {
|
|
325
|
+
return [this.secret];
|
|
326
|
+
}
|
|
327
|
+
// 3. fetch from JWKS — handles fal.ai and any other JWKS-based platform
|
|
328
|
+
const jwksUrl = this.config.customConfig?.jwksUrl;
|
|
329
|
+
if (!jwksUrl) {
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
return this.fetchJWKSKeys(jwksUrl);
|
|
292
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* Build fal.ai specific payload string.
|
|
336
|
+
*
|
|
337
|
+
* Per fal.ai docs, message is newline-separated (NOT dot-separated):
|
|
338
|
+
* {request-id}\n{user-id}\n{timestamp}\n{sha256hex(body)}
|
|
339
|
+
*/
|
|
293
340
|
buildFalPayload(rawBody, request) {
|
|
294
|
-
const requestIdHeader = this.config.customConfig?.requestIdHeader ||
|
|
295
|
-
const userIdHeader = this.config.customConfig?.userIdHeader ||
|
|
296
|
-
const timestampHeader = this.config.customConfig?.timestampHeader ||
|
|
297
|
-
const requestId = request.headers.get(requestIdHeader) ||
|
|
298
|
-
const userId = request.headers.get(userIdHeader) ||
|
|
299
|
-
const timestamp = request.headers.get(timestampHeader) ||
|
|
300
|
-
const bodyHash = (0, crypto_1.createHash)(
|
|
301
|
-
return `${requestId}
|
|
341
|
+
const requestIdHeader = this.config.customConfig?.requestIdHeader || "x-fal-webhook-request-id";
|
|
342
|
+
const userIdHeader = this.config.customConfig?.userIdHeader || "x-fal-webhook-user-id";
|
|
343
|
+
const timestampHeader = this.config.customConfig?.timestampHeader || "x-fal-webhook-timestamp";
|
|
344
|
+
const requestId = request.headers.get(requestIdHeader) || "";
|
|
345
|
+
const userId = request.headers.get(userIdHeader) || "";
|
|
346
|
+
const timestamp = request.headers.get(timestampHeader) || "";
|
|
347
|
+
const bodyHash = (0, crypto_1.createHash)("sha256").update(rawBody).digest("hex");
|
|
348
|
+
return `${requestId}\n${userId}\n${timestamp}\n${bodyHash}`;
|
|
302
349
|
}
|
|
303
350
|
async verify(request) {
|
|
304
351
|
try {
|
|
@@ -307,32 +354,79 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
|
|
|
307
354
|
return {
|
|
308
355
|
isValid: false,
|
|
309
356
|
error: `Missing signature header: ${this.config.headerName}`,
|
|
310
|
-
errorCode:
|
|
357
|
+
errorCode: "MISSING_SIGNATURE",
|
|
311
358
|
platform: this.platform,
|
|
312
359
|
};
|
|
313
360
|
}
|
|
314
361
|
const rawBody = await request.text();
|
|
315
|
-
|
|
362
|
+
// fal.ai timestamp validation before signature check
|
|
363
|
+
if (this.platform === "falai") {
|
|
364
|
+
const timestampHeader = this.config.customConfig?.timestampHeader ||
|
|
365
|
+
"x-fal-webhook-timestamp";
|
|
366
|
+
const timestampStr = request.headers.get(timestampHeader);
|
|
367
|
+
if (timestampStr) {
|
|
368
|
+
const timestamp = parseInt(timestampStr, 10);
|
|
369
|
+
if (!this.isTimestampValid(timestamp)) {
|
|
370
|
+
return {
|
|
371
|
+
isValid: false,
|
|
372
|
+
error: "Webhook timestamp expired",
|
|
373
|
+
errorCode: "TIMESTAMP_EXPIRED",
|
|
374
|
+
platform: this.platform,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const payload = this.platform === "falai"
|
|
316
380
|
? this.buildFalPayload(rawBody, request)
|
|
317
381
|
: this.formatPayload(rawBody, request);
|
|
318
|
-
|
|
319
|
-
|
|
382
|
+
// resolve all public keys
|
|
383
|
+
let publicKeys;
|
|
384
|
+
try {
|
|
385
|
+
publicKeys = await this.resolvePublicKeys(request);
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
return {
|
|
389
|
+
isValid: false,
|
|
390
|
+
error: `Failed to resolve public keys: ${error.message}`,
|
|
391
|
+
errorCode: "VERIFICATION_ERROR",
|
|
392
|
+
platform: this.platform,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
if (publicKeys.length === 0) {
|
|
320
396
|
return {
|
|
321
397
|
isValid: false,
|
|
322
|
-
error:
|
|
323
|
-
errorCode:
|
|
398
|
+
error: "No public keys available for ED25519 verification",
|
|
399
|
+
errorCode: "VERIFICATION_ERROR",
|
|
324
400
|
platform: this.platform,
|
|
325
401
|
};
|
|
326
402
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const
|
|
330
|
-
const
|
|
403
|
+
// fal.ai signature is hex encoded (not base64)
|
|
404
|
+
// other ED25519 platforms use base64 by default
|
|
405
|
+
const signatureEncoding = this.platform === "falai" ? "hex" : "base64";
|
|
406
|
+
const signatureBytes = new Uint8Array(Buffer.from(signature, signatureEncoding));
|
|
407
|
+
const payloadBytes = new Uint8Array(Buffer.from(payload, "utf-8"));
|
|
408
|
+
// try all keys — succeeds if any key validates
|
|
409
|
+
// this handles key rotation gracefully
|
|
410
|
+
let isValid = false;
|
|
411
|
+
for (const pem of publicKeys) {
|
|
412
|
+
try {
|
|
413
|
+
const keyObject = (0, crypto_1.createPublicKey)(pem);
|
|
414
|
+
const valid = (0, crypto_1.verify)(null, payloadBytes, keyObject, signatureBytes);
|
|
415
|
+
if (valid) {
|
|
416
|
+
isValid = true;
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
// this key failed — try next
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
331
425
|
if (!isValid) {
|
|
332
426
|
return {
|
|
333
427
|
isValid: false,
|
|
334
|
-
error:
|
|
335
|
-
errorCode:
|
|
428
|
+
error: "Invalid signature",
|
|
429
|
+
errorCode: "INVALID_SIGNATURE",
|
|
336
430
|
platform: this.platform,
|
|
337
431
|
};
|
|
338
432
|
}
|
|
@@ -349,9 +443,11 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
|
|
|
349
443
|
payload: parsedPayload,
|
|
350
444
|
metadata: {
|
|
351
445
|
algorithm: this.config.algorithm,
|
|
352
|
-
requestId: request.headers.get(this.config.customConfig?.requestIdHeader ||
|
|
353
|
-
|
|
354
|
-
|
|
446
|
+
requestId: request.headers.get(this.config.customConfig?.requestIdHeader ||
|
|
447
|
+
"x-fal-webhook-request-id"),
|
|
448
|
+
userId: request.headers.get(this.config.customConfig?.userIdHeader || "x-fal-webhook-user-id"),
|
|
449
|
+
timestamp: request.headers.get(this.config.customConfig?.timestampHeader ||
|
|
450
|
+
"x-fal-webhook-timestamp") || undefined,
|
|
355
451
|
},
|
|
356
452
|
};
|
|
357
453
|
}
|
|
@@ -359,7 +455,7 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
|
|
|
359
455
|
return {
|
|
360
456
|
isValid: false,
|
|
361
457
|
error: `${this.platform} verification error: ${error.message}`,
|
|
362
|
-
errorCode:
|
|
458
|
+
errorCode: "VERIFICATION_ERROR",
|
|
363
459
|
platform: this.platform,
|
|
364
460
|
};
|
|
365
461
|
}
|
|
@@ -367,33 +463,33 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
|
|
|
367
463
|
}
|
|
368
464
|
exports.Ed25519Verifier = Ed25519Verifier;
|
|
369
465
|
class HMACSHA256Verifier extends GenericHMACVerifier {
|
|
370
|
-
constructor(secret, config, platform =
|
|
466
|
+
constructor(secret, config, platform = "unknown", toleranceInSeconds = 300) {
|
|
371
467
|
super(secret, config, platform, toleranceInSeconds);
|
|
372
468
|
}
|
|
373
469
|
}
|
|
374
470
|
exports.HMACSHA256Verifier = HMACSHA256Verifier;
|
|
375
471
|
class HMACSHA1Verifier extends GenericHMACVerifier {
|
|
376
|
-
constructor(secret, config, platform =
|
|
472
|
+
constructor(secret, config, platform = "unknown", toleranceInSeconds = 300) {
|
|
377
473
|
super(secret, config, platform, toleranceInSeconds);
|
|
378
474
|
}
|
|
379
475
|
}
|
|
380
476
|
exports.HMACSHA1Verifier = HMACSHA1Verifier;
|
|
381
477
|
class HMACSHA512Verifier extends GenericHMACVerifier {
|
|
382
|
-
constructor(secret, config, platform =
|
|
478
|
+
constructor(secret, config, platform = "unknown", toleranceInSeconds = 300) {
|
|
383
479
|
super(secret, config, platform, toleranceInSeconds);
|
|
384
480
|
}
|
|
385
481
|
}
|
|
386
482
|
exports.HMACSHA512Verifier = HMACSHA512Verifier;
|
|
387
|
-
function createAlgorithmVerifier(secret, config, platform =
|
|
483
|
+
function createAlgorithmVerifier(secret, config, platform = "unknown", toleranceInSeconds = 300) {
|
|
388
484
|
switch (config.algorithm) {
|
|
389
|
-
case
|
|
390
|
-
case
|
|
391
|
-
case
|
|
485
|
+
case "hmac-sha256":
|
|
486
|
+
case "hmac-sha1":
|
|
487
|
+
case "hmac-sha512":
|
|
392
488
|
return new GenericHMACVerifier(secret, config, platform, toleranceInSeconds);
|
|
393
|
-
case
|
|
489
|
+
case "ed25519":
|
|
394
490
|
return new Ed25519Verifier(secret, config, platform, toleranceInSeconds);
|
|
395
|
-
case
|
|
396
|
-
case
|
|
491
|
+
case "rsa-sha256":
|
|
492
|
+
case "custom":
|
|
397
493
|
throw new Error(`Algorithm ${config.algorithm} not yet implemented`);
|
|
398
494
|
default:
|
|
399
495
|
throw new Error(`Unknown algorithm: ${config.algorithm}`);
|
package/package.json
CHANGED