added rate limiting
parent
87f476f29b
commit
3b2a262a72
11
index.js
11
index.js
|
@ -5,10 +5,21 @@ const JSDOM = require('jsdom').JSDOM;
|
||||||
|
|
||||||
service = new turndown();
|
service = new turndown();
|
||||||
|
|
||||||
|
const rateLimit = require('express-rate-limit');
|
||||||
|
|
||||||
|
const rateLimiter = rateLimit({
|
||||||
|
windowMs: 30 * 1000,
|
||||||
|
max: 5,
|
||||||
|
message: 'Rate limit exceeded',
|
||||||
|
headers: true
|
||||||
|
});
|
||||||
|
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
const app = express()
|
const app = express()
|
||||||
const port = process.env.PORT
|
const port = process.env.PORT
|
||||||
|
|
||||||
|
app.use(rateLimiter)
|
||||||
|
|
||||||
app.get('/', (req, res) => {
|
app.get('/', (req, res) => {
|
||||||
url = req.query.url;
|
url = req.query.url;
|
||||||
res.header("Access-Control-Allow-Origin", '*');
|
res.header("Access-Control-Allow-Origin", '*');
|
||||||
|
|
|
@ -406,6 +406,17 @@
|
||||||
"node": ">= 0.10.0"
|
"node": ">= 0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/express-rate-limit": {
|
||||||
|
"version": "6.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.0.5.tgz",
|
||||||
|
"integrity": "sha512-EB1mRTrzyyPfEsQZIQFXocd8NKZoDZbEwrtbdgkc20Yed6oYg02Xfjza2HHPI/0orp54BrFeHeT92ICB9ydokw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.5.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"express": "^4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-levenshtein": {
|
"node_modules/fast-levenshtein": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to
|
||||||
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [6.0.5](https://github.com/nfriedly/express-rate-limit/releases/tag/v6.0.5)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Use named imports for ExpressJS types so users do not need to enable the
|
||||||
|
`esModuleInterop` flag in their Typescript compiler configuration.
|
||||||
|
|
||||||
|
## [6.0.4](https://github.com/nfriedly/express-rate-limit/releases/tag/v6.0.4)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Upload the built package as a `.tgz` to GitHub releases.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Add ` main` and `module` fields to `package.json`. This helps tools such as
|
||||||
|
ESLint that do not yet support the `exports` field.
|
||||||
|
- Bumped the minimum node.js version in `package-lock.json` to match
|
||||||
|
`package.json`
|
||||||
|
|
||||||
|
## [6.0.3](https://github.com/nfriedly/express-rate-limit/releases/tag/v6.0.3)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Bumped minimum Node version from 12.9 to 14.5 in `package.json` because the
|
||||||
|
transpiled output uses the nullish coalescing operator (`??`), which
|
||||||
|
[isn't supported in node.js prior to 14.x](https://node.green/#ES2020-features--nullish-coalescing-operator-----).
|
||||||
|
|
||||||
|
## [6.0.2](https://github.com/nfriedly/express-rate-limit/releases/v6.0.2)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Ensure CommonJS projects can import the module.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add additional tests that test:
|
||||||
|
- importing the library in `js-cjs`, `js-esm`, `ts-cjs`, `ts-esm`
|
||||||
|
environments.
|
||||||
|
- usage of the library with external stores (`redis`, `mongo`, `memcached`,
|
||||||
|
`precise`).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Use [`esbuild`](https://esbuild.github.io/) to generate ESM and CJS output.
|
||||||
|
This reduces the size of the built package from 138 kb to 13kb and build time
|
||||||
|
to 4 ms! :rocket:
|
||||||
|
- Use [`dts-bundle-generator`](https://github.com/timocov/dts-bundle-generator)
|
||||||
|
to generate a single Typescript declaration file.
|
||||||
|
|
||||||
|
## [6.0.1](https://github.com/nfriedly/express-rate-limit/releases/v6.0.1)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Ensure CommonJS projects can import the module.
|
||||||
|
|
||||||
|
## [6.0.0](https://github.com/nfriedly/express-rate-limit/releases/v6.0.0)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `express` 4.x as a peer dependency.
|
||||||
|
- Better Typescript support (the library was rewritten in Typescript).
|
||||||
|
- Export the package as both ESM and CJS.
|
||||||
|
- Publish the built package (`.tgz` file) on GitHub releases as well as the npm
|
||||||
|
registry.
|
||||||
|
- Issue and PR templates.
|
||||||
|
- A contributing guide.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Rename the `draft_polli_ratelimit_headers` option to `standardHeaders`.
|
||||||
|
- Rename the `headers` option to `legacyHeaders`.
|
||||||
|
- `Retry-After` header is now sent if either `legacyHeaders` or
|
||||||
|
`standardHeaders` is set.
|
||||||
|
- Allow `keyGenerator` to be an async function/return a promise.
|
||||||
|
- Change the way custom stores are defined.
|
||||||
|
- Add the `init` method for stores to set themselves up using options passed
|
||||||
|
to the middleware.
|
||||||
|
- Rename the `incr` method to `increment`.
|
||||||
|
- Allow the `increment`, `decrement`, `resetKey` and `resetAll` methods to
|
||||||
|
return a promise.
|
||||||
|
- Old stores will automatically be promisified and used.
|
||||||
|
- The package can now only be used with NodeJS version 12.9.0 or greater.
|
||||||
|
- The `onLimitReached` configuration option is now deprecated. Replace it with a
|
||||||
|
custom `handler` that checks the number of hits.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Remove the deprecated `limiter.resetIp` method (use the `limiter.resetKey`
|
||||||
|
method instead).
|
||||||
|
- Remove the deprecated options `delayMs`, `delayAfter` (the delay functionality
|
||||||
|
was moved to the
|
||||||
|
[`express-slow-down`](https://github.com/nfriedly/express-slow-down) package)
|
||||||
|
and `global` (use a key generator that returns a constant value).
|
||||||
|
|
||||||
|
## [5.x](https://github.com/nfriedly/express-rate-limit/releases/tag/v5.5.1)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- The middleware ~throws~ logs an error if `request.ip` is undefined.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Removes typescript typings. (See
|
||||||
|
[#138](https://github.com/nfriedly/express-rate-limit/issues/138))
|
||||||
|
|
||||||
|
## [4.x](https://github.com/nfriedly/express-rate-limit/releases/tag/v4.0.4)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The library no longer modifies the passed-in options object, it instead makes
|
||||||
|
a clone of it.
|
||||||
|
|
||||||
|
## [3.x](https://github.com/nfriedly/express-rate-limit/releases/tag/v3.5.2)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Simplifies the default `handler` function so that it no longer changes the
|
||||||
|
response format. The default handler also uses
|
||||||
|
[response.send](https://expressjs.com/en/4x/api.html#response.send).
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- `onLimitReached` now only triggers once for a client and window. However, the
|
||||||
|
`handle` method is called for every blocked request.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- The `delayAfter` and `delayMs` options; they were moved to the
|
||||||
|
[express-slow-down](https://npmjs.org/package/express-slow-down) package.
|
||||||
|
|
||||||
|
## [2.x](https://github.com/nfriedly/express-rate-limit/releases/tag/v2.14.2)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- A `limiter.resetKey()` method to reset the hit counter for a particular client
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- The rate limiter now uses a less precise but less resource intensive method of
|
||||||
|
tracking hits from a client.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- The `global` option.
|
|
@ -0,0 +1,226 @@
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __markAsModule = (target) => __defProp(target, "__esModule", { value: true });
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __reExport = (target, module2, copyDefault, desc) => {
|
||||||
|
if (module2 && typeof module2 === "object" || typeof module2 === "function") {
|
||||||
|
for (let key of __getOwnPropNames(module2))
|
||||||
|
if (!__hasOwnProp.call(target, key) && (copyDefault || key !== "default"))
|
||||||
|
__defProp(target, key, { get: () => module2[key], enumerable: !(desc = __getOwnPropDesc(module2, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
};
|
||||||
|
var __toCommonJS = /* @__PURE__ */ ((cache) => {
|
||||||
|
return (module2, temp) => {
|
||||||
|
return cache && cache.get(module2) || (temp = __reExport(__markAsModule({}), module2, 1), cache && cache.set(module2, temp), temp);
|
||||||
|
};
|
||||||
|
})(typeof WeakMap !== "undefined" ? /* @__PURE__ */ new WeakMap() : 0);
|
||||||
|
|
||||||
|
// source/index.ts
|
||||||
|
var source_exports = {};
|
||||||
|
__export(source_exports, {
|
||||||
|
default: () => source_default
|
||||||
|
});
|
||||||
|
|
||||||
|
// source/memory-store.ts
|
||||||
|
var calculateNextResetTime = (windowMs) => {
|
||||||
|
const resetTime = new Date();
|
||||||
|
resetTime.setMilliseconds(resetTime.getMilliseconds() + windowMs);
|
||||||
|
return resetTime;
|
||||||
|
};
|
||||||
|
var MemoryStore = class {
|
||||||
|
init(options) {
|
||||||
|
this.windowMs = options.windowMs;
|
||||||
|
this.resetTime = calculateNextResetTime(this.windowMs);
|
||||||
|
this.hits = {};
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
await this.resetAll();
|
||||||
|
}, this.windowMs);
|
||||||
|
if (interval.unref) {
|
||||||
|
interval.unref();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async increment(key) {
|
||||||
|
const totalHits = (this.hits[key] ?? 0) + 1;
|
||||||
|
this.hits[key] = totalHits;
|
||||||
|
return {
|
||||||
|
totalHits,
|
||||||
|
resetTime: this.resetTime
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async decrement(key) {
|
||||||
|
const current = this.hits[key];
|
||||||
|
if (current) {
|
||||||
|
this.hits[key] = current - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async resetKey(key) {
|
||||||
|
delete this.hits[key];
|
||||||
|
}
|
||||||
|
async resetAll() {
|
||||||
|
this.hits = {};
|
||||||
|
this.resetTime = calculateNextResetTime(this.windowMs);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// source/lib.ts
|
||||||
|
var isLegacyStore = (store) => typeof store.incr === "function" && typeof store.increment !== "function";
|
||||||
|
var promisifyStore = (passedStore) => {
|
||||||
|
if (!isLegacyStore(passedStore)) {
|
||||||
|
return passedStore;
|
||||||
|
}
|
||||||
|
const legacyStore = passedStore;
|
||||||
|
class PromisifiedStore {
|
||||||
|
async increment(key) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
legacyStore.incr(key, (error, totalHits, resetTime) => {
|
||||||
|
if (error)
|
||||||
|
reject(error);
|
||||||
|
resolve({ totalHits, resetTime });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async decrement(key) {
|
||||||
|
return Promise.resolve(legacyStore.decrement(key));
|
||||||
|
}
|
||||||
|
async resetKey(key) {
|
||||||
|
return Promise.resolve(legacyStore.resetKey(key));
|
||||||
|
}
|
||||||
|
async resetAll() {
|
||||||
|
if (typeof legacyStore.resetAll === "function")
|
||||||
|
return Promise.resolve(legacyStore.resetAll());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new PromisifiedStore();
|
||||||
|
};
|
||||||
|
var parseOptions = (passedOptions) => {
|
||||||
|
const options = {
|
||||||
|
windowMs: 60 * 1e3,
|
||||||
|
store: new MemoryStore(),
|
||||||
|
max: 5,
|
||||||
|
message: "Too many requests, please try again later.",
|
||||||
|
statusCode: 429,
|
||||||
|
legacyHeaders: passedOptions.headers ?? true,
|
||||||
|
standardHeaders: passedOptions.draft_polli_ratelimit_headers ?? false,
|
||||||
|
requestPropertyName: "rateLimit",
|
||||||
|
skipFailedRequests: false,
|
||||||
|
skipSuccessfulRequests: false,
|
||||||
|
requestWasSuccessful: (_request, response) => response.statusCode < 400,
|
||||||
|
skip: (_request, _response) => false,
|
||||||
|
keyGenerator: (request, _response) => {
|
||||||
|
if (!request.ip) {
|
||||||
|
console.error("WARN | `express-rate-limit` | `request.ip` is undefined. You can avoid this by providing a custom `keyGenerator` function, but it may be indicative of a larger issue.");
|
||||||
|
}
|
||||||
|
return request.ip;
|
||||||
|
},
|
||||||
|
handler: (_request, response, _next, _optionsUsed) => {
|
||||||
|
response.status(options.statusCode).send(options.message);
|
||||||
|
},
|
||||||
|
onLimitReached: (_request, _response, _optionsUsed) => {
|
||||||
|
},
|
||||||
|
...passedOptions
|
||||||
|
};
|
||||||
|
if (typeof options.store.incr !== "function" && typeof options.store.increment !== "function" || typeof options.store.decrement !== "function" || typeof options.store.resetKey !== "function" || typeof options.store.resetAll !== "undefined" && typeof options.store.resetAll !== "function" || typeof options.store.init !== "undefined" && typeof options.store.init !== "function") {
|
||||||
|
throw new TypeError("An invalid store was passed. Please ensure that the store is a class that implements the `Store` interface.");
|
||||||
|
}
|
||||||
|
options.store = promisifyStore(options.store);
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
var handleAsyncErrors = (fn) => async (request, response, next) => {
|
||||||
|
try {
|
||||||
|
await Promise.resolve(fn(request, response, next)).catch(next);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var rateLimit = (passedOptions) => {
|
||||||
|
const options = parseOptions(passedOptions ?? {});
|
||||||
|
if (typeof options.store.init === "function")
|
||||||
|
options.store.init(options);
|
||||||
|
const middleware = handleAsyncErrors(async (request, response, next) => {
|
||||||
|
const skip = await options.skip(request, response);
|
||||||
|
if (skip) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const augmentedRequest = request;
|
||||||
|
const key = await options.keyGenerator(request, response);
|
||||||
|
const { totalHits, resetTime } = await options.store.increment(key);
|
||||||
|
const retrieveQuota = typeof options.max === "function" ? options.max(request, response) : options.max;
|
||||||
|
const maxHits = await retrieveQuota;
|
||||||
|
augmentedRequest[options.requestPropertyName] = {
|
||||||
|
limit: maxHits,
|
||||||
|
current: totalHits,
|
||||||
|
remaining: Math.max(maxHits - totalHits, 0),
|
||||||
|
resetTime
|
||||||
|
};
|
||||||
|
if (options.legacyHeaders && !response.headersSent) {
|
||||||
|
response.setHeader("X-RateLimit-Limit", maxHits);
|
||||||
|
response.setHeader("X-RateLimit-Remaining", augmentedRequest[options.requestPropertyName].remaining);
|
||||||
|
if (resetTime instanceof Date) {
|
||||||
|
response.setHeader("Date", new Date().toUTCString());
|
||||||
|
response.setHeader("X-RateLimit-Reset", Math.ceil(resetTime.getTime() / 1e3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (options.standardHeaders && !response.headersSent) {
|
||||||
|
response.setHeader("RateLimit-Limit", maxHits);
|
||||||
|
response.setHeader("RateLimit-Remaining", augmentedRequest[options.requestPropertyName].remaining);
|
||||||
|
if (resetTime) {
|
||||||
|
const deltaSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1e3);
|
||||||
|
response.setHeader("RateLimit-Reset", Math.max(0, deltaSeconds));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (options.skipFailedRequests || options.skipSuccessfulRequests) {
|
||||||
|
let decremented = false;
|
||||||
|
const decrementKey = async () => {
|
||||||
|
if (!decremented) {
|
||||||
|
await options.store.decrement(key);
|
||||||
|
decremented = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (options.skipFailedRequests) {
|
||||||
|
response.on("finish", async () => {
|
||||||
|
if (!options.requestWasSuccessful(request, response))
|
||||||
|
await decrementKey();
|
||||||
|
});
|
||||||
|
response.on("close", async () => {
|
||||||
|
if (!response.writableEnded)
|
||||||
|
await decrementKey();
|
||||||
|
});
|
||||||
|
response.on("error", async () => {
|
||||||
|
await decrementKey();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (options.skipSuccessfulRequests) {
|
||||||
|
response.on("finish", async () => {
|
||||||
|
if (options.requestWasSuccessful(request, response))
|
||||||
|
await decrementKey();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (maxHits && totalHits === maxHits + 1) {
|
||||||
|
options.onLimitReached(request, response, options);
|
||||||
|
}
|
||||||
|
if (maxHits && totalHits > maxHits) {
|
||||||
|
if ((options.legacyHeaders || options.standardHeaders) && !response.headersSent) {
|
||||||
|
response.setHeader("Retry-After", Math.ceil(options.windowMs / 1e3));
|
||||||
|
}
|
||||||
|
options.handler(request, response, next, options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
middleware.resetKey = options.store.resetKey.bind(options.store);
|
||||||
|
return middleware;
|
||||||
|
};
|
||||||
|
var lib_default = rateLimit;
|
||||||
|
|
||||||
|
// source/index.ts
|
||||||
|
var source_default = lib_default;
|
||||||
|
module.exports = __toCommonJS(source_exports);
|
||||||
|
module.exports = rateLimit;
|
|
@ -0,0 +1,248 @@
|
||||||
|
// Generated by dts-bundle-generator v6.3.0
|
||||||
|
|
||||||
|
import { NextFunction, Request, RequestHandler, Response } from 'express';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback that fires when a client's hit counter is incremented.
|
||||||
|
*
|
||||||
|
* @param error {Error | undefined} - The error that occurred, if any
|
||||||
|
* @param totalHits {number} - The number of hits for that client so far
|
||||||
|
* @param resetTime {Date | undefined} - The time when the counter resets
|
||||||
|
*/
|
||||||
|
export declare type IncrementCallback = (error: Error | undefined, totalHits: number, resetTime: Date | undefined) => void;
|
||||||
|
/**
|
||||||
|
* Method (in the form of middleware) to generate/retrieve a value based on the
|
||||||
|
* incoming request
|
||||||
|
*
|
||||||
|
* @param request {Request} - The Express request object
|
||||||
|
* @param response {Response} - The Express response object
|
||||||
|
*
|
||||||
|
* @returns {T} - The value needed
|
||||||
|
*/
|
||||||
|
export declare type ValueDeterminingMiddleware<T> = (request: Request, response: Response) => T | Promise<T>;
|
||||||
|
/**
|
||||||
|
* Express request handler that sends back a response when a client is
|
||||||
|
* rate-limited.
|
||||||
|
*
|
||||||
|
* @param request {Request} - The Express request object
|
||||||
|
* @param response {Response} - The Express response object
|
||||||
|
* @param next {NextFunction} - The Express `next` function, can be called to skip responding
|
||||||
|
* @param optionsUsed {Options} - The options used to set up the middleware
|
||||||
|
*/
|
||||||
|
export declare type RateLimitExceededEventHandler = (request: Request, response: Response, next: NextFunction, optionsUsed: Options) => void;
|
||||||
|
/**
|
||||||
|
* Event callback that is triggered on a client's first request that exceeds the limit
|
||||||
|
* but not for subsequent requests. May be used for logging, etc. Should *not*
|
||||||
|
* send a response.
|
||||||
|
*
|
||||||
|
* @param request {Request} - The Express request object
|
||||||
|
* @param response {Response} - The Express response object
|
||||||
|
* @param optionsUsed {Options} - The options used to set up the middleware
|
||||||
|
*/
|
||||||
|
export declare type RateLimitReachedEventHandler = (request: Request, response: Response, optionsUsed: Options) => void;
|
||||||
|
/**
|
||||||
|
* Data returned from the `Store` when a client's hit counter is incremented.
|
||||||
|
*
|
||||||
|
* @property totalHits {number} - The number of hits for that client so far
|
||||||
|
* @property resetTime {Date | undefined} - The time when the counter resets
|
||||||
|
*/
|
||||||
|
export declare type IncrementResponse = {
|
||||||
|
totalHits: number;
|
||||||
|
resetTime: Date | undefined;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* A modified Express request handler with the rate limit functions.
|
||||||
|
*/
|
||||||
|
export declare type RateLimitRequestHandler = RequestHandler & {
|
||||||
|
/**
|
||||||
|
* Method to reset a client's hit counter.
|
||||||
|
*
|
||||||
|
* @param key {string} - The identifier for a client
|
||||||
|
*/
|
||||||
|
resetKey: (key: string) => void;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* An interface that all hit counter stores must implement.
|
||||||
|
*
|
||||||
|
* @deprecated 6.x - Implement the `Store` interface instead.
|
||||||
|
*/
|
||||||
|
export interface LegacyStore {
|
||||||
|
/**
|
||||||
|
* Method to increment a client's hit counter.
|
||||||
|
*
|
||||||
|
* @param key {string} - The identifier for a client
|
||||||
|
* @param callback {IncrementCallback} - The callback to call once the counter is incremented
|
||||||
|
*/
|
||||||
|
incr: (key: string, callback: IncrementCallback) => void;
|
||||||
|
/**
|
||||||
|
* Method to decrement a client's hit counter.
|
||||||
|
*
|
||||||
|
* @param key {string} - The identifier for a client
|
||||||
|
*/
|
||||||
|
decrement: (key: string) => void;
|
||||||
|
/**
|
||||||
|
* Method to reset a client's hit counter.
|
||||||
|
*
|
||||||
|
* @param key {string} - The identifier for a client
|
||||||
|
*/
|
||||||
|
resetKey: (key: string) => void;
|
||||||
|
/**
|
||||||
|
* Method to reset everyone's hit counter.
|
||||||
|
*/
|
||||||
|
resetAll?: () => void;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* An interface that all hit counter stores must implement.
|
||||||
|
*/
|
||||||
|
export interface Store {
|
||||||
|
/**
|
||||||
|
* Method that initializes the store, and has access to the options passed to
|
||||||
|
* the middleware too.
|
||||||
|
*
|
||||||
|
* @param options {Options} - The options used to setup the middleware
|
||||||
|
*/
|
||||||
|
init?: (options: Options) => void;
|
||||||
|
/**
|
||||||
|
* Method to increment a client's hit counter.
|
||||||
|
*
|
||||||
|
* @param key {string} - The identifier for a client
|
||||||
|
*
|
||||||
|
* @returns {IncrementResponse} - The number of hits and reset time for that client
|
||||||
|
*/
|
||||||
|
increment: (key: string) => Promise<IncrementResponse> | IncrementResponse;
|
||||||
|
/**
|
||||||
|
* Method to decrement a client's hit counter.
|
||||||
|
*
|
||||||
|
* @param key {string} - The identifier for a client
|
||||||
|
*/
|
||||||
|
decrement: (key: string) => Promise<void> | void;
|
||||||
|
/**
|
||||||
|
* Method to reset a client's hit counter.
|
||||||
|
*
|
||||||
|
* @param key {string} - The identifier for a client
|
||||||
|
*/
|
||||||
|
resetKey: (key: string) => Promise<void> | void;
|
||||||
|
/**
|
||||||
|
* Method to reset everyone's hit counter.
|
||||||
|
*/
|
||||||
|
resetAll?: () => Promise<void> | void;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* The configuration options for the rate limiter.
|
||||||
|
*/
|
||||||
|
export interface Options {
|
||||||
|
/**
|
||||||
|
* How long we should remember the requests.
|
||||||
|
*/
|
||||||
|
readonly windowMs: number;
|
||||||
|
/**
|
||||||
|
* The maximum number of connection to allow during the `window` before
|
||||||
|
* rate limiting the client.
|
||||||
|
*
|
||||||
|
* Can be the limit itself as a number or express middleware that parses
|
||||||
|
* the request and then figures out the limit.
|
||||||
|
*/
|
||||||
|
readonly max: number | ValueDeterminingMiddleware<number>;
|
||||||
|
/**
|
||||||
|
* The response body to send back when a client is rate limited.
|
||||||
|
*/
|
||||||
|
readonly message: any;
|
||||||
|
/**
|
||||||
|
* The HTTP status code to send back when a client is rate limited.
|
||||||
|
*
|
||||||
|
* Defaults to `HTTP 429 Too Many Requests` (RFC 6585).
|
||||||
|
*/
|
||||||
|
readonly statusCode: number;
|
||||||
|
/**
|
||||||
|
* Whether to send `X-RateLimit-*` headers with the rate limit and the number
|
||||||
|
* of requests.
|
||||||
|
*/
|
||||||
|
readonly legacyHeaders: boolean;
|
||||||
|
/**
|
||||||
|
* Whether to enable support for the rate limit standardization headers (`RateLimit-*`).
|
||||||
|
*/
|
||||||
|
readonly standardHeaders: boolean;
|
||||||
|
/**
|
||||||
|
* The name of the property on the request object to store the rate limit info.
|
||||||
|
*
|
||||||
|
* Defaults to `rateLimit`.
|
||||||
|
*/
|
||||||
|
readonly requestPropertyName: string;
|
||||||
|
/**
|
||||||
|
* If `true`, the library will (by default) skip all requests that have a 4XX
|
||||||
|
* or 5XX status.
|
||||||
|
*/
|
||||||
|
readonly skipFailedRequests: boolean;
|
||||||
|
/**
|
||||||
|
* If `true`, the library will (by default) skip all requests that have a
|
||||||
|
* status code less than 400.
|
||||||
|
*/
|
||||||
|
readonly skipSuccessfulRequests: boolean;
|
||||||
|
/**
|
||||||
|
* Method to determine whether or not the request counts as 'succesful'. Used
|
||||||
|
* when either `skipSuccessfulRequests` or `skipFailedRequests` is set to true.
|
||||||
|
*/
|
||||||
|
readonly requestWasSuccessful: ValueDeterminingMiddleware<boolean>;
|
||||||
|
/**
|
||||||
|
* Method to generate custom identifiers for clients.
|
||||||
|
*
|
||||||
|
* By default, the client's IP address is used.
|
||||||
|
*/
|
||||||
|
readonly keyGenerator: ValueDeterminingMiddleware<string>;
|
||||||
|
/**
|
||||||
|
* Method (in the form of middleware) to determine whether or not this request
|
||||||
|
* counts towards a client's quota.
|
||||||
|
*/
|
||||||
|
readonly skip: ValueDeterminingMiddleware<boolean>;
|
||||||
|
/**
|
||||||
|
* Express request handler that sends back a response when a client is
|
||||||
|
* rate-limited.
|
||||||
|
*/
|
||||||
|
readonly handler: RateLimitExceededEventHandler;
|
||||||
|
/**
|
||||||
|
* Express request handler that sends back a response when a client has
|
||||||
|
* reached their rate limit, and will be rate limited on their next request.
|
||||||
|
*/
|
||||||
|
readonly onLimitReached: RateLimitReachedEventHandler;
|
||||||
|
/**
|
||||||
|
* The {@link Store} to use to store the hit count for each client.
|
||||||
|
*/
|
||||||
|
store: Store;
|
||||||
|
/**
|
||||||
|
* Whether to send `X-RateLimit-*` headers with the rate limit and the number
|
||||||
|
* of requests.
|
||||||
|
*
|
||||||
|
* @deprecated 6.x - This option was renamed to `legacyHeaders`.
|
||||||
|
*/
|
||||||
|
headers?: boolean;
|
||||||
|
/**
|
||||||
|
* Whether to send `RateLimit-*` headers with the rate limit and the number
|
||||||
|
* of requests.
|
||||||
|
*
|
||||||
|
* @deprecated 6.x - This option was renamed to `standardHeaders`.
|
||||||
|
*/
|
||||||
|
draft_polli_ratelimit_headers?: boolean;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* The extended request object that includes information about the client's
|
||||||
|
* rate limit.
|
||||||
|
*/
|
||||||
|
export declare type AugmentedRequest = Request & {
|
||||||
|
[key: string]: RateLimitInfo;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* The rate limit related information for each client included in the
|
||||||
|
* Express request object.
|
||||||
|
*/
|
||||||
|
export interface RateLimitInfo {
|
||||||
|
readonly limit: number;
|
||||||
|
readonly current: number;
|
||||||
|
readonly remaining: number;
|
||||||
|
readonly resetTime: Date | undefined;
|
||||||
|
}
|
||||||
|
declare const rateLimit: (passedOptions?: (Omit<Partial<Options>, "store"> & {
|
||||||
|
store?: LegacyStore | Store | undefined;
|
||||||
|
}) | undefined) => RateLimitRequestHandler;
|
||||||
|
export default rateLimit;
|
||||||
|
|
||||||
|
export {};
|
|
@ -0,0 +1,198 @@
|
||||||
|
// source/memory-store.ts
|
||||||
|
var calculateNextResetTime = (windowMs) => {
|
||||||
|
const resetTime = new Date();
|
||||||
|
resetTime.setMilliseconds(resetTime.getMilliseconds() + windowMs);
|
||||||
|
return resetTime;
|
||||||
|
};
|
||||||
|
var MemoryStore = class {
|
||||||
|
init(options) {
|
||||||
|
this.windowMs = options.windowMs;
|
||||||
|
this.resetTime = calculateNextResetTime(this.windowMs);
|
||||||
|
this.hits = {};
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
await this.resetAll();
|
||||||
|
}, this.windowMs);
|
||||||
|
if (interval.unref) {
|
||||||
|
interval.unref();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async increment(key) {
|
||||||
|
const totalHits = (this.hits[key] ?? 0) + 1;
|
||||||
|
this.hits[key] = totalHits;
|
||||||
|
return {
|
||||||
|
totalHits,
|
||||||
|
resetTime: this.resetTime
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async decrement(key) {
|
||||||
|
const current = this.hits[key];
|
||||||
|
if (current) {
|
||||||
|
this.hits[key] = current - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async resetKey(key) {
|
||||||
|
delete this.hits[key];
|
||||||
|
}
|
||||||
|
async resetAll() {
|
||||||
|
this.hits = {};
|
||||||
|
this.resetTime = calculateNextResetTime(this.windowMs);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// source/lib.ts
|
||||||
|
var isLegacyStore = (store) => typeof store.incr === "function" && typeof store.increment !== "function";
|
||||||
|
var promisifyStore = (passedStore) => {
|
||||||
|
if (!isLegacyStore(passedStore)) {
|
||||||
|
return passedStore;
|
||||||
|
}
|
||||||
|
const legacyStore = passedStore;
|
||||||
|
class PromisifiedStore {
|
||||||
|
async increment(key) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
legacyStore.incr(key, (error, totalHits, resetTime) => {
|
||||||
|
if (error)
|
||||||
|
reject(error);
|
||||||
|
resolve({ totalHits, resetTime });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async decrement(key) {
|
||||||
|
return Promise.resolve(legacyStore.decrement(key));
|
||||||
|
}
|
||||||
|
async resetKey(key) {
|
||||||
|
return Promise.resolve(legacyStore.resetKey(key));
|
||||||
|
}
|
||||||
|
async resetAll() {
|
||||||
|
if (typeof legacyStore.resetAll === "function")
|
||||||
|
return Promise.resolve(legacyStore.resetAll());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new PromisifiedStore();
|
||||||
|
};
|
||||||
|
var parseOptions = (passedOptions) => {
|
||||||
|
const options = {
|
||||||
|
windowMs: 60 * 1e3,
|
||||||
|
store: new MemoryStore(),
|
||||||
|
max: 5,
|
||||||
|
message: "Too many requests, please try again later.",
|
||||||
|
statusCode: 429,
|
||||||
|
legacyHeaders: passedOptions.headers ?? true,
|
||||||
|
standardHeaders: passedOptions.draft_polli_ratelimit_headers ?? false,
|
||||||
|
requestPropertyName: "rateLimit",
|
||||||
|
skipFailedRequests: false,
|
||||||
|
skipSuccessfulRequests: false,
|
||||||
|
requestWasSuccessful: (_request, response) => response.statusCode < 400,
|
||||||
|
skip: (_request, _response) => false,
|
||||||
|
keyGenerator: (request, _response) => {
|
||||||
|
if (!request.ip) {
|
||||||
|
console.error("WARN | `express-rate-limit` | `request.ip` is undefined. You can avoid this by providing a custom `keyGenerator` function, but it may be indicative of a larger issue.");
|
||||||
|
}
|
||||||
|
return request.ip;
|
||||||
|
},
|
||||||
|
handler: (_request, response, _next, _optionsUsed) => {
|
||||||
|
response.status(options.statusCode).send(options.message);
|
||||||
|
},
|
||||||
|
onLimitReached: (_request, _response, _optionsUsed) => {
|
||||||
|
},
|
||||||
|
...passedOptions
|
||||||
|
};
|
||||||
|
if (typeof options.store.incr !== "function" && typeof options.store.increment !== "function" || typeof options.store.decrement !== "function" || typeof options.store.resetKey !== "function" || typeof options.store.resetAll !== "undefined" && typeof options.store.resetAll !== "function" || typeof options.store.init !== "undefined" && typeof options.store.init !== "function") {
|
||||||
|
throw new TypeError("An invalid store was passed. Please ensure that the store is a class that implements the `Store` interface.");
|
||||||
|
}
|
||||||
|
options.store = promisifyStore(options.store);
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
var handleAsyncErrors = (fn) => async (request, response, next) => {
|
||||||
|
try {
|
||||||
|
await Promise.resolve(fn(request, response, next)).catch(next);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var rateLimit = (passedOptions) => {
|
||||||
|
const options = parseOptions(passedOptions ?? {});
|
||||||
|
if (typeof options.store.init === "function")
|
||||||
|
options.store.init(options);
|
||||||
|
const middleware = handleAsyncErrors(async (request, response, next) => {
|
||||||
|
const skip = await options.skip(request, response);
|
||||||
|
if (skip) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const augmentedRequest = request;
|
||||||
|
const key = await options.keyGenerator(request, response);
|
||||||
|
const { totalHits, resetTime } = await options.store.increment(key);
|
||||||
|
const retrieveQuota = typeof options.max === "function" ? options.max(request, response) : options.max;
|
||||||
|
const maxHits = await retrieveQuota;
|
||||||
|
augmentedRequest[options.requestPropertyName] = {
|
||||||
|
limit: maxHits,
|
||||||
|
current: totalHits,
|
||||||
|
remaining: Math.max(maxHits - totalHits, 0),
|
||||||
|
resetTime
|
||||||
|
};
|
||||||
|
if (options.legacyHeaders && !response.headersSent) {
|
||||||
|
response.setHeader("X-RateLimit-Limit", maxHits);
|
||||||
|
response.setHeader("X-RateLimit-Remaining", augmentedRequest[options.requestPropertyName].remaining);
|
||||||
|
if (resetTime instanceof Date) {
|
||||||
|
response.setHeader("Date", new Date().toUTCString());
|
||||||
|
response.setHeader("X-RateLimit-Reset", Math.ceil(resetTime.getTime() / 1e3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (options.standardHeaders && !response.headersSent) {
|
||||||
|
response.setHeader("RateLimit-Limit", maxHits);
|
||||||
|
response.setHeader("RateLimit-Remaining", augmentedRequest[options.requestPropertyName].remaining);
|
||||||
|
if (resetTime) {
|
||||||
|
const deltaSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1e3);
|
||||||
|
response.setHeader("RateLimit-Reset", Math.max(0, deltaSeconds));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (options.skipFailedRequests || options.skipSuccessfulRequests) {
|
||||||
|
let decremented = false;
|
||||||
|
const decrementKey = async () => {
|
||||||
|
if (!decremented) {
|
||||||
|
await options.store.decrement(key);
|
||||||
|
decremented = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (options.skipFailedRequests) {
|
||||||
|
response.on("finish", async () => {
|
||||||
|
if (!options.requestWasSuccessful(request, response))
|
||||||
|
await decrementKey();
|
||||||
|
});
|
||||||
|
response.on("close", async () => {
|
||||||
|
if (!response.writableEnded)
|
||||||
|
await decrementKey();
|
||||||
|
});
|
||||||
|
response.on("error", async () => {
|
||||||
|
await decrementKey();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (options.skipSuccessfulRequests) {
|
||||||
|
response.on("finish", async () => {
|
||||||
|
if (options.requestWasSuccessful(request, response))
|
||||||
|
await decrementKey();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (maxHits && totalHits === maxHits + 1) {
|
||||||
|
options.onLimitReached(request, response, options);
|
||||||
|
}
|
||||||
|
if (maxHits && totalHits > maxHits) {
|
||||||
|
if ((options.legacyHeaders || options.standardHeaders) && !response.headersSent) {
|
||||||
|
response.setHeader("Retry-After", Math.ceil(options.windowMs / 1e3));
|
||||||
|
}
|
||||||
|
options.handler(request, response, next, options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
middleware.resetKey = options.store.resetKey.bind(options.store);
|
||||||
|
return middleware;
|
||||||
|
};
|
||||||
|
var lib_default = rateLimit;
|
||||||
|
|
||||||
|
// source/index.ts
|
||||||
|
var source_default = lib_default;
|
||||||
|
export {
|
||||||
|
source_default as default
|
||||||
|
};
|
|
@ -0,0 +1,20 @@
|
||||||
|
# MIT License
|
||||||
|
|
||||||
|
Copyright 2021 Nathan Friedly
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,144 @@
|
||||||
|
{
|
||||||
|
"name": "express-rate-limit",
|
||||||
|
"version": "6.0.5",
|
||||||
|
"description": "Basic IP rate-limiting middleware for Express. Use to limit repeated requests to public APIs and/or endpoints such as password reset.",
|
||||||
|
"author": {
|
||||||
|
"name": "Nathan Friedly",
|
||||||
|
"url": "http://nfriedly.com/"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"homepage": "https://github.com/nfriedly/express-rate-limit",
|
||||||
|
"repository": "https://github.com/nfriedly/express-rate-limit",
|
||||||
|
"keywords": [
|
||||||
|
"express-rate-limit",
|
||||||
|
"express",
|
||||||
|
"rate",
|
||||||
|
"limit",
|
||||||
|
"ratelimit",
|
||||||
|
"rate-limit",
|
||||||
|
"middleware",
|
||||||
|
"ip",
|
||||||
|
"auth",
|
||||||
|
"authorization",
|
||||||
|
"security",
|
||||||
|
"brute",
|
||||||
|
"force",
|
||||||
|
"bruteforce",
|
||||||
|
"brute-force",
|
||||||
|
"attack"
|
||||||
|
],
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/index.mjs",
|
||||||
|
"require": "./dist/index.cjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"main": "./dist/index.cjs",
|
||||||
|
"module": "./dist/index.mjs",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist/",
|
||||||
|
"tsconfig.json",
|
||||||
|
"package.json",
|
||||||
|
"readme.md",
|
||||||
|
"license.md",
|
||||||
|
"changelog.md"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.5.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"clean": "del-cli dist/ coverage/ *.log *.tmp *.bak *.tgz",
|
||||||
|
"build:cjs": "esbuild --bundle --format=cjs --outfile=dist/index.cjs --footer:js=\"module.exports = rateLimit;\" source/index.ts",
|
||||||
|
"build:esm": "esbuild --bundle --format=esm --outfile=dist/index.mjs source/index.ts",
|
||||||
|
"build:types": "dts-bundle-generator --out-file=dist/index.d.ts source/index.ts",
|
||||||
|
"compile": "run-s clean build:*",
|
||||||
|
"lint:code": "xo --ignore test/external/",
|
||||||
|
"lint:rest": "prettier --ignore-path .gitignore --ignore-unknown --check .",
|
||||||
|
"lint": "run-s lint:*",
|
||||||
|
"autofix:code": "xo --ignore test/external/ --fix",
|
||||||
|
"autofix:rest": "prettier --ignore-path .gitignore --ignore-unknown --write .",
|
||||||
|
"autofix": "run-s autofix:*",
|
||||||
|
"test:lib": "cross-env NODE_OPTIONS=--experimental-vm-modules jest",
|
||||||
|
"test:ext": "npm pack && cd test/external/ && bash run-all-tests",
|
||||||
|
"test": "run-s lint test:*",
|
||||||
|
"pre-commit": "lint-staged",
|
||||||
|
"prepare": "run-s compile && husky install config/husky"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"express": "^4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@jest/globals": "^27.4.6",
|
||||||
|
"@types/express": "^4.17.13",
|
||||||
|
"@types/jest": "^27.4.0",
|
||||||
|
"@types/node": "^16.11.17",
|
||||||
|
"@types/supertest": "^2.0.11",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"del-cli": "^4.0.1",
|
||||||
|
"dts-bundle-generator": "^6.3.0",
|
||||||
|
"esbuild": "^0.14.10",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"husky": "^7.0.4",
|
||||||
|
"jest": "^27.4.7",
|
||||||
|
"lint-staged": "^12.1.5",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"supertest": "^6.1.6",
|
||||||
|
"ts-jest": "^27.1.1",
|
||||||
|
"ts-node": "^10.4.0",
|
||||||
|
"typescript": "^4.5.2",
|
||||||
|
"xo": "^0.47.0"
|
||||||
|
},
|
||||||
|
"xo": {
|
||||||
|
"prettier": true,
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/no-empty-function": 0,
|
||||||
|
"@typescript-eslint/no-dynamic-delete": 0,
|
||||||
|
"@typescript-eslint/no-confusing-void-expression": 0,
|
||||||
|
"@typescript-eslint/consistent-indexed-object-style": [
|
||||||
|
"error",
|
||||||
|
"index-signature"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"semi": false,
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"proseWrap": "always"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"preset": "ts-jest/presets/default-esm",
|
||||||
|
"globals": {
|
||||||
|
"ts-jest": {
|
||||||
|
"useESM": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"verbose": true,
|
||||||
|
"collectCoverage": true,
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"source/**/*.ts"
|
||||||
|
],
|
||||||
|
"testTimeout": 30000,
|
||||||
|
"testMatch": [
|
||||||
|
"**/test/library/**/*-test.[jt]s?(x)"
|
||||||
|
],
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"jsx",
|
||||||
|
"json",
|
||||||
|
"ts",
|
||||||
|
"tsx"
|
||||||
|
],
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"^(\\.{1,2}/.*)\\.js$": "$1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"{source,test}/**/*.ts": "xo --ignore test/external/ --fix",
|
||||||
|
"**/*.{json,yaml,md}": "prettier --ignore-path .gitignore --ignore-unknown --write "
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,515 @@
|
||||||
|
# <div align="center"> Express Rate Limit </div>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
[](https://github.com/nfriedly/express-rate-limit/actions)
|
||||||
|
[](https://npmjs.org/package/express-rate-limit 'View this project on NPM')
|
||||||
|
[](https://www.npmjs.com/package/express-rate-limit)
|
||||||
|
|
||||||
|
Basic rate-limiting middleware for Express. 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).
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### 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
|
||||||
|
external stores initially. These other options all are excellent pieces of
|
||||||
|
software and may be more appropriate for some situations:
|
||||||
|
|
||||||
|
- [rate-limiter-flexible](https://www.npmjs.com/package/rate-limiter-flexible)
|
||||||
|
- [express-brute](https://www.npmjs.com/package/express-brute)
|
||||||
|
- [rate-limiter](https://www.npmjs.com/package/express-limiter)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
From the npm registry:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Using npm
|
||||||
|
> npm install express-rate-limit
|
||||||
|
# Using yarn or pnpm
|
||||||
|
> yarn/pnpm add express-rate-limit
|
||||||
|
```
|
||||||
|
|
||||||
|
From Github Releases:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Using npm
|
||||||
|
> npm install https://github.com/nfriedly/express-rate-limit/releases/download/v{version}/express-rate-limit.tgz
|
||||||
|
# Using yarn or pnpm
|
||||||
|
> yarn/pnpm add https://github.com/nfriedly/express-rate-limit/releases/download/v{version}/express-rate-limit.tgz
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `{version}` with the version of the package that you want to your, e.g.:
|
||||||
|
`6.0.0`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Importing
|
||||||
|
|
||||||
|
This library is provided in ESM as well as CJS forms, and works with both
|
||||||
|
Javascript and Typescript projects.
|
||||||
|
|
||||||
|
**This package requires you to use Node 14 or above.**
|
||||||
|
|
||||||
|
Import it in a CommonJS project (`type: commonjs` or no `type` field in
|
||||||
|
`package.json`) as follows:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const rateLimit = require('express-rate-limit')
|
||||||
|
```
|
||||||
|
|
||||||
|
Import it in a ESM project (`type: module` in `package.json`) as follows:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import rateLimit from 'express-rate-limit'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
To use it in an API-only server where the rate-limiter should be applied to all
|
||||||
|
requests:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import rateLimit from 'express-rate-limit'
|
||||||
|
|
||||||
|
const limiter = 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
|
||||||
|
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
|
||||||
|
})
|
||||||
|
|
||||||
|
// Apply the rate limiting middleware to all requests
|
||||||
|
app.use(limiter)
|
||||||
|
```
|
||||||
|
|
||||||
|
To use it in a 'regular' web server (e.g. anything that uses
|
||||||
|
`express.static()`), where the rate-limiter should only apply to certain
|
||||||
|
requests:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import rateLimit from 'express-rate-limit'
|
||||||
|
|
||||||
|
const apiLimiter = 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
|
||||||
|
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
|
||||||
|
})
|
||||||
|
|
||||||
|
// Apply the rate limiting middleware to API calls only
|
||||||
|
app.use('/api', apiLimiter)
|
||||||
|
```
|
||||||
|
|
||||||
|
To create multiple instances to apply different rules to different endpoints:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import rateLimit from 'express-rate-limit'
|
||||||
|
|
||||||
|
const apiLimiter = 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
|
||||||
|
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use('/api/', apiLimiter)
|
||||||
|
|
||||||
|
const createAccountLimiter = rateLimit({
|
||||||
|
windowMs: 60 * 60 * 1000, // 1 hour
|
||||||
|
max: 5, // Limit each IP to 5 create account requests per `window` (here, per hour)
|
||||||
|
message:
|
||||||
|
'Too many accounts created from this IP, please try again after an hour',
|
||||||
|
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
|
||||||
|
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/create-account', createAccountLimiter, (request, response) => {
|
||||||
|
//...
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
To use a custom store:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import rateLimit from 'express-rate-limit'
|
||||||
|
import MemoryStore from 'express-rate-limit/memory-store.js'
|
||||||
|
|
||||||
|
const apiLimiter = 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(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Apply the rate limiting middleware to API calls only
|
||||||
|
app.use('/api', apiLimiter)
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** most stores will require additional configuration, such as custom
|
||||||
|
> prefixes, when using multiple instances. The default built-in memory store is
|
||||||
|
> an exception to this rule.
|
||||||
|
|
||||||
|
### Troubleshooting Proxy Issues
|
||||||
|
|
||||||
|
If you are behind a proxy/load balancer (usually the case with most hosting
|
||||||
|
services, e.g. Heroku, Bluemix, AWS ELB, Nginx, Cloudflare, Akamai, Fastly,
|
||||||
|
Firebase Hosting, Rackspace LB, Riverbed Stingray, etc.), the IP address of the
|
||||||
|
request might be the IP of the load balancer/reverse proxy (making the rate
|
||||||
|
limiter effectively a global one and blocking all requests once the limit is
|
||||||
|
reached) or `undefined`. To solve this issue, add the following line to your
|
||||||
|
code (right after you create the express application):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
app.set('trust proxy', numberOfProxies)
|
||||||
|
```
|
||||||
|
|
||||||
|
Where `numberOfProxies` is the number of proxies between the user and the
|
||||||
|
server. To find the correct number, create a test endpoint that returns the
|
||||||
|
client IP:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
app.set('trust proxy', 1)
|
||||||
|
app.get('/ip', (request, response) => response.send(request.ip))
|
||||||
|
```
|
||||||
|
|
||||||
|
Go to `/ip` and see the IP address returned in the response. If it matches your
|
||||||
|
IP address (which you can get by going to http://ip.nfriedly.com/ or
|
||||||
|
https://api.ipify.org/), then the number of proxies is correct and the rate
|
||||||
|
limiter should now work correctly. If not, then keep increasing the number until
|
||||||
|
it does.
|
||||||
|
|
||||||
|
For more information about the `trust proxy` setting, take a look at the
|
||||||
|
[official Express documentation](https://expressjs.com/en/guide/behind-proxies.html).
|
||||||
|
|
||||||
|
## Request API
|
||||||
|
|
||||||
|
A `request.rateLimit` property is added to all requests with the `limit`,
|
||||||
|
`current`, and `remaining` number of requests and, if the store provides it, a
|
||||||
|
`resetTime` Date object. These may be used in your application code to take
|
||||||
|
additional actions or inform the user of their status.
|
||||||
|
|
||||||
|
The property name can be configured with the configuration option
|
||||||
|
`requestPropertyName`
|
||||||
|
|
||||||
|
## Configuration options
|
||||||
|
|
||||||
|
### `windowMs`
|
||||||
|
|
||||||
|
Time frame for which requests are checked/remembered. Also used in the
|
||||||
|
`Retry-After` header when the limit is reached.
|
||||||
|
|
||||||
|
Note: with non-default stores, you may need to configure this value twice, once
|
||||||
|
here and once on the store. In some cases the units also differ (e.g. seconds vs
|
||||||
|
miliseconds)
|
||||||
|
|
||||||
|
Defaults to `60000` ms (= 1 minute).
|
||||||
|
|
||||||
|
### `max`
|
||||||
|
|
||||||
|
Max number of connections during `windowMs` milliseconds before sending a 429
|
||||||
|
response.
|
||||||
|
|
||||||
|
May be a number, or a function that returns a number or a promise. If `max` is a
|
||||||
|
function, it will be called with `request` and `response` params.
|
||||||
|
|
||||||
|
Defaults to `5`. Set to `0` to disable.
|
||||||
|
|
||||||
|
Example of using a function:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import rateLimit from 'express-rate-limit'
|
||||||
|
|
||||||
|
const isPremium = (request) => {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
const limiter = rateLimit({
|
||||||
|
// `max` could also be an async function or return a promise
|
||||||
|
max: (request, response) => {
|
||||||
|
if (isPremium(request)) return 10
|
||||||
|
else return 5
|
||||||
|
},
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
|
||||||
|
// Apply the rate limiting middleware to all requests
|
||||||
|
app.use(limiter)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `message`
|
||||||
|
|
||||||
|
Error message sent to user when `max` is exceeded.
|
||||||
|
|
||||||
|
May be a `string`, JSON object, or any other value that Express's
|
||||||
|
[response.send](https://expressjs.com/en/4x/api.html#response.send) method
|
||||||
|
supports.
|
||||||
|
|
||||||
|
Defaults to `'Too many requests, please try again later.'`
|
||||||
|
|
||||||
|
### `statusCode`
|
||||||
|
|
||||||
|
HTTP status code returned when `max` is exceeded.
|
||||||
|
|
||||||
|
Defaults to `429`.
|
||||||
|
|
||||||
|
### `legacyHeaders`
|
||||||
|
|
||||||
|
Enable headers for request limit (`X-RateLimit-Limit`) and current usage
|
||||||
|
(`X-RateLimit-Remaining`) on all responses and time to wait before retrying
|
||||||
|
(`Retry-After`) when `max` is exceeded.
|
||||||
|
|
||||||
|
Defaults to `true`.
|
||||||
|
|
||||||
|
> Renamed in `6.x` from `headers` to `legacyHeaders`.
|
||||||
|
|
||||||
|
### `standardHeaders`
|
||||||
|
|
||||||
|
Enable headers conforming to the
|
||||||
|
[ratelimit standardization draft](https://github.com/ietf-wg-httpapi/ratelimit-headers/blob/main/draft-ietf-httpapi-ratelimit-headers.md)
|
||||||
|
adopted by the IETF: `RateLimit-Limit`, `RateLimit-Remaining`, and, if the store
|
||||||
|
supports it, `RateLimit-Reset`. May be used in conjunction with, or instead of
|
||||||
|
the `legacyHeaders` option.
|
||||||
|
|
||||||
|
This setting also enables the `Retry-After` header when `max` is exceeded.
|
||||||
|
|
||||||
|
Defaults to `false` (for backward compatibility), but recommended to use.
|
||||||
|
|
||||||
|
> Renamed in `6.x` from `draft_polli_ratelimit_headers` to `standardHeaders`.
|
||||||
|
|
||||||
|
### `keyGenerator`
|
||||||
|
|
||||||
|
Function used to generate keys.
|
||||||
|
|
||||||
|
Defaults to `request.ip`, similar to this:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const keyGenerator = (request /*, response*/) => request.ip
|
||||||
|
```
|
||||||
|
|
||||||
|
### `handler`
|
||||||
|
|
||||||
|
The function to handle requests once the max limit is exceeded. It receives the
|
||||||
|
`request` and the `response` objects. The `next` param is available if you need
|
||||||
|
to pass to the next middleware/route. Finally, the `options` param has all of
|
||||||
|
the options that originally passed in when creating the current limiter and the
|
||||||
|
default values for other options.
|
||||||
|
|
||||||
|
The `request.rateLimit` object has `limit`, `current`, and `remaining` number of
|
||||||
|
requests and, if the store provides it, a `resetTime` Date object.
|
||||||
|
|
||||||
|
Defaults to:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const handler = (request, response, next, options) => {
|
||||||
|
response.status(options.statusCode).send(options.message)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `requestWasSuccessful`
|
||||||
|
|
||||||
|
Function that is called when `skipFailedRequests` and/or
|
||||||
|
`skipSuccessfulRequests` are set to `true`. May be overridden if, for example, a
|
||||||
|
service sends out a 200 status code on errors.
|
||||||
|
|
||||||
|
Defaults to
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const requestWasSuccessful = (request, response) => response.statusCode < 400
|
||||||
|
```
|
||||||
|
|
||||||
|
### `skipFailedRequests`
|
||||||
|
|
||||||
|
When set to `true`, failed requests won't be counted. Request considered failed
|
||||||
|
when:
|
||||||
|
|
||||||
|
- response status >= 400
|
||||||
|
- requests that were cancelled before last chunk of data was sent (response
|
||||||
|
`close` event triggered)
|
||||||
|
- response `error` event was triggered by response
|
||||||
|
|
||||||
|
(Technically they are counted and then un-counted, so a large number of slow
|
||||||
|
requests all at once could still trigger a rate-limit. This may be fixed in a
|
||||||
|
future release.)
|
||||||
|
|
||||||
|
Defaults to `false`.
|
||||||
|
|
||||||
|
### `skipSuccessfulRequests`
|
||||||
|
|
||||||
|
When set to `true` successful requests (response status < 400) won't be counted.
|
||||||
|
(Technically they are counted and then un-counted, so a large number of slow
|
||||||
|
requests all at once could still trigger a rate-limit. This may be fixed in a
|
||||||
|
future release.)
|
||||||
|
|
||||||
|
Defaults to `false`.
|
||||||
|
|
||||||
|
### `skip`
|
||||||
|
|
||||||
|
Function used to skip (whitelist) requests. Returning `true`, or a promise that
|
||||||
|
resolves with `true`, from the function will skip limiting for that request.
|
||||||
|
|
||||||
|
Defaults to always `false` (count all requests):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const skip = (/*request, response*/) => false
|
||||||
|
```
|
||||||
|
|
||||||
|
### `requestPropertyName`
|
||||||
|
|
||||||
|
The name of the property that contains the rate limit information to add to the
|
||||||
|
`request` object.
|
||||||
|
|
||||||
|
Defaults to `rateLimit`.
|
||||||
|
|
||||||
|
### `store`
|
||||||
|
|
||||||
|
The storage to use when persisting rate limit attempts.
|
||||||
|
|
||||||
|
By default, the [memory store](source/memory-store.ts) is used.
|
||||||
|
|
||||||
|
Available data stores are:
|
||||||
|
|
||||||
|
- [memory-store](source/memory-store.ts): _(default)_ Simple in-memory option.
|
||||||
|
Does not share state when app has multiple processes or servers.
|
||||||
|
- [rate-limit-redis](https://npmjs.com/package/rate-limit-redis): A
|
||||||
|
[Redis](http://redis.io/)-backed store, more suitable for large or demanding
|
||||||
|
deployments.
|
||||||
|
- [rate-limit-memcached](https://npmjs.org/package/rate-limit-memcached): A
|
||||||
|
[Memcached](https://memcached.org/)-backed store.
|
||||||
|
- [rate-limit-mongo](https://www.npmjs.com/package/rate-limit-mongo): A
|
||||||
|
[MongoDB](https://www.mongodb.com/)-backed store.
|
||||||
|
- [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 IP rather than bucketing them together.
|
||||||
|
|
||||||
|
You may also create your own store. It must implement the `Store` interface as
|
||||||
|
follows:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import rateLimit, {
|
||||||
|
Store,
|
||||||
|
Options,
|
||||||
|
IncrementResponse,
|
||||||
|
} from 'express-rate-limit'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link Store} that stores the hit count for each client.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
class SomeStore implements Store {
|
||||||
|
/**
|
||||||
|
* Some store-specific parameter.
|
||||||
|
*/
|
||||||
|
customParam!: string
|
||||||
|
/**
|
||||||
|
* The duration of time before which all hit counts are reset (in milliseconds).
|
||||||
|
*/
|
||||||
|
windowMs!: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor for {@link SomeStore}. Only required if the user needs to pass
|
||||||
|
* some store specific parameters. For example, in a Mongo Store, the user will
|
||||||
|
* need to pass the URI, username and password for the Mongo database.
|
||||||
|
*
|
||||||
|
* @param customParam {string} - Some store-specific parameter.
|
||||||
|
*/
|
||||||
|
constructor(customParam: string) {
|
||||||
|
this.customParam = customParam
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that actually initializes the store. Must be synchronous.
|
||||||
|
*
|
||||||
|
* @param options {Options} - The options used to setup the middleware.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
init(options: Options): void {
|
||||||
|
this.windowMs = options.windowMs
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to increment a client's hit counter.
|
||||||
|
*
|
||||||
|
* @param key {string} - The identifier for a client
|
||||||
|
*
|
||||||
|
* @returns {IncrementResponse} - The number of hits and reset time for that client
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
async increment(key: string): Promise<IncrementResponse> {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalHits,
|
||||||
|
resetTime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to decrement a client's hit counter.
|
||||||
|
*
|
||||||
|
* @param key {string} - The identifier for a client
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
async decrement(key: string): Promise<void> {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to reset a client's hit counter.
|
||||||
|
*
|
||||||
|
* @param key {string} - The identifier for a client
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
async resetKey(key: string): Promise<void> {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to reset everyone's hit counter.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
async resetAll(): Promise<void> {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SomeStore
|
||||||
|
```
|
||||||
|
|
||||||
|
## Instance API
|
||||||
|
|
||||||
|
### `resetKey(key)`
|
||||||
|
|
||||||
|
Resets the rate limiting for a given key. An example use case is to allow users
|
||||||
|
to complete a captcha or whatever to reset their rate limit, then call this
|
||||||
|
method.
|
||||||
|
|
||||||
|
## Issues and Contributing
|
||||||
|
|
||||||
|
If you encounter a bug or want to see something added/changed, please go ahead
|
||||||
|
and [open an issue](https://github.com/nfriedly/express-rate-limit/issues/new)!
|
||||||
|
If you need help with something, feel free to
|
||||||
|
[start a discussion](https://github.com/nfriedly/express-rate-limit/discussions/new)!
|
||||||
|
|
||||||
|
If you wish to contribute to the library, thanks! First, please read
|
||||||
|
[the contributing guide](contributing.md). Then you can pick up any issue and
|
||||||
|
fix/implement it!
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT © [Nathan Friedly](http://nfriedly.com/)
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"include": ["source/"],
|
||||||
|
"exclude": ["node_modules/"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"declaration": true,
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"moduleResolution": "node"
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mozilla/readability": "^0.3.0",
|
"@mozilla/readability": "^0.3.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
|
"express-rate-limit": "^6.0.5",
|
||||||
"jsdom": "^16.4.0",
|
"jsdom": "^16.4.0",
|
||||||
"turndown": "^7.0.0"
|
"turndown": "^7.0.0"
|
||||||
},
|
},
|
||||||
|
@ -418,6 +419,17 @@
|
||||||
"node": ">= 0.10.0"
|
"node": ">= 0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/express-rate-limit": {
|
||||||
|
"version": "6.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.0.5.tgz",
|
||||||
|
"integrity": "sha512-EB1mRTrzyyPfEsQZIQFXocd8NKZoDZbEwrtbdgkc20Yed6oYg02Xfjza2HHPI/0orp54BrFeHeT92ICB9ydokw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.5.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"express": "^4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-levenshtein": {
|
"node_modules/fast-levenshtein": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
||||||
|
@ -1433,6 +1445,12 @@
|
||||||
"vary": "~1.1.2"
|
"vary": "~1.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"express-rate-limit": {
|
||||||
|
"version": "6.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.0.5.tgz",
|
||||||
|
"integrity": "sha512-EB1mRTrzyyPfEsQZIQFXocd8NKZoDZbEwrtbdgkc20Yed6oYg02Xfjza2HHPI/0orp54BrFeHeT92ICB9ydokw==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"fast-levenshtein": {
|
"fast-levenshtein": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
||||||
|
|
|
@ -6,10 +6,10 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mozilla/readability": "^0.3.0",
|
"@mozilla/readability": "^0.3.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
|
"express-rate-limit": "^6.0.5",
|
||||||
"jsdom": "^16.4.0",
|
"jsdom": "^16.4.0",
|
||||||
"turndown": "^7.0.0"
|
"turndown": "^7.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node index.js"
|
"start": "node index.js"
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue