express-rate-limit
Advanced tools
+12
-0
@@ -9,2 +9,14 @@ # Changelog | ||
| ## [6.9.0](https://github.com/express-rate-limit/express-rate-limit/releases/tag/v6.9.0) | ||
| ### Added | ||
| - New validaion check for double-counted requests | ||
| - Added help link to each ValidationError, directing users to the appropriate | ||
| wiki page for more info | ||
| ### Changed | ||
| - Miscaleanous documenation improvements | ||
| ## [6.8.1](https://github.com/express-rate-limit/express-rate-limit/releases/tag/v6.8.0) & [6.7.2](https://github.com/express-rate-limit/express-rate-limit/releases/tag/v6.8.0) | ||
@@ -11,0 +23,0 @@ |
+54
-5
@@ -45,13 +45,13 @@ "use strict"; | ||
| constructor(code, message) { | ||
| super( | ||
| `express-rate-limit: ${code} - ${message} See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#${code.toLowerCase()} for more information on this error.` | ||
| ); | ||
| const url = `https://express-rate-limit.github.io/${code}/`; | ||
| super(`${message} See ${url} for more information on this error.`); | ||
| __publicField(this, "name"); | ||
| __publicField(this, "code"); | ||
| __publicField(this, "help"); | ||
| this.name = this.constructor.name; | ||
| this.code = code; | ||
| this.message = message; | ||
| this.help = url; | ||
| } | ||
| }; | ||
| var Validations = class { | ||
| var _Validations = class _Validations { | ||
| constructor(enabled) { | ||
@@ -133,2 +133,33 @@ // eslint-disable-next-line @typescript-eslint/parameter-properties | ||
| } | ||
| /** | ||
| * Ensures a given key is incremented only once per request. | ||
| * | ||
| * @param request {Request} - The Express request object. | ||
| * @param store {Store} - The store class. | ||
| * @param key {string} - The key used to store the client's hit count. | ||
| * | ||
| * @returns {void} | ||
| */ | ||
| singleCount(request, store, key) { | ||
| this.wrap(() => { | ||
| let storeKeys = _Validations.singleCountKeys.get(request); | ||
| if (!storeKeys) { | ||
| storeKeys = /* @__PURE__ */ new Map(); | ||
| _Validations.singleCountKeys.set(request, storeKeys); | ||
| } | ||
| const storeKey = store.localKeys ? store : store.constructor.name; | ||
| let keys = storeKeys.get(storeKey); | ||
| if (!keys) { | ||
| keys = []; | ||
| storeKeys.set(storeKey, keys); | ||
| } | ||
| if (keys.includes(key)) { | ||
| throw new ValidationError( | ||
| "ERR_ERL_DOUBLE_COUNT", | ||
| `The hit count for ${key} was incremented more than once for a single request.` | ||
| ); | ||
| } | ||
| keys.push(key); | ||
| }); | ||
| } | ||
| wrap(validation) { | ||
@@ -145,2 +176,14 @@ if (!this.enabled) { | ||
| }; | ||
| /** | ||
| * Maps the key used in a store for a certain request, and ensures that the | ||
| * same key isn't used more than once per request. | ||
| * | ||
| * The store can be any one of the following: | ||
| * - An instance, for stores like the MemoryStore where two instances do not | ||
| * share state. | ||
| * - A string (class name), for stores where multiple instances | ||
| * typically share state, such as the Redis store. | ||
| */ | ||
| __publicField(_Validations, "singleCountKeys", /* @__PURE__ */ new WeakMap()); | ||
| var Validations = _Validations; | ||
@@ -171,2 +214,7 @@ // source/memory-store.ts | ||
| __publicField(this, "interval"); | ||
| /** | ||
| * Confirmation that the keys incremented in once instance of MemoryStore | ||
| * cannot affect other instances. | ||
| */ | ||
| __publicField(this, "localKeys", true); | ||
| } | ||
@@ -373,2 +421,3 @@ /** | ||
| const { totalHits, resetTime } = await config.store.increment(key); | ||
| config.validations.singleCount(request, config.store, key); | ||
| const retrieveQuota = typeof config.max === "function" ? config.max(request, response) : config.max; | ||
@@ -375,0 +424,0 @@ const maxHits = await retrieveQuota; |
+13
-0
@@ -133,2 +133,10 @@ // Generated by dts-bundle-generator v7.0.0 | ||
| shutdown?: () => Promise<void> | void; | ||
| /** | ||
| * Flag to indicate that keys incremented in one instance of this store can | ||
| * not affect other instances. Typically false if a database is used, true for | ||
| * MemoryStore. | ||
| * | ||
| * Used to help detect double-counting misconfigurations. | ||
| */ | ||
| localKeys?: boolean; | ||
| }; | ||
@@ -314,2 +322,7 @@ /** | ||
| /** | ||
| * Confirmation that the keys incremented in once instance of MemoryStore | ||
| * cannot affect other instances. | ||
| */ | ||
| localKeys: boolean; | ||
| /** | ||
| * Method that initializes the store. | ||
@@ -316,0 +329,0 @@ * |
+13
-0
@@ -133,2 +133,10 @@ // Generated by dts-bundle-generator v7.0.0 | ||
| shutdown?: () => Promise<void> | void; | ||
| /** | ||
| * Flag to indicate that keys incremented in one instance of this store can | ||
| * not affect other instances. Typically false if a database is used, true for | ||
| * MemoryStore. | ||
| * | ||
| * Used to help detect double-counting misconfigurations. | ||
| */ | ||
| localKeys?: boolean; | ||
| }; | ||
@@ -314,2 +322,7 @@ /** | ||
| /** | ||
| * Confirmation that the keys incremented in once instance of MemoryStore | ||
| * cannot affect other instances. | ||
| */ | ||
| localKeys: boolean; | ||
| /** | ||
| * Method that initializes the store. | ||
@@ -316,0 +329,0 @@ * |
+13
-0
@@ -133,2 +133,10 @@ // Generated by dts-bundle-generator v7.0.0 | ||
| shutdown?: () => Promise<void> | void; | ||
| /** | ||
| * Flag to indicate that keys incremented in one instance of this store can | ||
| * not affect other instances. Typically false if a database is used, true for | ||
| * MemoryStore. | ||
| * | ||
| * Used to help detect double-counting misconfigurations. | ||
| */ | ||
| localKeys?: boolean; | ||
| }; | ||
@@ -314,2 +322,7 @@ /** | ||
| /** | ||
| * Confirmation that the keys incremented in once instance of MemoryStore | ||
| * cannot affect other instances. | ||
| */ | ||
| localKeys: boolean; | ||
| /** | ||
| * Method that initializes the store. | ||
@@ -316,0 +329,0 @@ * |
+54
-5
@@ -19,13 +19,13 @@ var __defProp = Object.defineProperty; | ||
| constructor(code, message) { | ||
| super( | ||
| `express-rate-limit: ${code} - ${message} See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#${code.toLowerCase()} for more information on this error.` | ||
| ); | ||
| const url = `https://express-rate-limit.github.io/${code}/`; | ||
| super(`${message} See ${url} for more information on this error.`); | ||
| __publicField(this, "name"); | ||
| __publicField(this, "code"); | ||
| __publicField(this, "help"); | ||
| this.name = this.constructor.name; | ||
| this.code = code; | ||
| this.message = message; | ||
| this.help = url; | ||
| } | ||
| }; | ||
| var Validations = class { | ||
| var _Validations = class _Validations { | ||
| constructor(enabled) { | ||
@@ -107,2 +107,33 @@ // eslint-disable-next-line @typescript-eslint/parameter-properties | ||
| } | ||
| /** | ||
| * Ensures a given key is incremented only once per request. | ||
| * | ||
| * @param request {Request} - The Express request object. | ||
| * @param store {Store} - The store class. | ||
| * @param key {string} - The key used to store the client's hit count. | ||
| * | ||
| * @returns {void} | ||
| */ | ||
| singleCount(request, store, key) { | ||
| this.wrap(() => { | ||
| let storeKeys = _Validations.singleCountKeys.get(request); | ||
| if (!storeKeys) { | ||
| storeKeys = /* @__PURE__ */ new Map(); | ||
| _Validations.singleCountKeys.set(request, storeKeys); | ||
| } | ||
| const storeKey = store.localKeys ? store : store.constructor.name; | ||
| let keys = storeKeys.get(storeKey); | ||
| if (!keys) { | ||
| keys = []; | ||
| storeKeys.set(storeKey, keys); | ||
| } | ||
| if (keys.includes(key)) { | ||
| throw new ValidationError( | ||
| "ERR_ERL_DOUBLE_COUNT", | ||
| `The hit count for ${key} was incremented more than once for a single request.` | ||
| ); | ||
| } | ||
| keys.push(key); | ||
| }); | ||
| } | ||
| wrap(validation) { | ||
@@ -119,2 +150,14 @@ if (!this.enabled) { | ||
| }; | ||
| /** | ||
| * Maps the key used in a store for a certain request, and ensures that the | ||
| * same key isn't used more than once per request. | ||
| * | ||
| * The store can be any one of the following: | ||
| * - An instance, for stores like the MemoryStore where two instances do not | ||
| * share state. | ||
| * - A string (class name), for stores where multiple instances | ||
| * typically share state, such as the Redis store. | ||
| */ | ||
| __publicField(_Validations, "singleCountKeys", /* @__PURE__ */ new WeakMap()); | ||
| var Validations = _Validations; | ||
@@ -145,2 +188,7 @@ // source/memory-store.ts | ||
| __publicField(this, "interval"); | ||
| /** | ||
| * Confirmation that the keys incremented in once instance of MemoryStore | ||
| * cannot affect other instances. | ||
| */ | ||
| __publicField(this, "localKeys", true); | ||
| } | ||
@@ -347,2 +395,3 @@ /** | ||
| const { totalHits, resetTime } = await config.store.increment(key); | ||
| config.validations.singleCount(request, config.store, key); | ||
| const retrieveQuota = typeof config.max === "function" ? config.max(request, response) : config.max; | ||
@@ -349,0 +398,0 @@ const maxHits = await retrieveQuota; |
+5
-5
| { | ||
| "name": "express-rate-limit", | ||
| "version": "6.8.1", | ||
| "version": "6.9.0", | ||
| "description": "Basic IP rate-limiting middleware for Express. Use to limit repeated requests to public APIs and/or endpoints such as password reset.", | ||
@@ -66,8 +66,8 @@ "author": { | ||
| "lint": "run-s lint:*", | ||
| "autofix:code": "npm run lint:code -- --fix", | ||
| "autofix:rest": "npm run lint:rest -- --write .", | ||
| "autofix": "run-s autofix:*", | ||
| "format:code": "npm run lint:code -- --fix", | ||
| "format:rest": "npm run lint:rest -- --write .", | ||
| "format": "run-s format:*", | ||
| "test:lib": "cross-env NODE_NO_WARNINGS=1 NODE_OPTIONS=--experimental-vm-modules jest", | ||
| "test:ext": "cd test/external/ && bash run-all-tests", | ||
| "test": "run-s lint test:*", | ||
| "test": "run-s lint test:lib", | ||
| "pre-commit": "lint-staged", | ||
@@ -74,0 +74,0 @@ "prepare": "run-s compile && husky install config/husky" |
+44
-11
@@ -19,4 +19,5 @@ # <div align="center"> Express Rate Limit </div> | ||
| Basic rate-limiting middleware for Express. Use to limit repeated requests to | ||
| public APIs and/or endpoints such as password reset. Plays nice with | ||
| Basic rate-limiting middleware for [Express](http://expressjs.com/). Use to | ||
| limit repeated requests to public APIs and/or endpoints such as password reset. | ||
| Plays nice with | ||
| [express-slow-down](https://www.npmjs.com/package/express-slow-down). | ||
@@ -26,8 +27,32 @@ | ||
| ## Use Cases | ||
| Depending on your use case, you may need to switch to a different | ||
| [store](#store). | ||
| #### Abuse Prevention | ||
| The default `MemoryStore` is probably fine. | ||
| #### API Rate Limit Enforcement | ||
| You likely want to switch to a different [store](#store). As a performance | ||
| optimization, the default `MemoryStore` uses a global time window, so if your | ||
| limit is 10 requests per minute, a single user might be able to get an initial | ||
| burst of up to 20 requests in a row if they happen to get the first 10 in at the | ||
| end of one minute and the next 10 in at the start of the next minute. (After the | ||
| initial burst, they will be limited to the expected 10 requests per minute.) All | ||
| other stores use per-user time windows, so a user will get exactly 10 requests | ||
| regardless. | ||
| Additionally, if you have multiple servers or processes (for example, with the | ||
| [node:cluster](https://nodejs.org/api/cluster.html) module), you'll likely want | ||
| to use an external data store to syhcnronize hits | ||
| ([redis](https://npmjs.com/package/rate-limit-redis), | ||
| [memcached](https://npmjs.org/package/rate-limit-memcached), [etc.](#store)) | ||
| This will guarentee the expected result even if some requests get handled by | ||
| different servers/processes. | ||
| ### Alternate Rate Limiters | ||
| > This module does not share state with other processes/servers by default. If | ||
| > you need a more robust solution, I recommend using an external store. See the | ||
| > [`stores` section](#store) below for a list of external stores. | ||
| This module was designed to only handle the basics and didn't even support | ||
@@ -99,2 +124,3 @@ external stores initially. These other options all are excellent pieces of | ||
| legacyHeaders: false, // Disable the `X-RateLimit-*` headers | ||
| // store: ... , // Use an external store for more precise rate limiting | ||
| }) | ||
@@ -118,2 +144,3 @@ | ||
| legacyHeaders: false, // Disable the `X-RateLimit-*` headers | ||
| // store: ... , // Use an external store for more precise rate limiting | ||
| }) | ||
@@ -135,2 +162,3 @@ | ||
| legacyHeaders: false, // Disable the `X-RateLimit-*` headers | ||
| // store: ... , // Use an external store for more precise rate limiting | ||
| }) | ||
@@ -157,13 +185,18 @@ | ||
| ```ts | ||
| import rateLimit, { MemoryStore } from 'express-rate-limit' | ||
| import rateLimit from 'express-rate-limit' | ||
| import RedisStore from 'rate-limit-redis' | ||
| import RedisClient from 'ioredis' | ||
| const apiLimiter = rateLimit({ | ||
| const redisClient = new RedisClient() | ||
| const rateLimiter = rateLimit({ | ||
| windowMs: 15 * 60 * 1000, // 15 minutes | ||
| max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes) | ||
| standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers | ||
| store: new MemoryStore(), | ||
| store: new RedisStore({ | ||
| /* ... */ | ||
| }), // Use the external store | ||
| }) | ||
| // Apply the rate limiting middleware to API calls only | ||
| app.use('/api', apiLimiter) | ||
| // Apply the rate limiting middleware to all requests | ||
| app.use(rateLimiter) | ||
| ``` | ||
@@ -170,0 +203,0 @@ |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
98282
6.9%1361
8.88%557
6.3%