@hookflo/tern 4.3.0-beta.0 → 4.3.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -17
- package/dist/adapters/cloudflare.d.ts +1 -2
- package/dist/adapters/cloudflare.js +5 -1
- package/dist/adapters/express.d.ts +1 -2
- package/dist/adapters/express.js +5 -1
- package/dist/adapters/hono.d.ts +1 -2
- package/dist/adapters/hono.js +5 -1
- package/dist/adapters/nextjs.d.ts +1 -2
- package/dist/adapters/nextjs.js +5 -1
- package/dist/adapters/shared.js +5 -2
- package/dist/index.d.ts +4 -5
- package/dist/index.js +23 -19
- package/dist/platforms/algorithms.d.ts +20 -0
- package/dist/platforms/algorithms.js +76 -1
- package/dist/types.d.ts +4 -65
- package/dist/types.js +3 -0
- package/dist/verifiers/algorithms.d.ts +6 -0
- package/dist/verifiers/algorithms.js +112 -18
- package/package.json +1 -1
- package/dist/normalization/index.d.ts +0 -20
- package/dist/normalization/index.js +0 -78
- package/dist/normalization/providers/payment/paypal.d.ts +0 -2
- package/dist/normalization/providers/payment/paypal.js +0 -12
- package/dist/normalization/providers/payment/razorpay.d.ts +0 -2
- package/dist/normalization/providers/payment/razorpay.js +0 -13
- package/dist/normalization/providers/payment/stripe.d.ts +0 -2
- package/dist/normalization/providers/payment/stripe.js +0 -13
- package/dist/normalization/providers/registry.d.ts +0 -5
- package/dist/normalization/providers/registry.js +0 -21
- package/dist/normalization/simple.d.ts +0 -4
- package/dist/normalization/simple.js +0 -126
- package/dist/normalization/storage/interface.d.ts +0 -13
- package/dist/normalization/storage/interface.js +0 -2
- package/dist/normalization/storage/memory.d.ts +0 -12
- package/dist/normalization/storage/memory.js +0 -39
- package/dist/normalization/templates/base/auth.d.ts +0 -2
- package/dist/normalization/templates/base/auth.js +0 -22
- package/dist/normalization/templates/base/ecommerce.d.ts +0 -2
- package/dist/normalization/templates/base/ecommerce.js +0 -25
- package/dist/normalization/templates/base/payment.d.ts +0 -2
- package/dist/normalization/templates/base/payment.js +0 -25
- package/dist/normalization/templates/registry.d.ts +0 -6
- package/dist/normalization/templates/registry.js +0 -22
- package/dist/normalization/transformer/engine.d.ts +0 -11
- package/dist/normalization/transformer/engine.js +0 -86
- package/dist/normalization/transformer/validator.d.ts +0 -12
- package/dist/normalization/transformer/validator.js +0 -56
- package/dist/normalization/types.d.ts +0 -79
- package/dist/normalization/types.js +0 -2
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
[](package.json)
|
|
9
9
|
|
|
10
|
-
Stop writing webhook verification from scratch. **Tern** handles signature verification for Stripe, GitHub, Clerk, Shopify, and 15+ more platforms — with one consistent API.
|
|
10
|
+
Stop writing webhook verification from scratch. **Tern** handles signature verification for Stripe, GitHub, Clerk, Shopify, and 15+ more platforms — with one consistent API. It also verifies **Standard Webhooks** (including Svix-style `svix-*` and canonical `webhook-*` headers) through a single `standardwebhooks` platform config.
|
|
11
11
|
|
|
12
12
|
> Need reliable delivery too? Tern supports inbound webhook delivery via Upstash QStash — automatic retries, DLQ management, replay controls, and Slack/Discord alerting. Bring your own Upstash account (BYOK).
|
|
13
13
|
|
|
@@ -171,6 +171,9 @@ app.post('/webhooks/stripe', createWebhookHandler({
|
|
|
171
171
|
|
|
172
172
|
## Supported Platforms
|
|
173
173
|
|
|
174
|
+
> ⚠️ Normalization is no longer supported in Tern and has been removed from the public verification APIs.
|
|
175
|
+
|
|
176
|
+
|
|
174
177
|
| Platform | Algorithm | Status |
|
|
175
178
|
|---|---|---|
|
|
176
179
|
| **Stripe** | HMAC-SHA256 | ✅ Tested |
|
|
@@ -189,6 +192,9 @@ app.post('/webhooks/stripe', createWebhookHandler({
|
|
|
189
192
|
| **Grafana** | HMAC-SHA256 | ✅ Tested |
|
|
190
193
|
| **Doppler** | HMAC-SHA256 | ✅ Tested |
|
|
191
194
|
| **Sanity** | HMAC-SHA256 | ✅ Tested |
|
|
195
|
+
| **Svix** | HMAC-SHA256 | ⚠️ Untested for now |
|
|
196
|
+
| **Standard Webhooks** (`standardwebhooks`) | HMAC-SHA256 | ✅ Tested |
|
|
197
|
+
| **Linear** | HMAC-SHA256 | ⚠️ Untested for now |
|
|
192
198
|
| **Razorpay** | HMAC-SHA256 | 🔄 Pending |
|
|
193
199
|
| **Vercel** | HMAC-SHA256 | 🔄 Pending |
|
|
194
200
|
|
|
@@ -196,7 +202,7 @@ app.post('/webhooks/stripe', createWebhookHandler({
|
|
|
196
202
|
|
|
197
203
|
### Platform signature notes
|
|
198
204
|
|
|
199
|
-
- **Standard Webhooks style**
|
|
205
|
+
- **Standard Webhooks style** providers are supported via the canonical `standardwebhooks` platform (with aliases for both `webhook-*` and `svix-*` headers). Clerk, Dodo Payments, Polar, and ReplicateAI all follow this pattern and commonly use a secret that starts with `whsec_...`.
|
|
200
206
|
- **ReplicateAI**: copy the webhook signing secret from your Replicate webhook settings and pass it directly as `secret`.
|
|
201
207
|
- **fal.ai**: supports JWKS key resolution out of the box — use `secret: ''` for auto key resolution, or pass a PEM public key explicitly.
|
|
202
208
|
|
|
@@ -337,25 +343,31 @@ const result = await WebhookVerificationService.verify(request, {
|
|
|
337
343
|
});
|
|
338
344
|
```
|
|
339
345
|
|
|
340
|
-
###
|
|
346
|
+
### Standard Webhooks config helpers (Svix-style and webhook-* headers)
|
|
341
347
|
|
|
342
348
|
```typescript
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
349
|
+
import {
|
|
350
|
+
createStandardWebhooksConfig,
|
|
351
|
+
STANDARD_WEBHOOKS_BASE,
|
|
352
|
+
} from '@hookflo/tern';
|
|
353
|
+
|
|
354
|
+
const signatureConfig = createStandardWebhooksConfig({
|
|
355
|
+
id: 'webhook-id',
|
|
356
|
+
timestamp: 'webhook-timestamp',
|
|
357
|
+
signature: 'webhook-signature',
|
|
358
|
+
idAliases: ['svix-id'],
|
|
359
|
+
timestampAliases: ['svix-timestamp'],
|
|
360
|
+
signatureAliases: ['svix-signature'],
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const result = await WebhookVerificationService.verify(request, {
|
|
364
|
+
platform: 'standardwebhooks',
|
|
365
|
+
secret: process.env.STANDARD_WEBHOOKS_SECRET!,
|
|
346
366
|
signatureConfig: {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
headerFormat: 'raw',
|
|
350
|
-
timestampHeader: 'webhook-timestamp',
|
|
351
|
-
timestampFormat: 'unix',
|
|
352
|
-
payloadFormat: 'custom',
|
|
353
|
-
customConfig: {
|
|
354
|
-
payloadFormat: '{id}.{timestamp}.{body}',
|
|
355
|
-
idHeader: 'webhook-id',
|
|
356
|
-
},
|
|
367
|
+
...STANDARD_WEBHOOKS_BASE,
|
|
368
|
+
...signatureConfig,
|
|
357
369
|
},
|
|
358
|
-
};
|
|
370
|
+
});
|
|
359
371
|
```
|
|
360
372
|
|
|
361
373
|
See the [SignatureConfig type](https://tern.hookflo.com) for all options.
|
|
@@ -403,6 +415,7 @@ interface WebhookVerificationResult {
|
|
|
403
415
|
|
|
404
416
|
## Troubleshooting
|
|
405
417
|
|
|
418
|
+
|
|
406
419
|
**`Module not found: Can't resolve "@hookflo/tern/nextjs"`**
|
|
407
420
|
|
|
408
421
|
```bash
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WebhookPlatform
|
|
1
|
+
import { WebhookPlatform } from '../types';
|
|
2
2
|
import { QueueOption } from '../upstash/types';
|
|
3
3
|
import type { AlertConfig, SendAlertOptions } from '../notifications/types';
|
|
4
4
|
export interface CloudflareWebhookHandlerOptions<TEnv = Record<string, unknown>, TPayload = any, TMetadata extends Record<string, unknown> = Record<string, unknown>, TResponse = unknown> {
|
|
@@ -6,7 +6,6 @@ export interface CloudflareWebhookHandlerOptions<TEnv = Record<string, unknown>,
|
|
|
6
6
|
secret?: string;
|
|
7
7
|
secretEnv?: string;
|
|
8
8
|
toleranceInSeconds?: number;
|
|
9
|
-
normalize?: boolean | NormalizeOptions;
|
|
10
9
|
queue?: QueueOption;
|
|
11
10
|
alerts?: AlertConfig;
|
|
12
11
|
alert?: Omit<SendAlertOptions, 'dlq' | 'dlqId' | 'source' | 'eventId'>;
|
|
@@ -39,7 +39,11 @@ function createWebhookHandler(options) {
|
|
|
39
39
|
}
|
|
40
40
|
return response;
|
|
41
41
|
}
|
|
42
|
-
const result = await index_1.WebhookVerificationService.
|
|
42
|
+
const result = await index_1.WebhookVerificationService.verify(request, {
|
|
43
|
+
platform: options.platform,
|
|
44
|
+
secret,
|
|
45
|
+
toleranceInSeconds: options.toleranceInSeconds,
|
|
46
|
+
});
|
|
43
47
|
if (!result.isValid) {
|
|
44
48
|
return Response.json({ error: result.error, errorCode: result.errorCode, platform: result.platform, metadata: result.metadata }, { status: 400 });
|
|
45
49
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WebhookPlatform, WebhookVerificationResult
|
|
1
|
+
import { WebhookPlatform, WebhookVerificationResult } from '../types';
|
|
2
2
|
import { QueueOption } from '../upstash/types';
|
|
3
3
|
import { MinimalNodeRequest } from './shared';
|
|
4
4
|
import type { AlertConfig, SendAlertOptions } from '../notifications/types';
|
|
@@ -14,7 +14,6 @@ export interface ExpressWebhookMiddlewareOptions {
|
|
|
14
14
|
platform: WebhookPlatform;
|
|
15
15
|
secret: string;
|
|
16
16
|
toleranceInSeconds?: number;
|
|
17
|
-
normalize?: boolean | NormalizeOptions;
|
|
18
17
|
queue?: QueueOption;
|
|
19
18
|
alerts?: AlertConfig;
|
|
20
19
|
alert?: Omit<SendAlertOptions, 'dlq' | 'dlqId' | 'source' | 'eventId'>;
|
package/dist/adapters/express.js
CHANGED
|
@@ -48,7 +48,11 @@ function createWebhookMiddleware(options) {
|
|
|
48
48
|
}
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
|
-
const result = await index_1.WebhookVerificationService.
|
|
51
|
+
const result = await index_1.WebhookVerificationService.verify(webRequest, {
|
|
52
|
+
platform: options.platform,
|
|
53
|
+
secret: options.secret,
|
|
54
|
+
toleranceInSeconds: options.toleranceInSeconds,
|
|
55
|
+
});
|
|
52
56
|
if (!result.isValid) {
|
|
53
57
|
res.status(400).json({
|
|
54
58
|
error: result.error,
|
package/dist/adapters/hono.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WebhookPlatform
|
|
1
|
+
import { WebhookPlatform } from '../types';
|
|
2
2
|
import { QueueOption } from '../upstash/types';
|
|
3
3
|
import type { AlertConfig, SendAlertOptions } from '../notifications/types';
|
|
4
4
|
export interface HonoContextLike {
|
|
@@ -11,7 +11,6 @@ export interface HonoWebhookHandlerOptions<TContext extends HonoContextLike = Ho
|
|
|
11
11
|
platform: WebhookPlatform;
|
|
12
12
|
secret: string;
|
|
13
13
|
toleranceInSeconds?: number;
|
|
14
|
-
normalize?: boolean | NormalizeOptions;
|
|
15
14
|
queue?: QueueOption;
|
|
16
15
|
alerts?: AlertConfig;
|
|
17
16
|
alert?: Omit<SendAlertOptions, 'dlq' | 'dlqId' | 'source' | 'eventId'>;
|
package/dist/adapters/hono.js
CHANGED
|
@@ -35,7 +35,11 @@ function createWebhookHandler(options) {
|
|
|
35
35
|
}
|
|
36
36
|
return response;
|
|
37
37
|
}
|
|
38
|
-
const result = await index_1.WebhookVerificationService.
|
|
38
|
+
const result = await index_1.WebhookVerificationService.verify(request, {
|
|
39
|
+
platform: options.platform,
|
|
40
|
+
secret: options.secret,
|
|
41
|
+
toleranceInSeconds: options.toleranceInSeconds,
|
|
42
|
+
});
|
|
39
43
|
if (!result.isValid) {
|
|
40
44
|
return c.json({
|
|
41
45
|
error: result.error,
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { WebhookPlatform
|
|
1
|
+
import { WebhookPlatform } from '../types';
|
|
2
2
|
import { QueueOption } from '../upstash/types';
|
|
3
3
|
import type { AlertConfig, SendAlertOptions } from '../notifications/types';
|
|
4
4
|
export interface NextWebhookHandlerOptions<TPayload = any, TMetadata extends Record<string, unknown> = Record<string, unknown>, TResponse = unknown> {
|
|
5
5
|
platform: WebhookPlatform;
|
|
6
6
|
secret: string;
|
|
7
7
|
toleranceInSeconds?: number;
|
|
8
|
-
normalize?: boolean | NormalizeOptions;
|
|
9
8
|
queue?: QueueOption;
|
|
10
9
|
alerts?: AlertConfig;
|
|
11
10
|
alert?: Omit<SendAlertOptions, 'dlq' | 'dlqId' | 'source' | 'eventId'>;
|
package/dist/adapters/nextjs.js
CHANGED
|
@@ -34,7 +34,11 @@ function createWebhookHandler(options) {
|
|
|
34
34
|
}
|
|
35
35
|
return response;
|
|
36
36
|
}
|
|
37
|
-
const result = await index_1.WebhookVerificationService.
|
|
37
|
+
const result = await index_1.WebhookVerificationService.verify(request, {
|
|
38
|
+
platform: options.platform,
|
|
39
|
+
secret: options.secret,
|
|
40
|
+
toleranceInSeconds: options.toleranceInSeconds,
|
|
41
|
+
});
|
|
38
42
|
if (!result.isValid) {
|
|
39
43
|
return Response.json({ error: result.error, errorCode: result.errorCode, platform: result.platform, metadata: result.metadata }, { status: 400 });
|
|
40
44
|
}
|
package/dist/adapters/shared.js
CHANGED
|
@@ -90,8 +90,11 @@ function toHeadersInit(headers) {
|
|
|
90
90
|
return normalized;
|
|
91
91
|
}
|
|
92
92
|
async function toWebRequest(request) {
|
|
93
|
-
const
|
|
94
|
-
const
|
|
93
|
+
const forwardedProto = getHeaderValue(request.headers, 'x-forwarded-proto')?.split(',')[0]?.trim();
|
|
94
|
+
const protocol = forwardedProto || request.protocol || 'https';
|
|
95
|
+
const forwardedHost = getHeaderValue(request.headers, 'x-forwarded-host')?.split(',')[0]?.trim();
|
|
96
|
+
const host = forwardedHost
|
|
97
|
+
|| request.get?.('host')
|
|
95
98
|
|| getHeaderValue(request.headers, 'host')
|
|
96
99
|
|| 'localhost';
|
|
97
100
|
const path = request.originalUrl || request.url || '/';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WebhookConfig, WebhookVerificationResult, WebhookPlatform, SignatureConfig, MultiPlatformSecrets
|
|
1
|
+
import { WebhookConfig, WebhookVerificationResult, WebhookPlatform, SignatureConfig, MultiPlatformSecrets } from './types';
|
|
2
2
|
import type { QueueOption } from './upstash/types';
|
|
3
3
|
import type { AlertConfig, SendAlertOptions } from './notifications/types';
|
|
4
4
|
export declare class WebhookVerificationService {
|
|
@@ -6,8 +6,8 @@ export declare class WebhookVerificationService {
|
|
|
6
6
|
private static getVerifier;
|
|
7
7
|
private static createAlgorithmBasedVerifier;
|
|
8
8
|
private static getLegacyVerifier;
|
|
9
|
-
static verifyWithPlatformConfig<TPayload = unknown>(request: Request, platform: WebhookPlatform, secret: string, toleranceInSeconds?: number
|
|
10
|
-
static verifyAny<TPayload = unknown>(request: Request, secrets: MultiPlatformSecrets, toleranceInSeconds?: number
|
|
9
|
+
static verifyWithPlatformConfig<TPayload = unknown>(request: Request, platform: WebhookPlatform, secret: string, toleranceInSeconds?: number): Promise<WebhookVerificationResult<TPayload>>;
|
|
10
|
+
static verifyAny<TPayload = unknown>(request: Request, secrets: MultiPlatformSecrets, toleranceInSeconds?: number): Promise<WebhookVerificationResult<TPayload>>;
|
|
11
11
|
private static resolveCanonicalEventId;
|
|
12
12
|
private static safeCompare;
|
|
13
13
|
private static pickString;
|
|
@@ -29,10 +29,9 @@ export declare class WebhookVerificationService {
|
|
|
29
29
|
static verifyTokenBased<TPayload = unknown>(request: Request, webhookId: string, webhookToken: string): Promise<WebhookVerificationResult<TPayload>>;
|
|
30
30
|
}
|
|
31
31
|
export * from './types';
|
|
32
|
-
export { getPlatformAlgorithmConfig, platformUsesAlgorithm, getPlatformsUsingAlgorithm, validateSignatureConfig, } from './platforms/algorithms';
|
|
32
|
+
export { getPlatformAlgorithmConfig, platformUsesAlgorithm, getPlatformsUsingAlgorithm, validateSignatureConfig, STANDARD_WEBHOOKS_BASE, createStandardWebhooksConfig, } from './platforms/algorithms';
|
|
33
33
|
export { createAlgorithmVerifier } from './verifiers/algorithms';
|
|
34
34
|
export { createCustomVerifier } from './verifiers/custom-algorithms';
|
|
35
|
-
export { normalizePayload, getPlatformNormalizationCategory, getPlatformsByCategory, } from './normalization/simple';
|
|
36
35
|
export * from './adapters';
|
|
37
36
|
export * from './alerts';
|
|
38
37
|
export default WebhookVerificationService;
|
package/dist/index.js
CHANGED
|
@@ -36,12 +36,11 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
36
36
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.
|
|
39
|
+
exports.createCustomVerifier = exports.createAlgorithmVerifier = exports.createStandardWebhooksConfig = exports.STANDARD_WEBHOOKS_BASE = exports.validateSignatureConfig = exports.getPlatformsUsingAlgorithm = exports.platformUsesAlgorithm = exports.getPlatformAlgorithmConfig = exports.WebhookVerificationService = void 0;
|
|
40
40
|
const crypto_1 = require("crypto");
|
|
41
41
|
const algorithms_1 = require("./verifiers/algorithms");
|
|
42
42
|
const custom_algorithms_1 = require("./verifiers/custom-algorithms");
|
|
43
43
|
const algorithms_2 = require("./platforms/algorithms");
|
|
44
|
-
const simple_1 = require("./normalization/simple");
|
|
45
44
|
const dispatch_1 = require("./notifications/dispatch");
|
|
46
45
|
class WebhookVerificationService {
|
|
47
46
|
static async verify(request, config) {
|
|
@@ -51,9 +50,6 @@ class WebhookVerificationService {
|
|
|
51
50
|
if (result.isValid) {
|
|
52
51
|
result.platform = config.platform;
|
|
53
52
|
result.eventId = this.resolveCanonicalEventId(config.platform, result.metadata, result.payload) ?? undefined;
|
|
54
|
-
if (config.normalize) {
|
|
55
|
-
result.payload = (0, simple_1.normalizePayload)(config.platform, result.payload, config.normalize);
|
|
56
|
-
}
|
|
57
53
|
}
|
|
58
54
|
return result;
|
|
59
55
|
}
|
|
@@ -70,12 +66,18 @@ class WebhookVerificationService {
|
|
|
70
66
|
if (!signatureConfig) {
|
|
71
67
|
throw new Error('Signature config is required for algorithm-based verification');
|
|
72
68
|
}
|
|
69
|
+
const effectiveSignatureConfig = {
|
|
70
|
+
...signatureConfig,
|
|
71
|
+
customConfig: {
|
|
72
|
+
...(signatureConfig.customConfig || {}),
|
|
73
|
+
},
|
|
74
|
+
};
|
|
73
75
|
// Use custom verifiers for special cases (token-based, etc.)
|
|
74
|
-
if (
|
|
75
|
-
return (0, custom_algorithms_1.createCustomVerifier)(secret,
|
|
76
|
+
if (effectiveSignatureConfig.algorithm === 'custom') {
|
|
77
|
+
return (0, custom_algorithms_1.createCustomVerifier)(secret, effectiveSignatureConfig, toleranceInSeconds);
|
|
76
78
|
}
|
|
77
79
|
// Use algorithm-based verifiers for standard algorithms
|
|
78
|
-
return (0, algorithms_1.createAlgorithmVerifier)(secret,
|
|
80
|
+
return (0, algorithms_1.createAlgorithmVerifier)(secret, effectiveSignatureConfig, config.platform, toleranceInSeconds);
|
|
79
81
|
}
|
|
80
82
|
static getLegacyVerifier(config) {
|
|
81
83
|
// For legacy support, we'll use the algorithm-based approach
|
|
@@ -87,29 +89,28 @@ class WebhookVerificationService {
|
|
|
87
89
|
return this.createAlgorithmBasedVerifier(configWithSignature);
|
|
88
90
|
}
|
|
89
91
|
// New method to create verifier using platform algorithm config
|
|
90
|
-
static async verifyWithPlatformConfig(request, platform, secret, toleranceInSeconds = 300
|
|
92
|
+
static async verifyWithPlatformConfig(request, platform, secret, toleranceInSeconds = 300) {
|
|
91
93
|
const platformConfig = (0, algorithms_2.getPlatformAlgorithmConfig)(platform);
|
|
92
94
|
const config = {
|
|
93
95
|
platform,
|
|
94
96
|
secret,
|
|
95
97
|
toleranceInSeconds,
|
|
96
|
-
signatureConfig: platformConfig.signatureConfig
|
|
97
|
-
normalize,
|
|
98
|
+
signatureConfig: platformConfig.signatureConfig
|
|
98
99
|
};
|
|
99
100
|
return this.verify(request, config);
|
|
100
101
|
}
|
|
101
|
-
static async verifyAny(request, secrets, toleranceInSeconds = 300
|
|
102
|
+
static async verifyAny(request, secrets, toleranceInSeconds = 300) {
|
|
102
103
|
const requestClone = request.clone();
|
|
103
104
|
const detectedPlatform = this.detectPlatform(requestClone);
|
|
104
105
|
if (detectedPlatform !== 'unknown' && secrets[detectedPlatform]) {
|
|
105
|
-
return this.verifyWithPlatformConfig(requestClone, detectedPlatform, secrets[detectedPlatform], toleranceInSeconds
|
|
106
|
+
return this.verifyWithPlatformConfig(requestClone, detectedPlatform, secrets[detectedPlatform], toleranceInSeconds);
|
|
106
107
|
}
|
|
107
108
|
const failedAttempts = [];
|
|
108
109
|
const verificationResults = await Promise.all(Object.entries(secrets)
|
|
109
110
|
.filter(([, secret]) => Boolean(secret))
|
|
110
111
|
.map(async ([platform, secret]) => {
|
|
111
112
|
const normalizedPlatform = platform.toLowerCase();
|
|
112
|
-
const result = await this.verifyWithPlatformConfig(requestClone, normalizedPlatform, secret, toleranceInSeconds
|
|
113
|
+
const result = await this.verifyWithPlatformConfig(requestClone, normalizedPlatform, secret, toleranceInSeconds);
|
|
113
114
|
return {
|
|
114
115
|
platform: normalizedPlatform,
|
|
115
116
|
result,
|
|
@@ -189,6 +190,9 @@ class WebhookVerificationService {
|
|
|
189
190
|
case 'workos':
|
|
190
191
|
case 'sentry':
|
|
191
192
|
case 'vercel':
|
|
193
|
+
case 'linear':
|
|
194
|
+
case 'svix':
|
|
195
|
+
case 'standardwebhooks':
|
|
192
196
|
return this.pickString(payload?.id) || null;
|
|
193
197
|
case 'doppler':
|
|
194
198
|
return this.pickString(payload?.event?.id, metadata?.id) || null;
|
|
@@ -220,7 +224,9 @@ class WebhookVerificationService {
|
|
|
220
224
|
if (headers.has('x-hub-signature-256'))
|
|
221
225
|
return 'github';
|
|
222
226
|
if (headers.has('svix-signature'))
|
|
223
|
-
return 'clerk';
|
|
227
|
+
return headers.has('svix-id') ? 'svix' : 'clerk';
|
|
228
|
+
if (headers.has('linear-signature'))
|
|
229
|
+
return 'linear';
|
|
224
230
|
if (headers.has('workos-signature'))
|
|
225
231
|
return 'workos';
|
|
226
232
|
if (headers.has('webhook-signature')) {
|
|
@@ -361,14 +367,12 @@ Object.defineProperty(exports, "getPlatformAlgorithmConfig", { enumerable: true,
|
|
|
361
367
|
Object.defineProperty(exports, "platformUsesAlgorithm", { enumerable: true, get: function () { return algorithms_3.platformUsesAlgorithm; } });
|
|
362
368
|
Object.defineProperty(exports, "getPlatformsUsingAlgorithm", { enumerable: true, get: function () { return algorithms_3.getPlatformsUsingAlgorithm; } });
|
|
363
369
|
Object.defineProperty(exports, "validateSignatureConfig", { enumerable: true, get: function () { return algorithms_3.validateSignatureConfig; } });
|
|
370
|
+
Object.defineProperty(exports, "STANDARD_WEBHOOKS_BASE", { enumerable: true, get: function () { return algorithms_3.STANDARD_WEBHOOKS_BASE; } });
|
|
371
|
+
Object.defineProperty(exports, "createStandardWebhooksConfig", { enumerable: true, get: function () { return algorithms_3.createStandardWebhooksConfig; } });
|
|
364
372
|
var algorithms_4 = require("./verifiers/algorithms");
|
|
365
373
|
Object.defineProperty(exports, "createAlgorithmVerifier", { enumerable: true, get: function () { return algorithms_4.createAlgorithmVerifier; } });
|
|
366
374
|
var custom_algorithms_2 = require("./verifiers/custom-algorithms");
|
|
367
375
|
Object.defineProperty(exports, "createCustomVerifier", { enumerable: true, get: function () { return custom_algorithms_2.createCustomVerifier; } });
|
|
368
|
-
var simple_2 = require("./normalization/simple");
|
|
369
|
-
Object.defineProperty(exports, "normalizePayload", { enumerable: true, get: function () { return simple_2.normalizePayload; } });
|
|
370
|
-
Object.defineProperty(exports, "getPlatformNormalizationCategory", { enumerable: true, get: function () { return simple_2.getPlatformNormalizationCategory; } });
|
|
371
|
-
Object.defineProperty(exports, "getPlatformsByCategory", { enumerable: true, get: function () { return simple_2.getPlatformsByCategory; } });
|
|
372
376
|
__exportStar(require("./adapters"), exports);
|
|
373
377
|
__exportStar(require("./alerts"), exports);
|
|
374
378
|
exports.default = WebhookVerificationService;
|
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
import { PlatformAlgorithmConfig, WebhookPlatform, SignatureConfig } from "../types";
|
|
2
|
+
export declare const STANDARD_WEBHOOKS_BASE: {
|
|
3
|
+
algorithm: "hmac-sha256";
|
|
4
|
+
headerFormat: "raw";
|
|
5
|
+
timestampFormat: "unix";
|
|
6
|
+
payloadFormat: "custom";
|
|
7
|
+
customConfig: {
|
|
8
|
+
signatureFormat: string;
|
|
9
|
+
payloadFormat: string;
|
|
10
|
+
encoding: string;
|
|
11
|
+
secretEncoding: string;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export declare function createStandardWebhooksConfig(headers: {
|
|
15
|
+
id: string;
|
|
16
|
+
timestamp: string;
|
|
17
|
+
signature: string;
|
|
18
|
+
idAliases?: string[];
|
|
19
|
+
timestampAliases?: string[];
|
|
20
|
+
signatureAliases?: string[];
|
|
21
|
+
}): SignatureConfig;
|
|
2
22
|
export declare const platformAlgorithmConfigs: Record<WebhookPlatform, PlatformAlgorithmConfig>;
|
|
3
23
|
export declare function getPlatformAlgorithmConfig(platform: WebhookPlatform): PlatformAlgorithmConfig;
|
|
4
24
|
export declare function platformUsesAlgorithm(platform: WebhookPlatform, algorithm: string): boolean;
|
|
@@ -1,10 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.platformAlgorithmConfigs = void 0;
|
|
3
|
+
exports.platformAlgorithmConfigs = exports.STANDARD_WEBHOOKS_BASE = void 0;
|
|
4
|
+
exports.createStandardWebhooksConfig = createStandardWebhooksConfig;
|
|
4
5
|
exports.getPlatformAlgorithmConfig = getPlatformAlgorithmConfig;
|
|
5
6
|
exports.platformUsesAlgorithm = platformUsesAlgorithm;
|
|
6
7
|
exports.getPlatformsUsingAlgorithm = getPlatformsUsingAlgorithm;
|
|
7
8
|
exports.validateSignatureConfig = validateSignatureConfig;
|
|
9
|
+
exports.STANDARD_WEBHOOKS_BASE = {
|
|
10
|
+
algorithm: "hmac-sha256",
|
|
11
|
+
headerFormat: "raw",
|
|
12
|
+
timestampFormat: "unix",
|
|
13
|
+
payloadFormat: "custom",
|
|
14
|
+
customConfig: {
|
|
15
|
+
signatureFormat: "v1={signature}",
|
|
16
|
+
payloadFormat: "{id}.{timestamp}.{body}",
|
|
17
|
+
encoding: "base64",
|
|
18
|
+
secretEncoding: "base64",
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
function createStandardWebhooksConfig(headers) {
|
|
22
|
+
return {
|
|
23
|
+
...exports.STANDARD_WEBHOOKS_BASE,
|
|
24
|
+
headerName: headers.signature,
|
|
25
|
+
timestampHeader: headers.timestamp,
|
|
26
|
+
customConfig: {
|
|
27
|
+
...exports.STANDARD_WEBHOOKS_BASE.customConfig,
|
|
28
|
+
idHeader: headers.id,
|
|
29
|
+
...(headers.idAliases && { idHeaderAliases: headers.idAliases }),
|
|
30
|
+
...(headers.timestampAliases && { timestampHeaderAliases: headers.timestampAliases }),
|
|
31
|
+
...(headers.signatureAliases && { signatureHeaderAliases: headers.signatureAliases }),
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
8
35
|
exports.platformAlgorithmConfigs = {
|
|
9
36
|
github: {
|
|
10
37
|
platform: "github",
|
|
@@ -51,6 +78,27 @@ exports.platformAlgorithmConfigs = {
|
|
|
51
78
|
},
|
|
52
79
|
description: "Clerk webhooks use HMAC-SHA256 with base64 encoding",
|
|
53
80
|
},
|
|
81
|
+
svix: {
|
|
82
|
+
platform: 'svix',
|
|
83
|
+
signatureConfig: {
|
|
84
|
+
algorithm: 'hmac-sha256',
|
|
85
|
+
headerName: 'svix-signature',
|
|
86
|
+
headerFormat: 'raw',
|
|
87
|
+
timestampHeader: 'svix-timestamp',
|
|
88
|
+
timestampFormat: 'unix',
|
|
89
|
+
payloadFormat: 'custom',
|
|
90
|
+
customConfig: {
|
|
91
|
+
signatureFormat: 'v1={signature}',
|
|
92
|
+
payloadFormat: '{id}.{timestamp}.{body}',
|
|
93
|
+
encoding: 'base64',
|
|
94
|
+
secretEncoding: 'base64',
|
|
95
|
+
idHeader: 'svix-id',
|
|
96
|
+
idHeaderAliases: ['webhook-id'],
|
|
97
|
+
timestampHeaderAliases: ['webhook-timestamp'],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
description: 'Svix webhooks use HMAC-SHA256 with Standard Webhooks format',
|
|
101
|
+
},
|
|
54
102
|
dodopayments: {
|
|
55
103
|
platform: "dodopayments",
|
|
56
104
|
signatureConfig: {
|
|
@@ -287,6 +335,33 @@ exports.platformAlgorithmConfigs = {
|
|
|
287
335
|
},
|
|
288
336
|
description: "Sanity webhooks use Stripe-compatible HMAC-SHA256 with base64 encoded signature and plain UTF-8 secret",
|
|
289
337
|
},
|
|
338
|
+
linear: {
|
|
339
|
+
platform: 'linear',
|
|
340
|
+
signatureConfig: {
|
|
341
|
+
algorithm: 'hmac-sha256',
|
|
342
|
+
headerName: 'linear-signature',
|
|
343
|
+
headerFormat: 'raw',
|
|
344
|
+
payloadFormat: 'raw',
|
|
345
|
+
customConfig: {
|
|
346
|
+
replayToleranceMs: 60000,
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
description: 'Linear webhooks use HMAC-SHA256 on the raw body with a 60s timestamp replay window',
|
|
350
|
+
},
|
|
351
|
+
standardwebhooks: {
|
|
352
|
+
platform: 'standardwebhooks',
|
|
353
|
+
signatureConfig: {
|
|
354
|
+
...createStandardWebhooksConfig({
|
|
355
|
+
id: 'webhook-id',
|
|
356
|
+
timestamp: 'webhook-timestamp',
|
|
357
|
+
signature: 'webhook-signature',
|
|
358
|
+
idAliases: ['svix-id'],
|
|
359
|
+
timestampAliases: ['svix-timestamp'],
|
|
360
|
+
signatureAliases: ['svix-signature'],
|
|
361
|
+
}),
|
|
362
|
+
},
|
|
363
|
+
description: 'Canonical Standard Webhooks implementation. Works for any platform using v1= HMAC-SHA256 signing regardless of header names.',
|
|
364
|
+
},
|
|
290
365
|
custom: {
|
|
291
366
|
platform: "custom",
|
|
292
367
|
signatureConfig: {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export type WebhookPlatform = 'custom' | 'clerk' | 'github' | 'stripe' | 'shopify' | 'vercel' | 'polar' | 'dodopayments' | 'gitlab' | 'paddle' | 'razorpay' | 'lemonsqueezy' | 'workos' | 'woocommerce' | 'replicateai' | 'falai' | 'sentry' | 'grafana' | 'doppler' | 'sanity' | 'unknown';
|
|
1
|
+
export type WebhookPlatform = 'custom' | 'clerk' | 'svix' | 'github' | 'stripe' | 'shopify' | 'vercel' | 'polar' | 'dodopayments' | 'gitlab' | 'paddle' | 'razorpay' | 'lemonsqueezy' | 'workos' | 'woocommerce' | 'replicateai' | 'falai' | 'sentry' | 'grafana' | 'doppler' | 'sanity' | 'linear' | 'standardwebhooks' | 'unknown';
|
|
2
2
|
export declare enum WebhookPlatformKeys {
|
|
3
3
|
GitHub = "github",
|
|
4
4
|
Stripe = "stripe",
|
|
5
5
|
Clerk = "clerk",
|
|
6
|
+
Svix = "svix",
|
|
6
7
|
DodoPayments = "dodopayments",
|
|
7
8
|
Shopify = "shopify",
|
|
8
9
|
Vercel = "vercel",
|
|
@@ -19,6 +20,8 @@ export declare enum WebhookPlatformKeys {
|
|
|
19
20
|
Grafana = "grafana",
|
|
20
21
|
Doppler = "doppler",
|
|
21
22
|
Sanity = "sanity",
|
|
23
|
+
Linear = "linear",
|
|
24
|
+
StandardWebhooks = "standardwebhooks",
|
|
22
25
|
Custom = "custom",
|
|
23
26
|
Unknown = "unknown"
|
|
24
27
|
}
|
|
@@ -35,69 +38,6 @@ export interface SignatureConfig {
|
|
|
35
38
|
customConfig?: Record<string, any>;
|
|
36
39
|
}
|
|
37
40
|
export type WebhookErrorCode = 'MISSING_SIGNATURE' | 'INVALID_SIGNATURE' | 'TIMESTAMP_EXPIRED' | 'MISSING_TOKEN' | 'INVALID_TOKEN' | 'PLATFORM_NOT_SUPPORTED' | 'NORMALIZATION_ERROR' | 'VERIFICATION_ERROR';
|
|
38
|
-
export type NormalizationCategory = 'payment' | 'auth' | 'ecommerce' | 'infrastructure';
|
|
39
|
-
export interface BaseNormalizedWebhook {
|
|
40
|
-
category: NormalizationCategory;
|
|
41
|
-
event: string;
|
|
42
|
-
_platform: WebhookPlatform | string;
|
|
43
|
-
_raw: unknown;
|
|
44
|
-
occurred_at?: string;
|
|
45
|
-
}
|
|
46
|
-
export type PaymentWebhookEvent = 'payment.succeeded' | 'payment.failed' | 'payment.refunded' | 'subscription.created' | 'subscription.cancelled' | 'payment.unknown';
|
|
47
|
-
export interface PaymentWebhookNormalized extends BaseNormalizedWebhook {
|
|
48
|
-
category: 'payment';
|
|
49
|
-
event: PaymentWebhookEvent;
|
|
50
|
-
amount?: number;
|
|
51
|
-
currency?: string;
|
|
52
|
-
customer_id?: string;
|
|
53
|
-
transaction_id?: string;
|
|
54
|
-
subscription_id?: string;
|
|
55
|
-
refund_amount?: number;
|
|
56
|
-
failure_reason?: string;
|
|
57
|
-
metadata?: Record<string, string>;
|
|
58
|
-
}
|
|
59
|
-
export type AuthWebhookEvent = 'user.created' | 'user.updated' | 'user.deleted' | 'session.started' | 'session.ended' | 'auth.unknown';
|
|
60
|
-
export interface AuthWebhookNormalized extends BaseNormalizedWebhook {
|
|
61
|
-
category: 'auth';
|
|
62
|
-
event: AuthWebhookEvent;
|
|
63
|
-
user_id?: string;
|
|
64
|
-
email?: string;
|
|
65
|
-
phone?: string;
|
|
66
|
-
metadata?: Record<string, string>;
|
|
67
|
-
}
|
|
68
|
-
export interface EcommerceWebhookNormalized extends BaseNormalizedWebhook {
|
|
69
|
-
category: 'ecommerce';
|
|
70
|
-
event: string;
|
|
71
|
-
order_id?: string;
|
|
72
|
-
customer_id?: string;
|
|
73
|
-
amount?: number;
|
|
74
|
-
currency?: string;
|
|
75
|
-
metadata?: Record<string, string>;
|
|
76
|
-
}
|
|
77
|
-
export interface InfrastructureWebhookNormalized extends BaseNormalizedWebhook {
|
|
78
|
-
category: 'infrastructure';
|
|
79
|
-
event: string;
|
|
80
|
-
project_id?: string;
|
|
81
|
-
deployment_id?: string;
|
|
82
|
-
status?: 'queued' | 'building' | 'ready' | 'error' | 'unknown';
|
|
83
|
-
metadata?: Record<string, string>;
|
|
84
|
-
}
|
|
85
|
-
export interface UnknownNormalizedWebhook extends BaseNormalizedWebhook {
|
|
86
|
-
event: string;
|
|
87
|
-
warning?: string;
|
|
88
|
-
}
|
|
89
|
-
export type NormalizedPayloadByCategory = {
|
|
90
|
-
payment: PaymentWebhookNormalized;
|
|
91
|
-
auth: AuthWebhookNormalized;
|
|
92
|
-
ecommerce: EcommerceWebhookNormalized;
|
|
93
|
-
infrastructure: InfrastructureWebhookNormalized;
|
|
94
|
-
};
|
|
95
|
-
export type AnyNormalizedWebhook = PaymentWebhookNormalized | AuthWebhookNormalized | EcommerceWebhookNormalized | InfrastructureWebhookNormalized | UnknownNormalizedWebhook;
|
|
96
|
-
export interface NormalizeOptions {
|
|
97
|
-
enabled?: boolean;
|
|
98
|
-
category?: NormalizationCategory;
|
|
99
|
-
includeRaw?: boolean;
|
|
100
|
-
}
|
|
101
41
|
export interface WebhookVerificationResult<TPayload = unknown> {
|
|
102
42
|
isValid: boolean;
|
|
103
43
|
error?: string;
|
|
@@ -116,7 +56,6 @@ export interface WebhookConfig {
|
|
|
116
56
|
secret: string;
|
|
117
57
|
toleranceInSeconds?: number;
|
|
118
58
|
signatureConfig?: SignatureConfig;
|
|
119
|
-
normalize?: boolean | NormalizeOptions;
|
|
120
59
|
}
|
|
121
60
|
export interface MultiPlatformSecrets {
|
|
122
61
|
[platform: string]: string | undefined;
|
package/dist/types.js
CHANGED
|
@@ -6,6 +6,7 @@ var WebhookPlatformKeys;
|
|
|
6
6
|
WebhookPlatformKeys["GitHub"] = "github";
|
|
7
7
|
WebhookPlatformKeys["Stripe"] = "stripe";
|
|
8
8
|
WebhookPlatformKeys["Clerk"] = "clerk";
|
|
9
|
+
WebhookPlatformKeys["Svix"] = "svix";
|
|
9
10
|
WebhookPlatformKeys["DodoPayments"] = "dodopayments";
|
|
10
11
|
WebhookPlatformKeys["Shopify"] = "shopify";
|
|
11
12
|
WebhookPlatformKeys["Vercel"] = "vercel";
|
|
@@ -22,6 +23,8 @@ var WebhookPlatformKeys;
|
|
|
22
23
|
WebhookPlatformKeys["Grafana"] = "grafana";
|
|
23
24
|
WebhookPlatformKeys["Doppler"] = "doppler";
|
|
24
25
|
WebhookPlatformKeys["Sanity"] = "sanity";
|
|
26
|
+
WebhookPlatformKeys["Linear"] = "linear";
|
|
27
|
+
WebhookPlatformKeys["StandardWebhooks"] = "standardwebhooks";
|
|
25
28
|
WebhookPlatformKeys["Custom"] = "custom";
|
|
26
29
|
WebhookPlatformKeys["Unknown"] = "unknown";
|
|
27
30
|
})(WebhookPlatformKeys || (exports.WebhookPlatformKeys = WebhookPlatformKeys = {}));
|
|
@@ -5,6 +5,11 @@ export declare abstract class AlgorithmBasedVerifier extends WebhookVerifier {
|
|
|
5
5
|
protected platform: WebhookPlatform;
|
|
6
6
|
constructor(secret: string, config: SignatureConfig, platform: WebhookPlatform, toleranceInSeconds?: number);
|
|
7
7
|
abstract verify(request: Request): Promise<WebhookVerificationResult>;
|
|
8
|
+
protected getMissingSignatureMessage(): string;
|
|
9
|
+
protected getMissingTimestampMessage(): string;
|
|
10
|
+
protected getTimestampExpiredMessage(): string;
|
|
11
|
+
protected getInvalidSignatureMessage(): string;
|
|
12
|
+
protected getVerificationErrorMessage(error: Error): string;
|
|
8
13
|
protected parseDelimitedHeader(headerValue: string): Record<string, string>;
|
|
9
14
|
protected extractSignatures(request: Request): string[];
|
|
10
15
|
protected extractTimestamp(request: Request): number | null;
|
|
@@ -18,6 +23,7 @@ export declare abstract class AlgorithmBasedVerifier extends WebhookVerifier {
|
|
|
18
23
|
protected extractMetadata(request: Request): Record<string, any>;
|
|
19
24
|
}
|
|
20
25
|
export declare class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
26
|
+
private validateLinearReplayWindow;
|
|
21
27
|
private resolveSentryPayloadCandidates;
|
|
22
28
|
verify(request: Request): Promise<WebhookVerificationResult>;
|
|
23
29
|
}
|