express-rate-limit 7.4.1 → 7.5.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/dist/index.cjs CHANGED
@@ -27,6 +27,9 @@ __export(source_exports, {
27
27
  module.exports = __toCommonJS(source_exports);
28
28
 
29
29
  // source/headers.ts
30
+ var import_node_buffer = require("buffer");
31
+ var import_node_crypto = require("crypto");
32
+ var SUPPORTED_DRAFT_VERSIONS = ["draft-6", "draft-7", "draft-8"];
30
33
  var getResetSeconds = (resetTime, windowMs) => {
31
34
  let resetSeconds = void 0;
32
35
  if (resetTime) {
@@ -37,6 +40,12 @@ var getResetSeconds = (resetTime, windowMs) => {
37
40
  }
38
41
  return resetSeconds;
39
42
  };
43
+ var getPartitionKey = (key) => {
44
+ const hash = (0, import_node_crypto.createHash)("sha256");
45
+ hash.update(key);
46
+ const partitionKey = hash.digest("hex").slice(0, 12);
47
+ return import_node_buffer.Buffer.from(partitionKey).toString("base64");
48
+ };
40
49
  var setLegacyHeaders = (response, info) => {
41
50
  if (response.headersSent)
42
51
  return;
@@ -72,6 +81,17 @@ var setDraft7Headers = (response, info, windowMs) => {
72
81
  `limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}`
73
82
  );
74
83
  };
84
+ var setDraft8Headers = (response, info, windowMs, name, key) => {
85
+ if (response.headersSent)
86
+ 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
96
  if (response.headersSent)
77
97
  return;
@@ -274,6 +294,22 @@ var validations = {
274
294
  );
275
295
  }
276
296
  },
297
+ /**
298
+ * Warns the user when an invalid/unsupported version of the draft spec is passed.
299
+ *
300
+ * @param version {any | undefined} - The version passed by the user.
301
+ *
302
+ * @returns {void}
303
+ */
304
+ headersDraftVersion(version) {
305
+ if (typeof version !== "string" || !SUPPORTED_DRAFT_VERSIONS.includes(version)) {
306
+ const versionString = SUPPORTED_DRAFT_VERSIONS.join(", ");
307
+ throw new ValidationError(
308
+ "ERR_ERL_HEADERS_UNSUPPORTED_DRAFT_VERSION",
309
+ `standardHeaders: only the following versions of the IETF draft specification are supported: ${versionString}.`
310
+ );
311
+ }
312
+ },
277
313
  /**
278
314
  * Warns the user when the selected headers option requires a reset time but
279
315
  * the store does not provide one.
@@ -606,6 +642,24 @@ var parseOptions = (passedOptions) => {
606
642
  message: "Too many requests, please try again later.",
607
643
  statusCode: 429,
608
644
  legacyHeaders: passedOptions.headers ?? true,
645
+ identifier(request, _response) {
646
+ let duration = "";
647
+ const property = config.requestPropertyName;
648
+ const { limit } = request[property];
649
+ const seconds = config.windowMs / 1e3;
650
+ const minutes = config.windowMs / (1e3 * 60);
651
+ const hours = config.windowMs / (1e3 * 60 * 60);
652
+ const days = config.windowMs / (1e3 * 60 * 60 * 24);
653
+ if (seconds < 60)
654
+ duration = `${seconds}sec`;
655
+ else if (minutes < 60)
656
+ duration = `${minutes}min`;
657
+ else if (hours < 24)
658
+ duration = `${hours}hr${hours > 1 ? "s" : ""}`;
659
+ else
660
+ duration = `${days}day${days > 1 ? "s" : ""}`;
661
+ return `${limit}-in-${duration}`;
662
+ },
609
663
  requestPropertyName: "rateLimit",
610
664
  skipFailedRequests: false,
611
665
  skipSuccessfulRequests: false,
@@ -628,7 +682,7 @@ var parseOptions = (passedOptions) => {
628
682
  }
629
683
  },
630
684
  passOnStoreError: false,
631
- // Allow the default options to be overriden by the options passed to the middleware.
685
+ // Allow the default options to be overriden by the passed options.
632
686
  ...notUndefinedOptions,
633
687
  // `standardHeaders` is resolved into a draft version above, use that.
634
688
  standardHeaders,
@@ -706,11 +760,27 @@ var rateLimit = (passedOptions) => {
706
760
  setLegacyHeaders(response, info);
707
761
  }
708
762
  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);
763
+ switch (config.standardHeaders) {
764
+ case "draft-6": {
765
+ setDraft6Headers(response, info, config.windowMs);
766
+ break;
767
+ }
768
+ case "draft-7": {
769
+ config.validations.headersResetTime(info.resetTime);
770
+ setDraft7Headers(response, info, config.windowMs);
771
+ break;
772
+ }
773
+ case "draft-8": {
774
+ const retrieveName = typeof config.identifier === "function" ? config.identifier(request, response) : config.identifier;
775
+ const name = await retrieveName;
776
+ config.validations.headersResetTime(info.resetTime);
777
+ setDraft8Headers(response, info, config.windowMs, name, key);
778
+ break;
779
+ }
780
+ default: {
781
+ config.validations.headersDraftVersion(config.standardHeaders);
782
+ break;
783
+ }
714
784
  }
715
785
  }
716
786
  if (config.skipFailedRequests || config.skipSuccessfulRequests) {
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,7 @@ declare const validations: {
111
119
  creationStack(store: Store): void;
112
120
  };
113
121
  export type Validations = typeof validations;
122
+ declare const SUPPORTED_DRAFT_VERSIONS: string[];
114
123
  /**
115
124
  * Callback that fires when a client's hit counter is incremented.
116
125
  *
@@ -271,7 +280,7 @@ export type Store = {
271
280
  */
272
281
  prefix?: string;
273
282
  };
274
- export type DraftHeadersVersion = "draft-6" | "draft-7";
283
+ export type DraftHeadersVersion = (typeof SUPPORTED_DRAFT_VERSIONS)[number];
275
284
  /**
276
285
  * Validate configuration object for enabling or disabling specific validations.
277
286
  *
@@ -326,6 +335,13 @@ export type Options = {
326
335
  * Defaults to `false` (for backward compatibility, but its use is recommended).
327
336
  */
328
337
  standardHeaders: boolean | DraftHeadersVersion;
338
+ /**
339
+ * The name used to identify the quota policy in the `RateLimit` headers as per
340
+ * the 8th draft of the IETF specification.
341
+ *
342
+ * Defaults to `{limit}-in-{window}`.
343
+ */
344
+ identifier: string | ValueDeterminingMiddleware<string>;
329
345
  /**
330
346
  * The name of the property on the request object to store the rate limit info.
331
347
  *
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,7 @@ declare const validations: {
111
119
  creationStack(store: Store): void;
112
120
  };
113
121
  export type Validations = typeof validations;
122
+ declare const SUPPORTED_DRAFT_VERSIONS: string[];
114
123
  /**
115
124
  * Callback that fires when a client's hit counter is incremented.
116
125
  *
@@ -271,7 +280,7 @@ export type Store = {
271
280
  */
272
281
  prefix?: string;
273
282
  };
