express-rate-limit 6.11.2 → 7.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/changelog.md +56 -15
- package/dist/index.cjs +260 -220
- package/dist/index.d.cts +169 -27
- package/dist/index.d.mts +169 -27
- package/dist/index.d.ts +169 -27
- package/dist/index.mjs +260 -222
- package/license.md +1 -1
- package/package.json +27 -27
- package/readme.md +88 -93
- package/tsconfig.json +4 -1
package/dist/index.cjs
CHANGED
|
@@ -3,7 +3,6 @@ var __defProp = Object.defineProperty;
|
|
|
3
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
7
6
|
var __export = (target, all) => {
|
|
8
7
|
for (var name in all)
|
|
9
8
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -17,10 +16,6 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
17
16
|
return to;
|
|
18
17
|
};
|
|
19
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
-
var __publicField = (obj, key, value) => {
|
|
21
|
-
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
22
|
-
return value;
|
|
23
|
-
};
|
|
24
19
|
|
|
25
20
|
// source/index.ts
|
|
26
21
|
var source_exports = {};
|
|
@@ -97,9 +92,6 @@ var ValidationError = class extends Error {
|
|
|
97
92
|
constructor(code, message) {
|
|
98
93
|
const url = `https://express-rate-limit.github.io/${code}/`;
|
|
99
94
|
super(`${message} See ${url} for more information.`);
|
|
100
|
-
__publicField(this, "name");
|
|
101
|
-
__publicField(this, "code");
|
|
102
|
-
__publicField(this, "help");
|
|
103
95
|
this.name = this.constructor.name;
|
|
104
96
|
this.code = code;
|
|
105
97
|
this.help = url;
|
|
@@ -107,18 +99,17 @@ var ValidationError = class extends Error {
|
|
|
107
99
|
};
|
|
108
100
|
var ChangeWarning = class extends ValidationError {
|
|
109
101
|
};
|
|
110
|
-
var
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
this.enabled = true;
|
|
118
|
-
}
|
|
102
|
+
var singleCountKeys = /* @__PURE__ */ new WeakMap();
|
|
103
|
+
var validations = {
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
105
|
+
enabled: {
|
|
106
|
+
default: true
|
|
107
|
+
},
|
|
108
|
+
// Should be EnabledValidations type, but that's a circular reference
|
|
119
109
|
disable() {
|
|
120
|
-
this.enabled
|
|
121
|
-
|
|
110
|
+
for (const k of Object.keys(this.enabled))
|
|
111
|
+
this.enabled[k] = false;
|
|
112
|
+
},
|
|
122
113
|
/**
|
|
123
114
|
* Checks whether the IP address is valid, and that it does not have a port
|
|
124
115
|
* number in it.
|
|
@@ -130,21 +121,19 @@ var _Validations = class _Validations {
|
|
|
130
121
|
* @returns {void}
|
|
131
122
|
*/
|
|
132
123
|
ip(ip) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
});
|
|
147
|
-
}
|
|
124
|
+
if (ip === void 0) {
|
|
125
|
+
throw new ValidationError(
|
|
126
|
+
"ERR_ERL_UNDEFINED_IP_ADDRESS",
|
|
127
|
+
`An undefined 'request.ip' was detected. This might indicate a misconfiguration or the connection being destroyed prematurely.`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (!(0, import_node_net.isIP)(ip)) {
|
|
131
|
+
throw new ValidationError(
|
|
132
|
+
"ERR_ERL_INVALID_IP_ADDRESS",
|
|
133
|
+
`An invalid 'request.ip' (${ip}) was detected. Consider passing a custom 'keyGenerator' function to the rate limiter.`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
148
137
|
/**
|
|
149
138
|
* Makes sure the trust proxy setting is not set to `true`.
|
|
150
139
|
*
|
|
@@ -155,15 +144,13 @@ var _Validations = class _Validations {
|
|
|
155
144
|
* @returns {void}
|
|
156
145
|
*/
|
|
157
146
|
trustProxy(request) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
});
|
|
166
|
-
}
|
|
147
|
+
if (request.app.get("trust proxy") === true) {
|
|
148
|
+
throw new ValidationError(
|
|
149
|
+
"ERR_ERL_PERMISSIVE_TRUST_PROXY",
|
|
150
|
+
`The Express 'trust proxy' setting is true, which allows anyone to trivially bypass IP-based rate limiting.`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
},
|
|
167
154
|
/**
|
|
168
155
|
* Makes sure the trust proxy setting is set in case the `X-Forwarded-For`
|
|
169
156
|
* header is present.
|
|
@@ -175,31 +162,26 @@ var _Validations = class _Validations {
|
|
|
175
162
|
* @returns {void}
|
|
176
163
|
*/
|
|
177
164
|
xForwardedForHeader(request) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
});
|
|
186
|
-
}
|
|
165
|
+
if (request.headers["x-forwarded-for"] && request.app.get("trust proxy") === false) {
|
|
166
|
+
throw new ValidationError(
|
|
167
|
+
"ERR_ERL_UNEXPECTED_X_FORWARDED_FOR",
|
|
168
|
+
`The 'X-Forwarded-For' header is set but the Express 'trust proxy' setting is false (default). This could indicate a misconfiguration which would prevent express-rate-limit from accurately identifying users.`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
},
|
|
187
172
|
/**
|
|
188
173
|
* Ensures totalHits value from store is a positive integer.
|
|
189
174
|
*
|
|
190
175
|
* @param hits {any} - The `totalHits` returned by the store.
|
|
191
176
|
*/
|
|
192
177
|
positiveHits(hits) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
}
|
|
178
|
+
if (typeof hits !== "number" || hits < 1 || hits !== Math.round(hits)) {
|
|
179
|
+
throw new ValidationError(
|
|
180
|
+
"ERR_ERL_INVALID_HITS",
|
|
181
|
+
`The totalHits value returned from the store must be a positive integer, got ${hits}`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
},
|
|
203
185
|
/**
|
|
204
186
|
* Ensures a given key is incremented only once per request.
|
|
205
187
|
*
|
|
@@ -210,83 +192,74 @@ var _Validations = class _Validations {
|
|
|
210
192
|
* @returns {void}
|
|
211
193
|
*/
|
|
212
194
|
singleCount(request, store, key) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
keys.push(prefixedKey);
|
|
234
|
-
});
|
|
235
|
-
}
|
|
195
|
+
let storeKeys = singleCountKeys.get(request);
|
|
196
|
+
if (!storeKeys) {
|
|
197
|
+
storeKeys = /* @__PURE__ */ new Map();
|
|
198
|
+
singleCountKeys.set(request, storeKeys);
|
|
199
|
+
}
|
|
200
|
+
const storeKey = store.localKeys ? store : store.constructor.name;
|
|
201
|
+
let keys = storeKeys.get(storeKey);
|
|
202
|
+
if (!keys) {
|
|
203
|
+
keys = [];
|
|
204
|
+
storeKeys.set(storeKey, keys);
|
|
205
|
+
}
|
|
206
|
+
const prefixedKey = `${store.prefix ?? ""}${key}`;
|
|
207
|
+
if (keys.includes(prefixedKey)) {
|
|
208
|
+
throw new ValidationError(
|
|
209
|
+
"ERR_ERL_DOUBLE_COUNT",
|
|
210
|
+
`The hit count for ${key} was incremented more than once for a single request.`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
keys.push(prefixedKey);
|
|
214
|
+
},
|
|
236
215
|
/**
|
|
237
|
-
* Warns the user that the behaviour for `max: 0` is changing in the next
|
|
216
|
+
* Warns the user that the behaviour for `max: 0` / `limit: 0` is changing in the next
|
|
238
217
|
* major release.
|
|
239
218
|
*
|
|
240
|
-
* @param
|
|
219
|
+
* @param limit {number} - The maximum number of hits per client.
|
|
241
220
|
*
|
|
242
221
|
* @returns {void}
|
|
243
222
|
*/
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
});
|
|
253
|
-
}
|
|
223
|
+
limit(limit) {
|
|
224
|
+
if (limit === 0) {
|
|
225
|
+
throw new ChangeWarning(
|
|
226
|
+
"WRN_ERL_MAX_ZERO",
|
|
227
|
+
`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`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
},
|
|
254
231
|
/**
|
|
255
232
|
* Warns the user that the `draft_polli_ratelimit_headers` option is deprecated
|
|
256
233
|
* and will be removed in the next major release.
|
|
257
234
|
*
|
|
258
|
-
* @param draft_polli_ratelimit_headers {
|
|
235
|
+
* @param draft_polli_ratelimit_headers {any | undefined} - The now-deprecated setting that was used to enable standard headers.
|
|
259
236
|
*
|
|
260
237
|
* @returns {void}
|
|
261
238
|
*/
|
|
262
239
|
draftPolliHeaders(draft_polli_ratelimit_headers) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
});
|
|
271
|
-
}
|
|
240
|
+
if (draft_polli_ratelimit_headers) {
|
|
241
|
+
throw new ChangeWarning(
|
|
242
|
+
"WRN_ERL_DEPRECATED_DRAFT_POLLI_HEADERS",
|
|
243
|
+
`The draft_polli_ratelimit_headers configuration option is deprecated and has been removed in express-rate-limit v7, please set standardHeaders: 'draft-6' instead.`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
},
|
|
272
247
|
/**
|
|
273
248
|
* Warns the user that the `onLimitReached` option is deprecated and will be removed in the next
|
|
274
249
|
* major release.
|
|
275
250
|
*
|
|
276
|
-
* @param onLimitReached {
|
|
251
|
+
* @param onLimitReached {any | undefined} - The maximum number of hits per client.
|
|
277
252
|
*
|
|
278
253
|
* @returns {void}
|
|
279
254
|
*/
|
|
280
255
|
onLimitReached(onLimitReached) {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
});
|
|
289
|
-
}
|
|
256
|
+
if (onLimitReached) {
|
|
257
|
+
throw new ChangeWarning(
|
|
258
|
+
"WRN_ERL_DEPRECATED_ON_LIMIT_REACHED",
|
|
259
|
+
`The onLimitReached configuration option is deprecated and has been removed in express-rate-limit v7.`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
},
|
|
290
263
|
/**
|
|
291
264
|
* Warns the user when the selected headers option requires a reset time but
|
|
292
265
|
* the store does not provide one.
|
|
@@ -296,71 +269,93 @@ var _Validations = class _Validations {
|
|
|
296
269
|
* @returns {void}
|
|
297
270
|
*/
|
|
298
271
|
headersResetTime(resetTime) {
|
|
299
|
-
|
|
300
|
-
|
|
272
|
+
if (!resetTime) {
|
|
273
|
+
throw new ValidationError(
|
|
274
|
+
"ERR_ERL_HEADERS_NO_RESET",
|
|
275
|
+
`standardHeaders: 'draft-7' requires a 'resetTime', but the store did not provide one. The 'windowMs' value will be used instead, which may cause clients to wait longer than necessary.`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
/**
|
|
280
|
+
* Checks the options.validate setting to ensure that only recognized validations are enabled or disabled.
|
|
281
|
+
*
|
|
282
|
+
* If any unrecognized values are found, an error is logged that includes the list of supported vaidations.
|
|
283
|
+
*/
|
|
284
|
+
validationsConfig() {
|
|
285
|
+
const supportedValidations = Object.keys(this).filter(
|
|
286
|
+
(k) => !["enabled", "disable"].includes(k)
|
|
287
|
+
);
|
|
288
|
+
supportedValidations.push("default");
|
|
289
|
+
for (const key of Object.keys(this.enabled)) {
|
|
290
|
+
if (!supportedValidations.includes(key)) {
|
|
301
291
|
throw new ValidationError(
|
|
302
|
-
"
|
|
303
|
-
`
|
|
292
|
+
"ERR_ERL_UNKNOWN_VALIDATION",
|
|
293
|
+
`options.validate.${key} is not recognized. Supported validate options are: ${supportedValidations.join(
|
|
294
|
+
", "
|
|
295
|
+
)}.`
|
|
304
296
|
);
|
|
305
297
|
}
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
wrap(validation) {
|
|
309
|
-
if (!this.enabled) {
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
try {
|
|
313
|
-
validation.call(this);
|
|
314
|
-
} catch (error) {
|
|
315
|
-
if (error instanceof ChangeWarning)
|
|
316
|
-
console.warn(error);
|
|
317
|
-
else
|
|
318
|
-
console.error(error);
|
|
319
298
|
}
|
|
320
299
|
}
|
|
321
300
|
};
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
301
|
+
var getValidations = (_enabled) => {
|
|
302
|
+
let enabled;
|
|
303
|
+
if (typeof _enabled === "boolean") {
|
|
304
|
+
enabled = {
|
|
305
|
+
default: _enabled
|
|
306
|
+
};
|
|
307
|
+
} else {
|
|
308
|
+
enabled = {
|
|
309
|
+
default: true,
|
|
310
|
+
..._enabled
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const wrappedValidations = {
|
|
314
|
+
enabled
|
|
315
|
+
};
|
|
316
|
+
for (const [name, validation] of Object.entries(validations)) {
|
|
317
|
+
if (typeof validation === "function")
|
|
318
|
+
wrappedValidations[name] = (...args) => {
|
|
319
|
+
if (!(enabled[name] ?? enabled.default)) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
;
|
|
324
|
+
validation.apply(
|
|
325
|
+
wrappedValidations,
|
|
326
|
+
args
|
|
327
|
+
);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
if (error instanceof ChangeWarning)
|
|
330
|
+
console.warn(error);
|
|
331
|
+
else
|
|
332
|
+
console.error(error);
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
return wrappedValidations;
|
|
337
|
+
};
|
|
334
338
|
|
|
335
339
|
// source/memory-store.ts
|
|
336
|
-
var calculateNextResetTime = (windowMs) => {
|
|
337
|
-
const resetTime = /* @__PURE__ */ new Date();
|
|
338
|
-
resetTime.setMilliseconds(resetTime.getMilliseconds() + windowMs);
|
|
339
|
-
return resetTime;
|
|
340
|
-
};
|
|
341
340
|
var MemoryStore = class {
|
|
342
341
|
constructor() {
|
|
343
342
|
/**
|
|
344
|
-
*
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
*
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
* The time at which all hit counts will be reset.
|
|
353
|
-
*/
|
|
354
|
-
__publicField(this, "resetTime");
|
|
355
|
-
/**
|
|
356
|
-
* Reference to the active timer.
|
|
343
|
+
* These two maps store usage (requests) and reset time by key (for example, IP
|
|
344
|
+
* addresses or API keys).
|
|
345
|
+
*
|
|
346
|
+
* They are split into two to avoid having to iterate through the entire set to
|
|
347
|
+
* determine which ones need reset. Instead, `Client`s are moved from `previous`
|
|
348
|
+
* to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients
|
|
349
|
+
* left in `previous`, i.e., those that have not made any recent requests, are
|
|
350
|
+
* known to be expired and can be deleted in bulk.
|
|
357
351
|
*/
|
|
358
|
-
|
|
352
|
+
this.previous = /* @__PURE__ */ new Map();
|
|
353
|
+
this.current = /* @__PURE__ */ new Map();
|
|
359
354
|
/**
|
|
360
355
|
* Confirmation that the keys incremented in once instance of MemoryStore
|
|
361
356
|
* cannot affect other instances.
|
|
362
357
|
*/
|
|
363
|
-
|
|
358
|
+
this.localKeys = true;
|
|
364
359
|
}
|
|
365
360
|
/**
|
|
366
361
|
* Method that initializes the store.
|
|
@@ -369,10 +364,10 @@ var MemoryStore = class {
|
|
|
369
364
|
*/
|
|
370
365
|
init(options) {
|
|
371
366
|
this.windowMs = options.windowMs;
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
this.interval = setInterval(
|
|
375
|
-
|
|
367
|
+
if (this.interval)
|
|
368
|
+
clearInterval(this.interval);
|
|
369
|
+
this.interval = setInterval(() => {
|
|
370
|
+
this.clearExpired();
|
|
376
371
|
}, this.windowMs);
|
|
377
372
|
if (this.interval.unref)
|
|
378
373
|
this.interval.unref();
|
|
@@ -387,12 +382,7 @@ var MemoryStore = class {
|
|
|
387
382
|
* @public
|
|
388
383
|
*/
|
|
389
384
|
async get(key) {
|
|
390
|
-
|
|
391
|
-
return {
|
|
392
|
-
totalHits: this.hits[key],
|
|
393
|
-
resetTime: this.resetTime
|
|
394
|
-
};
|
|
395
|
-
return void 0;
|
|
385
|
+
return this.current.get(key) ?? this.previous.get(key);
|
|
396
386
|
}
|
|
397
387
|
/**
|
|
398
388
|
* Method to increment a client's hit counter.
|
|
@@ -404,13 +394,13 @@ var MemoryStore = class {
|
|
|
404
394
|
* @public
|
|
405
395
|
*/
|
|
406
396
|
async increment(key) {
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
397
|
+
const client = this.getClient(key);
|
|
398
|
+
const now = Date.now();
|
|
399
|
+
if (client.resetTime.getTime() <= now) {
|
|
400
|
+
this.resetClient(client, now);
|
|
401
|
+
}
|
|
402
|
+
client.totalHits++;
|
|
403
|
+
return client;
|
|
414
404
|
}
|
|
415
405
|
/**
|
|
416
406
|
* Method to decrement a client's hit counter.
|
|
@@ -420,9 +410,9 @@ var MemoryStore = class {
|
|
|
420
410
|
* @public
|
|
421
411
|
*/
|
|
422
412
|
async decrement(key) {
|
|
423
|
-
const
|
|
424
|
-
if (
|
|
425
|
-
|
|
413
|
+
const client = this.getClient(key);
|
|
414
|
+
if (client.totalHits > 0)
|
|
415
|
+
client.totalHits--;
|
|
426
416
|
}
|
|
427
417
|
/**
|
|
428
418
|
* Method to reset a client's hit counter.
|
|
@@ -432,7 +422,8 @@ var MemoryStore = class {
|
|
|
432
422
|
* @public
|
|
433
423
|
*/
|
|
434
424
|
async resetKey(key) {
|
|
435
|
-
|
|
425
|
+
this.current.delete(key);
|
|
426
|
+
this.previous.delete(key);
|
|
436
427
|
}
|
|
437
428
|
/**
|
|
438
429
|
* Method to reset everyone's hit counter.
|
|
@@ -440,8 +431,8 @@ var MemoryStore = class {
|
|
|
440
431
|
* @public
|
|
441
432
|
*/
|
|
442
433
|
async resetAll() {
|
|
443
|
-
this.
|
|
444
|
-
this.
|
|
434
|
+
this.current.clear();
|
|
435
|
+
this.previous.clear();
|
|
445
436
|
}
|
|
446
437
|
/**
|
|
447
438
|
* Method to stop the timer (if currently running) and prevent any memory
|
|
@@ -451,6 +442,55 @@ var MemoryStore = class {
|
|
|
451
442
|
*/
|
|
452
443
|
shutdown() {
|
|
453
444
|
clearInterval(this.interval);
|
|
445
|
+
void this.resetAll();
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Recycles a client by setting its hit count to zero, and reset time to
|
|
449
|
+
* `windowMs` milliseconds from now.
|
|
450
|
+
*
|
|
451
|
+
* NOT to be confused with `#resetKey()`, which removes a client from both the
|
|
452
|
+
* `current` and `previous` maps.
|
|
453
|
+
*
|
|
454
|
+
* @param client {Client} - The client to recycle.
|
|
455
|
+
* @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client.
|
|
456
|
+
*
|
|
457
|
+
* @return {Client} - The modified client that was passed in, to allow for chaining.
|
|
458
|
+
*/
|
|
459
|
+
resetClient(client, now = Date.now()) {
|
|
460
|
+
client.totalHits = 0;
|
|
461
|
+
client.resetTime.setTime(now + this.windowMs);
|
|
462
|
+
return client;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Retrieves or creates a client, given a key. Also ensures that the client being
|
|
466
|
+
* returned is in the `current` map.
|
|
467
|
+
*
|
|
468
|
+
* @param key {string} - The key under which the client is (or is to be) stored.
|
|
469
|
+
*
|
|
470
|
+
* @returns {Client} - The requested client.
|
|
471
|
+
*/
|
|
472
|
+
getClient(key) {
|
|
473
|
+
if (this.current.has(key))
|
|
474
|
+
return this.current.get(key);
|
|
475
|
+
let client;
|
|
476
|
+
if (this.previous.has(key)) {
|
|
477
|
+
client = this.previous.get(key);
|
|
478
|
+
this.previous.delete(key);
|
|
479
|
+
} else {
|
|
480
|
+
client = { totalHits: 0, resetTime: /* @__PURE__ */ new Date() };
|
|
481
|
+
this.resetClient(client);
|
|
482
|
+
}
|
|
483
|
+
this.current.set(key, client);
|
|
484
|
+
return client;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Move current clients to previous, create a new map for current.
|
|
488
|
+
*
|
|
489
|
+
* This function is called every `windowMs`.
|
|
490
|
+
*/
|
|
491
|
+
clearExpired() {
|
|
492
|
+
this.previous = this.current;
|
|
493
|
+
this.current = /* @__PURE__ */ new Map();
|
|
454
494
|
}
|
|
455
495
|
};
|
|
456
496
|
|
|
@@ -497,10 +537,10 @@ var promisifyStore = (passedStore) => {
|
|
|
497
537
|
return new PromisifiedStore();
|
|
498
538
|
};
|
|
499
539
|
var getOptionsFromConfig = (config) => {
|
|
500
|
-
const { validations, ...directlyPassableEntries } = config;
|
|
540
|
+
const { validations: validations2, ...directlyPassableEntries } = config;
|
|
501
541
|
return {
|
|
502
542
|
...directlyPassableEntries,
|
|
503
|
-
validate:
|
|
543
|
+
validate: validations2.enabled
|
|
504
544
|
};
|
|
505
545
|
};
|
|
506
546
|
var omitUndefinedOptions = (passedOptions) => {
|
|
@@ -514,32 +554,33 @@ var omitUndefinedOptions = (passedOptions) => {
|
|
|
514
554
|
return omittedOptions;
|
|
515
555
|
};
|
|
516
556
|
var parseOptions = (passedOptions) => {
|
|
517
|
-
var _a, _b, _c, _d;
|
|
518
557
|
const notUndefinedOptions = omitUndefinedOptions(passedOptions);
|
|
519
|
-
const
|
|
520
|
-
|
|
558
|
+
const validations2 = getValidations(notUndefinedOptions?.validate ?? true);
|
|
559
|
+
validations2.validationsConfig();
|
|
560
|
+
validations2.draftPolliHeaders(
|
|
561
|
+
// @ts-expect-error see the note above.
|
|
521
562
|
notUndefinedOptions.draft_polli_ratelimit_headers
|
|
522
563
|
);
|
|
523
|
-
|
|
524
|
-
let standardHeaders =
|
|
525
|
-
if (standardHeaders === true
|
|
564
|
+
validations2.onLimitReached(notUndefinedOptions.onLimitReached);
|
|
565
|
+
let standardHeaders = notUndefinedOptions.standardHeaders ?? false;
|
|
566
|
+
if (standardHeaders === true)
|
|
526
567
|
standardHeaders = "draft-6";
|
|
527
|
-
}
|
|
528
568
|
const config = {
|
|
529
569
|
windowMs: 60 * 1e3,
|
|
530
|
-
|
|
570
|
+
limit: passedOptions.max ?? 5,
|
|
571
|
+
// `max` is deprecated, but support it anyways.
|
|
531
572
|
message: "Too many requests, please try again later.",
|
|
532
573
|
statusCode: 429,
|
|
533
|
-
legacyHeaders:
|
|
574
|
+
legacyHeaders: passedOptions.headers ?? true,
|
|
534
575
|
requestPropertyName: "rateLimit",
|
|
535
576
|
skipFailedRequests: false,
|
|
536
577
|
skipSuccessfulRequests: false,
|
|
537
578
|
requestWasSuccessful: (_request, response) => response.statusCode < 400,
|
|
538
579
|
skip: (_request, _response) => false,
|
|
539
580
|
keyGenerator(request, _response) {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
581
|
+
validations2.ip(request.ip);
|
|
582
|
+
validations2.trustProxy(request);
|
|
583
|
+
validations2.xForwardedForHeader(request);
|
|
543
584
|
return request.ip;
|
|
544
585
|
},
|
|
545
586
|
async handler(request, response, _next, _optionsUsed) {
|
|
@@ -552,17 +593,15 @@ var parseOptions = (passedOptions) => {
|
|
|
552
593
|
response.send(message);
|
|
553
594
|
}
|
|
554
595
|
},
|
|
555
|
-
onLimitReached(_request, _response, _optionsUsed) {
|
|
556
|
-
},
|
|
557
596
|
// Allow the default options to be overriden by the options passed to the middleware.
|
|
558
597
|
...notUndefinedOptions,
|
|
559
598
|
// `standardHeaders` is resolved into a draft version above, use that.
|
|
560
599
|
standardHeaders,
|
|
561
600
|
// Note that this field is declared after the user's options are spread in,
|
|
562
601
|
// so that this field doesn't get overriden with an un-promisified store!
|
|
563
|
-
store: promisifyStore(
|
|
602
|
+
store: promisifyStore(notUndefinedOptions.store ?? new MemoryStore()),
|
|
564
603
|
// Print an error to the console if a few known misconfigurations are detected.
|
|
565
|
-
validations
|
|
604
|
+
validations: validations2
|
|
566
605
|
};
|
|
567
606
|
if (typeof config.store.increment !== "function" || typeof config.store.decrement !== "function" || typeof config.store.resetKey !== "function" || config.store.resetAll !== void 0 && typeof config.store.resetAll !== "function" || config.store.init !== void 0 && typeof config.store.init !== "function") {
|
|
568
607
|
throw new TypeError(
|
|
@@ -579,8 +618,7 @@ var handleAsyncErrors = (fn) => async (request, response, next) => {
|
|
|
579
618
|
}
|
|
580
619
|
};
|
|
581
620
|
var rateLimit = (passedOptions) => {
|
|
582
|
-
|
|
583
|
-
const config = parseOptions(passedOptions != null ? passedOptions : {});
|
|
621
|
+
const config = parseOptions(passedOptions ?? {});
|
|
584
622
|
const options = getOptionsFromConfig(config);
|
|
585
623
|
if (typeof config.store.init === "function")
|
|
586
624
|
config.store.init(options);
|
|
@@ -596,15 +634,20 @@ var rateLimit = (passedOptions) => {
|
|
|
596
634
|
const { totalHits, resetTime } = await config.store.increment(key);
|
|
597
635
|
config.validations.positiveHits(totalHits);
|
|
598
636
|
config.validations.singleCount(request, config.store, key);
|
|
599
|
-
const
|
|
600
|
-
const
|
|
601
|
-
config.validations.
|
|
637
|
+
const retrieveLimit = typeof config.limit === "function" ? config.limit(request, response) : config.limit;
|
|
638
|
+
const limit = await retrieveLimit;
|
|
639
|
+
config.validations.limit(limit);
|
|
602
640
|
const info = {
|
|
603
|
-
limit
|
|
604
|
-
|
|
605
|
-
remaining: Math.max(
|
|
641
|
+
limit,
|
|
642
|
+
used: totalHits,
|
|
643
|
+
remaining: Math.max(limit - totalHits, 0),
|
|
606
644
|
resetTime
|
|
607
645
|
};
|
|
646
|
+
Object.defineProperty(info, "current", {
|
|
647
|
+
configurable: false,
|
|
648
|
+
enumerable: false,
|
|
649
|
+
value: totalHits
|
|
650
|
+
});
|
|
608
651
|
augmentedRequest[config.requestPropertyName] = info;
|
|
609
652
|
if (config.legacyHeaders && !response.headersSent) {
|
|
610
653
|
setLegacyHeaders(response, info);
|
|
@@ -645,11 +688,8 @@ var rateLimit = (passedOptions) => {
|
|
|
645
688
|
});
|
|
646
689
|
}
|
|
647
690
|
}
|
|
648
|
-
if (maxHits && totalHits === maxHits + 1) {
|
|
649
|
-
config.onLimitReached(request, response, options);
|
|
650
|
-
}
|
|
651
691
|
config.validations.disable();
|
|
652
|
-
if (
|
|
692
|
+
if (totalHits > limit) {
|
|
653
693
|
if (config.legacyHeaders || config.standardHeaders) {
|
|
654
694
|
setRetryAfterHeader(response, info, config.windowMs);
|
|
655
695
|
}
|
|
@@ -660,7 +700,7 @@ var rateLimit = (passedOptions) => {
|
|
|
660
700
|
}
|
|
661
701
|
);
|
|
662
702
|
middleware.resetKey = config.store.resetKey.bind(config.store);
|
|
663
|
-
middleware.getKey =
|
|
703
|
+
middleware.getKey = config.store.get?.bind(
|
|
664
704
|
config.store
|
|
665
705
|
);
|
|
666
706
|
return middleware;
|