@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.
@@ -0,0 +1,279 @@
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 __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.StripeCustomVerifier = exports.ClerkCustomVerifier = exports.TokenBasedVerifier = void 0;
37
+ exports.createCustomVerifier = createCustomVerifier;
38
+ const base_1 = require("./base");
39
+ // Custom verifier for token-based authentication (like Supabase)
40
+ class TokenBasedVerifier extends base_1.WebhookVerifier {
41
+ constructor(secret, config, toleranceInSeconds = 300) {
42
+ super(secret, toleranceInSeconds);
43
+ this.config = config;
44
+ }
45
+ async verify(request) {
46
+ try {
47
+ const token = request.headers.get(this.config.headerName);
48
+ const id = request.headers.get(this.config.customConfig?.idHeader || "x-webhook-id");
49
+ if (!token) {
50
+ return {
51
+ isValid: false,
52
+ error: `Missing token header: ${this.config.headerName}`,
53
+ platform: "unknown",
54
+ };
55
+ }
56
+ // Simple token comparison
57
+ const isValid = this.safeCompare(token, this.secret);
58
+ if (!isValid) {
59
+ return {
60
+ isValid: false,
61
+ error: "Invalid token",
62
+ platform: "unknown",
63
+ };
64
+ }
65
+ const rawBody = await request.text();
66
+ let payload;
67
+ try {
68
+ payload = JSON.parse(rawBody);
69
+ }
70
+ catch (e) {
71
+ payload = rawBody;
72
+ }
73
+ return {
74
+ isValid: true,
75
+ platform: "unknown",
76
+ payload,
77
+ metadata: {
78
+ id,
79
+ algorithm: "token-based",
80
+ },
81
+ };
82
+ }
83
+ catch (error) {
84
+ return {
85
+ isValid: false,
86
+ error: `Token-based verification error: ${error.message}`,
87
+ platform: "unknown",
88
+ };
89
+ }
90
+ }
91
+ }
92
+ exports.TokenBasedVerifier = TokenBasedVerifier;
93
+ // Custom verifier for Clerk's specific format
94
+ class ClerkCustomVerifier extends base_1.WebhookVerifier {
95
+ constructor(secret, config, toleranceInSeconds = 300) {
96
+ super(secret, toleranceInSeconds);
97
+ this.config = config;
98
+ }
99
+ async verify(request) {
100
+ try {
101
+ const body = await request.text();
102
+ const svixId = request.headers.get("svix-id");
103
+ const svixTimestamp = request.headers.get("svix-timestamp");
104
+ const svixSignature = request.headers.get("svix-signature");
105
+ if (!svixId || !svixTimestamp || !svixSignature) {
106
+ return {
107
+ isValid: false,
108
+ error: "Missing required Clerk webhook headers",
109
+ platform: "clerk",
110
+ };
111
+ }
112
+ const timestamp = parseInt(svixTimestamp, 10);
113
+ if (!this.isTimestampValid(timestamp)) {
114
+ return {
115
+ isValid: false,
116
+ error: "Webhook timestamp is too old",
117
+ platform: "clerk",
118
+ };
119
+ }
120
+ const signedContent = `${svixId}.${svixTimestamp}.${body}`;
121
+ const secretBytes = new Uint8Array(Buffer.from(this.secret.split("_")[1], "base64"));
122
+ const { createHmac } = await Promise.resolve().then(() => __importStar(require("crypto")));
123
+ const expectedSignature = createHmac("sha256", secretBytes)
124
+ .update(signedContent)
125
+ .digest("base64");
126
+ const signatures = svixSignature.split(" ");
127
+ let isValid = false;
128
+ for (const sig of signatures) {
129
+ const [version, signature] = sig.split(",");
130
+ if (version === "v1" &&
131
+ this.safeCompare(signature, expectedSignature)) {
132
+ isValid = true;
133
+ break;
134
+ }
135
+ }
136
+ if (!isValid) {
137
+ return {
138
+ isValid: false,
139
+ error: "Invalid signature",
140
+ platform: "clerk",
141
+ };
142
+ }
143
+ return {
144
+ isValid: true,
145
+ platform: "clerk",
146
+ payload: JSON.parse(body),
147
+ metadata: {
148
+ id: svixId,
149
+ timestamp: svixTimestamp,
150
+ algorithm: "clerk-custom",
151
+ },
152
+ };
153
+ }
154
+ catch (error) {
155
+ return {
156
+ isValid: false,
157
+ error: error instanceof Error ? error.message : "Unknown error",
158
+ platform: "clerk",
159
+ };
160
+ }
161
+ }
162
+ }
163
+ exports.ClerkCustomVerifier = ClerkCustomVerifier;
164
+ // Custom verifier for Stripe's specific format
165
+ class StripeCustomVerifier extends base_1.WebhookVerifier {
166
+ constructor(secret, config, toleranceInSeconds = 300) {
167
+ super(secret, toleranceInSeconds);
168
+ this.config = config;
169
+ }
170
+ async verify(request) {
171
+ try {
172
+ const signature = request.headers.get("Stripe-Signature") ||
173
+ request.headers.get("stripe-signature") ||
174
+ request.headers.get("x-stripe-signature");
175
+ if (!signature) {
176
+ return {
177
+ isValid: false,
178
+ error: "Missing Stripe signature header",
179
+ platform: "stripe",
180
+ };
181
+ }
182
+ const rawBody = await request.text();
183
+ const sigParts = signature.split(",");
184
+ const sigMap = {};
185
+ for (const part of sigParts) {
186
+ const [key, value] = part.split("=");
187
+ if (key && value) {
188
+ sigMap[key] = value;
189
+ }
190
+ }
191
+ const timestamp = sigMap.t;
192
+ const sig = sigMap.v1;
193
+ if (!timestamp || !sig) {
194
+ return {
195
+ isValid: false,
196
+ error: "Invalid Stripe signature format",
197
+ platform: "stripe",
198
+ };
199
+ }
200
+ const timestampNum = parseInt(timestamp, 10);
201
+ if (!this.isTimestampValid(timestampNum)) {
202
+ return {
203
+ isValid: false,
204
+ error: "Stripe webhook timestamp expired",
205
+ platform: "stripe",
206
+ };
207
+ }
208
+ const signedPayload = `${timestamp}.${rawBody}`;
209
+ const { createHmac } = await Promise.resolve().then(() => __importStar(require("crypto")));
210
+ const hmac = createHmac("sha256", this.secret);
211
+ hmac.update(signedPayload);
212
+ const expectedSignature = hmac.digest("hex");
213
+ const isValid = this.safeCompare(sig, expectedSignature);
214
+ if (!isValid) {
215
+ console.error("Stripe signature verification failed:", {
216
+ received: sig,
217
+ expected: expectedSignature,
218
+ timestamp,
219
+ bodyLength: rawBody.length,
220
+ signedPayload: `${signedPayload.substring(0, 50)}...`,
221
+ });
222
+ return {
223
+ isValid: false,
224
+ error: "Invalid Stripe signature",
225
+ platform: "stripe",
226
+ };
227
+ }
228
+ let payload;
229
+ try {
230
+ payload = JSON.parse(rawBody);
231
+ }
232
+ catch (e) {
233
+ return {
234
+ isValid: true,
235
+ platform: "stripe",
236
+ metadata: {
237
+ timestamp,
238
+ id: sigMap.id,
239
+ algorithm: "stripe-custom",
240
+ },
241
+ };
242
+ }
243
+ return {
244
+ isValid: true,
245
+ platform: "stripe",
246
+ payload,
247
+ metadata: {
248
+ timestamp,
249
+ id: sigMap.id,
250
+ algorithm: "stripe-custom",
251
+ },
252
+ };
253
+ }
254
+ catch (error) {
255
+ console.error("Stripe verification error:", error);
256
+ return {
257
+ isValid: false,
258
+ error: `Stripe verification error: ${error.message}`,
259
+ platform: "stripe",
260
+ };
261
+ }
262
+ }
263
+ }
264
+ exports.StripeCustomVerifier = StripeCustomVerifier;
265
+ // Factory function for custom verifiers
266
+ function createCustomVerifier(secret, config, toleranceInSeconds = 300) {
267
+ const customType = config.customConfig?.type;
268
+ switch (customType) {
269
+ case "token-based":
270
+ return new TokenBasedVerifier(secret, config, toleranceInSeconds);
271
+ case "clerk-custom":
272
+ return new ClerkCustomVerifier(secret, config, toleranceInSeconds);
273
+ case "stripe-custom":
274
+ return new StripeCustomVerifier(secret, config, toleranceInSeconds);
275
+ default:
276
+ // Fallback to token-based for unknown custom types
277
+ return new TokenBasedVerifier(secret, config, toleranceInSeconds);
278
+ }
279
+ }
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@hookflo/tern",
3
+ "version": "1.0.0",
4
+ "description": "A robust, scalable webhook verification framework supporting multiple platforms and signature algorithms",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "dev": "tsc --watch",
10
+ "test": "node dist/test.js",
11
+ "lint": "eslint src/**/*.ts",
12
+ "lint:fix": "eslint 'src/**/*.ts' --fix",
13
+ "format": "prettier --write src/**/*.ts",
14
+ "prepare": "npm run build",
15
+ "publish": "npm publish",
16
+ "clean": "rm -rf dist",
17
+ "prebuild": "npm run clean"
18
+ },
19
+ "keywords": [
20
+ "Hookflo",
21
+ "webhook",
22
+ "verification",
23
+ "security",
24
+ "hmac",
25
+ "signature",
26
+ "github",
27
+ "stripe",
28
+ "clerk",
29
+ "supabase",
30
+ "typescript",
31
+ "node",
32
+ "express",
33
+ "nextjs"
34
+ ],
35
+ "author": "Prateek Jain",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/Hookflo/tern"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/Hookflo/tern/issues"
43
+ },
44
+ "homepage": "https://github.com/Hookflo/tern#readme",
45
+ "devDependencies": {
46
+ "@types/express": "^5.0.3",
47
+ "@types/jest": "29.5.0",
48
+ "@types/node": "20.0.0",
49
+ "@typescript-eslint/eslint-plugin": "^8.39.0",
50
+ "@typescript-eslint/parser": "^8.39.0",
51
+ "eslint": "^8.57.1",
52
+ "eslint-config-airbnb-base": "^15.0.0",
53
+ "eslint-plugin-import": "^2.32.0",
54
+ "jest": "29.5.0",
55
+ "prettier": "3.0.0",
56
+ "ts-jest": "29.1.0",
57
+ "typescript": "^5.0.0"
58
+ },
59
+ "engines": {
60
+ "node": ">=16.0.0"
61
+ },
62
+ "files": [
63
+ "dist",
64
+ "README.md",
65
+ "LICENSE"
66
+ ],
67
+ "publishConfig": {
68
+ "access": "public"
69
+ }
70
+ }