express-rate-limit 6.7.1 → 6.8.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 CHANGED
@@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to
7
7
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [6.8.0](https://github.com/express-rate-limit/express-rate-limit/releases/tag/v6.8.0)
10
+
11
+ ### Changed
12
+
13
+ - Added a set of validation checks to execute on the first request. (See
14
+ [#358](https://github.com/express-rate-limit/express-rate-limit/issues/358))
15
+
9
16
  ## [6.7.1](https://github.com/express-rate-limit/express-rate-limit/releases/tag/v6.7.1)
10
17
 
11
18
  ### Fixed
package/dist/index.cjs CHANGED
@@ -31,6 +31,116 @@ __export(source_exports, {
31
31
  });
32
32
  module.exports = __toCommonJS(source_exports);
33
33
 
34
+ // source/validations.ts
35
+ var import_node_net = require("net");
36
+ var ValidationError = class extends Error {
37
+ /**
38
+ * The code must be a string, in snake case and all capital, that starts with
39
+ * the substring `ERR_ERL_`.
40
+ *
41
+ * The message must be a string, starting with a lowercase character,
42
+ * describing the issue in detail.
43
+ */
44
+ constructor(code, message) {
45
+ super(
46
+ `express-rate-limit: ${code} - ${message} See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#${code.toLowerCase()} for more information on this error.`
47
+ );
48
+ __publicField(this, "name");
49
+ __publicField(this, "code");
50
+ this.name = this.constructor.name;
51
+ this.code = code;
52
+ this.message = message;
53
+ }
54
+ };
55
+ var Validations = class {
56
+ constructor(enabled) {
57
+ // eslint-disable-next-line @typescript-eslint/parameter-properties
58
+ __publicField(this, "enabled");
59
+ this.enabled = enabled;
60
+ }
61
+ enable() {
62
+ this.enabled = true;
63
+ }
64
+ disable() {
65
+ this.enabled = false;
66
+ }
67
+ /**
68
+ * Checks whether the IP address is valid, and that it does not have a port
69
+ * number in it.
70
+ *
71
+ * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_invalid_ip_address.
72
+ *
73
+ * @param ip {string | undefined} - The IP address provided by Express as request.ip.
74
+ *
75
+ * @returns {void}
76
+ */
77
+ ip(ip) {
78
+ this.wrap(() => {
79
+ if (ip === void 0) {
80
+ throw new ValidationError(
81
+ "ERR_ERL_UNDEFINED_IP_ADDRESS",
82
+ `An undefined 'request.ip' was detected. This might indicate a misconfiguration or the connection being destroyed prematurely.`
83
+ );
84
+ }
85
+ if (!(0, import_node_net.isIP)(ip)) {
86
+ throw new ValidationError(
87
+ "ERR_ERL_INVALID_IP_ADDRESS",
88
+ `An invalid 'request.ip' (${ip}) was detected. Consider passing a custom 'keyGenerator' function to the rate limiter.`
89
+ );
90
+ }
91
+ });
92
+ }
93
+ /**
94
+ * Makes sure the trust proxy setting is not set to `true`.
95
+ *
96
+ * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_permissive_trust_proxy.
97
+ *
98
+ * @param request {Request} - The Express request object.
99
+ *
100
+ * @returns {void}
101
+ */
102
+ trustProxy(request) {
103
+ this.wrap(() => {
104
+ if (request.app.get("trust proxy") === true) {
105
+ throw new ValidationError(
106
+ "ERR_ERL_PERMISSIVE_TRUST_PROXY",
107
+ `The Express 'trust proxy' setting is true, which allows anyone to trivially bypass IP-based rate limiting.`
108
+ );
109
+ }
110
+ });
111
+ }
112
+ /**
113
+ * Makes sure the trust proxy setting is set in case the `X-Forwarded-For`
114
+ * header is present.
115
+ *
116
+ * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_unset_trust_proxy.
117
+ *
118
+ * @param request {Request} - The Express request object.
119
+ *
120
+ * @returns {void}
121
+ */
122
+ xForwardedForHeader(request) {
123
+ this.wrap(() => {
124
+ if (request.headers["x-forwarded-for"] && request.app.get("trust proxy") === false) {
125
+ throw new ValidationError(
126
+ "ERR_ERL_UNEXPECTED_X_FORWARDED_FOR",
127
+ `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.`
128
+ );
129
+ }
130
+ });
131
+ }
132
+ wrap(validation) {
133
+ if (!this.enabled) {
134
+ return;
135
+ }
136
+ try {
137
+ validation.call(this);
138
+ } catch (error) {
139
+ console.error(error);
140
+ }
141
+ }
142
+ };
143
+
34
144
  // source/memory-store.ts
35
145
  var calculateNextResetTime = (windowMs) => {
36
146
  const resetTime = /* @__PURE__ */ new Date();
@@ -168,27 +278,43 @@ var promisifyStore = (passedStore) => {
168
278
  }
169
279
  return new PromisifiedStore();
170
280
  };
281
+ var getOptionsFromConfig = (config) => {
282
+ const { validations, ...directlyPassableEntries } = config;
283
+ return {
284
+ ...directlyPassableEntries,
285
+ validate: validations.enabled
286
+ };
287
+ };
288
+ var omitUndefinedOptions = (passedOptions) => {
289
+ const omittedOptions = {};
290
+ for (const k of Object.keys(passedOptions)) {
291
+ const key = k;
292
+ if (passedOptions[key] !== void 0) {
293
+ omittedOptions[key] = passedOptions[key];
294
+ }
295
+ }
296
+ return omittedOptions;
297
+ };
171
298
  var parseOptions = (passedOptions) => {
172
- var _a, _b, _c;
299
+ var _a, _b, _c, _d;
173
300
  const notUndefinedOptions = omitUndefinedOptions(passedOptions);
301
+ const validations = new Validations((_a = notUndefinedOptions == null ? void 0 : notUndefinedOptions.validate) != null ? _a : true);
174
302
  const config = {
175
303
  windowMs: 60 * 1e3,
176
304
  max: 5,
177
305
  message: "Too many requests, please try again later.",
178
306
  statusCode: 429,
179
- legacyHeaders: (_a = passedOptions.headers) != null ? _a : true,
180
- standardHeaders: (_b = passedOptions.draft_polli_ratelimit_headers) != null ? _b : false,
307
+ legacyHeaders: (_b = passedOptions.headers) != null ? _b : true,
308
+ standardHeaders: (_c = passedOptions.draft_polli_ratelimit_headers) != null ? _c : false,
181
309
  requestPropertyName: "rateLimit",
182
310
  skipFailedRequests: false,
183
311
  skipSuccessfulRequests: false,
184
312
  requestWasSuccessful: (_request, response) => response.statusCode < 400,
185
313
  skip: (_request, _response) => false,
186
314
  keyGenerator(request, _response) {
187
- if (!request.ip) {
188
- console.error(
189
- "WARN | `express-rate-limit` | `request.ip` is undefined. You can avoid this by providing a custom `keyGenerator` function, but it may be indicative of a larger issue."
190
- );
191
- }
315
+ validations.ip(request.ip);
316
+ validations.trustProxy(request);
317
+ validations.xForwardedForHeader(request);
192
318
  return request.ip;
193
319
  },
194
320
  async handler(request, response, _next, _optionsUsed) {
@@ -207,7 +333,9 @@ var parseOptions = (passedOptions) => {
207
333
  ...notUndefinedOptions,
208
334
  // Note that this field is declared after the user's options are spread in,
209
335
  // so that this field doesn't get overriden with an un-promisified store!
210
- store: promisifyStore((_c = notUndefinedOptions.store) != null ? _c : new MemoryStore())
336
+ store: promisifyStore((_d = notUndefinedOptions.store) != null ? _d : new MemoryStore()),
337
+ // Print an error to the console if a few known misconfigurations are detected.
338
+ validations
211
339
  };
212
340
  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") {
213
341
  throw new TypeError(
@@ -224,32 +352,33 @@ var handleAsyncErrors = (fn) => async (request, response, next) => {
224
352
  }
225
353
  };
226
354
  var rateLimit = (passedOptions) => {
227
- const options = parseOptions(passedOptions != null ? passedOptions : {});
228
- if (typeof options.store.init === "function")
229
- options.store.init(options);
355
+ const config = parseOptions(passedOptions != null ? passedOptions : {});
356
+ const options = getOptionsFromConfig(config);
357
+ if (typeof config.store.init === "function")
358
+ config.store.init(options);
230
359
  const middleware = handleAsyncErrors(
231
360
  async (request, response, next) => {
232
- const skip = await options.skip(request, response);
361
+ const skip = await config.skip(request, response);
233
362
  if (skip) {
234
363
  next();
235
364
  return;
236
365
  }
237
366
  const augmentedRequest = request;
238
- const key = await options.keyGenerator(request, response);
239
- const { totalHits, resetTime } = await options.store.increment(key);
240
- const retrieveQuota = typeof options.max === "function" ? options.max(request, response) : options.max;
367
+ const key = await config.keyGenerator(request, response);
368
+ const { totalHits, resetTime } = await config.store.increment(key);
369
+ const retrieveQuota = typeof config.max === "function" ? config.max(request, response) : config.max;
241
370
  const maxHits = await retrieveQuota;
242
- augmentedRequest[options.requestPropertyName] = {
371
+ augmentedRequest[config.requestPropertyName] = {
243
372
  limit: maxHits,
244
373
  current: totalHits,
245
374
  remaining: Math.max(maxHits - totalHits, 0),
246
375
  resetTime
247
376
  };
248
- if (options.legacyHeaders && !response.headersSent) {
377
+ if (config.legacyHeaders && !response.headersSent) {
249
378
  response.setHeader("X-RateLimit-Limit", maxHits);
250
379
  response.setHeader(
251
380
  "X-RateLimit-Remaining",
252
- augmentedRequest[options.requestPropertyName].remaining
381
+ augmentedRequest[config.requestPropertyName].remaining
253
382
  );
254
383
  if (resetTime instanceof Date) {
255
384
  response.setHeader("Date", (/* @__PURE__ */ new Date()).toUTCString());
@@ -259,11 +388,11 @@ var rateLimit = (passedOptions) => {
259
388
  );
260
389
  }
261
390
  }
262
- if (options.standardHeaders && !response.headersSent) {
391
+ if (config.standardHeaders && !response.headersSent) {
263
392
  response.setHeader("RateLimit-Limit", maxHits);
264
393
  response.setHeader(
265
394
  "RateLimit-Remaining",
266
- augmentedRequest[options.requestPropertyName].remaining
395
+ augmentedRequest[config.requestPropertyName].remaining
267
396
  );
268
397
  if (resetTime) {
269
398
  const deltaSeconds = Math.ceil(
@@ -272,17 +401,17 @@ var rateLimit = (passedOptions) => {
272
401
  response.setHeader("RateLimit-Reset", Math.max(0, deltaSeconds));
273
402
  }
274
403
  }
275
- if (options.skipFailedRequests || options.skipSuccessfulRequests) {
404
+ if (config.skipFailedRequests || config.skipSuccessfulRequests) {
276
405
  let decremented = false;
277
406
  const decrementKey = async () => {
278
407
  if (!decremented) {
279
- await options.store.decrement(key);
408
+ await config.store.decrement(key);
280
409
  decremented = true;
281
410
  }
282
411
  };
283
- if (options.skipFailedRequests) {
412
+ if (config.skipFailedRequests) {
284
413
  response.on("finish", async () => {
285
- if (!options.requestWasSuccessful(request, response))
414
+ if (!config.requestWasSuccessful(request, response))
286
415
  await decrementKey();
287
416
  });
288
417
  response.on("close", async () => {
@@ -293,38 +422,34 @@ var rateLimit = (passedOptions) => {
293
422
  await decrementKey();
294
423
  });
295
424
  }
296
- if (options.skipSuccessfulRequests) {
425
+ if (config.skipSuccessfulRequests) {
297
426
  response.on("finish", async () => {
298
- if (options.requestWasSuccessful(request, response))
427
+ if (config.requestWasSuccessful(request, response))
299
428
  await decrementKey();
300
429
  });
301
430
  }
302
431
  }
303
432
  if (maxHits && totalHits === maxHits + 1) {
304
- options.onLimitReached(request, response, options);
433
+ config.onLimitReached(request, response, options);
305
434
  }
435
+ config.validations.disable();
306
436
  if (maxHits && totalHits > maxHits) {
307
- if ((options.legacyHeaders || options.standardHeaders) && !response.headersSent) {
308
- response.setHeader("Retry-After", Math.ceil(options.windowMs / 1e3));
437
+ if ((config.legacyHeaders || config.standardHeaders) && !response.headersSent) {
438
+ response.setHeader("Retry-After", Math.ceil(config.windowMs / 1e3));
309
439
  }
310
- options.handler(request, response, next, options);
440
+ config.handler(request, response, next, options);
311
441
  return;
312
442
  }
313
443
  next();
314
444
  }
315
445
  );
316
- middleware.resetKey = options.store.resetKey.bind(options.store);
446
+ middleware.resetKey = config.store.resetKey.bind(config.store);
317
447
  return middleware;
318
448
  };
319
- var omitUndefinedOptions = (passedOptions) => {
320
- const omittedOptions = {};
321
- for (const k of Object.keys(passedOptions)) {
322
- const key = k;
323
- if (passedOptions[key] !== void 0) {
324
- omittedOptions[key] = passedOptions[key];
325
- }
326
- }
327
- return omittedOptions;
328
- };
329
449
  var lib_default = rateLimit;
450
+ // Annotate the CommonJS export names for ESM import in node:
451
+ 0 && (module.exports = {
452
+ MemoryStore,
453
+ rateLimit
454
+ });
330
455
  module.exports = rateLimit; module.exports.default = rateLimit; module.exports.rateLimit = rateLimit; module.exports.MemoryStore = MemoryStore;
package/dist/index.d.cts CHANGED
@@ -140,7 +140,7 @@ export type Options = {
140
140
  *
141
141
  * Defaults to `60000` ms (= 1 minute).
142
142
  */
143
- readonly windowMs: number;
143
+ windowMs: number;
144
144
  /**
145
145
  * The maximum number of connections to allow during the `window` before
146
146
  * rate limiting the client.
@@ -150,65 +150,65 @@ export type Options = {
150
150
  *
151
151
  * Defaults to `5`.
152
152
  */
153
- readonly max: number | ValueDeterminingMiddleware<number>;
153
+ max: number | ValueDeterminingMiddleware<number>;
154
154
  /**
155
155
  * The response body to send back when a client is rate limited.
156
156
  *
157
157
  * Defaults to `'Too many requests, please try again later.'`
158
158
  */
159
- readonly message: any | ValueDeterminingMiddleware<any>;
159
+ message: any | ValueDeterminingMiddleware<any>;
160
160
  /**
161
161
  * The HTTP status code to send back when a client is rate limited.
162
162
  *
163
163
  * Defaults to `HTTP 429 Too Many Requests` (RFC 6585).
164
164
  */
165
- readonly statusCode: number;
165
+ statusCode: number;
166
166
  /**
167
167
  * Whether to send `X-RateLimit-*` headers with the rate limit and the number
168
168
  * of requests.
169
169
  *
170
170
  * Defaults to `true` (for backward compatibility).
171
171
  */
172
- readonly legacyHeaders: boolean;
172
+ legacyHeaders: boolean;
173
173
  /**
174
174
  * Whether to enable support for the standardized rate limit headers (`RateLimit-*`).
175
175
  *
176
176
  * Defaults to `false` (for backward compatibility, but its use is recommended).
177
177
  */
178
- readonly standardHeaders: boolean;
178
+ standardHeaders: boolean;
179
179
  /**
180
180
  * The name of the property on the request object to store the rate limit info.
181
181
  *
182
182
  * Defaults to `rateLimit`.
183
183
  */
184
- readonly requestPropertyName: string;
184
+ requestPropertyName: string;
185
185
  /**
186
186
  * If `true`, the library will (by default) skip all requests that have a 4XX
187
187
  * or 5XX status.
188
188
  *
189
189
  * Defaults to `false`.
190
190
  */
191
- readonly skipFailedRequests: boolean;
191
+ skipFailedRequests: boolean;
192
192
  /**
193
193
  * If `true`, the library will (by default) skip all requests that have a
194
194
  * status code less than 400.
195
195
  *
196
196
  * Defaults to `false`.
197
197
  */
198
- readonly skipSuccessfulRequests: boolean;
198
+ skipSuccessfulRequests: boolean;
199
199
  /**
200
200
  * Method to generate custom identifiers for clients.
201
201
  *
202
202
  * By default, the client's IP address is used.
203
203
  */
204
- readonly keyGenerator: ValueDeterminingMiddleware<string>;
204
+ keyGenerator: ValueDeterminingMiddleware<string>;
205
205
  /**
206
206
  * Express request handler that sends back a response when a client is
207
207
  * rate-limited.
208
208
  *
209
209
  * By default, sends back the `statusCode` and `message` set via the options.
210
210
  */
211
- readonly handler: RateLimitExceededEventHandler;
211
+ handler: RateLimitExceededEventHandler;
212
212
  /**
213
213
  * Express request handler that sends back a response when a client has
214
214
  * reached their rate limit, and will be rate limited on their next request.
@@ -216,14 +216,14 @@ export type Options = {
216
216
  * @deprecated 6.x - Please use a custom `handler` that checks the number of
217
217
  * hits instead.
218
218
  */
219
- readonly onLimitReached: RateLimitReachedEventHandler;
219
+ onLimitReached: RateLimitReachedEventHandler;
220
220
  /**
221
221
  * Method (in the form of middleware) to determine whether or not this request
222
222
  * counts towards a client's quota.
223
223
  *
224
224
  * By default, skips no requests.
225
225
  */
226
- readonly skip: ValueDeterminingMiddleware<boolean>;
226
+ skip: ValueDeterminingMiddleware<boolean>;
227
227
  /**
228
228
  * Method to determine whether or not the request counts as 'succesful'. Used
229
229
  * when either `skipSuccessfulRequests` or `skipFailedRequests` is set to true.
@@ -231,13 +231,17 @@ export type Options = {
231
231
  * By default, requests with a response status code less than 400 are considered
232
232
  * successful.
233
233
  */
234
- readonly requestWasSuccessful: ValueDeterminingMiddleware<boolean>;
234
+ requestWasSuccessful: ValueDeterminingMiddleware<boolean>;
235
235
  /**
236
236
  * The `Store` to use to store the hit count for each client.
237
237
  *
238
238
  * By default, the built-in `MemoryStore` will be used.
239
239
  */
240
240
  store: Store | LegacyStore;
241
+ /**
242
+ * Whether or not the validation checks should run.
243
+ */
244
+ validate: boolean;
241
245
  /**
242
246
  * Whether to send `X-RateLimit-*` headers with the rate limit and the number
243
247
  * of requests.
@@ -265,10 +269,10 @@ export type AugmentedRequest = Request & {
265
269
  * Express request object.
266
270
  */
267
271
  export type RateLimitInfo = {
268
- readonly limit: number;
269
- readonly current: number;
270
- readonly remaining: number;
271
- readonly resetTime: Date | undefined;
272
+ limit: number;
273
+ current: number;
274
+ remaining: number;
275
+ resetTime: Date | undefined;
272
276
  };
273
277
  /**
274
278
  *
package/dist/index.d.mts CHANGED
@@ -140,7 +140,7 @@ export type Options = {
140
140
  *
141
141
  * Defaults to `60000` ms (= 1 minute).
142
142
  */
143
- readonly windowMs: number;
143
+ windowMs: number;
144
144
  /**
145
145
  * The maximum number of connections to allow during the `window` before
146
146
  * rate limiting the client.
@@ -150,65 +150,65 @@ export type Options = {
150
150
  *
151
151
  * Defaults to `5`.
152
152
  */
153
- readonly max: number | ValueDeterminingMiddleware<number>;
153
+ max: number | ValueDeterminingMiddleware<number>;
154
154
  /**
155
155
  * The response body to send back when a client is rate limited.
156
156
  *
157
157
  * Defaults to `'Too many requests, please try again later.'`
158
158
  */
159
- readonly message: any | ValueDeterminingMiddleware<any>;
159
+ message: any | ValueDeterminingMiddleware<any>;
160
160
  /**
161
161
  * The HTTP status code to send back when a client is rate limited.
162
162
  *
163
163
  * Defaults to `HTTP 429 Too Many Requests` (RFC 6585).
164
164
  */
165
- readonly statusCode: number;
165
+ statusCode: number;
166
166
  /**
167
167
  * Whether to send `X-RateLimit-*` headers with the rate limit and the number
168
168
  * of requests.
169
169
  *
170
170
  * Defaults to `true` (for backward compatibility).
171
171
  */
172
- readonly legacyHeaders: boolean;
172
+ legacyHeaders: boolean;
173
173
  /**
174
174
  * Whether to enable support for the standardized rate limit headers (`RateLimit-*`).
175
175
  *
176
176
  * Defaults to `false` (for backward compatibility, but its use is recommended).
177
177
  */
178
- readonly standardHeaders: boolean;
178
+ standardHeaders: boolean;
179
179
  /**
180
180
  * The name of the property on the request object to store the rate limit info.
181
181
  *
182
182
  * Defaults to `rateLimit`.
183
183
  */
184
- readonly requestPropertyName: string;
184
+ requestPropertyName: string;
185
185
  /**
186
186
  * If `true`, the library will (by default) skip all requests that have a 4XX
187
187
  * or 5XX status.
188
188
  *
189
189
  * Defaults to `false`.
190
190
  */
191
- readonly skipFailedRequests: boolean;
191
+ skipFailedRequests: boolean;
192
192
  /**
193
193
  * If `true`, the library will (by default) skip all requests that have a
194
194
  * status code less than 400.
195
195
  *
196
196
  * Defaults to `false`.
197
197
  */
198
- readonly skipSuccessfulRequests: boolean;
198
+ skipSuccessfulRequests: boolean;
199
199
  /**
200
200
  * Method to generate custom identifiers for clients.
201
201
  *
202
202
  * By default, the client's IP address is used.
203
203
  */
204
- readonly keyGenerator: ValueDeterminingMiddleware<string>;
204
+ keyGenerator: ValueDeterminingMiddleware<string>;
205
205
  /**
206
206
  * Express request handler that sends back a response when a client is
207
207
  * rate-limited.
208
208
  *
209
209
  * By default, sends back the `statusCode` and `message` set via the options.
210
210
  */
211
- readonly handler: RateLimitExceededEventHandler;
211
+ handler: RateLimitExceededEventHandler;
212
212
  /**
213
213
  * Express request handler that sends back a response when a client has
214
214
  * reached their rate limit, and will be rate limited on their next request.
@@ -216,14 +216,14 @@ export type Options = {
216
216
  * @deprecated 6.x - Please use a custom `handler` that checks the number of
217
217
  * hits instead.
218
218
  */
219
- readonly onLimitReached: RateLimitReachedEventHandler;
219
+ onLimitReached: RateLimitReachedEventHandler;
220
220
  /**
221
221
  * Method (in the form of middleware) to determine whether or not this request
222
222
  * counts towards a client's quota.
223
223
  *
224
224
  * By default, skips no requests.
225
225
  */
226
- readonly skip: ValueDeterminingMiddleware<boolean>;
226
+ skip: ValueDeterminingMiddleware<boolean>;
227
227
  /**
228
228
  * Method to determine whether or not the request counts as 'succesful'. Used
229
229
  * when either `skipSuccessfulRequests` or `skipFailedRequests` is set to true.
@@ -231,13 +231,17 @@ export type Options = {
231
231
  * By default, requests with a response status code less than 400 are considered
232
232
  * successful.
233
233
  */
234
- readonly requestWasSuccessful: ValueDeterminingMiddleware<boolean>;
234
+ requestWasSuccessful: ValueDeterminingMiddleware<boolean>;
235
235
  /**
236
236
  * The `Store` to use to store the hit count for each client.
237
237
  *
238
238
  * By default, the built-in `MemoryStore` will be used.
239
239
  */
240
240
  store: Store | LegacyStore;
241
+ /**
242
+ * Whether or not the validation checks should run.
243
+ */
244
+ validate: boolean;
241
245
  /**
242
246
  * Whether to send `X-RateLimit-*` headers with the rate limit and the number
243
247
  * of requests.
@@ -265,10 +269,10 @@ export type AugmentedRequest = Request & {
265
269
  * Express request object.
266
270
  */
267
271
  export type RateLimitInfo = {
268
- readonly limit: number;
269
- readonly current: number;
270
- readonly remaining: number;
271
- readonly resetTime: Date | undefined;
272
+ limit: number;
273
+ current: number;
274
+ remaining: number;
275
+ resetTime: Date | undefined;
272
276
  };
273
277
  /**
274
278
  *
package/dist/index.d.ts CHANGED
@@ -140,7 +140,7 @@ export type Options = {
140
140
  *
141
141
  * Defaults to `60000` ms (= 1 minute).
142
142
  */
143
- readonly windowMs: number;
143
+ windowMs: number;
144
144
  /**
145
145
  * The maximum number of connections to allow during the `window` before
146
146
  * rate limiting the client.
@@ -150,65 +150,65 @@ export type Options = {
150
150
  *
151
151
  * Defaults to `5`.
152
152
  */
153
- readonly max: number | ValueDeterminingMiddleware<number>;
153
+ max: number | ValueDeterminingMiddleware<number>;
154
154
  /**
155
155
  * The response body to send back when a client is rate limited.
156
156
  *
157
157
  * Defaults to `'Too many requests, please try again later.'`
158
158
  */
159
- readonly message: any | ValueDeterminingMiddleware<any>;
159
+ message: any | ValueDeterminingMiddleware<any>;
160
160
  /**
161
161
  * The HTTP status code to send back when a client is rate limited.
162
162
  *
163
163
  * Defaults to `HTTP 429 Too Many Requests` (RFC 6585).
164
164
  */
165
- readonly statusCode: number;
165
+ statusCode: number;
166
166
  /**
167
167
  * Whether to send `X-RateLimit-*` headers with the rate limit and the number
168
168
  * of requests.
169
169
  *
170
170
  * Defaults to `true` (for backward compatibility).
171
171
  */
172
- readonly legacyHeaders: boolean;
172
+ legacyHeaders: boolean;
173
173
  /**
174
174
  * Whether to enable support for the standardized rate limit headers (`RateLimit-*`).
175
175
  *
176
176
  * Defaults to `false` (for backward compatibility, but its use is recommended).
177
177
  */
178
- readonly standardHeaders: boolean;
178
+ standardHeaders: boolean;
179
179
  /**
180
180
  * The name of the property on the request object to store the rate limit info.
181
181
  *
182
182
  * Defaults to `rateLimit`.
183
183
  */
184
- readonly requestPropertyName: string;
184
+ requestPropertyName: string;
185
185
  /**
186
186
  * If `true`, the library will (by default) skip all requests that have a 4XX
187
187
  * or 5XX status.
188
188
  *
189
189
  * Defaults to `false`.
190
190
  */
191
- readonly skipFailedRequests: boolean;
191
+ skipFailedRequests: boolean;
192
192
  /**
193
193
  * If `true`, the library will (by default) skip all requests that have a
194
194
  * status code less than 400.
195
195
  *
196
196
  * Defaults to `false`.
197
197
  */
198
- readonly skipSuccessfulRequests: boolean;
198
+ skipSuccessfulRequests: boolean;
199
199
  /**
200
200
  * Method to generate custom identifiers for clients.
201
201
  *
202
202
  * By default, the client's IP address is used.
203
203
  */
204
- readonly keyGenerator: ValueDeterminingMiddleware<string>;
204
+ keyGenerator: ValueDeterminingMiddleware<string>;
205
205
  /**
206
206
  * Express request handler that sends back a response when a client is
207
207
  * rate-limited.
208
208
  *
209
209
  * By default, sends back the `statusCode` and `message` set via the options.
210
210
  */
211
- readonly handler: RateLimitExceededEventHandler;
211
+ handler: RateLimitExceededEventHandler;
212
212
  /**
213
213
  * Express request handler that sends back a response when a client has
214
214
  * reached their rate limit, and will be rate limited on their next request.
@@ -216,14 +216,14 @@ export type Options = {
216
216
  * @deprecated 6.x - Please use a custom `handler` that checks the number of
217
217
  * hits instead.
218
218
  */
219
- readonly onLimitReached: RateLimitReachedEventHandler;
219
+ onLimitReached: RateLimitReachedEventHandler;
220
220
  /**
221
221
  * Method (in the form of middleware) to determine whether or not this request
222
222
  * counts towards a client's quota.
223
223
  *
224
224
  * By default, skips no requests.
225
225
  */
226
- readonly skip: ValueDeterminingMiddleware<boolean>;
226
+ skip: ValueDeterminingMiddleware<boolean>;
227
227
  /**
228
228
  * Method to determine whether or not the request counts as 'succesful'. Used
229
229
  * when either `skipSuccessfulRequests` or `skipFailedRequests` is set to true.
@@ -231,13 +231,17 @@ export type Options = {
231
231
  * By default, requests with a response status code less than 400 are considered
232
232
  * successful.
233
233
  */
234
- readonly requestWasSuccessful: ValueDeterminingMiddleware<boolean>;
234
+ requestWasSuccessful: ValueDeterminingMiddleware<boolean>;
235
235
  /**
236
236
  * The `Store` to use to store the hit count for each client.
237
237
  *
238
238
  * By default, the built-in `MemoryStore` will be used.
239
239
  */
240
240
  store: Store | LegacyStore;
241
+ /**
242
+ * Whether or not the validation checks should run.
243
+ */
244
+ validate: boolean;
241
245
  /**
242
246
  * Whether to send `X-RateLimit-*` headers with the rate limit and the number
243
247
  * of requests.
@@ -265,10 +269,10 @@ export type AugmentedRequest = Request & {
265
269
  * Express request object.
266
270
  */
267
271
  export type RateLimitInfo = {
268
- readonly limit: number;
269
- readonly current: number;
270
- readonly remaining: number;
271
- readonly resetTime: Date | undefined;
272
+ limit: number;
273
+ current: number;
274
+ remaining: number;
275
+ resetTime: Date | undefined;
272
276
  };
273
277
  /**
274
278
  *
package/dist/index.mjs CHANGED
@@ -5,6 +5,116 @@ var __publicField = (obj, key, value) => {
5
5
  return value;
6
6
  };
7
7
 
8
+ // source/validations.ts
9
+ import { isIP } from "net";
10
+ var ValidationError = class extends Error {
11
+ /**
12
+ * The code must be a string, in snake case and all capital, that starts with
13
+ * the substring `ERR_ERL_`.
14
+ *
15
+ * The message must be a string, starting with a lowercase character,
16
+ * describing the issue in detail.
17
+ */
18
+ constructor(code, message) {
19
+ super(
20
+ `express-rate-limit: ${code} - ${message} See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#${code.toLowerCase()} for more information on this error.`
21
+ );
22
+ __publicField(this, "name");
23
+ __publicField(this, "code");
24
+ this.name = this.constructor.name;
25
+ this.code = code;
26
+ this.message = message;
27
+ }
28
+ };
29
+ var Validations = class {
30
+ constructor(enabled) {
31
+ // eslint-disable-next-line @typescript-eslint/parameter-properties
32
+ __publicField(this, "enabled");
33
+ this.enabled = enabled;
34
+ }
35
+ enable() {
36
+ this.enabled = true;
37
+ }
38
+ disable() {
39
+ this.enabled = false;
40
+ }
41
+ /**
42
+ * Checks whether the IP address is valid, and that it does not have a port
43
+ * number in it.
44
+ *
45
+ * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_invalid_ip_address.
46
+ *
47
+ * @param ip {string | undefined} - The IP address provided by Express as request.ip.
48
+ *
49
+ * @returns {void}
50
+ */
51
+ ip(ip) {
52
+ this.wrap(() => {
53
+ if (ip === void 0) {
54
+ throw new ValidationError(
55
+ "ERR_ERL_UNDEFINED_IP_ADDRESS",
56
+ `An undefined 'request.ip' was detected. This might indicate a misconfiguration or the connection being destroyed prematurely.`
57
+ );
58
+ }
59
+ if (!isIP(ip)) {
60
+ throw new ValidationError(
61
+ "ERR_ERL_INVALID_IP_ADDRESS",
62
+ `An invalid 'request.ip' (${ip}) was detected. Consider passing a custom 'keyGenerator' function to the rate limiter.`
63
+ );
64
+ }
65
+ });
66
+ }
67
+ /**
68
+ * Makes sure the trust proxy setting is not set to `true`.
69
+ *
70
+ * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_permissive_trust_proxy.
71
+ *
72
+ * @param request {Request} - The Express request object.
73
+ *
74
+ * @returns {void}
75
+ */
76
+ trustProxy(request) {
77
+ this.wrap(() => {
78
+ if (request.app.get("trust proxy") === true) {
79
+ throw new ValidationError(
80
+ "ERR_ERL_PERMISSIVE_TRUST_PROXY",
81
+ `The Express 'trust proxy' setting is true, which allows anyone to trivially bypass IP-based rate limiting.`
82
+ );
83
+ }
84
+ });
85
+ }
86
+ /**
87
+ * Makes sure the trust proxy setting is set in case the `X-Forwarded-For`
88
+ * header is present.
89
+ *
90
+ * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_unset_trust_proxy.
91
+ *
92
+ * @param request {Request} - The Express request object.
93
+ *
94
+ * @returns {void}
95
+ */
96
+ xForwardedForHeader(request) {
97
+ this.wrap(() => {
98
+ if (request.headers["x-forwarded-for"] && request.app.get("trust proxy") === false) {
99
+ throw new ValidationError(
100
+ "ERR_ERL_UNEXPECTED_X_FORWARDED_FOR",
101
+ `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.`
102
+ );
103
+ }
104
+ });
105
+ }
106
+ wrap(validation) {
107
+ if (!this.enabled) {
108
+ return;
109
+ }
110
+ try {
111
+ validation.call(this);
112
+ } catch (error) {
113
+ console.error(error);
114
+ }
115
+ }
116
+ };
117
+
8
118
  // source/memory-store.ts
9
119
  var calculateNextResetTime = (windowMs) => {
10
120
  const resetTime = /* @__PURE__ */ new Date();
@@ -142,27 +252,43 @@ var promisifyStore = (passedStore) => {
142
252
  }
143
253
  return new PromisifiedStore();
144
254
  };
255
+ var getOptionsFromConfig = (config) => {
256
+ const { validations, ...directlyPassableEntries } = config;
257
+ return {
258
+ ...directlyPassableEntries,
259
+ validate: validations.enabled
260
+ };
261
+ };
262
+ var omitUndefinedOptions = (passedOptions) => {
263
+ const omittedOptions = {};
264
+ for (const k of Object.keys(passedOptions)) {
265
+ const key = k;
266
+ if (passedOptions[key] !== void 0) {
267
+ omittedOptions[key] = passedOptions[key];
268
+ }
269
+ }
270
+ return omittedOptions;
271
+ };
145
272
  var parseOptions = (passedOptions) => {
146
- var _a, _b, _c;
273
+ var _a, _b, _c, _d;
147
274
  const notUndefinedOptions = omitUndefinedOptions(passedOptions);
275
+ const validations = new Validations((_a = notUndefinedOptions == null ? void 0 : notUndefinedOptions.validate) != null ? _a : true);
148
276
  const config = {
149
277
  windowMs: 60 * 1e3,
150
278
  max: 5,
151
279
  message: "Too many requests, please try again later.",
152
280
  statusCode: 429,
153
- legacyHeaders: (_a = passedOptions.headers) != null ? _a : true,
154
- standardHeaders: (_b = passedOptions.draft_polli_ratelimit_headers) != null ? _b : false,
281
+ legacyHeaders: (_b = passedOptions.headers) != null ? _b : true,
282
+ standardHeaders: (_c = passedOptions.draft_polli_ratelimit_headers) != null ? _c : false,
155
283
  requestPropertyName: "rateLimit",
156
284
  skipFailedRequests: false,
157
285
  skipSuccessfulRequests: false,
158
286
  requestWasSuccessful: (_request, response) => response.statusCode < 400,
159
287
  skip: (_request, _response) => false,
160
288
  keyGenerator(request, _response) {
161
- if (!request.ip) {
162
- console.error(
163
- "WARN | `express-rate-limit` | `request.ip` is undefined. You can avoid this by providing a custom `keyGenerator` function, but it may be indicative of a larger issue."
164
- );
165
- }
289
+ validations.ip(request.ip);
290
+ validations.trustProxy(request);
291
+ validations.xForwardedForHeader(request);
166
292
  return request.ip;
167
293
  },
168
294
  async handler(request, response, _next, _optionsUsed) {
@@ -181,7 +307,9 @@ var parseOptions = (passedOptions) => {
181
307
  ...notUndefinedOptions,
182
308
  // Note that this field is declared after the user's options are spread in,
183
309
  // so that this field doesn't get overriden with an un-promisified store!
184
- store: promisifyStore((_c = notUndefinedOptions.store) != null ? _c : new MemoryStore())
310
+ store: promisifyStore((_d = notUndefinedOptions.store) != null ? _d : new MemoryStore()),
311
+ // Print an error to the console if a few known misconfigurations are detected.
312
+ validations
185
313
  };
186
314
  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") {
187
315
  throw new TypeError(
@@ -198,32 +326,33 @@ var handleAsyncErrors = (fn) => async (request, response, next) => {
198
326
  }
199
327
  };
200
328
  var rateLimit = (passedOptions) => {
201
- const options = parseOptions(passedOptions != null ? passedOptions : {});
202
- if (typeof options.store.init === "function")
203
- options.store.init(options);
329
+ const config = parseOptions(passedOptions != null ? passedOptions : {});
330
+ const options = getOptionsFromConfig(config);
331
+ if (typeof config.store.init === "function")
332
+ config.store.init(options);
204
333
  const middleware = handleAsyncErrors(
205
334
  async (request, response, next) => {
206
- const skip = await options.skip(request, response);
335
+ const skip = await config.skip(request, response);
207
336
  if (skip) {
208
337
  next();
209
338
  return;
210
339
  }
211
340
  const augmentedRequest = request;
212
- const key = await options.keyGenerator(request, response);
213
- const { totalHits, resetTime } = await options.store.increment(key);
214
- const retrieveQuota = typeof options.max === "function" ? options.max(request, response) : options.max;
341
+ const key = await config.keyGenerator(request, response);
342
+ const { totalHits, resetTime } = await config.store.increment(key);
343
+ const retrieveQuota = typeof config.max === "function" ? config.max(request, response) : config.max;
215
344
  const maxHits = await retrieveQuota;
216
- augmentedRequest[options.requestPropertyName] = {
345
+ augmentedRequest[config.requestPropertyName] = {
217
346
  limit: maxHits,
218
347
  current: totalHits,
219
348
  remaining: Math.max(maxHits - totalHits, 0),
220
349
  resetTime
221
350
  };
222
- if (options.legacyHeaders && !response.headersSent) {
351
+ if (config.legacyHeaders && !response.headersSent) {
223
352
  response.setHeader("X-RateLimit-Limit", maxHits);
224
353
  response.setHeader(
225
354
  "X-RateLimit-Remaining",
226
- augmentedRequest[options.requestPropertyName].remaining
355
+ augmentedRequest[config.requestPropertyName].remaining
227
356
  );
228
357
  if (resetTime instanceof Date) {
229
358
  response.setHeader("Date", (/* @__PURE__ */ new Date()).toUTCString());
@@ -233,11 +362,11 @@ var rateLimit = (passedOptions) => {
233
362
  );
234
363
  }
235
364
  }
236
- if (options.standardHeaders && !response.headersSent) {
365
+ if (config.standardHeaders && !response.headersSent) {
237
366
  response.setHeader("RateLimit-Limit", maxHits);
238
367
  response.setHeader(
239
368
  "RateLimit-Remaining",
240
- augmentedRequest[options.requestPropertyName].remaining
369
+ augmentedRequest[config.requestPropertyName].remaining
241
370
  );
242
371
  if (resetTime) {
243
372
  const deltaSeconds = Math.ceil(
@@ -246,17 +375,17 @@ var rateLimit = (passedOptions) => {
246
375
  response.setHeader("RateLimit-Reset", Math.max(0, deltaSeconds));
247
376
  }
248
377
  }
249
- if (options.skipFailedRequests || options.skipSuccessfulRequests) {
378
+ if (config.skipFailedRequests || config.skipSuccessfulRequests) {
250
379
  let decremented = false;
251
380
  const decrementKey = async () => {
252
381
  if (!decremented) {
253
- await options.store.decrement(key);
382
+ await config.store.decrement(key);
254
383
  decremented = true;
255
384
  }
256
385
  };
257
- if (options.skipFailedRequests) {
386
+ if (config.skipFailedRequests) {
258
387
  response.on("finish", async () => {
259
- if (!options.requestWasSuccessful(request, response))
388
+ if (!config.requestWasSuccessful(request, response))
260
389
  await decrementKey();
261
390
  });
262
391
  response.on("close", async () => {
@@ -267,39 +396,30 @@ var rateLimit = (passedOptions) => {
267
396
  await decrementKey();
268
397
  });
269
398
  }
270
- if (options.skipSuccessfulRequests) {
399
+ if (config.skipSuccessfulRequests) {
271
400
  response.on("finish", async () => {
272
- if (options.requestWasSuccessful(request, response))
401
+ if (config.requestWasSuccessful(request, response))
273
402
  await decrementKey();
274
403
  });
275
404
  }
276
405
  }
277
406
  if (maxHits && totalHits === maxHits + 1) {
278
- options.onLimitReached(request, response, options);
407
+ config.onLimitReached(request, response, options);
279
408
  }
409
+ config.validations.disable();
280
410
  if (maxHits && totalHits > maxHits) {
281
- if ((options.legacyHeaders || options.standardHeaders) && !response.headersSent) {
282
- response.setHeader("Retry-After", Math.ceil(options.windowMs / 1e3));
411
+ if ((config.legacyHeaders || config.standardHeaders) && !response.headersSent) {
412
+ response.setHeader("Retry-After", Math.ceil(config.windowMs / 1e3));
283
413
  }
284
- options.handler(request, response, next, options);
414
+ config.handler(request, response, next, options);
285
415
  return;
286
416
  }
287
417
  next();
288
418
  }
289
419
  );
290
- middleware.resetKey = options.store.resetKey.bind(options.store);
420
+ middleware.resetKey = config.store.resetKey.bind(config.store);
291
421
  return middleware;
292
422
  };
293
- var omitUndefinedOptions = (passedOptions) => {
294
- const omittedOptions = {};
295
- for (const k of Object.keys(passedOptions)) {
296
- const key = k;
297
- if (passedOptions[key] !== void 0) {
298
- omittedOptions[key] = passedOptions[key];
299
- }
300
- }
301
- return omittedOptions;
302
- };
303
423
  var lib_default = rateLimit;
304
424
  export {
305
425
  MemoryStore,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "express-rate-limit",
3
- "version": "6.7.1",
3
+ "version": "6.8.0",
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,17 +56,17 @@
56
56
  },
57
57
  "scripts": {
58
58
  "clean": "del-cli dist/ coverage/ *.log *.tmp *.bak *.tgz",
59
- "build:cjs": "esbuild --bundle --target=es2019 --format=cjs --outfile=dist/index.cjs --footer:js=\"module.exports = rateLimit; module.exports.default = rateLimit; module.exports.rateLimit = rateLimit; module.exports.MemoryStore = MemoryStore;\" source/index.ts",
60
- "build:esm": "esbuild --bundle --target=es2019 --format=esm --outfile=dist/index.mjs source/index.ts",
59
+ "build:cjs": "esbuild --platform=node --bundle --target=es2019 --format=cjs --outfile=dist/index.cjs --footer:js=\"module.exports = rateLimit; module.exports.default = rateLimit; module.exports.rateLimit = rateLimit; module.exports.MemoryStore = MemoryStore;\" source/index.ts",
60
+ "build:esm": "esbuild --platform=node --bundle --target=es2019 --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
  "lint:code": "xo --ignore test/external/",
64
64
  "lint:rest": "prettier --ignore-path .gitignore --ignore-unknown --check .",
65
65
  "lint": "run-s lint:*",
66
- "autofix:code": "run-s lint:code --fix",
67
- "autofix:rest": "run-s lint:rest --write .",
66
+ "autofix:code": "npm run lint:code -- --fix",
67
+ "autofix:rest": "npm run lint:rest -- --write .",
68
68
  "autofix": "run-s autofix:*",
69
- "test:lib": "cross-env NODE_OPTIONS=--experimental-vm-modules jest",
69
+ "test:lib": "cross-env NODE_NO_WARNINGS=1 NODE_OPTIONS=--experimental-vm-modules jest",
70
70
  "test:ext": "cd test/external/ && bash run-all-tests",
71
71
  "test": "run-s lint test:*",
72
72
  "pre-commit": "lint-staged",
@@ -107,7 +107,15 @@
107
107
  "index-signature"
108
108
  ],
109
109
  "n/no-unsupported-features/es-syntax": 0
110
- }
110
+ },
111
+ "overrides": [
112
+ {
113
+ "files": "test/library/*.ts",
114
+ "rules": {
115
+ "@typescript-eslint/no-unsafe-argument": 0
116
+ }
117
+ }
118
+ ]
111
119
  },
112
120
  "prettier": {
113
121
  "semi": false,
@@ -119,12 +127,6 @@
119
127
  },
120
128
  "jest": {
121
129
  "preset": "ts-jest/presets/default-esm",
122
- "globals": {
123
- "ts-jest": {
124
- "useESM": true
125
- }
126
- },
127
- "verbose": true,
128
130
  "collectCoverage": true,
129
131
  "collectCoverageFrom": [
130
132
  "source/**/*.ts"
package/readme.md CHANGED
@@ -451,6 +451,21 @@ const limiter = rateLimit({
451
451
  })
452
452
  ```
453
453
 
454
+ ### `validate`
455
+
456
+ > `boolean`
457
+
458
+ When enabled, a set of validation checks are run on the first request to detect
459
+ common misconfigurations with proxies, etc. Prints an error to the console if
460
+ any issue is detected.
461
+
462
+ Automatically disables after the first request is processed.
463
+
464
+ See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes
465
+ for more info.
466
+
467
+ Defaults to true.
468
+
454
469
  ### `store`
455
470
 
456
471
  > `Store`