@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/LICENSE +21 -0
- package/README.md +396 -0
- package/dist/examples.d.ts +28 -0
- package/dist/examples.js +160 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +142 -0
- package/dist/platforms/algorithms.d.ts +6 -0
- package/dist/platforms/algorithms.js +180 -0
- package/dist/test-compiled.d.ts +1 -0
- package/dist/test-compiled.js +4 -0
- package/dist/test.d.ts +6 -0
- package/dist/test.js +102 -0
- package/dist/types.d.ts +50 -0
- package/dist/types.js +16 -0
- package/dist/utils.d.ts +78 -0
- package/dist/utils.js +212 -0
- package/dist/verifiers/algorithms.d.ts +22 -0
- package/dist/verifiers/algorithms.js +273 -0
- package/dist/verifiers/base.d.ts +9 -0
- package/dist/verifiers/base.js +21 -0
- package/dist/verifiers/custom-algorithms.d.ts +18 -0
- package/dist/verifiers/custom-algorithms.js +279 -0
- package/package.json +70 -0
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;
|