express-rate-limit 6.11.2 → 7.0.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 +56 -15
- package/dist/index.cjs +260 -220
- package/dist/index.d.cts +169 -27
- package/dist/index.d.mts +169 -27
- package/dist/index.d.ts +169 -27
- package/dist/index.mjs +260 -222
- package/license.md +1 -1
- package/package.json +27 -27
- package/readme.md +88 -93
- package/tsconfig.json +4 -1
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
var __defProp = Object.defineProperty;
|
|
2
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
-
var __publicField = (obj, key, value) => {
|
|
4
|
-
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
5
|
-
return value;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
1
|
// source/headers.ts
|
|
9
2
|
var getResetSeconds = (resetTime, windowMs) => {
|
|
10
3
|
let resetSeconds = void 0;
|
|
@@ -71,9 +64,6 @@ var ValidationError = class extends Error {
|
|
|
71
64
|
constructor(code, message) {
|
|
72
65
|
const url = `https://express-rate-limit.github.io/${code}/`;
|
|
73
66
|
super(`${message} See ${url} for more information.`);
|
|
74
|
-
__publicField(this, "name");
|
|
75
|
-
__publicField(this, "code");
|
|
76
|
-
__publicField(this, "help");
|
|
77
67
|
this.name = this.constructor.name;
|
|
78
68
|
this.code = code;
|
|
79
69
|
this.help = url;
|
|
@@ -81,18 +71,17 @@ var ValidationError = class extends Error {
|
|
|
81
71
|
};
|
|
82
72
|
var ChangeWarning = class extends ValidationError {
|
|
83
73
|
};
|
|
84
|
-
var
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
this.enabled = true;
|
|
92
|
-
}
|
|
74
|
+
var singleCountKeys = /* @__PURE__ */ new WeakMap();
|
|
75
|
+
var validations = {
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
77
|
+
enabled: {
|
|
78
|
+
default: true
|
|
79
|
+
},
|
|
80
|
+
// Should be EnabledValidations type, but that's a circular reference
|
|
93
81
|
disable() {
|
|
94
|
-
this.enabled
|
|
95
|
-
|
|
82
|
+
for (const k of Object.keys(this.enabled))
|
|
83
|
+
this.enabled[k] = false;
|
|
84
|
+
},
|
|
96
85
|
/**
|
|
97
86
|
* Checks whether the IP address is valid, and that it does not have a port
|
|
98
87
|
* number in it.
|
|
@@ -104,21 +93,19 @@ var _Validations = class _Validations {
|
|
|
104
93
|
* @returns {void}
|
|
105
94
|
*/
|
|
106
95
|
ip(ip) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
});
|
|
121
|
-
}
|
|
96
|
+
if (ip === void 0) {
|
|
97
|
+
throw new ValidationError(
|
|
98
|
+
"ERR_ERL_UNDEFINED_IP_ADDRESS",
|
|
99
|
+
`An undefined 'request.ip' was detected. This might indicate a misconfiguration or the connection being destroyed prematurely.`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if (!isIP(ip)) {
|
|
103
|
+
throw new ValidationError(
|
|
104
|
+
"ERR_ERL_INVALID_IP_ADDRESS",
|
|
105
|
+
`An invalid 'request.ip' (${ip}) was detected. Consider passing a custom 'keyGenerator' function to the rate limiter.`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
122
109
|
/**
|
|
123
110
|
* Makes sure the trust proxy setting is not set to `true`.
|
|
124
111
|
*
|
|
@@ -129,15 +116,13 @@ var _Validations = class _Validations {
|
|
|
129
116
|
* @returns {void}
|
|
130
117
|
*/
|
|
131
118
|
trustProxy(request) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
});
|
|
140
|
-
}
|
|
119
|
+
if (request.app.get("trust proxy") === true) {
|
|
120
|
+
throw new ValidationError(
|
|
121
|
+
"ERR_ERL_PERMISSIVE_TRUST_PROXY",
|
|
122
|
+
`The Express 'trust proxy' setting is true, which allows anyone to trivially bypass IP-based rate limiting.`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
141
126
|
/**
|
|
142
127
|
* Makes sure the trust proxy setting is set in case the `X-Forwarded-For`
|
|
143
128
|
* header is present.
|
|
@@ -149,31 +134,26 @@ var _Validations = class _Validations {
|
|
|
149
134
|
* @returns {void}
|
|
150
135
|
*/
|
|
151
136
|
xForwardedForHeader(request) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
});
|
|
160
|
-
}
|
|
137
|
+
if (request.headers["x-forwarded-for"] && request.app.get("trust proxy") === false) {
|
|
138
|
+
throw new ValidationError(
|
|
139
|
+
"ERR_ERL_UNEXPECTED_X_FORWARDED_FOR",
|
|
140
|
+
`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.`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
161
144
|
/**
|
|
162
145
|
* Ensures totalHits value from store is a positive integer.
|
|
163
146
|
*
|
|
164
147
|
* @param hits {any} - The `totalHits` returned by the store.
|
|
165
148
|
*/
|
|
166
149
|
positiveHits(hits) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
}
|
|
150
|
+
if (typeof hits !== "number" || hits < 1 || hits !== Math.round(hits)) {
|
|
151
|
+
throw new ValidationError(
|
|
152
|
+
"ERR_ERL_INVALID_HITS",
|
|
153
|
+
`The totalHits value returned from the store must be a positive integer, got ${hits}`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
177
157
|
/**
|
|
178
158
|
* Ensures a given key is incremented only once per request.
|
|
179
159
|
*
|
|
@@ -184,83 +164,74 @@ var _Validations = class _Validations {
|
|
|
184
164
|
* @returns {void}
|
|
185
165
|
*/
|
|
186
166
|
singleCount(request, store, key) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
keys.push(prefixedKey);
|
|
208
|
-
});
|
|
209
|
-
}
|
|
167
|
+
let storeKeys = singleCountKeys.get(request);
|
|
168
|
+
if (!storeKeys) {
|
|
169
|
+
storeKeys = /* @__PURE__ */ new Map();
|
|
170
|
+
singleCountKeys.set(request, storeKeys);
|
|
171
|
+
}
|
|
172
|
+
const storeKey = store.localKeys ? store : store.constructor.name;
|
|
173
|
+
let keys = storeKeys.get(storeKey);
|
|
174
|
+
if (!keys) {
|
|
175
|
+
keys = [];
|
|
176
|
+
storeKeys.set(storeKey, keys);
|
|
177
|
+
}
|
|
178
|
+
const prefixedKey = `${store.prefix ?? ""}${key}`;
|
|
179
|
+
if (keys.includes(prefixedKey)) {
|
|
180
|
+
throw new ValidationError(
|
|
181
|
+
"ERR_ERL_DOUBLE_COUNT",
|
|
182
|
+
`The hit count for ${key} was incremented more than once for a single request.`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
keys.push(prefixedKey);
|
|
186
|
+
},
|
|
210
187
|
/**
|
|
211
|
-
* Warns the user that the behaviour for `max: 0` is changing in the next
|
|
188
|
+
* Warns the user that the behaviour for `max: 0` / `limit: 0` is changing in the next
|
|
212
189
|
* major release.
|
|
213
190
|
*
|
|
214
|
-
* @param
|
|
191
|
+
* @param limit {number} - The maximum number of hits per client.
|
|
215
192
|
*
|
|
216
193
|
* @returns {void}
|
|
217
194
|
*/
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
});
|
|
227
|
-
}
|
|
195
|
+
limit(limit) {
|
|
196
|
+
if (limit === 0) {
|
|
197
|
+
throw new ChangeWarning(
|
|
198
|
+
"WRN_ERL_MAX_ZERO",
|
|
199
|
+
`Setting limit or max to 0 disables rate limiting in express-rate-limit v6 and older, but will cause all requests to be blocked in v7`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
},
|
|
228
203
|
/**
|
|
229
204
|
* Warns the user that the `draft_polli_ratelimit_headers` option is deprecated
|
|
230
205
|
* and will be removed in the next major release.
|
|
231
206
|
*
|
|
232
|
-
* @param draft_polli_ratelimit_headers {
|
|
207
|
+
* @param draft_polli_ratelimit_headers {any | undefined} - The now-deprecated setting that was used to enable standard headers.
|
|
233
208
|
*
|
|
234
209
|
* @returns {void}
|
|
235
210
|
*/
|
|
236
211
|
draftPolliHeaders(draft_polli_ratelimit_headers) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
});
|
|
245
|
-
}
|
|
212
|
+
if (draft_polli_ratelimit_headers) {
|
|
213
|
+
throw new ChangeWarning(
|
|
214
|
+
"WRN_ERL_DEPRECATED_DRAFT_POLLI_HEADERS",
|
|
215
|
+
`The draft_polli_ratelimit_headers configuration option is deprecated and has been removed in express-rate-limit v7, please set standardHeaders: 'draft-6' instead.`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
},
|
|
246
219
|
/**
|
|
247
220
|
* Warns the user that the `onLimitReached` option is deprecated and will be removed in the next
|
|
248
221
|
* major release.
|
|
249
222
|
*
|
|
250
|
-
* @param onLimitReached {
|
|
223
|
+
* @param onLimitReached {any | undefined} - The maximum number of hits per client.
|
|
251
224
|
*
|
|
252
225
|
* @returns {void}
|
|
253
226
|
*/
|
|
254
227
|
onLimitReached(onLimitReached) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
});
|
|
263
|
-
}
|
|
228
|
+
if (onLimitReached) {
|
|
229
|
+
throw new ChangeWarning(
|
|
230
|
+
"WRN_ERL_DEPRECATED_ON_LIMIT_REACHED",
|
|
231
|
+
`The onLimitReached configuration option is deprecated and has been removed in express-rate-limit v7.`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
},
|
|
264
235
|
/**
|
|
265
236
|
* Warns the user when the selected headers option requires a reset time but
|
|
266
237
|
* the store does not provide one.
|
|
@@ -270,71 +241,93 @@ var _Validations = class _Validations {
|
|
|
270
241
|
* @returns {void}
|
|
271
242
|
*/
|
|
272
243
|
headersResetTime(resetTime) {
|
|
273
|
-
|
|
274
|
-
|
|
244
|
+
if (!resetTime) {
|
|
245
|
+
throw new ValidationError(
|
|
246
|
+
"ERR_ERL_HEADERS_NO_RESET",
|
|
247
|
+
`standardHeaders: 'draft-7' requires a 'resetTime', but the store did not provide one. The 'windowMs' value will be used instead, which may cause clients to wait longer than necessary.`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
/**
|
|
252
|
+
* Checks the options.validate setting to ensure that only recognized validations are enabled or disabled.
|
|
253
|
+
*
|
|
254
|
+
* If any unrecognized values are found, an error is logged that includes the list of supported vaidations.
|
|
255
|
+
*/
|
|
256
|
+
validationsConfig() {
|
|
257
|
+
const supportedValidations = Object.keys(this).filter(
|
|
258
|
+
(k) => !["enabled", "disable"].includes(k)
|
|
259
|
+
);
|
|
260
|
+
supportedValidations.push("default");
|
|
261
|
+
for (const key of Object.keys(this.enabled)) {
|
|
262
|
+
if (!supportedValidations.includes(key)) {
|
|
275
263
|
throw new ValidationError(
|
|
276
|
-
"
|
|
277
|
-
`
|
|
264
|
+
"ERR_ERL_UNKNOWN_VALIDATION",
|
|
265
|
+
`options.validate.${key} is not recognized. Supported validate options are: ${supportedValidations.join(
|
|
266
|
+
", "
|
|
267
|
+
)}.`
|
|
278
268
|
);
|
|
279
269
|
}
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
wrap(validation) {
|
|
283
|
-
if (!this.enabled) {
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
try {
|
|
287
|
-
validation.call(this);
|
|
288
|
-
} catch (error) {
|
|
289
|
-
if (error instanceof ChangeWarning)
|
|
290
|
-
console.warn(error);
|
|
291
|
-
else
|
|
292
|
-
console.error(error);
|
|
293
270
|
}
|
|
294
271
|
}
|
|
295
272
|
};
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
273
|
+
var getValidations = (_enabled) => {
|
|
274
|
+
let enabled;
|
|
275
|
+
if (typeof _enabled === "boolean") {
|
|
276
|
+
enabled = {
|
|
277
|
+
default: _enabled
|
|
278
|
+
};
|
|
279
|
+
} else {
|
|
280
|
+
enabled = {
|
|
281
|
+
default: true,
|
|
282
|
+
..._enabled
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const wrappedValidations = {
|
|
286
|
+
enabled
|
|
287
|
+
};
|
|
288
|
+
for (const [name, validation] of Object.entries(validations)) {
|
|
289
|
+
if (typeof validation === "function")
|
|
290
|
+
wrappedValidations[name] = (...args) => {
|
|
291
|
+
if (!(enabled[name] ?? enabled.default)) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
;
|
|
296
|
+
validation.apply(
|
|
297
|
+
wrappedValidations,
|
|
298
|
+
args
|
|
299
|
+
);
|
|
300
|
+
} catch (error) {
|
|
301
|
+
if (error instanceof ChangeWarning)
|
|
302
|
+
console.warn(error);
|
|
303
|
+
else
|
|
304
|
+
console.error(error);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
return wrappedValidations;
|
|
309
|
+
};
|
|
308
310
|
|
|
309
311
|
// source/memory-store.ts
|
|
310
|
-
var calculateNextResetTime = (windowMs) => {
|
|
311
|
-
const resetTime = /* @__PURE__ */ new Date();
|
|
312
|
-
resetTime.setMilliseconds(resetTime.getMilliseconds() + windowMs);
|
|
313
|
-
return resetTime;
|
|
314
|
-
};
|
|
315
312
|
var MemoryStore = class {
|
|
316
313
|
constructor() {
|
|
317
314
|
/**
|
|
318
|
-
*
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
*
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
* The time at which all hit counts will be reset.
|
|
327
|
-
*/
|
|
328
|
-
__publicField(this, "resetTime");
|
|
329
|
-
/**
|
|
330
|
-
* Reference to the active timer.
|
|
315
|
+
* These two maps store usage (requests) and reset time by key (for example, IP
|
|
316
|
+
* addresses or API keys).
|
|
317
|
+
*
|
|
318
|
+
* They are split into two to avoid having to iterate through the entire set to
|
|
319
|
+
* determine which ones need reset. Instead, `Client`s are moved from `previous`
|
|
320
|
+
* to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients
|
|
321
|
+
* left in `previous`, i.e., those that have not made any recent requests, are
|
|
322
|
+
* known to be expired and can be deleted in bulk.
|
|
331
323
|
*/
|
|
332
|
-
|
|
324
|
+
this.previous = /* @__PURE__ */ new Map();
|
|
325
|
+
this.current = /* @__PURE__ */ new Map();
|
|
333
326
|
/**
|
|
334
327
|
* Confirmation that the keys incremented in once instance of MemoryStore
|
|
335
328
|
* cannot affect other instances.
|
|
336
329
|
*/
|
|
337
|
-
|
|
330
|
+
this.localKeys = true;
|
|
338
331
|
}
|
|
339
332
|
/**
|
|
340
333
|
* Method that initializes the store.
|
|
@@ -343,10 +336,10 @@ var MemoryStore = class {
|
|
|
343
336
|
*/
|
|
344
337
|
init(options) {
|
|
345
338
|
this.windowMs = options.windowMs;
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
this.interval = setInterval(
|
|
349
|
-
|
|
339
|
+
if (this.interval)
|
|
340
|
+
clearInterval(this.interval);
|
|
341
|
+
this.interval = setInterval(() => {
|
|
342
|
+
this.clearExpired();
|
|
350
343
|
}, this.windowMs);
|
|
351
344
|
if (this.interval.unref)
|
|
352
345
|
this.interval.unref();
|
|
@@ -361,12 +354,7 @@ var MemoryStore = class {
|
|
|
361
354
|
* @public
|
|
362
355
|
*/
|
|
363
356
|
async get(key) {
|
|
364
|
-
|
|
365
|
-
return {
|
|
366
|
-
totalHits: this.hits[key],
|
|
367
|
-
resetTime: this.resetTime
|
|
368
|
-
};
|
|
369
|
-
return void 0;
|
|
357
|
+
return this.current.get(key) ?? this.previous.get(key);
|
|
370
358
|
}
|
|
371
359
|
/**
|
|
372
360
|
* Method to increment a client's hit counter.
|
|
@@ -378,13 +366,13 @@ var MemoryStore = class {
|
|
|
378
366
|
* @public
|
|
379
367
|
*/
|
|
380
368
|
async increment(key) {
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
369
|
+
const client = this.getClient(key);
|
|
370
|
+
const now = Date.now();
|
|
371
|
+
if (client.resetTime.getTime() <= now) {
|
|
372
|
+
this.resetClient(client, now);
|
|
373
|
+
}
|
|
374
|
+
client.totalHits++;
|
|
375
|
+
return client;
|
|
388
376
|
}
|
|
389
377
|
/**
|
|
390
378
|
* Method to decrement a client's hit counter.
|
|
@@ -394,9 +382,9 @@ var MemoryStore = class {
|
|
|
394
382
|
* @public
|
|
395
383
|
*/
|
|
396
384
|
async decrement(key) {
|
|
397
|
-
const
|
|
398
|
-
if (
|
|
399
|
-
|
|
385
|
+
const client = this.getClient(key);
|
|
386
|
+
if (client.totalHits > 0)
|
|
387
|
+
client.totalHits--;
|
|
400
388
|
}
|
|
401
389
|
/**
|
|
402
390
|
* Method to reset a client's hit counter.
|
|
@@ -406,7 +394,8 @@ var MemoryStore = class {
|
|
|
406
394
|
* @public
|
|
407
395
|
*/
|
|
408
396
|
async resetKey(key) {
|
|
409
|
-
|
|
397
|
+
this.current.delete(key);
|
|
398
|
+
this.previous.delete(key);
|
|
410
399
|
}
|
|
411
400
|
/**
|
|
412
401
|
* Method to reset everyone's hit counter.
|
|
@@ -414,8 +403,8 @@ var MemoryStore = class {
|
|
|
414
403
|
* @public
|
|
415
404
|
*/
|
|
416
405
|
async resetAll() {
|
|
417
|
-
this.
|
|
418
|
-
this.
|
|
406
|
+
this.current.clear();
|
|
407
|
+
this.previous.clear();
|
|
419
408
|
}
|
|
420
409
|
/**
|
|
421
410
|
* Method to stop the timer (if currently running) and prevent any memory
|
|
@@ -425,6 +414,55 @@ var MemoryStore = class {
|
|
|
425
414
|
*/
|
|
426
415
|
shutdown() {
|
|
427
416
|
clearInterval(this.interval);
|
|
417
|
+
void this.resetAll();
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Recycles a client by setting its hit count to zero, and reset time to
|
|
421
|
+
* `windowMs` milliseconds from now.
|
|
422
|
+
*
|
|
423
|
+
* NOT to be confused with `#resetKey()`, which removes a client from both the
|
|
424
|
+
* `current` and `previous` maps.
|
|
425
|
+
*
|
|
426
|
+
* @param client {Client} - The client to recycle.
|
|
427
|
+
* @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client.
|
|
428
|
+
*
|
|
429
|
+
* @return {Client} - The modified client that was passed in, to allow for chaining.
|
|
430
|
+
*/
|
|
431
|
+
resetClient(client, now = Date.now()) {
|
|
432
|
+
client.totalHits = 0;
|
|
433
|
+
client.resetTime.setTime(now + this.windowMs);
|
|
434
|
+
return client;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Retrieves or creates a client, given a key. Also ensures that the client being
|
|
438
|
+
* returned is in the `current` map.
|
|
439
|
+
*
|
|
440
|
+
* @param key {string} - The key under which the client is (or is to be) stored.
|
|
441
|
+
*
|
|
442
|
+
* @returns {Client} - The requested client.
|
|
443
|
+
*/
|
|
444
|
+
getClient(key) {
|
|
445
|
+
if (this.current.has(key))
|
|
446
|
+
return this.current.get(key);
|
|
447
|
+
let client;
|
|
448
|
+
if (this.previous.has(key)) {
|
|
449
|
+
client = this.previous.get(key);
|
|
450
|
+
this.previous.delete(key);
|
|
451
|
+
} else {
|
|
452
|
+
client = { totalHits: 0, resetTime: /* @__PURE__ */ new Date() };
|
|
453
|
+
this.resetClient(client);
|
|
454
|
+
}
|
|
455
|
+
this.current.set(key, client);
|
|
456
|
+
return client;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Move current clients to previous, create a new map for current.
|
|
460
|
+
*
|
|
461
|
+
* This function is called every `windowMs`.
|
|
462
|
+
*/
|
|
463
|
+
clearExpired() {
|
|
464
|
+
this.previous = this.current;
|
|
465
|
+
this.current = /* @__PURE__ */ new Map();
|
|
428
466
|
}
|
|
429
467
|
};
|
|
430
468
|
|
|
@@ -471,10 +509,10 @@ var promisifyStore = (passedStore) => {
|
|
|
471
509
|
return new PromisifiedStore();
|
|
472
510
|
};
|
|
473
511
|
var getOptionsFromConfig = (config) => {
|
|
474
|
-
const { validations, ...directlyPassableEntries } = config;
|
|
512
|
+
const { validations: validations2, ...directlyPassableEntries } = config;
|
|
475
513
|
return {
|
|
476
514
|
...directlyPassableEntries,
|
|
477
|
-
validate:
|
|
515
|
+
validate: validations2.enabled
|
|
478
516
|
};
|
|
479
517
|
};
|
|
480
518
|
var omitUndefinedOptions = (passedOptions) => {
|
|
@@ -488,32 +526,33 @@ var omitUndefinedOptions = (passedOptions) => {
|
|
|
488
526
|
return omittedOptions;
|
|
489
527
|
};
|
|
490
528
|
var parseOptions = (passedOptions) => {
|
|
491
|
-
var _a, _b, _c, _d;
|
|
492
529
|
const notUndefinedOptions = omitUndefinedOptions(passedOptions);
|
|
493
|
-
const
|
|
494
|
-
|
|
530
|
+
const validations2 = getValidations(notUndefinedOptions?.validate ?? true);
|
|
531
|
+
validations2.validationsConfig();
|
|
532
|
+
validations2.draftPolliHeaders(
|
|
533
|
+
// @ts-expect-error see the note above.
|
|
495
534
|
notUndefinedOptions.draft_polli_ratelimit_headers
|
|
496
535
|
);
|
|
497
|
-
|
|
498
|
-
let standardHeaders =
|
|
499
|
-
if (standardHeaders === true
|
|
536
|
+
validations2.onLimitReached(notUndefinedOptions.onLimitReached);
|
|
537
|
+
let standardHeaders = notUndefinedOptions.standardHeaders ?? false;
|
|
538
|
+
if (standardHeaders === true)
|
|
500
539
|
standardHeaders = "draft-6";
|
|
501
|
-
}
|
|
502
540
|
const config = {
|
|
503
541
|
windowMs: 60 * 1e3,
|
|
504
|
-
|
|
542
|
+
limit: passedOptions.max ?? 5,
|
|
543
|
+
// `max` is deprecated, but support it anyways.
|
|
505
544
|
message: "Too many requests, please try again later.",
|
|
506
545
|
statusCode: 429,
|
|
507
|
-
legacyHeaders:
|
|
546
|
+
legacyHeaders: passedOptions.headers ?? true,
|
|
508
547
|
requestPropertyName: "rateLimit",
|
|
509
548
|
skipFailedRequests: false,
|
|
510
549
|
skipSuccessfulRequests: false,
|
|
511
550
|
requestWasSuccessful: (_request, response) => response.statusCode < 400,
|
|
512
551
|
skip: (_request, _response) => false,
|
|
513
552
|
keyGenerator(request, _response) {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
553
|
+
validations2.ip(request.ip);
|
|
554
|
+
validations2.trustProxy(request);
|
|
555
|
+
validations2.xForwardedForHeader(request);
|
|
517
556
|
return request.ip;
|
|
518
557
|
},
|
|
519
558
|
async handler(request, response, _next, _optionsUsed) {
|
|
@@ -526,17 +565,15 @@ var parseOptions = (passedOptions) => {
|
|
|
526
565
|
response.send(message);
|
|
527
566
|
}
|
|
528
567
|
},
|
|
529
|
-
onLimitReached(_request, _response, _optionsUsed) {
|
|
530
|
-
},
|
|
531
568
|
// Allow the default options to be overriden by the options passed to the middleware.
|
|
532
569
|
...notUndefinedOptions,
|
|
533
570
|
// `standardHeaders` is resolved into a draft version above, use that.
|
|
534
571
|
standardHeaders,
|
|
535
572
|
// Note that this field is declared after the user's options are spread in,
|
|
536
573
|
// so that this field doesn't get overriden with an un-promisified store!
|
|
537
|
-
store: promisifyStore(
|
|
574
|
+
store: promisifyStore(notUndefinedOptions.store ?? new MemoryStore()),
|
|
538
575
|
// Print an error to the console if a few known misconfigurations are detected.
|
|
539
|
-
validations
|
|
576
|
+
validations: validations2
|
|
540
577
|
};
|
|
541
578
|
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") {
|
|
542
579
|
throw new TypeError(
|
|
@@ -553,8 +590,7 @@ var handleAsyncErrors = (fn) => async (request, response, next) => {
|
|
|
553
590
|
}
|
|
554
591
|
};
|
|
555
592
|
var rateLimit = (passedOptions) => {
|
|
556
|
-
|
|
557
|
-
const config = parseOptions(passedOptions != null ? passedOptions : {});
|
|
593
|
+
const config = parseOptions(passedOptions ?? {});
|
|
558
594
|
const options = getOptionsFromConfig(config);
|
|
559
595
|
if (typeof config.store.init === "function")
|
|
560
596
|
config.store.init(options);
|
|
@@ -570,15 +606,20 @@ var rateLimit = (passedOptions) => {
|
|
|
570
606
|
const { totalHits, resetTime } = await config.store.increment(key);
|
|
571
607
|
config.validations.positiveHits(totalHits);
|
|
572
608
|
config.validations.singleCount(request, config.store, key);
|
|
573
|
-
const
|
|
574
|
-
const
|
|
575
|
-
config.validations.
|
|
609
|
+
const retrieveLimit = typeof config.limit === "function" ? config.limit(request, response) : config.limit;
|
|
610
|
+
const limit = await retrieveLimit;
|
|
611
|
+
config.validations.limit(limit);
|
|
576
612
|
const info = {
|
|
577
|
-
limit
|
|
578
|
-
|
|
579
|
-
remaining: Math.max(
|
|
613
|
+
limit,
|
|
614
|
+
used: totalHits,
|
|
615
|
+
remaining: Math.max(limit - totalHits, 0),
|
|
580
616
|
resetTime
|
|
581
617
|
};
|
|
618
|
+
Object.defineProperty(info, "current", {
|
|
619
|
+
configurable: false,
|
|
620
|
+
enumerable: false,
|
|
621
|
+
value: totalHits
|
|
622
|
+
});
|
|
582
623
|
augmentedRequest[config.requestPropertyName] = info;
|
|
583
624
|
if (config.legacyHeaders && !response.headersSent) {
|
|
584
625
|
setLegacyHeaders(response, info);
|
|
@@ -619,11 +660,8 @@ var rateLimit = (passedOptions) => {
|
|
|
619
660
|
});
|
|
620
661
|
}
|
|
621
662
|
}
|
|
622
|
-
if (maxHits && totalHits === maxHits + 1) {
|
|
623
|
-
config.onLimitReached(request, response, options);
|
|
624
|
-
}
|
|
625
663
|
config.validations.disable();
|
|
626
|
-
if (
|
|
664
|
+
if (totalHits > limit) {
|
|
627
665
|
if (config.legacyHeaders || config.standardHeaders) {
|
|
628
666
|
setRetryAfterHeader(response, info, config.windowMs);
|
|
629
667
|
}
|
|
@@ -634,7 +672,7 @@ var rateLimit = (passedOptions) => {
|
|
|
634
672
|
}
|
|
635
673
|
);
|
|
636
674
|
middleware.resetKey = config.store.resetKey.bind(config.store);
|
|
637
|
-
middleware.getKey =
|
|
675
|
+
middleware.getKey = config.store.get?.bind(
|
|
638
676
|
config.store
|
|
639
677
|
);
|
|
640
678
|
return middleware;
|