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.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,27 +17,210 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// source/index.ts
|
|
21
|
-
var
|
|
22
|
-
__export(
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
23
33
|
MemoryStore: () => MemoryStore,
|
|
24
|
-
default: () =>
|
|
25
|
-
|
|
34
|
+
default: () => rate_limit_default,
|
|
35
|
+
ipKeyGenerator: () => ipKeyGenerator,
|
|
36
|
+
rateLimit: () => rate_limit_default
|
|
26
37
|
});
|
|
27
|
-
module.exports = __toCommonJS(
|
|
38
|
+
module.exports = __toCommonJS(index_exports);
|
|
39
|
+
|
|
40
|
+
// source/ip-key-generator.ts
|
|
41
|
+
var import_node_net = require("node:net");
|
|
42
|
+
var import_ip = __toESM(require("ip"), 1);
|
|
43
|
+
function ipKeyGenerator(ip, ipv6Subnet = 56) {
|
|
44
|
+
if (ipv6Subnet && (0, import_node_net.isIPv6)(ip)) {
|
|
45
|
+
return `${import_ip.default.mask(
|
|
46
|
+
ip,
|
|
47
|
+
import_ip.default.fromPrefixLen(ipv6Subnet)
|
|
48
|
+
)}/${ipv6Subnet}`;
|
|
49
|
+
}
|
|
50
|
+
return ip;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// source/memory-store.ts
|
|
54
|
+
var MemoryStore = class {
|
|
55
|
+
constructor() {
|
|
56
|
+
/**
|
|
57
|
+
* These two maps store usage (requests) and reset time by key (for example, IP
|
|
58
|
+
* addresses or API keys).
|
|
59
|
+
*
|
|
60
|
+
* They are split into two to avoid having to iterate through the entire set to
|
|
61
|
+
* determine which ones need reset. Instead, `Client`s are moved from `previous`
|
|
62
|
+
* to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients
|
|
63
|
+
* left in `previous`, i.e., those that have not made any recent requests, are
|
|
64
|
+
* known to be expired and can be deleted in bulk.
|
|
65
|
+
*/
|
|
66
|
+
this.previous = /* @__PURE__ */ new Map();
|
|
67
|
+
this.current = /* @__PURE__ */ new Map();
|
|
68
|
+
/**
|
|
69
|
+
* Confirmation that the keys incremented in once instance of MemoryStore
|
|
70
|
+
* cannot affect other instances.
|
|
71
|
+
*/
|
|
72
|
+
this.localKeys = true;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Method that initializes the store.
|
|
76
|
+
*
|
|
77
|
+
* @param options {Options} - The options used to setup the middleware.
|
|
78
|
+
*/
|
|
79
|
+
init(options) {
|
|
80
|
+
this.windowMs = options.windowMs;
|
|
81
|
+
if (this.interval) clearInterval(this.interval);
|
|
82
|
+
this.interval = setInterval(() => {
|
|
83
|
+
this.clearExpired();
|
|
84
|
+
}, this.windowMs);
|
|
85
|
+
this.interval.unref?.();
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Method to fetch a client's hit count and reset time.
|
|
89
|
+
*
|
|
90
|
+
* @param key {string} - The identifier for a client.
|
|
91
|
+
*
|
|
92
|
+
* @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
|
|
93
|
+
*
|
|
94
|
+
* @public
|
|
95
|
+
*/
|
|
96
|
+
async get(key) {
|
|
97
|
+
return this.current.get(key) ?? this.previous.get(key);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Method to increment a client's hit counter.
|
|
101
|
+
*
|
|
102
|
+
* @param key {string} - The identifier for a client.
|
|
103
|
+
*
|
|
104
|
+
* @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
|
|
105
|
+
*
|
|
106
|
+
* @public
|
|
107
|
+
*/
|
|
108
|
+
async increment(key) {
|
|
109
|
+
const client = this.getClient(key);
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
if (client.resetTime.getTime() <= now) {
|
|
112
|
+
this.resetClient(client, now);
|
|
113
|
+
}
|
|
114
|
+
client.totalHits++;
|
|
115
|
+
return client;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Method to decrement a client's hit counter.
|
|
119
|
+
*
|
|
120
|
+
* @param key {string} - The identifier for a client.
|
|
121
|
+
*
|
|
122
|
+
* @public
|
|
123
|
+
*/
|
|
124
|
+
async decrement(key) {
|
|
125
|
+
const client = this.getClient(key);
|
|
126
|
+
if (client.totalHits > 0) client.totalHits--;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Method to reset a client's hit counter.
|
|
130
|
+
*
|
|
131
|
+
* @param key {string} - The identifier for a client.
|
|
132
|
+
*
|
|
133
|
+
* @public
|
|
134
|
+
*/
|
|
135
|
+
async resetKey(key) {
|
|
136
|
+
this.current.delete(key);
|
|
137
|
+
this.previous.delete(key);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Method to reset everyone's hit counter.
|
|
141
|
+
*
|
|
142
|
+
* @public
|
|
143
|
+
*/
|
|
144
|
+
async resetAll() {
|
|
145
|
+
this.current.clear();
|
|
146
|
+
this.previous.clear();
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Method to stop the timer (if currently running) and prevent any memory
|
|
150
|
+
* leaks.
|
|
151
|
+
*
|
|
152
|
+
* @public
|
|
153
|
+
*/
|
|
154
|
+
shutdown() {
|
|
155
|
+
clearInterval(this.interval);
|
|
156
|
+
void this.resetAll();
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Recycles a client by setting its hit count to zero, and reset time to
|
|
160
|
+
* `windowMs` milliseconds from now.
|
|
161
|
+
*
|
|
162
|
+
* NOT to be confused with `#resetKey()`, which removes a client from both the
|
|
163
|
+
* `current` and `previous` maps.
|
|
164
|
+
*
|
|
165
|
+
* @param client {Client} - The client to recycle.
|
|
166
|
+
* @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client.
|
|
167
|
+
*
|
|
168
|
+
* @return {Client} - The modified client that was passed in, to allow for chaining.
|
|
169
|
+
*/
|
|
170
|
+
resetClient(client, now = Date.now()) {
|
|
171
|
+
client.totalHits = 0;
|
|
172
|
+
client.resetTime.setTime(now + this.windowMs);
|
|
173
|
+
return client;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Retrieves or creates a client, given a key. Also ensures that the client being
|
|
177
|
+
* returned is in the `current` map.
|
|
178
|
+
*
|
|
179
|
+
* @param key {string} - The key under which the client is (or is to be) stored.
|
|
180
|
+
*
|
|
181
|
+
* @returns {Client} - The requested client.
|
|
182
|
+
*/
|
|
183
|
+
getClient(key) {
|
|
184
|
+
if (this.current.has(key)) return this.current.get(key);
|
|
185
|
+
let client;
|
|
186
|
+
if (this.previous.has(key)) {
|
|
187
|
+
client = this.previous.get(key);
|
|
188
|
+
this.previous.delete(key);
|
|
189
|
+
} else {
|
|
190
|
+
client = { totalHits: 0, resetTime: /* @__PURE__ */ new Date() };
|
|
191
|
+
this.resetClient(client);
|
|
192
|
+
}
|
|
193
|
+
this.current.set(key, client);
|
|
194
|
+
return client;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Move current clients to previous, create a new map for current.
|
|
198
|
+
*
|
|
199
|
+
* This function is called every `windowMs`.
|
|
200
|
+
*/
|
|
201
|
+
clearExpired() {
|
|
202
|
+
this.previous = this.current;
|
|
203
|
+
this.current = /* @__PURE__ */ new Map();
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// source/rate-limit.ts
|
|
208
|
+
var import_node_net3 = require("node:net");
|
|
28
209
|
|
|
29
210
|
// source/headers.ts
|
|
30
|
-
var import_node_buffer = require("buffer");
|
|
31
|
-
var import_node_crypto = require("crypto");
|
|
32
|
-
var SUPPORTED_DRAFT_VERSIONS = [
|
|
33
|
-
|
|
34
|
-
|
|
211
|
+
var import_node_buffer = require("node:buffer");
|
|
212
|
+
var import_node_crypto = require("node:crypto");
|
|
213
|
+
var SUPPORTED_DRAFT_VERSIONS = [
|
|
214
|
+
"draft-6",
|
|
215
|
+
"draft-7",
|
|
216
|
+
"draft-8"
|
|
217
|
+
];
|
|
218
|
+
var getResetSeconds = (windowMs, resetTime) => {
|
|
219
|
+
let resetSeconds;
|
|
35
220
|
if (resetTime) {
|
|
36
221
|
const deltaSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1e3);
|
|
37
222
|
resetSeconds = Math.max(0, deltaSeconds);
|
|
38
|
-
} else
|
|
223
|
+
} else {
|
|
39
224
|
resetSeconds = Math.ceil(windowMs / 1e3);
|
|
40
225
|
}
|
|
41
226
|
return resetSeconds;
|
|
@@ -47,8 +232,7 @@ var getPartitionKey = (key) => {
|
|
|
47
232
|
return import_node_buffer.Buffer.from(partitionKey).toString("base64");
|
|
48
233
|
};
|
|
49
234
|
var setLegacyHeaders = (response, info) => {
|
|
50
|
-
if (response.headersSent)
|
|
51
|
-
return;
|
|
235
|
+
if (response.headersSent) return;
|
|
52
236
|
response.setHeader("X-RateLimit-Limit", info.limit.toString());
|
|
53
237
|
response.setHeader("X-RateLimit-Remaining", info.remaining.toString());
|
|
54
238
|
if (info.resetTime instanceof Date) {
|
|
@@ -60,10 +244,9 @@ var setLegacyHeaders = (response, info) => {
|
|
|
60
244
|
}
|
|
61
245
|
};
|
|
62
246
|
var setDraft6Headers = (response, info, windowMs) => {
|
|
63
|
-
if (response.headersSent)
|
|
64
|
-
return;
|
|
247
|
+
if (response.headersSent) return;
|
|
65
248
|
const windowSeconds = Math.ceil(windowMs / 1e3);
|
|
66
|
-
const resetSeconds = getResetSeconds(info.resetTime);
|
|
249
|
+
const resetSeconds = getResetSeconds(windowMs, info.resetTime);
|
|
67
250
|
response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
|
|
68
251
|
response.setHeader("RateLimit-Limit", info.limit.toString());
|
|
69
252
|
response.setHeader("RateLimit-Remaining", info.remaining.toString());
|
|
@@ -71,10 +254,9 @@ var setDraft6Headers = (response, info, windowMs) => {
|
|
|
71
254
|
response.setHeader("RateLimit-Reset", resetSeconds.toString());
|
|
72
255
|
};
|
|
73
256
|
var setDraft7Headers = (response, info, windowMs) => {
|
|
74
|
-
if (response.headersSent)
|
|
75
|
-
return;
|
|
257
|
+
if (response.headersSent) return;
|
|
76
258
|
const windowSeconds = Math.ceil(windowMs / 1e3);
|
|
77
|
-
const resetSeconds = getResetSeconds(info.resetTime
|
|
259
|
+
const resetSeconds = getResetSeconds(windowMs, info.resetTime);
|
|
78
260
|
response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
|
|
79
261
|
response.setHeader(
|
|
80
262
|
"RateLimit",
|
|
@@ -82,25 +264,35 @@ var setDraft7Headers = (response, info, windowMs) => {
|
|
|
82
264
|
);
|
|
83
265
|
};
|
|
84
266
|
var setDraft8Headers = (response, info, windowMs, name, key) => {
|
|
85
|
-
if (response.headersSent)
|
|
86
|
-
return;
|
|
267
|
+
if (response.headersSent) return;
|
|
87
268
|
const windowSeconds = Math.ceil(windowMs / 1e3);
|
|
88
|
-
const resetSeconds = getResetSeconds(info.resetTime
|
|
269
|
+
const resetSeconds = getResetSeconds(windowMs, info.resetTime);
|
|
89
270
|
const partitionKey = getPartitionKey(key);
|
|
90
|
-
const policy = `q=${info.limit}; w=${windowSeconds}; pk=:${partitionKey}:`;
|
|
91
271
|
const header = `r=${info.remaining}; t=${resetSeconds}`;
|
|
92
|
-
|
|
272
|
+
const policy = `q=${info.limit}; w=${windowSeconds}; pk=:${partitionKey}:`;
|
|
93
273
|
response.append("RateLimit", `"${name}"; ${header}`);
|
|
274
|
+
response.append("RateLimit-Policy", `"${name}"; ${policy}`);
|
|
94
275
|
};
|
|
95
276
|
var setRetryAfterHeader = (response, info, windowMs) => {
|
|
96
|
-
if (response.headersSent)
|
|
97
|
-
|
|
98
|
-
const resetSeconds = getResetSeconds(info.resetTime, windowMs);
|
|
277
|
+
if (response.headersSent) return;
|
|
278
|
+
const resetSeconds = getResetSeconds(windowMs, info.resetTime);
|
|
99
279
|
response.setHeader("Retry-After", resetSeconds.toString());
|
|
100
280
|
};
|
|
101
281
|
|
|
282
|
+
// source/utils.ts
|
|
283
|
+
var omitUndefinedProperties = (passedOptions) => {
|
|
284
|
+
const omittedOptions = {};
|
|
285
|
+
for (const k of Object.keys(passedOptions)) {
|
|
286
|
+
const key = k;
|
|
287
|
+
if (passedOptions[key] !== void 0) {
|
|
288
|
+
omittedOptions[key] = passedOptions[key];
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return omittedOptions;
|
|
292
|
+
};
|
|
293
|
+
|
|
102
294
|
// source/validations.ts
|
|
103
|
-
var
|
|
295
|
+
var import_node_net2 = require("node:net");
|
|
104
296
|
var ValidationError = class extends Error {
|
|
105
297
|
/**
|
|
106
298
|
* The code must be a string, in snake case and all capital, that starts with
|
|
@@ -122,14 +314,12 @@ var ChangeWarning = class extends ValidationError {
|
|
|
122
314
|
var usedStores = /* @__PURE__ */ new Set();
|
|
123
315
|
var singleCountKeys = /* @__PURE__ */ new WeakMap();
|
|
124
316
|
var validations = {
|
|
125
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
126
317
|
enabled: {
|
|
127
318
|
default: true
|
|
128
319
|
},
|
|
129
320
|
// Should be EnabledValidations type, but that's a circular reference
|
|
130
321
|
disable() {
|
|
131
|
-
for (const k of Object.keys(this.enabled))
|
|
132
|
-
this.enabled[k] = false;
|
|
322
|
+
for (const k of Object.keys(this.enabled)) this.enabled[k] = false;
|
|
133
323
|
},
|
|
134
324
|
/**
|
|
135
325
|
* Checks whether the IP address is valid, and that it does not have a port
|
|
@@ -148,7 +338,7 @@ var validations = {
|
|
|
148
338
|
`An undefined 'request.ip' was detected. This might indicate a misconfiguration or the connection being destroyed prematurely.`
|
|
149
339
|
);
|
|
150
340
|
}
|
|
151
|
-
if (!(0,
|
|
341
|
+
if (!(0, import_node_net2.isIP)(ip)) {
|
|
152
342
|
throw new ValidationError(
|
|
153
343
|
"ERR_ERL_INVALID_IP_ADDRESS",
|
|
154
344
|
`An invalid 'request.ip' (${ip}) was detected. Consider passing a custom 'keyGenerator' function to the rate limiter.`
|
|
@@ -302,7 +492,8 @@ var validations = {
|
|
|
302
492
|
* @returns {void}
|
|
303
493
|
*/
|
|
304
494
|
headersDraftVersion(version) {
|
|
305
|
-
if (typeof version !== "string" ||
|
|
495
|
+
if (typeof version !== "string" || // @ts-expect-error This is fine. If version is not in the array, it will just return false.
|
|
496
|
+
!SUPPORTED_DRAFT_VERSIONS.includes(version)) {
|
|
306
497
|
const versionString = SUPPORTED_DRAFT_VERSIONS.join(", ");
|
|
307
498
|
throw new ValidationError(
|
|
308
499
|
"ERR_ERL_HEADERS_UNSUPPORTED_DRAFT_VERSION",
|
|
@@ -358,7 +549,8 @@ var validations = {
|
|
|
358
549
|
const { stack } = new Error(
|
|
359
550
|
"express-rate-limit validation check (set options.validate.creationStack=false to disable)"
|
|
360
551
|
);
|
|
361
|
-
if (stack?.includes("Layer.handle [as handle_request]")
|
|
552
|
+
if (stack?.includes("Layer.handle [as handle_request]") || // express v4
|
|
553
|
+
stack?.includes("Layer.handleRequest")) {
|
|
362
554
|
if (!store.localKeys) {
|
|
363
555
|
throw new ValidationError(
|
|
364
556
|
"ERR_ERL_CREATED_IN_REQUEST_HANDLER",
|
|
@@ -370,6 +562,37 @@ var validations = {
|
|
|
370
562
|
`express-rate-limit instance should be created at app initialization, not when responding to a request.`
|
|
371
563
|
);
|
|
372
564
|
}
|
|
565
|
+
},
|
|
566
|
+
ipv6Subnet(ipv6Subnet) {
|
|
567
|
+
if (ipv6Subnet === false) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (!Number.isInteger(ipv6Subnet) || ipv6Subnet < 32 || ipv6Subnet > 64) {
|
|
571
|
+
throw new ValidationError(
|
|
572
|
+
"ERR_ERL_IPV6_SUBNET",
|
|
573
|
+
`Unexpected ipv6Subnet value: ${ipv6Subnet}. Expected an integer between 32 and 64 (usually 48-64).`
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
ipv6SubnetOrKeyGenerator(options) {
|
|
578
|
+
if (options.ipv6Subnet !== void 0 && options.keyGenerator) {
|
|
579
|
+
throw new ValidationError(
|
|
580
|
+
"ERR_ERL_IPV6SUBNET_OR_KEYGENERATOR",
|
|
581
|
+
`Incompatible options: the 'ipv6Subnet' option is ignored when a custom 'keyGenerator' function is also set.`
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
},
|
|
585
|
+
keyGeneratorIpFallback(keyGenerator) {
|
|
586
|
+
if (!keyGenerator) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
const src = keyGenerator.toString();
|
|
590
|
+
if ((src.includes("req.ip") || src.includes("request.ip")) && !src.includes("ipKeyGenerator")) {
|
|
591
|
+
throw new ValidationError(
|
|
592
|
+
"ERR_ERL_KEY_GEN_IPV6",
|
|
593
|
+
`Custom keyGenerator appears to use request IP without calling the ipKeyGenerator helper function for IPv6 addresses. This could allow IPv6 users to bypass limits.`
|
|
594
|
+
);
|
|
595
|
+
}
|
|
373
596
|
}
|
|
374
597
|
};
|
|
375
598
|
var getValidations = (_enabled) => {
|
|
@@ -384,9 +607,7 @@ var getValidations = (_enabled) => {
|
|
|
384
607
|
..._enabled
|
|
385
608
|
};
|
|
386
609
|
}
|
|
387
|
-
const wrappedValidations = {
|
|
388
|
-
enabled
|
|
389
|
-
};
|
|
610
|
+
const wrappedValidations = { enabled };
|
|
390
611
|
for (const [name, validation] of Object.entries(validations)) {
|
|
391
612
|
if (typeof validation === "function")
|
|
392
613
|
wrappedValidations[name] = (...args) => {
|
|
@@ -400,175 +621,15 @@ var getValidations = (_enabled) => {
|
|
|
400
621
|
args
|
|
401
622
|
);
|
|
402
623
|
} catch (error) {
|
|
403
|
-
if (error instanceof ChangeWarning)
|
|
404
|
-
|
|
405
|
-
else
|
|
406
|
-
console.error(error);
|
|
624
|
+
if (error instanceof ChangeWarning) console.warn(error);
|
|
625
|
+
else console.error(error);
|
|
407
626
|
}
|
|
408
627
|
};
|
|
409
628
|
}
|
|
410
629
|
return wrappedValidations;
|
|
411
630
|
};
|
|
412
631
|
|
|
413
|
-
// source/
|
|
414
|
-
var MemoryStore = class {
|
|
415
|
-
constructor() {
|
|
416
|
-
/**
|
|
417
|
-
* These two maps store usage (requests) and reset time by key (for example, IP
|
|
418
|
-
* addresses or API keys).
|
|
419
|
-
*
|
|
420
|
-
* They are split into two to avoid having to iterate through the entire set to
|
|
421
|
-
* determine which ones need reset. Instead, `Client`s are moved from `previous`
|
|
422
|
-
* to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients
|
|
423
|
-
* left in `previous`, i.e., those that have not made any recent requests, are
|
|
424
|
-
* known to be expired and can be deleted in bulk.
|
|
425
|
-
*/
|
|
426
|
-
this.previous = /* @__PURE__ */ new Map();
|
|
427
|
-
this.current = /* @__PURE__ */ new Map();
|
|
428
|
-
/**
|
|
429
|
-
* Confirmation that the keys incremented in once instance of MemoryStore
|
|
430
|
-
* cannot affect other instances.
|
|
431
|
-
*/
|
|
432
|
-
this.localKeys = true;
|
|
433
|
-
}
|
|
434
|
-
/**
|
|
435
|
-
* Method that initializes the store.
|
|
436
|
-
*
|
|
437
|
-
* @param options {Options} - The options used to setup the middleware.
|
|
438
|
-
*/
|
|
439
|
-
init(options) {
|
|
440
|
-
this.windowMs = options.windowMs;
|
|
441
|
-
if (this.interval)
|
|
442
|
-
clearInterval(this.interval);
|
|
443
|
-
this.interval = setInterval(() => {
|
|
444
|
-
this.clearExpired();
|
|
445
|
-
}, this.windowMs);
|
|
446
|
-
if (this.interval.unref)
|
|
447
|
-
this.interval.unref();
|
|
448
|
-
}
|
|
449
|
-
/**
|
|
450
|
-
* Method to fetch a client's hit count and reset time.
|
|
451
|
-
*
|
|
452
|
-
* @param key {string} - The identifier for a client.
|
|
453
|
-
*
|
|
454
|
-
* @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
|
|
455
|
-
*
|
|
456
|
-
* @public
|
|
457
|
-
*/
|
|
458
|
-
async get(key) {
|
|
459
|
-
return this.current.get(key) ?? this.previous.get(key);
|
|
460
|
-
}
|
|
461
|
-
/**
|
|
462
|
-
* Method to increment a client's hit counter.
|
|
463
|
-
*
|
|
464
|
-
* @param key {string} - The identifier for a client.
|
|
465
|
-
*
|
|
466
|
-
* @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
|
|
467
|
-
*
|
|
468
|
-
* @public
|
|
469
|
-
*/
|
|
470
|
-
async increment(key) {
|
|
471
|
-
const client = this.getClient(key);
|
|
472
|
-
const now = Date.now();
|
|
473
|
-
if (client.resetTime.getTime() <= now) {
|
|
474
|
-
this.resetClient(client, now);
|
|
475
|
-
}
|
|
476
|
-
client.totalHits++;
|
|
477
|
-
return client;
|
|
478
|
-
}
|
|
479
|
-
/**
|
|
480
|
-
* Method to decrement a client's hit counter.
|
|
481
|
-
*
|
|
482
|
-
* @param key {string} - The identifier for a client.
|
|
483
|
-
*
|
|
484
|
-
* @public
|
|
485
|
-
*/
|
|
486
|
-
async decrement(key) {
|
|
487
|
-
const client = this.getClient(key);
|
|
488
|
-
if (client.totalHits > 0)
|
|
489
|
-
client.totalHits--;
|
|
490
|
-
}
|
|
491
|
-
/**
|
|
492
|
-
* Method to reset a client's hit counter.
|
|
493
|
-
*
|
|
494
|
-
* @param key {string} - The identifier for a client.
|
|
495
|
-
*
|
|
496
|
-
* @public
|
|
497
|
-
*/
|
|
498
|
-
async resetKey(key) {
|
|
499
|
-
this.current.delete(key);
|
|
500
|
-
this.previous.delete(key);
|
|
501
|
-
}
|
|
502
|
-
/**
|
|
503
|
-
* Method to reset everyone's hit counter.
|
|
504
|
-
*
|
|
505
|
-
* @public
|
|
506
|
-
*/
|
|
507
|
-
async resetAll() {
|
|
508
|
-
this.current.clear();
|
|
509
|
-
this.previous.clear();
|
|
510
|
-
}
|
|
511
|
-
/**
|
|
512
|
-
* Method to stop the timer (if currently running) and prevent any memory
|
|
513
|
-
* leaks.
|
|
514
|
-
*
|
|
515
|
-
* @public
|
|
516
|
-
*/
|
|
517
|
-
shutdown() {
|
|
518
|
-
clearInterval(this.interval);
|
|
519
|
-
void this.resetAll();
|
|
520
|
-
}
|
|
521
|
-
/**
|
|
522
|
-
* Recycles a client by setting its hit count to zero, and reset time to
|
|
523
|
-
* `windowMs` milliseconds from now.
|
|
524
|
-
*
|
|
525
|
-
* NOT to be confused with `#resetKey()`, which removes a client from both the
|
|
526
|
-
* `current` and `previous` maps.
|
|
527
|
-
*
|
|
528
|
-
* @param client {Client} - The client to recycle.
|
|
529
|
-
* @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client.
|
|
530
|
-
*
|
|
531
|
-
* @return {Client} - The modified client that was passed in, to allow for chaining.
|
|
532
|
-
*/
|
|
533
|
-
resetClient(client, now = Date.now()) {
|
|
534
|
-
client.totalHits = 0;
|
|
535
|
-
client.resetTime.setTime(now + this.windowMs);
|
|
536
|
-
return client;
|
|
537
|
-
}
|
|
538
|
-
/**
|
|
539
|
-
* Retrieves or creates a client, given a key. Also ensures that the client being
|
|
540
|
-
* returned is in the `current` map.
|
|
541
|
-
*
|
|
542
|
-
* @param key {string} - The key under which the client is (or is to be) stored.
|
|
543
|
-
*
|
|
544
|
-
* @returns {Client} - The requested client.
|
|
545
|
-
*/
|
|
546
|
-
getClient(key) {
|
|
547
|
-
if (this.current.has(key))
|
|
548
|
-
return this.current.get(key);
|
|
549
|
-
let client;
|
|
550
|
-
if (this.previous.has(key)) {
|
|
551
|
-
client = this.previous.get(key);
|
|
552
|
-
this.previous.delete(key);
|
|
553
|
-
} else {
|
|
554
|
-
client = { totalHits: 0, resetTime: /* @__PURE__ */ new Date() };
|
|
555
|
-
this.resetClient(client);
|
|
556
|
-
}
|
|
557
|
-
this.current.set(key, client);
|
|
558
|
-
return client;
|
|
559
|
-
}
|
|
560
|
-
/**
|
|
561
|
-
* Move current clients to previous, create a new map for current.
|
|
562
|
-
*
|
|
563
|
-
* This function is called every `windowMs`.
|
|
564
|
-
*/
|
|
565
|
-
clearExpired() {
|
|
566
|
-
this.previous = this.current;
|
|
567
|
-
this.current = /* @__PURE__ */ new Map();
|
|
568
|
-
}
|
|
569
|
-
};
|
|
570
|
-
|
|
571
|
-
// source/lib.ts
|
|
632
|
+
// source/rate-limit.ts
|
|
572
633
|
var isLegacyStore = (store) => (
|
|
573
634
|
// Check that `incr` exists but `increment` does not - store authors might want
|
|
574
635
|
// to keep both around for backwards compatibility.
|
|
@@ -585,8 +646,7 @@ var promisifyStore = (passedStore) => {
|
|
|
585
646
|
legacyStore.incr(
|
|
586
647
|
key,
|
|
587
648
|
(error, totalHits, resetTime) => {
|
|
588
|
-
if (error)
|
|
589
|
-
reject(error);
|
|
649
|
+
if (error) reject(error);
|
|
590
650
|
resolve({ totalHits, resetTime });
|
|
591
651
|
}
|
|
592
652
|
);
|
|
@@ -613,18 +673,8 @@ var getOptionsFromConfig = (config) => {
|
|
|
613
673
|
validate: validations2.enabled
|
|
614
674
|
};
|
|
615
675
|
};
|
|
616
|
-
var omitUndefinedOptions = (passedOptions) => {
|
|
617
|
-
const omittedOptions = {};
|
|
618
|
-
for (const k of Object.keys(passedOptions)) {
|
|
619
|
-
const key = k;
|
|
620
|
-
if (passedOptions[key] !== void 0) {
|
|
621
|
-
omittedOptions[key] = passedOptions[key];
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
return omittedOptions;
|
|
625
|
-
};
|
|
626
676
|
var parseOptions = (passedOptions) => {
|
|
627
|
-
const notUndefinedOptions =
|
|
677
|
+
const notUndefinedOptions = omitUndefinedProperties(passedOptions);
|
|
628
678
|
const validations2 = getValidations(notUndefinedOptions?.validate ?? true);
|
|
629
679
|
validations2.validationsConfig();
|
|
630
680
|
validations2.draftPolliHeaders(
|
|
@@ -632,9 +682,13 @@ var parseOptions = (passedOptions) => {
|
|
|
632
682
|
notUndefinedOptions.draft_polli_ratelimit_headers
|
|
633
683
|
);
|
|
634
684
|
validations2.onLimitReached(notUndefinedOptions.onLimitReached);
|
|
685
|
+
if (notUndefinedOptions.ipv6Subnet !== void 0 && typeof notUndefinedOptions.ipv6Subnet !== "function") {
|
|
686
|
+
validations2.ipv6Subnet(notUndefinedOptions.ipv6Subnet);
|
|
687
|
+
}
|
|
688
|
+
validations2.keyGeneratorIpFallback(notUndefinedOptions.keyGenerator);
|
|
689
|
+
validations2.ipv6SubnetOrKeyGenerator(notUndefinedOptions);
|
|
635
690
|
let standardHeaders = notUndefinedOptions.standardHeaders ?? false;
|
|
636
|
-
if (standardHeaders === true)
|
|
637
|
-
standardHeaders = "draft-6";
|
|
691
|
+
if (standardHeaders === true) standardHeaders = "draft-6";
|
|
638
692
|
const config = {
|
|
639
693
|
windowMs: 60 * 1e3,
|
|
640
694
|
limit: passedOptions.max ?? 5,
|
|
@@ -650,14 +704,10 @@ var parseOptions = (passedOptions) => {
|
|
|
650
704
|
const minutes = config.windowMs / (1e3 * 60);
|
|
651
705
|
const hours = config.windowMs / (1e3 * 60 * 60);
|
|
652
706
|
const days = config.windowMs / (1e3 * 60 * 60 * 24);
|
|
653
|
-
if (seconds < 60)
|
|
654
|
-
|
|
655
|
-
else if (
|
|
656
|
-
|
|
657
|
-
else if (hours < 24)
|
|
658
|
-
duration = `${hours}hr${hours > 1 ? "s" : ""}`;
|
|
659
|
-
else
|
|
660
|
-
duration = `${days}day${days > 1 ? "s" : ""}`;
|
|
707
|
+
if (seconds < 60) duration = `${seconds}sec`;
|
|
708
|
+
else if (minutes < 60) duration = `${minutes}min`;
|
|
709
|
+
else if (hours < 24) duration = `${hours}hr${hours > 1 ? "s" : ""}`;
|
|
710
|
+
else duration = `${days}day${days > 1 ? "s" : ""}`;
|
|
661
711
|
return `${limit}-in-${duration}`;
|
|
662
712
|
},
|
|
663
713
|
requestPropertyName: "rateLimit",
|
|
@@ -665,29 +715,35 @@ var parseOptions = (passedOptions) => {
|
|
|
665
715
|
skipSuccessfulRequests: false,
|
|
666
716
|
requestWasSuccessful: (_request, response) => response.statusCode < 400,
|
|
667
717
|
skip: (_request, _response) => false,
|
|
668
|
-
keyGenerator(request,
|
|
718
|
+
async keyGenerator(request, response) {
|
|
669
719
|
validations2.ip(request.ip);
|
|
670
720
|
validations2.trustProxy(request);
|
|
671
721
|
validations2.xForwardedForHeader(request);
|
|
672
|
-
|
|
722
|
+
const ip = request.ip;
|
|
723
|
+
let subnet = 56;
|
|
724
|
+
if ((0, import_node_net3.isIPv6)(ip)) {
|
|
725
|
+
subnet = typeof config.ipv6Subnet === "function" ? await config.ipv6Subnet(request, response) : config.ipv6Subnet;
|
|
726
|
+
if (typeof config.ipv6Subnet === "function")
|
|
727
|
+
validations2.ipv6Subnet(subnet);
|
|
728
|
+
}
|
|
729
|
+
return ipKeyGenerator(ip, subnet);
|
|
673
730
|
},
|
|
731
|
+
ipv6Subnet: 56,
|
|
674
732
|
async handler(request, response, _next, _optionsUsed) {
|
|
675
733
|
response.status(config.statusCode);
|
|
676
734
|
const message = typeof config.message === "function" ? await config.message(
|
|
677
735
|
request,
|
|
678
736
|
response
|
|
679
737
|
) : config.message;
|
|
680
|
-
if (!response.writableEnded)
|
|
681
|
-
response.send(message);
|
|
682
|
-
}
|
|
738
|
+
if (!response.writableEnded) response.send(message);
|
|
683
739
|
},
|
|
684
740
|
passOnStoreError: false,
|
|
685
|
-
// Allow the default options to be
|
|
741
|
+
// Allow the default options to be overridden by the passed options.
|
|
686
742
|
...notUndefinedOptions,
|
|
687
743
|
// `standardHeaders` is resolved into a draft version above, use that.
|
|
688
744
|
standardHeaders,
|
|
689
745
|
// Note that this field is declared after the user's options are spread in,
|
|
690
|
-
// so that this field doesn't get
|
|
746
|
+
// so that this field doesn't get overridden with an un-promisified store!
|
|
691
747
|
store: promisifyStore(notUndefinedOptions.store ?? new MemoryStore()),
|
|
692
748
|
// Print an error to the console if a few known misconfigurations are detected.
|
|
693
749
|
validations: validations2
|
|
@@ -711,8 +767,7 @@ var rateLimit = (passedOptions) => {
|
|
|
711
767
|
const options = getOptionsFromConfig(config);
|
|
712
768
|
config.validations.creationStack(config.store);
|
|
713
769
|
config.validations.unsharedStore(config.store);
|
|
714
|
-
if (typeof config.store.init === "function")
|
|
715
|
-
config.store.init(options);
|
|
770
|
+
if (typeof config.store.init === "function") config.store.init(options);
|
|
716
771
|
const middleware = handleAsyncErrors(
|
|
717
772
|
async (request, response, next) => {
|
|
718
773
|
const skip = await config.skip(request, response);
|
|
@@ -748,7 +803,8 @@ var rateLimit = (passedOptions) => {
|
|
|
748
803
|
limit,
|
|
749
804
|
used: totalHits,
|
|
750
805
|
remaining: Math.max(limit - totalHits, 0),
|
|
751
|
-
resetTime
|
|
806
|
+
resetTime,
|
|
807
|
+
key
|
|
752
808
|
};
|
|
753
809
|
Object.defineProperty(info, "current", {
|
|
754
810
|
configurable: false,
|
|
@@ -797,8 +853,7 @@ var rateLimit = (passedOptions) => {
|
|
|
797
853
|
await decrementKey();
|
|
798
854
|
});
|
|
799
855
|
response.on("close", async () => {
|
|
800
|
-
if (!response.writableEnded)
|
|
801
|
-
await decrementKey();
|
|
856
|
+
if (!response.writableEnded) await decrementKey();
|
|
802
857
|
});
|
|
803
858
|
response.on("error", async () => {
|
|
804
859
|
await decrementKey();
|
|
@@ -829,10 +884,11 @@ var rateLimit = (passedOptions) => {
|
|
|
829
884
|
middleware.getKey = typeof config.store.get === "function" ? config.store.get.bind(config.store) : getThrowFn;
|
|
830
885
|
return middleware;
|
|
831
886
|
};
|
|
832
|
-
var
|
|
887
|
+
var rate_limit_default = rateLimit;
|
|
833
888
|
// Annotate the CommonJS export names for ESM import in node:
|
|
834
889
|
0 && (module.exports = {
|
|
835
890
|
MemoryStore,
|
|
891
|
+
ipKeyGenerator,
|
|
836
892
|
rateLimit
|
|
837
893
|
});
|
|
838
894
|
module.exports = rateLimit; module.exports.default = rateLimit; module.exports.rateLimit = rateLimit; module.exports.MemoryStore = MemoryStore;
|