express-rate-limit 6.8.0 → 6.9.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,11 +6,34 @@ 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)
9
+ ## [6.9.0](https://github.com/express-rate-limit/express-rate-limit/releases/tag/v6.9.0)
10
+
11
+ ### Added
12
+
13
+ - New validaion check for double-counted requests
14
+ - Added help link to each ValidationError, directing users to the appropriate
15
+ wiki page for more info
10
16
 
11
17
  ### Changed
12
18
 
13
- - Added a set of validation checks to execute on the first request. (See
19
+ - Miscaleanous documenation improvements
20
+
21
+ ## [6.8.1](https://github.com/express-rate-limit/express-rate-limit/releases/tag/v6.8.0) & [6.7.2](https://github.com/express-rate-limit/express-rate-limit/releases/tag/v6.8.0)
22
+
23
+ ### Changed
24
+
25
+ - Revert 6.7.1 change that bumped typescript from 5.x to 4.x and
26
+ dts-bundle-generator from 8.x to 7.x (See
27
+ [#360](https://github.com/express-rate-limit/express-rate-limit/issues/360))
28
+
29
+ ## [6.8.0](https://github.com/express-rate-limit/express-rate-limit/releases/tag/v6.8.0)
30
+
31
+ ### Added
32
+
33
+ - Added a set of validation checks that will log an error if failed. See
34
+ https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes for
35
+ a list of potential errors. Can be disabled by setting `validate: false` in
36
+ the configuration. Automatically disables after the first request. (See
14
37
  [#358](https://github.com/express-rate-limit/express-rate-limit/issues/358))
15
38
 
16
39
  ## [6.7.1](https://github.com/express-rate-limit/express-rate-limit/releases/tag/v6.7.1)
@@ -23,7 +46,9 @@ and this project adheres to
23
46
 
24
47
  ### Changed
25
48
 
26
- - Bumped development dependencies.
49
+ - Bumped development dependencies
50
+ - This initially include bumping typescript from 4.x to 5.x and
51
+ dts-bundle-generator from 7.x to 8.x
27
52
  - Added `node` 20 to list of versions the CI jobs run on.
28
53
 
29
54
  No functional changes.
package/dist/index.cjs CHANGED
@@ -42,17 +42,17 @@ var ValidationError = class extends Error {
42
42
  * describing the issue in detail.
43
43
  */
44
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
- );
45
+ const url = `https://express-rate-limit.github.io/${code}/`;
46
+ super(`${message} See ${url} for more information on this error.`);
48
47
  __publicField(this, "name");
49
48
  __publicField(this, "code");
49
+ __publicField(this, "help");
50
50
  this.name = this.constructor.name;
51
51
  this.code = code;
52
- this.message = message;
52
+ this.help = url;
53
53
  }
54
54
  };
55
- var Validations = class {
55
+ var _Validations = class _Validations {
56
56
  constructor(enabled) {
57
57
  // eslint-disable-next-line @typescript-eslint/parameter-properties
58
58
  __publicField(this, "enabled");
@@ -129,6 +129,37 @@ var Validations = class {
129
129
  }
130
130
  });
131
131
  }
132
+ /**
133
+ * Ensures a given key is incremented only once per request.
134
+ *
135
+ * @param request {Request} - The Express request object.
136
+ * @param store {Store} - The store class.
137
+ * @param key {string} - The key used to store the client's hit count.
138
+ *
139
+ * @returns {void}
140
+ */
141
+ singleCount(request, store, key) {
142
+ this.wrap(() => {
143
+ let storeKeys = _Validations.singleCountKeys.get(request);
144
+ if (!storeKeys) {
145
+ storeKeys = /* @__PURE__ */ new Map();
146
+ _Validations.singleCountKeys.set(request, storeKeys);
147
+ }
148
+ const storeKey = store.localKeys ? store : store.constructor.name;
149
+ let keys = storeKeys.get(storeKey);
150
+ if (!keys) {
151
+ keys = [];
152
+ storeKeys.set(storeKey, keys);
153
+ }
154
+ if (keys.includes(key)) {
155
+ throw new ValidationError(
156
+ "ERR_ERL_DOUBLE_COUNT",
157
+ `The hit count for ${key} was incremented more than once for a single request.`
158
+ );
159
+ }
160
+ keys.push(key);
161
+ });
162
+ }
132
163
  wrap(validation) {
133
164
  if (!this.enabled) {
134
165
  return;
@@ -140,6 +171,18 @@ var Validations = class {
140
171
  }
141
172
  }
142
173
  };
174
+ /**
175
+ * Maps the key used in a store for a certain request, and ensures that the
176
+ * same key isn't used more than once per request.
177
+ *
178
+ * The store can be any one of the following:
179
+ * - An instance, for stores like the MemoryStore where two instances do not
180
+ * share state.
181
+ * - A string (class name), for stores where multiple instances
182
+ * typically share state, such as the Redis store.
183
+ */
184
+ __publicField(_Validations, "singleCountKeys", /* @__PURE__ */ new WeakMap());
185
+ var Validations = _Validations;
143
186
 
144
187
  // source/memory-store.ts
145
188
  var calculateNextResetTime = (windowMs) => {
@@ -165,6 +208,11 @@ var MemoryStore = class {
165
208
  * Reference to the active timer.
166
209
  */
167
210
  __publicField(this, "interval");
211
+ /**
212
+ * Confirmation that the keys incremented in once instance of MemoryStore
213
+ * cannot affect other instances.
214
+ */
215
+ __publicField(this, "localKeys", true);
168
216
  }
169
217
  /**
170
218
  * Method that initializes the store.
@@ -366,6 +414,7 @@ var rateLimit = (passedOptions) => {
366
414
  const augmentedRequest = request;
367
415
  const key = await config.keyGenerator(request, response);
368
416
  const { totalHits, resetTime } = await config.store.increment(key);
417
+ config.validations.singleCount(request, config.store, key);
369
418
  const retrieveQuota = typeof config.max === "function" ? config.max(request, response) : config.max;
370
419
  const maxHits = await retrieveQuota;
371
420
  augmentedRequest[config.requestPropertyName] = {
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- // Generated by dts-bundle-generator v8.0.1
1
+ // Generated by dts-bundle-generator v7.0.0
2
2
 
3
3
  import { NextFunction, Request, RequestHandler, Response } from 'express';
4
4
 
@@ -130,6 +130,14 @@ export type Store = {
130
130
  * Method to shutdown the store, stop timers, and release all resources.
131
131
  */
132
132
  shutdown?: () => Promise<void> | void;
133
+ /**
134
+ * Flag to indicate that keys incremented in one instance of this store can
135
+ * not affect other instances. Typically false if a database is used, true for
136
+ * MemoryStore.
137
+ *
138
+ * Used to help detect double-counting misconfigurations.
139
+ */
140
+ localKeys?: boolean;
133
141
  };
134
142
  /**
135
143
  * The configuration options for the rate limiter.
@@ -309,6 +317,11 @@ export declare class MemoryStore implements Store {
309
317
  * Reference to the active timer.
310
318
  */
311
319
  interval?: NodeJS.Timer;
320
+ /**
321
+ * Confirmation that the keys incremented in once instance of MemoryStore
322
+ * cannot affect other instances.
323
+ */
324
+ localKeys: boolean;
312
325
  /**
313
326
  * Method that initializes the store.
314
327
  *
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- // Generated by dts-bundle-generator v8.0.1
1
+ // Generated by dts-bundle-generator v7.0.0
2
2
 
3
3
  import { NextFunction, Request, RequestHandler, Response } from 'express';
4
4
 
@@ -130,6 +130,14 @@ export type Store = {
130
130
  * Method to shutdown the store, stop timers, and release all resources.
131
131
  */
132
132
  shutdown?: () => Promise<void> | void;
133
+ /**
134
+ * Flag to indicate that keys incremented in one instance of this store can
135
+ * not affect other instances. Typically false if a database is used, true for
136
+ * MemoryStore.
137
+ *
138
+ * Used to help detect double-counting misconfigurations.
139
+ */
140
+ localKeys?: boolean;
133
141
  };
134
142
  /**
135
143
  * The configuration options for the rate limiter.
@@ -309,6 +317,11 @@ export declare class MemoryStore implements Store {
309
317
  * Reference to the active timer.
310
318
  */
311
319
  interval?: NodeJS.Timer;
320
+ /**
321
+ * Confirmation that the keys incremented in once instance of MemoryStore
322
+ * cannot affect other instances.
323
+ */
324
+ localKeys: boolean;
312
325
  /**
313
326
  * Method that initializes the store.
314
327
  *
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Generated by dts-bundle-generator v8.0.1
1
+ // Generated by dts-bundle-generator v7.0.0
2
2
 
3
3
  import { NextFunction, Request, RequestHandler, Response } from 'express';
4
4
 
@@ -130,6 +130,14 @@ export type Store = {
130
130
  * Method to shutdown the store, stop timers, and release all resources.
131
131
  */
132
132
  shutdown?: () => Promise<void> | void;
133
+ /**
134
+ * Flag to indicate that keys incremented in one instance of this store can
135
+ * not affect other instances. Typically false if a database is used, true for
136
+ * MemoryStore.
137
+ *
138
+ * Used to help detect double-counting misconfigurations.
139
+ */
140
+ localKeys?: boolean;
133
141
  };
134
142
  /**
135
143
  * The configuration options for the rate limiter.
@@ -309,6 +317,11 @@ export declare class MemoryStore implements Store {
309
317
  * Reference to the active timer.
310
318
  */
311
319
  interval?: NodeJS.Timer;
320
+ /**
321
+ * Confirmation that the keys incremented in once instance of MemoryStore
322
+ * cannot affect other instances.
323
+ */
324
+ localKeys: boolean;
312
325
  /**
313
326
  * Method that initializes the store.
314
327
  *
package/dist/index.mjs CHANGED
@@ -16,17 +16,17 @@ var ValidationError = class extends Error {
16
16
  * describing the issue in detail.
17
17
  */
18
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
- );
19
+ const url = `https://express-rate-limit.github.io/${code}/`;
20
+ super(`${message} See ${url} for more information on this error.`);
22
21
  __publicField(this, "name");
