express-rate-limit 6.9.0 → 6.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/changelog.md CHANGED
@@ -6,6 +6,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to
7
7
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [6.11.0](https://github.com/express-rate-limit/express-rate-limit/releases/tag/v6.11.0)
10
+
11
+ ### Added
12
+
13
+ - Support for retrieving the current hit count and reset time for a given key
14
+ from a store (See
15
+ [#390](https://github.com/express-rate-limit/express-rate-limit/issues/389)).
16
+
17
+ ## [6.10.0](https://github.com/express-rate-limit/express-rate-limit/releases/tag/v6.10.0)
18
+
19
+ ### Added
20
+
21
+ - Support for combined `RateLimit` header from the
22
+ [RateLimit header fields for HTTP standardization draft](https://github.com/ietf-wg-httpapi/ratelimit-headers)
23
+ adopted by the IETF. Enable by setting `standardHeaders: 'draft-7'`
24
+ - New `standardHeaders: 'draft-6'` option, treated equivalent to
25
+ `standardHeaders: true` from previous releases. (`true` and `false` are still
26
+ supported.)
27
+ - New `RateLimit-Policy` header added when `standardHeaders` is set to
28
+ `'draft-6'`, `'draft-7'`, or `true`
29
+ - Warning when using deprecated `draft_polli_ratelimit_headers` option
30
+ - Warning when using deprecated `onLimitReached` option
31
+ - Warning when `totalHits` value returned from Store is invalid
32
+
9
33
  ## [6.9.0](https://github.com/express-rate-limit/express-rate-limit/releases/tag/v6.9.0)
10
34
 
11
35
  ### Added
package/dist/index.cjs CHANGED
@@ -31,6 +31,59 @@ __export(source_exports, {
31
31
  });
32
32
  module.exports = __toCommonJS(source_exports);
33
33
 
34
+ // source/headers.ts
35
+ var getResetSeconds = (resetTime, windowMs) => {
36
+ let resetSeconds = void 0;
37
+ if (resetTime) {
38
+ const deltaSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1e3);
39
+ resetSeconds = Math.max(0, deltaSeconds);
40
+ } else if (windowMs) {
41
+ resetSeconds = Math.ceil(windowMs / 1e3);
42
+ }
43
+ return resetSeconds;
44
+ };
45
+ var setLegacyHeaders = (response, info) => {
46
+ if (response.headersSent)
47
+ return;
48
+ response.setHeader("X-RateLimit-Limit", info.limit);
49
+ response.setHeader("X-RateLimit-Remaining", info.remaining);
50
+ if (info.resetTime instanceof Date) {
51
+ response.setHeader("Date", (/* @__PURE__ */ new Date()).toUTCString());
52
+ response.setHeader(
53
+ "X-RateLimit-Reset",
54
+ Math.ceil(info.resetTime.getTime() / 1e3)
55
+ );
56
+ }
57
+ };
58
+ var setDraft6Headers = (response, info, windowMs) => {
59
+ if (response.headersSent)
60
+ return;
61
+ const windowSeconds = Math.ceil(windowMs / 1e3);
62
+ const resetSeconds = getResetSeconds(info.resetTime);
63
+ response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
64
+ response.setHeader("RateLimit-Limit", info.limit);
65
+ response.setHeader("RateLimit-Remaining", info.remaining);
66
+ if (resetSeconds)
67
+ response.setHeader("RateLimit-Reset", resetSeconds);
68
+ };
69
+ var setDraft7Headers = (response, info, windowMs) => {
70
+ if (response.headersSent)
71
+ return;
72
+ const windowSeconds = Math.ceil(windowMs / 1e3);
73
+ const resetSeconds = getResetSeconds(info.resetTime, windowMs);
74
+ response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
75
+ response.setHeader(
76
+ "RateLimit",
77
+ `limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}`
78
+ );
79
+ };
80
+ var setRetryAfterHeader = (response, info, windowMs) => {
81
+ if (response.headersSent)
82
+ return;
83
+ const resetSeconds = getResetSeconds(info.resetTime, windowMs);
84
+ response.setHeader("Retry-After", resetSeconds);
85
+ };
86
+
34
87
  // source/validations.ts
35
88
  var import_node_net = require("net");
36
89
  var ValidationError = class extends Error {
@@ -38,12 +91,12 @@ var ValidationError = class extends Error {
38
91
  * The code must be a string, in snake case and all capital, that starts with
39
92
  * the substring `ERR_ERL_`.
40
93
  *
41
- * The message must be a string, starting with a lowercase character,
94
+ * The message must be a string, starting with an uppercase character,
42
95
  * describing the issue in detail.
43
96
  */
44
97
  constructor(code, message) {
45
98
  const url = `https://express-rate-limit.github.io/${code}/`;
46
- super(`${message} See ${url} for more information on this error.`);
99
+ super(`${message} See ${url} for more information.`);
47
100
  __publicField(this, "name");
48
101
  __publicField(this, "code");
49
102
  __publicField(this, "help");
@@ -52,6 +105,8 @@ var ValidationError = class extends Error {
52
105
  this.help = url;
53
106
  }
54
107
  };
108
+ var ChangeWarning = class extends ValidationError {
109
+ };
55
110
  var _Validations = class _Validations {
56
111
  constructor(enabled) {
57
112
  // eslint-disable-next-line @typescript-eslint/parameter-properties
@@ -129,6 +184,22 @@ var _Validations = class _Validations {
129
184
  }
130
185
  });
131
186
  }
187
+ /**
188
+ * Ensures totalHits value from store is a positive integer.
189
+ *
190
+ * @param hits {any} - The `totalHits` returned by the store.
191
+ */
192
+ positiveHits(hits) {
193
+ this.wrap(() => {
194
+ if (typeof hits !== "number" || hits < 1 || hits !== Math.round(hits)) {
195
+ throw new ValidationError(
196
+ "ERR_ERL_INVALID_HITS",
197
+ `The totalHits value returned from the store must be a positive integer, got ${hits}`
198
+ // eslint-disable-line @typescript-eslint/restrict-template-expressions
199
+ );
200
+ }
201
+ });
202
+ }
132
203
  /**
133
204
  * Ensures a given key is incremented only once per request.
134
205
  *
@@ -160,6 +231,78 @@ var _Validations = class _Validations {
160
231
  keys.push(key);
161
232
  });
162
233
  }
234
+ /**
235
+ * Warns the user that the behaviour for `max: 0` is changing in the next
236
+ * major release.
237
+ *
238
+ * @param max {number} - The maximum number of hits per client.
239
+ *
240
+ * @returns {void}
241
+ */
242
+ max(max) {
243
+ this.wrap(() => {
244
+ if (max === 0) {
245
+ throw new ChangeWarning(
246
+ "WRN_ERL_MAX_ZERO",
247
+ `Setting max to 0 disables rate limiting in express-rate-limit v6 and older, but will cause all requests to be blocked in v7`
248
+ );
249
+ }
250
+ });
251
+ }
252
+ /**
253
+ * Warns the user that the `draft_polli_ratelimit_headers` option is deprecated
254
+ * and will be removed in the next major release.
255
+ *
256
+ * @param draft_polli_ratelimit_headers {boolean|undefined} - The now-deprecated setting that was used to enable standard headers.
257
+ *
258
+ * @returns {void}
259
+ */
260
+ draftPolliHeaders(draft_polli_ratelimit_headers) {
261
+ this.wrap(() => {
262
+ if (draft_polli_ratelimit_headers) {
263
+ throw new ChangeWarning(
264
+ "WRN_ERL_DEPRECATED_DRAFT_POLLI_HEADERS",
265
+ `The draft_polli_ratelimit_headers configuration option is deprecated and will be removed in express-rate-limit v7, please set standardHeaders: 'draft-6' instead.`
266
+ );
267
+ }
268
+ });
269
+ }
270
+ /**
271
+ * Warns the user that the `onLimitReached` option is deprecated and will be removed in the next
272
+ * major release.
273
+ *
274
+ * @param onLimitReached {function|undefined} - The maximum number of hits per client.
275
+ *
276
+ * @returns {void}
277
+ */
278
+ onLimitReached(onLimitReached) {
279
+ this.wrap(() => {
280
+ if (onLimitReached) {
281
+ throw new ChangeWarning(
282
+ "WRN_ERL_DEPRECATED_ON_LIMIT_REACHED",
283
+ `The onLimitReached configuration option is deprecated and will be removed in express-rate-limit v7.`
284
+ );
285
+ }
286
+ });
287
+ }
288
+ /**
289
+ * Warns the user when the selected headers option requires a reset time but
290
+ * the store does not provide one.
291
+ *
292
+ * @param resetTime {Date | undefined} - The timestamp when the client's hit count will be reset.
293
+ *
294
+ * @returns {void}
295
+ */
296
+ headersResetTime(resetTime) {
297
+ this.wrap(() => {
298
+ if (!resetTime) {
299
+ throw new ValidationError(
300
+ "ERR_ERL_HEADERS_NO_RESET",
301
+ `standardHeaders: 'draft-7' requires a 'resetTime', but the store did not provide one. The 'windowMs' value will be used instead, which may cause clients to wait longer than necessary.`
302
+ );
303
+ }
304
+ });
305
+ }
163
306
  wrap(validation) {
164
307
  if (!this.enabled) {
165
308
  return;
@@ -167,7 +310,10 @@ var _Validations = class _Validations {
167
310
  try {
168
311
  validation.call(this);
169
312
  } catch (error) {
170
- console.error(error);
313
+ if (error instanceof ChangeWarning)
314
+ console.warn(error);
315
+ else
316
+ console.error(error);
171
317
  }
172
318
  }
173
319
  };
@@ -229,12 +375,29 @@ var MemoryStore = class {
229
375
  if (this.interval.unref)
230
376
  this.interval.unref();
231
377
  }
378
+ /**
379
+ * Method to fetch a client's hit count and reset time.
380
+ *
381
+ * @param key {string} - The identifier for a client.
382
+ *
383
+ * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
384
+ *
385
+ * @public
386
+ */
387
+ async get(key) {
388
+ if (this.hits[key] !== void 0)
389
+ return {
390
+ totalHits: this.hits[key],
391
+ resetTime: this.resetTime
392
+ };
393
+ return void 0;
394
+ }
232
395
  /**
233
396
  * Method to increment a client's hit counter.
234
397
  *
235
398
  * @param key {string} - The identifier for a client.
236
399
  *
237
- * @returns {IncrementResponse} - The number of hits and reset time for that client.
400
+ * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
238
401
  *
239
402
  * @public
240
403
  */
@@ -301,6 +464,10 @@ var promisifyStore = (passedStore) => {
301
464
  }
302
465
  const legacyStore = passedStore;
303
466
  class PromisifiedStore {
467
+ /* istanbul ignore next */
468
+ async get(key) {
469
+ return void 0;
470
+ }
304
471
  async increment(key) {
305
472
  return new Promise((resolve, reject) => {
306
473
  legacyStore.incr(
@@ -319,6 +486,7 @@ var promisifyStore = (passedStore) => {
319
486
  async resetKey(key) {
320
487
  return legacyStore.resetKey(key);
321
488
  }
489
+ /* istanbul ignore next */
322
490
  async resetAll() {
323
491
  if (typeof legacyStore.resetAll === "function")
324
492
  return legacyStore.resetAll();
@@ -347,13 +515,20 @@ var parseOptions = (passedOptions) => {
347
515
  var _a, _b, _c, _d;
348
516
  const notUndefinedOptions = omitUndefinedOptions(passedOptions);
349
517
  const validations = new Validations((_a = notUndefinedOptions == null ? void 0 : notUndefinedOptions.validate) != null ? _a : true);
518
+ validations.draftPolliHeaders(
519
+ notUndefinedOptions.draft_polli_ratelimit_headers
520
+ );
521
+ validations.onLimitReached(notUndefinedOptions.onLimitReached);
522
+ let standardHeaders = (_b = notUndefinedOptions.standardHeaders) != null ? _b : false;
523
+ if (standardHeaders === true || standardHeaders === void 0 && notUndefinedOptions.draft_polli_ratelimit_headers) {
524
+ standardHeaders = "draft-6";
525
+ }
350
526
  const config = {
351
527
  windowMs: 60 * 1e3,
352
528
  max: 5,
353
529
  message: "Too many requests, please try again later.",
354
530
  statusCode: 429,
355
- legacyHeaders: (_b = passedOptions.headers) != null ? _b : true,
356
- standardHeaders: (_c = passedOptions.draft_polli_ratelimit_headers) != null ? _c : false,
531
+ legacyHeaders: (_c = passedOptions.headers) != null ? _c : true,
357
532
  requestPropertyName: "rateLimit",
358
533
  skipFailedRequests: false,
359
534
  skipSuccessfulRequests: false,
@@ -372,13 +547,15 @@ var parseOptions = (passedOptions) => {
372
547
  response
373
548
  ) : config.message;
374
549
  if (!response.writableEnded) {
375
- response.send(message != null ? message : "Too many requests, please try again later.");
550
+ response.send(message);
376
551
  }
377
552
  },
378
553
  onLimitReached(_request, _response, _optionsUsed) {
379
554
  },
380
- // Allow the options object to be overriden by the options passed to the middleware.
555
+ // Allow the default options to be overriden by the options passed to the middleware.
381
556
  ...notUndefinedOptions,
557
+ // `standardHeaders` is resolved into a draft version above, use that.
558
+ standardHeaders,
382
559
  // Note that this field is declared after the user's options are spread in,
383
560
  // so that this field doesn't get overriden with an un-promisified store!
384
561
  store: promisifyStore((_d = notUndefinedOptions.store) != null ? _d : new MemoryStore()),
@@ -400,6 +577,7 @@ var handleAsyncErrors = (fn) => async (request, response, next) => {
400
577
  }
401
578
  };
402
579
  var rateLimit = (passedOptions) => {
580
+ var _a;
403
581
  const config = parseOptions(passedOptions != null ? passedOptions : {});
404
582
  const options = getOptionsFromConfig(config);
405
583
  if (typeof config.store.init === "function")
@@ -414,40 +592,27 @@ var rateLimit = (passedOptions) => {
414
592
  const augmentedRequest = request;
415
593
  const key = await config.keyGenerator(request, response);
416
594
  const { totalHits, resetTime } = await config.store.increment(key);
595
+ config.validations.positiveHits(totalHits);
417
596
  config.validations.singleCount(request, config.store, key);
418
597
  const retrieveQuota = typeof config.max === "function" ? config.max(request, response) : config.max;
419
598
  const maxHits = await retrieveQuota;
420
- augmentedRequest[config.requestPropertyName] = {
599
+ config.validations.max(maxHits);
600
+ const info = {
421
601
  limit: maxHits,
422
602
  current: totalHits,
423
603
  remaining: Math.max(maxHits - totalHits, 0),
424
604
  resetTime
425
605
  };
606
+ augmentedRequest[config.requestPropertyName] = info;
426
607
  if (config.legacyHeaders && !response.headersSent) {
427
- response.setHeader("X-RateLimit-Limit", maxHits);
428
- response.setHeader(
429
- "X-RateLimit-Remaining",
430
- augmentedRequest[config.requestPropertyName].remaining
431
- );
432
- if (resetTime instanceof Date) {
433
- response.setHeader("Date", (/* @__PURE__ */ new Date()).toUTCString());
434
- response.setHeader(
435
- "X-RateLimit-Reset",
436
- Math.ceil(resetTime.getTime() / 1e3)
437
- );
438
- }
608
+ setLegacyHeaders(response, info);
439
609
  }
440
610
  if (config.standardHeaders && !response.headersSent) {
441
- response.setHeader("RateLimit-Limit", maxHits);
442
- response.setHeader(
443
- "RateLimit-Remaining",
444
- augmentedRequest[config.requestPropertyName].remaining
445
- );
446
- if (resetTime) {
447
- const deltaSeconds = Math.ceil(
448
- (resetTime.getTime() - Date.now()) / 1e3
449
- );
450
- response.setHeader("RateLimit-Reset", Math.max(0, deltaSeconds));
611
+ if (config.standardHeaders === "draft-6") {
612
+ setDraft6Headers(response, info, config.windowMs);
613
+ } else if (config.standardHeaders === "draft-7") {
614
+ config.validations.headersResetTime(info.resetTime);
615
+ setDraft7Headers(response, info, config.windowMs);
451
616
  }
452
617
  }
453
618
  if (config.skipFailedRequests || config.skipSuccessfulRequests) {
@@ -483,8 +648,8 @@ var rateLimit = (passedOptions) => {
483
648
  }
484
649
  config.validations.disable();
485
650
  if (maxHits && totalHits > maxHits) {
486
- if ((config.legacyHeaders || config.standardHeaders) && !response.headersSent) {
487
- response.setHeader("Retry-After", Math.ceil(config.windowMs / 1e3));
651
+ if (config.legacyHeaders || config.standardHeaders) {
652
+ setRetryAfterHeader(response, info, config.windowMs);
488
653
  }
489
654
  config.handler(request, response, next, options);
490
655
  return;
@@ -493,6 +658,9 @@ var rateLimit = (passedOptions) => {
493
658
  }
494
659
  );
495
660
  middleware.resetKey = config.store.resetKey.bind(config.store);
661
+ middleware.getKey = (_a = config.store.get) == null ? void 0 : _a.bind(
662
+ config.store
663
+ );
496
664
  return middleware;
497
665
  };
498
666
  var lib_default = rateLimit;
package/dist/index.d.cts CHANGED
@@ -46,7 +46,7 @@ export type RateLimitReachedEventHandler = (request: Request, response: Response
46
46
  * @property totalHits {number} - The number of hits for that client so far.
47
47
  * @property resetTime {Date | undefined} - The time when the counter resets.
48
48
  */
49
- export type IncrementResponse = {
49
+ export type ClientRateLimitInfo = {
50
50
  totalHits: number;
51
51
  resetTime: Date | undefined;
52
52
  };
@@ -60,6 +60,14 @@ export type RateLimitRequestHandler = RequestHandler & {
60
60
  * @param key {string} - The identifier for a client.
61
61
  */
62
62
  resetKey: (key: string) => void;
63
+ /**
64
+ * Method to fetch a client's hit count and reset time.
65
+ *
66
+ * @param key {string} - The identifier for a client.
67
+ *
68
+ * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
69
+ */
70
+ getKey?: (key: string) => Promise<ClientRateLimitInfo | undefined> | ClientRateLimitInfo | undefined;
63
71
  };
64
72
  /**
65
73
  * An interface that all hit counter stores must implement.
@@ -102,14 +110,22 @@ export type Store = {
102
110
  * @param options {Options} - The options used to setup the middleware.
103
111
  */
104
112
  init?: (options: Options) => void;
113
+ /**
114
+ * Method to fetch a client's hit count and reset time.
115
+ *
116
+ * @param key {string} - The identifier for a client.
117
+ *
118
+ * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
119
+ */
120
+ get?: (key: string) => Promise<ClientRateLimitInfo | undefined> | ClientRateLimitInfo | undefined;
105
121
  /**
106
122
  * Method to increment a client's hit counter.
107
123
  *
108
124
  * @param key {string} - The identifier for a client.
109
125
  *
110
- * @returns {IncrementResponse} - The number of hits and reset time for that client.
126
+ * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
111
127
  */
112
- increment: (key: string) => Promise<IncrementResponse> | IncrementResponse;
128
+ increment: (key: string) => Promise<ClientRateLimitInfo> | ClientRateLimitInfo;
113
129
  /**
114
130
  * Method to decrement a client's hit counter.
115
131
  *
@@ -139,6 +155,7 @@ export type Store = {
139
155
  */
140
156
  localKeys?: boolean;
141
157
  };
158
+ export type DraftHeadersVersion = "draft-6" | "draft-7";
142
159
  /**
143
160
  * The configuration options for the rate limiter.
144
161
  */
@@ -183,7 +200,7 @@ export type Options = {
183
200
  *
184
201
  * Defaults to `false` (for backward compatibility, but its use is recommended).
185
202
  */
186
- standardHeaders: boolean;
203
+ standardHeaders: boolean | DraftHeadersVersion;
187
204
  /**
188
205
  * The name of the property on the request object to store the rate limit info.
189
206
  *
@@ -328,16 +345,26 @@ export declare class MemoryStore implements Store {
328
345
  * @param options {Options} - The options used to setup the middleware.
329
346
  */
330
347
  init(options: Options): void;
348
+ /**
349
+ * Method to fetch a client's hit count and reset time.
350
+ *
351
+ * @param key {string} - The identifier for a client.
352
+ *
353
+ * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
354
+ *
355
+ * @public
356
+ */
357
+ get(key: string): Promise<ClientRateLimitInfo | undefined>;
331
358
  /**
332
359
  * Method to increment a client's hit counter.
333
360
  *
334
361
  * @param key {string} - The identifier for a client.
335
362
  *
336
- * @returns {IncrementResponse} - The number of hits and reset time for that client.
363
+ * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
337
364
  *
338
365
  * @public
339
366
  */
340
- increment(key: string): Promise<IncrementResponse>;
367
+ increment(key: string): Promise<ClientRateLimitInfo>;
341
368
  /**
342
369
  * Method to decrement a client's hit counter.
343
370
  *
package/dist/index.d.mts CHANGED
@@ -46,7 +46,7 @@ export type RateLimitReachedEventHandler = (request: Request, response: Response
46
46
  * @property totalHits {number} - The number of hits for that client so far.
47
47
  * @property resetTime {Date | undefined} - The time when the counter resets.
48
48
  */
49
- export type IncrementResponse = {
49
+ export type ClientRateLimitInfo = {
50
50
  totalHits: number;
51
51
  resetTime: Date | undefined;
52
52
  };
@@ -60,6 +60,14 @@ export type RateLimitRequestHandler = RequestHandler & {
60
60
  * @param key {string} - The identifier for a client.
61
61
  */
62
62
  resetKey: (key: string) => void;
63
+ /**
64
+ * Method to fetch a client's hit count and reset time.
65
+ *
66
+ * @param key {string} - The identifier for a client.
67
+ *
68
+ * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
69
+ */
70
+ getKey?: (key: string) => Promise<ClientRateLimitInfo | undefined> | ClientRateLimitInfo | undefined;
63
71
  };
64
72
  /**
65
73
  * An interface that all hit counter stores must implement.
@@ -102,14 +110,22 @@ export type Store = {
102
110
  * @param options {Options} - The options used to setup the middleware.
103
111
  */
104
112
  init?: (options: Options) => void;
113
+ /**
114
+ * Method to fetch a client's hit count and reset time.
115
+ *
116
+ * @param key {string} - The identifier for a client.
117
+ *
118
+ * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
119
+ */
120
+ get?: (key: string) => Promise<ClientRateLimitInfo | undefined> | ClientRateLimitInfo | undefined;
105
121
  /**
106
122
  * Method to increment a client's hit counter.
107
123
  *
108
124
  * @param key {string} - The identifier for a client.
109
125
  *
110
- * @returns {IncrementResponse} - The number of hits and reset time for that client.
126
+ * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
111
127
  */
112
- increment: (key: string) => Promise<IncrementResponse> | IncrementResponse;
128
+ increment: (key: string) => Promise<ClientRateLimitInfo> | ClientRateLimitInfo;
113
129
  /**
114
130
  * Method to decrement a client's hit counter.
115
131
  *
@@ -139,6 +155,7 @@ export type Store = {
139
155
  */
140
156
  localKeys?: boolean;
141
157
  };
158
+ export type DraftHeadersVersion = "draft-6" | "draft-7";
142
159
  /**
143
160
  * The configuration options for the rate limiter.
144
161
  */
@@ -183,7 +200,7 @@ export type Options = {
183
200
  *
184
201
  * Defaults to `false` (for backward compatibility, but its use is recommended).
185
202
  */
186
- standardHeaders: boolean;
203
+ standardHeaders: boolean | DraftHeadersVersion;
187
204
  /**
188
205
  * The name of the property on the request object to store the rate limit info.
189
206
  *
@@ -328,16 +345,26 @@ export declare class MemoryStore implements Store {
328
345
  * @param options {Options} - The options used to setup the middleware.
329
346
  */
330
347
  init(options: Options): void;
348
+ /**
349
+ * Method to fetch a client's hit count and reset time.
350
+ *
351
+ * @param key {string} - The identifier for a client.
352
+ *
353
+ * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
354
+ *
355
+ * @public
356
+ */
357
+ get(key: string): Promise<ClientRateLimitInfo | undefined>;
331
358
  /**
332
359
  * Method to increment a client's hit counter.
333
360
  *
334
361
  * @param key {string} - The identifier for a client.
335
362
  *
336
- * @returns {IncrementResponse} - The number of hits and reset time for that client.
363
+ * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
337
364
  *
338
365
  * @public
339
366
  */
340
- increment(key: string): Promise<IncrementResponse>;
367
+ increment(key: string): Promise<ClientRateLimitInfo>;
341
368
  /**
342
369
  * Method to decrement a client's hit counter.
343
370
  *
package/dist/index.d.ts CHANGED
@@ -46,7 +46,7 @@ export type RateLimitReachedEventHandler = (request: Request, response: Response
46
46
  * @property totalHits {number} - The number of hits for that client so far.
47
47
  * @property resetTime {Date | undefined} - The time when the counter resets.
48
48
  */
49
- export type IncrementResponse = {
49
+ export type ClientRateLimitInfo = {
50
50
  totalHits: number;
51
51
  resetTime: Date | undefined;
52
52
  };
@@ -60,6 +60,14 @@ export type RateLimitRequestHandler = RequestHandler & {
60
60
  * @param key {string} - The identifier for a client.
61
61
  */
62
62
  resetKey: (key: string) => void;
63
+ /**
64
+ * Method to fetch a client's hit count and reset time.
65
+ *
66
+ * @param key {string} - The identifier for a client.
67
+ *
68
+ * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
69
+ */
70
+ getKey?: (key: string) => Promise<ClientRateLimitInfo | undefined> | ClientRateLimitInfo | undefined;
63
71
  };
64
72
  /**
65
73
  * An interface that all hit counter stores must implement.
@@ -102,14 +110,22 @@ export type Store = {
102
110
  * @param options {Options} - The options used to setup the middleware.
103
111
  */
104
112
  init?: (options: Options) => void;
113
+ /**
114
+ * Method to fetch a client's hit count and reset time.
115
+ *
116
+ * @param key {string} - The identifier for a client.
117
+ *
118
+ * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
119
+ */
120
+ get?: (key: string) => Promise<ClientRateLimitInfo | undefined> | ClientRateLimitInfo | undefined;
105
121
  /**
106
122
  * Method to increment a client's hit counter.
107
123
  *
108
124
  * @param key {string} - The identifier for a client.
109
125
  *
110
- * @returns {IncrementResponse} - The number of hits and reset time for that client.
126
+ * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
111
127
  */
112
- increment: (key: string) => Promise<IncrementResponse> | IncrementResponse;
128
+ increment: (key: string) => Promise<ClientRateLimitInfo> | ClientRateLimitInfo;
113
129
  /**
114
130
  * Method to decrement a client's hit counter.
115
131
  *
@@ -139,6 +155,7 @@ export type Store = {
139
155
  */
140
156
  localKeys?: boolean;
141
157
  };
158
+ export type DraftHeadersVersion = "draft-6" | "draft-7";
142
159
  /**
143
160
  * The configuration options for the rate limiter.
144
161
  */
@@ -183,7 +200,7 @@ export type Options = {
183
200
  *
184
201
  * Defaults to `false` (for backward compatibility, but its use is recommended).
185
202
  */
186
- standardHeaders: boolean;
203
+ standardHeaders: boolean | DraftHeadersVersion;
187
204
  /**
188
205
  * The name of the property on the request object to store the rate limit info.
189
206
  *
@@ -328,16 +345,26 @@ export declare class MemoryStore implements Store {
328
345
  * @param options {Options} - The options used to setup the middleware.
329
346
  */
330
347
  init(options: Options): void;
348
+ /**
349
+ * Method to fetch a client's hit count and reset time.
350
+ *
351
+ * @param key {string} - The identifier for a client.
352
+ *
353
+ * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
354
+ *
355
+ * @public
356
+ */
357
+ get(key: string): Promise<ClientRateLimitInfo | undefined>;
331
358
  /**
332
359
  * Method to increment a client's hit counter.
333
360
  *
334
361
  * @param key {string} - The identifier for a client.
335
362
  *
336
- * @returns {IncrementResponse} - The number of hits and reset time for that client.
363
+ * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
337
364
  *
338
365
  * @public
339
366
  */
340
- increment(key: string): Promise<IncrementResponse>;
367
+ increment(key: string): Promise<ClientRateLimitInfo>;
341
368
  /**
342
369
  * Method to decrement a client's hit counter.
343
370
  *
package/dist/index.mjs CHANGED
@@ -5,6 +5,59 @@ var __publicField = (obj, key, value) => {
5
5
  return value;
6
6
  };
7
7
 
8
+ // source/headers.ts
9
+ var getResetSeconds = (resetTime, windowMs) => {
10
+ let resetSeconds = void 0;
11
+ if (resetTime) {
12
+ const deltaSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1e3);
13
+ resetSeconds = Math.max(0, deltaSeconds);
14
+ } else if (windowMs) {
15
+ resetSeconds = Math.ceil(windowMs / 1e3);
16
+ }
17
+ return resetSeconds;
18
+ };
19
+ var setLegacyHeaders = (response, info) => {
20
+ if (response.headersSent)
21
+ return;
22
+ response.setHeader("X-RateLimit-Limit", info.limit);
23
+ response.setHeader("X-RateLimit-Remaining", info.remaining);
24
+ if (info.resetTime instanceof Date) {
25
+ response.setHeader("Date", (/* @__PURE__ */ new Date()).toUTCString());
26
+ response.setHeader(
27
+ "X-RateLimit-Reset",
28
+ Math.ceil(info.resetTime.getTime() / 1e3)
29
+ );
30
+ }
31
+ };
32
+ var setDraft6Headers = (response, info, windowMs) => {
33
+ if (response.headersSent)
34
+ return;
35
+ const windowSeconds = Math.ceil(windowMs / 1e3);
36
+ const resetSeconds = getResetSeconds(info.resetTime);
37
+ response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
38
+ response.setHeader("RateLimit-Limit", info.limit);
39
+ response.setHeader("RateLimit-Remaining", info.remaining);
40
+ if (resetSeconds)
41
+ response.setHeader("RateLimit-Reset", resetSeconds);
42
+ };
43
+ var setDraft7Headers = (response, info, windowMs) => {
44
+ if (response.headersSent)
45
+ return;
46
+ const windowSeconds = Math.ceil(windowMs / 1e3);
47
+ const resetSeconds = getResetSeconds(info.resetTime, windowMs);
48
+ response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
49
+ response.setHeader(
50
+ "RateLimit",
51
+ `limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}`
52
+ );
53
+ };
54
+ var setRetryAfterHeader = (response, info, windowMs) => {
55
+ if (response.headersSent)
56
+ return;
57
+ const resetSeconds = getResetSeconds(info.resetTime, windowMs);
58
+ response.setHeader("Retry-After", resetSeconds);
59
+ };
60
+
8
61
  // source/validations.ts
9
62
  import { isIP } from "net";
10
63
  var ValidationError = class extends Error {
@@ -12,12 +65,12 @@ var ValidationError = class extends Error {
12
65
  * The code must be a string, in snake case and all capital, that starts with
13
66
  * the substring `ERR_ERL_`.
14
67
  *
15
- * The message must be a string, starting with a lowercase character,
68
+ * The message must be a string, starting with an uppercase character,
16
69
  * describing the issue in detail.
17
70
  */
18
71
  constructor(code, message) {
19
72
  const url = `https://express-rate-limit.github.io/${code}/`;
20
- super(`${message} See ${url} for more information on this error.`);
73
+ super(`${message} See ${url} for more information.`);
21
74
  __publicField(this, "name");
22
75
  __publicField(this, "code");
23
76
  __publicField(this, "help");
@@ -26,6 +79,8 @@ var ValidationError = class extends Error {
26
79
  this.help = url;
27
80
  }
28
81
  };
82
+ var ChangeWarning = class extends ValidationError {
83
+ };
29
84
  var _Validations = class _Validations {
30
85
  constructor(enabled) {
31
86
  // eslint-disable-next-line @typescript-eslint/parameter-properties
@@ -103,6 +158,22 @@ var _Validations = class _Validations {
103
158
  }
104
159
  });
105
160
  }
161
+ /**
162
+ * Ensures totalHits value from store is a positive integer.
163
+ *
164
+ * @param hits {any} - The `totalHits` returned by the store.
165
+ */
166
+ positiveHits(hits) {
167
+ this.wrap(() => {
168
+ if (typeof hits !== "number" || hits < 1 || hits !== Math.round(hits)) {
169
+ throw new ValidationError(
170
+ "ERR_ERL_INVALID_HITS",
171
+ `The totalHits value returned from the store must be a positive integer, got ${hits}`
172
+ // eslint-disable-line @typescript-eslint/restrict-template-expressions
173
+ );
174
+ }
175
+ });
176
+ }
106
177
  /**
107
178
  * Ensures a given key is incremented only once per request.
108
179
  *
@@ -134,6 +205,78 @@ var _Validations = class _Validations {
134
205
  keys.push(key);
135
206
  });
136
207
  }
208
+ /**
209
+ * Warns the user that the behaviour for `max: 0` is changing in the next
210
+ * major release.
211
+ *
212
+ * @param max {number} - The maximum number of hits per client.
213
+ *
214
+ * @returns {void}
215
+ */
216
+ max(max) {
217
+ this.wrap(() => {
218
+ if (max === 0) {
219
+ throw new ChangeWarning(
220
+ "WRN_ERL_MAX_ZERO",
221
+ `Setting max to 0 disables rate limiting in express-rate-limit v6 and older, but will cause all requests to be blocked in v7`
222
+ );
223
+ }
224
+ });
225
+ }
226
+ /**
227
+ * Warns the user that the `draft_polli_ratelimit_headers` option is deprecated
228
+ * and will be removed in the next major release.
229
+ *
230
+ * @param draft_polli_ratelimit_headers {boolean|undefined} - The now-deprecated setting that was used to enable standard headers.
231
+ *
232
+ * @returns {void}
233
+ */
234
+ draftPolliHeaders(draft_polli_ratelimit_headers) {
235
+ this.wrap(() => {
236
+ if (draft_polli_ratelimit_headers) {
237
+ throw new ChangeWarning(
238
+ "WRN_ERL_DEPRECATED_DRAFT_POLLI_HEADERS",
239
+ `The draft_polli_ratelimit_headers configuration option is deprecated and will be removed in express-rate-limit v7, please set standardHeaders: 'draft-6' instead.`
240
+ );
241
+ }
242
+ });
243
+ }
244
+ /**
245
+ * Warns the user that the `onLimitReached` option is deprecated and will be removed in the next
246
+ * major release.
247
+ *
248
+ * @param onLimitReached {function|undefined} - The maximum number of hits per client.
249
+ *
250
+ * @returns {void}
251
+ */
252
+ onLimitReached(onLimitReached) {
253
+ this.wrap(() => {
254
+ if (onLimitReached) {
255
+ throw new ChangeWarning(
256
+ "WRN_ERL_DEPRECATED_ON_LIMIT_REACHED",
257
+ `The onLimitReached configuration option is deprecated and will be removed in express-rate-limit v7.`
258
+ );
259
+ }
260
+ });
261
+ }
262
+ /**
263
+ * Warns the user when the selected headers option requires a reset time but
264
+ * the store does not provide one.
265
+ *
266
+ * @param resetTime {Date | undefined} - The timestamp when the client's hit count will be reset.
267
+ *
268
+ * @returns {void}
269
+ */
270
+ headersResetTime(resetTime) {
271
+ this.wrap(() => {
272
+ if (!resetTime) {
273
+ throw new ValidationError(
274
+ "ERR_ERL_HEADERS_NO_RESET",
275
+ `standardHeaders: 'draft-7' requires a 'resetTime', but the store did not provide one. The 'windowMs' value will be used instead, which may cause clients to wait longer than necessary.`
276
+ );
277
+ }
278
+ });
279
+ }
137
280
  wrap(validation) {
138
281
  if (!this.enabled) {
139
282
  return;
@@ -141,7 +284,10 @@ var _Validations = class _Validations {
141
284
  try {
142
285
  validation.call(this);
143
286
  } catch (error) {
144
- console.error(error);
287
+ if (error instanceof ChangeWarning)
288
+ console.warn(error);
289
+ else
290
+ console.error(error);
145
291
  }
146
292
  }
147
293
  };
@@ -203,12 +349,29 @@ var MemoryStore = class {
203
349
  if (this.interval.unref)
204
350
  this.interval.unref();
205
351
  }
352
+ /**
353
+ * Method to fetch a client's hit count and reset time.
354
+ *
355
+ * @param key {string} - The identifier for a client.
356
+ *
357
+ * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
358
+ *
359
+ * @public
360
+ */
361
+ async get(key) {
362
+ if (this.hits[key] !== void 0)
363
+ return {
364
+ totalHits: this.hits[key],
365
+ resetTime: this.resetTime
366
+ };
367
+ return void 0;
368
+ }
206
369
  /**
207
370
  * Method to increment a client's hit counter.
208
371
  *
209
372
  * @param key {string} - The identifier for a client.
210
373
  *
211
- * @returns {IncrementResponse} - The number of hits and reset time for that client.
374
+ * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
212
375
  *
213
376
  * @public
214
377
  */
@@ -275,6 +438,10 @@ var promisifyStore = (passedStore) => {
275
438
  }
276
439
  const legacyStore = passedStore;
277
440
  class PromisifiedStore {
441
+ /* istanbul ignore next */
442
+ async get(key) {
443
+ return void 0;
444
+ }
278
445
  async increment(key) {
279
446
  return new Promise((resolve, reject) => {
280
447
  legacyStore.incr(
@@ -293,6 +460,7 @@ var promisifyStore = (passedStore) => {
293
460
  async resetKey(key) {
294
461
  return legacyStore.resetKey(key);
295
462
  }
463
+ /* istanbul ignore next */
296
464
  async resetAll() {
297
465
  if (typeof legacyStore.resetAll === "function")
298
466
  return legacyStore.resetAll();
@@ -321,13 +489,20 @@ var parseOptions = (passedOptions) => {
321
489
  var _a, _b, _c, _d;
322
490
  const notUndefinedOptions = omitUndefinedOptions(passedOptions);
323
491
  const validations = new Validations((_a = notUndefinedOptions == null ? void 0 : notUndefinedOptions.validate) != null ? _a : true);
492
+ validations.draftPolliHeaders(
493
+ notUndefinedOptions.draft_polli_ratelimit_headers
494
+ );
495
+ validations.onLimitReached(notUndefinedOptions.onLimitReached);
496
+ let standardHeaders = (_b = notUndefinedOptions.standardHeaders) != null ? _b : false;
497
+ if (standardHeaders === true || standardHeaders === void 0 && notUndefinedOptions.draft_polli_ratelimit_headers) {
498
+ standardHeaders = "draft-6";
499
+ }
324
500
  const config = {
325
501
  windowMs: 60 * 1e3,
326
502
  max: 5,
327
503
  message: "Too many requests, please try again later.",
328
504
  statusCode: 429,
329
- legacyHeaders: (_b = passedOptions.headers) != null ? _b : true,
330
- standardHeaders: (_c = passedOptions.draft_polli_ratelimit_headers) != null ? _c : false,
505
+ legacyHeaders: (_c = passedOptions.headers) != null ? _c : true,
331
506
  requestPropertyName: "rateLimit",
332
507
  skipFailedRequests: false,
333
508
  skipSuccessfulRequests: false,
@@ -346,13 +521,15 @@ var parseOptions = (passedOptions) => {
346
521
  response
347
522
  ) : config.message;
348
523
  if (!response.writableEnded) {
349
- response.send(message != null ? message : "Too many requests, please try again later.");
524
+ response.send(message);
350
525
  }
351
526
  },
352
527
  onLimitReached(_request, _response, _optionsUsed) {
353
528
  },
354
- // Allow the options object to be overriden by the options passed to the middleware.
529
+ // Allow the default options to be overriden by the options passed to the middleware.
355
530
  ...notUndefinedOptions,
531
+ // `standardHeaders` is resolved into a draft version above, use that.
532
+ standardHeaders,
356
533
  // Note that this field is declared after the user's options are spread in,
357
534
  // so that this field doesn't get overriden with an un-promisified store!
358
535
  store: promisifyStore((_d = notUndefinedOptions.store) != null ? _d : new MemoryStore()),
@@ -374,6 +551,7 @@ var handleAsyncErrors = (fn) => async (request, response, next) => {
374
551
  }
375
552
  };
376
553
  var rateLimit = (passedOptions) => {
554
+ var _a;
377
555
  const config = parseOptions(passedOptions != null ? passedOptions : {});
378
556
  const options = getOptionsFromConfig(config);
379
557
  if (typeof config.store.init === "function")
@@ -388,40 +566,27 @@ var rateLimit = (passedOptions) => {
388
566
  const augmentedRequest = request;
389
567
  const key = await config.keyGenerator(request, response);
390
568
  const { totalHits, resetTime } = await config.store.increment(key);
569
+ config.validations.positiveHits(totalHits);
391
570
  config.validations.singleCount(request, config.store, key);
392
571
  const retrieveQuota = typeof config.max === "function" ? config.max(request, response) : config.max;
393
572
  const maxHits = await retrieveQuota;
394
- augmentedRequest[config.requestPropertyName] = {
573
+ config.validations.max(maxHits);
574
+ const info = {
395
575
  limit: maxHits,
396
576
  current: totalHits,
397
577
  remaining: Math.max(maxHits - totalHits, 0),
398
578
  resetTime
399
579
  };
580
+ augmentedRequest[config.requestPropertyName] = info;
400
581
  if (config.legacyHeaders && !response.headersSent) {
401
- response.setHeader("X-RateLimit-Limit", maxHits);
402
- response.setHeader(
403
- "X-RateLimit-Remaining",
404
- augmentedRequest[config.requestPropertyName].remaining
405
- );
406
- if (resetTime instanceof Date) {
407
- response.setHeader("Date", (/* @__PURE__ */ new Date()).toUTCString());
408
- response.setHeader(
409
- "X-RateLimit-Reset",
410
- Math.ceil(resetTime.getTime() / 1e3)
411
- );
412
- }
582
+ setLegacyHeaders(response, info);
413
583
  }
414
584
  if (config.standardHeaders && !response.headersSent) {
415
- response.setHeader("RateLimit-Limit", maxHits);
416
- response.setHeader(
417
- "RateLimit-Remaining",
418
- augmentedRequest[config.requestPropertyName].remaining
419
- );
420
- if (resetTime) {
421
- const deltaSeconds = Math.ceil(
422
- (resetTime.getTime() - Date.now()) / 1e3
423
- );
424
- response.setHeader("RateLimit-Reset", Math.max(0, deltaSeconds));
585
+ if (config.standardHeaders === "draft-6") {
586
+ setDraft6Headers(response, info, config.windowMs);
587
+ } else if (config.standardHeaders === "draft-7") {
588
+ config.validations.headersResetTime(info.resetTime);
589
+ setDraft7Headers(response, info, config.windowMs);
425
590
  }
426
591
  }
427
592
  if (config.skipFailedRequests || config.skipSuccessfulRequests) {
@@ -457,8 +622,8 @@ var rateLimit = (passedOptions) => {
457
622
  }
458
623
  config.validations.disable();
459
624
  if (maxHits && totalHits > maxHits) {
460
- if ((config.legacyHeaders || config.standardHeaders) && !response.headersSent) {
461
- response.setHeader("Retry-After", Math.ceil(config.windowMs / 1e3));
625
+ if (config.legacyHeaders || config.standardHeaders) {
626
+ setRetryAfterHeader(response, info, config.windowMs);
462
627
  }
463
628
  config.handler(request, response, next, options);
464
629
  return;
@@ -467,6 +632,9 @@ var rateLimit = (passedOptions) => {
467
632
  }
468
633
  );
469
634
  middleware.resetKey = config.store.resetKey.bind(config.store);
635
+ middleware.getKey = (_a = config.store.get) == null ? void 0 : _a.bind(
636
+ config.store
637
+ );
470
638
  return middleware;
471
639
  };
472
640
  var lib_default = rateLimit;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "express-rate-limit",
3
- "version": "6.9.0",
3
+ "version": "6.11.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",
@@ -52,7 +52,7 @@
52
52
  "changelog.md"
53
53
  ],
54
54
  "engines": {
55
- "node": ">= 14.0.0"
55
+ "node": ">= 14"
56
56
  },
57
57
  "scripts": {
58
58
  "clean": "del-cli dist/ coverage/ *.log *.tmp *.bak *.tgz",
@@ -60,13 +60,13 @@
60
60
  "build:esm": "esbuild --platform=node --bundle --target=es2019 --format=esm --outfile=dist/index.mjs source/index.ts",
61
61
  "build:types": "dts-bundle-generator --out-file=dist/index.d.ts source/index.ts && cp dist/index.d.ts dist/index.d.cts && cp dist/index.d.ts dist/index.d.mts",
62
62
  "compile": "run-s clean build:*",
63
- "lint:code": "xo --ignore test/external/",
64
- "lint:rest": "prettier --ignore-path .gitignore --ignore-unknown --check .",
63
+ "lint:code": "xo",
64
+ "lint:rest": "prettier --check .",
65
65
  "lint": "run-s lint:*",
66
- "format:code": "npm run lint:code -- --fix",
67
- "format:rest": "npm run lint:rest -- --write .",
66
+ "format:code": "xo --fix",
67
+ "format:rest": "prettier --write .",
68
68
  "format": "run-s format:*",
69
- "test:lib": "cross-env NODE_NO_WARNINGS=1 NODE_OPTIONS=--experimental-vm-modules jest",
69
+ "test:lib": "cross-env NODE_NO_WARNINGS=1 NODE_OPTIONS=--experimental-vm-modules jest --config config/jest.json",
70
70
  "test:ext": "cd test/external/ && bash run-all-tests",
71
71
  "test": "run-s lint test:lib",
72
72
  "pre-commit": "lint-staged",
@@ -76,9 +76,11 @@
76
76
  "express": "^4 || ^5"
77
77
  },
78
78
  "devDependencies": {
79
- "@jest/globals": "29.6.1",
79
+ "@express-rate-limit/prettier": "1.0.0",
80
+ "@express-rate-limit/tsconfig": "1.0.0",
81
+ "@jest/globals": "29.6.2",
80
82
  "@types/express": "4.17.17",
81
- "@types/jest": "29.5.2",
83
+ "@types/jest": "29.5.3",
82
84
  "@types/node": "20.4.0",
83
85
  "@types/supertest": "2.0.12",
84
86
  "cross-env": "7.0.3",
@@ -87,9 +89,10 @@
87
89
  "esbuild": "0.18.11",
88
90
  "express": "4.18.2",
89
91
  "husky": "8.0.3",
90
- "jest": "29.6.1",
92
+ "jest": "29.6.2",
91
93
  "lint-staged": "13.2.3",
92
94
  "npm-run-all": "4.1.5",
95
+ "ratelimit-header-parser": "0.1.0",
93
96
  "supertest": "6.3.3",
94
97
  "ts-jest": "29.1.1",
95
98
  "ts-node": "10.9.1",
@@ -112,42 +115,18 @@
112
115
  {
113
116
  "files": "test/library/*.ts",
114
117
  "rules": {
115
- "@typescript-eslint/no-unsafe-argument": 0
118
+ "@typescript-eslint/no-unsafe-argument": 0,
119
+ "@typescript-eslint/no-unsafe-assignment": 0
116
120
  }
117
121
  }
118
- ]
119
- },
120
- "prettier": {
121
- "semi": false,
122
- "useTabs": true,
123
- "singleQuote": true,
124
- "bracketSpacing": true,
125
- "trailingComma": "all",
126
- "proseWrap": "always"
127
- },
128
- "jest": {
129
- "preset": "ts-jest/presets/default-esm",
130
- "collectCoverage": true,
131
- "collectCoverageFrom": [
132
- "source/**/*.ts"
133
- ],
134
- "testTimeout": 30000,
135
- "testMatch": [
136
- "**/test/library/**/*-test.[jt]s?(x)"
137
122
  ],
138
- "moduleFileExtensions": [
139
- "js",
140
- "jsx",
141
- "json",
142
- "ts",
143
- "tsx"
144
- ],
145
- "moduleNameMapper": {
146
- "^(\\.{1,2}/.*)\\.js$": "$1"
147
- }
123
+ "ignore": [
124
+ "test/external"
125
+ ]
148
126
  },
127
+ "prettier": "@express-rate-limit/prettier",
149
128
  "lint-staged": {
150
- "{source,test}/**/*.ts": "xo --ignore test/external/ --fix",
151
- "**/*.{json,yaml,md}": "prettier --ignore-path .gitignore --ignore-unknown --write "
129
+ "{source,test}/**/*.ts": "xo --fix",
130
+ "**/*.{json,yaml,md}": "prettier --write "
152
131
  }
153
132
  }
package/readme.md CHANGED
@@ -12,7 +12,7 @@ authentication and more to any API in minutes. Learn more at
12
12
 
13
13
  <div align="center">
14
14
 
15
- [![Tests](https://github.com/express-rate-limit/express-rate-limit/workflows/Test/badge.svg)](https://github.com/express-rate-limit/express-rate-limit/actions)
15
+ [![tests](https://github.com/express-rate-limit/express-rate-limit/actions/workflows/ci.yaml/badge.svg)](https://github.com/express-rate-limit/express-rate-limit/actions/workflows/ci.yaml)
16
16
  [![npm version](https://img.shields.io/npm/v/express-rate-limit.svg)](https://npmjs.org/package/express-rate-limit 'View this project on NPM')
17
17
  [![npm downloads](https://img.shields.io/npm/dm/express-rate-limit)](https://www.npmjs.com/package/express-rate-limit)
18
18
 
@@ -97,13 +97,13 @@ Import it in a CommonJS project (`type: commonjs` or no `type` field in
97
97
  `package.json`) as follows:
98
98
 
99
99
  ```ts
100
- const rateLimit = require('express-rate-limit')
100
+ const { rateLimit } = require('express-rate-limit')
101
101
  ```
102
102
 
103
103
  Import it in a ESM project (`type: module` in `package.json`) as follows:
104
104
 
105
105
  ```ts
106
- import rateLimit from 'express-rate-limit'
106
+ import { rateLimit } from 'express-rate-limit'
107
107
  ```
108
108
 
109
109
  ### Examples
@@ -112,13 +112,13 @@ To use it in an API-only server where the rate-limiter should be applied to all
112
112
  requests:
113
113
 
114
114
  ```ts
115
- import rateLimit from 'express-rate-limit'
115
+ import { rateLimit } from 'express-rate-limit'
116
116
 
117
117
  const limiter = rateLimit({
118
118
  windowMs: 15 * 60 * 1000, // 15 minutes
119
119
  max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
120
- standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
121
- legacyHeaders: false, // Disable the `X-RateLimit-*` headers
120
+ standardHeaders: 'draft-7', // draft-6: RateLimit-* headers; draft-7: combined RateLimit header
121
+ legacyHeaders: false, // X-RateLimit-* headers
122
122
  // store: ... , // Use an external store for more precise rate limiting
123
123
  })
124
124
 
@@ -131,12 +131,12 @@ To use it in a 'regular' web server (e.g. anything that uses
131
131
  requests:
132
132
 
133
133
  ```ts
134
- import rateLimit from 'express-rate-limit'
134
+ import { rateLimit } from 'express-rate-limit'
135
135
 
136
136
  const apiLimiter = rateLimit({
137
137
  windowMs: 15 * 60 * 1000, // 15 minutes
138
138
  max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
139
- standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
139
+ standardHeaders: 'draft-7', // Set `RateLimit` and `RateLimit-Policy`` headers
140
140
  legacyHeaders: false, // Disable the `X-RateLimit-*` headers
141
141
  // store: ... , // Use an external store for more precise rate limiting
142
142
  })
@@ -148,13 +148,13 @@ app.use('/api', apiLimiter)
148
148
  To create multiple instances to apply different rules to different endpoints:
149
149
 
150
150
  ```ts
151
- import rateLimit from 'express-rate-limit'
151
+ import { rateLimit } from 'express-rate-limit'
152
152
 
153
153
  const apiLimiter = rateLimit({
154
154
  windowMs: 15 * 60 * 1000, // 15 minutes
155
155
  max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
156
- standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
157
- legacyHeaders: false, // Disable the `X-RateLimit-*` headers
156
+ standardHeaders: 'draft-7', // draft-6: RateLimit-* headers; draft-7: combined RateLimit header
157
+ legacyHeaders: false, // X-RateLimit-* headers
158
158
  // store: ... , // Use an external store for more precise rate limiting
159
159
  })
160
160
 
@@ -165,8 +165,8 @@ const createAccountLimiter = rateLimit({
165
165
  max: 5, // Limit each IP to 5 create account requests per `window` (here, per hour)
166
166
  message:
167
167
  'Too many accounts created from this IP, please try again after an hour',
168
- standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
169
- legacyHeaders: false, // Disable the `X-RateLimit-*` headers
168
+ standardHeaders: 'draft-7', // draft-6: RateLimit-* headers; draft-7: combined RateLimit header
169
+ legacyHeaders: false, // X-RateLimit-* headers
170
170
  })
171
171
 
172
172
  app.post('/create-account', createAccountLimiter, (request, response) => {
@@ -177,7 +177,7 @@ app.post('/create-account', createAccountLimiter, (request, response) => {
177
177
  To use a custom store:
178
178
 
179
179
  ```ts
180
- import rateLimit from 'express-rate-limit'
180
+ import { rateLimit } from 'express-rate-limit'
181
181
  import RedisStore from 'rate-limit-redis'
182
182
  import RedisClient from 'ioredis'
183
183
 
@@ -185,7 +185,8 @@ const redisClient = new RedisClient()
185
185
  const rateLimiter = rateLimit({
186
186
  windowMs: 15 * 60 * 1000, // 15 minutes
187
187
  max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
188
- standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
188
+ standardHeaders: 'draft-7', // draft-6: RateLimit-* headers; draft-7: combined RateLimit header
189
+ legacyHeaders: false, // X-RateLimit-* headers
189
190
  store: new RedisStore({
190
191
  /* ... */
191
192
  }), // Use the external store
@@ -329,16 +330,38 @@ Defaults to `true` (for backward compatibility).
329
330
 
330
331
  ### `standardHeaders`
331
332
 
332
- > `boolean`
333
+ > `boolean` | `'draft-6'` | `'draft-7'`
333
334
 
334
335
  Whether to enable support for headers conforming to the
335
- [ratelimit standardization draft](https://github.com/ietf-wg-httpapi/ratelimit-headers/blob/main/draft-ietf-httpapi-ratelimit-headers.md)
336
- adopted by the IETF (`RateLimit-Limit`, `RateLimit-Remaining`, and, if the store
337
- supports it, `RateLimit-Reset`). If set to `true`, the middleware also sends the
338
- `Retry-After` header on all blocked requests. May be used in conjunction with,
339
- or instead of the `legacyHeaders` option.
336
+ [RateLimit header fields for HTTP standardization draft](https://github.com/ietf-wg-httpapi/ratelimit-headers)
337
+ adopted by the IETF.
338
+
339
+ If set to `draft-6`, separate `RateLimit-Policy` `RateLimit-Limit`,
340
+ `RateLimit-Remaining`, and, if the store supports it, `RateLimit-Reset` headers
341
+ are set on the response, in accordance with
342
+ [draft-ietf-httpapi-ratelimit-headers-06](https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-ratelimit-headers-06).
343
+
344
+ If set to `draft-7`, a combined `RateLimit` header is set containing limit,
345
+ remaining, and reset values, and a `RateLimit-Policy` header is set, in
346
+ accordiance with
347
+ [draft-ietf-httpapi-ratelimit-headers-07](https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-ratelimit-headers-07).
348
+ `windowMs` is used for the reset value if the store does not provide a reset
349
+ time.
340
350
 
341
- Defaults to `false` (for backward compatibility, but its use is recommended).
351
+ If set to `true`, it is treated as `draft-6`, however this behavior may change
352
+ in a future semver major release.
353
+
354
+ If set to any truthy value, the middleware also sends the `Retry-After` header
355
+ on all blocked requests.
356
+
357
+ The `standardHeaders` option may be used in conjunction with, or instead of the
358
+ `legacyHeaders` option.
359
+
360
+ ℹ️ Tip: use
361
+ [ratelimit-header-parser](https://www.npmjs.com/package/ratelimit-header-parser)
362
+ in clients to read/parse any form of express-rate-limit's headers.
363
+
364
+ Defaults to `false`.
342
365
 
343
366
  > Renamed in `6.x` from `draft_polli_ratelimit_headers` to `standardHeaders`.
344
367
 
@@ -430,8 +453,8 @@ const limiter = rateLimit({
430
453
  > `function`
431
454
 
432
455
  A (sync/async) function that accepts the Express `request` and `response`
433
- objects that is called when a client has reached their rate limit, and will be
434
- rate limited on their next request.
456
+ objects that is called the on the request where a client has just exceeded their
457
+ rate limit.
435
458
 
436
459
  This method was
437
460
  [deprecated in v6](https://github.com/express-rate-limit/express-rate-limit/releases/v6.0.0) -
@@ -515,7 +538,7 @@ Here is a list of external stores:
515
538
  | [`rate-limit-redis`](https://npmjs.com/package/rate-limit-redis) | A [Redis](http://redis.io/)-backed store, more suitable for large or demanding deployments. | Modern as of v3.0.0 |
516
539
  | [`rate-limit-memcached`](https://npmjs.org/package/rate-limit-memcached) | A [Memcached](https://memcached.org/)-backed store. | Legacy |
517
540
  | [`rate-limit-mongo`](https://www.npmjs.com/package/rate-limit-mongo) | A [MongoDB](https://www.mongodb.com/)-backed store. | Legacy |
518
- | [`precise-memory-rate-limit`](https://www.npmjs.com/package/precise-memory-rate-limit) | A memory store similar to the built-in one, except that it stores a distinct timestamp for each key. | Legacy |
541
+ | [`precise-memory-rate-limit`](https://www.npmjs.com/package/precise-memory-rate-limit) | A memory store similar to the built-in one, except that it stores a distinct timestamp for each key. | Modern as of v2.0.0 |
519
542
 
520
543
  Take a look at
521
544
  [this guide](https://github.com/express-rate-limit/express-rate-limit/wiki/Creating-Your-Own-Store)
package/tsconfig.json CHANGED
@@ -1,13 +1,5 @@
1
1
  {
2
2
  "include": ["source/"],
3
3
  "exclude": ["node_modules/"],
4
- "compilerOptions": {
5
- "declaration": true,
6
- "strict": true,
7
- "noUnusedLocals": true,
8
- "noImplicitReturns": true,
9
- "noFallthroughCasesInSwitch": true,
10
- "esModuleInterop": true,
11
- "moduleResolution": "node"
12
- }
4
+ "extends": "@express-rate-limit/tsconfig"
13
5
  }