@hookflo/tern 4.3.0-beta.0 → 4.3.0-beta.1
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 +31 -0
- package/dist/adapters/cloudflare.d.ts +2 -2
- package/dist/adapters/cloudflare.js +6 -1
- package/dist/adapters/express.d.ts +2 -2
- package/dist/adapters/express.js +6 -1
- package/dist/adapters/hono.d.ts +2 -2
- package/dist/adapters/hono.js +6 -1
- package/dist/adapters/nextjs.d.ts +2 -2
- package/dist/adapters/nextjs.js +6 -1
- package/dist/adapters/shared.js +5 -2
- package/dist/index.d.ts +3 -4
- package/dist/index.js +29 -19
- package/dist/platforms/algorithms.js +65 -0
- package/dist/types.d.ts +6 -65
- package/dist/types.js +4 -0
- package/dist/verifiers/algorithms.d.ts +8 -0
- package/dist/verifiers/algorithms.js +145 -17
- 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
|
@@ -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 = {};
|
|
@@ -63,9 +97,34 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
63
97
|
}
|
|
64
98
|
// Accept "v1=<signature>" variants used by some providers/docs.
|
|
65
99
|
if (sig.startsWith("v1=")) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
100
|
+
if (this.config.customConfig?.comparePrefixed) {
|
|
101
|
+
for (const fragment of sig.split(',')) {
|
|
102
|
+
const candidate = fragment.trim();
|
|
103
|
+
if (candidate.startsWith('v1=')) {
|
|
104
|
+
normalized.push(candidate);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const [, value] = sig.split("=", 2);
|
|
110
|
+
if (value) {
|
|
111
|
+
normalized.push(value.trim());
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
for (const fragment of sig.split(',')) {
|
|
117
|
+
const candidate = fragment.trim();
|
|
118
|
+
if (candidate.startsWith('v1=')) {
|
|
119
|
+
if (this.config.customConfig?.comparePrefixed) {
|
|
120
|
+
normalized.push(candidate);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const [, value] = candidate.split('=', 2);
|
|
124
|
+
if (value) {
|
|
125
|
+
normalized.push(value.trim());
|
|
126
|
+
}
|
|
127
|
+
}
|
|
69
128
|
}
|
|
70
129
|
}
|
|
71
130
|
}
|
|
@@ -77,7 +136,9 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
77
136
|
extractTimestamp(request) {
|
|
78
137
|
if (!this.config.timestampHeader)
|
|
79
138
|
return null;
|
|
80
|
-
const timestampHeader = request.headers.get(this.config.timestampHeader)
|
|
139
|
+
const timestampHeader = request.headers.get(this.config.timestampHeader)
|
|
140
|
+
|| this.config.customConfig?.timestampHeaderAliases?.map((alias) => request.headers.get(alias)).find(Boolean)
|
|
141
|
+
|| null;
|
|
81
142
|
if (!timestampHeader)
|
|
82
143
|
return null;
|
|
83
144
|
switch (this.config.timestampFormat) {
|
|
@@ -108,7 +169,7 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
108
169
|
return true;
|
|
109
170
|
// These platforms have timestampHeader in config but timestamp
|
|
110
171
|
// is optional in their spec — validate only if present, never mandate
|
|
111
|
-
const optionalTimestampPlatforms = ['vercel', 'sentry', 'grafana'];
|
|
172
|
+
const optionalTimestampPlatforms = ['vercel', 'sentry', 'grafana', 'twilio'];
|
|
112
173
|
if (optionalTimestampPlatforms.includes(this.platform))
|
|
113
174
|
return false;
|
|
114
175
|
// For all other platforms: infer from config
|
|
@@ -125,6 +186,16 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
125
186
|
return true;
|
|
126
187
|
return false;
|
|
127
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
|
+
}
|
|
128
199
|
formatPayload(rawBody, request) {
|
|
129
200
|
switch (this.config.payloadFormat) {
|
|
130
201
|
case "timestamped": {
|
|
@@ -152,7 +223,7 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
152
223
|
}
|
|
153
224
|
const customFormat = this.config.customConfig.payloadFormat;
|
|
154
225
|
if (customFormat.includes("{id}") && customFormat.includes("{timestamp}")) {
|
|
155
|
-
const id = request.headers.get(this.config.customConfig.idHeader || "x-webhook-id");
|
|
226
|
+
const id = request.headers.get(this.config.customConfig.idHeader || "x-webhook-id") || this.config.customConfig?.idHeaderAliases?.map((alias) => request.headers.get(alias)).find(Boolean);
|
|
156
227
|
const timestamp = request.headers.get(this.config.timestampHeader ||
|
|
157
228
|
this.config.customConfig?.timestampHeader ||
|
|
158
229
|
"x-webhook-timestamp");
|
|
@@ -167,6 +238,11 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
167
238
|
.replace("{timestamp}", timestamp.trim() || "")
|
|
168
239
|
.replace("{body}", rawBody);
|
|
169
240
|
}
|
|
241
|
+
if (customFormat.includes('{url}')) {
|
|
242
|
+
return customFormat
|
|
243
|
+
.replace('{url}', this.platform === 'twilio' ? this.resolveTwilioSignatureUrl(request) : request.url)
|
|
244
|
+
.replace('{body}', rawBody);
|
|
245
|
+
}
|
|
170
246
|
if (customFormat.includes("{timestamp}") &&
|
|
171
247
|
customFormat.includes("{body}")) {
|
|
172
248
|
const timestamp = this.extractTimestampFromSignature(request) ||
|
|
@@ -250,6 +326,40 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
|
|
|
250
326
|
}
|
|
251
327
|
exports.AlgorithmBasedVerifier = AlgorithmBasedVerifier;
|
|
252
328
|
class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
329
|
+
validateLinearReplayWindow(rawBody) {
|
|
330
|
+
if (this.platform !== 'linear')
|
|
331
|
+
return null;
|
|
332
|
+
try {
|
|
333
|
+
const parsed = JSON.parse(rawBody);
|
|
334
|
+
const rawTimestamp = parsed.webhookTimestamp;
|
|
335
|
+
const timestampMs = Number(rawTimestamp);
|
|
336
|
+
if (!Number.isFinite(timestampMs)) {
|
|
337
|
+
return 'Missing or invalid Linear webhookTimestamp';
|
|
338
|
+
}
|
|
339
|
+
const replayToleranceMs = this.config.customConfig?.replayToleranceMs || 60000;
|
|
340
|
+
if (Math.abs(Date.now() - timestampMs) > replayToleranceMs) {
|
|
341
|
+
return 'Linear webhook timestamp is outside the replay window';
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
return 'Linear webhook replay check requires JSON payload';
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
validateTwilioBodyHash(rawBody, request) {
|
|
350
|
+
if (this.platform !== 'twilio' || !this.config.customConfig?.validateBodySHA256) {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
const url = new URL(this.resolveTwilioSignatureUrl(request));
|
|
354
|
+
const bodySha = url.searchParams.get('bodySHA256');
|
|
355
|
+
if (!bodySha)
|
|
356
|
+
return null;
|
|
357
|
+
const computed = (0, crypto_1.createHash)('sha256').update(rawBody).digest('hex');
|
|
358
|
+
if (!this.safeCompare(computed, bodySha)) {
|
|
359
|
+
return 'Twilio bodySHA256 query param does not match payload hash';
|
|
360
|
+
}
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
253
363
|
resolveSentryPayloadCandidates(rawBody, request) {
|
|
254
364
|
const candidates = [
|
|
255
365
|
this.formatPayload(rawBody, request),
|
|
@@ -283,12 +393,30 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
283
393
|
if (signatures.length === 0) {
|
|
284
394
|
return {
|
|
285
395
|
isValid: false,
|
|
286
|
-
error:
|
|
396
|
+
error: this.getMissingSignatureMessage(),
|
|
287
397
|
errorCode: "MISSING_SIGNATURE",
|
|
288
398
|
platform: this.platform,
|
|
289
399
|
};
|
|
290
400
|
}
|
|
291
401
|
const rawBody = await request.text();
|
|
402
|
+
const linearReplayError = this.validateLinearReplayWindow(rawBody);
|
|
403
|
+
if (linearReplayError) {
|
|
404
|
+
return {
|
|
405
|
+
isValid: false,
|
|
406
|
+
error: linearReplayError,
|
|
407
|
+
errorCode: 'TIMESTAMP_EXPIRED',
|
|
408
|
+
platform: this.platform,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
const twilioBodyHashError = this.validateTwilioBodyHash(rawBody, request);
|
|
412
|
+
if (twilioBodyHashError) {
|
|
413
|
+
return {
|
|
414
|
+
isValid: false,
|
|
415
|
+
error: twilioBodyHashError,
|
|
416
|
+
errorCode: 'INVALID_SIGNATURE',
|
|
417
|
+
platform: this.platform,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
292
420
|
let timestamp = null;
|
|
293
421
|
if (this.config.headerFormat === "comma-separated") {
|
|
294
422
|
timestamp = this.extractTimestampFromSignature(request);
|
|
@@ -299,7 +427,7 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
299
427
|
if (this.requiresTimestamp() && !timestamp) {
|
|
300
428
|
return {
|
|
301
429
|
isValid: false,
|
|
302
|
-
error:
|
|
430
|
+
error: this.getMissingTimestampMessage(),
|
|
303
431
|
errorCode: 'MISSING_SIGNATURE',
|
|
304
432
|
platform: this.platform,
|
|
305
433
|
};
|
|
@@ -307,7 +435,7 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
307
435
|
if (timestamp && !this.isTimestampValid(timestamp)) {
|
|
308
436
|
return {
|
|
309
437
|
isValid: false,
|
|
310
|
-
error:
|
|
438
|
+
error: this.getTimestampExpiredMessage(),
|
|
311
439
|
errorCode: "TIMESTAMP_EXPIRED",
|
|
312
440
|
platform: this.platform,
|
|
313
441
|
};
|
|
@@ -322,7 +450,7 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
322
450
|
if (this.config.customConfig?.encoding === "base64") {
|
|
323
451
|
isValid = this.verifyHMACWithBase64(payload, signature, algorithm);
|
|
324
452
|
}
|
|
325
|
-
else if (this.config.headerFormat === "prefixed") {
|
|
453
|
+
else if (this.config.headerFormat === "prefixed" || this.config.customConfig?.comparePrefixed) {
|
|
326
454
|
isValid = this.verifyHMACWithPrefix(payload, signature, algorithm);
|
|
327
455
|
}
|
|
328
456
|
else {
|
|
@@ -339,7 +467,7 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
339
467
|
if (!isValid) {
|
|
340
468
|
return {
|
|
341
469
|
isValid: false,
|
|
342
|
-
error:
|
|
470
|
+
error: this.getInvalidSignatureMessage(),
|
|
343
471
|
errorCode: "INVALID_SIGNATURE",
|
|
344
472
|
platform: this.platform,
|
|
345
473
|
};
|
|
@@ -368,7 +496,7 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
|
|
|
368
496
|
catch (error) {
|
|
369
497
|
return {
|
|
370
498
|
isValid: false,
|
|
371
|
-
error:
|
|
499
|
+
error: this.getVerificationErrorMessage(error),
|
|
372
500
|
errorCode: "VERIFICATION_ERROR",
|
|
373
501
|
platform: this.platform,
|
|
374
502
|
};
|
|
@@ -477,7 +605,7 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
|
|
|
477
605
|
if (signatures.length === 0) {
|
|
478
606
|
return {
|
|
479
607
|
isValid: false,
|
|
480
|
-
error:
|
|
608
|
+
error: this.getMissingSignatureMessage(),
|
|
481
609
|
errorCode: "MISSING_SIGNATURE",
|
|
482
610
|
platform: this.platform,
|
|
483
611
|
};
|
|
@@ -491,7 +619,7 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
|
|
|
491
619
|
if (!timestampStr) {
|
|
492
620
|
return {
|
|
493
621
|
isValid: false,
|
|
494
|
-
error:
|
|
622
|
+
error: this.getMissingTimestampMessage(),
|
|
495
623
|
errorCode: 'MISSING_SIGNATURE',
|
|
496
624
|
platform: this.platform,
|
|
497
625
|
};
|
|
@@ -500,7 +628,7 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
|
|
|
500
628
|
if (!this.isTimestampValid(timestamp)) {
|
|
501
629
|
return {
|
|
502
630
|
isValid: false,
|
|
503
|
-
error:
|
|
631
|
+
error: this.getTimestampExpiredMessage(),
|
|
504
632
|
errorCode: "TIMESTAMP_EXPIRED",
|
|
505
633
|
platform: this.platform,
|
|
506
634
|
};
|
|
@@ -560,7 +688,7 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
|
|
|
560
688
|
if (!isValid) {
|
|
561
689
|
return {
|
|
562
690
|
isValid: false,
|
|
563
|
-
error:
|
|
691
|
+
error: this.getInvalidSignatureMessage(),
|
|
564
692
|
errorCode: "INVALID_SIGNATURE",
|
|
565
693
|
platform: this.platform,
|
|
566
694
|
};
|
|
@@ -589,7 +717,7 @@ class Ed25519Verifier extends AlgorithmBasedVerifier {
|
|
|
589
717
|
catch (error) {
|
|
590
718
|
return {
|
|
591
719
|
isValid: false,
|
|
592
|
-
error:
|
|
720
|
+
error: this.getVerificationErrorMessage(error),
|
|
593
721
|
errorCode: "VERIFICATION_ERROR",
|
|
594
722
|
platform: this.platform,
|
|
595
723
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hookflo/tern",
|
|
3
|
-
"version": "4.3.0-beta.
|
|
3
|
+
"version": "4.3.0-beta.1",
|
|
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",
|
|
@@ -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; } });
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.paypalDefaultMapping = void 0;
|
|
4
|
-
exports.paypalDefaultMapping = {
|
|
5
|
-
provider: 'paypal',
|
|
6
|
-
fieldMappings: [
|
|
7
|
-
{ schemaFieldId: 'event_type', providerPath: 'event_type' },
|
|
8
|
-
{ schemaFieldId: 'amount', providerPath: 'resource.amount.value', transform: 'toNumber' },
|
|
9
|
-
{ schemaFieldId: 'currency', providerPath: 'resource.amount.currency_code', transform: 'toUpperCase' },
|
|
10
|
-
{ schemaFieldId: 'transaction_id', providerPath: 'resource.id' },
|
|
11
|
-
],
|
|
12
|
-
};
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.razorpayDefaultMapping = void 0;
|
|
4
|
-
exports.razorpayDefaultMapping = {
|
|
5
|
-
provider: 'razorpay',
|
|
6
|
-
fieldMappings: [
|
|
7
|
-
{ schemaFieldId: 'event_type', providerPath: 'event' },
|
|
8
|
-
{ schemaFieldId: 'amount', providerPath: 'payload.payment.entity.amount' },
|
|
9
|
-
{ schemaFieldId: 'currency', providerPath: 'payload.payment.entity.currency', transform: 'toUpperCase' },
|
|
10
|
-
{ schemaFieldId: 'transaction_id', providerPath: 'payload.payment.entity.id' },
|
|
11
|
-
{ schemaFieldId: 'customer_id', providerPath: 'payload.payment.entity.contact' },
|
|
12
|
-
],
|
|
13
|
-
};
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.stripeDefaultMapping = void 0;
|
|
4
|
-
exports.stripeDefaultMapping = {
|
|
5
|
-
provider: 'stripe',
|
|
6
|
-
fieldMappings: [
|
|
7
|
-
{ schemaFieldId: 'event_type', providerPath: 'type' },
|
|
8
|
-
{ schemaFieldId: 'amount', providerPath: 'data.object.amount_received' },
|
|
9
|
-
{ schemaFieldId: 'currency', providerPath: 'data.object.currency', transform: 'toUpperCase' },
|
|
10
|
-
{ schemaFieldId: 'transaction_id', providerPath: 'data.object.id' },
|
|
11
|
-
{ schemaFieldId: 'customer_id', providerPath: 'data.object.customer' },
|
|
12
|
-
],
|
|
13
|
-
};
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.providerRegistry = void 0;
|
|
4
|
-
const providers = [
|
|
5
|
-
{ id: 'stripe', name: 'Stripe', category: 'payment' },
|
|
6
|
-
{ id: 'razorpay', name: 'Razorpay', category: 'payment' },
|
|
7
|
-
{ id: 'paypal', name: 'PayPal', category: 'payment' },
|
|
8
|
-
{ id: 'clerk', name: 'Clerk', category: 'auth' },
|
|
9
|
-
{ id: 'shopify', name: 'Shopify', category: 'ecommerce' },
|
|
10
|
-
{ id: 'woocommerce', name: 'WooCommerce', category: 'ecommerce' },
|
|
11
|
-
];
|
|
12
|
-
exports.providerRegistry = {
|
|
13
|
-
list(category) {
|
|
14
|
-
if (!category)
|
|
15
|
-
return providers;
|
|
16
|
-
return providers.filter((p) => p.category === category);
|
|
17
|
-
},
|
|
18
|
-
getById(id) {
|
|
19
|
-
return providers.find((p) => p.id === id);
|
|
20
|
-
},
|
|
21
|
-
};
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import { AnyNormalizedWebhook, NormalizeOptions, NormalizationCategory, WebhookPlatform } from '../types';
|
|
2
|
-
export declare function getPlatformNormalizationCategory(platform: WebhookPlatform): NormalizationCategory | null;
|
|
3
|
-
export declare function getPlatformsByCategory(category: NormalizationCategory): WebhookPlatform[];
|
|
4
|
-
export declare function normalizePayload(platform: WebhookPlatform, payload: any, normalize?: boolean | NormalizeOptions): AnyNormalizedWebhook | unknown;
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getPlatformNormalizationCategory = getPlatformNormalizationCategory;
|
|
4
|
-
exports.getPlatformsByCategory = getPlatformsByCategory;
|
|
5
|
-
exports.normalizePayload = normalizePayload;
|
|
6
|
-
function readPath(payload, path) {
|
|
7
|
-
return path.split('.').reduce((acc, key) => {
|
|
8
|
-
if (acc === undefined || acc === null) {
|
|
9
|
-
return undefined;
|
|
10
|
-
}
|
|
11
|
-
return acc[key];
|
|
12
|
-
}, payload);
|
|
13
|
-
}
|
|
14
|
-
const platformNormalizers = {
|
|
15
|
-
stripe: {
|
|
16
|
-
platform: 'stripe',
|
|
17
|
-
category: 'payment',
|
|
18
|
-
normalize: (payload) => ({
|
|
19
|
-
category: 'payment',
|
|
20
|
-
event: readPath(payload, 'type') === 'payment_intent.succeeded'
|
|
21
|
-
? 'payment.succeeded'
|
|
22
|
-
: 'payment.unknown',
|
|
23
|
-
amount: readPath(payload, 'data.object.amount_received')
|
|
24
|
-
?? readPath(payload, 'data.object.amount'),
|
|
25
|
-
currency: String(readPath(payload, 'data.object.currency') ?? '').toUpperCase() || undefined,
|
|
26
|
-
customer_id: readPath(payload, 'data.object.customer'),
|
|
27
|
-
transaction_id: readPath(payload, 'data.object.id'),
|
|
28
|
-
metadata: {},
|
|
29
|
-
occurred_at: new Date().toISOString(),
|
|
30
|
-
}),
|
|
31
|
-
},
|
|
32
|
-
polar: {
|
|
33
|
-
platform: 'polar',
|
|
34
|
-
category: 'payment',
|
|
35
|
-
normalize: (payload) => ({
|
|
36
|
-
category: 'payment',
|
|
37
|
-
event: readPath(payload, 'event') === 'payment.completed'
|
|
38
|
-
? 'payment.succeeded'
|
|
39
|
-
: 'payment.unknown',
|
|
40
|
-
amount: readPath(payload, 'payload.amount_cents'),
|
|
41
|
-
currency: String(readPath(payload, 'payload.currency_code') ?? '').toUpperCase() || undefined,
|
|
42
|
-
customer_id: readPath(payload, 'payload.customer_id'),
|
|
43
|
-
transaction_id: readPath(payload, 'payload.transaction_id'),
|
|
44
|
-
metadata: {},
|
|
45
|
-
occurred_at: new Date().toISOString(),
|
|
46
|
-
}),
|
|
47
|
-
},
|
|
48
|
-
clerk: {
|
|
49
|
-
platform: 'clerk',
|
|
50
|
-
category: 'auth',
|
|
51
|
-
normalize: (payload) => ({
|
|
52
|
-
category: 'auth',
|
|
53
|
-
event: readPath(payload, 'type') || 'auth.unknown',
|
|
54
|
-
user_id: readPath(payload, 'data.id'),
|
|
55
|
-
email: readPath(payload, 'data.email_addresses.0.email_address'),
|
|
56
|
-
metadata: {},
|
|
57
|
-
occurred_at: new Date().toISOString(),
|
|
58
|
-
}),
|
|
59
|
-
},
|
|
60
|
-
vercel: {
|
|
61
|
-
platform: 'vercel',
|
|
62
|
-
category: 'infrastructure',
|
|
63
|
-
normalize: (payload) => ({
|
|
64
|
-
category: 'infrastructure',
|
|
65
|
-
event: readPath(payload, 'type') || 'deployment.unknown',
|
|
66
|
-
project_id: readPath(payload, 'payload.project.id'),
|
|
67
|
-
deployment_id: readPath(payload, 'payload.deployment.id'),
|
|
68
|
-
status: 'unknown',
|
|
69
|
-
metadata: {},
|
|
70
|
-
occurred_at: new Date().toISOString(),
|
|
71
|
-
}),
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
function getPlatformNormalizationCategory(platform) {
|
|
75
|
-
return platformNormalizers[platform]?.category || null;
|
|
76
|
-
}
|
|
77
|
-
function getPlatformsByCategory(category) {
|
|
78
|
-
return Object.values(platformNormalizers)
|
|
79
|
-
.filter((spec) => !!spec)
|
|
80
|
-
.filter((spec) => spec.category === category)
|
|
81
|
-
.map((spec) => spec.platform);
|
|
82
|
-
}
|
|
83
|
-
function resolveNormalizeOptions(normalize) {
|
|
84
|
-
if (typeof normalize === 'boolean') {
|
|
85
|
-
return {
|
|
86
|
-
enabled: normalize,
|
|
87
|
-
category: undefined,
|
|
88
|
-
includeRaw: true,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
return {
|
|
92
|
-
enabled: normalize?.enabled ?? true,
|
|
93
|
-
category: normalize?.category,
|
|
94
|
-
includeRaw: normalize?.includeRaw ?? true,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
function buildUnknownNormalizedPayload(platform, payload, category, includeRaw, warning) {
|
|
98
|
-
return {
|
|
99
|
-
category: category || 'infrastructure',
|
|
100
|
-
event: payload?.type ?? payload?.event ?? 'unknown',
|
|
101
|
-
_platform: platform,
|
|
102
|
-
_raw: includeRaw ? payload : undefined,
|
|
103
|
-
warning,
|
|
104
|
-
occurred_at: new Date().toISOString(),
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
function normalizePayload(platform, payload, normalize) {
|
|
108
|
-
const options = resolveNormalizeOptions(normalize);
|
|
109
|
-
if (!options.enabled) {
|
|
110
|
-
return payload;
|
|
111
|
-
}
|
|
112
|
-
const spec = platformNormalizers[platform];
|
|
113
|
-
const inferredCategory = spec?.category;
|
|
114
|
-
if (!spec) {
|
|
115
|
-
return buildUnknownNormalizedPayload(platform, payload, options.category, options.includeRaw);
|
|
116
|
-
}
|
|
117
|
-
if (options.category && options.category !== inferredCategory) {
|
|
118
|
-
return buildUnknownNormalizedPayload(platform, payload, inferredCategory, options.includeRaw, `Requested normalization category '${options.category}' does not match platform category '${inferredCategory}'`);
|
|
119
|
-
}
|
|
120
|
-
const normalized = spec.normalize(payload);
|
|
121
|
-
return {
|
|
122
|
-
...normalized,
|
|
123
|
-
_platform: platform,
|
|
124
|
-
_raw: options.includeRaw ? payload : undefined,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { BaseTemplate, UpdateSchemaInput, UserSchema } from '../types';
|
|
2
|
-
export interface StorageAdapter {
|
|
3
|
-
saveSchema(schema: UserSchema): Promise<void>;
|
|
4
|
-
getSchema(id: string): Promise<UserSchema | null>;
|
|
5
|
-
updateSchema(id: string, updates: UpdateSchemaInput): Promise<void>;
|
|
6
|
-
deleteSchema(id: string): Promise<void>;
|
|
7
|
-
listSchemas(userId: string): Promise<UserSchema[]>;
|
|
8
|
-
getBaseTemplate(id: string): Promise<BaseTemplate | null>;
|
|
9
|
-
listBaseTemplates(): Promise<BaseTemplate[]>;
|
|
10
|
-
}
|
|
11
|
-
export interface NormalizationStorageOptions {
|
|
12
|
-
adapter: StorageAdapter;
|
|
13
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { BaseTemplate, UpdateSchemaInput, UserSchema } from '../types';
|
|
2
|
-
import { StorageAdapter } from './interface';
|
|
3
|
-
export declare class InMemoryStorageAdapter implements StorageAdapter {
|
|
4
|
-
private schemas;
|
|
5
|
-
saveSchema(schema: UserSchema): Promise<void>;
|
|
6
|
-
getSchema(id: string): Promise<UserSchema | null>;
|
|
7
|
-
updateSchema(id: string, updates: UpdateSchemaInput): Promise<void>;
|
|
8
|
-
deleteSchema(id: string): Promise<void>;
|
|
9
|
-
listSchemas(userId: string): Promise<UserSchema[]>;
|
|
10
|
-
getBaseTemplate(id: string): Promise<BaseTemplate | null>;
|
|
11
|
-
listBaseTemplates(): Promise<BaseTemplate[]>;
|
|
12
|
-
}
|