23
22
  __publicField(this, "code");
23
+ __publicField(this, "help");
24
24
  this.name = this.constructor.name;
25
25
  this.code = code;
26
- this.message = message;
26
+ this.help = url;
27
27
  }
28
28
  };
29
- var Validations = class {
29
+ var _Validations = class _Validations {
30
30
  constructor(enabled) {
31
31
  // eslint-disable-next-line @typescript-eslint/parameter-properties
32
32
  __publicField(this, "enabled");
@@ -103,6 +103,37 @@ var Validations = class {
103
103
  }
104
104
  });
105
105
  }
106
+ /**
107
+ * Ensures a given key is incremented only once per request.
108
+ *
109
+ * @param request {Request} - The Express request object.
110
+ * @param store {Store} - The store class.
111
+ * @param key {string} - The key used to store the client's hit count.
112
+ *
113
+ * @returns {void}
114
+ */
115
+ singleCount(request, store, key) {
116
+ this.wrap(() => {
117
+ let storeKeys = _Validations.singleCountKeys.get(request);
118
+ if (!storeKeys) {
119
+ storeKeys = /* @__PURE__ */ new Map();
120
+ _Validations.singleCountKeys.set(request, storeKeys);
121
+ }
122
+ const storeKey = store.localKeys ? store : store.constructor.name;
123
+ let keys = storeKeys.get(storeKey);
124
+ if (!keys) {
125
+ keys = [];
126
+ storeKeys.set(storeKey, keys);
127
+ }
128
+ if (keys.includes(key)) {
129
+ throw new ValidationError(
130
+ "ERR_ERL_DOUBLE_COUNT",
131
+ `The hit count for ${key} was incremented more than once for a single request.`
132
+ );
133
+ }
134
+ keys.push(key);
135
+ });
136
+ }
106
137
  wrap(validation) {
107
138
  if (!this.enabled) {
108
139
  return;
@@ -114,6 +145,18 @@ var Validations = class {
114
145
  }
115
146
  }
116
147
  };
