express-rate-limit 7.4.1 → 7.5.1
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/dist/index.cjs +96 -41
- package/dist/index.d.cts +21 -1
- package/dist/index.d.mts +21 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.mjs +93 -38
- package/package.json +4 -4
- package/readme.md +22 -19
package/dist/index.cjs
CHANGED
|
@@ -18,15 +18,22 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
18
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
19
|
|
|
20
20
|
// source/index.ts
|
|
21
|
-
var
|
|
22
|
-
__export(
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
23
|
MemoryStore: () => MemoryStore,
|
|
24
24
|
default: () => lib_default,
|
|
25
25
|
rateLimit: () => lib_default
|
|
26
26
|
});
|
|
27
|
-
module.exports = __toCommonJS(
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
28
|
|
|
29
29
|
// source/headers.ts
|
|
30
|
+
var import_node_buffer = require("node:buffer");
|
|
31
|
+
var import_node_crypto = require("node:crypto");
|
|
32
|
+
var SUPPORTED_DRAFT_VERSIONS = [
|
|
33
|
+
"draft-6",
|
|
34
|
+
"draft-7",
|
|
35
|
+
"draft-8"
|
|
36
|
+
];
|
|
30
37
|
var getResetSeconds = (resetTime, windowMs) => {
|
|
31
38
|
let resetSeconds = void 0;
|
|
32
39
|
if (resetTime) {
|
|
@@ -37,9 +44,14 @@ var getResetSeconds = (resetTime, windowMs) => {
|
|
|
37
44
|
}
|
|
38
45
|
return resetSeconds;
|
|
39
46
|
};
|
|
47
|
+
var getPartitionKey = (key) => {
|
|
48
|
+
const hash = (0, import_node_crypto.createHash)("sha256");
|
|
49
|
+
hash.update(key);
|
|
50
|
+
const partitionKey = hash.digest("hex").slice(0, 12);
|
|
51
|
+
return import_node_buffer.Buffer.from(partitionKey).toString("base64");
|
|
52
|
+
};
|
|
40
53
|
var setLegacyHeaders = (response, info) => {
|
|
41
|
-
if (response.headersSent)
|
|
42
|
-
return;
|
|
54
|
+
if (response.headersSent) return;
|
|
43
55
|
response.setHeader("X-RateLimit-Limit", info.limit.toString());
|
|
44
56
|
response.setHeader("X-RateLimit-Remaining", info.remaining.toString());
|
|
45
57
|
if (info.resetTime instanceof Date) {
|
|
@@ -51,8 +63,7 @@ var setLegacyHeaders = (response, info) => {
|
|
|
51
63
|
}
|
|
52
64
|
};
|
|
53
65
|
var setDraft6Headers = (response, info, windowMs) => {
|
|
54
|
-
if (response.headersSent)
|
|
55
|
-
return;
|
|
66
|
+
if (response.headersSent) return;
|
|
56
67
|
const windowSeconds = Math.ceil(windowMs / 1e3);
|
|
57
68
|
const resetSeconds = getResetSeconds(info.resetTime);
|
|
58
69
|
response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
|
|
@@ -62,8 +73,7 @@ var setDraft6Headers = (response, info, windowMs) => {
|
|
|
62
73
|
response.setHeader("RateLimit-Reset", resetSeconds.toString());
|
|
63
74
|
};
|
|
64
75
|
var setDraft7Headers = (response, info, windowMs) => {
|
|
65
|
-
if (response.headersSent)
|
|
66
|
-
return;
|
|
76
|
+
if (response.headersSent) return;
|
|
67
77
|
const windowSeconds = Math.ceil(windowMs / 1e3);
|
|
68
78
|
const resetSeconds = getResetSeconds(info.resetTime, windowMs);
|
|
69
79
|
response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
|
|
@@ -72,15 +82,24 @@ var setDraft7Headers = (response, info, windowMs) => {
|
|
|
72
82
|
`limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}`
|
|
73
83
|
);
|
|
74
84
|
};
|
|
85
|
+
var setDraft8Headers = (response, info, windowMs, name, key) => {
|
|
86
|
+
if (response.headersSent) return;
|
|
87
|
+
const windowSeconds = Math.ceil(windowMs / 1e3);
|
|
88
|
+
const resetSeconds = getResetSeconds(info.resetTime, windowMs);
|
|
89
|
+
const partitionKey = getPartitionKey(key);
|
|
90
|
+
const policy = `q=${info.limit}; w=${windowSeconds}; pk=:${partitionKey}:`;
|
|
91
|
+
const header = `r=${info.remaining}; t=${resetSeconds}`;
|
|
92
|
+
response.append("RateLimit-Policy", `"${name}"; ${policy}`);
|
|
93
|
+
response.append("RateLimit", `"${name}"; ${header}`);
|
|
94
|
+
};
|
|
75
95
|
var setRetryAfterHeader = (response, info, windowMs) => {
|
|
76
|
-
if (response.headersSent)
|
|
77
|
-
return;
|
|
96
|
+
if (response.headersSent) return;
|
|
78
97
|
const resetSeconds = getResetSeconds(info.resetTime, windowMs);
|
|
79
98
|
response.setHeader("Retry-After", resetSeconds.toString());
|
|
80
99
|
};
|
|
81
100
|
|
|
82
101
|
// source/validations.ts
|
|
83
|
-
var import_node_net = require("net");
|
|
102
|
+
var import_node_net = require("node:net");
|
|
84
103
|
var ValidationError = class extends Error {
|
|
85
104
|
/**
|
|
86
105
|
* The code must be a string, in snake case and all capital, that starts with
|
|
@@ -108,8 +127,7 @@ var validations = {
|
|
|
108
127
|
},
|
|
109
128
|
// Should be EnabledValidations type, but that's a circular reference
|
|
110
129
|
disable() {
|
|
111
|
-
for (const k of Object.keys(this.enabled))
|
|
112
|
-
this.enabled[k] = false;
|
|
130
|
+
for (const k of Object.keys(this.enabled)) this.enabled[k] = false;
|
|
113
131
|
},
|
|
114
132
|
/**
|
|
115
133
|
* Checks whether the IP address is valid, and that it does not have a port
|
|
@@ -274,6 +292,23 @@ var validations = {
|
|
|
274
292
|
);
|
|
275
293
|
}
|
|
276
294
|
},
|
|
295
|
+
/**
|
|
296
|
+
* Warns the user when an invalid/unsupported version of the draft spec is passed.
|
|
297
|
+
*
|
|
298
|
+
* @param version {any | undefined} - The version passed by the user.
|
|
299
|
+
*
|
|
300
|
+
* @returns {void}
|
|
301
|
+
*/
|
|
302
|
+
headersDraftVersion(version) {
|
|
303
|
+
if (typeof version !== "string" || // @ts-expect-error This is fine. If version is not in the array, it will just return false.
|
|
304
|
+
!SUPPORTED_DRAFT_VERSIONS.includes(version)) {
|
|
305
|
+
const versionString = SUPPORTED_DRAFT_VERSIONS.join(", ");
|
|
306
|
+
throw new ValidationError(
|
|
307
|
+
"ERR_ERL_HEADERS_UNSUPPORTED_DRAFT_VERSION",
|
|
308
|
+
`standardHeaders: only the following versions of the IETF draft specification are supported: ${versionString}.`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
},
|
|
277
312
|
/**
|
|
278
313
|
* Warns the user when the selected headers option requires a reset time but
|
|
279
314
|
* the store does not provide one.
|
|
@@ -364,10 +399,8 @@ var getValidations = (_enabled) => {
|
|
|
364
399
|
args
|
|
365
400
|
);
|
|
366
401
|
} catch (error) {
|
|
367
|
-
if (error instanceof ChangeWarning)
|
|
368
|
-
|
|
369
|
-
else
|
|
370
|
-
console.error(error);
|
|
402
|
+
if (error instanceof ChangeWarning) console.warn(error);
|
|
403
|
+
else console.error(error);
|
|
371
404
|
}
|
|
372
405
|
};
|
|
373
406
|
}
|
|
@@ -402,13 +435,11 @@ var MemoryStore = class {
|
|
|
402
435
|
*/
|
|
403
436
|
init(options) {
|
|
404
437
|
this.windowMs = options.windowMs;
|
|
405
|
-
if (this.interval)
|
|
406
|
-
clearInterval(this.interval);
|
|
438
|
+
if (this.interval) clearInterval(this.interval);
|
|
407
439
|
this.interval = setInterval(() => {
|
|
408
440
|
this.clearExpired();
|
|
409
441
|
}, this.windowMs);
|
|
410
|
-
if (this.interval.unref)
|
|
411
|
-
this.interval.unref();
|
|
442
|
+
if (this.interval.unref) this.interval.unref();
|
|
412
443
|
}
|
|
413
444
|
/**
|
|
414
445
|
* Method to fetch a client's hit count and reset time.
|
|
@@ -449,8 +480,7 @@ var MemoryStore = class {
|
|
|
449
480
|
*/
|
|
450
481
|
async decrement(key) {
|
|
451
482
|
const client = this.getClient(key);
|
|
452
|
-
if (client.totalHits > 0)
|
|
453
|
-
client.totalHits--;
|
|
483
|
+
if (client.totalHits > 0) client.totalHits--;
|
|
454
484
|
}
|
|
455
485
|
/**
|
|
456
486
|
* Method to reset a client's hit counter.
|
|
@@ -508,8 +538,7 @@ var MemoryStore = class {
|
|
|
508
538
|
* @returns {Client} - The requested client.
|
|
509
539
|
*/
|
|
510
540
|
getClient(key) {
|
|
511
|
-
if (this.current.has(key))
|
|
512
|
-
return this.current.get(key);
|
|
541
|
+
if (this.current.has(key)) return this.current.get(key);
|
|
513
542
|
let client;
|
|
514
543
|
if (this.previous.has(key)) {
|
|
515
544
|
client = this.previous.get(key);
|
|
@@ -549,8 +578,7 @@ var promisifyStore = (passedStore) => {
|
|
|
549
578
|
legacyStore.incr(
|
|
550
579
|
key,
|
|
551
580
|
(error, totalHits, resetTime) => {
|
|
552
|
-
if (error)
|
|
553
|
-
reject(error);
|
|
581
|
+
if (error) reject(error);
|
|
554
582
|
resolve({ totalHits, resetTime });
|
|
555
583
|
}
|
|
556
584
|
);
|
|
@@ -597,8 +625,7 @@ var parseOptions = (passedOptions) => {
|
|
|
597
625
|
);
|
|
598
626
|
validations2.onLimitReached(notUndefinedOptions.onLimitReached);
|
|
599
627
|
let standardHeaders = notUndefinedOptions.standardHeaders ?? false;
|
|
600
|
-
if (standardHeaders === true)
|
|
601
|
-
standardHeaders = "draft-6";
|
|
628
|
+
if (standardHeaders === true) standardHeaders = "draft-6";
|
|
602
629
|
const config = {
|
|
603
630
|
windowMs: 60 * 1e3,
|
|
604
631
|
limit: passedOptions.max ?? 5,
|
|
@@ -606,6 +633,20 @@ var parseOptions = (passedOptions) => {
|
|
|
606
633
|
message: "Too many requests, please try again later.",
|
|
607
634
|
statusCode: 429,
|
|
608
635
|
legacyHeaders: passedOptions.headers ?? true,
|
|
636
|
+
identifier(request, _response) {
|
|
637
|
+
let duration = "";
|
|
638
|
+
const property = config.requestPropertyName;
|
|
639
|
+
const { limit } = request[property];
|
|
640
|
+
const seconds = config.windowMs / 1e3;
|
|
641
|
+
const minutes = config.windowMs / (1e3 * 60);
|
|
642
|
+
const hours = config.windowMs / (1e3 * 60 * 60);
|
|
643
|
+
const days = config.windowMs / (1e3 * 60 * 60 * 24);
|
|
644
|
+
if (seconds < 60) duration = `${seconds}sec`;
|
|
645
|
+
else if (minutes < 60) duration = `${minutes}min`;
|
|
646
|
+
else if (hours < 24) duration = `${hours}hr${hours > 1 ? "s" : ""}`;
|
|
647
|
+
else duration = `${days}day${days > 1 ? "s" : ""}`;
|
|
648
|
+
return `${limit}-in-${duration}`;
|
|
649
|
+
},
|
|
609
650
|
requestPropertyName: "rateLimit",
|
|
610
651
|
skipFailedRequests: false,
|
|
611
652
|
skipSuccessfulRequests: false,
|
|
@@ -628,12 +669,12 @@ var parseOptions = (passedOptions) => {
|
|
|
628
669
|
}
|
|
629
670
|
},
|
|
630
671
|
passOnStoreError: false,
|
|
631
|
-
// Allow the default options to be
|
|
672
|
+
// Allow the default options to be overridden by the passed options.
|
|
632
673
|
...notUndefinedOptions,
|
|
633
674
|
// `standardHeaders` is resolved into a draft version above, use that.
|
|
634
675
|
standardHeaders,
|
|
635
676
|
// Note that this field is declared after the user's options are spread in,
|
|
636
|
-
// so that this field doesn't get
|
|
677
|
+
// so that this field doesn't get overridden with an un-promisified store!
|
|
637
678
|
store: promisifyStore(notUndefinedOptions.store ?? new MemoryStore()),
|
|
638
679
|
// Print an error to the console if a few known misconfigurations are detected.
|
|
639
680
|
validations: validations2
|
|
@@ -657,8 +698,7 @@ var rateLimit = (passedOptions) => {
|
|
|
657
698
|
const options = getOptionsFromConfig(config);
|
|
658
699
|
config.validations.creationStack(config.store);
|
|
659
700
|
config.validations.unsharedStore(config.store);
|
|
660
|
-
if (typeof config.store.init === "function")
|
|
661
|
-
config.store.init(options);
|
|
701
|
+
if (typeof config.store.init === "function") config.store.init(options);
|
|
662
702
|
const middleware = handleAsyncErrors(
|
|
663
703
|
async (request, response, next) => {
|
|
664
704
|
const skip = await config.skip(request, response);
|
|
@@ -706,11 +746,27 @@ var rateLimit = (passedOptions) => {
|
|
|
706
746
|
setLegacyHeaders(response, info);
|
|
707
747
|
}
|
|
708
748
|
if (config.standardHeaders && !response.headersSent) {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
749
|
+
switch (config.standardHeaders) {
|
|
750
|
+
case "draft-6": {
|
|
751
|
+
setDraft6Headers(response, info, config.windowMs);
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
case "draft-7": {
|
|
755
|
+
config.validations.headersResetTime(info.resetTime);
|
|
756
|
+
setDraft7Headers(response, info, config.windowMs);
|
|
757
|
+
break;
|
|
758
|
+
}
|
|
759
|
+
case "draft-8": {
|
|
760
|
+
const retrieveName = typeof config.identifier === "function" ? config.identifier(request, response) : config.identifier;
|
|
761
|
+
const name = await retrieveName;
|
|
762
|
+
config.validations.headersResetTime(info.resetTime);
|
|
763
|
+
setDraft8Headers(response, info, config.windowMs, name, key);
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
default: {
|
|
767
|
+
config.validations.headersDraftVersion(config.standardHeaders);
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
714
770
|
}
|
|
715
771
|
}
|
|
716
772
|
if (config.skipFailedRequests || config.skipSuccessfulRequests) {
|
|
@@ -727,8 +783,7 @@ var rateLimit = (passedOptions) => {
|
|
|
727
783
|
await decrementKey();
|
|
728
784
|
});
|
|
729
785
|
response.on("close", async () => {
|
|
730
|
-
if (!response.writableEnded)
|
|
731
|
-
await decrementKey();
|
|
786
|
+
if (!response.writableEnded) await decrementKey();
|
|
732
787
|
});
|
|
733
788
|
response.on("error", async () => {
|
|
734
789
|
await decrementKey();
|
package/dist/index.d.cts
CHANGED
|
@@ -86,6 +86,14 @@ declare const validations: {
|
|
|
86
86
|
* @returns {void}
|
|
87
87
|
*/
|
|
88
88
|
onLimitReached(onLimitReached?: any): void;
|
|
89
|
+
/**
|
|
90
|
+
* Warns the user when an invalid/unsupported version of the draft spec is passed.
|
|
91
|
+
*
|
|
92
|
+
* @param version {any | undefined} - The version passed by the user.
|
|
93
|
+
*
|
|
94
|
+
* @returns {void}
|
|
95
|
+
*/
|
|
96
|
+
headersDraftVersion(version?: any): void;
|
|
89
97
|
/**
|
|
90
98
|
* Warns the user when the selected headers option requires a reset time but
|
|
91
99
|
* the store does not provide one.
|
|
@@ -111,6 +119,11 @@ declare const validations: {
|
|
|
111
119
|
creationStack(store: Store): void;
|
|
112
120
|
};
|
|
113
121
|
export type Validations = typeof validations;
|
|
122
|
+
declare const SUPPORTED_DRAFT_VERSIONS: readonly [
|
|
123
|
+
"draft-6",
|
|
124
|
+
"draft-7",
|
|
125
|
+
"draft-8"
|
|
126
|
+
];
|
|
114
127
|
/**
|
|
115
128
|
* Callback that fires when a client's hit counter is incremented.
|
|
116
129
|
*
|
|
@@ -271,7 +284,7 @@ export type Store = {
|
|
|
271
284
|
*/
|
|
272
285
|
prefix?: string;
|
|
273
286
|
};
|
|
274
|
-
export type DraftHeadersVersion =
|
|
287
|
+
export type DraftHeadersVersion = (typeof SUPPORTED_DRAFT_VERSIONS)[number];
|
|
275
288
|
/**
|
|
276
289
|
* Validate configuration object for enabling or disabling specific validations.
|
|
277
290
|
*
|
|
@@ -326,6 +339,13 @@ export type Options = {
|
|
|
326
339
|
* Defaults to `false` (for backward compatibility, but its use is recommended).
|
|
327
340
|
*/
|
|
328
341
|
standardHeaders: boolean | DraftHeadersVersion;
|
|
342
|
+
/**
|
|
343
|
+
* The name used to identify the quota policy in the `RateLimit` headers as per
|
|
344
|
+
* the 8th draft of the IETF specification.
|
|
345
|
+
*
|
|
346
|
+
* Defaults to `{limit}-in-{window}`.
|
|
347
|
+
*/
|
|
348
|
+
identifier: string | ValueDeterminingMiddleware<string>;
|
|
329
349
|
/**
|
|
330
350
|
* The name of the property on the request object to store the rate limit info.
|
|
331
351
|
*
|
package/dist/index.d.mts
CHANGED
|
@@ -86,6 +86,14 @@ declare const validations: {
|
|
|
86
86
|
* @returns {void}
|
|
87
87
|
*/
|
|
88
88
|
onLimitReached(onLimitReached?: any): void;
|
|
89
|
+
/**
|
|
90
|
+
* Warns the user when an invalid/unsupported version of the draft spec is passed.
|
|
91
|
+
*
|
|
92
|
+
* @param version {any | undefined} - The version passed by the user.
|
|
93
|
+
*
|
|
94
|
+
* @returns {void}
|
|
95
|
+
*/
|
|
96
|
+
headersDraftVersion(version?: any): void;
|
|
89
97
|
/**
|
|
90
98
|
* Warns the user when the selected headers option requires a reset time but
|
|
91
99
|
* the store does not provide one.
|
|
@@ -111,6 +119,11 @@ declare const validations: {
|
|
|
111
119
|
creationStack(store: Store): void;
|
|
112
120
|
};
|
|
113
121
|
export type Validations = typeof validations;
|
|
122
|
+
declare const SUPPORTED_DRAFT_VERSIONS: readonly [
|
|
123
|
+
"draft-6",
|
|
124
|
+
"draft-7",
|
|
125
|
+
"draft-8"
|
|
126
|
+
];
|
|
114
127
|
/**
|
|
115
128
|
* Callback that fires when a client's hit counter is incremented.
|
|
116
129
|
*
|
|
@@ -271,7 +284,7 @@ export type Store = {
|
|
|
271
284
|
*/
|
|
272
285
|
prefix?: string;
|
|
273
286
|
};
|
|
274
|
-
export type DraftHeadersVersion =
|
|
287
|
+
export type DraftHeadersVersion = (typeof SUPPORTED_DRAFT_VERSIONS)[number];
|
|
275
288
|
/**
|
|
276
289
|
* Validate configuration object for enabling or disabling specific validations.
|
|
277
290
|
*
|
|
@@ -326,6 +339,13 @@ export type Options = {
|
|
|
326
339
|
* Defaults to `false` (for backward compatibility, but its use is recommended).
|
|
327
340
|
*/
|
|
328
341
|
standardHeaders: boolean | DraftHeadersVersion;
|
|
342
|
+
/**
|
|
343
|
+
* The name used to identify the quota policy in the `RateLimit` headers as per
|
|
344
|
+
* the 8th draft of the IETF specification.
|
|
345
|
+
*
|
|
346
|
+
* Defaults to `{limit}-in-{window}`.
|
|
347
|
+
*/
|
|
348
|
+
identifier: string | ValueDeterminingMiddleware<string>;
|
|
329
349
|
/**
|
|
330
350
|
* The name of the property on the request object to store the rate limit info.
|
|
331
351
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -86,6 +86,14 @@ declare const validations: {
|
|
|
86
86
|
* @returns {void}
|
|
87
87
|
*/
|
|
88
88
|
onLimitReached(onLimitReached?: any): void;
|
|
89
|
+
/**
|
|
90
|
+
* Warns the user when an invalid/unsupported version of the draft spec is passed.
|
|
91
|
+
*
|
|
92
|
+
* @param version {any | undefined} - The version passed by the user.
|
|
93
|
+
*
|
|
94
|
+
* @returns {void}
|
|
95
|
+
*/
|
|
96
|
+
headersDraftVersion(version?: any): void;
|
|
89
97
|
/**
|
|
90
98
|
* Warns the user when the selected headers option requires a reset time but
|
|
91
99
|
* the store does not provide one.
|
|
@@ -111,6 +119,11 @@ declare const validations: {
|
|
|
111
119
|
creationStack(store: Store): void;
|
|
112
120
|
};
|
|
113
121
|
export type Validations = typeof validations;
|
|
122
|
+
declare const SUPPORTED_DRAFT_VERSIONS: readonly [
|
|
123
|
+
"draft-6",
|
|
124
|
+
"draft-7",
|
|
125
|
+
"draft-8"
|
|
126
|
+
];
|
|
114
127
|
/**
|
|
115
128
|
* Callback that fires when a client's hit counter is incremented.
|
|
116
129
|
*
|
|
@@ -271,7 +284,7 @@ export type Store = {
|
|
|
271
284
|
*/
|
|
272
285
|
prefix?: string;
|
|
273
286
|
};
|
|
274
|
-
export type DraftHeadersVersion =
|
|
287
|
+
export type DraftHeadersVersion = (typeof SUPPORTED_DRAFT_VERSIONS)[number];
|
|
275
288
|
/**
|
|
276
289
|
* Validate configuration object for enabling or disabling specific validations.
|
|
277
290
|
*
|
|
@@ -326,6 +339,13 @@ export type Options = {
|
|
|
326
339
|
* Defaults to `false` (for backward compatibility, but its use is recommended).
|
|
327
340
|
*/
|
|
328
341
|
standardHeaders: boolean | DraftHeadersVersion;
|
|
342
|
+
/**
|
|
343
|
+
* The name used to identify the quota policy in the `RateLimit` headers as per
|
|
344
|
+
* the 8th draft of the IETF specification.
|
|
345
|
+
*
|
|
346
|
+
* Defaults to `{limit}-in-{window}`.
|
|
347
|
+
*/
|
|
348
|
+
identifier: string | ValueDeterminingMiddleware<string>;
|
|
329
349
|
/**
|
|
330
350
|
* The name of the property on the request object to store the rate limit info.
|
|
331
351
|
*
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
// source/headers.ts
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
var SUPPORTED_DRAFT_VERSIONS = [
|
|
5
|
+
"draft-6",
|
|
6
|
+
"draft-7",
|
|
7
|
+
"draft-8"
|
|
8
|
+
];
|
|
2
9
|
var getResetSeconds = (resetTime, windowMs) => {
|
|
3
10
|
let resetSeconds = void 0;
|
|
4
11
|
if (resetTime) {
|
|
@@ -9,9 +16,14 @@ var getResetSeconds = (resetTime, windowMs) => {
|
|
|
9
16
|
}
|
|
10
17
|
return resetSeconds;
|
|
11
18
|
};
|
|
19
|
+
var getPartitionKey = (key) => {
|
|
20
|
+
const hash = createHash("sha256");
|
|
21
|
+
hash.update(key);
|
|
22
|
+
const partitionKey = hash.digest("hex").slice(0, 12);
|
|
23
|
+
return Buffer.from(partitionKey).toString("base64");
|
|
24
|
+
};
|
|
12
25
|
var setLegacyHeaders = (response, info) => {
|
|
13
|
-
if (response.headersSent)
|
|
14
|
-
return;
|
|
26
|
+
if (response.headersSent) return;
|
|
15
27
|
response.setHeader("X-RateLimit-Limit", info.limit.toString());
|
|
16
28
|
response.setHeader("X-RateLimit-Remaining", info.remaining.toString());
|
|
17
29
|
if (info.resetTime instanceof Date) {
|
|
@@ -23,8 +35,7 @@ var setLegacyHeaders = (response, info) => {
|
|
|
23
35
|
}
|
|
24
36
|
};
|
|
25
37
|
var setDraft6Headers = (response, info, windowMs) => {
|
|
26
|
-
if (response.headersSent)
|
|
27
|
-
return;
|
|
38
|
+
if (response.headersSent) return;
|
|
28
39
|
const windowSeconds = Math.ceil(windowMs / 1e3);
|
|
29
40
|
const resetSeconds = getResetSeconds(info.resetTime);
|
|
30
41
|
response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
|
|
@@ -34,8 +45,7 @@ var setDraft6Headers = (response, info, windowMs) => {
|
|
|
34
45
|
response.setHeader("RateLimit-Reset", resetSeconds.toString());
|
|
35
46
|
};
|
|
36
47
|
var setDraft7Headers = (response, info, windowMs) => {
|
|
37
|
-
if (response.headersSent)
|
|
38
|
-
return;
|
|
48
|
+
if (response.headersSent) return;
|
|
39
49
|
const windowSeconds = Math.ceil(windowMs / 1e3);
|
|
40
50
|
const resetSeconds = getResetSeconds(info.resetTime, windowMs);
|
|
41
51
|
response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
|
|
@@ -44,15 +54,24 @@ var setDraft7Headers = (response, info, windowMs) => {
|
|
|
44
54
|
`limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}`
|
|
45
55
|
);
|
|
46
56
|
};
|
|
57
|
+
var setDraft8Headers = (response, info, windowMs, name, key) => {
|
|
58
|
+
if (response.headersSent) return;
|
|
59
|
+
const windowSeconds = Math.ceil(windowMs / 1e3);
|
|
60
|
+
const resetSeconds = getResetSeconds(info.resetTime, windowMs);
|
|
61
|
+
const partitionKey = getPartitionKey(key);
|
|
62
|
+
const policy = `q=${info.limit}; w=${windowSeconds}; pk=:${partitionKey}:`;
|
|
63
|
+
const header = `r=${info.remaining}; t=${resetSeconds}`;
|
|
64
|
+
response.append("RateLimit-Policy", `"${name}"; ${policy}`);
|
|
65
|
+
response.append("RateLimit", `"${name}"; ${header}`);
|
|
66
|
+
};
|
|
47
67
|
var setRetryAfterHeader = (response, info, windowMs) => {
|
|
48
|
-
if (response.headersSent)
|
|
49
|
-
return;
|
|
68
|
+
if (response.headersSent) return;
|
|
50
69
|
const resetSeconds = getResetSeconds(info.resetTime, windowMs);
|
|
51
70
|
response.setHeader("Retry-After", resetSeconds.toString());
|
|
52
71
|
};
|
|
53
72
|
|
|
54
73
|
// source/validations.ts
|
|
55
|
-
import { isIP } from "net";
|
|
74
|
+
import { isIP } from "node:net";
|
|
56
75
|
var ValidationError = class extends Error {
|
|
57
76
|
/**
|
|
58
77
|
* The code must be a string, in snake case and all capital, that starts with
|
|
@@ -80,8 +99,7 @@ var validations = {
|
|
|
80
99
|
},
|
|
81
100
|
// Should be EnabledValidations type, but that's a circular reference
|
|
82
101
|
disable() {
|
|
83
|
-
for (const k of Object.keys(this.enabled))
|
|
84
|
-
this.enabled[k] = false;
|
|
102
|
+
for (const k of Object.keys(this.enabled)) this.enabled[k] = false;
|
|
85
103
|
},
|
|
86
104
|
/**
|
|
87
105
|
* Checks whether the IP address is valid, and that it does not have a port
|
|
@@ -246,6 +264,23 @@ var validations = {
|
|
|
246
264
|
);
|
|
247
265
|
}
|
|
248
266
|
},
|
|
267
|
+
/**
|
|
268
|
+
* Warns the user when an invalid/unsupported version of the draft spec is passed.
|
|
269
|
+
*
|
|
270
|
+
* @param version {any | undefined} - The version passed by the user.
|
|
271
|
+
*
|
|
272
|
+
* @returns {void}
|
|
273
|
+
*/
|
|
274
|
+
headersDraftVersion(version) {
|
|
275
|
+
if (typeof version !== "string" || // @ts-expect-error This is fine. If version is not in the array, it will just return false.
|
|
276
|
+
!SUPPORTED_DRAFT_VERSIONS.includes(version)) {
|
|
277
|
+
const versionString = SUPPORTED_DRAFT_VERSIONS.join(", ");
|
|
278
|
+
throw new ValidationError(
|
|
279
|
+
"ERR_ERL_HEADERS_UNSUPPORTED_DRAFT_VERSION",
|
|
280
|
+
`standardHeaders: only the following versions of the IETF draft specification are supported: ${versionString}.`
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
},
|
|
249
284
|
/**
|
|
250
285
|
* Warns the user when the selected headers option requires a reset time but
|
|
251
286
|
* the store does not provide one.
|
|
@@ -336,10 +371,8 @@ var getValidations = (_enabled) => {
|
|
|
336
371
|
args
|
|
337
372
|
);
|
|
338
373
|
} catch (error) {
|
|
339
|
-
if (error instanceof ChangeWarning)
|
|
340
|
-
|
|
341
|
-
else
|
|
342
|
-
console.error(error);
|
|
374
|
+
if (error instanceof ChangeWarning) console.warn(error);
|
|
375
|
+
else console.error(error);
|
|
343
376
|
}
|
|
344
377
|
};
|
|
345
378
|
}
|
|
@@ -374,13 +407,11 @@ var MemoryStore = class {
|
|
|
374
407
|
*/
|
|
375
408
|
init(options) {
|
|
376
409
|
this.windowMs = options.windowMs;
|
|
377
|
-
if (this.interval)
|
|
378
|
-
clearInterval(this.interval);
|
|
410
|
+
if (this.interval) clearInterval(this.interval);
|
|
379
411
|
this.interval = setInterval(() => {
|
|
380
412
|
this.clearExpired();
|
|
381
413
|
}, this.windowMs);
|
|
382
|
-
if (this.interval.unref)
|
|
383
|
-
this.interval.unref();
|
|
414
|
+
if (this.interval.unref) this.interval.unref();
|
|
384
415
|
}
|
|
385
416
|
/**
|
|
386
417
|
* Method to fetch a client's hit count and reset time.
|
|
@@ -421,8 +452,7 @@ var MemoryStore = class {
|
|
|
421
452
|
*/
|
|
422
453
|
async decrement(key) {
|
|
423
454
|
const client = this.getClient(key);
|
|
424
|
-
if (client.totalHits > 0)
|
|
425
|
-
client.totalHits--;
|
|
455
|
+
if (client.totalHits > 0) client.totalHits--;
|
|
426
456
|
}
|
|
427
457
|
/**
|
|
428
458
|
* Method to reset a client's hit counter.
|
|
@@ -480,8 +510,7 @@ var MemoryStore = class {
|
|
|
480
510
|
* @returns {Client} - The requested client.
|
|
481
511
|
*/
|
|
482
512
|
getClient(key) {
|
|
483
|
-
if (this.current.has(key))
|
|
484
|
-
return this.current.get(key);
|
|
513
|
+
if (this.current.has(key)) return this.current.get(key);
|
|
485
514
|
let client;
|
|
486
515
|
if (this.previous.has(key)) {
|
|
487
516
|
client = this.previous.get(key);
|
|
@@ -521,8 +550,7 @@ var promisifyStore = (passedStore) => {
|
|
|
521
550
|
legacyStore.incr(
|
|
522
551
|
key,
|
|
523
552
|
(error, totalHits, resetTime) => {
|
|
524
|
-
if (error)
|
|
525
|
-
reject(error);
|
|
553
|
+
if (error) reject(error);
|
|
526
554
|
resolve({ totalHits, resetTime });
|
|
527
555
|
}
|
|
528
556
|
);
|
|
@@ -569,8 +597,7 @@ var parseOptions = (passedOptions) => {
|
|
|
569
597
|
);
|
|
570
598
|
validations2.onLimitReached(notUndefinedOptions.onLimitReached);
|
|
571
599
|
let standardHeaders = notUndefinedOptions.standardHeaders ?? false;
|
|
572
|
-
if (standardHeaders === true)
|
|
573
|
-
standardHeaders = "draft-6";
|
|
600
|
+
if (standardHeaders === true) standardHeaders = "draft-6";
|
|
574
601
|
const config = {
|
|
575
602
|
windowMs: 60 * 1e3,
|
|
576
603
|
limit: passedOptions.max ?? 5,
|
|
@@ -578,6 +605,20 @@ var parseOptions = (passedOptions) => {
|
|
|
578
605
|
message: "Too many requests, please try again later.",
|
|
579
606
|
statusCode: 429,
|
|
580
607
|
legacyHeaders: passedOptions.headers ?? true,
|
|
608
|
+
identifier(request, _response) {
|
|
609
|
+
let duration = "";
|
|
610
|
+
const property = config.requestPropertyName;
|
|
611
|
+
const { limit } = request[property];
|
|
612
|
+
const seconds = config.windowMs / 1e3;
|
|
613
|
+
const minutes = config.windowMs / (1e3 * 60);
|
|
614
|
+
const hours = config.windowMs / (1e3 * 60 * 60);
|
|
615
|
+
const days = config.windowMs / (1e3 * 60 * 60 * 24);
|
|
616
|
+
if (seconds < 60) duration = `${seconds}sec`;
|
|
617
|
+
else if (minutes < 60) duration = `${minutes}min`;
|
|
618
|
+
else if (hours < 24) duration = `${hours}hr${hours > 1 ? "s" : ""}`;
|
|
619
|
+
else duration = `${days}day${days > 1 ? "s" : ""}`;
|
|
620
|
+
return `${limit}-in-${duration}`;
|
|
621
|
+
},
|
|
581
622
|
requestPropertyName: "rateLimit",
|
|
582
623
|
skipFailedRequests: false,
|
|
583
624
|
skipSuccessfulRequests: false,
|
|
@@ -600,12 +641,12 @@ var parseOptions = (passedOptions) => {
|
|
|
600
641
|
}
|
|
601
642
|
},
|
|
602
643
|
passOnStoreError: false,
|
|
603
|
-
// Allow the default options to be
|
|
644
|
+
// Allow the default options to be overridden by the passed options.
|
|
604
645
|
...notUndefinedOptions,
|
|
605
646
|
// `standardHeaders` is resolved into a draft version above, use that.
|
|
606
647
|
standardHeaders,
|
|
607
648
|
// Note that this field is declared after the user's options are spread in,
|
|
608
|
-
// so that this field doesn't get
|
|
649
|
+
// so that this field doesn't get overridden with an un-promisified store!
|
|
609
650
|
store: promisifyStore(notUndefinedOptions.store ?? new MemoryStore()),
|
|
610
651
|
// Print an error to the console if a few known misconfigurations are detected.
|
|
611
652
|
validations: validations2
|
|
@@ -629,8 +670,7 @@ var rateLimit = (passedOptions) => {
|
|
|
629
670
|
const options = getOptionsFromConfig(config);
|
|
630
671
|
config.validations.creationStack(config.store);
|
|
631
672
|
config.validations.unsharedStore(config.store);
|
|
632
|
-
if (typeof config.store.init === "function")
|
|
633
|
-
config.store.init(options);
|
|
673
|
+
if (typeof config.store.init === "function") config.store.init(options);
|
|
634
674
|
const middleware = handleAsyncErrors(
|
|
635
675
|
async (request, response, next) => {
|
|
636
676
|
const skip = await config.skip(request, response);
|
|
@@ -678,11 +718,27 @@ var rateLimit = (passedOptions) => {
|
|
|
678
718
|
setLegacyHeaders(response, info);
|
|
679
719
|
}
|
|
680
720
|
if (config.standardHeaders && !response.headersSent) {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
721
|
+
switch (config.standardHeaders) {
|
|
722
|
+
case "draft-6": {
|
|
723
|
+
setDraft6Headers(response, info, config.windowMs);
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
726
|
+
case "draft-7": {
|
|
727
|
+
config.validations.headersResetTime(info.resetTime);
|
|
728
|
+
setDraft7Headers(response, info, config.windowMs);
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
case "draft-8": {
|
|
732
|
+
const retrieveName = typeof config.identifier === "function" ? config.identifier(request, response) : config.identifier;
|
|
733
|
+
const name = await retrieveName;
|
|
734
|
+
config.validations.headersResetTime(info.resetTime);
|
|
735
|
+
setDraft8Headers(response, info, config.windowMs, name, key);
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
default: {
|
|
739
|
+
config.validations.headersDraftVersion(config.standardHeaders);
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
686
742
|
}
|
|
687
743
|
}
|
|
688
744
|
if (config.skipFailedRequests || config.skipSuccessfulRequests) {
|
|
@@ -699,8 +755,7 @@ var rateLimit = (passedOptions) => {
|
|
|
699
755
|
await decrementKey();
|
|
700
756
|
});
|
|
701
757
|
response.on("close", async () => {
|
|
702
|
-
if (!response.writableEnded)
|
|
703
|
-
await decrementKey();
|
|
758
|
+
if (!response.writableEnded) await decrementKey();
|
|
704
759
|
});
|
|
705
760
|
response.on("error", async () => {
|
|
706
761
|
await decrementKey();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "express-rate-limit",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.5.1",
|
|
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",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"prepare": "run-s compile && husky install config/husky"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
|
-
"express": "4
|
|
77
|
+
"express": ">= 4.11"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
80
|
"@express-rate-limit/prettier": "1.1.1",
|
|
@@ -86,8 +86,8 @@
|
|
|
86
86
|
"@types/supertest": "2.0.15",
|
|
87
87
|
"del-cli": "5.1.0",
|
|
88
88
|
"dts-bundle-generator": "8.0.1",
|
|
89
|
-
"esbuild": "0.
|
|
90
|
-
"express": "4.21.
|
|
89
|
+
"esbuild": "0.25.0",
|
|
90
|
+
"express": "4.21.1",
|
|
91
91
|
"husky": "8.0.3",
|
|
92
92
|
"jest": "29.7.0",
|
|
93
93
|
"lint-staged": "15.0.2",
|
package/readme.md
CHANGED
|
@@ -26,7 +26,7 @@ import { rateLimit } from 'express-rate-limit'
|
|
|
26
26
|
const limiter = rateLimit({
|
|
27
27
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
28
28
|
limit: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes).
|
|
29
|
-
standardHeaders: 'draft-
|
|
29
|
+
standardHeaders: 'draft-8', // draft-6: `RateLimit-*` headers; draft-7 & draft-8: combined `RateLimit` header
|
|
30
30
|
legacyHeaders: false, // Disable the `X-RateLimit-*` headers.
|
|
31
31
|
// store: ... , // Redis, Memcached, etc. See below.
|
|
32
32
|
})
|
|
@@ -45,24 +45,25 @@ The rate limiter comes with a built-in memory store, and supports a variety of
|
|
|
45
45
|
All function options may be async. Click the name for additional info and
|
|
46
46
|
default values.
|
|
47
47
|
|
|
48
|
-
| Option | Type
|
|
49
|
-
| -------------------------- |
|
|
50
|
-
| [`windowMs`] | `number`
|
|
51
|
-
| [`limit`] | `number` \| `function`
|
|
52
|
-
| [`message`] | `string` \| `json` \| `function`
|
|
53
|
-
| [`statusCode`] | `number`
|
|
54
|
-
| [`handler`] | `function`
|
|
55
|
-
| [`legacyHeaders`] | `boolean`
|
|
56
|
-
| [`standardHeaders`] | `'draft-6'` \| `'draft-7'`
|
|
57
|
-
| [`
|
|
58
|
-
| [`
|
|
59
|
-
| [`
|
|
60
|
-
| [`
|
|
61
|
-
| [`
|
|
62
|
-
| [`
|
|
63
|
-
| [`
|
|
64
|
-
| [`
|
|
65
|
-
| [`
|
|
48
|
+
| Option | Type | Remarks |
|
|
49
|
+
| -------------------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------- |
|
|
50
|
+
| [`windowMs`] | `number` | How long to remember requests for, in milliseconds. |
|
|
51
|
+
| [`limit`] | `number` \| `function` | How many requests to allow. |
|
|
52
|
+
| [`message`] | `string` \| `json` \| `function` | Response to return after limit is reached. |
|
|
53
|
+
| [`statusCode`] | `number` | HTTP status code after limit is reached (default is 429). |
|
|
54
|
+
| [`handler`] | `function` | Function to run after limit is reached (overrides `message` and `statusCode` settings, if set). |
|
|
55
|
+
| [`legacyHeaders`] | `boolean` | Enable the `X-Rate-Limit` header. |
|
|
56
|
+
| [`standardHeaders`] | `'draft-6'` \| `'draft-7'` \| `'draft-8'` | Enable the `Ratelimit` header. |
|
|
57
|
+
| [`identifier`] | `string` \| `function` | Name associated with the quota policy enforced by this rate limiter. |
|
|
58
|
+
| [`store`] | `Store` | Use a custom store to share hit counts across multiple nodes. |
|
|
59
|
+
| [`passOnStoreError`] | `boolean` | Allow (`true`) or block (`false`, default) traffic if the store becomes unavailable. |
|
|
60
|
+
| [`keyGenerator`] | `function` | Identify users (defaults to IP address). |
|
|
61
|
+
| [`requestPropertyName`] | `string` | Add rate limit info to the `req` object. |
|
|
62
|
+
| [`skip`] | `function` | Return `true` to bypass the limiter for the given request. |
|
|
63
|
+
| [`skipSuccessfulRequests`] | `boolean` | Uncount 1xx/2xx/3xx responses. |
|
|
64
|
+
| [`skipFailedRequests`] | `boolean` | Uncount 4xx/5xx responses. |
|
|
65
|
+
| [`requestWasSuccessful`] | `function` | Used by `skipSuccessfulRequests` and `skipFailedRequests`. |
|
|
66
|
+
| [`validate`] | `boolean` \| `object` | Enable or disable built-in validation checks. |
|
|
66
67
|
|
|
67
68
|
## Thank You
|
|
68
69
|
|
|
@@ -126,6 +127,8 @@ MIT © [Nathan Friedly](http://nfriedly.com/),
|
|
|
126
127
|
https://express-rate-limit.mintlify.app/reference/configuration#legacyheaders
|
|
127
128
|
[`standardHeaders`]:
|
|
128
129
|
https://express-rate-limit.mintlify.app/reference/configuration#standardheaders
|
|
130
|
+
[`identifier`]:
|
|
131
|
+
https://express-rate-limit.mintlify.app/reference/configuration#identifier
|
|
129
132
|
[`store`]: https://express-rate-limit.mintlify.app/reference/configuration#store
|
|
130
133
|
[`passOnStoreError`]:
|
|
131
134
|
https://express-rate-limit.mintlify.app/reference/configuration#passOnStoreError
|