express-rate-limit 6.7.0 → 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,15 +6,38 @@ 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
+
16
+ ## [6.7.1](https://github.com/express-rate-limit/express-rate-limit/releases/tag/v6.7.1)
17
+
18
+ ### Fixed
19
+
20
+ - Fixed compatibility with TypeScript's TypeScript new `node16` module
21
+ resolution strategy (See
22
+ [#355](https://github.com/express-rate-limit/express-rate-limit/issues/355))
23
+
24
+ ### Changed
25
+
26
+ - Bumped development dependencies.
27
+ - Added `node` 20 to list of versions the CI jobs run on.
28
+
29
+ No functional changes.
30
+
9
31
  ## [6.7.0](https://github.com/express-rate-limit/express-rate-limit/releases/tag/v6.7.0)
10
32
 
11
33
  ### Changed
12
34
 
13
- - Updated links to point to new express-rate-limit organization on GitHub.
14
- - Added advertisement to Readme for project sponsor
35
+ - Updated links to point to the new `express-rate-limit` organization on GitHub.
36
+ - Added advertisement to `readme.md` for project sponsor
15
37
  [Zuplo](https://zuplo.link/express-rate-limit).
16
- - Updated TypeScript version and other dev dependencies
17
- - Changed CI test suite: dropped node.js 12, added node.js 19
38
+ - Updated to `typescript` version 5 and bumped other dependencies.
39
+ - Dropped `node` 12, and added `node` 19 to the list of versions the CI jobs run
40
+ on.
18
41
 
19
42
  No functional changes.
20
43
 
package/dist/index.cjs CHANGED
@@ -3,6 +3,7 @@ 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;
6
7
  var __export = (target, all) => {
7
8
  for (var name in all)
8
9
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -16,6 +17,10 @@ var __copyProps = (to, from, except, desc) => {
16
17
  return to;
17
18
  };
18
19
  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
+ };
19
24
 
20
25
  // source/index.ts
21
26
  var source_exports = {};
@@ -26,13 +31,146 @@ __export(source_exports, {
26
31
  });
27
32
  module.exports = __toCommonJS(source_exports);
28
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
+
29
144
  // source/memory-store.ts
30
145
  var calculateNextResetTime = (windowMs) => {
31
- const resetTime = new Date();
146
+ const resetTime = /* @__PURE__ */ new Date();
32
147
  resetTime.setMilliseconds(resetTime.getMilliseconds() + windowMs);
33
148
  return resetTime;
34
149
  };
35
150
  var MemoryStore = class {
151
+ constructor() {
152
+ /**
153
+ * The duration of time before which all hit counts are reset (in milliseconds).
154
+ */
155
+ __publicField(this, "windowMs");
156
+ /**
157
+ * The map that stores the number of hits for each client in memory.
158
+ */
159
+ __publicField(this, "hits");
160
+ /**
161
+ * The time at which all hit counts will be reset.
162
+ */
163
+ __publicField(this, "resetTime");
164
+ /**
165
+ * Reference to the active timer.
166
+ */
167
+ __publicField(this, "interval");
168
+ }
169
+ /**
170
+ * Method that initializes the store.
171
+ *
172
+ * @param options {Options} - The options used to setup the middleware.
173
+ */
36
174
  init(options) {
37
175
  this.windowMs = options.windowMs;
38
176
  this.resetTime = calculateNextResetTime(this.windowMs);
@@ -43,6 +181,15 @@ var MemoryStore = class {
43
181
  if (this.interval.unref)
44
182
  this.interval.unref();
45
183
  }
184
+ /**
185
+ * Method to increment a client's hit counter.
186
+ *
187
+ * @param key {string} - The identifier for a client.
188
+ *
189
+ * @returns {IncrementResponse} - The number of hits and reset time for that client.
190
+ *
191
+ * @public
192
+ */
46
193
  async increment(key) {
47
194
  var _a;
48
195
  const totalHits = ((_a = this.hits[key]) != null ? _a : 0) + 1;
@@ -52,25 +199,54 @@ var MemoryStore = class {
52
199
  resetTime: this.resetTime
53
200
  };
54
201
  }
202
+ /**
203
+ * Method to decrement a client's hit counter.
204
+ *
205
+ * @param key {string} - The identifier for a client.
206
+ *
207
+ * @public
208
+ */
55
209
  async decrement(key) {
56
210
  const current = this.hits[key];
57
211
  if (current)
58
212
  this.hits[key] = current - 1;
59
213
  }
214
+ /**
215
+ * Method to reset a client's hit counter.
216
+ *
217
+ * @param key {string} - The identifier for a client.
218
+ *
219
+ * @public
220
+ */
60
221
  async resetKey(key) {
61
222
  delete this.hits[key];
62
223
  }
224
+ /**
225
+ * Method to reset everyone's hit counter.
226
+ *
227
+ * @public
228
+ */
63
229
  async resetAll() {
64
230
  this.hits = {};
65
231
  this.resetTime = calculateNextResetTime(this.windowMs);
66
232
  }
233
+ /**
234
+ * Method to stop the timer (if currently running) and prevent any memory
235
+ * leaks.
236
+ *
237
+ * @public
238
+ */
67
239
  shutdown() {
68
240
  clearInterval(this.interval);
69
241
  }
70
242
  };
71
243
 
72
244
  // source/lib.ts
73
- var isLegacyStore = (store) => typeof store.incr === "function" && typeof store.increment !== "function";
245
+ var isLegacyStore = (store) => (
246
+ // Check that `incr` exists but `increment` does not - store authors might want
247
+ // to keep both around for backwards compatibility.
248
+ typeof store.incr === "function" && typeof store.increment !== "function"
249
+ );
74
250
  var promisifyStore = (passedStore) => {
75
251
  if (!isLegacyStore(passedStore)) {
76
252
  return passedStore;
@@ -102,27 +278,43 @@ var promisifyStore = (passedStore) => {
102
278
  }
103
279
  return new PromisifiedStore();
104
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
+ };
105
298
  var parseOptions = (passedOptions) => {
106
- var _a, _b, _c;
299
+ var _a, _b, _c, _d;
107
300
  const notUndefinedOptions = omitUndefinedOptions(passedOptions);
301
+ const validations = new Validations((_a = notUndefinedOptions == null ? void 0 : notUndefinedOptions.validate) != null ? _a : true);
108
302
  const config = {
109
303
  windowMs: 60 * 1e3,
110
304
  max: 5,
111
305
  message: "Too many requests, please try again later.",
112
306
  statusCode: 429,
113
- legacyHeaders: (_a = passedOptions.headers) != null ? _a : true,
114
- 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,
115
309
  requestPropertyName: "rateLimit",
116
310
  skipFailedRequests: false,
117
311
  skipSuccessfulRequests: false,
118
312
  requestWasSuccessful: (_request, response) => response.statusCode < 400,
119
313
  skip: (_request, _response) => false,
120
314
  keyGenerator(request, _response) {
121
- if (!request.ip) {
122
- console.error(
123
- "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."
124
- );
125
- }
315
+ validations.ip(request.ip);
316
+ validations.trustProxy(request);
317
+ validations.xForwardedForHeader(request);
126
318
  return request.ip;
127
319
  },
128
320
  async handler(request, response, _next, _optionsUsed) {
@@ -137,10 +329,15 @@ var parseOptions = (passedOptions) => {
137
329
  },
138
330
  onLimitReached(_request, _response, _optionsUsed) {
139
331
  },
332
+ // Allow the options object to be overriden by the options passed to the middleware.
140
333
  ...notUndefinedOptions,
141
- store: promisifyStore((_c = notUndefinedOptions.store) != null ? _c : new MemoryStore())
334
+ // Note that this field is declared after the user's options are spread in,
335
+ // so that this field doesn't get overriden with an un-promisified store!
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
142
339
  };
143
- if (typeof config.store.increment !== "function" || typeof config.store.decrement !== "function" || typeof config.store.resetKey !== "function" || typeof config.store.resetAll !== "undefined" && typeof config.store.resetAll !== "function" || typeof config.store.init !== "undefined" && typeof config.store.init !== "function") {
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") {
144
341
  throw new TypeError(
145
342
  "An invalid store was passed. Please ensure that the store is a class that implements the `Store` interface."
146
343
  );
@@ -155,46 +352,47 @@ var handleAsyncErrors = (fn) => async (request, response, next) => {
155
352
  }
156
353
  };
157
354
  var rateLimit = (passedOptions) => {
158
- const options = parseOptions(passedOptions != null ? passedOptions : {});
159
- if (typeof options.store.init === "function")
160
- 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);
161
359
  const middleware = handleAsyncErrors(
162
360
  async (request, response, next) => {
163
- const skip = await options.skip(request, response);
361
+ const skip = await config.skip(request, response);
164
362
  if (skip) {
165
363
  next();
166
364
  return;
167
365
  }
168
366
  const augmentedRequest = request;
169
- const key = await options.keyGenerator(request, response);
170
- const { totalHits, resetTime } = await options.store.increment(key);
171
- 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;
172
370
  const maxHits = await retrieveQuota;
173
- augmentedRequest[options.requestPropertyName] = {
371
+ augmentedRequest[config.requestPropertyName] = {
174
372
  limit: maxHits,
175
373
  current: totalHits,
176
374
  remaining: Math.max(maxHits - totalHits, 0),
177
375
  resetTime
178
376
  };
179
- if (options.legacyHeaders && !response.headersSent) {
377
+ if (config.legacyHeaders && !response.headersSent) {
180
378
  response.setHeader("X-RateLimit-Limit", maxHits);
181
379
  response.setHeader(
182
380
  "X-RateLimit-Remaining",
183
- augmentedRequest[options.requestPropertyName].remaining
381
+ augmentedRequest[config.requestPropertyName].remaining
184
382
  );
185
383
  if (resetTime instanceof Date) {
186
- response.setHeader("Date", new Date().toUTCString());
384
+ response.setHeader("Date", (/* @__PURE__ */ new Date()).toUTCString());
187
385
  response.setHeader(
188
386
  "X-RateLimit-Reset",
189
387
  Math.ceil(resetTime.getTime() / 1e3)
190
388
  );
191
389
  }
192
390
  }
193
- if (options.standardHeaders && !response.headersSent) {
391
+ if (config.standardHeaders && !response.headersSent) {
194
392
  response.setHeader("RateLimit-Limit", maxHits);
195
393
  response.setHeader(
196
394
  "RateLimit-Remaining",
197
- augmentedRequest[options.requestPropertyName].remaining
395
+ augmentedRequest[config.requestPropertyName].remaining
198
396
  );
199
397
  if (resetTime) {
200
398
  const deltaSeconds = Math.ceil(
@@ -203,17 +401,17 @@ var rateLimit = (passedOptions) => {
203
401
  response.setHeader("RateLimit-Reset", Math.max(0, deltaSeconds));
204
402
  }
205
403
  }
206
- if (options.skipFailedRequests || options.skipSuccessfulRequests) {
404
+ if (config.skipFailedRequests || config.skipSuccessfulRequests) {
207
405
  let decremented = false;
208
406
  const decrementKey = async () => {
209
407
  if (!decremented) {
210
- await options.store.decrement(key);
408
+ await config.store.decrement(key);
211
409
  decremented = true;
212
410
  }
213
411
  };
214
- if (options.skipFailedRequests) {
412
+ if (config.skipFailedRequests) {
215
413
  response.on("finish", async () => {
216
- if (!options.requestWasSuccessful(request, response))
414
+ if (!config.requestWasSuccessful(request, response))
217
415
  await decrementKey();
218
416
  });
219
417
  response.on("close", async () => {
@@ -224,38 +422,34 @@ var rateLimit = (passedOptions) => {
224
422
  await decrementKey();
225
423
  });
226
424
  }
227
- if (options.skipSuccessfulRequests) {
425
+ if (config.skipSuccessfulRequests) {
228
426
  response.on("finish", async () => {
229
- if (options.requestWasSuccessful(request, response))
427
+ if (config.requestWasSuccessful(request, response))
230
428
  await decrementKey();
231
429
  });
232
430
  }
233
431
  }
234
432
  if (maxHits && totalHits === maxHits + 1) {
235
- options.onLimitReached(request, response, options);
433
+ config.onLimitReached(request, response, options);
236
434
  }
435
+ config.validations.disable();
237
436
  if (maxHits && totalHits > maxHits) {
238
- if ((options.legacyHeaders || options.standardHeaders) && !response.headersSent) {
239
- 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));
240
439
  }
241
- options.handler(request, response, next, options);
440
+ config.handler(request, response, next, options);
242
441
  return;
243
442
  }
244
443
  next();
245
444
  }
246
445
  );
247
- middleware.resetKey = options.store.resetKey.bind(options.store);
446
+ middleware.resetKey = config.store.resetKey.bind(config.store);
248
447
  return middleware;
249
448
  };
250
- var omitUndefinedOptions = (passedOptions) => {
251
- const omittedOptions = {};
252
- for (const k of Object.keys(passedOptions)) {
253
- const key = k;
254
- if (passedOptions[key] !== void 0) {
255
- omittedOptions[key] = passedOptions[key];
256
- }
257
- }
258
- return omittedOptions;
259
- };
260
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
+ });
261
455
  module.exports = rateLimit; module.exports.default = rateLimit; module.exports.rateLimit = rateLimit; module.exports.MemoryStore = MemoryStore;