express-rate-limit 7.5.1 → 8.0.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.mjs CHANGED
@@ -1,3 +1,170 @@
1
+ // source/ip-key-generator.ts
2
+ import { isIPv6 } from "node:net";
3
+ import { Address6 } from "ip-address";
4
+ function ipKeyGenerator(ip, ipv6Subnet = 56) {
5
+ if (ipv6Subnet && isIPv6(ip)) {
6
+ return `${new Address6(`${ip}/${ipv6Subnet}`).startAddress().correctForm()}/${ipv6Subnet}`;
7
+ }
8
+ return ip;
9
+ }
10
+
11
+ // source/memory-store.ts
12
+ var MemoryStore = class {
13
+ constructor() {
14
+ /**
15
+ * These two maps store usage (requests) and reset time by key (for example, IP
16
+ * addresses or API keys).
17
+ *
18
+ * They are split into two to avoid having to iterate through the entire set to
19
+ * determine which ones need reset. Instead, `Client`s are moved from `previous`
20
+ * to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients
21
+ * left in `previous`, i.e., those that have not made any recent requests, are
22
+ * known to be expired and can be deleted in bulk.
23
+ */
24
+ this.previous = /* @__PURE__ */ new Map();
25
+ this.current = /* @__PURE__ */ new Map();
26
+ /**
27
+ * Confirmation that the keys incremented in once instance of MemoryStore
28
+ * cannot affect other instances.
29
+ */
30
+ this.localKeys = true;
31
+ }
32
+ /**
33
+ * Method that initializes the store.
34
+ *
35
+ * @param options {Options} - The options used to setup the middleware.
36
+ */
37
+ init(options) {
38
+ this.windowMs = options.windowMs;
39
+ if (this.interval) clearInterval(this.interval);
40
+ this.interval = setInterval(() => {
41
+ this.clearExpired();
42
+ }, this.windowMs);
43
+ this.interval.unref?.();
44
+ }
45
+ /**
46
+ * Method to fetch a client's hit count and reset time.
47
+ *
48
+ * @param key {string} - The identifier for a client.
49
+ *
50
+ * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
51
+ *
52
+ * @public
53
+ */
54
+ async get(key) {
55
+ return this.current.get(key) ?? this.previous.get(key);
56
+ }
57
+ /**
58
+ * Method to increment a client's hit counter.
59
+ *
60
+ * @param key {string} - The identifier for a client.
61
+ *
62
+ * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
63
+ *
64
+ * @public
65
+ */
66
+ async increment(key) {
67
+ const client = this.getClient(key);
68
+ const now = Date.now();
69
+ if (client.resetTime.getTime() <= now) {
70
+ this.resetClient(client, now);
71
+ }
72
+ client.totalHits++;
73
+ return client;
74
+ }
75
+ /**
76
+ * Method to decrement a client's hit counter.
77
+ *
78
+ * @param key {string} - The identifier for a client.
79
+ *
80
+ * @public
81
+ */
82
+ async decrement(key) {
83
+ const client = this.getClient(key);
84
+ if (client.totalHits > 0) client.totalHits--;
85
+ }
86
+ /**
87
+ * Method to reset a client's hit counter.
88
+ *
89
+ * @param key {string} - The identifier for a client.
90
+ *
91
+ * @public
92
+ */
93
+ async resetKey(key) {
94
+ this.current.delete(key);
95
+ this.previous.delete(key);
96
+ }
97
+ /**
98
+ * Method to reset everyone's hit counter.
99
+ *
100
+ * @public
101
+ */
102
+ async resetAll() {
103
+ this.current.clear();
104
+ this.previous.clear();
105
+ }
106
+ /**
107
+ * Method to stop the timer (if currently running) and prevent any memory
108
+ * leaks.
109
+ *
110
+ * @public
111
+ */
112
+ shutdown() {
113
+ clearInterval(this.interval);
114
+ void this.resetAll();
115
+ }
116
+ /**
117
+ * Recycles a client by setting its hit count to zero, and reset time to
118
+ * `windowMs` milliseconds from now.
119
+ *
120
+ * NOT to be confused with `#resetKey()`, which removes a client from both the
121
+ * `current` and `previous` maps.
122
+ *
123
+ * @param client {Client} - The client to recycle.
124
+ * @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client.
125
+ *
126
+ * @return {Client} - The modified client that was passed in, to allow for chaining.
127
+ */
128
+ resetClient(client, now = Date.now()) {
129
+ client.totalHits = 0;
130
+ client.resetTime.setTime(now + this.windowMs);
131
+ return client;
132
+ }
133
+ /**
134
+ * Retrieves or creates a client, given a key. Also ensures that the client being
135
+ * returned is in the `current` map.
136
+ *
137
+ * @param key {string} - The key under which the client is (or is to be) stored.
138
+ *
139
+ * @returns {Client} - The requested client.
140
+ */
141
+ getClient(key) {
142
+ if (this.current.has(key)) return this.current.get(key);
143
+ let client;
144
+ if (this.previous.has(key)) {
145
+ client = this.previous.get(key);
146
+ this.previous.delete(key);
147
+ } else {
148
+ client = { totalHits: 0, resetTime: /* @__PURE__ */ new Date() };
149
+ this.resetClient(client);
150
+ }
151
+ this.current.set(key, client);
152
+ return client;
153
+ }
154
+ /**
155
+ * Move current clients to previous, create a new map for current.
156
+ *
157
+ * This function is called every `windowMs`.
158
+ */
159
+ clearExpired() {
160
+ this.previous = this.current;
161
+ this.current = /* @__PURE__ */ new Map();
162
+ }
163
+ };
164
+
165
+ // source/rate-limit.ts
166
+ import { isIPv6 as isIPv62 } from "node:net";
167
+
1
168
  // source/headers.ts
