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.
@@ -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.1.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": "^8.3.2"
32
+ "uuid": "8.3.2"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/node": "^20.0.0",