@hookflo/tern 4.0.3 → 4.1.0
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/dist/adapters/express.d.ts +1 -0
- package/dist/adapters/express.js +10 -1
- package/dist/adapters/shared.d.ts +1 -0
- package/dist/adapters/shared.js +10 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +30 -16
- package/dist/upstash/queue.js +11 -16
- package/dist/verifiers/algorithms.d.ts +1 -0
- package/dist/verifiers/algorithms.js +58 -13
- package/package.json +2 -2
- package/dist/adapters/testExpress.d.ts +0 -1
- package/dist/adapters/testExpress.js +0 -83
- package/dist/test-compiled.d.ts +0 -1
- package/dist/test-compiled.js +0 -4
- package/dist/test.d.ts +0 -2
- package/dist/test.js +0 -668
|
@@ -16,5 +16,6 @@ export interface ExpressWebhookMiddlewareOptions {
|
|
|
16
16
|
normalize?: boolean | NormalizeOptions;
|
|
17
17
|
queue?: QueueOption;
|
|
18
18
|
onError?: (error: Error) => void;
|
|
19
|
+
strictRawBody?: boolean;
|
|
19
20
|
}
|
|
20
21
|
export declare function createWebhookMiddleware(options: ExpressWebhookMiddlewareOptions): (req: ExpressLikeRequest, res: ExpressLikeResponse, next: ExpressLikeNext) => Promise<void>;
|
package/dist/adapters/express.js
CHANGED
|
@@ -7,6 +7,15 @@ const shared_1 = require("./shared");
|
|
|
7
7
|
function createWebhookMiddleware(options) {
|
|
8
8
|
return async (req, res, next) => {
|
|
9
9
|
try {
|
|
10
|
+
const strictRawBody = options.strictRawBody ?? true;
|
|
11
|
+
if (strictRawBody && (0, shared_1.hasParsedBody)(req)) {
|
|
12
|
+
res.status(400).json({
|
|
13
|
+
error: 'Webhook request body must be raw bytes. Configure express.raw({ type: "*/*" }) before this middleware.',
|
|
14
|
+
errorCode: 'VERIFICATION_ERROR',
|
|
15
|
+
platform: options.platform,
|
|
16
|
+
});
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
10
19
|
const webRequest = await (0, shared_1.toWebRequest)(req);
|
|
11
20
|
if (options.queue) {
|
|
12
21
|
const queueConfig = (0, queue_1.resolveQueueConfig)(options.queue);
|
|
@@ -17,7 +26,7 @@ function createWebhookMiddleware(options) {
|
|
|
17
26
|
toleranceInSeconds: options.toleranceInSeconds ?? 300,
|
|
18
27
|
});
|
|
19
28
|
const bodyText = await queueResponse.text();
|
|
20
|
-
let body
|
|
29
|
+
let body;
|
|
21
30
|
if (bodyText) {
|
|
22
31
|
try {
|
|
23
32
|
body = JSON.parse(bodyText);
|
|
@@ -8,6 +8,7 @@ export interface MinimalNodeRequest {
|
|
|
8
8
|
url?: string;
|
|
9
9
|
on?: (event: string, cb: (chunk?: unknown) => void) => void;
|
|
10
10
|
}
|
|
11
|
+
export declare function hasParsedBody(request: MinimalNodeRequest): boolean;
|
|
11
12
|
export declare function extractRawBody(request: MinimalNodeRequest): Promise<Uint8Array>;
|
|
12
13
|
export declare function toHeadersInit(headers: Record<string, string | string[] | undefined>): HeadersInit;
|
|
13
14
|
export declare function toWebRequest(request: MinimalNodeRequest): Promise<Request>;
|
package/dist/adapters/shared.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hasParsedBody = hasParsedBody;
|
|
3
4
|
exports.extractRawBody = extractRawBody;
|
|
4
5
|
exports.toHeadersInit = toHeadersInit;
|
|
5
6
|
exports.toWebRequest = toWebRequest;
|
|
@@ -10,6 +11,14 @@ function getHeaderValue(headers, name) {
|
|
|
10
11
|
}
|
|
11
12
|
return value;
|
|
12
13
|
}
|
|
14
|
+
function hasParsedBody(request) {
|
|
15
|
+
const { body } = request;
|
|
16
|
+
return body !== null
|
|
17
|
+
&& body !== undefined
|
|
18
|
+
&& typeof body === 'object'
|
|
19
|
+
&& !(body instanceof Uint8Array)
|
|
20
|
+
&& !(body instanceof ArrayBuffer);
|
|
21
|
+
}
|
|
13
22
|
async function readIncomingMessageBodyAsBuffer(request) {
|
|
14
23
|
if (!request.on)
|
|
15
24
|
return new Uint8Array(0);
|
|
@@ -52,7 +61,7 @@ async function extractRawBody(request) {
|
|
|
52
61
|
if (typeof body === 'string') {
|
|
53
62
|
return new TextEncoder().encode(body);
|
|
54
63
|
}
|
|
55
|
-
if (
|
|
64
|
+
if (hasParsedBody(request)) {
|
|
56
65
|
console.warn('[Tern] Warning: request body is already parsed as JSON. '
|
|
57
66
|
+ 'Signature verification may fail. '
|
|
58
67
|
+ 'Add express.raw({ type: "*/*" }) before Tern on webhook routes, '
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export declare class WebhookVerificationService {
|
|
|
8
8
|
static verifyWithPlatformConfig<TPayload = unknown>(request: Request, platform: WebhookPlatform, secret: string, toleranceInSeconds?: number, normalize?: boolean | NormalizeOptions): Promise<WebhookVerificationResult<TPayload>>;
|
|
9
9
|
static verifyAny<TPayload = unknown>(request: Request, secrets: MultiPlatformSecrets, toleranceInSeconds?: number, normalize?: boolean | NormalizeOptions): Promise<WebhookVerificationResult<TPayload>>;
|
|
10
10
|
private static resolveCanonicalEventId;
|
|
11
|
+
private static safeCompare;
|
|
11
12
|
private static pickString;
|
|
12
13
|
private static resolveRawEventId;
|
|
13
14
|
static detectPlatform(request: Request): WebhookPlatform;
|
package/dist/index.js
CHANGED
|
@@ -37,6 +37,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.getPlatformsByCategory = exports.getPlatformNormalizationCategory = exports.normalizePayload = exports.createCustomVerifier = exports.createAlgorithmVerifier = exports.validateSignatureConfig = exports.getPlatformsUsingAlgorithm = exports.platformUsesAlgorithm = exports.getPlatformAlgorithmConfig = exports.WebhookVerificationService = void 0;
|
|
40
|
+
const crypto_1 = require("crypto");
|
|
40
41
|
const algorithms_1 = require("./verifiers/algorithms");
|
|
41
42
|
const custom_algorithms_1 = require("./verifiers/custom-algorithms");
|
|
42
43
|
const algorithms_2 = require("./platforms/algorithms");
|
|
@@ -103,18 +104,25 @@ class WebhookVerificationService {
|
|
|
103
104
|
return this.verifyWithPlatformConfig(requestClone, detectedPlatform, secrets[detectedPlatform], toleranceInSeconds, normalize);
|
|
104
105
|
}
|
|
105
106
|
const failedAttempts = [];
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const result = await this.verifyWithPlatformConfig(requestClone,
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
107
|
+
const verificationResults = await Promise.all(Object.entries(secrets)
|
|
108
|
+
.filter(([, secret]) => Boolean(secret))
|
|
109
|
+
.map(async ([platform, secret]) => {
|
|
110
|
+
const normalizedPlatform = platform.toLowerCase();
|
|
111
|
+
const result = await this.verifyWithPlatformConfig(requestClone, normalizedPlatform, secret, toleranceInSeconds, normalize);
|
|
112
|
+
return {
|
|
113
|
+
platform: normalizedPlatform,
|
|
114
|
+
result,
|
|
115
|
+
};
|
|
116
|
+
}));
|
|
117
|
+
const firstValid = verificationResults.find((item) => item.result.isValid);
|
|
118
|
+
if (firstValid) {
|
|
119
|
+
return firstValid.result;
|
|
120
|
+
}
|
|
121
|
+
for (const item of verificationResults) {
|
|
114
122
|
failedAttempts.push({
|
|
115
|
-
platform: platform
|
|
116
|
-
error: result.error,
|
|
117
|
-
errorCode: result.errorCode,
|
|
123
|
+
platform: item.platform,
|
|
124
|
+
error: item.result.error,
|
|
125
|
+
errorCode: item.result.errorCode,
|
|
118
126
|
});
|
|
119
127
|
}
|
|
120
128
|
const details = failedAttempts
|
|
@@ -138,6 +146,12 @@ class WebhookVerificationService {
|
|
|
138
146
|
return null;
|
|
139
147
|
return `${platform}_${rawId}`;
|
|
140
148
|
}
|
|
149
|
+
static safeCompare(a, b) {
|
|
150
|
+
if (a.length !== b.length) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
return (0, crypto_1.timingSafeEqual)(new TextEncoder().encode(a), new TextEncoder().encode(b));
|
|
154
|
+
}
|
|
141
155
|
static pickString(...candidates) {
|
|
142
156
|
for (const candidate of candidates) {
|
|
143
157
|
if (candidate === undefined || candidate === null) {
|
|
@@ -178,7 +192,7 @@ class WebhookVerificationService {
|
|
|
178
192
|
case 'doppler':
|
|
179
193
|
return this.pickString(payload?.event?.id, metadata?.id) || null;
|
|
180
194
|
case 'sanity':
|
|
181
|
-
return this.pickString(payload?.transactionId, payload?._id) || null;
|
|
195
|
+
return this.pickString(payload?.transactionId, payload?.['_id']) || null;
|
|
182
196
|
case 'razorpay':
|
|
183
197
|
return this.pickString(payload?.payload?.payment?.entity?.id, payload?.payload?.order?.entity?.id, payload?.payload?.subscription?.entity?.id) || null;
|
|
184
198
|
case 'lemonsqueezy': {
|
|
@@ -199,7 +213,7 @@ class WebhookVerificationService {
|
|
|
199
213
|
}
|
|
200
214
|
}
|
|
201
215
|
static detectPlatform(request) {
|
|
202
|
-
const headers = request
|
|
216
|
+
const { headers } = request;
|
|
203
217
|
if (headers.has('stripe-signature'))
|
|
204
218
|
return 'stripe';
|
|
205
219
|
if (headers.has('x-hub-signature-256'))
|
|
@@ -269,8 +283,8 @@ class WebhookVerificationService {
|
|
|
269
283
|
platform: 'custom',
|
|
270
284
|
};
|
|
271
285
|
}
|
|
272
|
-
|
|
273
|
-
|
|
286
|
+
const isValid = this.safeCompare(idHeader, webhookId)
|
|
287
|
+
&& this.safeCompare(tokenHeader, webhookToken);
|
|
274
288
|
if (!isValid) {
|
|
275
289
|
return {
|
|
276
290
|
isValid: false,
|
|
@@ -291,7 +305,7 @@ class WebhookVerificationService {
|
|
|
291
305
|
isValid: true,
|
|
292
306
|
platform: 'custom',
|
|
293
307
|
payload: payload,
|
|
294
|
-
eventId: `
|
|
308
|
+
eventId: `custom_${idHeader}`,
|
|
295
309
|
metadata: {
|
|
296
310
|
id: idHeader,
|
|
297
311
|
algorithm: 'token-based',
|
package/dist/upstash/queue.js
CHANGED
|
@@ -39,16 +39,12 @@ exports.handleProcess = handleProcess;
|
|
|
39
39
|
exports.handleQueuedRequest = handleQueuedRequest;
|
|
40
40
|
const QStash = __importStar(require("@upstash/qstash"));
|
|
41
41
|
const index_1 = require("../index");
|
|
42
|
-
async function dynamicImport(modulePath) {
|
|
43
|
-
return new Function('modulePath', 'return import(modulePath);')(modulePath);
|
|
44
|
-
}
|
|
45
42
|
async function loadQStashModules() {
|
|
46
43
|
const optionalImports = await Promise.allSettled([
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
dynamicImport('@upstash/qstash/cloudflare'),
|
|
44
|
+
Promise.resolve().then(() => __importStar(require('@upstash/qstash'))),
|
|
45
|
+
Promise.resolve().then(() => __importStar(require('@upstash/qstash/nextjs'))),
|
|
46
|
+
Promise.resolve().then(() => __importStar(require('@upstash/qstash/nuxt'))),
|
|
47
|
+
Promise.resolve().then(() => __importStar(require('@upstash/qstash/cloudflare'))),
|
|
52
48
|
]);
|
|
53
49
|
return [
|
|
54
50
|
QStash,
|
|
@@ -92,25 +88,24 @@ function resolveNestedConstructor(modules, key) {
|
|
|
92
88
|
}
|
|
93
89
|
async function createQStashReceiver(queueConfig) {
|
|
94
90
|
const modules = await loadQStashModules();
|
|
95
|
-
const
|
|
91
|
+
const ReceiverCtor = resolveModuleExport(modules, 'Receiver')
|
|
96
92
|
?? resolveNestedConstructor(modules, 'Receiver');
|
|
97
|
-
if (typeof
|
|
93
|
+
if (typeof ReceiverCtor !== 'function') {
|
|
98
94
|
throw new Error('[tern] Incompatible @upstash/qstash version: Receiver export not found. Ensure @upstash/qstash is installed and up-to-date.');
|
|
99
95
|
}
|
|
100
|
-
return new
|
|
96
|
+
return new ReceiverCtor({
|
|
101
97
|
currentSigningKey: queueConfig.signingKey,
|
|
102
98
|
nextSigningKey: queueConfig.nextSigningKey,
|
|
103
99
|
});
|
|
104
100
|
}
|
|
105
101
|
async function createQStashClient(queueConfig) {
|
|
106
102
|
const modules = await loadQStashModules();
|
|
107
|
-
const
|
|
108
|
-
const resolvedClientExport = clientExport
|
|
103
|
+
const ClientCtor = resolveModuleExport(modules, 'Client')
|
|
109
104
|
?? resolveNestedConstructor(modules, 'Client');
|
|
110
|
-
if (typeof
|
|
105
|
+
if (typeof ClientCtor !== 'function') {
|
|
111
106
|
throw new Error('[tern] Incompatible @upstash/qstash version: Client export not found. Ensure @upstash/qstash is installed and up-to-date.');
|
|
112
107
|
}
|
|
113
|
-
return new
|
|
108
|
+
return new ClientCtor({ token: queueConfig.token });
|
|
114
109
|
}
|
|
115
110
|
function nonRetryableResponse(message, status = 489) {
|
|
116
111
|
return new Response(JSON.stringify({ error: message }), {
|
|
@@ -147,7 +142,7 @@ function toStableJson(value) {
|
|
|
147
142
|
}
|
|
148
143
|
async function resolveDeduplicationId(request, verificationResult) {
|
|
149
144
|
const payload = verificationResult.payload;
|
|
150
|
-
const headers = request
|
|
145
|
+
const { headers } = request;
|
|
151
146
|
const payloadId = typeof payload?.id === 'string' ? payload.id : null;
|
|
152
147
|
const payloadRequestId = typeof payload?.request_id === 'string' ? payload.request_id : null;
|
|
153
148
|
const nestedPayloadId = payload?.data && typeof payload.data === 'object' && typeof payload.data.id === 'string'
|
|
@@ -9,6 +9,7 @@ export declare abstract class AlgorithmBasedVerifier extends WebhookVerifier {
|
|
|
9
9
|
protected extractSignatures(request: Request): string[];
|
|
10
10
|
protected extractTimestamp(request: Request): number | null;
|
|
11
11
|
protected extractTimestampFromSignature(request: Request): number | null;
|
|
12
|
+
protected requiresTimestamp(): boolean;
|
|
12
13
|
protected formatPayload(rawBody: string, request: Request): string;
|
|
13
14
|
protected formatCustomPayload(rawBody: string, request: Request): string;
|
|
14
15
|
protected verifyHMAC(payload: string, signature: string, algorithm?: string): boolean;
|
|
@@ -102,6 +102,29 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
102
102
|
const timestampKey = this.config.customConfig?.timestampKey || "t";
|
|
103
103
|
return sigMap[timestampKey] ? parseInt(sigMap[timestampKey], 10) : null;
|
|
104
104
|
}
|
|
105
|
+
requiresTimestamp() {
|
|
106
|
+
// fal.ai timestamp is part of the signed payload itself — always required
|
|
107
|
+
if (this.platform === 'falai')
|
|
108
|
+
return true;
|
|
109
|
+
// These platforms have timestampHeader in config but timestamp
|
|
110
|
+
// is optional in their spec — validate only if present, never mandate
|
|
111
|
+
const optionalTimestampPlatforms = ['vercel', 'sentry', 'grafana'];
|
|
112
|
+
if (optionalTimestampPlatforms.includes(this.platform))
|
|
113
|
+
return false;
|
|
114
|
+
// For all other platforms: infer from config
|
|
115
|
+
if (this.config.timestampHeader)
|
|
116
|
+
return true;
|
|
117
|
+
if (this.config.payloadFormat === 'timestamped')
|
|
118
|
+
return true;
|
|
119
|
+
if (this.config.payloadFormat === 'custom'
|
|
120
|
+
&& this.config.customConfig?.payloadFormat
|
|
121
|
+
&& `${this.config.customConfig.payloadFormat}`.includes('{timestamp}'))
|
|
122
|
+
return true;
|
|
123
|
+
if (this.config.headerFormat === 'comma-separated'
|
|
124
|
+
&& this.config.customConfig?.timestampKey)
|
|
125
|
+
return true;
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
105
128
|
formatPayload(rawBody, request) {
|
|
106
129
|
switch (this.config.payloadFormat) {
|
|
107
130
|
case "timestamped": {
|
|
@@ -273,6 +296,14 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
273
296
|
else {
|
|
274
297
|
timestamp = this.extractTimestamp(request);
|
|
275
298
|
}
|
|
299
|
+
if (this.requiresTimestamp() && !timestamp) {
|
|
300
|
+
return {
|
|
301
|
+
isValid: false,
|
|
302
|
+
error: 'Missing required timestamp for webhook verification',
|
|
303
|
+
errorCode: 'MISSING_SIGNATURE',
|
|
304
|
+
platform: this.platform,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
276
307
|
if (timestamp && !this.isTimestampValid(timestamp)) {
|
|
277
308
|
return {
|
|
278
309
|
isValid: false,
|
|
@@ -357,7 +388,15 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
|
|
|
357
388
|
if (cached && Date.now() < cached.expiresAt) {
|
|
358
389
|
return cached.pems;
|
|
359
390
|
}
|
|
360
|
-
const
|
|
391
|
+
const abortController = new AbortController();
|
|
392
|
+
const timeout = setTimeout(() => abortController.abort(), 3000);
|
|
393
|
+
let response;
|
|
394
|
+
try {
|
|
395
|
+
response = await fetch(jwksUrl, { signal: abortController.signal });
|
|
396
|
+
}
|
|
397
|
+
finally {
|
|
398
|
+
clearTimeout(timeout);
|
|
399
|
+
}
|
|
361
400
|
if (!response.ok) {
|
|
362
401
|
throw new Error(`Failed to fetch JWKS from ${jwksUrl}: ${response.status}`);
|
|
363
402
|
}
|
|
@@ -396,7 +435,7 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
|
|
|
396
435
|
* 2. Non-empty secret passed directly (user-provided PEM)
|
|
397
436
|
* 3. JWKS URL in config (fal.ai — fetches all keys, tries each)
|
|
398
437
|
*/
|
|
399
|
-
async resolvePublicKeys(
|
|
438
|
+
async resolvePublicKeys() {
|
|
400
439
|
// 1. explicit public key in config
|
|
401
440
|
const configPublicKey = this.config.customConfig?.publicKey;
|
|
402
441
|
if (configPublicKey) {
|
|
@@ -449,16 +488,22 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
|
|
|
449
488
|
const timestampHeader = this.config.customConfig?.timestampHeader ||
|
|
450
489
|
"x-fal-webhook-timestamp";
|
|
451
490
|
const timestampStr = request.headers.get(timestampHeader);
|
|
452
|
-
if (timestampStr) {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
491
|
+
if (!timestampStr) {
|
|
492
|
+
return {
|
|
493
|
+
isValid: false,
|
|
494
|
+
error: 'Missing required timestamp for webhook verification',
|
|
495
|
+
errorCode: 'MISSING_SIGNATURE',
|
|
496
|
+
platform: this.platform,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
const timestamp = parseInt(timestampStr, 10);
|
|
500
|
+
if (!this.isTimestampValid(timestamp)) {
|
|
501
|
+
return {
|
|
502
|
+
isValid: false,
|
|
503
|
+
error: "Webhook timestamp expired",
|
|
504
|
+
errorCode: "TIMESTAMP_EXPIRED",
|
|
505
|
+
platform: this.platform,
|
|
506
|
+
};
|
|
462
507
|
}
|
|
463
508
|
}
|
|
464
509
|
const payload = this.platform === "falai"
|
|
@@ -467,7 +512,7 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
|
|
|
467
512
|
// resolve all public keys
|
|
468
513
|
let publicKeys;
|
|
469
514
|
try {
|
|
470
|
-
publicKeys = await this.resolvePublicKeys(
|
|
515
|
+
publicKeys = await this.resolvePublicKeys();
|
|
471
516
|
}
|
|
472
517
|
catch (error) {
|
|
473
518
|
return {
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hookflo/tern",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.1.0",
|
|
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",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc",
|
|
9
9
|
"dev": "tsc --watch",
|
|
10
|
-
"test": "node
|
|
10
|
+
"test": "ts-node src/test.ts",
|
|
11
11
|
"lint": "eslint src/**/*.ts",
|
|
12
12
|
"lint:fix": "eslint 'src/**/*.ts' --fix",
|
|
13
13
|
"format": "prettier --write src/**/*.ts",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,83 +0,0 @@
|
|
|
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 } })}\n`;
|
|
11
|
-
function buildStripeSignature(body, secret) {
|
|
12
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
13
|
-
const signed = `${timestamp}.${body}`;
|
|
14
|
-
const hmac = crypto_1.default
|
|
15
|
-
.createHmac('sha256', secret.replace('whsec_', ''))
|
|
16
|
-
.update(signed)
|
|
17
|
-
.digest('hex');
|
|
18
|
-
return `t=${timestamp},v1=${hmac}`;
|
|
19
|
-
}
|
|
20
|
-
// ── Test 1: raw stream — no middleware ran ───────────────────
|
|
21
|
-
async function testRawStream() {
|
|
22
|
-
const sig = buildStripeSignature(payload, secret); // ✓ fixed: was missing secret arg
|
|
23
|
-
const chunks = [Buffer.from(payload, 'utf8')];
|
|
24
|
-
const req = {
|
|
25
|
-
method: 'POST',
|
|
26
|
-
headers: {
|
|
27
|
-
'stripe-signature': sig,
|
|
28
|
-
'content-type': 'application/json',
|
|
29
|
-
},
|
|
30
|
-
body: undefined,
|
|
31
|
-
on: (event, cb) => {
|
|
32
|
-
if (event === 'data') {
|
|
33
|
-
for (const chunk of chunks)
|
|
34
|
-
cb(chunk);
|
|
35
|
-
}
|
|
36
|
-
if (event === 'end') {
|
|
37
|
-
cb();
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
const webReq = await (0, shared_1.toWebRequest)(req);
|
|
42
|
-
const result = await __1.default.verifyWithPlatformConfig(webReq, 'stripe', secret);
|
|
43
|
-
console.log('Test 1 Raw stream (no middleware):', result.isValid ? '✓ PASS' : `✗ FAIL — ${result.error}`);
|
|
44
|
-
}
|
|
45
|
-
// ── Test 2: Buffer body — express.raw() ran ──────────────────
|
|
46
|
-
async function testBufferBody() {
|
|
47
|
-
const sig = buildStripeSignature(payload, secret);
|
|
48
|
-
const req = {
|
|
49
|
-
method: 'POST',
|
|
50
|
-
headers: {
|
|
51
|
-
'content-type': 'application/json',
|
|
52
|
-
'stripe-signature': sig,
|
|
53
|
-
},
|
|
54
|
-
body: Buffer.from(payload, 'utf8'),
|
|
55
|
-
};
|
|
56
|
-
const webReq = await (0, shared_1.toWebRequest)(req);
|
|
57
|
-
const result = await __1.default.verifyWithPlatformConfig(webReq, 'stripe', secret);
|
|
58
|
-
console.log('Test 2 Buffer body (express.raw):', result.isValid ? '✓ PASS' : `✗ FAIL — ${result.error}`);
|
|
59
|
-
}
|
|
60
|
-
// ── Test 3: parsed object — express.json() ran ───────────────
|
|
61
|
-
async function testParsedObject() {
|
|
62
|
-
// signature computed against raw payload including \n
|
|
63
|
-
const sig = buildStripeSignature(payload, secret);
|
|
64
|
-
const req = {
|
|
65
|
-
method: 'POST',
|
|
66
|
-
headers: {
|
|
67
|
-
'content-type': 'application/json',
|
|
68
|
-
'stripe-signature': sig,
|
|
69
|
-
},
|
|
70
|
-
body: JSON.parse(payload), // \n gone, bytes lost
|
|
71
|
-
};
|
|
72
|
-
const webReq = await (0, shared_1.toWebRequest)(req);
|
|
73
|
-
const result = await __1.default.verifyWithPlatformConfig(webReq, 'stripe', secret);
|
|
74
|
-
// should FAIL — \n was in original signature but lost after JSON.parse
|
|
75
|
-
console.log('Test 3 Parsed object (express.json):', !result.isValid ? '✓ FAILS AS EXPECTED' : '✗ SHOULD HAVE FAILED');
|
|
76
|
-
}
|
|
77
|
-
async function run() {
|
|
78
|
-
await testRawStream(); // no middleware → should PASS
|
|
79
|
-
await testBufferBody(); // express.raw() → should PASS
|
|
80
|
-
await testParsedObject(); // express.json() → should FAIL as expected
|
|
81
|
-
console.log('\nDone. Test 3 failing is correct behavior.');
|
|
82
|
-
}
|
|
83
|
-
run();
|
package/dist/test-compiled.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/test-compiled.js
DELETED
package/dist/test.d.ts
DELETED
package/dist/test.js
DELETED
|
@@ -1,668 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.runTests = runTests;
|
|
4
|
-
const crypto_1 = require("crypto");
|
|
5
|
-
const index_1 = require("./index");
|
|
6
|
-
const testSecret = 'whsec_test_secret_key_12345';
|
|
7
|
-
const testBody = JSON.stringify({ event: 'test', data: { id: '123' } });
|
|
8
|
-
function createMockRequest(headers, body = testBody) {
|
|
9
|
-
return new Request('https://example.com/webhook', {
|
|
10
|
-
method: 'POST',
|
|
11
|
-
headers,
|
|
12
|
-
body,
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
function createStripeSignature(body, secret, timestamp) {
|
|
16
|
-
const signedPayload = `${timestamp}.${body}`;
|
|
17
|
-
const hmac = (0, crypto_1.createHmac)('sha256', secret);
|
|
18
|
-
hmac.update(signedPayload);
|
|
19
|
-
const signature = hmac.digest('hex');
|
|
20
|
-
return `t=${timestamp},v1=${signature}`;
|
|
21
|
-
}
|
|
22
|
-
function createGitHubSignature(body, secret) {
|
|
23
|
-
const hmac = (0, crypto_1.createHmac)('sha256', secret);
|
|
24
|
-
hmac.update(body);
|
|
25
|
-
return `sha256=${hmac.digest('hex')}`;
|
|
26
|
-
}
|
|
27
|
-
function createGitLabSignature(body, secret) {
|
|
28
|
-
// GitLab just compares the token in X-Gitlab-Token header
|
|
29
|
-
return secret;
|
|
30
|
-
}
|
|
31
|
-
function createClerkSignature(body, secret, id, timestamp) {
|
|
32
|
-
const signedContent = `${id}.${timestamp}.${body}`;
|
|
33
|
-
const secretBytes = new Uint8Array(Buffer.from(secret.split('_')[1], 'base64'));
|
|
34
|
-
const hmac = (0, crypto_1.createHmac)('sha256', secretBytes);
|
|
35
|
-
hmac.update(signedContent);
|
|
36
|
-
return `v1,${hmac.digest('base64')}`;
|
|
37
|
-
}
|
|
38
|
-
function createStandardWebhooksSignature(body, secret, id, timestamp) {
|
|
39
|
-
const signedContent = `${id}.${timestamp}.${body}`;
|
|
40
|
-
const base64Secret = secret.includes('_') ? secret.split('_')[1] : secret;
|
|
41
|
-
const secretBytes = new Uint8Array(Buffer.from(base64Secret, 'base64'));
|
|
42
|
-
const hmac = (0, crypto_1.createHmac)('sha256', secretBytes);
|
|
43
|
-
hmac.update(signedContent);
|
|
44
|
-
return `v1,${hmac.digest('base64')}`;
|
|
45
|
-
}
|
|
46
|
-
function createPaddleSignature(body, secret, timestamp) {
|
|
47
|
-
const signedPayload = `${timestamp}:${body}`;
|
|
48
|
-
const hmac = (0, crypto_1.createHmac)('sha256', secret);
|
|
49
|
-
hmac.update(signedPayload);
|
|
50
|
-
return `ts=${timestamp};h1=${hmac.digest('hex')}`;
|
|
51
|
-
}
|
|
52
|
-
function createShopifySignature(body, secret) {
|
|
53
|
-
const hmac = (0, crypto_1.createHmac)('sha256', secret);
|
|
54
|
-
hmac.update(body);
|
|
55
|
-
return hmac.digest('base64');
|
|
56
|
-
}
|
|
57
|
-
function createWooCommerceSignature(body, secret) {
|
|
58
|
-
const hmac = (0, crypto_1.createHmac)('sha256', secret);
|
|
59
|
-
hmac.update(body);
|
|
60
|
-
return hmac.digest('base64');
|
|
61
|
-
}
|
|
62
|
-
function createWorkOSSignature(body, secret, timestamp) {
|
|
63
|
-
const signedPayload = `${timestamp}.${body}`;
|
|
64
|
-
const hmac = (0, crypto_1.createHmac)('sha256', secret);
|
|
65
|
-
hmac.update(signedPayload);
|
|
66
|
-
return `t=${timestamp},v1=${hmac.digest('hex')}`;
|
|
67
|
-
}
|
|
68
|
-
function createSentrySignature(body, secret) {
|
|
69
|
-
const hmac = (0, crypto_1.createHmac)('sha256', secret);
|
|
70
|
-
hmac.update(JSON.stringify(JSON.parse(body)));
|
|
71
|
-
return hmac.digest('hex');
|
|
72
|
-
}
|
|
73
|
-
function createSentryIssueAlertSignature(issueAlertObject, secret) {
|
|
74
|
-
const hmac = (0, crypto_1.createHmac)('sha256', secret);
|
|
75
|
-
hmac.update(JSON.stringify(issueAlertObject));
|
|
76
|
-
return hmac.digest('hex');
|
|
77
|
-
}
|
|
78
|
-
function createGrafanaSignature(body, secret, timestamp) {
|
|
79
|
-
const payload = timestamp ? `${timestamp}.${body}` : body;
|
|
80
|
-
const hmac = (0, crypto_1.createHmac)('sha256', secret);
|
|
81
|
-
hmac.update(payload);
|
|
82
|
-
return hmac.digest('hex');
|
|
83
|
-
}
|
|
84
|
-
function createDopplerSignature(body, secret) {
|
|
85
|
-
const hmac = (0, crypto_1.createHmac)('sha256', secret);
|
|
86
|
-
hmac.update(body);
|
|
87
|
-
return `sha256=${hmac.digest('hex')}`;
|
|
88
|
-
}
|
|
89
|
-
function createSanitySignature(body, secret, timestamp) {
|
|
90
|
-
const hmac = (0, crypto_1.createHmac)('sha256', secret);
|
|
91
|
-
hmac.update(`${timestamp}.${body}`);
|
|
92
|
-
return `t=${timestamp},v1=${hmac.digest('hex')}`;
|
|
93
|
-
}
|
|
94
|
-
function createFalPayloadToSign(body, requestId, userId, timestamp) {
|
|
95
|
-
const bodyHash = (0, crypto_1.createHash)('sha256').update(body).digest('hex');
|
|
96
|
-
return `${requestId}\n${userId}\n${timestamp}\n${bodyHash}`;
|
|
97
|
-
}
|
|
98
|
-
async function runTests() {
|
|
99
|
-
console.log('🧪 Running Webhook Verification Tests...\n');
|
|
100
|
-
// Test 1: Stripe Webhook
|
|
101
|
-
console.log('1. Testing Stripe Webhook...');
|
|
102
|
-
try {
|
|
103
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
104
|
-
const stripeSignature = createStripeSignature(testBody, testSecret, timestamp);
|
|
105
|
-
const stripeRequest = createMockRequest({
|
|
106
|
-
'stripe-signature': stripeSignature,
|
|
107
|
-
'content-type': 'application/json',
|
|
108
|
-
});
|
|
109
|
-
const stripeResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(stripeRequest, 'stripe', testSecret);
|
|
110
|
-
const stripePassed = stripeResult.isValid && Boolean(stripeResult.eventId?.startsWith('stripe:'));
|
|
111
|
-
console.log(' ✅ Stripe:', stripePassed ? 'PASSED' : 'FAILED');
|
|
112
|
-
if (!stripeResult.isValid) {
|
|
113
|
-
console.log(' ❌ Error:', stripeResult.error);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
catch (error) {
|
|
117
|
-
console.log(' ❌ Stripe test failed:', error);
|
|
118
|
-
}
|
|
119
|
-
// Test 2: GitHub Webhook
|
|
120
|
-
console.log('\n2. Testing GitHub Webhook...');
|
|
121
|
-
try {
|
|
122
|
-
const githubSignature = createGitHubSignature(testBody, testSecret);
|
|
123
|
-
const githubRequest = createMockRequest({
|
|
124
|
-
'x-hub-signature-256': githubSignature,
|
|
125
|
-
'x-github-event': 'push',
|
|
126
|
-
'x-github-delivery': 'test-delivery-id',
|
|
127
|
-
'content-type': 'application/json',
|
|
128
|
-
});
|
|
129
|
-
const githubResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(githubRequest, 'github', testSecret);
|
|
130
|
-
console.log(' ✅ GitHub:', githubResult.isValid ? 'PASSED' : 'FAILED');
|
|
131
|
-
if (!githubResult.isValid) {
|
|
132
|
-
console.log(' ❌ Error:', githubResult.error);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
catch (error) {
|
|
136
|
-
console.log(' ❌ GitHub test failed:', error);
|
|
137
|
-
}
|
|
138
|
-
// Test 3: Clerk Webhook
|
|
139
|
-
console.log('\n3. Testing Clerk Webhook...');
|
|
140
|
-
try {
|
|
141
|
-
// Create a proper Clerk-style secret (whsec_ + base64 encoded secret)
|
|
142
|
-
const base64Secret = Buffer.from(testSecret).toString('base64');
|
|
143
|
-
const clerkSecret = `whsec_${base64Secret}`;
|
|
144
|
-
const id = 'test-id-123';
|
|
145
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
146
|
-
const clerkSignature = createClerkSignature(testBody, clerkSecret, id, timestamp);
|
|
147
|
-
const clerkRequest = createMockRequest({
|
|
148
|
-
'svix-signature': clerkSignature,
|
|
149
|
-
'svix-id': id,
|
|
150
|
-
'svix-timestamp': timestamp.toString(),
|
|
151
|
-
'content-type': 'application/json',
|
|
152
|
-
});
|
|
153
|
-
const clerkResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(clerkRequest, 'clerk', clerkSecret);
|
|
154
|
-
console.log(' ✅ Clerk:', clerkResult.isValid ? 'PASSED' : 'FAILED');
|
|
155
|
-
if (!clerkResult.isValid) {
|
|
156
|
-
console.log(' ❌ Error:', clerkResult.error);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
catch (error) {
|
|
160
|
-
console.log(' ❌ Clerk test failed:', error);
|
|
161
|
-
}
|
|
162
|
-
// Test 4: Standard HMAC-SHA256 (Generic)
|
|
163
|
-
console.log('\n4. Testing Standard HMAC-SHA256...');
|
|
164
|
-
try {
|
|
165
|
-
const hmac = (0, crypto_1.createHmac)('sha256', testSecret);
|
|
166
|
-
hmac.update(testBody);
|
|
167
|
-
const signature = hmac.digest('hex');
|
|
168
|
-
const genericRequest = createMockRequest({
|
|
169
|
-
'x-webhook-signature': signature,
|
|
170
|
-
'content-type': 'application/json',
|
|
171
|
-
});
|
|
172
|
-
const genericResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(genericRequest, 'unknown', testSecret);
|
|
173
|
-
console.log(' ✅ Generic HMAC-SHA256:', genericResult.isValid ? 'PASSED' : 'FAILED');
|
|
174
|
-
if (!genericResult.isValid) {
|
|
175
|
-
console.log(' ❌ Error:', genericResult.error);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
catch (error) {
|
|
179
|
-
console.log(' ❌ Generic test failed:', error);
|
|
180
|
-
}
|
|
181
|
-
// Test 4.5: Dodo Payments (Standard Webhooks / svix-style)
|
|
182
|
-
console.log('\n4.5. Testing Dodo Payments...');
|
|
183
|
-
try {
|
|
184
|
-
const webhookId = 'test-webhook-id-123';
|
|
185
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
186
|
-
// Create a proper secret format for Standard Webhooks (whsec_ + base64 encoded secret)
|
|
187
|
-
const base64Secret = Buffer.from(testSecret).toString('base64');
|
|
188
|
-
const dodoSecret = `whsec_${base64Secret}`;
|
|
189
|
-
// Create svix-style signature: {webhook-id}.{webhook-timestamp}.{payload}
|
|
190
|
-
const signedContent = `${webhookId}.${timestamp}.${testBody}`;
|
|
191
|
-
// Use the base64-decoded secret for HMAC (like the Standard Webhooks library)
|
|
192
|
-
const secretBytes = new Uint8Array(Buffer.from(base64Secret, 'base64'));
|
|
193
|
-
const hmac = (0, crypto_1.createHmac)('sha256', secretBytes);
|
|
194
|
-
hmac.update(signedContent);
|
|
195
|
-
const signature = `v1,${hmac.digest('base64')}`;
|
|
196
|
-
const dodoRequest = createMockRequest({
|
|
197
|
-
'webhook-signature': signature,
|
|
198
|
-
'webhook-id': webhookId,
|
|
199
|
-
'webhook-timestamp': timestamp.toString(),
|
|
200
|
-
'content-type': 'application/json',
|
|
201
|
-
});
|
|
202
|
-
const dodoResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(dodoRequest, 'dodopayments', dodoSecret);
|
|
203
|
-
console.log(' ✅ Dodo Payments:', dodoResult.isValid ? 'PASSED' : 'FAILED');
|
|
204
|
-
if (!dodoResult.isValid) {
|
|
205
|
-
console.log(' ❌ Error:', dodoResult.error);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
catch (error) {
|
|
209
|
-
console.log(' ❌ Dodo Payments test failed:', error);
|
|
210
|
-
}
|
|
211
|
-
// Test 5: Token-based authentication helper
|
|
212
|
-
console.log('\n5. Testing Token-based Authentication...');
|
|
213
|
-
try {
|
|
214
|
-
const webhookId = 'test-webhook-id';
|
|
215
|
-
const webhookToken = 'test-webhook-token';
|
|
216
|
-
const tokenRequest = createMockRequest({
|
|
217
|
-
'x-webhook-id': webhookId,
|
|
218
|
-
'x-webhook-token': webhookToken,
|
|
219
|
-
'content-type': 'application/json',
|
|
220
|
-
});
|
|
221
|
-
const tokenResult = await index_1.WebhookVerificationService.verifyTokenAuth(tokenRequest.clone(), webhookId, webhookToken);
|
|
222
|
-
const tokenAliasResult = await index_1.WebhookVerificationService.verifyTokenBased(tokenRequest.clone(), webhookId, webhookToken);
|
|
223
|
-
const tokenPassed = tokenResult.isValid && tokenAliasResult.isValid;
|
|
224
|
-
console.log(' ✅ Token-based:', tokenPassed ? 'PASSED' : 'FAILED');
|
|
225
|
-
if (!tokenResult.isValid) {
|
|
226
|
-
console.log(' ❌ Error:', tokenResult.error);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
catch (error) {
|
|
230
|
-
console.log(' ❌ Token-based test failed:', error);
|
|
231
|
-
}
|
|
232
|
-
// Test 5.5: Sentry webhook verification (standard + issue alert fallback)
|
|
233
|
-
console.log('\n5.5. Testing Sentry Webhook...');
|
|
234
|
-
try {
|
|
235
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
236
|
-
const requestId = 'sentry-request-id-123';
|
|
237
|
-
const sentryBody = JSON.stringify({
|
|
238
|
-
action: 'triggered',
|
|
239
|
-
data: {
|
|
240
|
-
issue_alert: {
|
|
241
|
-
id: 'alert-1',
|
|
242
|
-
title: 'CPU high',
|
|
243
|
-
project: 'infra',
|
|
244
|
-
},
|
|
245
|
-
},
|
|
246
|
-
});
|
|
247
|
-
const sentryRequest = createMockRequest({
|
|
248
|
-
'sentry-hook-signature': createSentrySignature(sentryBody, testSecret),
|
|
249
|
-
'sentry-hook-timestamp': timestamp.toString(),
|
|
250
|
-
'request-id': requestId,
|
|
251
|
-
'content-type': 'application/json',
|
|
252
|
-
}, sentryBody);
|
|
253
|
-
const sentryResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(sentryRequest, 'sentry', testSecret);
|
|
254
|
-
const sentryIssueAlertRequest = createMockRequest({
|
|
255
|
-
'sentry-hook-signature': createSentryIssueAlertSignature({ id: 'alert-1', title: 'CPU high', project: 'infra' }, testSecret),
|
|
256
|
-
'sentry-hook-timestamp': timestamp.toString(),
|
|
257
|
-
'request-id': `${requestId}-issue-alert`,
|
|
258
|
-
'content-type': 'application/json',
|
|
259
|
-
}, sentryBody);
|
|
260
|
-
const sentryIssueAlertResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(sentryIssueAlertRequest, 'sentry', testSecret);
|
|
261
|
-
const sentryPassed = sentryResult.isValid && sentryIssueAlertResult.isValid && sentryResult.eventId?.startsWith('sentry:');
|
|
262
|
-
console.log(' ✅ Sentry:', sentryPassed ? 'PASSED' : 'FAILED');
|
|
263
|
-
}
|
|
264
|
-
catch (error) {
|
|
265
|
-
console.log(' ❌ Sentry test failed:', error);
|
|
266
|
-
}
|
|
267
|
-
// Test 5.6: Grafana webhook verification
|
|
268
|
-
console.log('\n5.6. Testing Grafana Webhook...');
|
|
269
|
-
try {
|
|
270
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
271
|
-
const grafanaSignature = createGrafanaSignature(testBody, testSecret, timestamp);
|
|
272
|
-
const grafanaRequest = createMockRequest({
|
|
273
|
-
'x-grafana-alerting-signature': grafanaSignature,
|
|
274
|
-
'x-grafana-alerting-timestamp': timestamp.toString(),
|
|
275
|
-
'content-type': 'application/json',
|
|
276
|
-
});
|
|
277
|
-
const grafanaResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(grafanaRequest, 'grafana', testSecret);
|
|
278
|
-
console.log(' ✅ Grafana:', grafanaResult.isValid ? 'PASSED' : 'FAILED');
|
|
279
|
-
}
|
|
280
|
-
catch (error) {
|
|
281
|
-
console.log(' ❌ Grafana test failed:', error);
|
|
282
|
-
}
|
|
283
|
-
// Test 5.7: Doppler webhook verification
|
|
284
|
-
console.log('\n5.7. Testing Doppler Webhook...');
|
|
285
|
-
try {
|
|
286
|
-
const dopplerRequest = createMockRequest({
|
|
287
|
-
'x-doppler-signature': createDopplerSignature(testBody, testSecret),
|
|
288
|
-
'content-type': 'application/json',
|
|
289
|
-
});
|
|
290
|
-
const dopplerResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(dopplerRequest, 'doppler', testSecret);
|
|
291
|
-
const dopplerPassed = dopplerResult.isValid && Boolean(dopplerResult.eventId?.startsWith('doppler:'));
|
|
292
|
-
console.log(' ✅ Doppler:', dopplerPassed ? 'PASSED' : 'FAILED');
|
|
293
|
-
}
|
|
294
|
-
catch (error) {
|
|
295
|
-
console.log(' ❌ Doppler test failed:', error);
|
|
296
|
-
}
|
|
297
|
-
// Test 5.8: Sanity webhook verification
|
|
298
|
-
console.log('\n5.8. Testing Sanity Webhook...');
|
|
299
|
-
try {
|
|
300
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
301
|
-
const idempotencyKey = 'sanity-idem-123';
|
|
302
|
-
const sanityRequest = createMockRequest({
|
|
303
|
-
'sanity-webhook-signature': createSanitySignature(testBody, testSecret, timestamp),
|
|
304
|
-
'idempotency-key': idempotencyKey,
|
|
305
|
-
'content-type': 'application/json',
|
|
306
|
-
});
|
|
307
|
-
const sanityResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(sanityRequest, 'sanity', testSecret);
|
|
308
|
-
const sanityPassed = sanityResult.isValid && sanityResult.eventId === `sanity:${idempotencyKey}`;
|
|
309
|
-
console.log(' ✅ Sanity:', sanityPassed ? 'PASSED' : 'FAILED');
|
|
310
|
-
}
|
|
311
|
-
catch (error) {
|
|
312
|
-
console.log(' ❌ Sanity test failed:', error);
|
|
313
|
-
}
|
|
314
|
-
// Test 6: Invalid signatures
|
|
315
|
-
console.log('\n6. Testing Invalid Signatures...');
|
|
316
|
-
try {
|
|
317
|
-
const invalidRequest = createMockRequest({
|
|
318
|
-
'stripe-signature': 't=1234567890,v1=invalid_signature',
|
|
319
|
-
'content-type': 'application/json',
|
|
320
|
-
});
|
|
321
|
-
const invalidResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(invalidRequest, 'stripe', testSecret);
|
|
322
|
-
const invalidSigPassed = !invalidResult.isValid && (invalidResult.errorCode === 'INVALID_SIGNATURE'
|
|
323
|
-
|| invalidResult.errorCode === 'TIMESTAMP_EXPIRED');
|
|
324
|
-
console.log(' ✅ Invalid signature correctly rejected:', invalidSigPassed ? 'PASSED' : 'FAILED');
|
|
325
|
-
if (invalidResult.isValid) {
|
|
326
|
-
console.log(' ❌ Should have been rejected');
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
catch (error) {
|
|
330
|
-
console.log(' ❌ Invalid signature test failed:', error);
|
|
331
|
-
}
|
|
332
|
-
// Test 7: Missing headers
|
|
333
|
-
console.log('\n7. Testing Missing Headers...');
|
|
334
|
-
try {
|
|
335
|
-
const missingHeaderRequest = createMockRequest({
|
|
336
|
-
'content-type': 'application/json',
|
|
337
|
-
});
|
|
338
|
-
const missingHeaderResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(missingHeaderRequest, 'stripe', testSecret);
|
|
339
|
-
const missingHeaderPassed = !missingHeaderResult.isValid && missingHeaderResult.errorCode === 'MISSING_SIGNATURE';
|
|
340
|
-
console.log(' ✅ Missing headers correctly rejected:', missingHeaderPassed ? 'PASSED' : 'FAILED');
|
|
341
|
-
if (missingHeaderResult.isValid) {
|
|
342
|
-
console.log(' ❌ Should have been rejected');
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
catch (error) {
|
|
346
|
-
console.log(' ❌ Missing headers test failed:', error);
|
|
347
|
-
}
|
|
348
|
-
// Test 8: GitLab Webhook
|
|
349
|
-
console.log('\n8. Testing GitLab Webhook...');
|
|
350
|
-
try {
|
|
351
|
-
const gitlabSecret = testSecret;
|
|
352
|
-
const gitlabRequest = createMockRequest({
|
|
353
|
-
'X-Gitlab-Token': gitlabSecret,
|
|
354
|
-
'content-type': 'application/json',
|
|
355
|
-
});
|
|
356
|
-
const gitlabResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(gitlabRequest, 'gitlab', gitlabSecret);
|
|
357
|
-
console.log(' ✅ GitLab:', gitlabResult.isValid ? 'PASSED' : 'FAILED');
|
|
358
|
-
if (!gitlabResult.isValid) {
|
|
359
|
-
console.log(' ❌ Error:', gitlabResult.error);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
catch (error) {
|
|
363
|
-
console.log(' ❌ GitLab test failed:', error);
|
|
364
|
-
}
|
|
365
|
-
// Test 9: GitLab Invalid Token
|
|
366
|
-
console.log('\n9. Testing GitLab Invalid Token...');
|
|
367
|
-
try {
|
|
368
|
-
const gitlabRequest = createMockRequest({
|
|
369
|
-
'X-Gitlab-Token': 'wrong_secret',
|
|
370
|
-
'content-type': 'application/json',
|
|
371
|
-
});
|
|
372
|
-
const gitlabResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(gitlabRequest, 'gitlab', testSecret);
|
|
373
|
-
console.log(' ✅ Invalid token correctly rejected:', !gitlabResult.isValid ? 'PASSED' : 'FAILED');
|
|
374
|
-
}
|
|
375
|
-
catch (error) {
|
|
376
|
-
console.log(' ❌ GitLab invalid token test failed:', error);
|
|
377
|
-
}
|
|
378
|
-
// Test 10: verifyAny should auto-detect Stripe
|
|
379
|
-
console.log('\n10. Testing verifyAny auto-detection...');
|
|
380
|
-
try {
|
|
381
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
382
|
-
const stripeSignature = createStripeSignature(testBody, testSecret, timestamp);
|
|
383
|
-
const request = createMockRequest({
|
|
384
|
-
'stripe-signature': stripeSignature,
|
|
385
|
-
'content-type': 'application/json',
|
|
386
|
-
});
|
|
387
|
-
const result = await index_1.WebhookVerificationService.verifyAny(request, {
|
|
388
|
-
stripe: testSecret,
|
|
389
|
-
github: 'wrong-secret',
|
|
390
|
-
});
|
|
391
|
-
console.log(' ✅ verifyAny:', result.isValid && result.platform === 'stripe' ? 'PASSED' : 'FAILED');
|
|
392
|
-
}
|
|
393
|
-
catch (error) {
|
|
394
|
-
console.log(' ❌ verifyAny test failed:', error);
|
|
395
|
-
}
|
|
396
|
-
// Test 10.5: verifyAny error diagnostics
|
|
397
|
-
console.log('\n10.5. Testing verifyAny error diagnostics...');
|
|
398
|
-
try {
|
|
399
|
-
const unknownRequest = createMockRequest({
|
|
400
|
-
'content-type': 'application/json',
|
|
401
|
-
});
|
|
402
|
-
const invalidVerifyAny = await index_1.WebhookVerificationService.verifyAny(unknownRequest, {
|
|
403
|
-
stripe: testSecret,
|
|
404
|
-
shopify: testSecret,
|
|
405
|
-
});
|
|
406
|
-
const hasDetailedErrors = Boolean(invalidVerifyAny.error
|
|
407
|
-
&& invalidVerifyAny.error.includes('Attempts ->')
|
|
408
|
-
&& invalidVerifyAny.metadata?.attempts?.length === 2);
|
|
409
|
-
console.log(' ✅ verifyAny diagnostics:', hasDetailedErrors ? 'PASSED' : 'FAILED');
|
|
410
|
-
}
|
|
411
|
-
catch (error) {
|
|
412
|
-
console.log(' ❌ verifyAny diagnostics test failed:', error);
|
|
413
|
-
}
|
|
414
|
-
// Test 11: Normalization for Stripe
|
|
415
|
-
console.log('\n11. Testing payload normalization...');
|
|
416
|
-
try {
|
|
417
|
-
const normalizedStripeBody = JSON.stringify({
|
|
418
|
-
type: 'payment_intent.succeeded',
|
|
419
|
-
data: {
|
|
420
|
-
object: {
|
|
421
|
-
id: 'pi_123',
|
|
422
|
-
amount: 5000,
|
|
423
|
-
currency: 'usd',
|
|
424
|
-
customer: 'cus_456',
|
|
425
|
-
},
|
|
426
|
-
},
|
|
427
|
-
});
|
|
428
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
429
|
-
const stripeSignature = createStripeSignature(normalizedStripeBody, testSecret, timestamp);
|
|
430
|
-
const request = createMockRequest({
|
|
431
|
-
'stripe-signature': stripeSignature,
|
|
432
|
-
'content-type': 'application/json',
|
|
433
|
-
}, normalizedStripeBody);
|
|
434
|
-
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, 'stripe', testSecret, 300, true);
|
|
435
|
-
const payload = result.payload;
|
|
436
|
-
const passed = result.isValid
|
|
437
|
-
&& payload.event === 'payment.succeeded'
|
|
438
|
-
&& payload.currency === 'USD'
|
|
439
|
-
&& payload.transaction_id === 'pi_123';
|
|
440
|
-
console.log(' ✅ Normalization:', passed ? 'PASSED' : 'FAILED');
|
|
441
|
-
}
|
|
442
|
-
catch (error) {
|
|
443
|
-
console.log(' ❌ Normalization test failed:', error);
|
|
444
|
-
}
|
|
445
|
-
// Test 12: Category-aware normalization registry
|
|
446
|
-
console.log('\n12. Testing category-based platform registry...');
|
|
447
|
-
try {
|
|
448
|
-
const paymentPlatforms = (0, index_1.getPlatformsByCategory)('payment');
|
|
449
|
-
const hasStripeAndPolar = paymentPlatforms.includes('stripe') && paymentPlatforms.includes('polar');
|
|
450
|
-
console.log(' ✅ Category registry:', hasStripeAndPolar ? 'PASSED' : 'FAILED');
|
|
451
|
-
}
|
|
452
|
-
catch (error) {
|
|
453
|
-
console.log(' ❌ Category registry test failed:', error);
|
|
454
|
-
}
|
|
455
|
-
// Test 13: Razorpay
|
|
456
|
-
console.log('\n13. Testing Razorpay webhook...');
|
|
457
|
-
try {
|
|
458
|
-
const hmac = (0, crypto_1.createHmac)('sha256', testSecret);
|
|
459
|
-
hmac.update(testBody);
|
|
460
|
-
const signature = hmac.digest('hex');
|
|
461
|
-
const request = createMockRequest({
|
|
462
|
-
'x-razorpay-signature': signature,
|
|
463
|
-
'content-type': 'application/json',
|
|
464
|
-
});
|
|
465
|
-
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, 'razorpay', testSecret);
|
|
466
|
-
console.log(' ✅ Razorpay:', result.isValid ? 'PASSED' : 'FAILED');
|
|
467
|
-
}
|
|
468
|
-
catch (error) {
|
|
469
|
-
console.log(' ❌ Razorpay test failed:', error);
|
|
470
|
-
}
|
|
471
|
-
// Test 14: Lemon Squeezy
|
|
472
|
-
console.log('\n14. Testing Lemon Squeezy webhook...');
|
|
473
|
-
try {
|
|
474
|
-
const hmac = (0, crypto_1.createHmac)('sha256', testSecret);
|
|
475
|
-
hmac.update(testBody);
|
|
476
|
-
const signature = hmac.digest('hex');
|
|
477
|
-
const request = createMockRequest({
|
|
478
|
-
'x-signature': signature,
|
|
479
|
-
'content-type': 'application/json',
|
|
480
|
-
});
|
|
481
|
-
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, 'lemonsqueezy', testSecret);
|
|
482
|
-
console.log(' ✅ Lemon Squeezy:', result.isValid ? 'PASSED' : 'FAILED');
|
|
483
|
-
}
|
|
484
|
-
catch (error) {
|
|
485
|
-
console.log(' ❌ Lemon Squeezy test failed:', error);
|
|
486
|
-
}
|
|
487
|
-
// Test 15: Paddle
|
|
488
|
-
console.log('\n15. Testing Paddle webhook...');
|
|
489
|
-
try {
|
|
490
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
491
|
-
const signature = createPaddleSignature(testBody, testSecret, timestamp);
|
|
492
|
-
const request = createMockRequest({
|
|
493
|
-
'paddle-signature': signature,
|
|
494
|
-
'content-type': 'application/json',
|
|
495
|
-
});
|
|
496
|
-
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, 'paddle', testSecret);
|
|
497
|
-
console.log(' ✅ Paddle:', result.isValid ? 'PASSED' : 'FAILED');
|
|
498
|
-
}
|
|
499
|
-
catch (error) {
|
|
500
|
-
console.log(' ❌ Paddle test failed:', error);
|
|
501
|
-
}
|
|
502
|
-
// Test 16: WorkOS
|
|
503
|
-
console.log('\n16. Testing WorkOS webhook...');
|
|
504
|
-
try {
|
|
505
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
506
|
-
const signature = createWorkOSSignature(testBody, testSecret, timestamp);
|
|
507
|
-
const request = createMockRequest({
|
|
508
|
-
'workos-signature': signature,
|
|
509
|
-
'content-type': 'application/json',
|
|
510
|
-
});
|
|
511
|
-
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, 'workos', testSecret);
|
|
512
|
-
console.log(' ✅ WorkOS:', result.isValid ? 'PASSED' : 'FAILED');
|
|
513
|
-
}
|
|
514
|
-
catch (error) {
|
|
515
|
-
console.log(' ❌ WorkOS test failed:', error);
|
|
516
|
-
}
|
|
517
|
-
// Test 17.5: Shopify
|
|
518
|
-
console.log('\n17. Testing Shopify webhook...');
|
|
519
|
-
try {
|
|
520
|
-
const signature = createShopifySignature(testBody, testSecret);
|
|
521
|
-
const request = createMockRequest({
|
|
522
|
-
'x-shopify-hmac-sha256': signature,
|
|
523
|
-
'content-type': 'application/json',
|
|
524
|
-
});
|
|
525
|
-
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, 'shopify', testSecret);
|
|
526
|
-
console.log(' ✅ Shopify:', result.isValid ? 'PASSED' : 'FAILED');
|
|
527
|
-
if (!result.isValid) {
|
|
528
|
-
console.log(' ❌ Error:', result.error);
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
catch (error) {
|
|
532
|
-
console.log(' ❌ Shopify test failed:', error);
|
|
533
|
-
}
|
|
534
|
-
// Test 18: WooCommerce
|
|
535
|
-
console.log('\n18. Testing WooCommerce webhook...');
|
|
536
|
-
try {
|
|
537
|
-
const signature = createWooCommerceSignature(testBody, testSecret);
|
|
538
|
-
const request = createMockRequest({
|
|
539
|
-
'x-wc-webhook-signature': signature,
|
|
540
|
-
'content-type': 'application/json',
|
|
541
|
-
});
|
|
542
|
-
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, 'woocommerce', testSecret);
|
|
543
|
-
console.log(' ✅ WooCommerce:', result.isValid ? 'PASSED' : 'FAILED');
|
|
544
|
-
}
|
|
545
|
-
catch (error) {
|
|
546
|
-
console.log(' ❌ WooCommerce test failed:', error);
|
|
547
|
-
}
|
|
548
|
-
// Test 18.5: Polar (Standard Webhooks)
|
|
549
|
-
console.log('\n18.5. Testing Polar webhook...');
|
|
550
|
-
try {
|
|
551
|
-
const secret = `whsec_${Buffer.from(testSecret).toString('base64')}`;
|
|
552
|
-
const webhookId = 'polar-webhook-id-1';
|
|
553
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
554
|
-
const signature = createStandardWebhooksSignature(testBody, secret, webhookId, timestamp);
|
|
555
|
-
const request = createMockRequest({
|
|
556
|
-
'webhook-signature': signature,
|
|
557
|
-
'webhook-id': webhookId,
|
|
558
|
-
'webhook-timestamp': timestamp.toString(),
|
|
559
|
-
'user-agent': 'Polar.sh Webhooks',
|
|
560
|
-
'content-type': 'application/json',
|
|
561
|
-
});
|
|
562
|
-
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, 'polar', secret);
|
|
563
|
-
const detectedPlatform = index_1.WebhookVerificationService.detectPlatform(request);
|
|
564
|
-
console.log(' ✅ Polar verification:', result.isValid ? 'PASSED' : 'FAILED');
|
|
565
|
-
console.log(' ✅ Polar auto-detect:', detectedPlatform === 'polar' ? 'PASSED' : 'FAILED');
|
|
566
|
-
}
|
|
567
|
-
catch (error) {
|
|
568
|
-
console.log(' ❌ Polar test failed:', error);
|
|
569
|
-
}
|
|
570
|
-
// Test 19: Replicate
|
|
571
|
-
console.log('\n19. Testing Replicate webhook...');
|
|
572
|
-
try {
|
|
573
|
-
const secret = `whsec_${Buffer.from(testSecret).toString('base64')}`;
|
|
574
|
-
const webhookId = 'replicate-webhook-id-1';
|
|
575
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
576
|
-
const signature = createStandardWebhooksSignature(testBody, secret, webhookId, timestamp);
|
|
577
|
-
const request = createMockRequest({
|
|
578
|
-
'webhook-signature': signature,
|
|
579
|
-
'webhook-id': webhookId,
|
|
580
|
-
'webhook-timestamp': timestamp.toString(),
|
|
581
|
-
'user-agent': 'Replicate-Webhooks/1.0',
|
|
582
|
-
'content-type': 'application/json',
|
|
583
|
-
});
|
|
584
|
-
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, 'replicateai', secret);
|
|
585
|
-
console.log(' ✅ Replicate:', result.isValid ? 'PASSED' : 'FAILED');
|
|
586
|
-
}
|
|
587
|
-
catch (error) {
|
|
588
|
-
console.log(' ❌ Replicate test failed:', error);
|
|
589
|
-
}
|
|
590
|
-
// Test 20: fal.ai
|
|
591
|
-
console.log('\n20. Testing fal.ai webhook...');
|
|
592
|
-
try {
|
|
593
|
-
const { privateKey, publicKey } = (0, crypto_1.generateKeyPairSync)('ed25519');
|
|
594
|
-
const requestId = 'fal-request-id';
|
|
595
|
-
const userId = 'fal-user-id';
|
|
596
|
-
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
597
|
-
const payloadToSign = createFalPayloadToSign(testBody, requestId, userId, timestamp);
|
|
598
|
-
const payloadBytes = new Uint8Array(Buffer.from(payloadToSign));
|
|
599
|
-
const signature = (0, crypto_1.sign)(null, payloadBytes, privateKey).toString('hex');
|
|
600
|
-
const request = createMockRequest({
|
|
601
|
-
'x-fal-webhook-signature': signature,
|
|
602
|
-
'x-fal-webhook-request-id': requestId,
|
|
603
|
-
'x-fal-webhook-user-id': userId,
|
|
604
|
-
'x-fal-webhook-timestamp': timestamp,
|
|
605
|
-
'content-type': 'application/json',
|
|
606
|
-
});
|
|
607
|
-
const result = await index_1.WebhookVerificationService.verify(request, {
|
|
608
|
-
platform: 'falai',
|
|
609
|
-
secret: '',
|
|
610
|
-
signatureConfig: {
|
|
611
|
-
algorithm: 'ed25519',
|
|
612
|
-
headerName: 'x-fal-webhook-signature',
|
|
613
|
-
headerFormat: 'raw',
|
|
614
|
-
payloadFormat: 'custom',
|
|
615
|
-
customConfig: {
|
|
616
|
-
publicKey: publicKey.export({ type: 'spki', format: 'pem' }).toString(),
|
|
617
|
-
requestIdHeader: 'x-fal-webhook-request-id',
|
|
618
|
-
userIdHeader: 'x-fal-webhook-user-id',
|
|
619
|
-
timestampHeader: 'x-fal-webhook-timestamp',
|
|
620
|
-
},
|
|
621
|
-
},
|
|
622
|
-
});
|
|
623
|
-
console.log(' ✅ fal.ai:', result.isValid ? 'PASSED' : 'FAILED');
|
|
624
|
-
}
|
|
625
|
-
catch (error) {
|
|
626
|
-
console.log(' ❌ fal.ai test failed:', error);
|
|
627
|
-
}
|
|
628
|
-
// Test 21: Core SDK queue entry point
|
|
629
|
-
console.log('\n21. Testing core SDK handleWithQueue...');
|
|
630
|
-
try {
|
|
631
|
-
const request = createMockRequest({
|
|
632
|
-
'content-type': 'application/json',
|
|
633
|
-
});
|
|
634
|
-
const originalToken = process.env.QSTASH_TOKEN;
|
|
635
|
-
const originalCurrent = process.env.QSTASH_CURRENT_SIGNING_KEY;
|
|
636
|
-
const originalNext = process.env.QSTASH_NEXT_SIGNING_KEY;
|
|
637
|
-
delete process.env.QSTASH_TOKEN;
|
|
638
|
-
delete process.env.QSTASH_CURRENT_SIGNING_KEY;
|
|
639
|
-
delete process.env.QSTASH_NEXT_SIGNING_KEY;
|
|
640
|
-
let threw = false;
|
|
641
|
-
try {
|
|
642
|
-
await index_1.WebhookVerificationService.handleWithQueue(request, {
|
|
643
|
-
platform: 'stripe',
|
|
644
|
-
secret: testSecret,
|
|
645
|
-
queue: true,
|
|
646
|
-
handler: async () => undefined,
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
catch (error) {
|
|
650
|
-
threw = error.message.includes('queue: true requires QSTASH_TOKEN');
|
|
651
|
-
}
|
|
652
|
-
if (originalToken !== undefined)
|
|
653
|
-
process.env.QSTASH_TOKEN = originalToken;
|
|
654
|
-
if (originalCurrent !== undefined)
|
|
655
|
-
process.env.QSTASH_CURRENT_SIGNING_KEY = originalCurrent;
|
|
656
|
-
if (originalNext !== undefined)
|
|
657
|
-
process.env.QSTASH_NEXT_SIGNING_KEY = originalNext;
|
|
658
|
-
console.log(' ✅ handleWithQueue:', threw ? 'PASSED' : 'FAILED');
|
|
659
|
-
}
|
|
660
|
-
catch (error) {
|
|
661
|
-
console.log(' ❌ handleWithQueue test failed:', error);
|
|
662
|
-
}
|
|
663
|
-
console.log('\n🎉 All tests completed!');
|
|
664
|
-
}
|
|
665
|
-
// Run tests if this file is executed directly
|
|
666
|
-
if (require.main === module) {
|
|
667
|
-
runTests().catch(console.error);
|
|
668
|
-
}
|