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 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 source_exports = {};
22
- __export(source_exports, {
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(source_exports);
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
- console.warn(error);
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 overriden by the options passed to the middleware.
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 overriden with an un-promisified store!
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
- if (config.standardHeaders === "draft-6") {
710
- setDraft6Headers(response, info, config.windowMs);
711
- } else if (config.standardHeaders === "draft-7") {
712
- config.validations.headersResetTime(info.resetTime);
713
- setDraft7Headers(response, info, config.windowMs);
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 = "draft-6" | "draft-7";
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 = "draft-6" | "draft-7";
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 = "draft-6" | "draft-7";
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
- console.warn(error);
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 overriden by the options passed to the middleware.
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 overriden with an un-promisified store!
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
- if (config.standardHeaders === "draft-6") {
682
- setDraft6Headers(response, info, config.windowMs);
683
- } else if (config.standardHeaders === "draft-7") {
684
- config.validations.headersResetTime(info.resetTime);
685
- setDraft7Headers(response, info, config.windowMs);
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.4.1",
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 || 5 || ^5.0.0-beta.1"
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.19.5",
90
- "express": "4.21.0",
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-7', // draft-6: `RateLimit-*` headers; draft-7: combined `RateLimit` header
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 | 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'` | Enable the `Ratelimit` header. |
57
- | [`store`] | `Store` | Use a custom store to share hit counts across multiple nodes. |
58
- | [`passOnStoreError`] | `boolean` | Allow (`true`) or block (`false`, default) traffic if the store becomes unavailable. |
59
- | [`keyGenerator`] | `function` | Identify users (defaults to IP address). |
60
- | [`requestPropertyName`] | `string` | Add rate limit info to the `req` object. |
61
- | [`skip`] | `function` | Return `true` to bypass the limiter for the given request. |
62
- | [`skipSuccessfulRequests`] | `boolean` | Uncount 1xx/2xx/3xx responses. |
63
- | [`skipFailedRequests`] | `boolean` | Uncount 4xx/5xx responses. |
64
- | [`requestWasSuccessful`] | `function` | Used by `skipSuccessfulRequests` and `skipFailedRequests`. |
65
- | [`validate`] | `boolean` \| `object` | Enable or disable built-in validation checks. |
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