@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.
Files changed (38) hide show
  1. package/README.md +70 -10
  2. package/dist/normalization/index.d.ts +20 -0
  3. package/dist/normalization/index.js +78 -0
  4. package/dist/normalization/providers/payment/paypal.d.ts +2 -0
  5. package/dist/normalization/providers/payment/paypal.js +12 -0
  6. package/dist/normalization/providers/payment/razorpay.d.ts +2 -0
  7. package/dist/normalization/providers/payment/razorpay.js +13 -0
  8. package/dist/normalization/providers/payment/stripe.d.ts +2 -0
  9. package/dist/normalization/providers/payment/stripe.js +13 -0
  10. package/dist/normalization/providers/registry.d.ts +5 -0
  11. package/dist/normalization/providers/registry.js +23 -0
  12. package/dist/normalization/storage/interface.d.ts +13 -0
  13. package/dist/normalization/storage/interface.js +2 -0
  14. package/dist/normalization/storage/memory.d.ts +12 -0
  15. package/dist/normalization/storage/memory.js +39 -0
  16. package/dist/normalization/templates/base/auth.d.ts +2 -0
  17. package/dist/normalization/templates/base/auth.js +22 -0
  18. package/dist/normalization/templates/base/ecommerce.d.ts +2 -0
  19. package/dist/normalization/templates/base/ecommerce.js +25 -0
  20. package/dist/normalization/templates/base/payment.d.ts +2 -0
  21. package/dist/normalization/templates/base/payment.js +25 -0
  22. package/dist/normalization/templates/registry.d.ts +6 -0
  23. package/dist/normalization/templates/registry.js +22 -0
  24. package/dist/normalization/transformer/engine.d.ts +11 -0
  25. package/dist/normalization/transformer/engine.js +86 -0
  26. package/dist/normalization/transformer/validator.d.ts +12 -0
  27. package/dist/normalization/transformer/validator.js +56 -0
  28. package/dist/normalization/types.d.ts +79 -0
  29. package/dist/normalization/types.js +2 -0
  30. package/dist/platforms/algorithms.d.ts +1 -1
  31. package/dist/platforms/algorithms.js +89 -88
  32. package/dist/test.js +8 -3
  33. package/dist/verifiers/algorithms.d.ts +2 -2
  34. package/dist/verifiers/algorithms.js +62 -63
  35. package/dist/verifiers/base.d.ts +1 -1
  36. package/dist/verifiers/custom-algorithms.d.ts +2 -2
  37. package/dist/verifiers/custom-algorithms.js +8 -8
  38. package/package.json +1 -1
@@ -15,28 +15,27 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
15
15
  if (!headerValue)
16
16
  return null;