274
- export type DraftHeadersVersion = "draft-6" | "draft-7";
283
+ export type DraftHeadersVersion = (typeof SUPPORTED_DRAFT_VERSIONS)[number];
275
284
  /**
276
285
  * Validate configuration object for enabling or disabling specific validations.
277
286
  *
@@ -326,6 +335,13 @@ export type Options = {
326
335
  * Defaults to `false` (for backward compatibility, but its use is recommended).
327
336
  */
328
337
  standardHeaders: boolean | DraftHeadersVersion;
338
+ /**
339
+ * The name used to identify the quota policy in the `RateLimit` headers as per
340
+ * the 8th draft of the IETF specification.
341
+ *
342
+ * Defaults to `{limit}-in-{window}`.
343
+ */
344
+ identifier: string | ValueDeterminingMiddleware<string>;
329
345
  /**
330
346
  * The name of the property on the request object to store the rate limit info.
331
347
  *
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,7 @@ declare const validations: {
111
119
  creationStack(store: Store): void;
112
120
  };
113
121
  export type Validations = typeof validations;
122
+ declare const SUPPORTED_DRAFT_VERSIONS: string[];
114
123
  /**
115
124
  * Callback that fires when a client's hit counter is incremented.
116
125
  *
@@ -271,7 +280,7 @@ export type Store = {
271
280
  */
272
281
  prefix?: string;
273
282
  };
274
- export type DraftHeadersVersion = "draft-6" | "draft-7";
283
+ export type DraftHeadersVersion = (typeof SUPPORTED_DRAFT_VERSIONS)[number];
275
284
  /**
276
285
  * Validate configuration object for enabling or disabling specific validations.
277
286
  *
@@ -326,6 +335,13 @@ export type Options = {
326
335
  * Defaults to `false` (for backward compatibility, but its use is recommended).
327
336
  */
328
337
  standardHeaders: boolean | DraftHeadersVersion;
