@hookflo/tern 4.3.1-beta → 4.3.3-beta
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 +27 -0
- package/dist/adapters/cloudflare.d.ts +1 -2
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/express.d.ts +1 -2
- package/dist/adapters/express.js +1 -1
- package/dist/adapters/hono.d.ts +1 -2
- package/dist/adapters/hono.js +1 -1
- package/dist/adapters/nextjs.d.ts +1 -2
- package/dist/adapters/nextjs.js +1 -1
- package/dist/adapters/shared.js +5 -2
- package/dist/index.d.ts +3 -4
- package/dist/index.js +18 -18
- package/dist/types.d.ts +1 -64
- package/dist/verifiers/algorithms.d.ts +6 -0
- package/dist/verifiers/algorithms.js +56 -12
- 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
|
@@ -79,6 +79,27 @@ const result = await WebhookVerificationService.verifyAny(request, {
|
|
|
79
79
|
console.log(`Verified ${result.platform} webhook`);
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
+
### Twilio example
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { WebhookVerificationService } from '@hookflo/tern';
|
|
86
|
+
|
|
87
|
+
export async function POST(request: Request) {
|
|
88
|
+
const result = await WebhookVerificationService.verify(request, {
|
|
89
|
+
platform: 'twilio',
|
|
90
|
+
secret: process.env.TWILIO_AUTH_TOKEN!,
|
|
91
|
+
// Optional when behind proxies/CDNs if request.url differs from the public Twilio URL:
|
|
92
|
+
twilioBaseUrl: 'https://yourdomain.com/api/webhooks/twilio',
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (!result.isValid) {
|
|
96
|
+
return Response.json({ error: result.error }, { status: 400 });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return Response.json({ ok: true });
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
82
103
|
### Core SDK (runtime-agnostic)
|
|
83
104
|
|
|
84
105
|
Use Tern without framework adapters in any runtime that supports the Web `Request` API.
|
|
@@ -171,6 +192,9 @@ app.post('/webhooks/stripe', createWebhookHandler({
|
|
|
171
192
|
|
|
172
193
|
## Supported Platforms
|
|
173
194
|
|
|
195
|
+
> ⚠️ Normalization is no longer supported in Tern and has been removed from the public verification APIs.
|
|
196
|
+
|
|
197
|
+
|
|
174
198
|
| Platform | Algorithm | Status |
|
|
175
199
|
|---|---|---|
|
|
176
200
|
| **Stripe** | HMAC-SHA256 | ✅ Tested |
|
|
@@ -407,6 +431,9 @@ interface WebhookVerificationResult {
|
|
|
407
431
|
|
|
408
432
|
## Troubleshooting
|
|
409
433
|
|
|
434
|
+
- **Twilio invalid signature behind proxies/CDNs**: if your runtime `request.url` differs from the public Twilio webhook URL, pass `twilioBaseUrl` in `WebhookVerificationService.verify(...)` for platform `twilio`.
|
|
435
|
+
|
|
436
|
+
|
|
410
437
|
**`Module not found: Can't resolve "@hookflo/tern/nextjs"`**
|
|
411
438
|
|
|
412
439
|
```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,7 @@ function createWebhookHandler(options) {
|
|
|
39
39
|
}
|
|
40
40
|
return response;
|
|
41
41
|
}
|
|
42
|
-
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, options.platform, secret, options.toleranceInSeconds
|
|
42
|
+
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, options.platform, secret, options.toleranceInSeconds);
|
|
43
43
|
if (!result.isValid) {
|
|
44
44
|
return Response.json({ error: result.error, errorCode: result.errorCode, platform: result.platform, metadata: result.metadata }, { status: 400 });
|
|
45
45
|
}
|
|
@@ -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,7 @@ function createWebhookMiddleware(options) {
|
|
|
48
48
|
}
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
|
-
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(webRequest, options.platform, options.secret, options.toleranceInSeconds
|
|
51
|
+
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(webRequest, options.platform, options.secret, options.toleranceInSeconds);
|
|
52
52
|
if (!result.isValid) {
|
|
53
53
|
res.status(400).json({
|
|
54
54
|
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,7 @@ function createWebhookHandler(options) {
|
|
|
35
35
|
}
|
|
36
36
|
return response;
|
|
37
37
|
}
|
|
38
|
-
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, options.platform, options.secret, options.toleranceInSeconds
|
|
38
|
+
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, options.platform, options.secret, options.toleranceInSeconds);
|
|
39
39
|
if (!result.isValid) {
|
|
40
40
|
return c.json({
|
|
41
41
|
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,7 @@ function createWebhookHandler(options) {
|
|
|
34
34
|
}
|
|
35
35
|
return response;
|
|
36
36
|
}
|
|
37
|
-
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, options.platform, options.secret, options.toleranceInSeconds
|
|
37
|
+
const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, options.platform, options.secret, options.toleranceInSeconds);
|
|
38
38
|
if (!result.isValid) {
|
|
39
39
|
return Response.json({ error: result.error, errorCode: result.errorCode, platform: result.platform, metadata: result.metadata }, { status: 400 });
|
|
40
40
|
}
|
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;
|
|
@@ -32,7 +32,6 @@ export * from './types';
|
|
|
32
32
|
export { getPlatformAlgorithmConfig, platformUsesAlgorithm, getPlatformsUsingAlgorithm, validateSignatureConfig, } 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.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,21 @@ 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
|
+
...(config.platform === 'twilio' && config.twilioBaseUrl
|
|
74
|
+
? { twilioBaseUrl: config.twilioBaseUrl }
|
|
75
|
+
: {}),
|
|
76
|
+
},
|
|
77
|
+
};
|
|
73
78
|
// Use custom verifiers for special cases (token-based, etc.)
|
|
74
|
-
if (
|
|
75
|
-
return (0, custom_algorithms_1.createCustomVerifier)(secret,
|
|
79
|
+
if (effectiveSignatureConfig.algorithm === 'custom') {
|
|
80
|
+
return (0, custom_algorithms_1.createCustomVerifier)(secret, effectiveSignatureConfig, toleranceInSeconds);
|
|
76
81
|
}
|
|
77
82
|
// Use algorithm-based verifiers for standard algorithms
|
|
78
|
-
return (0, algorithms_1.createAlgorithmVerifier)(secret,
|
|
83
|
+
return (0, algorithms_1.createAlgorithmVerifier)(secret, effectiveSignatureConfig, config.platform, toleranceInSeconds);
|
|
79
84
|
}
|
|
80
85
|
static getLegacyVerifier(config) {
|
|
81
86
|
// For legacy support, we'll use the algorithm-based approach
|
|
@@ -87,29 +92,28 @@ class WebhookVerificationService {
|
|
|
87
92
|
return this.createAlgorithmBasedVerifier(configWithSignature);
|
|
88
93
|
}
|
|
89
94
|
// New method to create verifier using platform algorithm config
|
|
90
|
-
static async verifyWithPlatformConfig(request, platform, secret, toleranceInSeconds = 300
|
|
95
|
+
static async verifyWithPlatformConfig(request, platform, secret, toleranceInSeconds = 300) {
|
|
91
96
|
const platformConfig = (0, algorithms_2.getPlatformAlgorithmConfig)(platform);
|
|
92
97
|
const config = {
|
|
93
98
|
platform,
|
|
94
99
|
secret,
|
|
95
100
|
toleranceInSeconds,
|
|
96
|
-
signatureConfig: platformConfig.signatureConfig
|
|
97
|
-
normalize,
|
|
101
|
+
signatureConfig: platformConfig.signatureConfig
|
|
98
102
|
};
|
|
99
103
|
return this.verify(request, config);
|
|
100
104
|
}
|
|
101
|
-
static async verifyAny(request, secrets, toleranceInSeconds = 300
|
|
105
|
+
static async verifyAny(request, secrets, toleranceInSeconds = 300) {
|
|
102
106
|
const requestClone = request.clone();
|
|
103
107
|
const detectedPlatform = this.detectPlatform(requestClone);
|
|
104
108
|
if (detectedPlatform !== 'unknown' && secrets[detectedPlatform]) {
|
|
105
|
-
return this.verifyWithPlatformConfig(requestClone, detectedPlatform, secrets[detectedPlatform], toleranceInSeconds
|
|
109
|
+
return this.verifyWithPlatformConfig(requestClone, detectedPlatform, secrets[detectedPlatform], toleranceInSeconds);
|
|
106
110
|
}
|
|
107
111
|
const failedAttempts = [];
|
|
108
112
|
const verificationResults = await Promise.all(Object.entries(secrets)
|
|
109
113
|
.filter(([, secret]) => Boolean(secret))
|
|
110
114
|
.map(async ([platform, secret]) => {
|
|
111
115
|
const normalizedPlatform = platform.toLowerCase();
|
|
112
|
-
const result = await this.verifyWithPlatformConfig(requestClone, normalizedPlatform, secret, toleranceInSeconds
|
|
116
|
+
const result = await this.verifyWithPlatformConfig(requestClone, normalizedPlatform, secret, toleranceInSeconds);
|
|
113
117
|
return {
|
|
114
118
|
platform: normalizedPlatform,
|
|
115
119
|
result,
|
|
@@ -375,10 +379,6 @@ var algorithms_4 = require("./verifiers/algorithms");
|
|
|
375
379
|
Object.defineProperty(exports, "createAlgorithmVerifier", { enumerable: true, get: function () { return algorithms_4.createAlgorithmVerifier; } });
|
|
376
380
|
var custom_algorithms_2 = require("./verifiers/custom-algorithms");
|
|
377
381
|
Object.defineProperty(exports, "createCustomVerifier", { enumerable: true, get: function () { return custom_algorithms_2.createCustomVerifier; } });
|
|
378
|
-
var simple_2 = require("./normalization/simple");
|
|
379
|
-
Object.defineProperty(exports, "normalizePayload", { enumerable: true, get: function () { return simple_2.normalizePayload; } });
|
|
380
|
-
Object.defineProperty(exports, "getPlatformNormalizationCategory", { enumerable: true, get: function () { return simple_2.getPlatformNormalizationCategory; } });
|
|
381
|
-
Object.defineProperty(exports, "getPlatformsByCategory", { enumerable: true, get: function () { return simple_2.getPlatformsByCategory; } });
|
|
382
382
|
__exportStar(require("./adapters"), exports);
|
|
383
383
|
__exportStar(require("./alerts"), exports);
|
|
384
384
|
exports.default = WebhookVerificationService;
|
package/dist/types.d.ts
CHANGED
|
@@ -39,69 +39,6 @@ export interface SignatureConfig {
|
|
|
39
39
|
customConfig?: Record<string, any>;
|
|
40
40
|
}
|
|
41
41
|
export type WebhookErrorCode = 'MISSING_SIGNATURE' | 'INVALID_SIGNATURE' | 'TIMESTAMP_EXPIRED' | 'MISSING_TOKEN' | 'INVALID_TOKEN' | 'PLATFORM_NOT_SUPPORTED' | 'NORMALIZATION_ERROR' | 'VERIFICATION_ERROR';
|
|
42
|
-
export type NormalizationCategory = 'payment' | 'auth' | 'ecommerce' | 'infrastructure';
|
|
43
|
-
export interface BaseNormalizedWebhook {
|
|
44
|
-
category: NormalizationCategory;
|
|
45
|
-
event: string;
|
|
46
|
-
_platform: WebhookPlatform | string;
|
|
47
|
-
_raw: unknown;
|
|
48
|
-
occurred_at?: string;
|
|
49
|
-
}
|
|
50
|
-
export type PaymentWebhookEvent = 'payment.succeeded' | 'payment.failed' | 'payment.refunded' | 'subscription.created' | 'subscription.cancelled' | 'payment.unknown';
|
|
51
|
-
export interface PaymentWebhookNormalized extends BaseNormalizedWebhook {
|
|
52
|
-
category: 'payment';
|
|
53
|
-
event: PaymentWebhookEvent;
|
|
54
|
-
amount?: number;
|
|
55
|
-
currency?: string;
|
|
56
|
-
customer_id?: string;
|
|
57
|
-
transaction_id?: string;
|
|
58
|
-
subscription_id?: string;
|
|
59
|
-
refund_amount?: number;
|
|
60
|
-
failure_reason?: string;
|
|
61
|
-
metadata?: Record<string, string>;
|
|
62
|
-
}
|
|
63
|
-
export type AuthWebhookEvent = 'user.created' | 'user.updated' | 'user.deleted' | 'session.started' | 'session.ended' | 'auth.unknown';
|
|
64
|
-
export interface AuthWebhookNormalized extends BaseNormalizedWebhook {
|
|
65
|
-
category: 'auth';
|
|
66
|
-
event: AuthWebhookEvent;
|
|
67
|
-
user_id?: string;
|
|
68
|
-
email?: string;
|
|
69
|
-
phone?: string;
|
|
70
|
-
metadata?: Record<string, string>;
|
|
71
|
-
}
|
|
72
|
-
export interface EcommerceWebhookNormalized extends BaseNormalizedWebhook {
|
|
73
|
-
category: 'ecommerce';
|
|
74
|
-
event: string;
|
|
75
|
-
order_id?: string;
|
|
76
|
-
customer_id?: string;
|
|
77
|
-
amount?: number;
|
|
78
|
-
currency?: string;
|
|
79
|
-
metadata?: Record<string, string>;
|
|
80
|
-
}
|
|
81
|
-
export interface InfrastructureWebhookNormalized extends BaseNormalizedWebhook {
|
|
82
|
-
category: 'infrastructure';
|
|
83
|
-
event: string;
|
|
84
|
-
project_id?: string;
|
|
85
|
-
deployment_id?: string;
|
|
86
|
-
status?: 'queued' | 'building' | 'ready' | 'error' | 'unknown';
|
|
87
|
-
metadata?: Record<string, string>;
|
|
88
|
-
}
|
|
89
|
-
export interface UnknownNormalizedWebhook extends BaseNormalizedWebhook {
|
|
90
|
-
event: string;
|
|
91
|
-
warning?: string;
|
|
92
|
-
}
|
|
93
|
-
export type NormalizedPayloadByCategory = {
|
|
94
|
-
payment: PaymentWebhookNormalized;
|
|
95
|
-
auth: AuthWebhookNormalized;
|
|
96
|
-
ecommerce: EcommerceWebhookNormalized;
|
|
97
|
-
infrastructure: InfrastructureWebhookNormalized;
|
|
98
|
-
};
|
|
99
|
-
export type AnyNormalizedWebhook = PaymentWebhookNormalized | AuthWebhookNormalized | EcommerceWebhookNormalized | InfrastructureWebhookNormalized | UnknownNormalizedWebhook;
|
|
100
|
-
export interface NormalizeOptions {
|
|
101
|
-
enabled?: boolean;
|
|
102
|
-
category?: NormalizationCategory;
|
|
103
|
-
includeRaw?: boolean;
|
|
104
|
-
}
|
|
105
42
|
export interface WebhookVerificationResult<TPayload = unknown> {
|
|
106
43
|
isValid: boolean;
|
|
107
44
|
error?: string;
|
|
@@ -120,7 +57,7 @@ export interface WebhookConfig {
|
|
|
120
57
|
secret: string;
|
|
121
58
|
toleranceInSeconds?: number;
|
|
122
59
|
signatureConfig?: SignatureConfig;
|
|
123
|
-
|
|
60
|
+
twilioBaseUrl?: string;
|
|
124
61
|
}
|
|
125
62
|
export interface MultiPlatformSecrets {
|
|
126
63
|
[platform: string]: string | undefined;
|
|
@@ -5,11 +5,17 @@ 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;
|
|
11
16
|
protected extractTimestampFromSignature(request: Request): number | null;
|
|
12
17
|
protected requiresTimestamp(): boolean;
|
|
18
|
+
protected resolveTwilioSignatureUrl(request: Request): string;
|
|
13
19
|
protected formatPayload(rawBody: string, request: Request): string;
|
|
14
20
|
protected formatCustomPayload(rawBody: string, request: Request): string;
|
|
15
21
|
protected verifyHMAC(payload: string, signature: string, algorithm?: string): boolean;
|
|
@@ -13,6 +13,40 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
13
13
|
this.config = config;
|
|
14
14
|
this.platform = platform;
|
|
15
15
|
}
|
|
16
|
+
getMissingSignatureMessage() {
|
|
17
|
+
return `Missing signature header: ${this.config.headerName}. Ensure your webhook provider sends this header and your adapter forwards it unchanged.`;
|
|
18
|
+
}
|
|
19
|
+
getMissingTimestampMessage() {
|
|
20
|
+
const timestampHeader = this.config.timestampHeader || this.config.customConfig?.timestampHeader || 'timestamp';
|
|
21
|
+
return `Missing required timestamp for webhook verification. Verify header '${timestampHeader}' is present and passed through by your framework/proxy.`;
|
|
22
|
+
}
|
|
23
|
+
getTimestampExpiredMessage() {
|
|
24
|
+
return 'Webhook timestamp expired. Check server clock drift and increase tolerance only if your provider allows it.';
|
|
25
|
+
}
|
|
26
|
+
getInvalidSignatureMessage() {
|
|
27
|
+
const genericHint = `Invalid signature for ${this.platform}. Confirm webhook secret, raw request body handling, and signature header formatting.`;
|
|
28
|
+
switch (this.platform) {
|
|
29
|
+
case 'twilio':
|
|
30
|
+
return `${genericHint} Twilio also requires the exact public URL used for signing (including query params like bodySHA256). Use twilioBaseUrl if your runtime URL is rewritten behind a proxy.`;
|
|
31
|
+
case 'stripe':
|
|
32
|
+
return `${genericHint} Stripe signatures require the exact raw body and Stripe-Signature timestamp/value pair.`;
|
|
33
|
+
case 'github':
|
|
34
|
+
return `${genericHint} GitHub signatures must include the sha256= prefix from x-hub-signature-256.`;
|
|
35
|
+
case 'svix':
|
|
36
|
+
case 'clerk':
|
|
37
|
+
case 'dodopayments':
|
|
38
|
+
case 'replicateai':
|
|
39
|
+
case 'polar':
|
|
40
|
+
return `${genericHint} Standard Webhooks payload must be signed as id.timestamp.body and secrets may need whsec_ base64 decoding.`;
|
|
41
|
+
case 'pagerduty':
|
|
42
|
+
return `${genericHint} PagerDuty expects v1=<hex> signature values from x-pagerduty-signature.`;
|
|
43
|
+
default:
|
|
44
|
+
return genericHint;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
getVerificationErrorMessage(error) {
|
|
48
|
+
return `${this.platform} verification error: ${error.message}. Check webhook secret configuration and ensure your framework preserves raw body + headers.`;
|
|
49
|
+
}
|
|
16
50
|
parseDelimitedHeader(headerValue) {
|
|
17
51
|
const parts = headerValue.split(/[;,]/);
|
|
18
52
|
const values = {};
|
|
@@ -152,6 +186,16 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
152
186
|
return true;
|
|
153
187
|
return false;
|
|
154
188
|
}
|
|
189
|
+
resolveTwilioSignatureUrl(request) {
|
|
190
|
+
const overrideBaseUrl = this.config.customConfig?.twilioBaseUrl;
|
|
191
|
+
if (!overrideBaseUrl) {
|
|
192
|
+
return request.url;
|
|
193
|
+
}
|
|
194
|
+
const requestUrl = new URL(request.url);
|
|
195
|
+
const baseUrl = new URL(overrideBaseUrl);
|
|
196
|
+
baseUrl.search = requestUrl.search;
|
|
197
|
+
return baseUrl.toString();
|
|
198
|
+
}
|
|
155
199
|
formatPayload(rawBody, request) {
|
|
156
200
|
switch (this.config.payloadFormat) {
|
|
157
201
|
case "timestamped": {
|
|
@@ -196,7 +240,7 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
196
240
|
}
|
|
197
241
|
if (customFormat.includes('{url}')) {
|
|
198
242
|
return customFormat
|
|
199
|
-
.replace('{url}', request.url)
|
|
243
|
+
.replace('{url}', this.platform === 'twilio' ? this.resolveTwilioSignatureUrl(request) : request.url)
|
|
200
244
|
.replace('{body}', rawBody);
|
|
201
245
|
}
|
|
202
246
|
if (customFormat.includes("{timestamp}") &&
|
|
@@ -306,7 +350,7 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
306
350
|
if (this.platform !== 'twilio' || !this.config.customConfig?.validateBodySHA256) {
|
|
307
351
|
return null;
|
|
308
352
|
}
|
|
309
|
-
const url = new URL(request
|
|
353
|
+
const url = new URL(this.resolveTwilioSignatureUrl(request));
|
|
310
354
|
const bodySha = url.searchParams.get('bodySHA256');
|
|
311
355
|
if (!bodySha)
|
|
312
356
|
return null;
|
|
@@ -349,7 +393,7 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
349
393
|
if (signatures.length === 0) {
|
|
350
394
|
return {
|
|
351
395
|
isValid: false,
|
|
352
|
-
error:
|
|
396
|
+
error: this.getMissingSignatureMessage(),
|
|
353
397
|
errorCode: "MISSING_SIGNATURE",
|
|
354
398
|
platform: this.platform,
|
|
355
399
|
};
|
|
@@ -383,7 +427,7 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
383
427
|
if (this.requiresTimestamp() && !timestamp) {
|
|
384
428
|
return {
|
|
385
429
|
isValid: false,
|
|
386
|
-
error:
|
|
430
|
+
error: this.getMissingTimestampMessage(),
|
|
387
431
|
errorCode: 'MISSING_SIGNATURE',
|
|
388
432
|
platform: this.platform,
|
|
389
433
|
};
|
|
@@ -391,7 +435,7 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
391
435
|
if (timestamp && !this.isTimestampValid(timestamp)) {
|
|
392
436
|
return {
|
|
393
437
|
isValid: false,
|
|
394
|
-
error:
|
|
438
|
+
error: this.getTimestampExpiredMessage(),
|
|
395
439
|
errorCode: "TIMESTAMP_EXPIRED",
|
|
396
440
|
platform: this.platform,
|
|
397
441
|
};
|
|
@@ -423,7 +467,7 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
423
467
|
if (!isValid) {
|
|
424
468
|
return {
|
|
425
469
|
isValid: false,
|
|
426
|
-
error:
|
|
470
|
+
error: this.getInvalidSignatureMessage(),
|
|
427
471
|
errorCode: "INVALID_SIGNATURE",
|
|
428
472
|
platform: this.platform,
|
|
429
473
|
};
|
|
@@ -452,7 +496,7 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
452
496
|
catch (error) {
|
|
453
497
|
return {
|
|
454
498
|
isValid: false,
|
|
455
|
-
error:
|
|
499
|
+
error: this.getVerificationErrorMessage(error),
|
|
456
500
|
errorCode: "VERIFICATION_ERROR",
|
|
457
501
|
platform: this.platform,
|
|
458
502
|
};
|
|
@@ -561,7 +605,7 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
|
|
|
561
605
|
if (signatures.length === 0) {
|
|
562
606
|
return {
|
|
563
607
|
isValid: false,
|
|
564
|
-
error:
|
|
608
|
+
error: this.getMissingSignatureMessage(),
|
|
565
609
|
errorCode: "MISSING_SIGNATURE",
|
|
566
610
|
platform: this.platform,
|
|
567
611
|
};
|
|
@@ -575,7 +619,7 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
|
|
|
575
619
|
if (!timestampStr) {
|
|
576
620
|
return {
|
|
577
621
|
isValid: false,
|
|
578
|
-
error:
|
|
622
|
+
error: this.getMissingTimestampMessage(),
|
|
579
623
|
errorCode: 'MISSING_SIGNATURE',
|
|
580
624
|
platform: this.platform,
|
|
581
625
|
};
|
|
@@ -584,7 +628,7 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
|
|
|
584
628
|
if (!this.isTimestampValid(timestamp)) {
|
|
585
629
|
return {
|
|
586
630
|
isValid: false,
|
|
587
|
-
error:
|
|
631
|
+
error: this.getTimestampExpiredMessage(),
|
|
588
632
|
errorCode: "TIMESTAMP_EXPIRED",
|
|
589
633
|
platform: this.platform,
|
|
590
634
|
};
|
|
@@ -644,7 +688,7 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
|
|
|
644
688
|
if (!isValid) {
|
|
645
689
|
return {
|
|
646
690
|
isValid: false,
|
|
647
|
-
error:
|
|
691
|
+
error: this.getInvalidSignatureMessage(),
|
|
648
692
|
errorCode: "INVALID_SIGNATURE",
|
|
649
693
|
platform: this.platform,
|
|
650
694
|
};
|
|
@@ -673,7 +717,7 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
|
|
|
673
717
|
catch (error) {
|
|
674
718
|
return {
|
|
675
719
|
isValid: false,
|
|
676
|
-
error:
|
|
720
|
+
error: this.getVerificationErrorMessage(error),
|
|
677
721
|
errorCode: "VERIFICATION_ERROR",
|
|
678
722
|
platform: this.platform,
|
|
679
723
|
};
|
package/package.json
CHANGED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { BaseTemplate, CreateSchemaInput, NormalizedResult, ProviderInfo, TemplateCategory, TransformParams, UpdateSchemaInput, UserSchema } from './types';
|
|
2
|
-
import { StorageAdapter } from './storage/interface';
|
|
3
|
-
export declare class Normalizer {
|
|
4
|
-
private readonly storage;
|
|
5
|
-
private engine;
|
|
6
|
-
constructor(storage?: StorageAdapter);
|
|
7
|
-
getBaseTemplates(): Promise<BaseTemplate[]>;
|
|
8
|
-
getProviders(category?: TemplateCategory): Promise<ProviderInfo[]>;
|
|
9
|
-
createSchema(input: CreateSchemaInput): Promise<UserSchema>;
|
|
10
|
-
updateSchema(schemaId: string, updates: UpdateSchemaInput): Promise<void>;
|
|
11
|
-
getSchema(id: string): Promise<UserSchema | null>;
|
|
12
|
-
transform(params: TransformParams): Promise<NormalizedResult>;
|
|
13
|
-
validateSchema(schema: UserSchema): Promise<{
|
|
14
|
-
valid: boolean;
|
|
15
|
-
errors: string[];
|
|
16
|
-
}>;
|
|
17
|
-
}
|
|
18
|
-
export * from './types';
|
|
19
|
-
export * from './storage/interface';
|
|
20
|
-
export { InMemoryStorageAdapter } from './storage/memory';
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
-
};
|
|
16
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.InMemoryStorageAdapter = exports.Normalizer = void 0;
|
|
18
|
-
const registry_1 = require("./providers/registry");
|
|
19
|
-
const registry_2 = require("./templates/registry");
|
|
20
|
-
const memory_1 = require("./storage/memory");
|
|
21
|
-
const engine_1 = require("./transformer/engine");
|
|
22
|
-
const validator_1 = require("./transformer/validator");
|
|
23
|
-
class Normalizer {
|
|
24
|
-
constructor(storage = new memory_1.InMemoryStorageAdapter()) {
|
|
25
|
-
this.storage = storage;
|
|
26
|
-
this.engine = new engine_1.NormalizationEngine(storage, new validator_1.SchemaValidator());
|
|
27
|
-
}
|
|
28
|
-
async getBaseTemplates() {
|
|
29
|
-
return this.storage.listBaseTemplates();
|
|
30
|
-
}
|
|
31
|
-
async getProviders(category) {
|
|
32
|
-
return registry_1.providerRegistry.list(category);
|
|
33
|
-
}
|
|
34
|
-
async createSchema(input) {
|
|
35
|
-
const schema = {
|
|
36
|
-
id: generateId(),
|
|
37
|
-
userId: input.userId,
|
|
38
|
-
baseTemplateId: input.baseTemplateId,
|
|
39
|
-
category: input.category,
|
|
40
|
-
fields: input.fields,
|
|
41
|
-
providerMappings: input.providerMappings,
|
|
42
|
-
createdAt: new Date(),
|
|
43
|
-
updatedAt: new Date(),
|
|
44
|
-
};
|
|
45
|
-
await this.storage.saveSchema(schema);
|
|
46
|
-
return schema;
|
|
47
|
-
}
|
|
48
|
-
async updateSchema(schemaId, updates) {
|
|
49
|
-
await this.storage.updateSchema(schemaId, updates);
|
|
50
|
-
}
|
|
51
|
-
async getSchema(id) {
|
|
52
|
-
return this.storage.getSchema(id);
|
|
53
|
-
}
|
|
54
|
-
async transform(params) {
|
|
55
|
-
return this.engine.transform(params);
|
|
56
|
-
}
|
|
57
|
-
async validateSchema(schema) {
|
|
58
|
-
const base = (await this.storage.getBaseTemplate(schema.baseTemplateId))
|
|
59
|
-
?? registry_2.templateRegistry.getById(schema.baseTemplateId);
|
|
60
|
-
if (!base) {
|
|
61
|
-
return {
|
|
62
|
-
valid: false,
|
|
63
|
-
errors: [`Base template not found: ${schema.baseTemplateId}`],
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
const validator = new validator_1.SchemaValidator();
|
|
67
|
-
return validator.validateSchema(schema, base);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
exports.Normalizer = Normalizer;
|
|
71
|
-
function generateId() {
|
|
72
|
-
// Simple non-crypto unique ID generator for framework default
|
|
73
|
-
return (`sch_${Math.random().toString(36).slice(2, 10)}${Date.now().toString(36)}`);
|
|
74
|
-
}
|
|
75
|
-
__exportStar(require("./types"), exports);
|
|
76
|
-
__exportStar(require("./storage/interface"), exports);
|
|
77
|
-
var memory_2 = require("./storage/memory");
|
|
78
|
-
Object.defineProperty(exports, "InMemoryStorageAdapter", { enumerable: true, get: function () { return memory_2.InMemoryStorageAdapter; } });
|