148
+ /**
149
+ * Maps the key used in a store for a certain request, and ensures that the
150
+ * same key isn't used more than once per request.
151
+ *
152
+ * The store can be any one of the following:
153
+ * - An instance, for stores like the MemoryStore where two instances do not
154
+ * share state.
155
+ * - A string (class name), for stores where multiple instances
156
+ * typically share state, such as the Redis store.
157
+ */
158
+ __publicField(_Validations, "singleCountKeys", /* @__PURE__ */ new WeakMap());
159
+ var Validations = _Validations;
117
160
 
118
161
  // source/memory-store.ts
119
162
  var calculateNextResetTime = (windowMs) => {
@@ -139,6 +182,11 @@ var MemoryStore = class {
139
182
  * Reference to the active timer.
140
183
  */
141
184
  __publicField(this, "interval");
185
+ /**
186
+ * Confirmation that the keys incremented in once instance of MemoryStore
187
+ * cannot affect other instances.
188
+ */
189
+ __publicField(this, "localKeys", true);
142
190
  }
143
191
  /**
144
192
  * Method that initializes the store.
@@ -340,6 +388,7 @@ var rateLimit = (passedOptions) => {
340
388
  const augmentedRequest = request;
341
389
  const key = await config.keyGenerator(request, response);
342
390
  const { totalHits, resetTime } = await config.store.increment(key);
391
+ config.validations.singleCount(request, config.store, key);
343
392
  const retrieveQuota = typeof config.max === "function" ? config.max(request, response) : config.max;
344
393
  const maxHits = await retrieveQuota;
345
394
  augmentedRequest[config.requestPropertyName] = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "express-rate-limit",
3
- "version": "6.8.0",
3
+ "version": "6.9.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",
@@ -63,12 +63,12 @@
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": "npm run lint:code -- --fix",
67
- "autofix:rest": "npm run lint:rest -- --write .",
68
- "autofix": "run-s autofix:*",
66
+ "format:code": "npm run lint:code -- --fix",
67
+ "format:rest": "npm run lint:rest -- --write .",
68
+ "format": "run-s format:*",
69
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
- "test": "run-s lint test:*",
71
+ "test": "run-s lint test:lib",
72
72
  "pre-commit": "lint-staged",
73
73
  "prepare": "run-s compile && husky install config/husky"
74
74
  },
@@ -83,7 +83,7 @@
83
83
  "@types/supertest": "2.0.12",
84
84
  "cross-env": "7.0.3",
85
85
  "del-cli": "5.0.0",
86
- "dts-bundle-generator": "8.0.1",
86
+ "dts-bundle-generator": "7.0.0",
87
87
  "esbuild": "0.18.11",
88
88
  "express": "4.18.2",
89
89
  "husky": "8.0.3",
@@ -93,7 +93,7 @@
93
93
  "supertest": "6.3.3",
94
94
  "ts-jest": "29.1.1",
95
95
  "ts-node": "10.9.1",
96
- "typescript": "5.1.6",
96
+ "typescript": "4.9.5",
97
97
  "xo": "0.54.2"
98
98
  },
99
99
  "xo": {
package/readme.md CHANGED
@@ -16,17 +16,42 @@ authentication and more to any API in minutes. Learn more at
16
16
  [![npm version](https://img.shields.io/npm/v/express-rate-limit.svg)](https://npmjs.org/package/express-rate-limit 'View this project on NPM')
17
17
  [![npm downloads](https://img.shields.io/npm/dm/express-rate-limit)](https://www.npmjs.com/package/express-rate-limit)
18
18
 
19
- Basic rate-limiting middleware for Express. Use to limit repeated requests to
20
- public APIs and/or endpoints such as password reset. Plays nice with
19
+ Basic rate-limiting middleware for [Express](http://expressjs.com/). Use to
20
+ limit repeated requests to public APIs and/or endpoints such as password reset.
21
+ Plays nice with
21
22
  [express-slow-down](https://www.npmjs.com/package/express-slow-down).
22
23
 
23
24
  </div>
24
25
 
25
- ### Alternate Rate Limiters
26
+ ## Use Cases
27
+
28
+ Depending on your use case, you may need to switch to a different
29
+ [store](#store).
30
+
31
+ #### Abuse Prevention
32
+
33
+ The default `MemoryStore` is probably fine.
26
34
 
27
- > This module does not share state with other processes/servers by default. If
28
- > you need a more robust solution, I recommend using an external store. See the
29
- > [`stores` section](#store) below for a list of external stores.
35
+ #### API Rate Limit Enforcement
36
+
37
+ You likely want to switch to a different [store](#store). As a performance
38
+ optimization, the default `MemoryStore` uses a global time window, so if your
39
+ limit is 10 requests per minute, a single user might be able to get an initial
40
+ burst of up to 20 requests in a row if they happen to get the first 10 in at the
41
+ end of one minute and the next 10 in at the start of the next minute. (After the
42
+ initial burst, they will be limited to the expected 10 requests per minute.) All
43
+ other stores use per-user time windows, so a user will get exactly 10 requests
44
+ regardless.
45
+
46
+ Additionally, if you have multiple servers or processes (for example, with the
47
+ [node:cluster](https://nodejs.org/api/cluster.html) module), you'll likely want
48
+ to use an external data store to syhcnronize hits
49
+ ([redis](https://npmjs.com/package/rate-limit-redis),
50
+ [memcached](https://npmjs.org/package/rate-limit-memcached), [etc.](#store))
51
+ This will guarentee the expected result even if some requests get handled by
52
+ different servers/processes.
53
+
54
+ ### Alternate Rate Limiters
30
55
 
31
56
  This module was designed to only handle the basics and didn't even support
32
57
  external stores initially. These other options all are excellent pieces of
@@ -94,6 +119,7 @@ const limiter = rateLimit({
94
119
  max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
95
120
  standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
96
121
  legacyHeaders: false, // Disable the `X-RateLimit-*` headers
122
+ // store: ... , // Use an external store for more precise rate limiting
97
123
  })
98
124
 
99
125
  // Apply the rate limiting middleware to all requests
@@ -112,6 +138,7 @@ const apiLimiter = rateLimit({
112
138
  max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
113
139
  standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
114
140
  legacyHeaders: false, // Disable the `X-RateLimit-*` headers
141
+ // store: ... , // Use an external store for more precise rate limiting
115
142
  })
116
143
 
117
144
  // Apply the rate limiting middleware to API calls only
@@ -128,6 +155,7 @@ const apiLimiter = rateLimit({
128
155
  max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
129
156
  standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
130
157
  legacyHeaders: false, // Disable the `X-RateLimit-*` headers
158
+ // store: ... , // Use an external store for more precise rate limiting
131
159
  })
132
160
 
133
161
  app.use('/api/', apiLimiter)
@@ -149,17 +177,22 @@ app.post('/create-account', createAccountLimiter, (request, response) => {
149
177
  To use a custom store:
150
178
 
151
179
  ```ts
152
- import rateLimit, { MemoryStore } from 'express-rate-limit'
180
+ import rateLimit from 'express-rate-limit'
181
+ import RedisStore from 'rate-limit-redis'
182
+ import RedisClient from 'ioredis'
153
183
 
154
- const apiLimiter = rateLimit({
184
+ const redisClient = new RedisClient()
185
+ const rateLimiter = rateLimit({
155
186
  windowMs: 15 * 60 * 1000, // 15 minutes
156
187
  max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
157
188
  standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
158
- store: new MemoryStore(),
189
+ store: new RedisStore({
190
+ /* ... */
191
+ }), // Use the external store
159
192
  })
160
193
 
161
- // Apply the rate limiting middleware to API calls only
162
- app.use('/api', apiLimiter)
194
+ // Apply the rate limiting middleware to all requests
195
+ app.use(rateLimiter)
163
196
  ```
164
197
 
165
198
  > **Note:** most stores will require additional configuration, such as custom