express-rate-limit 6.8.1 → 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 +12 -0
- package/dist/index.cjs +54 -5
- package/dist/index.d.cts +13 -0
- package/dist/index.d.mts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.mjs +54 -5
- package/package.json +5 -5
- package/readme.md +44 -11
package/changelog.md
CHANGED
|
@@ -6,6 +6,18 @@ 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.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
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Miscaleanous documenation improvements
|
|
20
|
+
|
|
9
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)
|
|
10
22
|
|
|
11
23
|
### Changed
|
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
|
-
|
|
46
|
-
|
|
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.
|
|
52
|
+
this.help = url;
|
|
53
53
|
}
|
|
54
54
|
};
|
|
55
|
-
var
|
|
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
|
@@ -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
|
@@ -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
|
@@ -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
|
-
|
|
20
|
-
|
|
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.
|
|
26
|
+
this.help = url;
|
|
27
27
|
}
|
|
28
28
|
};
|
|
29
|
-
var
|
|
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.
|
|
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
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
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
|
},
|
package/readme.md
CHANGED
|
@@ -16,17 +16,42 @@ authentication and more to any API in minutes. Learn more at
|
|
|
16
16
|
[](https://npmjs.org/package/express-rate-limit 'View this project on NPM')
|
|
17
17
|
[](https://www.npmjs.com/package/express-rate-limit)
|
|
18
18
|
|
|
19
|
-
Basic rate-limiting middleware for Express. Use to
|
|
20
|
-
public APIs and/or endpoints such as password reset.
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
180
|
+
import rateLimit from 'express-rate-limit'
|
|
181
|
+
import RedisStore from 'rate-limit-redis'
|
|
182
|
+
import RedisClient from 'ioredis'
|
|
153
183
|
|
|
154
|
-
const
|
|
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
|
|
189
|
+
store: new RedisStore({
|
|
190
|
+
/* ... */
|
|
191
|
+
}), // Use the external store
|
|
159
192
|
})
|
|
160
193
|
|
|
161
|
-
// Apply the rate limiting middleware to
|
|
162
|
-
app.use(
|
|
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
|