@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.
@@ -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>;
@@ -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 = undefined;
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>;
@@ -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 (body !== null && body !== undefined && typeof body === 'object') {
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
- for (const [platform, secret] of Object.entries(secrets)) {
107
- if (!secret) {
108
- continue;
109
- }
110
- const result = await this.verifyWithPlatformConfig(requestClone, platform.toLowerCase(), secret, toleranceInSeconds, normalize);
111
- if (result.isValid) {
112
- return result;
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.toLowerCase(),
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.headers;
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
- // Simple comparison - we don't actually verify, just check if tokens match
273
- const isValid = idHeader === webhookId && tokenHeader === webhookToken;
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: `custom:${idHeader}`,
308
+ eventId: `custom_${idHeader}`,
295
309
  metadata: {
296
310
  id: idHeader,
297
311
  algorithm: 'token-based',
@@ -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
- dynamicImport('@upstash/qstash'),
48
- dynamicImport('@upstash/qstash/nextjs'),
49
- dynamicImport('@upstash/qstash/nuxt'),
50
- dynamicImport('@upstash/qstash/sveltekit'),
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 receiverExport = resolveModuleExport(modules, 'Receiver')
91
+ const ReceiverCtor = resolveModuleExport(modules, 'Receiver')
96
92
  ?? resolveNestedConstructor(modules, 'Receiver');
97
- if (typeof receiverExport !== 'function') {
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 receiverExport({
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 clientExport = resolveModuleExport(modules, 'Client');
108
- const resolvedClientExport = clientExport
103
+ const ClientCtor = resolveModuleExport(modules, 'Client')
109
104
  ?? resolveNestedConstructor(modules, 'Client');
110
- if (typeof resolvedClientExport !== 'function') {
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 resolvedClientExport({ token: queueConfig.token });
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.headers;
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 response = await fetch(jwksUrl);
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(request) {
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
- const timestamp = parseInt(timestampStr, 10);
454
- if (!this.isTimestampValid(timestamp)) {
455
- return {
456
- isValid: false,
457
- error: "Webhook timestamp expired",
458
- errorCode: "TIMESTAMP_EXPIRED",
459
- platform: this.platform,
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(request);
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",
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 dist/test.js",
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();
@@ -1 +0,0 @@
1
- export {};
@@ -1,4 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const test_1 = require("./test");
4
- (0, test_1.runTests)();
package/dist/test.d.ts DELETED
@@ -1,2 +0,0 @@
1
- declare function runTests(): Promise<void>;
2
- export { runTests };
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
- }