338
+ /**
339
+ * The name used to identify the quota policy in the `RateLimit` headers as per
340
+ * the 8th draft of the IETF specification.
341
+ *
342
+ * Defaults to `{limit}-in-{window}`.
343
+ */
344
+ identifier: string | ValueDeterminingMiddleware<string>;
329
345
  /**
330
346
  * The name of the property on the request object to store the rate limit info.
331
347
  *
package/dist/index.mjs CHANGED
@@ -1,4 +1,7 @@
1
1
  // source/headers.ts
2
+ import { Buffer } from "buffer";
3
+ import { createHash } from "crypto";
4
+ var SUPPORTED_DRAFT_VERSIONS = ["draft-6", "draft-7", "draft-8"];
2
5
  var getResetSeconds = (resetTime, windowMs) => {
3
6
  let resetSeconds = void 0;
4
7
  if (resetTime) {
@@ -9,6 +12,12 @@ var getResetSeconds = (resetTime, windowMs) => {
9
12
  }
10
13
  return resetSeconds;
11
14
  };
15
+ var getPartitionKey = (key) => {
16
+ const hash = createHash("sha256");
17
+ hash.update(key);
18
+ const partitionKey = hash.digest("hex").slice(0, 12);
19
+ return Buffer.from(partitionKey).toString("base64");
20
+ };
12
21
  var setLegacyHeaders = (response, info) => {
13
22
  if (response.headersSent)
14
23
  return;
@@ -44,6 +53,17 @@ var setDraft7Headers = (response, info, windowMs) => {
44
53
  `limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}`
45
54
  );
46
55
  };
56
+ var setDraft8Headers = (response, info, windowMs, name, key) => {
57
+ if (response.headersSent)
58
+ 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
68
  if (response.headersSent)
49
69
  return;
@@ -246,6 +266,22 @@ var validations = {
246
266
  );
247
267
  }
248
268
  },
269
+ /**
270
+ * Warns the user when an invalid/unsupported version of the draft spec is passed.
271
+ *
272
+ * @param version {any | undefined} - The version passed by the user.
273
+ *
274
+ * @returns {void}
275
+ */
276
+ headersDraftVersion(version) {
277
+ if (typeof version !== "string" || !SUPPORTED_DRAFT_VERSIONS.includes(version)) {
278
+ const versionString = SUPPORTED_DRAFT_VERSIONS.join(", ");
279
+ throw new ValidationError(
280
+ "ERR_ERL_HEADERS_UNSUPPORTED_DRAFT_VERSION",
281
+ `standardHeaders: only the following versions of the IETF draft specification are supported: ${versionString}.`
282
+ );
283
+ }
284
+ },
249
285
  /**
250
286
  * Warns the user when the selected headers option requires a reset time but
251
287
  * the store does not provide one.
@@ -578,6 +614,24 @@ var parseOptions = (passedOptions) => {
578
614
  message: "Too many requests, please try again later.",
579
615
  statusCode: 429,
580
616
  legacyHeaders: passedOptions.headers ?? true,
617
+ identifier(request, _response) {
618
+ let duration = "";
619
+ const property = config.requestPropertyName;
620
+ const { limit } = request[property];
621
+ const seconds = config.windowMs / 1e3;
622
+ const minutes = config.windowMs / (1e3 * 60);
623
+ const hours = config.windowMs / (1e3 * 60 * 60);
624
+ const days = config.windowMs / (1e3 * 60 * 60 * 24);
625
+ if (seconds < 60)
626
+ duration = `${seconds}sec`;
627
+ else if (minutes < 60)
628
+ duration = `${minutes}min`;
629
+ else if (hours < 24)
630
+ duration = `${hours}hr${hours > 1 ? "s" : ""}`;
631
+ else
632
+ duration = `${days}day${days > 1 ? "s" : ""}`;
633
+ return `${limit}-in-${duration}`;
634
+ },
581
635
  requestPropertyName: "rateLimit",
582
636
  skipFailedRequests: false,
583
637
  skipSuccessfulRequests: false,
@@ -600,7 +654,7 @@ var parseOptions = (passedOptions) => {
600
654
  }
601
655
  },
602
656
  passOnStoreError: false,
603
- // Allow the default options to be overriden by the options passed to the middleware.
657
+ // Allow the default options to be overriden by the passed options.
604
658
  ...notUndefinedOptions,
605
659
  // `standardHeaders` is resolved into a draft version above, use that.
606
660
  standardHeaders,
@@ -678,11 +732,27 @@ var rateLimit = (passedOptions) => {
678
732
  setLegacyHeaders(response, info);
679
733
  }
680
734
  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);
735
+ switch (config.standardHeaders) {
736
+ case "draft-6": {
737
+ setDraft6Headers(response, info, config.windowMs);
738
+ break;
739
+ }
740
+ case "draft-7": {
741
+ config.validations.headersResetTime(info.resetTime);
742
+ setDraft7Headers(response, info, config.windowMs);
743
+ break;
744
+ }
745
+ case "draft-8": {
746
+ const retrieveName = typeof config.identifier === "function" ? config.identifier(request, response) : config.identifier;
747
+ const name = await retrieveName;
748
+ config.validations.headersResetTime(info.resetTime);
749
+ setDraft8Headers(response, info, config.windowMs, name, key);
750
+ break;
751
+ }
752
+ default: {
753
+ config.validations.headersDraftVersion(config.standardHeaders);
754
+ break;
755
+ }
686
756
  }
687
757
  }
688
758
  if (config.skipFailedRequests || config.skipSuccessfulRequests) {
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.0",
4
4
  "description": "Basic IP rate-limiting middleware for Express. Use to limit repeated requests to public APIs and/or endpoints such as password reset.",
5
5
  "author": {
6
6
  "name": "Nathan Friedly",
@@ -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 || 5 || ^5.0.0-beta.1"
78
78
  },
79
79
  "devDependencies": {
80
80
  "@express-rate-limit/prettier": "1.1.1",
@@ -87,7 +87,7 @@
87
87
  "del-cli": "5.1.0",
88
88
  "dts-bundle-generator": "8.0.1",
89
89
  "esbuild": "0.19.5",
90
- "express": "4.21.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