express-rate-limit 7.5.1 → 8.0.1
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/index.cjs +253 -195
- package/dist/index.d.cts +54 -18
- package/dist/index.d.mts +54 -18
- package/dist/index.d.ts +54 -18
- package/dist/index.mjs +249 -192
- package/package.json +31 -52
- package/readme.md +5 -1
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,170 @@
|
|
|
1
|
+
// source/ip-key-generator.ts
|
|
2
|
+
import { isIPv6 } from "node:net";
|
|
3
|
+
import { Address6 } from "ip-address";
|
|
4
|
+
function ipKeyGenerator(ip, ipv6Subnet = 56) {
|
|
5
|
+
if (ipv6Subnet && isIPv6(ip)) {
|
|
6
|
+
return `${new Address6(`${ip}/${ipv6Subnet}`).startAddress().correctForm()}/${ipv6Subnet}`;
|
|
7
|
+
}
|
|
8
|
+
return ip;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// source/memory-store.ts
|
|
12
|
+
var MemoryStore = class {
|
|
13
|
+
constructor() {
|
|
14
|
+
/**
|
|
15
|
+
* These two maps store usage (requests) and reset time by key (for example, IP
|
|
16
|
+
* addresses or API keys).
|
|
17
|
+
*
|
|
18
|
+
* They are split into two to avoid having to iterate through the entire set to
|
|
19
|
+
* determine which ones need reset. Instead, `Client`s are moved from `previous`
|
|
20
|
+
* to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients
|
|
21
|
+
* left in `previous`, i.e., those that have not made any recent requests, are
|
|
22
|
+
* known to be expired and can be deleted in bulk.
|
|
23
|
+
*/
|
|
24
|
+
this.previous = /* @__PURE__ */ new Map();
|
|
25
|
+
this.current = /* @__PURE__ */ new Map();
|
|
26
|
+
/**
|
|
27
|
+
* Confirmation that the keys incremented in once instance of MemoryStore
|
|
28
|
+
* cannot affect other instances.
|
|
29
|
+
*/
|
|
30
|
+
this.localKeys = true;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Method that initializes the store.
|
|
34
|
+
*
|
|
35
|
+
* @param options {Options} - The options used to setup the middleware.
|
|
36
|
+
*/
|
|
37
|
+
init(options) {
|
|
38
|
+
this.windowMs = options.windowMs;
|
|
39
|
+
if (this.interval) clearInterval(this.interval);
|
|
40
|
+
this.interval = setInterval(() => {
|
|
41
|
+
this.clearExpired();
|
|
42
|
+
}, this.windowMs);
|
|
43
|
+
this.interval.unref?.();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Method to fetch a client's hit count and reset time.
|
|
47
|
+
*
|
|
48
|
+
* @param key {string} - The identifier for a client.
|
|
49
|
+
*
|
|
50
|
+
* @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
|
|
51
|
+
*
|
|
52
|
+
* @public
|
|
53
|
+
*/
|
|
54
|
+
async get(key) {
|
|
55
|
+
return this.current.get(key) ?? this.previous.get(key);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Method to increment a client's hit counter.
|
|
59
|
+
*
|
|
60
|
+
* @param key {string} - The identifier for a client.
|
|
61
|
+
*
|
|
62
|
+
* @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
|
|
63
|
+
*
|
|
64
|
+
* @public
|
|
65
|
+
*/
|
|
66
|
+
async increment(key) {
|
|
67
|
+
const client = this.getClient(key);
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
if (client.resetTime.getTime() <= now) {
|
|
70
|
+
this.resetClient(client, now);
|
|
71
|
+
}
|
|
72
|
+
client.totalHits++;
|
|
73
|
+
return client;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Method to decrement a client's hit counter.
|
|
77
|
+
*
|
|
78
|
+
* @param key {string} - The identifier for a client.
|
|
79
|
+
*
|
|
80
|
+
* @public
|
|
81
|
+
*/
|
|
82
|
+
async decrement(key) {
|
|
83
|
+
const client = this.getClient(key);
|
|
84
|
+
if (client.totalHits > 0) client.totalHits--;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Method to reset a client's hit counter.
|
|
88
|
+
*
|
|
89
|
+
* @param key {string} - The identifier for a client.
|
|
90
|
+
*
|
|
91
|
+
* @public
|
|
92
|
+
*/
|
|
93
|
+
async resetKey(key) {
|
|
94
|
+
this.current.delete(key);
|
|
95
|
+
this.previous.delete(key);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Method to reset everyone's hit counter.
|
|
99
|
+
*
|
|
100
|
+
* @public
|
|
101
|
+
*/
|
|
102
|
+
async resetAll() {
|
|
103
|
+
this.current.clear();
|
|
104
|
+
this.previous.clear();
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Method to stop the timer (if currently running) and prevent any memory
|
|
108
|
+
* leaks.
|
|
109
|
+
*
|
|
110
|
+
* @public
|
|
111
|
+
*/
|
|
112
|
+
shutdown() {
|
|
113
|
+
clearInterval(this.interval);
|
|
114
|
+
void this.resetAll();
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Recycles a client by setting its hit count to zero, and reset time to
|
|
118
|
+
* `windowMs` milliseconds from now.
|
|
119
|
+
*
|
|
120
|
+
* NOT to be confused with `#resetKey()`, which removes a client from both the
|
|
121
|
+
* `current` and `previous` maps.
|
|
122
|
+
*
|
|
123
|
+
* @param client {Client} - The client to recycle.
|
|
124
|
+
* @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client.
|
|
125
|
+
*
|
|
126
|
+
* @return {Client} - The modified client that was passed in, to allow for chaining.
|
|
127
|
+
*/
|
|
128
|
+
resetClient(client, now = Date.now()) {
|
|
129
|
+
client.totalHits = 0;
|
|
130
|
+
client.resetTime.setTime(now + this.windowMs);
|
|
131
|
+
return client;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Retrieves or creates a client, given a key. Also ensures that the client being
|
|
135
|
+
* returned is in the `current` map.
|
|
136
|
+
*
|
|
137
|
+
* @param key {string} - The key under which the client is (or is to be) stored.
|
|
138
|
+
*
|
|
139
|
+
* @returns {Client} - The requested client.
|
|
140
|
+
*/
|
|
141
|
+
getClient(key) {
|
|
142
|
+
if (this.current.has(key)) return this.current.get(key);
|
|
143
|
+
let client;
|
|
144
|
+
if (this.previous.has(key)) {
|
|
145
|
+
client = this.previous.get(key);
|
|
146
|
+
this.previous.delete(key);
|
|
147
|
+
} else {
|
|
148
|
+
client = { totalHits: 0, resetTime: /* @__PURE__ */ new Date() };
|
|
149
|
+
this.resetClient(client);
|
|
150
|
+
}
|
|
151
|
+
this.current.set(key, client);
|
|
152
|
+
return client;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Move current clients to previous, create a new map for current.
|
|
156
|
+
*
|
|
157
|
+
* This function is called every `windowMs`.
|
|
158
|
+
*/
|
|
159
|
+
clearExpired() {
|
|
160
|
+
this.previous = this.current;
|
|
161
|
+
this.current = /* @__PURE__ */ new Map();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// source/rate-limit.ts
|
|
166
|
+
import { isIPv6 as isIPv62 } from "node:net";
|
|
167
|
+
|
|
1
168
|
// source/headers.ts
|
|
2
169
|
import { Buffer } from "node:buffer";
|
|
3
170
|
import { createHash } from "node:crypto";
|
|
@@ -6,12 +173,12 @@ var SUPPORTED_DRAFT_VERSIONS = [
|
|
|
6
173
|
"draft-7",
|
|
7
174
|
"draft-8"
|
|
8
175
|
];
|
|
9
|
-
var getResetSeconds = (
|
|
10
|
-
let resetSeconds
|
|
176
|
+
var getResetSeconds = (windowMs, resetTime) => {
|
|
177
|
+
let resetSeconds;
|
|
11
178
|
if (resetTime) {
|
|
12
179
|
const deltaSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1e3);
|
|
13
180
|
resetSeconds = Math.max(0, deltaSeconds);
|
|
14
|
-
} else
|
|
181
|
+
} else {
|
|
15
182
|
resetSeconds = Math.ceil(windowMs / 1e3);
|
|
16
183
|
}
|
|
17
184
|
return resetSeconds;
|
|
@@ -37,7 +204,7 @@ var setLegacyHeaders = (response, info) => {
|
|
|
37
204
|
var setDraft6Headers = (response, info, windowMs) => {
|
|
38
205
|
if (response.headersSent) return;
|
|
39
206
|
const windowSeconds = Math.ceil(windowMs / 1e3);
|
|
40
|
-
const resetSeconds = getResetSeconds(info.resetTime);
|
|
207
|
+
const resetSeconds = getResetSeconds(windowMs, info.resetTime);
|
|
41
208
|
response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
|
|
42
209
|
response.setHeader("RateLimit-Limit", info.limit.toString());
|
|
43
210
|
response.setHeader("RateLimit-Remaining", info.remaining.toString());
|
|
@@ -47,7 +214,7 @@ var setDraft6Headers = (response, info, windowMs) => {
|
|
|
47
214
|
var setDraft7Headers = (response, info, windowMs) => {
|
|
48
215
|
if (response.headersSent) return;
|
|
49
216
|
const windowSeconds = Math.ceil(windowMs / 1e3);
|
|
50
|
-
const resetSeconds = getResetSeconds(info.resetTime
|
|
217
|
+
const resetSeconds = getResetSeconds(windowMs, info.resetTime);
|
|
51
218
|
response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
|
|
52
219
|
response.setHeader(
|
|
53
220
|
"RateLimit",
|
|
@@ -57,19 +224,31 @@ var setDraft7Headers = (response, info, windowMs) => {
|
|
|
57
224
|
var setDraft8Headers = (response, info, windowMs, name, key) => {
|
|
58
225
|
if (response.headersSent) return;
|
|
59
226
|
const windowSeconds = Math.ceil(windowMs / 1e3);
|
|
60
|
-
const resetSeconds = getResetSeconds(info.resetTime
|
|
227
|
+
const resetSeconds = getResetSeconds(windowMs, info.resetTime);
|
|
61
228
|
const partitionKey = getPartitionKey(key);
|
|
62
|
-
const policy = `q=${info.limit}; w=${windowSeconds}; pk=:${partitionKey}:`;
|
|
63
229
|
const header = `r=${info.remaining}; t=${resetSeconds}`;
|
|
64
|
-
|
|
230
|
+
const policy = `q=${info.limit}; w=${windowSeconds}; pk=:${partitionKey}:`;
|
|
65
231
|
response.append("RateLimit", `"${name}"; ${header}`);
|
|
232
|
+
response.append("RateLimit-Policy", `"${name}"; ${policy}`);
|
|
66
233
|
};
|
|
67
234
|
var setRetryAfterHeader = (response, info, windowMs) => {
|
|
68
235
|
if (response.headersSent) return;
|
|
69
|
-
const resetSeconds = getResetSeconds(info.resetTime
|
|
236
|
+
const resetSeconds = getResetSeconds(windowMs, info.resetTime);
|
|
70
237
|
response.setHeader("Retry-After", resetSeconds.toString());
|
|
71
238
|
};
|
|
72
239
|
|
|
240
|
+
// source/utils.ts
|
|
241
|
+
var omitUndefinedProperties = (passedOptions) => {
|
|
242
|
+
const omittedOptions = {};
|
|
243
|
+
for (const k of Object.keys(passedOptions)) {
|
|
244
|
+
const key = k;
|
|
245
|
+
if (passedOptions[key] !== void 0) {
|
|
246
|
+
omittedOptions[key] = passedOptions[key];
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return omittedOptions;
|
|
250
|
+
};
|
|
251
|
+
|
|
73
252
|
// source/validations.ts
|
|
74
253
|
import { isIP } from "node:net";
|
|
75
254
|
var ValidationError = class extends Error {
|
|
@@ -93,7 +272,6 @@ var ChangeWarning = class extends ValidationError {
|
|
|
93
272
|
var usedStores = /* @__PURE__ */ new Set();
|
|
94
273
|
var singleCountKeys = /* @__PURE__ */ new WeakMap();
|
|
95
274
|
var validations = {
|
|
96
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
97
275
|
enabled: {
|
|
98
276
|
default: true
|
|
99
277
|
},
|
|
@@ -228,7 +406,7 @@ var validations = {
|
|
|
228
406
|
if (limit === 0) {
|
|
229
407
|
throw new ChangeWarning(
|
|
230
408
|
"WRN_ERL_MAX_ZERO",
|
|
231
|
-
|
|
409
|
+
"Setting limit or max to 0 disables rate limiting in express-rate-limit v6 and older, but will cause all requests to be blocked in v7"
|
|
232
410
|
);
|
|
233
411
|
}
|
|
234
412
|
},
|
|
@@ -260,7 +438,7 @@ var validations = {
|
|
|
260
438
|
if (onLimitReached) {
|
|
261
439
|
throw new ChangeWarning(
|
|
262
440
|
"WRN_ERL_DEPRECATED_ON_LIMIT_REACHED",
|
|
263
|
-
|
|
441
|
+
"The onLimitReached configuration option is deprecated and has been removed in express-rate-limit v7."
|
|
264
442
|
);
|
|
265
443
|
}
|
|
266
444
|
},
|
|
@@ -329,7 +507,8 @@ var validations = {
|
|
|
329
507
|
const { stack } = new Error(
|
|
330
508
|
"express-rate-limit validation check (set options.validate.creationStack=false to disable)"
|
|
331
509
|
);
|
|
332
|
-
if (stack?.includes("Layer.handle [as handle_request]")
|
|
510
|
+
if (stack?.includes("Layer.handle [as handle_request]") || // express v4
|
|
511
|
+
stack?.includes("Layer.handleRequest")) {
|
|
333
512
|
if (!store.localKeys) {
|
|
334
513
|
throw new ValidationError(
|
|
335
514
|
"ERR_ERL_CREATED_IN_REQUEST_HANDLER",
|
|
@@ -338,7 +517,38 @@ var validations = {
|
|
|
338
517
|
}
|
|
339
518
|
throw new ValidationError(
|
|
340
519
|
"ERR_ERL_CREATED_IN_REQUEST_HANDLER",
|
|
341
|
-
|
|
520
|
+
"express-rate-limit instance should be created at app initialization, not when responding to a request."
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
ipv6Subnet(ipv6Subnet) {
|
|
525
|
+
if (ipv6Subnet === false) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (!Number.isInteger(ipv6Subnet) || ipv6Subnet < 32 || ipv6Subnet > 64) {
|
|
529
|
+
throw new ValidationError(
|
|
530
|
+
"ERR_ERL_IPV6_SUBNET",
|
|
531
|
+
`Unexpected ipv6Subnet value: ${ipv6Subnet}. Expected an integer between 32 and 64 (usually 48-64).`
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
},
|
|
535
|
+
ipv6SubnetOrKeyGenerator(options) {
|
|
536
|
+
if (options.ipv6Subnet !== void 0 && options.keyGenerator) {
|
|
537
|
+
throw new ValidationError(
|
|
538
|
+
"ERR_ERL_IPV6SUBNET_OR_KEYGENERATOR",
|
|
539
|
+
`Incompatible options: the 'ipv6Subnet' option is ignored when a custom 'keyGenerator' function is also set.`
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
keyGeneratorIpFallback(keyGenerator) {
|
|
544
|
+
if (!keyGenerator) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
const src = keyGenerator.toString();
|
|
548
|
+
if ((src.includes("req.ip") || src.includes("request.ip")) && !src.includes("ipKeyGenerator")) {
|
|
549
|
+
throw new ValidationError(
|
|
550
|
+
"ERR_ERL_KEY_GEN_IPV6",
|
|
551
|
+
"Custom keyGenerator appears to use request IP without calling the ipKeyGenerator helper function for IPv6 addresses. This could allow IPv6 users to bypass limits."
|
|
342
552
|
);
|
|
343
553
|
}
|
|
344
554
|
}
|
|
@@ -355,9 +565,7 @@ var getValidations = (_enabled) => {
|
|
|
355
565
|
..._enabled
|
|
356
566
|
};
|
|
357
567
|
}
|
|
358
|
-
const wrappedValidations = {
|
|
359
|
-
enabled
|
|
360
|
-
};
|
|
568
|
+
const wrappedValidations = { enabled };
|
|
361
569
|
for (const [name, validation] of Object.entries(validations)) {
|
|
362
570
|
if (typeof validation === "function")
|
|
363
571
|
wrappedValidations[name] = (...args) => {
|
|
@@ -379,161 +587,7 @@ var getValidations = (_enabled) => {
|
|
|
379
587
|
return wrappedValidations;
|
|
380
588
|
};
|
|
381
589
|
|
|
382
|
-
// source/
|
|
383
|
-
var MemoryStore = class {
|
|
384
|
-
constructor() {
|
|
385
|
-
/**
|
|
386
|
-
* These two maps store usage (requests) and reset time by key (for example, IP
|
|
387
|
-
* addresses or API keys).
|
|
388
|
-
*
|
|
389
|
-
* They are split into two to avoid having to iterate through the entire set to
|
|
390
|
-
* determine which ones need reset. Instead, `Client`s are moved from `previous`
|
|
391
|
-
* to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients
|
|
392
|
-
* left in `previous`, i.e., those that have not made any recent requests, are
|
|
393
|
-
* known to be expired and can be deleted in bulk.
|
|
394
|
-
*/
|
|
395
|
-
this.previous = /* @__PURE__ */ new Map();
|
|
396
|
-
this.current = /* @__PURE__ */ new Map();
|
|
397
|
-
/**
|
|
398
|
-
* Confirmation that the keys incremented in once instance of MemoryStore
|
|
399
|
-
* cannot affect other instances.
|
|
400
|
-
*/
|
|
401
|
-
this.localKeys = true;
|
|
402
|
-
}
|
|
403
|
-
/**
|
|
404
|
-
* Method that initializes the store.
|
|
405
|
-
*
|
|
406
|
-
* @param options {Options} - The options used to setup the middleware.
|
|
407
|
-
*/
|
|
408
|
-
init(options) {
|
|
409
|
-
this.windowMs = options.windowMs;
|
|
410
|
-
if (this.interval) clearInterval(this.interval);
|
|
411
|
-
this.interval = setInterval(() => {
|
|
412
|
-
this.clearExpired();
|
|
413
|
-
}, this.windowMs);
|
|
414
|
-
if (this.interval.unref) this.interval.unref();
|
|
415
|
-
}
|
|
416
|
-
/**
|
|
417
|
-
* Method to fetch a client's hit count and reset time.
|
|
418
|
-
*
|
|
419
|
-
* @param key {string} - The identifier for a client.
|
|
420
|
-
*
|
|
421
|
-
* @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
|
|
422
|
-
*
|
|
423
|
-
* @public
|
|
424
|
-
*/
|
|
425
|
-
async get(key) {
|
|
426
|
-
return this.current.get(key) ?? this.previous.get(key);
|
|
427
|
-
}
|
|
428
|
-
/**
|
|
429
|
-
* Method to increment a client's hit counter.
|
|
430
|
-
*
|
|
431
|
-
* @param key {string} - The identifier for a client.
|
|
432
|
-
*
|
|
433
|
-
* @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
|
|
434
|
-
*
|
|
435
|
-
* @public
|
|
436
|
-
*/
|
|
437
|
-
async increment(key) {
|
|
438
|
-
const client = this.getClient(key);
|
|
439
|
-
const now = Date.now();
|
|
440
|
-
if (client.resetTime.getTime() <= now) {
|
|
441
|
-
this.resetClient(client, now);
|
|
442
|
-
}
|
|
443
|
-
client.totalHits++;
|
|
444
|
-
return client;
|
|
445
|
-
}
|
|
446
|
-
/**
|
|
447
|
-
* Method to decrement a client's hit counter.
|
|
448
|
-
*
|
|
449
|
-
* @param key {string} - The identifier for a client.
|
|
450
|
-
*
|
|
451
|
-
* @public
|
|
452
|
-
*/
|
|
453
|
-
async decrement(key) {
|
|
454
|
-
const client = this.getClient(key);
|
|
455
|
-
if (client.totalHits > 0) client.totalHits--;
|
|
456
|
-
}
|
|
457
|
-
/**
|
|
458
|
-
* Method to reset a client's hit counter.
|
|
459
|
-
*
|
|
460
|
-
* @param key {string} - The identifier for a client.
|
|
461
|
-
*
|
|
462
|
-
* @public
|
|
463
|
-
*/
|
|
464
|
-
async resetKey(key) {
|
|
465
|
-
this.current.delete(key);
|
|
466
|
-
this.previous.delete(key);
|
|
467
|
-
}
|
|
468
|
-
/**
|
|
469
|
-
* Method to reset everyone's hit counter.
|
|
470
|
-
*
|
|
471
|
-
* @public
|
|
472
|
-
*/
|
|
473
|
-
async resetAll() {
|
|
474
|
-
this.current.clear();
|
|
475
|
-
this.previous.clear();
|
|
476
|
-
}
|
|
477
|
-
/**
|
|
478
|
-
* Method to stop the timer (if currently running) and prevent any memory
|
|
479
|
-
* leaks.
|
|
480
|
-
*
|
|
481
|
-
* @public
|
|
482
|
-
*/
|
|
483
|
-
shutdown() {
|
|
484
|
-
clearInterval(this.interval);
|
|
485
|
-
void this.resetAll();
|
|
486
|
-
}
|
|
487
|
-
/**
|
|
488
|
-
* Recycles a client by setting its hit count to zero, and reset time to
|
|
489
|
-
* `windowMs` milliseconds from now.
|
|
490
|
-
*
|
|
491
|
-
* NOT to be confused with `#resetKey()`, which removes a client from both the
|
|
492
|
-
* `current` and `previous` maps.
|
|
493
|
-
*
|
|
494
|
-
* @param client {Client} - The client to recycle.
|
|
495
|
-
* @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client.
|
|
496
|
-
*
|
|
497
|
-
* @return {Client} - The modified client that was passed in, to allow for chaining.
|
|
498
|
-
*/
|
|
499
|
-
resetClient(client, now = Date.now()) {
|
|
500
|
-
client.totalHits = 0;
|
|
501
|
-
client.resetTime.setTime(now + this.windowMs);
|
|
502
|
-
return client;
|
|
503
|
-
}
|
|
504
|
-
/**
|
|
505
|
-
* Retrieves or creates a client, given a key. Also ensures that the client being
|
|
506
|
-
* returned is in the `current` map.
|
|
507
|
-
*
|
|
508
|
-
* @param key {string} - The key under which the client is (or is to be) stored.
|
|
509
|
-
*
|
|
510
|
-
* @returns {Client} - The requested client.
|
|
511
|
-
*/
|
|
512
|
-
getClient(key) {
|
|
513
|
-
if (this.current.has(key)) return this.current.get(key);
|
|
514
|
-
let client;
|
|
515
|
-
if (this.previous.has(key)) {
|
|
516
|
-
client = this.previous.get(key);
|
|
517
|
-
this.previous.delete(key);
|
|
518
|
-
} else {
|
|
519
|
-
client = { totalHits: 0, resetTime: /* @__PURE__ */ new Date() };
|
|
520
|
-
this.resetClient(client);
|
|
521
|
-
}
|
|
522
|
-
this.current.set(key, client);
|
|
523
|
-
return client;
|
|
524
|
-
}
|
|
525
|
-
/**
|
|
526
|
-
* Move current clients to previous, create a new map for current.
|
|
527
|
-
*
|
|
528
|
-
* This function is called every `windowMs`.
|
|
529
|
-
*/
|
|
530
|
-
clearExpired() {
|
|
531
|
-
this.previous = this.current;
|
|
532
|
-
this.current = /* @__PURE__ */ new Map();
|
|
533
|
-
}
|
|
534
|
-
};
|
|
535
|
-
|
|
536
|
-
// source/lib.ts
|
|
590
|
+
// source/rate-limit.ts
|
|
537
591
|
var isLegacyStore = (store) => (
|
|
538
592
|
// Check that `incr` exists but `increment` does not - store authors might want
|
|
539
593
|
// to keep both around for backwards compatibility.
|
|
@@ -577,18 +631,8 @@ var getOptionsFromConfig = (config) => {
|
|
|
577
631
|
validate: validations2.enabled
|
|
578
632
|
};
|
|
579
633
|
};
|
|
580
|
-
var omitUndefinedOptions = (passedOptions) => {
|
|
581
|
-
const omittedOptions = {};
|
|
582
|
-
for (const k of Object.keys(passedOptions)) {
|
|
583
|
-
const key = k;
|
|
584
|
-
if (passedOptions[key] !== void 0) {
|
|
585
|
-
omittedOptions[key] = passedOptions[key];
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
return omittedOptions;
|
|
589
|
-
};
|
|
590
634
|
var parseOptions = (passedOptions) => {
|
|
591
|
-
const notUndefinedOptions =
|
|
635
|
+
const notUndefinedOptions = omitUndefinedProperties(passedOptions);
|
|
592
636
|
const validations2 = getValidations(notUndefinedOptions?.validate ?? true);
|
|
593
637
|
validations2.validationsConfig();
|
|
594
638
|
validations2.draftPolliHeaders(
|
|
@@ -596,6 +640,11 @@ var parseOptions = (passedOptions) => {
|
|
|
596
640
|
notUndefinedOptions.draft_polli_ratelimit_headers
|
|
597
641
|
);
|
|
598
642
|
validations2.onLimitReached(notUndefinedOptions.onLimitReached);
|
|
643
|
+
if (notUndefinedOptions.ipv6Subnet !== void 0 && typeof notUndefinedOptions.ipv6Subnet !== "function") {
|
|
644
|
+
validations2.ipv6Subnet(notUndefinedOptions.ipv6Subnet);
|
|
645
|
+
}
|
|
646
|
+
validations2.keyGeneratorIpFallback(notUndefinedOptions.keyGenerator);
|
|
647
|
+
validations2.ipv6SubnetOrKeyGenerator(notUndefinedOptions);
|
|
599
648
|
let standardHeaders = notUndefinedOptions.standardHeaders ?? false;
|
|
600
649
|
if (standardHeaders === true) standardHeaders = "draft-6";
|
|
601
650
|
const config = {
|
|
@@ -624,21 +673,27 @@ var parseOptions = (passedOptions) => {
|
|
|
624
673
|
skipSuccessfulRequests: false,
|
|
625
674
|
requestWasSuccessful: (_request, response) => response.statusCode < 400,
|
|
626
675
|
skip: (_request, _response) => false,
|
|
627
|
-
keyGenerator(request,
|
|
676
|
+
async keyGenerator(request, response) {
|
|
628
677
|
validations2.ip(request.ip);
|
|
629
678
|
validations2.trustProxy(request);
|
|
630
679
|
validations2.xForwardedForHeader(request);
|
|
631
|
-
|
|
680
|
+
const ip = request.ip;
|
|
681
|
+
let subnet = 56;
|
|
682
|
+
if (isIPv62(ip)) {
|
|
683
|
+
subnet = typeof config.ipv6Subnet === "function" ? await config.ipv6Subnet(request, response) : config.ipv6Subnet;
|
|
684
|
+
if (typeof config.ipv6Subnet === "function")
|
|
685
|
+
validations2.ipv6Subnet(subnet);
|
|
686
|
+
}
|
|
687
|
+
return ipKeyGenerator(ip, subnet);
|
|
632
688
|
},
|
|
689
|
+
ipv6Subnet: 56,
|
|
633
690
|
async handler(request, response, _next, _optionsUsed) {
|
|
634
691
|
response.status(config.statusCode);
|
|
635
692
|
const message = typeof config.message === "function" ? await config.message(
|
|
636
693
|
request,
|
|
637
694
|
response
|
|
638
695
|
) : config.message;
|
|
639
|
-
if (!response.writableEnded)
|
|
640
|
-
response.send(message);
|
|
641
|
-
}
|
|
696
|
+
if (!response.writableEnded) response.send(message);
|
|
642
697
|
},
|
|
643
698
|
passOnStoreError: false,
|
|
644
699
|
// Allow the default options to be overridden by the passed options.
|
|
@@ -706,7 +761,8 @@ var rateLimit = (passedOptions) => {
|
|
|
706
761
|
limit,
|
|
707
762
|
used: totalHits,
|
|
708
763
|
remaining: Math.max(limit - totalHits, 0),
|
|
709
|
-
resetTime
|
|
764
|
+
resetTime,
|
|
765
|
+
key
|
|
710
766
|
};
|
|
711
767
|
Object.defineProperty(info, "current", {
|
|
712
768
|
configurable: false,
|
|
@@ -786,9 +842,10 @@ var rateLimit = (passedOptions) => {
|
|
|
786
842
|
middleware.getKey = typeof config.store.get === "function" ? config.store.get.bind(config.store) : getThrowFn;
|
|
787
843
|
return middleware;
|
|
788
844
|
};
|
|
789
|
-
var
|
|
845
|
+
var rate_limit_default = rateLimit;
|
|
790
846
|
export {
|
|
791
847
|
MemoryStore,
|
|
792
|
-
|
|
793
|
-
|
|
848
|
+
rate_limit_default as default,
|
|
849
|
+
ipKeyGenerator,
|
|
850
|
+
rate_limit_default as rateLimit
|
|
794
851
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "express-rate-limit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.1",
|
|
4
4
|
"description": "Basic IP rate-limiting middleware for Express. Use to limit repeated requests to public APIs and/or endpoints such as password reset.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Nathan Friedly",
|
|
@@ -56,78 +56,57 @@
|
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"clean": "del-cli dist/ coverage/ *.log *.tmp *.bak *.tgz",
|
|
59
|
-
"build:cjs": "esbuild --platform=node --bundle --target=es2022 --format=cjs --outfile=dist/index.cjs --footer:js=\"module.exports =
|
|
60
|
-
"build:esm": "esbuild --platform=node --bundle --target=es2022 --format=esm --outfile=dist/index.mjs source/index.ts",
|
|
59
|
+
"build:cjs": "esbuild --packages=external --platform=node --bundle --target=es2022 --format=cjs --outfile=dist/index.cjs --footer:js=\"module.exports = Object.assign(rateLimit, module.exports);\" source/index.ts",
|
|
60
|
+
"build:esm": "esbuild --packages=external --platform=node --bundle --target=es2022 --format=esm --outfile=dist/index.mjs source/index.ts",
|
|
61
61
|
"build:types": "dts-bundle-generator --out-file=dist/index.d.ts source/index.ts && cp dist/index.d.ts dist/index.d.cts && cp dist/index.d.ts dist/index.d.mts",
|
|
62
62
|
"compile": "run-s clean build:*",
|
|
63
63
|
"docs": "cd docs && mintlify dev",
|
|
64
|
-
"lint:code": "
|
|
65
|
-
"lint:
|
|
64
|
+
"lint:code": "biome check",
|
|
65
|
+
"lint:docs": "prettier --check docs/ *.md",
|
|
66
66
|
"lint": "run-s lint:*",
|
|
67
|
-
"format:code": "
|
|
68
|
-
"format:
|
|
67
|
+
"format:code": "biome check --write",
|
|
68
|
+
"format:docs": "prettier --write docs/ *.md",
|
|
69
69
|
"format": "run-s format:*",
|
|
70
70
|
"test:lib": "jest",
|
|
71
71
|
"test:ext": "cd test/external/ && bash run-all-tests",
|
|
72
72
|
"test": "run-s lint test:lib",
|
|
73
73
|
"pre-commit": "lint-staged",
|
|
74
|
-
"prepare": "run-s compile && husky
|
|
74
|
+
"prepare": "run-s compile && husky"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
77
|
"express": ">= 4.11"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
|
+
"@biomejs/biome": "2.1.1",
|
|
80
81
|
"@express-rate-limit/prettier": "1.1.1",
|
|
81
82
|
"@express-rate-limit/tsconfig": "1.0.2",
|
|
82
|
-
"@jest/globals": "
|
|
83
|
-
"@types/express": "
|
|
84
|
-
"@types/jest": "
|
|
85
|
-
"@types/node": "
|
|
86
|
-
"@types/supertest": "
|
|
87
|
-
"del-cli": "
|
|
83
|
+
"@jest/globals": "30.0.4",
|
|
84
|
+
"@types/express": "5.0.3",
|
|
85
|
+
"@types/jest": "30.0.0",
|
|
86
|
+
"@types/node": "24.0.14",
|
|
87
|
+
"@types/supertest": "6.0.3",
|
|
88
|
+
"del-cli": "6.0.0",
|
|
88
89
|
"dts-bundle-generator": "8.0.1",
|
|
89
|
-
"esbuild": "0.25.
|
|
90
|
-
"express": "
|
|
91
|
-
"husky": "
|
|
92
|
-
"jest": "
|
|
93
|
-
"lint-staged": "
|
|
94
|
-
"mintlify": "4.
|
|
90
|
+
"esbuild": "0.25.6",
|
|
91
|
+
"express": "5.1.0",
|
|
92
|
+
"husky": "9.1.7",
|
|
93
|
+
"jest": "30.0.4",
|
|
94
|
+
"lint-staged": "16.1.2",
|
|
95
|
+
"mintlify": "4.2.15",
|
|
95
96
|
"npm-run-all": "4.1.5",
|
|
97
|
+
"prettier": "3.6.2",
|
|
96
98
|
"ratelimit-header-parser": "0.1.0",
|
|
97
|
-
"supertest": "
|
|
98
|
-
"ts-jest": "29.
|
|
99
|
-
"ts-node": "10.9.
|
|
100
|
-
"typescript": "5.
|
|
101
|
-
"xo": "0.56.0"
|
|
102
|
-
},
|
|
103
|
-
"xo": {
|
|
104
|
-
"prettier": true,
|
|
105
|
-
"rules": {
|
|
106
|
-
"@typescript-eslint/no-empty-function": 0,
|
|
107
|
-
"@typescript-eslint/no-dynamic-delete": 0,
|
|
108
|
-
"@typescript-eslint/no-confusing-void-expression": 0,
|
|
109
|
-
"@typescript-eslint/consistent-indexed-object-style": [
|
|
110
|
-
"error",
|
|
111
|
-
"index-signature"
|
|
112
|
-
],
|
|
113
|
-
"n/no-unsupported-features/es-syntax": 0
|
|
114
|
-
},
|
|
115
|
-
"overrides": [
|
|
116
|
-
{
|
|
117
|
-
"files": "test/library/*.ts",
|
|
118
|
-
"rules": {
|
|
119
|
-
"@typescript-eslint/no-unsafe-argument": 0,
|
|
120
|
-
"@typescript-eslint/no-unsafe-assignment": 0
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
],
|
|
124
|
-
"ignore": [
|
|
125
|
-
"test/external"
|
|
126
|
-
]
|
|
99
|
+
"supertest": "7.1.3",
|
|
100
|
+
"ts-jest": "29.4.0",
|
|
101
|
+
"ts-node": "10.9.2",
|
|
102
|
+
"typescript": "5.8.3"
|
|
127
103
|
},
|
|
128
104
|
"prettier": "@express-rate-limit/prettier",
|
|
129
105
|
"lint-staged": {
|
|
130
|
-
"{
|
|
131
|
-
"
|
|
106
|
+
"*.{js,ts,json}": "biome check --write",
|
|
107
|
+
"*.{md,yaml}": "prettier --write"
|
|
108
|
+
},
|
|
109
|
+
"dependencies": {
|
|
110
|
+
"ip-address": "10.0.1"
|
|
132
111
|
}
|
|
133
112
|
}
|