2
169
  import { Buffer } from "node:buffer";
3
170
  import { createHash } from "node:crypto";
@@ -6,12 +173,12 @@ var SUPPORTED_DRAFT_VERSIONS = [
6
173
  "draft-7",
7
174
  "draft-8"
8
175
  ];
9
- var getResetSeconds = (resetTime, windowMs) => {
10
- let resetSeconds = void 0;
176
+ var getResetSeconds = (windowMs, resetTime) => {
177
+ let resetSeconds;
11
178
  if (resetTime) {
12
179
  const deltaSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1e3);
13
180
  resetSeconds = Math.max(0, deltaSeconds);
14
- } else if (windowMs) {
181
+ } else {
15
182
  resetSeconds = Math.ceil(windowMs / 1e3);
16
183
  }
17
184
  return resetSeconds;
@@ -37,7 +204,7 @@ var setLegacyHeaders = (response, info) => {
37
204
  var setDraft6Headers = (response, info, windowMs) => {
38
205
  if (response.headersSent) return;
39
206
  const windowSeconds = Math.ceil(windowMs / 1e3);
40
- const resetSeconds = getResetSeconds(info.resetTime);
207
+ const resetSeconds = getResetSeconds(windowMs, info.resetTime);
41
208
  response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
42
209
  response.setHeader("RateLimit-Limit", info.limit.toString());
43
210
  response.setHeader("RateLimit-Remaining", info.remaining.toString());
@@ -47,7 +214,7 @@ var setDraft6Headers = (response, info, windowMs) => {
47
214
  var setDraft7Headers = (response, info, windowMs) => {
48
215
  if (response.headersSent) return;
49
216
  const windowSeconds = Math.ceil(windowMs / 1e3);
50
- const resetSeconds = getResetSeconds(info.resetTime, windowMs);
217
+ const resetSeconds = getResetSeconds(windowMs, info.resetTime);
51
218
  response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
52
219
  response.setHeader(
53
220
  "RateLimit",
@@ -57,19 +224,31 @@ var setDraft7Headers = (response, info, windowMs) => {
57
224
  var setDraft8Headers = (response, info, windowMs, name, key) => {
58
225
  if (response.headersSent) return;
59
226
  const windowSeconds = Math.ceil(windowMs / 1e3);
60
- const resetSeconds = getResetSeconds(info.resetTime, windowMs);
227
+ const resetSeconds = getResetSeconds(windowMs, info.resetTime);
61
228
  const partitionKey = getPartitionKey(key);
62
- const policy = `q=${info.limit}; w=${windowSeconds}; pk=:${partitionKey}:`;
63
229
  const header = `r=${info.remaining}; t=${resetSeconds}`;
64
- response.append("RateLimit-Policy", `"${name}"; ${policy}`);
230
+ const policy = `q=${info.limit}; w=${windowSeconds}; pk=:${partitionKey}:`;
65
231
  response.append("RateLimit", `"${name}"; ${header}`);
232
+ response.append("RateLimit-Policy", `"${name}"; ${policy}`);
66
233
  };
67
234
  var setRetryAfterHeader = (response, info, windowMs) => {
68
235
  if (response.headersSent) return;
69
- const resetSeconds = getResetSeconds(info.resetTime, windowMs);
236
+ const resetSeconds = getResetSeconds(windowMs, info.resetTime);
70
237
  response.setHeader("Retry-After", resetSeconds.toString());
71
238
  };
72
239
 
240
+ // source/utils.ts
241
+ var omitUndefinedProperties = (passedOptions) => {
242
+ const omittedOptions = {};
243
+ for (const k of Object.keys(passedOptions)) {
244
+ const key = k;
245
+ if (passedOptions[key] !== void 0) {
246
+ omittedOptions[key] = passedOptions[key];
247
+ }
248
+ }
249
+ return omittedOptions;
250
+ };
251
+
73
252
  // source/validations.ts
74
253
  import { isIP } from "node:net";
75
254
  var ValidationError = class extends Error {
@@ -93,7 +272,6 @@ var ChangeWarning = class extends ValidationError {
93
272
  var usedStores = /* @__PURE__ */ new Set();
94
273
  var singleCountKeys = /* @__PURE__ */ new WeakMap();
95
274
  var validations = {
96
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
97
275
  enabled: {
98
276
  default: true
99
277
  },
@@ -228,7 +406,7 @@ var validations = {
228
406
  if (limit === 0) {
229
407
  throw new ChangeWarning(
230
408
  "WRN_ERL_MAX_ZERO",
231
- `Setting limit or max to 0 disables rate limiting in express-rate-limit v6 and older, but will cause all requests to be blocked in v7`
409
+ "Setting limit or max to 0 disables rate limiting in express-rate-limit v6 and older, but will cause all requests to be blocked in v7"
232
410
  );
233
411
  }
234
412
  },
@@ -260,7 +438,7 @@ var validations = {
260
438
  if (onLimitReached) {
261
439
  throw new ChangeWarning(
262
440
  "WRN_ERL_DEPRECATED_ON_LIMIT_REACHED",
263
- `The onLimitReached configuration option is deprecated and has been removed in express-rate-limit v7.`
441
+ "The onLimitReached configuration option is deprecated and has been removed in express-rate-limit v7."
264
442
  );
265
443
  }
266
444
  },
@@ -329,7 +507,8 @@ var validations = {
329
507
  const { stack } = new Error(
330
508
  "express-rate-limit validation check (set options.validate.creationStack=false to disable)"
331
509
  );
332
- if (stack?.includes("Layer.handle [as handle_request]")) {
510
+ if (stack?.includes("Layer.handle [as handle_request]") || // express v4
511
+ stack?.includes("Layer.handleRequest")) {
333
512
  if (!store.localKeys) {
334
513
  throw new ValidationError(
335
514
  "ERR_ERL_CREATED_IN_REQUEST_HANDLER",
@@ -338,7 +517,38 @@ var validations = {
338
517
  }
339
518
  throw new ValidationError(
340
519
  "ERR_ERL_CREATED_IN_REQUEST_HANDLER",
341
- `express-rate-limit instance should be created at app initialization, not when responding to a request.`
520
+ "express-rate-limit instance should be created at app initialization, not when responding to a request."
521
+ );
522
+ }
523
+ },
524
+ ipv6Subnet(ipv6Subnet) {
525
+ if (ipv6Subnet === false) {
526
+ return;
527
+ }
528
+ if (!Number.isInteger(ipv6Subnet) || ipv6Subnet < 32 || ipv6Subnet > 64) {
529
+ throw new ValidationError(
530
+ "ERR_ERL_IPV6_SUBNET",
531
+ `Unexpected ipv6Subnet value: ${ipv6Subnet}. Expected an integer between 32 and 64 (usually 48-64).`
532
+ );
533
+ }
534
+ },
535
+ ipv6SubnetOrKeyGenerator(options) {
536
+ if (options.ipv6Subnet !== void 0 && options.keyGenerator) {
537
+ throw new ValidationError(
538
+ "ERR_ERL_IPV6SUBNET_OR_KEYGENERATOR",
539
+ `Incompatible options: the 'ipv6Subnet' option is ignored when a custom 'keyGenerator' function is also set.`
540
+ );
541
+ }
542
+ },
543
+ keyGeneratorIpFallback(keyGenerator) {
544
+ if (!keyGenerator) {
545
+ return;
546
+ }
547
+ const src = keyGenerator.toString();
548
+ if ((src.includes("req.ip") || src.includes("request.ip")) && !src.includes("ipKeyGenerator")) {
549
+ throw new ValidationError(
550
+ "ERR_ERL_KEY_GEN_IPV6",
551
+ "Custom keyGenerator appears to use request IP without calling the ipKeyGenerator helper function for IPv6 addresses. This could allow IPv6 users to bypass limits."
342
552
  );
343
553
  }
344
554
  }
@@ -355,9 +565,7 @@ var getValidations = (_enabled) => {
355
565
  ..._enabled
356
566
  };
357
567
  }
358
- const wrappedValidations = {
359
- enabled
360
- };
568
+ const wrappedValidations = { enabled };
361
569
  for (const [name, validation] of Object.entries(validations)) {
362
570
  if (typeof validation === "function")
363
571
  wrappedValidations[name] = (...args) => {
@@ -379,161 +587,7 @@ var getValidations = (_enabled) => {
379
587
  return wrappedValidations;
380
588
  };
381
589
 
382
- // source/memory-store.ts
383
- var MemoryStore = class {
384
- constructor() {
385
- /**
386
- * These two maps store usage (requests) and reset time by key (for example, IP
387
- * addresses or API keys).
388
- *
389
- * They are split into two to avoid having to iterate through the entire set to
390
- * determine which ones need reset. Instead, `Client`s are moved from `previous`
391
- * to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients
392
- * left in `previous`, i.e., those that have not made any recent requests, are
393
- * known to be expired and can be deleted in bulk.
394
- */
395
- this.previous = /* @__PURE__ */ new Map();
396
- this.current = /* @__PURE__ */ new Map();
397
- /**
398
- * Confirmation that the keys incremented in once instance of MemoryStore
399
- * cannot affect other instances.
400
- */
401
- this.localKeys = true;
402
- }
403
- /**
404
- * Method that initializes the store.
405
- *
406
- * @param options {Options} - The options used to setup the middleware.
407
- */
408
- init(options) {
409
- this.windowMs = options.windowMs;
410
- if (this.interval) clearInterval(this.interval);
411
- this.interval = setInterval(() => {
412
- this.clearExpired();
413
- }, this.windowMs);
414
- if (this.interval.unref) this.interval.unref();
415
- }
416
- /**
417
- * Method to fetch a client's hit count and reset time.
418
- *
419
- * @param key {string} - The identifier for a client.
420
- *
421
- * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
422
- *
423
- * @public
424
- */
425
- async get(key) {
426
- return this.current.get(key) ?? this.previous.get(key);
427
- }
428
- /**
429
- * Method to increment a client's hit counter.
430
- *
431
- * @param key {string} - The identifier for a client.
432
- *
433
- * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
434
- *
435
- * @public
436
- */
437
- async increment(key) {
438
- const client = this.getClient(key);
439
- const now = Date.now();
440
- if (client.resetTime.getTime() <= now) {
441
- this.resetClient(client, now);
442
- }
443
- client.totalHits++;
444
- return client;
445
- }
446
- /**
447
- * Method to decrement a client's hit counter.
448
- *
449
- * @param key {string} - The identifier for a client.
450
- *
451
- * @public
452
- */
453
- async decrement(key) {
454
- const client = this.getClient(key);
455
- if (client.totalHits > 0) client.totalHits--;
456
- }
457
- /**
458
- * Method to reset a client's hit counter.
459
- *
460
- * @param key {string} - The identifier for a client.
461
- *
462
- * @public
463
- */
464
- async resetKey(key) {
465
- this.current.delete(key);
466
- this.previous.delete(key);
467
- }
468
- /**
469
- * Method to reset everyone's hit counter.
470
- *
471
- * @public
472
- */
473
- async resetAll() {
474
- this.current.clear();
475
- this.previous.clear();
476
- }
477
- /**
478
- * Method to stop the timer (if currently running) and prevent any memory
479
- * leaks.
480
- *
481
- * @public
482
- */
483
- shutdown() {
484
- clearInterval(this.interval);
485
- void this.resetAll();
486
- }
487
- /**
488
- * Recycles a client by setting its hit count to zero, and reset time to
489
- * `windowMs` milliseconds from now.
490
- *
491
- * NOT to be confused with `#resetKey()`, which removes a client from both the
492
- * `current` and `previous` maps.
493
- *
494
- * @param client {Client} - The client to recycle.
495
- * @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client.
496
- *
497
- * @return {Client} - The modified client that was passed in, to allow for chaining.
498
- */
499
- resetClient(client, now = Date.now()) {
500
- client.totalHits = 0;
501
- client.resetTime.setTime(now + this.windowMs);
502
- return client;
503
- }
504
- /**
505
- * Retrieves or creates a client, given a key. Also ensures that the client being
506
- * returned is in the `current` map.
507
- *
508
- * @param key {string} - The key under which the client is (or is to be) stored.
509
- *
510
- * @returns {Client} - The requested client.
511
- */
512
- getClient(key) {
513
- if (this.current.has(key)) return this.current.get(key);
514
- let client;
515
- if (this.previous.has(key)) {
516
- client = this.previous.get(key);
517
- this.previous.delete(key);
518
- } else {
519
- client = { totalHits: 0, resetTime: /* @__PURE__ */ new Date() };
520
- this.resetClient(client);
521
- }
522
- this.current.set(key, client);
523
- return client;
524
- }
525
- /**
526
- * Move current clients to previous, create a new map for current.
527
- *
528
- * This function is called every `windowMs`.
529
- */
530
- clearExpired() {
531
- this.previous = this.current;
532
- this.current = /* @__PURE__ */ new Map();
533
- }
534
- };
535
-
536
- // source/lib.ts
590
+ // source/rate-limit.ts
537
591
  var isLegacyStore = (store) => (
538
592
  // Check that `incr` exists but `increment` does not - store authors might want
539
593
  // to keep both around for backwards compatibility.
@@ -577,18 +631,8 @@ var getOptionsFromConfig = (config) => {
577
631
  validate: validations2.enabled
578
632
  };
579
633
  };
580
- var omitUndefinedOptions = (passedOptions) => {
581
- const omittedOptions = {};
582
- for (const k of Object.keys(passedOptions)) {
583
- const key = k;
584
- if (passedOptions[key] !== void 0) {
585
- omittedOptions[key] = passedOptions[key];
586
- }
587
- }
588
- return omittedOptions;
589
- };
590
634
  var parseOptions = (passedOptions) => {
591
- const notUndefinedOptions = omitUndefinedOptions(passedOptions);
635
+ const notUndefinedOptions = omitUndefinedProperties(passedOptions);
592
636
  const validations2 = getValidations(notUndefinedOptions?.validate ?? true);
593
637
  validations2.validationsConfig();
594
638
  validations2.draftPolliHeaders(
@@ -596,6 +640,11 @@ var parseOptions = (passedOptions) => {
596
640
  notUndefinedOptions.draft_polli_ratelimit_headers
597
641
  );
598
642
  validations2.onLimitReached(notUndefinedOptions.onLimitReached);
643
+ if (notUndefinedOptions.ipv6Subnet !== void 0 && typeof notUndefinedOptions.ipv6Subnet !== "function") {
644
+ validations2.ipv6Subnet(notUndefinedOptions.ipv6Subnet);
645
+ }
646
+ validations2.keyGeneratorIpFallback(notUndefinedOptions.keyGenerator);
647
+ validations2.ipv6SubnetOrKeyGenerator(notUndefinedOptions);
599
648
  let standardHeaders = notUndefinedOptions.standardHeaders ?? false;
600
649
  if (standardHeaders === true) standardHeaders = "draft-6";
601
650
  const config = {
@@ -624,21 +673,27 @@ var parseOptions = (passedOptions) => {
624
673
  skipSuccessfulRequests: false,
625
674
  requestWasSuccessful: (_request, response) => response.statusCode < 400,
626
675
  skip: (_request, _response) => false,
627
- keyGenerator(request, _response) {
676
+ async keyGenerator(request, response) {
628
677
  validations2.ip(request.ip);
629
678
  validations2.trustProxy(request);
630
679
  validations2.xForwardedForHeader(request);
631
- return request.ip;
680
+ const ip = request.ip;
681
+ let subnet = 56;
682
+ if (isIPv62(ip)) {
683
+ subnet = typeof config.ipv6Subnet === "function" ? await config.ipv6Subnet(request, response) : config.ipv6Subnet;
684
+ if (typeof config.ipv6Subnet === "function")
685
+ validations2.ipv6Subnet(subnet);
686
+ }
687
+ return ipKeyGenerator(ip, subnet);
632
688
  },
689
+ ipv6Subnet: 56,
633
690
  async handler(request, response, _next, _optionsUsed) {
634
691
  response.status(config.statusCode);
635
692
  const message = typeof config.message === "function" ? await config.message(
636
693
  request,
637
694
  response
638
695
  ) : config.message;
639
- if (!response.writableEnded) {
640
- response.send(message);
641
- }
696
+ if (!response.writableEnded) response.send(message);
642
697
  },
643
698
  passOnStoreError: false,
644
699
  // Allow the default options to be overridden by the passed options.
@@ -706,7 +761,8 @@ var rateLimit = (passedOptions) => {
706
761
  limit,
707
762
  used: totalHits,
708
763
  remaining: Math.max(limit - totalHits, 0),
709
- resetTime
764
+ resetTime,
765
+ key
710
766
  };
711
767
  Object.defineProperty(info, "current", {
712
768
  configurable: false,
@@ -786,9 +842,10 @@ var rateLimit = (passedOptions) => {
786
842
  middleware.getKey = typeof config.store.get === "function" ? config.store.get.bind(config.store) : getThrowFn;
787
843
  return middleware;
788
844
  };
789
- var lib_default = rateLimit;
845
+ var rate_limit_default = rateLimit;
790
846
  export {
791
847
  MemoryStore,
792
- lib_default as default,
793
- lib_default as rateLimit
848
+ rate_limit_default as default,
849
+ ipKeyGenerator,
850
+ rate_limit_default as rateLimit
794
851
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "express-rate-limit",
3
- "version": "7.5.1",
3
+ "version": "8.0.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",
@@ -56,78 +56,57 @@
56
56
  },
57
57
  "scripts": {
58
58
  "clean": "del-cli dist/ coverage/ *.log *.tmp *.bak *.tgz",
59
- "build:cjs": "esbuild --platform=node --bundle --target=es2022 --format=cjs --outfile=dist/index.cjs --footer:js=\"module.exports = rateLimit; module.exports.default = rateLimit; module.exports.rateLimit = rateLimit; module.exports.MemoryStore = MemoryStore;\" source/index.ts",
60
- "build:esm": "esbuild --platform=node --bundle --target=es2022 --format=esm --outfile=dist/index.mjs source/index.ts",
59
+ "build:cjs": "esbuild --packages=external --platform=node --bundle --target=es2022 --format=cjs --outfile=dist/index.cjs --footer:js=\"module.exports = Object.assign(rateLimit, module.exports);\" source/index.ts",
60
+ "build:esm": "esbuild --packages=external --platform=node --bundle --target=es2022 --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
63
  "docs": "cd docs && mintlify dev",
64
- "lint:code": "xo",
65
- "lint:rest": "prettier --check .",
64
+ "lint:code": "biome check",
65
+ "lint:docs": "prettier --check docs/ *.md",
66
66
  "lint": "run-s lint:*",
67
- "format:code": "xo --fix",
68
- "format:rest": "prettier --write .",
67
+ "format:code": "biome check --write",
68
+ "format:docs": "prettier --write docs/ *.md",
69
69
  "format": "run-s format:*",
70
70
  "test:lib": "jest",
71
71
  "test:ext": "cd test/external/ && bash run-all-tests",
72
72
  "test": "run-s lint test:lib",
73
73
  "pre-commit": "lint-staged",
74
- "prepare": "run-s compile && husky install config/husky"
74
+ "prepare": "run-s compile && husky"
75
75
  },
76
76
  "peerDependencies": {
77
77
  "express": ">= 4.11"
78
78
  },
79
79
  "devDependencies": {
80
+ "@biomejs/biome": "2.1.1",
80
81
  "@express-rate-limit/prettier": "1.1.1",
81
82
  "@express-rate-limit/tsconfig": "1.0.2",
82
- "@jest/globals": "29.7.0",
83
- "@types/express": "4.17.20",
84
- "@types/jest": "29.5.6",
85
- "@types/node": "20.8.7",
86
- "@types/supertest": "2.0.15",
87
- "del-cli": "5.1.0",
83
+ "@jest/globals": "30.0.4",
84
+ "@types/express": "5.0.3",
85
+ "@types/jest": "30.0.0",
86
+ "@types/node": "24.0.14",
87
+ "@types/supertest": "6.0.3",
88
+ "del-cli": "6.0.0",
88
89
  "dts-bundle-generator": "8.0.1",
89
- "esbuild": "0.25.0",
90
- "express": "4.21.1",
91
- "husky": "8.0.3",
92
- "jest": "29.7.0",
93
- "lint-staged": "15.0.2",
94
- "mintlify": "4.0.63",
90
+ "esbuild": "0.25.6",
91
+ "express": "5.1.0",
92
+ "husky": "9.1.7",
93
+ "jest": "30.0.4",
94
+ "lint-staged": "16.1.2",
95
+ "mintlify": "4.2.15",
95
96
  "npm-run-all": "4.1.5",
97
+ "prettier": "3.6.2",
96
98
  "ratelimit-header-parser": "0.1.0",
97
- "supertest": "6.3.3",
98
- "ts-jest": "29.1.1",
99
- "ts-node": "10.9.1",
100
- "typescript": "5.2.2",
101
- "xo": "0.56.0"
102
- },
103
- "xo": {
104
- "prettier": true,
105
- "rules": {
106
- "@typescript-eslint/no-empty-function": 0,
107
- "@typescript-eslint/no-dynamic-delete": 0,
108
- "@typescript-eslint/no-confusing-void-expression": 0,
109
- "@typescript-eslint/consistent-indexed-object-style": [
110
- "error",
111
- "index-signature"
112
- ],
113
- "n/no-unsupported-features/es-syntax": 0
114
- },
115
- "overrides": [
116
- {
117
- "files": "test/library/*.ts",
118
- "rules": {
119
- "@typescript-eslint/no-unsafe-argument": 0,
120
- "@typescript-eslint/no-unsafe-assignment": 0
121
- }
122
- }
123
- ],
124
- "ignore": [
125
- "test/external"
126
- ]
99
+ "supertest": "7.1.3",
100
+ "ts-jest": "29.4.0",
101
+ "ts-node": "10.9.2",
102
+ "typescript": "5.8.3"
127
103
  },
128
104
  "prettier": "@express-rate-limit/prettier",
129
105
  "lint-staged": {
130
- "{source,test}/**/*.ts": "xo --fix",
131
- "**/*.{json,yaml,md}": "prettier --write "
106
+ "*.{js,ts,json}": "biome check --write",
107
+ "*.{md,yaml}": "prettier --write"
108
+ },
109
+ "dependencies": {
110
+ "ip-address": "10.0.1"
132
111
  }
133
112
  }