@hookflo/tern 1.0.5 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -10
- package/dist/normalization/index.d.ts +20 -0
- package/dist/normalization/index.js +78 -0
- package/dist/normalization/providers/payment/paypal.d.ts +2 -0
- package/dist/normalization/providers/payment/paypal.js +12 -0
- package/dist/normalization/providers/payment/razorpay.d.ts +2 -0
- package/dist/normalization/providers/payment/razorpay.js +13 -0
- package/dist/normalization/providers/payment/stripe.d.ts +2 -0
- package/dist/normalization/providers/payment/stripe.js +13 -0
- package/dist/normalization/providers/registry.d.ts +5 -0
- package/dist/normalization/providers/registry.js +23 -0
- package/dist/normalization/storage/interface.d.ts +13 -0
- package/dist/normalization/storage/interface.js +2 -0
- package/dist/normalization/storage/memory.d.ts +12 -0
- package/dist/normalization/storage/memory.js +39 -0
- package/dist/normalization/templates/base/auth.d.ts +2 -0
- package/dist/normalization/templates/base/auth.js +22 -0
- package/dist/normalization/templates/base/ecommerce.d.ts +2 -0
- package/dist/normalization/templates/base/ecommerce.js +25 -0
- package/dist/normalization/templates/base/payment.d.ts +2 -0
- package/dist/normalization/templates/base/payment.js +25 -0
- package/dist/normalization/templates/registry.d.ts +6 -0
- package/dist/normalization/templates/registry.js +22 -0
- package/dist/normalization/transformer/engine.d.ts +11 -0
- package/dist/normalization/transformer/engine.js +86 -0
- package/dist/normalization/transformer/validator.d.ts +12 -0
- package/dist/normalization/transformer/validator.js +56 -0
- package/dist/normalization/types.d.ts +79 -0
- package/dist/normalization/types.js +2 -0
- package/dist/platforms/algorithms.d.ts +1 -1
- package/dist/platforms/algorithms.js +89 -88
- package/dist/test.js +8 -3
- package/dist/verifiers/algorithms.d.ts +2 -2
- package/dist/verifiers/algorithms.js +62 -63
- package/dist/verifiers/base.d.ts +1 -1
- package/dist/verifiers/custom-algorithms.d.ts +2 -2
- package/dist/verifiers/custom-algorithms.js +8 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# Tern - Algorithm Agnostic Webhook Verification Framework
|
|
2
2
|
|
|
3
3
|
A robust, algorithm-agnostic webhook verification framework that supports multiple platforms with accurate signature verification and payload retrieval.
|
|
4
|
+
The same framework that secures webhook verification at [Hookflo](https://hookflo.com).
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install @hookflo/tern
|
|
8
|
+
```
|
|
4
9
|
|
|
5
10
|
[](https://www.npmjs.com/package/@hookflo/tern)
|
|
6
11
|
[](https://www.typescriptlang.org/)
|
|
@@ -9,12 +14,13 @@ A robust, algorithm-agnostic webhook verification framework that supports multip
|
|
|
9
14
|
Tern is a zero-dependency TypeScript framework for robust webhook verification across multiple platforms and algorithms.
|
|
10
15
|
|
|
11
16
|
<img width="1396" height="470" style="border-radius: 10px" alt="tern bird nature" src="https://github.com/user-attachments/assets/5f0da3e6-1aba-4f88-a9d7-9d8698845c39" />
|
|
17
|
+
|
|
12
18
|
## Features
|
|
13
19
|
|
|
14
20
|
- **Algorithm Agnostic**: Decouples platform logic from signature verification — verify based on cryptographic algorithm, not hardcoded platform rules.
|
|
15
21
|
Supports HMAC-SHA256, HMAC-SHA1, HMAC-SHA512, and custom algorithms
|
|
16
22
|
|
|
17
|
-
- **Platform Specific**: Accurate implementations for Stripe, GitHub,
|
|
23
|
+
- **Platform Specific**: Accurate implementations for **Stripe, GitHub, Supabase, Clerk**, and other platforms
|
|
18
24
|
- **Flexible Configuration**: Custom signature configurations for any webhook format
|
|
19
25
|
- **Type Safe**: Full TypeScript support with comprehensive type definitions
|
|
20
26
|
- **Framework Agnostic**: Works with Express.js, Next.js, Cloudflare Workers, and more
|
|
@@ -40,15 +46,18 @@ npm install @hookflo/tern
|
|
|
40
46
|
### Basic Usage
|
|
41
47
|
|
|
42
48
|
```typescript
|
|
43
|
-
import { WebhookVerificationService } from '@hookflo/tern';
|
|
49
|
+
import { WebhookVerificationService, platformManager } from '@hookflo/tern';
|
|
44
50
|
|
|
45
|
-
//
|
|
51
|
+
// Method 1: Using the service (recommended)
|
|
46
52
|
const result = await WebhookVerificationService.verifyWithPlatformConfig(
|
|
47
53
|
request,
|
|
48
54
|
'stripe',
|
|
49
55
|
'whsec_your_stripe_webhook_secret'
|
|
50
56
|
);
|
|
51
57
|
|
|
58
|
+
// Method 2: Using platform manager (for platform-specific operations)
|
|
59
|
+
const stripeResult = await platformManager.verify(request, 'stripe', 'whsec_your_secret');
|
|
60
|
+
|
|
52
61
|
if (result.isValid) {
|
|
53
62
|
console.log('Webhook verified!', result.payload);
|
|
54
63
|
} else {
|
|
@@ -56,6 +65,21 @@ if (result.isValid) {
|
|
|
56
65
|
}
|
|
57
66
|
```
|
|
58
67
|
|
|
68
|
+
### Platform-Specific Usage
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { platformManager } from '@hookflo/tern';
|
|
72
|
+
|
|
73
|
+
// Run tests for a specific platform
|
|
74
|
+
const testsPassed = await platformManager.runPlatformTests('stripe');
|
|
75
|
+
|
|
76
|
+
// Get platform configuration
|
|
77
|
+
const config = platformManager.getConfig('stripe');
|
|
78
|
+
|
|
79
|
+
// Get platform documentation
|
|
80
|
+
const docs = platformManager.getDocumentation('stripe');
|
|
81
|
+
```
|
|
82
|
+
|
|
59
83
|
### Platform-Specific Configurations
|
|
60
84
|
|
|
61
85
|
```typescript
|
|
@@ -330,16 +354,33 @@ interface WebhookConfig {
|
|
|
330
354
|
|
|
331
355
|
## Testing
|
|
332
356
|
|
|
333
|
-
Run
|
|
357
|
+
### Run All Tests
|
|
334
358
|
|
|
335
359
|
```bash
|
|
336
360
|
npm test
|
|
337
361
|
```
|
|
338
362
|
|
|
339
|
-
|
|
363
|
+
### Platform-Specific Testing
|
|
340
364
|
|
|
341
365
|
```bash
|
|
342
|
-
|
|
366
|
+
# Test a specific platform
|
|
367
|
+
npm run test:platform stripe
|
|
368
|
+
|
|
369
|
+
# Test all platforms
|
|
370
|
+
npm run test:all
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Documentation and Analysis
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
# Fetch platform documentation
|
|
377
|
+
npm run docs:fetch
|
|
378
|
+
|
|
379
|
+
# Generate diffs between versions
|
|
380
|
+
npm run docs:diff
|
|
381
|
+
|
|
382
|
+
# Analyze changes and generate reports
|
|
383
|
+
npm run docs:analyze
|
|
343
384
|
```
|
|
344
385
|
|
|
345
386
|
## Examples
|
|
@@ -348,11 +389,30 @@ See the [examples.ts](./src/examples.ts) file for comprehensive usage examples.
|
|
|
348
389
|
|
|
349
390
|
## Contributing
|
|
350
391
|
|
|
392
|
+
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for detailed information on how to:
|
|
393
|
+
|
|
394
|
+
- Set up your development environment
|
|
395
|
+
- Add new platforms
|
|
396
|
+
- Write tests
|
|
397
|
+
- Submit pull requests
|
|
398
|
+
- Follow our code style guidelines
|
|
399
|
+
|
|
400
|
+
### Quick Start for Contributors
|
|
401
|
+
|
|
351
402
|
1. Fork the repository
|
|
352
|
-
2.
|
|
353
|
-
3.
|
|
354
|
-
4.
|
|
355
|
-
5.
|
|
403
|
+
2. Clone your fork: `git clone https://github.com/your-username/tern.git`
|
|
404
|
+
3. Create a feature branch: `git checkout -b feature/your-feature-name`
|
|
405
|
+
4. Make your changes
|
|
406
|
+
5. Run tests: `npm test`
|
|
407
|
+
6. Submit a pull request
|
|
408
|
+
|
|
409
|
+
### Adding a New Platform
|
|
410
|
+
|
|
411
|
+
See our [Platform Development Guide](CONTRIBUTING.md#adding-new-platforms) for step-by-step instructions on adding support for new webhook platforms.
|
|
412
|
+
|
|
413
|
+
## Code of Conduct
|
|
414
|
+
|
|
415
|
+
This project adheres to our [Code of Conduct](CODE_OF_CONDUCT.md). Please read it before contributing.
|
|
356
416
|
|
|
357
417
|
## 📄 License
|
|
358
418
|
|
|
@@ -0,0 +1,20 @@
|
|
|
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';
|
|
@@ -0,0 +1,78 @@
|
|
|
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; } });
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
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: 'auth0', name: 'Auth0', category: 'auth' },
|
|
10
|
+
{ id: 'supabase', name: 'Supabase', category: 'auth' },
|
|
11
|
+
{ id: 'shopify', name: 'Shopify', category: 'ecommerce' },
|
|
12
|
+
{ id: 'woocommerce', name: 'WooCommerce', category: 'ecommerce' },
|
|
13
|
+
];
|
|
14
|
+
exports.providerRegistry = {
|
|
15
|
+
list(category) {
|
|
16
|
+
if (!category)
|
|
17
|
+
return providers;
|
|
18
|
+
return providers.filter((p) => p.category === category);
|
|
19
|
+
},
|
|
20
|
+
getById(id) {
|
|
21
|
+
return providers.find((p) => p.id === id);
|
|
22
|
+
},
|
|
23
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InMemoryStorageAdapter = void 0;
|
|
4
|
+
const registry_1 = require("../templates/registry");
|
|
5
|
+
class InMemoryStorageAdapter {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.schemas = new Map();
|
|
8
|
+
}
|
|
9
|
+
async saveSchema(schema) {
|
|
10
|
+
this.schemas.set(schema.id, schema);
|
|
11
|
+
}
|
|
12
|
+
async getSchema(id) {
|
|
13
|
+
return this.schemas.get(id) ?? null;
|
|
14
|
+
}
|
|
15
|
+
async updateSchema(id, updates) {
|
|
16
|
+
const existing = this.schemas.get(id);
|
|
17
|
+
if (!existing)
|
|
18
|
+
return;
|
|
19
|
+
const updated = {
|
|
20
|
+
...existing,
|
|
21
|
+
...updates,
|
|
22
|
+
updatedAt: new Date(),
|
|
23
|
+
};
|
|
24
|
+
this.schemas.set(id, updated);
|
|
25
|
+
}
|
|
26
|
+
async deleteSchema(id) {
|
|
27
|
+
this.schemas.delete(id);
|
|
28
|
+
}
|
|
29
|
+
async listSchemas(userId) {
|
|
30
|
+
return Array.from(this.schemas.values()).filter((s) => s.userId === userId);
|
|
31
|
+
}
|
|
32
|
+
async getBaseTemplate(id) {
|
|
33
|
+
return registry_1.templateRegistry.getById(id) ?? null;
|
|
34
|
+
}
|
|
35
|
+
async listBaseTemplates() {
|
|
36
|
+
return registry_1.templateRegistry.listAll();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
exports.InMemoryStorageAdapter = InMemoryStorageAdapter;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.authBaseTemplate = void 0;
|
|
4
|
+
exports.authBaseTemplate = {
|
|
5
|
+
id: 'auth_v1',
|
|
6
|
+
category: 'auth',
|
|
7
|
+
version: '1.0.0',
|
|
8
|
+
fields: [
|
|
9
|
+
{
|
|
10
|
+
id: 'event_type', name: 'event_type', type: 'string', required: true,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: 'user_id', name: 'user_id', type: 'string', required: true,
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: 'email', name: 'email', type: 'string', required: false,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'status', name: 'status', type: 'string', required: true,
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ecommerceBaseTemplate = void 0;
|
|
4
|
+
exports.ecommerceBaseTemplate = {
|
|
5
|
+
id: 'ecommerce_v1',
|
|
6
|
+
category: 'ecommerce',
|
|
7
|
+
version: '1.0.0',
|
|
8
|
+
fields: [
|
|
9
|
+
{
|
|
10
|
+
id: 'event_type', name: 'event_type', type: 'string', required: true,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: 'order_id', name: 'order_id', type: 'string', required: true,
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: 'total', name: 'total', type: 'number', required: true,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'currency', name: 'currency', type: 'string', required: true,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 'customer_id', name: 'customer_id', type: 'string', required: false,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.paymentBaseTemplate = void 0;
|
|
4
|
+
exports.paymentBaseTemplate = {
|
|
5
|
+
id: 'payment_v1',
|
|
6
|
+
category: 'payment',
|
|
7
|
+
version: '1.0.0',
|
|
8
|
+
fields: [
|
|
9
|
+
{
|
|
10
|
+
id: 'event_type', name: 'event_type', type: 'string', required: true, description: 'Type of payment event',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: 'amount', name: 'amount', type: 'number', required: true, description: 'Amount in the smallest currency unit',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: 'currency', name: 'currency', type: 'string', required: true, description: 'Three-letter currency code',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'transaction_id', name: 'transaction_id', type: 'string', required: true, description: 'Unique transaction identifier',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 'customer_id', name: 'customer_id', type: 'string', required: false, description: 'Customer identifier',
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.templateRegistry = void 0;
|
|
4
|
+
const payment_1 = require("./base/payment");
|
|
5
|
+
const auth_1 = require("./base/auth");
|
|
6
|
+
const ecommerce_1 = require("./base/ecommerce");
|
|
7
|
+
const templates = {
|
|
8
|
+
[payment_1.paymentBaseTemplate.id]: payment_1.paymentBaseTemplate,
|
|
9
|
+
[auth_1.authBaseTemplate.id]: auth_1.authBaseTemplate,
|
|
10
|
+
[ecommerce_1.ecommerceBaseTemplate.id]: ecommerce_1.ecommerceBaseTemplate,
|
|
11
|
+
};
|
|
12
|
+
exports.templateRegistry = {
|
|
13
|
+
getById(id) {
|
|
14
|
+
return templates[id];
|
|
15
|
+
},
|
|
16
|
+
listByCategory(category) {
|
|
17
|
+
return Object.values(templates).filter((t) => t.category === category);
|
|
18
|
+
},
|
|
19
|
+
listAll() {
|
|
20
|
+
return Object.values(templates);
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { NormalizedResult, TransformParams } from '../types';
|
|
2
|
+
import { StorageAdapter } from '../storage/interface';
|
|
3
|
+
import { SchemaValidator } from './validator';
|
|
4
|
+
export declare class NormalizationEngine {
|
|
5
|
+
private readonly storage;
|
|
6
|
+
private readonly validator;
|
|
7
|
+
constructor(storage: StorageAdapter, validator?: SchemaValidator);
|
|
8
|
+
transform(params: TransformParams): Promise<NormalizedResult>;
|
|
9
|
+
private extractValue;
|
|
10
|
+
private applyTransform;
|
|
11
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NormalizationEngine = void 0;
|
|
4
|
+
const registry_1 = require("../templates/registry");
|
|
5
|
+
const validator_1 = require("./validator");
|
|
6
|
+
class NormalizationEngine {
|
|
7
|
+
constructor(storage, validator = new validator_1.SchemaValidator()) {
|
|
8
|
+
this.storage = storage;
|
|
9
|
+
this.validator = validator;
|
|
10
|
+
}
|
|
11
|
+
async transform(params) {
|
|
12
|
+
const { rawPayload, provider, schemaId } = params;
|
|
13
|
+
const schema = await this.storage.getSchema(schemaId);
|
|
14
|
+
if (!schema)
|
|
15
|
+
throw new Error(`Schema not found: ${schemaId}`);
|
|
16
|
+
const baseTemplate = await this.storage.getBaseTemplate(schema.baseTemplateId) || registry_1.templateRegistry.getById(schema.baseTemplateId);
|
|
17
|
+
if (!baseTemplate)
|
|
18
|
+
throw new Error(`Base template not found: ${schema.baseTemplateId}`);
|
|
19
|
+
const validation = this.validator.validateSchema(schema, baseTemplate);
|
|
20
|
+
if (!validation.valid) {
|
|
21
|
+
throw new Error(`Invalid schema: ${validation.errors.join('; ')}`);
|
|
22
|
+
}
|
|
23
|
+
const providerMapping = schema.providerMappings.find((m) => m.provider === provider);
|
|
24
|
+
if (!providerMapping)
|
|
25
|
+
throw new Error(`No mapping found for provider: ${provider}`);
|
|
26
|
+
const normalized = {};
|
|
27
|
+
for (const field of schema.fields) {
|
|
28
|
+
if (!field.enabled)
|
|
29
|
+
continue;
|
|
30
|
+
const mapping = providerMapping.fieldMappings.find((m) => m.schemaFieldId === field.id);
|
|
31
|
+
if (mapping) {
|
|
32
|
+
const value = this.extractValue(rawPayload, mapping.providerPath);
|
|
33
|
+
const finalValue = this.applyTransform(value, mapping.transform);
|
|
34
|
+
normalized[field.name] = finalValue ?? field.defaultValue;
|
|
35
|
+
}
|
|
36
|
+
else if (field.required) {
|
|
37
|
+
if (field.defaultValue !== undefined) {
|
|
38
|
+
normalized[field.name] = field.defaultValue;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
throw new Error(`Required field ${field.name} has no mapping`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const outValidation = this.validator.validateOutput(normalized, schema, baseTemplate);
|
|
46
|
+
if (!outValidation.valid) {
|
|
47
|
+
throw new Error(`Normalized output invalid: ${outValidation.errors.join('; ')}`);
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
normalized,
|
|
51
|
+
meta: {
|
|
52
|
+
provider,
|
|
53
|
+
schemaId,
|
|
54
|
+
schemaVersion: schema.baseTemplateId,
|
|
55
|
+
transformedAt: new Date(),
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
extractValue(obj, path) {
|
|
60
|
+
if (!path)
|
|
61
|
+
return undefined;
|
|
62
|
+
return path.split('.').reduce((acc, key) => (acc == null ? undefined : acc[key]), obj);
|
|
63
|
+
}
|
|
64
|
+
applyTransform(value, transform) {
|
|
65
|
+
if (transform == null)
|
|
66
|
+
return value;
|
|
67
|
+
if (value == null)
|
|
68
|
+
return value;
|
|
69
|
+
if (transform === 'toUpperCase')
|
|
70
|
+
return String(value).toUpperCase();
|
|
71
|
+
if (transform === 'toLowerCase')
|
|
72
|
+
return String(value).toLowerCase();
|
|
73
|
+
if (transform === 'toNumber')
|
|
74
|
+
return typeof value === 'number' ? value : Number(value);
|
|
75
|
+
if (transform.startsWith('divide:')) {
|
|
76
|
+
const denominator = Number(transform.split(':')[1]);
|
|
77
|
+
return Number(value) / denominator;
|
|
78
|
+
}
|
|
79
|
+
if (transform.startsWith('multiply:')) {
|
|
80
|
+
const factor = Number(transform.split(':')[1]);
|
|
81
|
+
return Number(value) * factor;
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
exports.NormalizationEngine = NormalizationEngine;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BaseTemplate, UserSchema } from '../types';
|
|
2
|
+
export declare class SchemaValidator {
|
|
3
|
+
validateSchema(userSchema: UserSchema, baseTemplate: BaseTemplate): {
|
|
4
|
+
valid: boolean;
|
|
5
|
+
errors: string[];
|
|
6
|
+
};
|
|
7
|
+
validateOutput(output: Record<string, unknown>, userSchema: UserSchema, baseTemplate: BaseTemplate): {
|
|
8
|
+
valid: boolean;
|
|
9
|
+
errors: string[];
|
|
10
|
+
};
|
|
11
|
+
private matchesType;
|
|
12
|
+
}
|