ezthrottle 1.1.1 → 1.4.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 +824 -0
- package/dist/client.d.ts +108 -0
- package/dist/client.js +251 -0
- package/dist/forward.d.ts +75 -0
- package/dist/forward.js +101 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +9 -1
- package/dist/webhookUtils.d.ts +116 -0
- package/dist/webhookUtils.js +224 -0
- package/package.json +3 -3
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook utilities for EZThrottle SDK.
|
|
3
|
+
* Provides HMAC signature verification for secure webhook delivery.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Result of webhook signature verification
|
|
7
|
+
*/
|
|
8
|
+
export interface VerificationResult {
|
|
9
|
+
verified: boolean;
|
|
10
|
+
reason: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Custom error for webhook verification failures
|
|
14
|
+
*/
|
|
15
|
+
export declare class WebhookVerificationError extends Error {
|
|
16
|
+
constructor(message: string);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Verify HMAC-SHA256 signature from X-EZThrottle-Signature header.
|
|
20
|
+
*
|
|
21
|
+
* @param payload - Raw webhook payload (request body as Buffer or string)
|
|
22
|
+
* @param signatureHeader - Value of X-EZThrottle-Signature header
|
|
23
|
+
* @param secret - Your webhook secret (primary or secondary)
|
|
24
|
+
* @param tolerance - Maximum age of timestamp in seconds (default: 300 = 5 minutes)
|
|
25
|
+
* @returns Object with verified boolean and reason string
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* import express from 'express';
|
|
30
|
+
* import { verifyWebhookSignature } from 'ezthrottle';
|
|
31
|
+
*
|
|
32
|
+
* const app = express();
|
|
33
|
+
* const WEBHOOK_SECRET = 'your_webhook_secret';
|
|
34
|
+
*
|
|
35
|
+
* app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
|
|
36
|
+
* const signature = req.headers['x-ezthrottle-signature'] as string;
|
|
37
|
+
* const { verified, reason } = verifyWebhookSignature(
|
|
38
|
+
* req.body,
|
|
39
|
+
* signature,
|
|
40
|
+
* WEBHOOK_SECRET
|
|
41
|
+
* );
|
|
42
|
+
*
|
|
43
|
+
* if (!verified) {
|
|
44
|
+
* return res.status(401).json({ error: `Invalid signature: ${reason}` });
|
|
45
|
+
* }
|
|
46
|
+
*
|
|
47
|
+
* // Process webhook...
|
|
48
|
+
* const data = JSON.parse(req.body.toString());
|
|
49
|
+
* console.log(`Job ${data.job_id} completed: ${data.status}`);
|
|
50
|
+
*
|
|
51
|
+
* res.json({ ok: true });
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare function verifyWebhookSignature(payload: Buffer | string, signatureHeader: string, secret: string, tolerance?: number): VerificationResult;
|
|
56
|
+
/**
|
|
57
|
+
* Verify webhook signature and throw exception if invalid.
|
|
58
|
+
*
|
|
59
|
+
* @param payload - Raw webhook payload
|
|
60
|
+
* @param signatureHeader - Value of X-EZThrottle-Signature header
|
|
61
|
+
* @param secret - Your webhook secret
|
|
62
|
+
* @param tolerance - Maximum age of timestamp in seconds (default: 300)
|
|
63
|
+
* @throws {WebhookVerificationError} If signature verification fails
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* import express from 'express';
|
|
68
|
+
* import { verifyWebhookSignatureStrict, WebhookVerificationError } from 'ezthrottle';
|
|
69
|
+
*
|
|
70
|
+
* app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
|
|
71
|
+
* try {
|
|
72
|
+
* verifyWebhookSignatureStrict(
|
|
73
|
+
* req.body,
|
|
74
|
+
* req.headers['x-ezthrottle-signature'] as string,
|
|
75
|
+
* WEBHOOK_SECRET
|
|
76
|
+
* );
|
|
77
|
+
* } catch (error) {
|
|
78
|
+
* if (error instanceof WebhookVerificationError) {
|
|
79
|
+
* return res.status(401).json({ error: error.message });
|
|
80
|
+
* }
|
|
81
|
+
* throw error;
|
|
82
|
+
* }
|
|
83
|
+
*
|
|
84
|
+
* // Process webhook...
|
|
85
|
+
* res.json({ ok: true });
|
|
86
|
+
* });
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export declare function verifyWebhookSignatureStrict(payload: Buffer | string, signatureHeader: string, secret: string, tolerance?: number): void;
|
|
90
|
+
/**
|
|
91
|
+
* Try verifying signature with primary secret, fall back to secondary if provided.
|
|
92
|
+
* Useful during secret rotation when you have both old and new secrets active.
|
|
93
|
+
*
|
|
94
|
+
* @param payload - Raw webhook payload
|
|
95
|
+
* @param signatureHeader - Value of X-EZThrottle-Signature header
|
|
96
|
+
* @param primarySecret - Your primary webhook secret
|
|
97
|
+
* @param secondarySecret - Your secondary webhook secret (optional)
|
|
98
|
+
* @param tolerance - Maximum age of timestamp in seconds
|
|
99
|
+
* @returns Object with verified boolean and reason string
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* // During secret rotation
|
|
104
|
+
* const { verified, reason } = tryVerifyWithSecrets(
|
|
105
|
+
* req.body,
|
|
106
|
+
* req.headers['x-ezthrottle-signature'] as string,
|
|
107
|
+
* 'new_secret_after_rotation',
|
|
108
|
+
* 'old_secret_before_rotation'
|
|
109
|
+
* );
|
|
110
|
+
*
|
|
111
|
+
* if (verified) {
|
|
112
|
+
* console.log(`Signature verified with ${reason}`); // "valid_primary" or "valid_secondary"
|
|
113
|
+
* }
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export declare function tryVerifyWithSecrets(payload: Buffer | string, signatureHeader: string, primarySecret: string, secondarySecret?: string, tolerance?: number): VerificationResult;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Webhook utilities for EZThrottle SDK.
|
|
4
|
+
* Provides HMAC signature verification for secure webhook delivery.
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.WebhookVerificationError = void 0;
|
|
41
|
+
exports.verifyWebhookSignature = verifyWebhookSignature;
|
|
42
|
+
exports.verifyWebhookSignatureStrict = verifyWebhookSignatureStrict;
|
|
43
|
+
exports.tryVerifyWithSecrets = tryVerifyWithSecrets;
|
|
44
|
+
const crypto = __importStar(require("crypto"));
|
|
45
|
+
/**
|
|
46
|
+
* Custom error for webhook verification failures
|
|
47
|
+
*/
|
|
48
|
+
class WebhookVerificationError extends Error {
|
|
49
|
+
constructor(message) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = 'WebhookVerificationError';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.WebhookVerificationError = WebhookVerificationError;
|
|
55
|
+
/**
|
|
56
|
+
* Verify HMAC-SHA256 signature from X-EZThrottle-Signature header.
|
|
57
|
+
*
|
|
58
|
+
* @param payload - Raw webhook payload (request body as Buffer or string)
|
|
59
|
+
* @param signatureHeader - Value of X-EZThrottle-Signature header
|
|
60
|
+
* @param secret - Your webhook secret (primary or secondary)
|
|
61
|
+
* @param tolerance - Maximum age of timestamp in seconds (default: 300 = 5 minutes)
|
|
62
|
+
* @returns Object with verified boolean and reason string
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* import express from 'express';
|
|
67
|
+
* import { verifyWebhookSignature } from 'ezthrottle';
|
|
68
|
+
*
|
|
69
|
+
* const app = express();
|
|
70
|
+
* const WEBHOOK_SECRET = 'your_webhook_secret';
|
|
71
|
+
*
|
|
72
|
+
* app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
|
|
73
|
+
* const signature = req.headers['x-ezthrottle-signature'] as string;
|
|
74
|
+
* const { verified, reason } = verifyWebhookSignature(
|
|
75
|
+
* req.body,
|
|
76
|
+
* signature,
|
|
77
|
+
* WEBHOOK_SECRET
|
|
78
|
+
* );
|
|
79
|
+
*
|
|
80
|
+
* if (!verified) {
|
|
81
|
+
* return res.status(401).json({ error: `Invalid signature: ${reason}` });
|
|
82
|
+
* }
|
|
83
|
+
*
|
|
84
|
+
* // Process webhook...
|
|
85
|
+
* const data = JSON.parse(req.body.toString());
|
|
86
|
+
* console.log(`Job ${data.job_id} completed: ${data.status}`);
|
|
87
|
+
*
|
|
88
|
+
* res.json({ ok: true });
|
|
89
|
+
* });
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
function verifyWebhookSignature(payload, signatureHeader, secret, tolerance = 300) {
|
|
93
|
+
if (!signatureHeader) {
|
|
94
|
+
return { verified: false, reason: 'no_signature_header' };
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
// Parse "t=timestamp,v1=signature" format
|
|
98
|
+
const parts = {};
|
|
99
|
+
signatureHeader.split(',').forEach(part => {
|
|
100
|
+
const [key, value] = part.split('=');
|
|
101
|
+
if (key && value) {
|
|
102
|
+
parts[key] = value;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
const timestampStr = parts['t'] || '0';
|
|
106
|
+
const signature = parts['v1'] || '';
|
|
107
|
+
if (!signature) {
|
|
108
|
+
return { verified: false, reason: 'missing_v1_signature' };
|
|
109
|
+
}
|
|
110
|
+
// Check timestamp tolerance
|
|
111
|
+
const now = Math.floor(Date.now() / 1000);
|
|
112
|
+
const sigTime = parseInt(timestampStr, 10);
|
|
113
|
+
const timeDiff = Math.abs(now - sigTime);
|
|
114
|
+
if (timeDiff > tolerance) {
|
|
115
|
+
return {
|
|
116
|
+
verified: false,
|
|
117
|
+
reason: `timestamp_expired (diff=${timeDiff}s, tolerance=${tolerance}s)`
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// Compute expected signature
|
|
121
|
+
const payloadStr = Buffer.isBuffer(payload) ? payload.toString('utf-8') : payload;
|
|
122
|
+
const signedPayload = `${timestampStr}.${payloadStr}`;
|
|
123
|
+
const expected = crypto
|
|
124
|
+
.createHmac('sha256', secret)
|
|
125
|
+
.update(signedPayload)
|
|
126
|
+
.digest('hex');
|
|
127
|
+
// Constant-time comparison
|
|
128
|
+
if (crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
|
|
129
|
+
return { verified: true, reason: 'valid' };
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
return { verified: false, reason: 'signature_mismatch' };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
return {
|
|
137
|
+
verified: false,
|
|
138
|
+
reason: `verification_error: ${error instanceof Error ? error.message : String(error)}`
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Verify webhook signature and throw exception if invalid.
|
|
144
|
+
*
|
|
145
|
+
* @param payload - Raw webhook payload
|
|
146
|
+
* @param signatureHeader - Value of X-EZThrottle-Signature header
|
|
147
|
+
* @param secret - Your webhook secret
|
|
148
|
+
* @param tolerance - Maximum age of timestamp in seconds (default: 300)
|
|
149
|
+
* @throws {WebhookVerificationError} If signature verification fails
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* import express from 'express';
|
|
154
|
+
* import { verifyWebhookSignatureStrict, WebhookVerificationError } from 'ezthrottle';
|
|
155
|
+
*
|
|
156
|
+
* app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
|
|
157
|
+
* try {
|
|
158
|
+
* verifyWebhookSignatureStrict(
|
|
159
|
+
* req.body,
|
|
160
|
+
* req.headers['x-ezthrottle-signature'] as string,
|
|
161
|
+
* WEBHOOK_SECRET
|
|
162
|
+
* );
|
|
163
|
+
* } catch (error) {
|
|
164
|
+
* if (error instanceof WebhookVerificationError) {
|
|
165
|
+
* return res.status(401).json({ error: error.message });
|
|
166
|
+
* }
|
|
167
|
+
* throw error;
|
|
168
|
+
* }
|
|
169
|
+
*
|
|
170
|
+
* // Process webhook...
|
|
171
|
+
* res.json({ ok: true });
|
|
172
|
+
* });
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
function verifyWebhookSignatureStrict(payload, signatureHeader, secret, tolerance = 300) {
|
|
176
|
+
const { verified, reason } = verifyWebhookSignature(payload, signatureHeader, secret, tolerance);
|
|
177
|
+
if (!verified) {
|
|
178
|
+
throw new WebhookVerificationError(`Webhook signature verification failed: ${reason}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Try verifying signature with primary secret, fall back to secondary if provided.
|
|
183
|
+
* Useful during secret rotation when you have both old and new secrets active.
|
|
184
|
+
*
|
|
185
|
+
* @param payload - Raw webhook payload
|
|
186
|
+
* @param signatureHeader - Value of X-EZThrottle-Signature header
|
|
187
|
+
* @param primarySecret - Your primary webhook secret
|
|
188
|
+
* @param secondarySecret - Your secondary webhook secret (optional)
|
|
189
|
+
* @param tolerance - Maximum age of timestamp in seconds
|
|
190
|
+
* @returns Object with verified boolean and reason string
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```typescript
|
|
194
|
+
* // During secret rotation
|
|
195
|
+
* const { verified, reason } = tryVerifyWithSecrets(
|
|
196
|
+
* req.body,
|
|
197
|
+
* req.headers['x-ezthrottle-signature'] as string,
|
|
198
|
+
* 'new_secret_after_rotation',
|
|
199
|
+
* 'old_secret_before_rotation'
|
|
200
|
+
* );
|
|
201
|
+
*
|
|
202
|
+
* if (verified) {
|
|
203
|
+
* console.log(`Signature verified with ${reason}`); // "valid_primary" or "valid_secondary"
|
|
204
|
+
* }
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
function tryVerifyWithSecrets(payload, signatureHeader, primarySecret, secondarySecret, tolerance = 300) {
|
|
208
|
+
// Try primary secret first
|
|
209
|
+
const primaryResult = verifyWebhookSignature(payload, signatureHeader, primarySecret, tolerance);
|
|
210
|
+
if (primaryResult.verified) {
|
|
211
|
+
return { verified: true, reason: 'valid_primary' };
|
|
212
|
+
}
|
|
213
|
+
// Try secondary secret if provided
|
|
214
|
+
if (secondarySecret) {
|
|
215
|
+
const secondaryResult = verifyWebhookSignature(payload, signatureHeader, secondarySecret, tolerance);
|
|
216
|
+
if (secondaryResult.verified) {
|
|
217
|
+
return { verified: true, reason: 'valid_secondary' };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
verified: false,
|
|
222
|
+
reason: `both_secrets_failed (primary: ${primaryResult.reason})`
|
|
223
|
+
};
|
|
224
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ezthrottle",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Node.js SDK for EZThrottle - The API Dam for rate-limited services",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"repository": {
|
|
23
23
|
"type": "git",
|
|
24
|
-
"url": "https://github.com/rjpruitt16/ezthrottle-node"
|
|
24
|
+
"url": "git+https://github.com/rjpruitt16/ezthrottle-node.git"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"dist",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"node-fetch": "^2.7.0",
|
|
32
|
-
"uuid": "
|
|
32
|
+
"uuid": "8.3.2"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/node": "^20.0.0",
|