17
17
  switch (this.config.headerFormat) {
18
- case "prefixed":
18
+ case 'prefixed':
19
19
  // For GitHub, return the full signature including prefix for comparison
20
20
  return headerValue;
21
- case "comma-separated":
21
+ case 'comma-separated':
22
22
  // Handle comma-separated format like Stripe: "t=1234567890,v1=abc123"
23
- const parts = headerValue.split(",");
23
+ const parts = headerValue.split(',');
24
24
  const sigMap = {};
25
25
  for (const part of parts) {
26
- const [key, value] = part.split("=");
26
+ const [key, value] = part.split('=');
27
27
  if (key && value) {
28
28
  sigMap[key] = value;
29
29
  }
30
30
  }
31
31
  return sigMap.v1 || sigMap.signature || null;
32
- case "raw":
32
+ case 'raw':
33
33
  default:
34
- // For Clerk, handle space-separated signatures
35
- if (this.platform === "clerk") {
36
- const signatures = headerValue.split(" ");
34
+ if (this.platform === 'clerk' || this.platform === 'dodopayments') {
35
+ const signatures = headerValue.split(' ');
37
36
  for (const sig of signatures) {
38
- const [version, signature] = sig.split(",");
39
- if (version === "v1") {
37
+ const [version, signature] = sig.split(',');
38
+ if (version === 'v1') {
40
39
  return signature;
41
40
  }
42
41
  }
@@ -52,11 +51,11 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
52
51
  if (!timestampHeader)
53
52
  return null;
54
53
  switch (this.config.timestampFormat) {
55
- case "unix":
54
+ case 'unix':
56
55
  return parseInt(timestampHeader, 10);
57
- case "iso":
56
+ case 'iso':
58
57
  return Math.floor(new Date(timestampHeader).getTime() / 1000);
59
- case "custom":
58
+ case 'custom':
60
59
  // Custom timestamp parsing logic can be added here
61
60
  return parseInt(timestampHeader, 10);
62
61
  default:
@@ -65,14 +64,14 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
65
64
  }
66
65
  extractTimestampFromSignature(request) {
67
66
  // For platforms like Stripe where timestamp is embedded in signature
68
- if (this.config.headerFormat === "comma-separated") {
67
+ if (this.config.headerFormat === 'comma-separated') {
69
68
  const headerValue = request.headers.get(this.config.headerName);
70
69
  if (!headerValue)
71
70
  return null;
72
- const parts = headerValue.split(",");
71
+ const parts = headerValue.split(',');
73
72
  const sigMap = {};
74
73
  for (const part of parts) {
75
- const [key, value] = part.split("=");
74
+ const [key, value] = part.split('=');
76
75
  if (key && value) {
77
76
  sigMap[key] = value;
78
77
  }
@@ -83,14 +82,14 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
83
82
  }
84
83
  formatPayload(rawBody, request) {
85
84
  switch (this.config.payloadFormat) {
86
- case "timestamped":
85
+ case 'timestamped':
87
86
  // For Stripe, timestamp is embedded in signature
88
- const timestamp = this.extractTimestampFromSignature(request) ||
89
- this.extractTimestamp(request);
87
+ const timestamp = this.extractTimestampFromSignature(request)
88
+ || this.extractTimestamp(request);
90
89
  return timestamp ? `${timestamp}.${rawBody}` : rawBody;
91
- case "custom":
90
+ case 'custom':
92
91
  return this.formatCustomPayload(rawBody, request);
93
- case "raw":
92
+ case 'raw':
94
93
  default:
95
94
  return rawBody;
96
95
  }
@@ -101,42 +100,42 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
101
100
  }
102
101
  const customFormat = this.config.customConfig.payloadFormat;
103
102
  // Handle Clerk-style format: {id}.{timestamp}.{body}
104
- if (customFormat.includes("{id}") && customFormat.includes("{timestamp}")) {
105
- const id = request.headers.get(this.config.customConfig.idHeader || "x-webhook-id");
106
- const timestamp = request.headers.get(this.config.timestampHeader || "x-webhook-timestamp");
103
+ if (customFormat.includes('{id}') && customFormat.includes('{timestamp}')) {
104
+ const id = request.headers.get(this.config.customConfig.idHeader || 'x-webhook-id');
105
+ const timestamp = request.headers.get(this.config.timestampHeader || 'x-webhook-timestamp');
107
106
  return customFormat
108
- .replace("{id}", id || "")
109
- .replace("{timestamp}", timestamp || "")
110
- .replace("{body}", rawBody);
107
+ .replace('{id}', id || '')
108
+ .replace('{timestamp}', timestamp || '')
109
+ .replace('{body}', rawBody);
111
110
  }
112
111
  // Handle Stripe-style format: {timestamp}.{body}
113
- if (customFormat.includes("{timestamp}") &&
114
- customFormat.includes("{body}")) {
112
+ if (customFormat.includes('{timestamp}')
113
+ && customFormat.includes('{body}')) {
115
114
  const timestamp = this.extractTimestamp(request);
116
115
  return customFormat
117
- .replace("{timestamp}", timestamp?.toString() || "")
118
- .replace("{body}", rawBody);
116
+ .replace('{timestamp}', timestamp?.toString() || '')
117
+ .replace('{body}', rawBody);
119
118
  }
120
119
  return rawBody;
121
120
  }
122
- verifyHMAC(payload, signature, algorithm = "sha256") {
121
+ verifyHMAC(payload, signature, algorithm = 'sha256') {
123
122
  const hmac = (0, crypto_1.createHmac)(algorithm, this.secret);
124
123
  hmac.update(payload);
125
- const expectedSignature = hmac.digest("hex");
124
+ const expectedSignature = hmac.digest('hex');
126
125
  return this.safeCompare(signature, expectedSignature);
127
126
  }
128
- verifyHMACWithPrefix(payload, signature, algorithm = "sha256") {
127
+ verifyHMACWithPrefix(payload, signature, algorithm = 'sha256') {
129
128
  const hmac = (0, crypto_1.createHmac)(algorithm, this.secret);
130
129
  hmac.update(payload);
131
- const expectedSignature = `${this.config.prefix || ""}${hmac.digest("hex")}`;
130
+ const expectedSignature = `${this.config.prefix || ''}${hmac.digest('hex')}`;
132
131
  return this.safeCompare(signature, expectedSignature);
133
132
  }
134
- verifyHMACWithBase64(payload, signature, algorithm = "sha256") {
133
+ verifyHMACWithBase64(payload, signature, algorithm = 'sha256') {
135
134
  // For platforms like Clerk that use base64 encoding
136
- const secretBytes = new Uint8Array(Buffer.from(this.secret.split("_")[1], "base64"));
135
+ const secretBytes = new Uint8Array(Buffer.from(this.secret.split('_')[1], 'base64'));
137
136
  const hmac = (0, crypto_1.createHmac)(algorithm, secretBytes);
138
137
  hmac.update(payload);
139
- const expectedSignature = hmac.digest("base64");
138
+ const expectedSignature = hmac.digest('base64');
140
139
  return this.safeCompare(signature, expectedSignature);
141
140
  }
142
141
  extractMetadata(request) {
@@ -150,18 +149,18 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
150
149
  }
151
150
  // Add platform-specific metadata
152
151
  switch (this.platform) {
153
- case "github":
154
- metadata.event = request.headers.get("x-github-event");
155
- metadata.delivery = request.headers.get("x-github-delivery");
152
+ case 'github':
153
+ metadata.event = request.headers.get('x-github-event');
154
+ metadata.delivery = request.headers.get('x-github-delivery');
156
155
  break;
157
- case "stripe":
156
+ case 'stripe':
158
157
  // Extract Stripe-specific metadata from signature
159
158
  const headerValue = request.headers.get(this.config.headerName);
160
- if (headerValue && this.config.headerFormat === "comma-separated") {
161
- const parts = headerValue.split(",");
159
+ if (headerValue && this.config.headerFormat === 'comma-separated') {
160
+ const parts = headerValue.split(',');
162
161
  const sigMap = {};
163
162
  for (const part of parts) {
164
- const [key, value] = part.split("=");
163
+ const [key, value] = part.split('=');
165
164
  if (key && value) {
166
165
  sigMap[key] = value;
167
166
  }
@@ -169,8 +168,8 @@ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
169
168
  metadata.id = sigMap.id;
170
169
  }
171
170
  break;
172
- case "clerk":
173
- metadata.id = request.headers.get("svix-id");
171
+ case 'clerk':
172
+ metadata.id = request.headers.get('svix-id');
174
173
  break;
175
174
  }
176
175
  return metadata;
@@ -192,7 +191,7 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
192
191
  const rawBody = await request.text();
193
192
  // Extract timestamp based on platform configuration
194
193
  let timestamp = null;
195
- if (this.config.headerFormat === "comma-separated") {
194
+ if (this.config.headerFormat === 'comma-separated') {
196
195
  // For platforms like Stripe where timestamp is embedded in signature
197
196
  timestamp = this.extractTimestampFromSignature(request);
198
197
  }
@@ -204,7 +203,7 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
204
203
  if (timestamp && !this.isTimestampValid(timestamp)) {
205
204
  return {
206
205
  isValid: false,
207
- error: "Webhook timestamp expired",
206
+ error: 'Webhook timestamp expired',
208
207
  platform: this.platform,
209
208
  };
210
209
  }
@@ -212,12 +211,12 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
212
211
  const payload = this.formatPayload(rawBody, request);
213
212
  // Verify signature based on platform configuration
214
213
  let isValid = false;
215
- const algorithm = this.config.algorithm.replace("hmac-", "");
216
- if (this.config.customConfig?.encoding === "base64") {
214
+ const algorithm = this.config.algorithm.replace('hmac-', '');
215
+ if (this.config.customConfig?.encoding === 'base64') {
217
216
  // For platforms like Clerk that use base64 encoding
218
217
  isValid = this.verifyHMACWithBase64(payload, signature, algorithm);
219
218
  }
220
- else if (this.config.headerFormat === "prefixed") {
219
+ else if (this.config.headerFormat === 'prefixed') {
221
220
  // For platforms like GitHub that use prefixed signatures
222
221
  isValid = this.verifyHMACWithPrefix(payload, signature, algorithm);
223
222
  }
@@ -228,7 +227,7 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
228
227
  if (!isValid) {
229
228
  return {
230
229
  isValid: false,
231
- error: "Invalid signature",
230
+ error: 'Invalid signature',
232
231
  platform: this.platform,
233
232
  };
234
233
  }
@@ -261,33 +260,33 @@ class GenericHMACVerifier extends AlgorithmBasedVerifier {
261
260
  exports.GenericHMACVerifier = GenericHMACVerifier;
262
261
  // Legacy verifiers for backward compatibility
263
262
  class HMACSHA256Verifier extends GenericHMACVerifier {
264
- constructor(secret, config, platform = "unknown", toleranceInSeconds = 300) {
263
+ constructor(secret, config, platform = 'unknown', toleranceInSeconds = 300) {
265
264
  super(secret, config, platform, toleranceInSeconds);
266
265
  }
267
266
  }
268
267
  exports.HMACSHA256Verifier = HMACSHA256Verifier;
269
268
  class HMACSHA1Verifier extends GenericHMACVerifier {
270
- constructor(secret, config, platform = "unknown", toleranceInSeconds = 300) {
269
+ constructor(secret, config, platform = 'unknown', toleranceInSeconds = 300) {
271
270
  super(secret, config, platform, toleranceInSeconds);
272
271
  }
273
272
  }
274
273
  exports.HMACSHA1Verifier = HMACSHA1Verifier;
275
274
  class HMACSHA512Verifier extends GenericHMACVerifier {
276
- constructor(secret, config, platform = "unknown", toleranceInSeconds = 300) {
275
+ constructor(secret, config, platform = 'unknown', toleranceInSeconds = 300) {
277
276
  super(secret, config, platform, toleranceInSeconds);
278
277
  }
279
278
  }
280
279
  exports.HMACSHA512Verifier = HMACSHA512Verifier;
281
280
  // Factory function to create verifiers based on algorithm
282
- function createAlgorithmVerifier(secret, config, platform = "unknown", toleranceInSeconds = 300) {
281
+ function createAlgorithmVerifier(secret, config, platform = 'unknown', toleranceInSeconds = 300) {
283
282
  switch (config.algorithm) {
284
- case "hmac-sha256":
285
- case "hmac-sha1":
286
- case "hmac-sha512":
283
+ case 'hmac-sha256':
284
+ case 'hmac-sha1':
285
+ case 'hmac-sha512':
287
286
  return new GenericHMACVerifier(secret, config, platform, toleranceInSeconds);
288
- case "rsa-sha256":
289
- case "ed25519":
290
- case "custom":
287
+ case 'rsa-sha256':
288
+ case 'ed25519':
289
+ case 'custom':
291
290
  // These can be implemented as needed
292
291
  throw new Error(`Algorithm ${config.algorithm} not yet implemented`);
293
292
  default:
@@ -1,4 +1,4 @@
1
- import { WebhookVerificationResult } from "../types";
1
+ import { WebhookVerificationResult } from '../types';
2
2
  export declare abstract class WebhookVerifier {
3
3
  protected secret: string;
4
4
  protected toleranceInSeconds: number;
@@ -1,5 +1,5 @@
1
- import { WebhookVerifier } from "./base";
2
- import { WebhookVerificationResult, SignatureConfig } from "../types";
1
+ import { WebhookVerifier } from './base';
2
+ import { WebhookVerificationResult, SignatureConfig } from '../types';
3
3
  export declare class TokenBasedVerifier extends WebhookVerifier {
4
4
  private config;
5
5
  constructor(secret: string, config: SignatureConfig, toleranceInSeconds?: number);
@@ -12,12 +12,12 @@ class TokenBasedVerifier extends base_1.WebhookVerifier {
12
12
  async verify(request) {
13
13
  try {
14
14
  const token = request.headers.get(this.config.headerName);
15
- const id = request.headers.get(this.config.customConfig?.idHeader || "x-webhook-id");
15
+ const id = request.headers.get(this.config.customConfig?.idHeader || 'x-webhook-id');
16
16
  if (!token) {
17
17
  return {
18
18
  isValid: false,
19
19
  error: `Missing token header: ${this.config.headerName}`,
20
- platform: "custom",
20
+ platform: 'custom',
21
21
  };
22
22
  }
23
23
  // Simple token comparison
@@ -25,8 +25,8 @@ class TokenBasedVerifier extends base_1.WebhookVerifier {
25
25
  if (!isValid) {
26
26
  return {
27
27
  isValid: false,
28
- error: "Invalid token",
29
- platform: "custom",
28
+ error: 'Invalid token',
29
+ platform: 'custom',
30
30
  };
31
31
  }
32
32
  const rawBody = await request.text();
@@ -39,11 +39,11 @@ class TokenBasedVerifier extends base_1.WebhookVerifier {
39
39
  }
40
40
  return {
41
41
  isValid: true,
42
- platform: "custom",
42
+ platform: 'custom',
43
43
  payload,
44
44
  metadata: {
45
45
  id,
46
- algorithm: "token-based",
46
+ algorithm: 'token-based',
47
47
  },
48
48
  };
49
49
  }
@@ -51,7 +51,7 @@ class TokenBasedVerifier extends base_1.WebhookVerifier {
51
51
  return {
52
52
  isValid: false,
53
53
  error: `Token-based verification error: ${error.message}`,
54
- platform: "custom",
54
+ platform: 'custom',
55
55
  };
56
56
  }
57
57
  }
@@ -61,7 +61,7 @@ exports.TokenBasedVerifier = TokenBasedVerifier;
61
61
  function createCustomVerifier(secret, config, toleranceInSeconds = 300) {
62
62
  const customType = config.customConfig?.type;
63
63
  switch (customType) {
64
- case "token-based":
64
+ case 'token-based':
65
65
  return new TokenBasedVerifier(secret, config, toleranceInSeconds);
66
66
  default:
67
67
  // Fallback to token-based for unknown custom types
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hookflo/tern",
3
- "version": "1.0.5",
3
+ "version": "2.0.0",
4
4
  "description": "A robust, scalable webhook verification framework supporting multiple platforms and signature algorithms",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",