@hookflo/tern 1.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/dist/utils.js ADDED
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getRecommendedAlgorithm = getRecommendedAlgorithm;
4
+ exports.platformSupportsAlgorithm = platformSupportsAlgorithm;
5
+ exports.getPlatformsByAlgorithm = getPlatformsByAlgorithm;
6
+ exports.createSignatureConfigForPlatform = createSignatureConfigForPlatform;
7
+ exports.isValidSignatureConfig = isValidSignatureConfig;
8
+ exports.getPlatformDescription = getPlatformDescription;
9
+ exports.isCustomAlgorithm = isCustomAlgorithm;
10
+ exports.getAlgorithmStats = getAlgorithmStats;
11
+ exports.getMostCommonAlgorithm = getMostCommonAlgorithm;
12
+ exports.detectPlatformFromHeaders = detectPlatformFromHeaders;
13
+ exports.getPlatformSummary = getPlatformSummary;
14
+ exports.compareSignatureConfigs = compareSignatureConfigs;
15
+ exports.cloneSignatureConfig = cloneSignatureConfig;
16
+ exports.mergeSignatureConfigs = mergeSignatureConfigs;
17
+ exports.validatePlatformConfig = validatePlatformConfig;
18
+ exports.getValidPlatforms = getValidPlatforms;
19
+ exports.getPlatformsByAlgorithmType = getPlatformsByAlgorithmType;
20
+ exports.cleanHeaders = cleanHeaders;
21
+ const types_1 = require("./types");
22
+ const algorithms_1 = require("./platforms/algorithms");
23
+ /**
24
+ * Utility functions for the scalable webhook verification framework
25
+ */
26
+ /**
27
+ * Get the recommended algorithm for a platform
28
+ */
29
+ function getRecommendedAlgorithm(platform) {
30
+ const config = (0, algorithms_1.getPlatformAlgorithmConfig)(platform);
31
+ return config.signatureConfig.algorithm;
32
+ }
33
+ /**
34
+ * Check if a platform supports a specific algorithm
35
+ */
36
+ function platformSupportsAlgorithm(platform, algorithm) {
37
+ const config = (0, algorithms_1.getPlatformAlgorithmConfig)(platform);
38
+ return config.signatureConfig.algorithm === algorithm;
39
+ }
40
+ /**
41
+ * Get all platforms that support a specific algorithm
42
+ */
43
+ function getPlatformsByAlgorithm(algorithm) {
44
+ const { getPlatformsUsingAlgorithm } = require('./platforms/algorithms');
45
+ return getPlatformsUsingAlgorithm(algorithm);
46
+ }
47
+ /**
48
+ * Create a signature config for a platform
49
+ */
50
+ function createSignatureConfigForPlatform(platform) {
51
+ const config = (0, algorithms_1.getPlatformAlgorithmConfig)(platform);
52
+ return config.signatureConfig;
53
+ }
54
+ /**
55
+ * Validate a signature config
56
+ */
57
+ function isValidSignatureConfig(config) {
58
+ return (0, algorithms_1.validateSignatureConfig)(config);
59
+ }
60
+ /**
61
+ * Get platform description
62
+ */
63
+ function getPlatformDescription(platform) {
64
+ const config = (0, algorithms_1.getPlatformAlgorithmConfig)(platform);
65
+ return config.description || `Webhook verification for ${platform}`;
66
+ }
67
+ /**
68
+ * Check if a platform uses custom algorithm
69
+ */
70
+ function isCustomAlgorithm(platform) {
71
+ const config = (0, algorithms_1.getPlatformAlgorithmConfig)(platform);
72
+ return config.signatureConfig.algorithm === 'custom';
73
+ }
74
+ /**
75
+ * Get algorithm statistics
76
+ */
77
+ function getAlgorithmStats() {
78
+ const platforms = Object.values(types_1.WebhookPlatformKeys);
79
+ const stats = {};
80
+ for (const platform of platforms) {
81
+ const config = (0, algorithms_1.getPlatformAlgorithmConfig)(platform);
82
+ const { algorithm } = config.signatureConfig;
83
+ stats[algorithm] = (stats[algorithm] || 0) + 1;
84
+ }
85
+ return stats;
86
+ }
87
+ /**
88
+ * Get most common algorithm
89
+ */
90
+ function getMostCommonAlgorithm() {
91
+ const stats = getAlgorithmStats();
92
+ return Object.entries(stats).reduce((a, b) => (stats[a[0]] > stats[b[0]] ? a : b))[0];
93
+ }
94
+ /**
95
+ * Check if a request matches a platform's signature pattern
96
+ */
97
+ function detectPlatformFromHeaders(headers) {
98
+ const headerMap = new Map();
99
+ headers.forEach((value, key) => {
100
+ headerMap.set(key.toLowerCase(), value);
101
+ });
102
+ // GitHub
103
+ if (headerMap.has('x-hub-signature-256')) {
104
+ return 'github';
105
+ }
106
+ // Stripe
107
+ if (headerMap.has('stripe-signature')) {
108
+ return 'stripe';
109
+ }
110
+ // Clerk
111
+ if (headerMap.has('svix-signature')) {
112
+ return 'clerk';
113
+ }
114
+ // Dodo Payments
115
+ if (headerMap.has('webhook-signature')) {
116
+ return 'dodopayments';
117
+ }
118
+ // Shopify
119
+ if (headerMap.has('x-shopify-hmac-sha256')) {
120
+ return 'shopify';
121
+ }
122
+ // Vercel
123
+ if (headerMap.has('x-vercel-signature')) {
124
+ return 'vercel';
125
+ }
126
+ // Polar
127
+ if (headerMap.has('x-polar-signature')) {
128
+ return 'polar';
129
+ }
130
+ // Supabase
131
+ if (headerMap.has('x-webhook-token')) {
132
+ return 'supabase';
133
+ }
134
+ return null;
135
+ }
136
+ /**
137
+ * Get platform configuration summary
138
+ */
139
+ function getPlatformSummary() {
140
+ const platforms = Object.values(types_1.WebhookPlatformKeys);
141
+ return platforms.map((platform) => {
142
+ const config = (0, algorithms_1.getPlatformAlgorithmConfig)(platform);
143
+ return {
144
+ platform,
145
+ algorithm: config.signatureConfig.algorithm,
146
+ description: config.description || '',
147
+ isCustom: config.signatureConfig.algorithm === 'custom',
148
+ };
149
+ });
150
+ }
151
+ /**
152
+ * Compare two signature configs
153
+ */
154
+ function compareSignatureConfigs(config1, config2) {
155
+ return JSON.stringify(config1) === JSON.stringify(config2);
156
+ }
157
+ /**
158
+ * Clone a signature config
159
+ */
160
+ function cloneSignatureConfig(config) {
161
+ return JSON.parse(JSON.stringify(config));
162
+ }
163
+ /**
164
+ * Merge signature configs (config2 overrides config1)
165
+ */
166
+ function mergeSignatureConfigs(config1, config2) {
167
+ return { ...config1, ...config2 };
168
+ }
169
+ /**
170
+ * Validate platform configuration
171
+ */
172
+ function validatePlatformConfig(platform) {
173
+ try {
174
+ const config = (0, algorithms_1.getPlatformAlgorithmConfig)(platform);
175
+ return (0, algorithms_1.validateSignatureConfig)(config.signatureConfig);
176
+ }
177
+ catch {
178
+ return false;
179
+ }
180
+ }
181
+ /**
182
+ * Get all valid platforms
183
+ */
184
+ function getValidPlatforms() {
185
+ const platforms = Object.values(types_1.WebhookPlatformKeys);
186
+ return platforms.filter((platform) => validatePlatformConfig(platform));
187
+ }
188
+ /**
189
+ * Get platforms by algorithm type
190
+ */
191
+ function getPlatformsByAlgorithmType() {
192
+ const platforms = Object.values(types_1.WebhookPlatformKeys);
193
+ const result = {};
194
+ for (const platform of platforms) {
195
+ const config = (0, algorithms_1.getPlatformAlgorithmConfig)(platform);
196
+ const { algorithm } = config.signatureConfig;
197
+ if (!result[algorithm]) {
198
+ result[algorithm] = [];
199
+ }
200
+ result[algorithm].push(platform);
201
+ }
202
+ return result;
203
+ }
204
+ function cleanHeaders(headers) {
205
+ const cleaned = {};
206
+ for (const [key, value] of Object.entries(headers)) {
207
+ if (value !== undefined) {
208
+ cleaned[key] = value;
209
+ }
210
+ }
211
+ return cleaned;
212
+ }
@@ -0,0 +1,22 @@
1
+ import { WebhookVerifier } from "./base";
2
+ import { WebhookVerificationResult, SignatureConfig } from "../types";
3
+ export declare abstract class AlgorithmBasedVerifier extends WebhookVerifier {
4
+ protected config: SignatureConfig;
5
+ constructor(secret: string, config: SignatureConfig, toleranceInSeconds?: number);
6
+ abstract verify(request: Request): Promise<WebhookVerificationResult>;
7
+ protected extractSignature(request: Request): string | null;
8
+ protected extractTimestamp(request: Request): number | null;
9
+ protected formatPayload(rawBody: string, timestamp?: number | null): string;
10
+ protected verifyHMAC(payload: string, signature: string, algorithm?: string): boolean;
11
+ protected verifyHMACWithPrefix(payload: string, signature: string, algorithm?: string): boolean;
12
+ }
13
+ export declare class HMACSHA256Verifier extends AlgorithmBasedVerifier {
14
+ verify(request: Request): Promise<WebhookVerificationResult>;
15
+ }
16
+ export declare class HMACSHA1Verifier extends AlgorithmBasedVerifier {
17
+ verify(request: Request): Promise<WebhookVerificationResult>;
18
+ }
19
+ export declare class HMACSHA512Verifier extends AlgorithmBasedVerifier {
20
+ verify(request: Request): Promise<WebhookVerificationResult>;
21
+ }
22
+ export declare function createAlgorithmVerifier(secret: string, config: SignatureConfig, toleranceInSeconds?: number): AlgorithmBasedVerifier;
@@ -0,0 +1,273 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HMACSHA512Verifier = exports.HMACSHA1Verifier = exports.HMACSHA256Verifier = exports.AlgorithmBasedVerifier = void 0;
4
+ exports.createAlgorithmVerifier = createAlgorithmVerifier;
5
+ const crypto_1 = require("crypto");
6
+ const base_1 = require("./base");
7
+ class AlgorithmBasedVerifier extends base_1.WebhookVerifier {
8
+ constructor(secret, config, toleranceInSeconds = 300) {
9
+ super(secret, toleranceInSeconds);
10
+ this.config = config;
11
+ }
12
+ extractSignature(request) {
13
+ const headerValue = request.headers.get(this.config.headerName);
14
+ if (!headerValue)
15
+ return null;
16
+ switch (this.config.headerFormat) {
17
+ case "prefixed":
18
+ return headerValue.startsWith(this.config.prefix || "")
19
+ ? headerValue.substring((this.config.prefix || "").length)
20
+ : null;
21
+ case "comma-separated":
22
+ // Handle comma-separated format like Stripe: "t=1234567890,v1=abc123"
23
+ const parts = headerValue.split(",");
24
+ const sigMap = {};
25
+ for (const part of parts) {
26
+ const [key, value] = part.split("=");
27
+ if (key && value) {
28
+ sigMap[key] = value;
29
+ }
30
+ }
31
+ return sigMap.v1 || sigMap.signature || null;
32
+ case "raw":
33
+ default:
34
+ return headerValue;
35
+ }
36
+ }
37
+ extractTimestamp(request) {
38
+ if (!this.config.timestampHeader)
39
+ return null;
40
+ const timestampHeader = request.headers.get(this.config.timestampHeader);
41
+ if (!timestampHeader)
42
+ return null;
43
+ switch (this.config.timestampFormat) {
44
+ case "unix":
45
+ return parseInt(timestampHeader, 10);
46
+ case "iso":
47
+ return Math.floor(new Date(timestampHeader).getTime() / 1000);
48
+ case "custom":
49
+ // Custom timestamp parsing logic can be added here
50
+ return parseInt(timestampHeader, 10);
51
+ default:
52
+ return parseInt(timestampHeader, 10);
53
+ }
54
+ }
55
+ formatPayload(rawBody, timestamp) {
56
+ switch (this.config.payloadFormat) {
57
+ case "timestamped":
58
+ return timestamp ? `${timestamp}.${rawBody}` : rawBody;
59
+ case "custom":
60
+ // Custom payload formatting logic can be added here
61
+ return rawBody;
62
+ case "raw":
63
+ default:
64
+ return rawBody;
65
+ }
66
+ }
67
+ verifyHMAC(payload, signature, algorithm = "sha256") {
68
+ const hmac = (0, crypto_1.createHmac)(algorithm, this.secret);
69
+ hmac.update(payload);
70
+ const expectedSignature = hmac.digest("hex");
71
+ return this.safeCompare(signature, expectedSignature);
72
+ }
73
+ verifyHMACWithPrefix(payload, signature, algorithm = "sha256") {
74
+ const hmac = (0, crypto_1.createHmac)(algorithm, this.secret);
75
+ hmac.update(payload);
76
+ const expectedSignature = `${this.config.prefix || ""}${hmac.digest("hex")}`;
77
+ return this.safeCompare(signature, expectedSignature);
78
+ }
79
+ }
80
+ exports.AlgorithmBasedVerifier = AlgorithmBasedVerifier;
81
+ class HMACSHA256Verifier extends AlgorithmBasedVerifier {
82
+ async verify(request) {
83
+ try {
84
+ const signature = this.extractSignature(request);
85
+ if (!signature) {
86
+ return {
87
+ isValid: false,
88
+ error: `Missing signature header: ${this.config.headerName}`,
89
+ platform: "unknown",
90
+ };
91
+ }
92
+ const rawBody = await request.text();
93
+ const timestamp = this.extractTimestamp(request);
94
+ if (timestamp && !this.isTimestampValid(timestamp)) {
95
+ return {
96
+ isValid: false,
97
+ error: "Webhook timestamp expired",
98
+ platform: "unknown",
99
+ };
100
+ }
101
+ const payload = this.formatPayload(rawBody, timestamp);
102
+ const isValid = this.config.prefix
103
+ ? this.verifyHMACWithPrefix(payload, signature, "sha256")
104
+ : this.verifyHMAC(payload, signature, "sha256");
105
+ if (!isValid) {
106
+ return {
107
+ isValid: false,
108
+ error: "Invalid signature",
109
+ platform: "unknown",
110
+ };
111
+ }
112
+ let parsedPayload;
113
+ try {
114
+ parsedPayload = JSON.parse(rawBody);
115
+ }
116
+ catch (e) {
117
+ // Return valid even if JSON parsing fails
118
+ parsedPayload = rawBody;
119
+ }
120
+ return {
121
+ isValid: true,
122
+ platform: "unknown",
123
+ payload: parsedPayload,
124
+ metadata: {
125
+ timestamp: timestamp?.toString(),
126
+ algorithm: "hmac-sha256",
127
+ },
128
+ };
129
+ }
130
+ catch (error) {
131
+ return {
132
+ isValid: false,
133
+ error: `HMAC-SHA256 verification error: ${error.message}`,
134
+ platform: "unknown",
135
+ };
136
+ }
137
+ }
138
+ }
139
+ exports.HMACSHA256Verifier = HMACSHA256Verifier;
140
+ class HMACSHA1Verifier extends AlgorithmBasedVerifier {
141
+ async verify(request) {
142
+ try {
143
+ const signature = this.extractSignature(request);
144
+ if (!signature) {
145
+ return {
146
+ isValid: false,
147
+ error: `Missing signature header: ${this.config.headerName}`,
148
+ platform: "unknown",
149
+ };
150
+ }
151
+ const rawBody = await request.text();
152
+ const timestamp = this.extractTimestamp(request);
153
+ if (timestamp && !this.isTimestampValid(timestamp)) {
154
+ return {
155
+ isValid: false,
156
+ error: "Webhook timestamp expired",
157
+ platform: "unknown",
158
+ };
159
+ }
160
+ const payload = this.formatPayload(rawBody, timestamp);
161
+ const isValid = this.config.prefix
162
+ ? this.verifyHMACWithPrefix(payload, signature, "sha1")
163
+ : this.verifyHMAC(payload, signature, "sha1");
164
+ if (!isValid) {
165
+ return {
166
+ isValid: false,
167
+ error: "Invalid signature",
168
+ platform: "unknown",
169
+ };
170
+ }
171
+ let parsedPayload;
172
+ try {
173
+ parsedPayload = JSON.parse(rawBody);
174
+ }
175
+ catch (e) {
176
+ parsedPayload = rawBody;
177
+ }
178
+ return {
179
+ isValid: true,
180
+ platform: "unknown",
181
+ payload: parsedPayload,
182
+ metadata: {
183
+ timestamp: timestamp?.toString(),
184
+ algorithm: "hmac-sha1",
185
+ },
186
+ };
187
+ }
188
+ catch (error) {
189
+ return {
190
+ isValid: false,
191
+ error: `HMAC-SHA1 verification error: ${error.message}`,
192
+ platform: "unknown",
193
+ };
194
+ }
195
+ }
196
+ }
197
+ exports.HMACSHA1Verifier = HMACSHA1Verifier;
198
+ class HMACSHA512Verifier extends AlgorithmBasedVerifier {
199
+ async verify(request) {
200
+ try {
201
+ const signature = this.extractSignature(request);
202
+ if (!signature) {
203
+ return {
204
+ isValid: false,
205
+ error: `Missing signature header: ${this.config.headerName}`,
206
+ platform: "unknown",
207
+ };
208
+ }
209
+ const rawBody = await request.text();
210
+ const timestamp = this.extractTimestamp(request);
211
+ if (timestamp && !this.isTimestampValid(timestamp)) {
212
+ return {
213
+ isValid: false,
214
+ error: "Webhook timestamp expired",
215
+ platform: "unknown",
216
+ };
217
+ }
218
+ const payload = this.formatPayload(rawBody, timestamp);
219
+ const isValid = this.config.prefix
220
+ ? this.verifyHMACWithPrefix(payload, signature, "sha512")
221
+ : this.verifyHMAC(payload, signature, "sha512");
222
+ if (!isValid) {
223
+ return {
224
+ isValid: false,
225
+ error: "Invalid signature",
226
+ platform: "unknown",
227
+ };
228
+ }
229
+ let parsedPayload;
230
+ try {
231
+ parsedPayload = JSON.parse(rawBody);
232
+ }
233
+ catch (e) {
234
+ parsedPayload = rawBody;
235
+ }
236
+ return {
237
+ isValid: true,
238
+ platform: "unknown",
239
+ payload: parsedPayload,
240
+ metadata: {
241
+ timestamp: timestamp?.toString(),
242
+ algorithm: "hmac-sha512",
243
+ },
244
+ };
245
+ }
246
+ catch (error) {
247
+ return {
248
+ isValid: false,
249
+ error: `HMAC-SHA512 verification error: ${error.message}`,
250
+ platform: "unknown",
251
+ };
252
+ }
253
+ }
254
+ }
255
+ exports.HMACSHA512Verifier = HMACSHA512Verifier;
256
+ // Factory function to create verifiers based on algorithm
257
+ function createAlgorithmVerifier(secret, config, toleranceInSeconds = 300) {
258
+ switch (config.algorithm) {
259
+ case "hmac-sha256":
260
+ return new HMACSHA256Verifier(secret, config, toleranceInSeconds);
261
+ case "hmac-sha1":
262
+ return new HMACSHA1Verifier(secret, config, toleranceInSeconds);
263
+ case "hmac-sha512":
264
+ return new HMACSHA512Verifier(secret, config, toleranceInSeconds);
265
+ case "rsa-sha256":
266
+ case "ed25519":
267
+ case "custom":
268
+ // These can be implemented as needed
269
+ throw new Error(`Algorithm ${config.algorithm} not yet implemented`);
270
+ default:
271
+ throw new Error(`Unknown algorithm: ${config.algorithm}`);
272
+ }
273
+ }
@@ -0,0 +1,9 @@
1
+ import { WebhookVerificationResult } from "../types";
2
+ export declare abstract class WebhookVerifier {
3
+ protected secret: string;
4
+ protected toleranceInSeconds: number;
5
+ constructor(secret: string, toleranceInSeconds?: number);
6
+ abstract verify(request: Request): Promise<WebhookVerificationResult>;
7
+ protected isTimestampValid(timestamp: number): boolean;
8
+ protected safeCompare(a: string, b: string): boolean;
9
+ }
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebhookVerifier = void 0;
4
+ const crypto_1 = require("crypto");
5
+ class WebhookVerifier {
6
+ constructor(secret, toleranceInSeconds = 300) {
7
+ this.secret = secret;
8
+ this.toleranceInSeconds = toleranceInSeconds;
9
+ }
10
+ isTimestampValid(timestamp) {
11
+ const now = Math.floor(Date.now() / 1000);
12
+ return Math.abs(now - timestamp) <= this.toleranceInSeconds;
13
+ }
14
+ safeCompare(a, b) {
15
+ if (a.length !== b.length) {
16
+ return false;
17
+ }
18
+ return (0, crypto_1.timingSafeEqual)(new TextEncoder().encode(a), new TextEncoder().encode(b));
19
+ }
20
+ }
21
+ exports.WebhookVerifier = WebhookVerifier;
@@ -0,0 +1,18 @@
1
+ import { WebhookVerifier } from "./base";
2
+ import { WebhookVerificationResult, SignatureConfig } from "../types";
3
+ export declare class TokenBasedVerifier extends WebhookVerifier {
4
+ private config;
5
+ constructor(secret: string, config: SignatureConfig, toleranceInSeconds?: number);
6
+ verify(request: Request): Promise<WebhookVerificationResult>;
7
+ }
8
+ export declare class ClerkCustomVerifier extends WebhookVerifier {
9
+ private config;
10
+ constructor(secret: string, config: SignatureConfig, toleranceInSeconds?: number);
11
+ verify(request: Request): Promise<WebhookVerificationResult>;
12
+ }
13
+ export declare class StripeCustomVerifier extends WebhookVerifier {
14
+ private config;
15
+ constructor(secret: string, config: SignatureConfig, toleranceInSeconds?: number);
16
+ verify(request: Request): Promise<WebhookVerificationResult>;
17
+ }
18
+ export declare function createCustomVerifier(secret: string, config: SignatureConfig, toleranceInSeconds?: number): WebhookVerifier;