express-rate-limit 7.5.0 → 8.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/dist/index.cjs +292 -236
- package/dist/index.d.cts +56 -16
- package/dist/index.d.mts +56 -16
- package/dist/index.d.ts +56 -16
- package/dist/index.mjs +277 -232
- package/package.json +34 -54
- package/readme.md +5 -1
package/dist/index.mjs
CHANGED
|
@@ -1,13 +1,187 @@
|
|
|
1
|
+
// source/ip-key-generator.ts
|
|
2
|
+
import { isIPv6 } from "node:net";
|
|
3
|
+
import iptools from "ip";
|
|
4
|
+
function ipKeyGenerator(ip, ipv6Subnet = 56) {
|
|
5
|
+
if (ipv6Subnet && isIPv6(ip)) {
|
|
6
|
+
return `${iptools.mask(
|
|
7
|
+
ip,
|
|
8
|
+
iptools.fromPrefixLen(ipv6Subnet)
|
|
9
|
+
)}/${ipv6Subnet}`;
|
|
10
|
+
}
|
|
11
|
+
return ip;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// source/memory-store.ts
|
|
15
|
+
var MemoryStore = class {
|
|
16
|
+
constructor() {
|
|
17
|
+
/**
|
|
18
|
+
* These two maps store usage (requests) and reset time by key (for example, IP
|
|
19
|
+
* addresses or API keys).
|
|
20
|
+
*
|
|
21
|
+
* They are split into two to avoid having to iterate through the entire set to
|
|
22
|
+
* determine which ones need reset. Instead, `Client`s are moved from `previous`
|
|
23
|
+
* to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients
|
|
24
|
+
* left in `previous`, i.e., those that have not made any recent requests, are
|
|
25
|
+
* known to be expired and can be deleted in bulk.
|
|
26
|
+
*/
|
|
27
|
+
this.previous = /* @__PURE__ */ new Map();
|
|
28
|
+
this.current = /* @__PURE__ */ new Map();
|
|
29
|
+
/**
|
|
30
|
+
* Confirmation that the keys incremented in once instance of MemoryStore
|
|
31
|
+
* cannot affect other instances.
|
|
32
|
+
*/
|
|
33
|
+
this.localKeys = true;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Method that initializes the store.
|
|
37
|
+
*
|
|
38
|
+
* @param options {Options} - The options used to setup the middleware.
|
|
39
|
+
*/
|
|
40
|
+
init(options) {
|
|
41
|
+
this.windowMs = options.windowMs;
|
|
42
|
+
if (this.interval) clearInterval(this.interval);
|
|
43
|
+
this.interval = setInterval(() => {
|
|
44
|
+
this.clearExpired();
|
|
45
|
+
}, this.windowMs);
|
|
46
|
+
this.interval.unref?.();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Method to fetch a client's hit count and reset time.
|
|
50
|
+
*
|
|
51
|
+
* @param key {string} - The identifier for a client.
|
|
52
|
+
*
|
|
53
|
+
* @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
|
|
54
|
+
*
|
|
55
|
+
* @public
|
|
56
|
+
*/
|
|
57
|
+
async get(key) {
|
|
58
|
+
return this.current.get(key) ?? this.previous.get(key);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Method to increment a client's hit counter.
|
|
62
|
+
*
|
|
63
|
+
* @param key {string} - The identifier for a client.
|
|
64
|
+
*
|
|
65
|
+
* @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
|
|
66
|
+
*
|
|
67
|
+
* @public
|
|
68
|
+
*/
|
|
69
|
+
async increment(key) {
|
|
70
|
+
const client = this.getClient(key);
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
if (client.resetTime.getTime() <= now) {
|
|
73
|
+
this.resetClient(client, now);
|
|
74
|
+
}
|
|
75
|
+
client.totalHits++;
|
|
76
|
+
return client;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Method to decrement a client's hit counter.
|
|
80
|
+
*
|
|
81
|
+
* @param key {string} - The identifier for a client.
|
|
82
|
+
*
|
|
83
|
+
* @public
|
|
84
|
+
*/
|
|
85
|
+
async decrement(key) {
|
|
86
|
+
const client = this.getClient(key);
|
|
87
|
+
if (client.totalHits > 0) client.totalHits--;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Method to reset a client's hit counter.
|
|
91
|
+
*
|
|
92
|
+
* @param key {string} - The identifier for a client.
|
|
93
|
+
*
|
|
94
|
+
* @public
|
|
95
|
+
*/
|
|
96
|
+
async resetKey(key) {
|
|
97
|
+
this.current.delete(key);
|
|
98
|
+
this.previous.delete(key);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Method to reset everyone's hit counter.
|
|
102
|
+
*
|
|
103
|
+
* @public
|
|
104
|
+
*/
|
|
105
|
+
async resetAll() {
|
|
106
|
+
this.current.clear();
|
|
107
|
+
this.previous.clear();
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Method to stop the timer (if currently running) and prevent any memory
|
|
111
|
+
* leaks.
|
|
112
|
+
*
|
|
113
|
+
* @public
|
|
114
|
+
*/
|
|
115
|
+
shutdown() {
|
|
116
|
+
clearInterval(this.interval);
|
|
117
|
+
void this.resetAll();
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Recycles a client by setting its hit count to zero, and reset time to
|
|
121
|
+
* `windowMs` milliseconds from now.
|
|
122
|
+
*
|
|
123
|
+
* NOT to be confused with `#resetKey()`, which removes a client from both the
|
|
124
|
+
* `current` and `previous` maps.
|
|
125
|
+
*
|
|
126
|
+
* @param client {Client} - The client to recycle.
|
|
127
|
+
* @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client.
|
|
128
|
+
*
|
|
129
|
+
* @return {Client} - The modified client that was passed in, to allow for chaining.
|
|
130
|
+
*/
|
|
131
|
+
resetClient(client, now = Date.now()) {
|
|
132
|
+
client.totalHits = 0;
|
|
133
|
+
client.resetTime.setTime(now + this.windowMs);
|
|
134
|
+
return client;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Retrieves or creates a client, given a key. Also ensures that the client being
|
|
138
|
+
* returned is in the `current` map.
|
|
139
|
+
*
|
|
140
|
+
* @param key {string} - The key under which the client is (or is to be) stored.
|
|
141
|
+
*
|
|
142
|
+
* @returns {Client} - The requested client.
|
|
143
|
+
*/
|
|
144
|
+
getClient(key) {
|
|
145
|
+
if (this.current.has(key)) return this.current.get(key);
|
|
146
|
+
let client;
|
|
147
|
+
if (this.previous.has(key)) {
|
|
148
|
+
client = this.previous.get(key);
|
|
149
|
+
this.previous.delete(key);
|
|
150
|
+
} else {
|
|
151
|
+
client = { totalHits: 0, resetTime: /* @__PURE__ */ new Date() };
|
|
152
|
+
this.resetClient(client);
|
|
153
|
+
}
|
|
154
|
+
this.current.set(key, client);
|
|
155
|
+
return client;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Move current clients to previous, create a new map for current.
|
|
159
|
+
*
|
|
160
|
+
* This function is called every `windowMs`.
|
|
161
|
+
*/
|
|
162
|
+
clearExpired() {
|
|
163
|
+
this.previous = this.current;
|
|
164
|
+
this.current = /* @__PURE__ */ new Map();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// source/rate-limit.ts
|
|
169
|
+
import { isIPv6 as isIPv62 } from "node:net";
|
|
170
|
+
|
|
1
171
|
// source/headers.ts
|
|
2
|
-
import { Buffer } from "buffer";
|
|
3
|
-
import { createHash } from "crypto";
|
|
4
|
-
var SUPPORTED_DRAFT_VERSIONS = [
|
|
5
|
-
|
|
6
|
-
|
|
172
|
+
import { Buffer } from "node:buffer";
|
|
173
|
+
import { createHash } from "node:crypto";
|
|
174
|
+
var SUPPORTED_DRAFT_VERSIONS = [
|
|
175
|
+
"draft-6",
|
|
176
|
+
"draft-7",
|
|
177
|
+
"draft-8"
|
|
178
|
+
];
|
|
179
|
+
var getResetSeconds = (windowMs, resetTime) => {
|
|
180
|
+
let resetSeconds;
|
|
7
181
|
if (resetTime) {
|
|
8
182
|
const deltaSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1e3);
|
|
9
183
|
resetSeconds = Math.max(0, deltaSeconds);
|
|
10
|
-
} else
|
|
184
|
+
} else {
|
|
11
185
|
resetSeconds = Math.ceil(windowMs / 1e3);
|
|
12
186
|
}
|
|
13
187
|
return resetSeconds;
|
|
@@ -19,8 +193,7 @@ var getPartitionKey = (key) => {
|
|
|
19
193
|
return Buffer.from(partitionKey).toString("base64");
|
|
20
194
|
};
|
|
21
195
|
var setLegacyHeaders = (response, info) => {
|
|
22
|
-
if (response.headersSent)
|
|
23
|
-
return;
|
|
196
|
+
if (response.headersSent) return;
|
|
24
197
|
response.setHeader("X-RateLimit-Limit", info.limit.toString());
|
|
25
198
|
response.setHeader("X-RateLimit-Remaining", info.remaining.toString());
|
|
26
199
|
if (info.resetTime instanceof Date) {
|
|
@@ -32,10 +205,9 @@ var setLegacyHeaders = (response, info) => {
|
|
|
32
205
|
}
|
|
33
206
|
};
|
|
34
207
|
var setDraft6Headers = (response, info, windowMs) => {
|
|
35
|
-
if (response.headersSent)
|
|
36
|
-
return;
|
|
208
|
+
if (response.headersSent) return;
|
|
37
209
|
const windowSeconds = Math.ceil(windowMs / 1e3);
|
|
38
|
-
const resetSeconds = getResetSeconds(info.resetTime);
|
|
210
|
+
const resetSeconds = getResetSeconds(windowMs, info.resetTime);
|
|
39
211
|
response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
|
|
40
212
|
response.setHeader("RateLimit-Limit", info.limit.toString());
|
|
41
213
|
response.setHeader("RateLimit-Remaining", info.remaining.toString());
|
|
@@ -43,10 +215,9 @@ var setDraft6Headers = (response, info, windowMs) => {
|
|
|
43
215
|
response.setHeader("RateLimit-Reset", resetSeconds.toString());
|
|
44
216
|
};
|
|
45
217
|
var setDraft7Headers = (response, info, windowMs) => {
|
|
46
|
-
if (response.headersSent)
|
|
47
|
-
return;
|
|
218
|
+
if (response.headersSent) return;
|
|
48
219
|
const windowSeconds = Math.ceil(windowMs / 1e3);
|
|
49
|
-
const resetSeconds = getResetSeconds(info.resetTime
|
|
220
|
+
const resetSeconds = getResetSeconds(windowMs, info.resetTime);
|
|
50
221
|
response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
|
|
51
222
|
response.setHeader(
|
|
52
223
|
"RateLimit",
|
|
@@ -54,25 +225,35 @@ var setDraft7Headers = (response, info, windowMs) => {
|
|
|
54
225
|
);
|
|
55
226
|
};
|
|
56
227
|
var setDraft8Headers = (response, info, windowMs, name, key) => {
|
|
57
|
-
if (response.headersSent)
|
|
58
|
-
return;
|
|
228
|
+
if (response.headersSent) return;
|
|
59
229
|
const windowSeconds = Math.ceil(windowMs / 1e3);
|
|
60
|
-
const resetSeconds = getResetSeconds(info.resetTime
|
|
230
|
+
const resetSeconds = getResetSeconds(windowMs, info.resetTime);
|
|
61
231
|
const partitionKey = getPartitionKey(key);
|
|
62
|
-
const policy = `q=${info.limit}; w=${windowSeconds}; pk=:${partitionKey}:`;
|
|
63
232
|
const header = `r=${info.remaining}; t=${resetSeconds}`;
|
|
64
|
-
|
|
233
|
+
const policy = `q=${info.limit}; w=${windowSeconds}; pk=:${partitionKey}:`;
|
|
65
234
|
response.append("RateLimit", `"${name}"; ${header}`);
|
|
235
|
+
response.append("RateLimit-Policy", `"${name}"; ${policy}`);
|
|
66
236
|
};
|
|
67
237
|
var setRetryAfterHeader = (response, info, windowMs) => {
|
|
68
|
-
if (response.headersSent)
|
|
69
|
-
|
|
70
|
-
const resetSeconds = getResetSeconds(info.resetTime, windowMs);
|
|
238
|
+
if (response.headersSent) return;
|
|
239
|
+
const resetSeconds = getResetSeconds(windowMs, info.resetTime);
|
|
71
240
|
response.setHeader("Retry-After", resetSeconds.toString());
|
|
72
241
|
};
|
|
73
242
|
|
|
243
|
+
// source/utils.ts
|
|
244
|
+
var omitUndefinedProperties = (passedOptions) => {
|
|
245
|
+
const omittedOptions = {};
|
|
246
|
+
for (const k of Object.keys(passedOptions)) {
|
|
247
|
+
const key = k;
|
|
248
|
+
if (passedOptions[key] !== void 0) {
|
|
249
|
+
omittedOptions[key] = passedOptions[key];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return omittedOptions;
|
|
253
|
+
};
|
|
254
|
+
|
|
74
255
|
// source/validations.ts
|
|
75
|
-
import { isIP } from "net";
|
|
256
|
+
import { isIP } from "node:net";
|
|
76
257
|
var ValidationError = class extends Error {
|
|
77
258
|
/**
|
|
78
259
|
* The code must be a string, in snake case and all capital, that starts with
|
|
@@ -94,14 +275,12 @@ var ChangeWarning = class extends ValidationError {
|
|
|
94
275
|
var usedStores = /* @__PURE__ */ new Set();
|
|
95
276
|
var singleCountKeys = /* @__PURE__ */ new WeakMap();
|
|
96
277
|
var validations = {
|
|
97
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
98
278
|
enabled: {
|
|
99
279
|
default: true
|
|
100
280
|
},
|
|
101
281
|
// Should be EnabledValidations type, but that's a circular reference
|
|
102
282
|
disable() {
|
|
103
|
-
for (const k of Object.keys(this.enabled))
|
|
104
|
-
this.enabled[k] = false;
|
|
283
|
+
for (const k of Object.keys(this.enabled)) this.enabled[k] = false;
|
|
105
284
|
},
|
|
106
285
|
/**
|
|
107
286
|
* Checks whether the IP address is valid, and that it does not have a port
|
|
@@ -274,7 +453,8 @@ var validations = {
|
|
|
274
453
|
* @returns {void}
|
|
275
454
|
*/
|
|
276
455
|
headersDraftVersion(version) {
|
|
277
|
-
if (typeof version !== "string" ||
|
|
456
|
+
if (typeof version !== "string" || // @ts-expect-error This is fine. If version is not in the array, it will just return false.
|
|
457
|
+
!SUPPORTED_DRAFT_VERSIONS.includes(version)) {
|
|
278
458
|
const versionString = SUPPORTED_DRAFT_VERSIONS.join(", ");
|
|
279
459
|
throw new ValidationError(
|
|
280
460
|
"ERR_ERL_HEADERS_UNSUPPORTED_DRAFT_VERSION",
|
|
@@ -330,7 +510,8 @@ var validations = {
|
|
|
330
510
|
const { stack } = new Error(
|
|
331
511
|
"express-rate-limit validation check (set options.validate.creationStack=false to disable)"
|
|
332
512
|
);
|
|
333
|
-
if (stack?.includes("Layer.handle [as handle_request]")
|
|
513
|
+
if (stack?.includes("Layer.handle [as handle_request]") || // express v4
|
|
514
|
+
stack?.includes("Layer.handleRequest")) {
|
|
334
515
|
if (!store.localKeys) {
|
|
335
516
|
throw new ValidationError(
|
|
336
517
|
"ERR_ERL_CREATED_IN_REQUEST_HANDLER",
|
|
@@ -342,6 +523,37 @@ var validations = {
|
|
|
342
523
|
`express-rate-limit instance should be created at app initialization, not when responding to a request.`
|
|
343
524
|
);
|
|
344
525
|
}
|
|
526
|
+
},
|
|
527
|
+
ipv6Subnet(ipv6Subnet) {
|
|
528
|
+
if (ipv6Subnet === false) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (!Number.isInteger(ipv6Subnet) || ipv6Subnet < 32 || ipv6Subnet > 64) {
|
|
532
|
+
throw new ValidationError(
|
|
533
|
+
"ERR_ERL_IPV6_SUBNET",
|
|
534
|
+
`Unexpected ipv6Subnet value: ${ipv6Subnet}. Expected an integer between 32 and 64 (usually 48-64).`
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
ipv6SubnetOrKeyGenerator(options) {
|
|
539
|
+
if (options.ipv6Subnet !== void 0 && options.keyGenerator) {
|
|
540
|
+
throw new ValidationError(
|
|
541
|
+
"ERR_ERL_IPV6SUBNET_OR_KEYGENERATOR",
|
|
542
|
+
`Incompatible options: the 'ipv6Subnet' option is ignored when a custom 'keyGenerator' function is also set.`
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
keyGeneratorIpFallback(keyGenerator) {
|
|
547
|
+
if (!keyGenerator) {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const src = keyGenerator.toString();
|
|
551
|
+
if ((src.includes("req.ip") || src.includes("request.ip")) && !src.includes("ipKeyGenerator")) {
|
|
552
|
+
throw new ValidationError(
|
|
553
|
+
"ERR_ERL_KEY_GEN_IPV6",
|
|
554
|
+
`Custom keyGenerator appears to use request IP without calling the ipKeyGenerator helper function for IPv6 addresses. This could allow IPv6 users to bypass limits.`
|
|
555
|
+
);
|
|
556
|
+
}
|
|
345
557
|
}
|
|
346
558
|
};
|
|
347
559
|
var getValidations = (_enabled) => {
|
|
@@ -356,9 +568,7 @@ var getValidations = (_enabled) => {
|
|
|
356
568
|
..._enabled
|
|
357
569
|
};
|
|
358
570
|
}
|
|
359
|
-
const wrappedValidations = {
|
|
360
|
-
enabled
|
|
361
|
-
};
|
|
571
|
+
const wrappedValidations = { enabled };
|
|
362
572
|
for (const [name, validation] of Object.entries(validations)) {
|
|
363
573
|
if (typeof validation === "function")
|
|
364
574
|
wrappedValidations[name] = (...args) => {
|
|
@@ -372,175 +582,15 @@ var getValidations = (_enabled) => {
|
|
|
372
582
|
args
|
|
373
583
|
);
|
|
374
584
|
} catch (error) {
|
|
375
|
-
if (error instanceof ChangeWarning)
|
|
376
|
-
|
|
377
|
-
else
|
|
378
|
-
console.error(error);
|
|
585
|
+
if (error instanceof ChangeWarning) console.warn(error);
|
|
586
|
+
else console.error(error);
|
|
379
587
|
}
|
|
380
588
|
};
|
|
381
589
|
}
|
|
382
590
|
return wrappedValidations;
|
|
383
591
|
};
|
|
384
592
|
|
|
385
|
-
// source/
|
|
386
|
-
var MemoryStore = class {
|
|
387
|
-
constructor() {
|
|
388
|
-
/**
|
|
389
|
-
* These two maps store usage (requests) and reset time by key (for example, IP
|
|
390
|
-
* addresses or API keys).
|
|
391
|
-
*
|
|
392
|
-
* They are split into two to avoid having to iterate through the entire set to
|
|
393
|
-
* determine which ones need reset. Instead, `Client`s are moved from `previous`
|
|
394
|
-
* to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients
|
|
395
|
-
* left in `previous`, i.e., those that have not made any recent requests, are
|
|
396
|
-
* known to be expired and can be deleted in bulk.
|
|
397
|
-
*/
|
|
398
|
-
this.previous = /* @__PURE__ */ new Map();
|
|
399
|
-
this.current = /* @__PURE__ */ new Map();
|
|
400
|
-
/**
|
|
401
|
-
* Confirmation that the keys incremented in once instance of MemoryStore
|
|
402
|
-
* cannot affect other instances.
|
|
403
|
-
*/
|
|
404
|
-
this.localKeys = true;
|
|
405
|
-
}
|
|
406
|
-
/**
|
|
407
|
-
* Method that initializes the store.
|
|
408
|
-
*
|
|
409
|
-
* @param options {Options} - The options used to setup the middleware.
|
|
410
|
-
*/
|
|
411
|
-
init(options) {
|
|
412
|
-
this.windowMs = options.windowMs;
|
|
413
|
-
if (this.interval)
|
|
414
|
-
clearInterval(this.interval);
|
|
415
|
-
this.interval = setInterval(() => {
|
|
416
|
-
this.clearExpired();
|
|
417
|
-
}, this.windowMs);
|
|
418
|
-
if (this.interval.unref)
|
|
419
|
-
this.interval.unref();
|
|
420
|
-
}
|
|
421
|
-
/**
|
|
422
|
-
* Method to fetch a client's hit count and reset time.
|
|
423
|
-
*
|
|
424
|
-
* @param key {string} - The identifier for a client.
|
|
425
|
-
*
|
|
426
|
-
* @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
|
|
427
|
-
*
|
|
428
|
-
* @public
|
|
429
|
-
*/
|
|
430
|
-
async get(key) {
|
|
431
|
-
return this.current.get(key) ?? this.previous.get(key);
|
|
432
|
-
}
|
|
433
|
-
/**
|
|
434
|
-
* Method to increment a client's hit counter.
|
|
435
|
-
*
|
|
436
|
-
* @param key {string} - The identifier for a client.
|
|
437
|
-
*
|
|
438
|
-
* @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
|
|
439
|
-
*
|
|
440
|
-
* @public
|
|
441
|
-
*/
|
|
442
|
-
async increment(key) {
|
|
443
|
-
const client = this.getClient(key);
|
|
444
|
-
const now = Date.now();
|
|
445
|
-
if (client.resetTime.getTime() <= now) {
|
|
446
|
-
this.resetClient(client, now);
|
|
447
|
-
}
|
|
448
|
-
client.totalHits++;
|
|
449
|
-
return client;
|
|
450
|
-
}
|
|
451
|
-
/**
|
|
452
|
-
* Method to decrement a client's hit counter.
|
|
453
|
-
*
|
|
454
|
-
* @param key {string} - The identifier for a client.
|
|
455
|
-
*
|
|
456
|
-
* @public
|
|
457
|
-
*/
|
|
458
|
-
async decrement(key) {
|
|
459
|
-
const client = this.getClient(key);
|
|
460
|
-
if (client.totalHits > 0)
|
|
461
|
-
client.totalHits--;
|
|
462
|
-
}
|
|
463
|
-
/**
|
|
464
|
-
* Method to reset a client's hit counter.
|
|
465
|
-
*
|
|
466
|
-
* @param key {string} - The identifier for a client.
|
|
467
|
-
*
|
|
468
|
-
* @public
|
|
469
|
-
*/
|
|
470
|
-
async resetKey(key) {
|
|
471
|
-
this.current.delete(key);
|
|
472
|
-
this.previous.delete(key);
|
|
473
|
-
}
|
|
474
|
-
/**
|
|
475
|
-
* Method to reset everyone's hit counter.
|
|
476
|
-
*
|
|
477
|
-
* @public
|
|
478
|
-
*/
|
|
479
|
-
async resetAll() {
|
|
480
|
-
this.current.clear();
|
|
481
|
-
this.previous.clear();
|
|
482
|
-
}
|
|
483
|
-
/**
|
|
484
|
-
* Method to stop the timer (if currently running) and prevent any memory
|
|
485
|
-
* leaks.
|
|
486
|
-
*
|
|
487
|
-
* @public
|
|
488
|
-
*/
|
|
489
|
-
shutdown() {
|
|
490
|
-
clearInterval(this.interval);
|
|
491
|
-
void this.resetAll();
|
|
492
|
-
}
|
|
493
|
-
/**
|
|
494
|
-
* Recycles a client by setting its hit count to zero, and reset time to
|
|
495
|
-
* `windowMs` milliseconds from now.
|
|
496
|
-
*
|
|
497
|
-
* NOT to be confused with `#resetKey()`, which removes a client from both the
|
|
498
|
-
* `current` and `previous` maps.
|
|
499
|
-
*
|
|
500
|
-
* @param client {Client} - The client to recycle.
|
|
501
|
-
* @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client.
|
|
502
|
-
*
|
|
503
|
-
* @return {Client} - The modified client that was passed in, to allow for chaining.
|
|
504
|
-
*/
|
|
505
|
-
resetClient(client, now = Date.now()) {
|
|
506
|
-
client.totalHits = 0;
|
|
507
|
-
client.resetTime.setTime(now + this.windowMs);
|
|
508
|
-
return client;
|
|
509
|
-
}
|
|
510
|
-
/**
|
|
511
|
-
* Retrieves or creates a client, given a key. Also ensures that the client being
|
|
512
|
-
* returned is in the `current` map.
|
|
513
|
-
*
|
|
514
|
-
* @param key {string} - The key under which the client is (or is to be) stored.
|
|
515
|
-
*
|
|
516
|
-
* @returns {Client} - The requested client.
|
|
517
|
-
*/
|
|
518
|
-
getClient(key) {
|
|
519
|
-
if (this.current.has(key))
|
|
520
|
-
return this.current.get(key);
|
|
521
|
-
let client;
|
|
522
|
-
if (this.previous.has(key)) {
|
|
523
|
-
client = this.previous.get(key);
|
|
524
|
-
this.previous.delete(key);
|
|
525
|
-
} else {
|
|
526
|
-
client = { totalHits: 0, resetTime: /* @__PURE__ */ new Date() };
|
|
527
|
-
this.resetClient(client);
|
|
528
|
-
}
|
|
529
|
-
this.current.set(key, client);
|
|
530
|
-
return client;
|
|
531
|
-
}
|
|
532
|
-
/**
|
|
533
|
-
* Move current clients to previous, create a new map for current.
|
|
534
|
-
*
|
|
535
|
-
* This function is called every `windowMs`.
|
|
536
|
-
*/
|
|
537
|
-
clearExpired() {
|
|
538
|
-
this.previous = this.current;
|
|
539
|
-
this.current = /* @__PURE__ */ new Map();
|
|
540
|
-
}
|
|
541
|
-
};
|
|
542
|
-
|
|
543
|
-
// source/lib.ts
|
|
593
|
+
// source/rate-limit.ts
|
|
544
594
|
var isLegacyStore = (store) => (
|
|
545
595
|
// Check that `incr` exists but `increment` does not - store authors might want
|
|
546
596
|
// to keep both around for backwards compatibility.
|
|
@@ -557,8 +607,7 @@ var promisifyStore = (passedStore) => {
|
|
|
557
607
|
legacyStore.incr(
|
|
558
608
|
key,
|
|
559
609
|
(error, totalHits, resetTime) => {
|
|
560
|
-
if (error)
|
|
561
|
-
reject(error);
|
|
610
|
+
if (error) reject(error);
|
|
562
611
|
resolve({ totalHits, resetTime });
|
|
563
612
|
}
|
|
564
613
|
);
|
|
@@ -585,18 +634,8 @@ var getOptionsFromConfig = (config) => {
|
|
|
585
634
|
validate: validations2.enabled
|
|
586
635
|
};
|
|
587
636
|
};
|
|
588
|
-
var omitUndefinedOptions = (passedOptions) => {
|
|
589
|
-
const omittedOptions = {};
|
|
590
|
-
for (const k of Object.keys(passedOptions)) {
|
|
591
|
-
const key = k;
|
|
592
|
-
if (passedOptions[key] !== void 0) {
|
|
593
|
-
omittedOptions[key] = passedOptions[key];
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
return omittedOptions;
|
|
597
|
-
};
|
|
598
637
|
var parseOptions = (passedOptions) => {
|
|
599
|
-
const notUndefinedOptions =
|
|
638
|
+
const notUndefinedOptions = omitUndefinedProperties(passedOptions);
|
|
600
639
|
const validations2 = getValidations(notUndefinedOptions?.validate ?? true);
|
|
601
640
|
validations2.validationsConfig();
|
|
602
641
|
validations2.draftPolliHeaders(
|
|
@@ -604,9 +643,13 @@ var parseOptions = (passedOptions) => {
|
|
|
604
643
|
notUndefinedOptions.draft_polli_ratelimit_headers
|
|
605
644
|
);
|
|
606
645
|
validations2.onLimitReached(notUndefinedOptions.onLimitReached);
|
|
646
|
+
if (notUndefinedOptions.ipv6Subnet !== void 0 && typeof notUndefinedOptions.ipv6Subnet !== "function") {
|
|
647
|
+
validations2.ipv6Subnet(notUndefinedOptions.ipv6Subnet);
|
|
648
|
+
}
|
|
649
|
+
validations2.keyGeneratorIpFallback(notUndefinedOptions.keyGenerator);
|
|
650
|
+
validations2.ipv6SubnetOrKeyGenerator(notUndefinedOptions);
|
|
607
651
|
let standardHeaders = notUndefinedOptions.standardHeaders ?? false;
|
|
608
|
-
if (standardHeaders === true)
|
|
609
|
-
standardHeaders = "draft-6";
|
|
652
|
+
if (standardHeaders === true) standardHeaders = "draft-6";
|
|
610
653
|
const config = {
|
|
611
654
|
windowMs: 60 * 1e3,
|
|
612
655
|
limit: passedOptions.max ?? 5,
|
|
@@ -622,14 +665,10 @@ var parseOptions = (passedOptions) => {
|
|
|
622
665
|
const minutes = config.windowMs / (1e3 * 60);
|
|
623
666
|
const hours = config.windowMs / (1e3 * 60 * 60);
|
|
624
667
|
const days = config.windowMs / (1e3 * 60 * 60 * 24);
|
|
625
|
-
if (seconds < 60)
|
|
626
|
-
|
|
627
|
-
else if (
|
|
628
|
-
|
|
629
|
-
else if (hours < 24)
|
|
630
|
-
duration = `${hours}hr${hours > 1 ? "s" : ""}`;
|
|
631
|
-
else
|
|
632
|
-
duration = `${days}day${days > 1 ? "s" : ""}`;
|
|
668
|
+
if (seconds < 60) duration = `${seconds}sec`;
|
|
669
|
+
else if (minutes < 60) duration = `${minutes}min`;
|
|
670
|
+
else if (hours < 24) duration = `${hours}hr${hours > 1 ? "s" : ""}`;
|
|
671
|
+
else duration = `${days}day${days > 1 ? "s" : ""}`;
|
|
633
672
|
return `${limit}-in-${duration}`;
|
|
634
673
|
},
|
|
635
674
|
requestPropertyName: "rateLimit",
|
|
@@ -637,29 +676,35 @@ var parseOptions = (passedOptions) => {
|
|
|
637
676
|
skipSuccessfulRequests: false,
|
|
638
677
|
requestWasSuccessful: (_request, response) => response.statusCode < 400,
|
|
639
678
|
skip: (_request, _response) => false,
|
|
640
|
-
keyGenerator(request,
|
|
679
|
+
async keyGenerator(request, response) {
|
|
641
680
|
validations2.ip(request.ip);
|
|
642
681
|
validations2.trustProxy(request);
|
|
643
682
|
validations2.xForwardedForHeader(request);
|
|
644
|
-
|
|
683
|
+
const ip = request.ip;
|
|
684
|
+
let subnet = 56;
|
|
685
|
+
if (isIPv62(ip)) {
|
|
686
|
+
subnet = typeof config.ipv6Subnet === "function" ? await config.ipv6Subnet(request, response) : config.ipv6Subnet;
|
|
687
|
+
if (typeof config.ipv6Subnet === "function")
|
|
688
|
+
validations2.ipv6Subnet(subnet);
|
|
689
|
+
}
|
|
690
|
+
return ipKeyGenerator(ip, subnet);
|
|
645
691
|
},
|
|
692
|
+
ipv6Subnet: 56,
|
|
646
693
|
async handler(request, response, _next, _optionsUsed) {
|
|
647
694
|
response.status(config.statusCode);
|
|
648
695
|
const message = typeof config.message === "function" ? await config.message(
|
|
649
696
|
request,
|
|
650
697
|
response
|
|
651
698
|
) : config.message;
|
|
652
|
-
if (!response.writableEnded)
|
|
653
|
-
response.send(message);
|
|
654
|
-
}
|
|
699
|
+
if (!response.writableEnded) response.send(message);
|
|
655
700
|
},
|
|
656
701
|
passOnStoreError: false,
|
|
657
|
-
// Allow the default options to be
|
|
702
|
+
// Allow the default options to be overridden by the passed options.
|
|
658
703
|
...notUndefinedOptions,
|
|
659
704
|
// `standardHeaders` is resolved into a draft version above, use that.
|
|
660
705
|
standardHeaders,
|
|
661
706
|
// Note that this field is declared after the user's options are spread in,
|
|
662
|
-
// so that this field doesn't get
|
|
707
|
+
// so that this field doesn't get overridden with an un-promisified store!
|
|
663
708
|
store: promisifyStore(notUndefinedOptions.store ?? new MemoryStore()),
|
|
664
709
|
// Print an error to the console if a few known misconfigurations are detected.
|
|
665
710
|
validations: validations2
|
|
@@ -683,8 +728,7 @@ var rateLimit = (passedOptions) => {
|
|
|
683
728
|
const options = getOptionsFromConfig(config);
|
|
684
729
|
config.validations.creationStack(config.store);
|
|
685
730
|
config.validations.unsharedStore(config.store);
|
|
686
|
-
if (typeof config.store.init === "function")
|
|
687
|
-
config.store.init(options);
|
|
731
|
+
if (typeof config.store.init === "function") config.store.init(options);
|
|
688
732
|
const middleware = handleAsyncErrors(
|
|
689
733
|
async (request, response, next) => {
|
|
690
734
|
const skip = await config.skip(request, response);
|
|
@@ -720,7 +764,8 @@ var rateLimit = (passedOptions) => {
|
|
|
720
764
|
limit,
|
|
721
765
|
used: totalHits,
|
|
722
766
|
remaining: Math.max(limit - totalHits, 0),
|
|
723
|
-
resetTime
|
|
767
|
+
resetTime,
|
|
768
|
+
key
|
|
724
769
|
};
|
|
725
770
|
Object.defineProperty(info, "current", {
|
|
726
771
|
configurable: false,
|
|
@@ -769,8 +814,7 @@ var rateLimit = (passedOptions) => {
|
|
|
769
814
|
await decrementKey();
|
|
770
815
|
});
|
|
771
816
|
response.on("close", async () => {
|
|
772
|
-
if (!response.writableEnded)
|
|
773
|
-
await decrementKey();
|
|
817
|
+
if (!response.writableEnded) await decrementKey();
|
|
774
818
|
});
|
|
775
819
|
response.on("error", async () => {
|
|
776
820
|
await decrementKey();
|
|
@@ -801,9 +845,10 @@ var rateLimit = (passedOptions) => {
|
|
|
801
845
|
middleware.getKey = typeof config.store.get === "function" ? config.store.get.bind(config.store) : getThrowFn;
|
|
802
846
|
return middleware;
|
|
803
847
|
};
|
|
804
|
-
var
|
|
848
|
+
var rate_limit_default = rateLimit;
|
|
805
849
|
export {
|
|
806
850
|
MemoryStore,
|
|
807
|
-
|
|
808
|
-
|
|
851
|
+
rate_limit_default as default,
|
|
852
|
+
ipKeyGenerator,
|
|
853
|
+
rate_limit_default as rateLimit
|
|
809
854
|
};
|