express-rate-limit 5.5.0 → 6.0.2
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 +125 -0
- package/dist/index.cjs +226 -0
- package/dist/index.d.ts +248 -0
- package/dist/index.mjs +198 -0
- package/license.md +20 -0
- package/package.json +142 -51
- package/readme.md +492 -0
- package/tsconfig.json +15 -0
- package/LICENSE +0 -7
- package/README.md +0 -349
- package/lib/express-rate-limit.js +0 -190
- package/lib/memory-store.js +0 -47
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// source/memory-store.ts
|
|
2
|
+
var calculateNextResetTime = (windowMs) => {
|
|
3
|
+
const resetTime = new Date();
|
|
4
|
+
resetTime.setMilliseconds(resetTime.getMilliseconds() + windowMs);
|
|
5
|
+
return resetTime;
|
|
6
|
+
};
|
|
7
|
+
var MemoryStore = class {
|
|
8
|
+
init(options) {
|
|
9
|
+
this.windowMs = options.windowMs;
|
|
10
|
+
this.resetTime = calculateNextResetTime(this.windowMs);
|
|
11
|
+
this.hits = {};
|
|
12
|
+
const interval = setInterval(async () => {
|
|
13
|
+
await this.resetAll();
|
|
14
|
+
}, this.windowMs);
|
|
15
|
+
if (interval.unref) {
|
|
16
|
+
interval.unref();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async increment(key) {
|
|
20
|
+
const totalHits = (this.hits[key] ?? 0) + 1;
|
|
21
|
+
this.hits[key] = totalHits;
|
|
22
|
+
return {
|
|
23
|
+
totalHits,
|
|
24
|
+
resetTime: this.resetTime
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
async decrement(key) {
|
|
28
|
+
const current = this.hits[key];
|
|
29
|
+
if (current) {
|
|
30
|
+
this.hits[key] = current - 1;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async resetKey(key) {
|
|
34
|
+
delete this.hits[key];
|
|
35
|
+
}
|
|
36
|
+
async resetAll() {
|
|
37
|
+
this.hits = {};
|
|
38
|
+
this.resetTime = calculateNextResetTime(this.windowMs);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// source/lib.ts
|
|
43
|
+
var isLegacyStore = (store) => typeof store.incr === "function" && typeof store.increment !== "function";
|
|
44
|
+
var promisifyStore = (passedStore) => {
|
|
45
|
+
if (!isLegacyStore(passedStore)) {
|
|
46
|
+
return passedStore;
|
|
47
|
+
}
|
|
48
|
+
const legacyStore = passedStore;
|
|
49
|
+
class PromisifiedStore {
|
|
50
|
+
async increment(key) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
legacyStore.incr(key, (error, totalHits, resetTime) => {
|
|
53
|
+
if (error)
|
|
54
|
+
reject(error);
|
|
55
|
+
resolve({ totalHits, resetTime });
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async decrement(key) {
|
|
60
|
+
return Promise.resolve(legacyStore.decrement(key));
|
|
61
|
+
}
|
|
62
|
+
async resetKey(key) {
|
|
63
|
+
return Promise.resolve(legacyStore.resetKey(key));
|
|
64
|
+
}
|
|
65
|
+
async resetAll() {
|
|
66
|
+
if (typeof legacyStore.resetAll === "function")
|
|
67
|
+
return Promise.resolve(legacyStore.resetAll());
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return new PromisifiedStore();
|
|
71
|
+
};
|
|
72
|
+
var parseOptions = (passedOptions) => {
|
|
73
|
+
const options = {
|
|
74
|
+
windowMs: 60 * 1e3,
|
|
75
|
+
store: new MemoryStore(),
|
|
76
|
+
max: 5,
|
|
77
|
+
message: "Too many requests, please try again later.",
|
|
78
|
+
statusCode: 429,
|
|
79
|
+
legacyHeaders: passedOptions.headers ?? true,
|
|
80
|
+
standardHeaders: passedOptions.draft_polli_ratelimit_headers ?? false,
|
|
81
|
+
requestPropertyName: "rateLimit",
|
|
82
|
+
skipFailedRequests: false,
|
|
83
|
+
skipSuccessfulRequests: false,
|
|
84
|
+
requestWasSuccessful: (_request, response) => response.statusCode < 400,
|
|
85
|
+
skip: (_request, _response) => false,
|
|
86
|
+
keyGenerator: (request, _response) => {
|
|
87
|
+
if (!request.ip) {
|
|
88
|
+
console.error("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.");
|
|
89
|
+
}
|
|
90
|
+
return request.ip;
|
|
91
|
+
},
|
|
92
|
+
handler: (_request, response, _next, _optionsUsed) => {
|
|
93
|
+
response.status(options.statusCode).send(options.message);
|
|
94
|
+
},
|
|
95
|
+
onLimitReached: (_request, _response, _optionsUsed) => {
|
|
96
|
+
},
|
|
97
|
+
...passedOptions
|
|
98
|
+
};
|
|
99
|
+
if (typeof options.store.incr !== "function" && typeof options.store.increment !== "function" || typeof options.store.decrement !== "function" || typeof options.store.resetKey !== "function" || typeof options.store.resetAll !== "undefined" && typeof options.store.resetAll !== "function" || typeof options.store.init !== "undefined" && typeof options.store.init !== "function") {
|
|
100
|
+
throw new TypeError("An invalid store was passed. Please ensure that the store is a class that implements the `Store` interface.");
|
|
101
|
+
}
|
|
102
|
+
options.store = promisifyStore(options.store);
|
|
103
|
+
return options;
|
|
104
|
+
};
|
|
105
|
+
var handleAsyncErrors = (fn) => async (request, response, next) => {
|
|
106
|
+
try {
|
|
107
|
+
await Promise.resolve(fn(request, response, next)).catch(next);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
next(error);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
var rateLimit = (passedOptions) => {
|
|
113
|
+
const options = parseOptions(passedOptions ?? {});
|
|
114
|
+
if (typeof options.store.init === "function")
|
|
115
|
+
options.store.init(options);
|
|
116
|
+
const middleware = handleAsyncErrors(async (request, response, next) => {
|
|
117
|
+
const skip = await options.skip(request, response);
|
|
118
|
+
if (skip) {
|
|
119
|
+
next();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const augmentedRequest = request;
|
|
123
|
+
const key = await options.keyGenerator(request, response);
|
|
124
|
+
const { totalHits, resetTime } = await options.store.increment(key);
|
|
125
|
+
const retrieveQuota = typeof options.max === "function" ? options.max(request, response) : options.max;
|
|
126
|
+
const maxHits = await retrieveQuota;
|
|
127
|
+
augmentedRequest[options.requestPropertyName] = {
|
|
128
|
+
limit: maxHits,
|
|
129
|
+
current: totalHits,
|
|
130
|
+
remaining: Math.max(maxHits - totalHits, 0),
|
|
131
|
+
resetTime
|
|
132
|
+
};
|
|
133
|
+
if (options.legacyHeaders && !response.headersSent) {
|
|
134
|
+
response.setHeader("X-RateLimit-Limit", maxHits);
|
|
135
|
+
response.setHeader("X-RateLimit-Remaining", augmentedRequest[options.requestPropertyName].remaining);
|
|
136
|
+
if (resetTime instanceof Date) {
|
|
137
|
+
response.setHeader("Date", new Date().toUTCString());
|
|
138
|
+
response.setHeader("X-RateLimit-Reset", Math.ceil(resetTime.getTime() / 1e3));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (options.standardHeaders && !response.headersSent) {
|
|
142
|
+
response.setHeader("RateLimit-Limit", maxHits);
|
|
143
|
+
response.setHeader("RateLimit-Remaining", augmentedRequest[options.requestPropertyName].remaining);
|
|
144
|
+
if (resetTime) {
|
|
145
|
+
const deltaSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1e3);
|
|
146
|
+
response.setHeader("RateLimit-Reset", Math.max(0, deltaSeconds));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (options.skipFailedRequests || options.skipSuccessfulRequests) {
|
|
150
|
+
let decremented = false;
|
|
151
|
+
const decrementKey = async () => {
|
|
152
|
+
if (!decremented) {
|
|
153
|
+
await options.store.decrement(key);
|
|
154
|
+
decremented = true;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
if (options.skipFailedRequests) {
|
|
158
|
+
response.on("finish", async () => {
|
|
159
|
+
if (!options.requestWasSuccessful(request, response))
|
|
160
|
+
await decrementKey();
|
|
161
|
+
});
|
|
162
|
+
response.on("close", async () => {
|
|
163
|
+
if (!response.writableEnded)
|
|
164
|
+
await decrementKey();
|
|
165
|
+
});
|
|
166
|
+
response.on("error", async () => {
|
|
167
|
+
await decrementKey();
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (options.skipSuccessfulRequests) {
|
|
171
|
+
response.on("finish", async () => {
|
|
172
|
+
if (options.requestWasSuccessful(request, response))
|
|
173
|
+
await decrementKey();
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (maxHits && totalHits === maxHits + 1) {
|
|
178
|
+
options.onLimitReached(request, response, options);
|
|
179
|
+
}
|
|
180
|
+
if (maxHits && totalHits > maxHits) {
|
|
181
|
+
if ((options.legacyHeaders || options.standardHeaders) && !response.headersSent) {
|
|
182
|
+
response.setHeader("Retry-After", Math.ceil(options.windowMs / 1e3));
|
|
183
|
+
}
|
|
184
|
+
options.handler(request, response, next, options);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
next();
|
|
188
|
+
});
|
|
189
|
+
middleware.resetKey = options.store.resetKey.bind(options.store);
|
|
190
|
+
return middleware;
|
|
191
|
+
};
|
|
192
|
+
var lib_default = rateLimit;
|
|
193
|
+
|
|
194
|
+
// source/index.ts
|
|
195
|
+
var source_default = lib_default;
|
|
196
|
+
export {
|
|
197
|
+
source_default as default
|
|
198
|
+
};
|
package/license.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright 2021 Nathan Friedly
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/package.json
CHANGED
|
@@ -1,53 +1,144 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
2
|
+
"name": "express-rate-limit",
|
|
3
|
+
"version": "6.0.2",
|
|
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
|
+
"author": {
|
|
6
|
+
"name": "Nathan Friedly",
|
|
7
|
+
"url": "http://nfriedly.com/"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"homepage": "https://github.com/nfriedly/express-rate-limit",
|
|
11
|
+
"repository": "https://github.com/nfriedly/express-rate-limit",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"express-rate-limit",
|
|
14
|
+
"express",
|
|
15
|
+
"rate",
|
|
16
|
+
"limit",
|
|
17
|
+
"ratelimit",
|
|
18
|
+
"rate-limit",
|
|
19
|
+
"middleware",
|
|
20
|
+
"ip",
|
|
21
|
+
"auth",
|
|
22
|
+
"authorization",
|
|
23
|
+
"security",
|
|
24
|
+
"brute",
|
|
25
|
+
"force",
|
|
26
|
+
"bruteforce",
|
|
27
|
+
"brute-force",
|
|
28
|
+
"attack"
|
|
29
|
+
],
|
|
30
|
+
"type": "module",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"import": "./dist/index.mjs",
|
|
35
|
+
"require": "./dist/index.cjs"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist/",
|
|
40
|
+
"tsconfig.json",
|
|
41
|
+
"package.json",
|
|
42
|
+
"readme.md",
|
|
43
|
+
"license.md",
|
|
44
|
+
"changelog.md"
|
|
45
|
+
],
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">= 12.9.0"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"clean": "del-cli dist/ coverage/ *.log *.tmp *.bak *.tgz",
|
|
51
|
+
"build:cjs": "esbuild source/index.ts --bundle --format=cjs --outfile=dist/index.cjs --footer:js='module.exports = rateLimit;'",
|
|
52
|
+
"build:esm": "esbuild source/index.ts --bundle --format=esm --outfile=dist/index.mjs",
|
|
53
|
+
"build:types": "dts-bundle-generator --out-file=dist/index.d.ts source/index.ts",
|
|
54
|
+
"compile": "run-s clean build:*",
|
|
55
|
+
"lint:code": "xo --ignore test/external/",
|
|
56
|
+
"lint:rest": "prettier --ignore-path .gitignore --ignore-unknown --check .",
|
|
57
|
+
"lint": "run-s lint:*",
|
|
58
|
+
"autofix:code": "xo --ignore test/external/ --fix",
|
|
59
|
+
"autofix:rest": "prettier --ignore-path .gitignore --ignore-unknown --write .",
|
|
60
|
+
"autofix": "run-s autofix:*",
|
|
61
|
+
"test:lib": "cross-env NODE_OPTIONS=--experimental-vm-modules jest",
|
|
62
|
+
"test:ext": "cd test/external/ && bash run-all-tests",
|
|
63
|
+
"test": "npm pack && run-s lint test:*",
|
|
64
|
+
"pre-commit": "lint-staged",
|
|
65
|
+
"prepare": "run-s compile && husky install config/husky"
|
|
66
|
+
},
|
|
67
|
+
"peerDependencies": {
|
|
68
|
+
"express": "^4"
|
|
69
|
+
},
|
|
70
|
+
"devDependencies": {
|
|
71
|
+
"@jest/globals": "^27.4.2",
|
|
72
|
+
"@types/express": "^4.17.13",
|
|
73
|
+
"@types/jest": "^27.0.3",
|
|
74
|
+
"@types/node": "^16.11.17",
|
|
75
|
+
"@types/supertest": "^2.0.11",
|
|
76
|
+
"cross-env": "^7.0.3",
|
|
77
|
+
"del-cli": "^4.0.1",
|
|
78
|
+
"dts-bundle-generator": "^6.2.0",
|
|
79
|
+
"esbuild": "^0.14.8",
|
|
80
|
+
"express": "^4.17.1",
|
|
81
|
+
"husky": "^7.0.4",
|
|
82
|
+
"jest": "^27.4.3",
|
|
83
|
+
"lint-staged": "^12.1.2",
|
|
84
|
+
"npm-run-all": "^4.1.5",
|
|
85
|
+
"supertest": "^6.1.6",
|
|
86
|
+
"ts-jest": "^27.1.1",
|
|
87
|
+
"ts-node": "^10.4.0",
|
|
88
|
+
"typescript": "^4.5.2",
|
|
89
|
+
"xo": "^0.47.0"
|
|
90
|
+
},
|
|
91
|
+
"xo": {
|
|
92
|
+
"prettier": true,
|
|
93
|
+
"rules": {
|
|
94
|
+
"@typescript-eslint/no-empty-function": 0,
|
|
95
|
+
"@typescript-eslint/no-dynamic-delete": 0,
|
|
96
|
+
"@typescript-eslint/no-confusing-void-expression": 0,
|
|
97
|
+
"@typescript-eslint/consistent-indexed-object-style": [
|
|
98
|
+
"error",
|
|
99
|
+
"index-signature"
|
|
100
|
+
],
|
|
101
|
+
"import/no-named-as-default-member": 0,
|
|
102
|
+
"import/no-cycle": 0
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"prettier": {
|
|
106
|
+
"semi": false,
|
|
107
|
+
"useTabs": true,
|
|
108
|
+
"singleQuote": true,
|
|
109
|
+
"bracketSpacing": true,
|
|
110
|
+
"trailingComma": "all",
|
|
111
|
+
"proseWrap": "always"
|
|
112
|
+
},
|
|
113
|
+
"jest": {
|
|
114
|
+
"preset": "ts-jest/presets/default-esm",
|
|
115
|
+
"globals": {
|
|
116
|
+
"ts-jest": {
|
|
117
|
+
"useESM": true
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
"verbose": true,
|
|
121
|
+
"collectCoverage": true,
|
|
122
|
+
"collectCoverageFrom": [
|
|
123
|
+
"source/**/*.ts"
|
|
124
|
+
],
|
|
125
|
+
"testTimeout": 30000,
|
|
126
|
+
"testMatch": [
|
|
127
|
+
"**/test/library/**/*-test.[jt]s?(x)"
|
|
128
|
+
],
|
|
129
|
+
"moduleFileExtensions": [
|
|
130
|
+
"js",
|
|
131
|
+
"jsx",
|
|
132
|
+
"json",
|
|
133
|
+
"ts",
|
|
134
|
+
"tsx"
|
|
135
|
+
],
|
|
136
|
+
"moduleNameMapper": {
|
|
137
|
+
"^(\\.{1,2}/.*)\\.js$": "$1"
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
"lint-staged": {
|
|
141
|
+
"{source,test}/**/*.ts": "xo --ignore test/external/ --fix",
|
|
142
|
+
"**/*.{json,yaml,md}": "prettier --ignore-path .gitignore --ignore-unknown --write "
|
|
143
|
+
}
|
|
53
144
|
}
|