diff --git a/index.js b/index.js index a892477..a593db0 100644 --- a/index.js +++ b/index.js @@ -13,6 +13,18 @@ const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const JWT_SECRET = "supergeheimes_tg_cms_secret"; +//Rate Limiter fuer Logins +const rateLimit = require("express-rate-limit"); + + +const loginLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 5, + message: "Zu viele Login-Versuche. Bitte in 15 Minuten erneut versuchen.", + standardHeaders: true, + legacyHeaders: false, +}); + // Bodyparser für JSON aktivieren app.use(cors({ @@ -150,7 +162,7 @@ app.post("/api/users", async (req, res) => { }); // Benutzer Login -app.post("/api/login", async (req, res) => { +app.post("/api/login", loginLimiter, async (req, res) => { const { username, password } = req.body; if (!username || !password) { @@ -855,7 +867,7 @@ app.get("/api/events", async (req, res) => { ? "SELECT * FROM events ORDER BY created_at DESC" : "SELECT * FROM events WHERE is_private = FALSE ORDER BY created_at DESC"; - const result = await db.query(query); + const result = await pool.query(query); res.json(result.rows); }); @@ -864,7 +876,7 @@ app.get("/api/events", async (req, res) => { app.post("/api/events", async (req, res) => { const { title, description, max_participants, fee, address, is_private } = req.body; - const result = await db.query( + const result = await pool.query( `INSERT INTO events (title, description, max_participants, fee, address, is_private) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`, [title, description, max_participants, fee, address, is_private] @@ -879,7 +891,7 @@ app.put("/api/events/:id", async (req, res) => { const { id } = req.params; const { title, description, max_participants, fee, address, is_private } = req.body; - const result = await db.query( + const result = await pool.query( `UPDATE events SET title = $1, description = $2, @@ -904,7 +916,7 @@ app.put("/api/events/:id", async (req, res) => { app.delete("/api/events/:id", async (req, res) => { const { id } = req.params; - const result = await db.query(`DELETE FROM events WHERE id = $1`, [id]); + const result = await pool.query(`DELETE FROM events WHERE id = $1`, [id]); if (result.rowCount === 0) { return res.status(404).json({ message: "Event not found" }); diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json index a7e19e2..a5f4b19 100644 --- a/node_modules/.package-lock.json +++ b/node_modules/.package-lock.json @@ -349,6 +349,20 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, "node_modules/finalhandler": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", diff --git a/node_modules/express-rate-limit/dist/index.cjs b/node_modules/express-rate-limit/dist/index.cjs new file mode 100644 index 0000000..36d5e1b --- /dev/null +++ b/node_modules/express-rate-limit/dist/index.cjs @@ -0,0 +1,838 @@ +"use strict"; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// source/index.ts +var source_exports = {}; +__export(source_exports, { + MemoryStore: () => MemoryStore, + default: () => lib_default, + rateLimit: () => lib_default +}); +module.exports = __toCommonJS(source_exports); + +// source/headers.ts +var import_node_buffer = require("buffer"); +var import_node_crypto = require("crypto"); +var SUPPORTED_DRAFT_VERSIONS = ["draft-6", "draft-7", "draft-8"]; +var getResetSeconds = (resetTime, windowMs) => { + let resetSeconds = void 0; + if (resetTime) { + const deltaSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1e3); + resetSeconds = Math.max(0, deltaSeconds); + } else if (windowMs) { + resetSeconds = Math.ceil(windowMs / 1e3); + } + return resetSeconds; +}; +var getPartitionKey = (key) => { + const hash = (0, import_node_crypto.createHash)("sha256"); + hash.update(key); + const partitionKey = hash.digest("hex").slice(0, 12); + return import_node_buffer.Buffer.from(partitionKey).toString("base64"); +}; +var setLegacyHeaders = (response, info) => { + if (response.headersSent) + return; + response.setHeader("X-RateLimit-Limit", info.limit.toString()); + response.setHeader("X-RateLimit-Remaining", info.remaining.toString()); + if (info.resetTime instanceof Date) { + response.setHeader("Date", (/* @__PURE__ */ new Date()).toUTCString()); + response.setHeader( + "X-RateLimit-Reset", + Math.ceil(info.resetTime.getTime() / 1e3).toString() + ); + } +}; +var setDraft6Headers = (response, info, windowMs) => { + if (response.headersSent) + return; + const windowSeconds = Math.ceil(windowMs / 1e3); + const resetSeconds = getResetSeconds(info.resetTime); + response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`); + response.setHeader("RateLimit-Limit", info.limit.toString()); + response.setHeader("RateLimit-Remaining", info.remaining.toString()); + if (resetSeconds) + response.setHeader("RateLimit-Reset", resetSeconds.toString()); +}; +var setDraft7Headers = (response, info, windowMs) => { + if (response.headersSent) + return; + const windowSeconds = Math.ceil(windowMs / 1e3); + const resetSeconds = getResetSeconds(info.resetTime, windowMs); + response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`); + response.setHeader( + "RateLimit", + `limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}` + ); +}; +var setDraft8Headers = (response, info, windowMs, name, key) => { + if (response.headersSent) + return; + const windowSeconds = Math.ceil(windowMs / 1e3); + const resetSeconds = getResetSeconds(info.resetTime, windowMs); + const partitionKey = getPartitionKey(key); + const policy = `q=${info.limit}; w=${windowSeconds}; pk=:${partitionKey}:`; + const header = `r=${info.remaining}; t=${resetSeconds}`; + response.append("RateLimit-Policy", `"${name}"; ${policy}`); + response.append("RateLimit", `"${name}"; ${header}`); +}; +var setRetryAfterHeader = (response, info, windowMs) => { + if (response.headersSent) + return; + const resetSeconds = getResetSeconds(info.resetTime, windowMs); + response.setHeader("Retry-After", resetSeconds.toString()); +}; + +// source/validations.ts +var import_node_net = require("net"); +var ValidationError = class extends Error { + /** + * The code must be a string, in snake case and all capital, that starts with + * the substring `ERR_ERL_`. + * + * The message must be a string, starting with an uppercase character, + * describing the issue in detail. + */ + constructor(code, message) { + const url = `https://express-rate-limit.github.io/${code}/`; + super(`${message} See ${url} for more information.`); + this.name = this.constructor.name; + this.code = code; + this.help = url; + } +}; +var ChangeWarning = class extends ValidationError { +}; +var usedStores = /* @__PURE__ */ new Set(); +var singleCountKeys = /* @__PURE__ */ new WeakMap(); +var validations = { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + enabled: { + default: true + }, + // Should be EnabledValidations type, but that's a circular reference + disable() { + for (const k of Object.keys(this.enabled)) + this.enabled[k] = false; + }, + /** + * Checks whether the IP address is valid, and that it does not have a port + * number in it. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_invalid_ip_address. + * + * @param ip {string | undefined} - The IP address provided by Express as request.ip. + * + * @returns {void} + */ + ip(ip) { + if (ip === void 0) { + throw new ValidationError( + "ERR_ERL_UNDEFINED_IP_ADDRESS", + `An undefined 'request.ip' was detected. This might indicate a misconfiguration or the connection being destroyed prematurely.` + ); + } + if (!(0, import_node_net.isIP)(ip)) { + throw new ValidationError( + "ERR_ERL_INVALID_IP_ADDRESS", + `An invalid 'request.ip' (${ip}) was detected. Consider passing a custom 'keyGenerator' function to the rate limiter.` + ); + } + }, + /** + * Makes sure the trust proxy setting is not set to `true`. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_permissive_trust_proxy. + * + * @param request {Request} - The Express request object. + * + * @returns {void} + */ + trustProxy(request) { + if (request.app.get("trust proxy") === true) { + throw new ValidationError( + "ERR_ERL_PERMISSIVE_TRUST_PROXY", + `The Express 'trust proxy' setting is true, which allows anyone to trivially bypass IP-based rate limiting.` + ); + } + }, + /** + * Makes sure the trust proxy setting is set in case the `X-Forwarded-For` + * header is present. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_unset_trust_proxy. + * + * @param request {Request} - The Express request object. + * + * @returns {void} + */ + xForwardedForHeader(request) { + if (request.headers["x-forwarded-for"] && request.app.get("trust proxy") === false) { + throw new ValidationError( + "ERR_ERL_UNEXPECTED_X_FORWARDED_FOR", + `The 'X-Forwarded-For' header is set but the Express 'trust proxy' setting is false (default). This could indicate a misconfiguration which would prevent express-rate-limit from accurately identifying users.` + ); + } + }, + /** + * Ensures totalHits value from store is a positive integer. + * + * @param hits {any} - The `totalHits` returned by the store. + */ + positiveHits(hits) { + if (typeof hits !== "number" || hits < 1 || hits !== Math.round(hits)) { + throw new ValidationError( + "ERR_ERL_INVALID_HITS", + `The totalHits value returned from the store must be a positive integer, got ${hits}` + ); + } + }, + /** + * Ensures a single store instance is not used with multiple express-rate-limit instances + */ + unsharedStore(store) { + if (usedStores.has(store)) { + const maybeUniquePrefix = store?.localKeys ? "" : " (with a unique prefix)"; + throw new ValidationError( + "ERR_ERL_STORE_REUSE", + `A Store instance must not be shared across multiple rate limiters. Create a new instance of ${store.constructor.name}${maybeUniquePrefix} for each limiter instead.` + ); + } + usedStores.add(store); + }, + /** + * 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) { + let storeKeys = singleCountKeys.get(request); + if (!storeKeys) { + storeKeys = /* @__PURE__ */ new Map(); + singleCountKeys.set(request, storeKeys); + } + const storeKey = store.localKeys ? store : store.constructor.name; + let keys = storeKeys.get(storeKey); + if (!keys) { + keys = []; + storeKeys.set(storeKey, keys); + } + const prefixedKey = `${store.prefix ?? ""}${key}`; + if (keys.includes(prefixedKey)) { + throw new ValidationError( + "ERR_ERL_DOUBLE_COUNT", + `The hit count for ${key} was incremented more than once for a single request.` + ); + } + keys.push(prefixedKey); + }, + /** + * Warns the user that the behaviour for `max: 0` / `limit: 0` is + * changing in the next major release. + * + * @param limit {number} - The maximum number of hits per client. + * + * @returns {void} + */ + limit(limit) { + if (limit === 0) { + throw new ChangeWarning( + "WRN_ERL_MAX_ZERO", + `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` + ); + } + }, + /** + * Warns the user that the `draft_polli_ratelimit_headers` option is deprecated + * and will be removed in the next major release. + * + * @param draft_polli_ratelimit_headers {any | undefined} - The now-deprecated setting that was used to enable standard headers. + * + * @returns {void} + */ + draftPolliHeaders(draft_polli_ratelimit_headers) { + if (draft_polli_ratelimit_headers) { + throw new ChangeWarning( + "WRN_ERL_DEPRECATED_DRAFT_POLLI_HEADERS", + `The draft_polli_ratelimit_headers configuration option is deprecated and has been removed in express-rate-limit v7, please set standardHeaders: 'draft-6' instead.` + ); + } + }, + /** + * Warns the user that the `onLimitReached` option is deprecated and + * will be removed in the next major release. + * + * @param onLimitReached {any | undefined} - The maximum number of hits per client. + * + * @returns {void} + */ + onLimitReached(onLimitReached) { + if (onLimitReached) { + throw new ChangeWarning( + "WRN_ERL_DEPRECATED_ON_LIMIT_REACHED", + `The onLimitReached configuration option is deprecated and has been removed in express-rate-limit v7.` + ); + } + }, + /** + * Warns the user when an invalid/unsupported version of the draft spec is passed. + * + * @param version {any | undefined} - The version passed by the user. + * + * @returns {void} + */ + headersDraftVersion(version) { + if (typeof version !== "string" || !SUPPORTED_DRAFT_VERSIONS.includes(version)) { + const versionString = SUPPORTED_DRAFT_VERSIONS.join(", "); + throw new ValidationError( + "ERR_ERL_HEADERS_UNSUPPORTED_DRAFT_VERSION", + `standardHeaders: only the following versions of the IETF draft specification are supported: ${versionString}.` + ); + } + }, + /** + * Warns the user when the selected headers option requires a reset time but + * the store does not provide one. + * + * @param resetTime {Date | undefined} - The timestamp when the client's hit count will be reset. + * + * @returns {void} + */ + headersResetTime(resetTime) { + if (!resetTime) { + throw new ValidationError( + "ERR_ERL_HEADERS_NO_RESET", + `standardHeaders: 'draft-7' requires a 'resetTime', but the store did not provide one. The 'windowMs' value will be used instead, which may cause clients to wait longer than necessary.` + ); + } + }, + /** + * Checks the options.validate setting to ensure that only recognized + * validations are enabled or disabled. + * + * If any unrecognized values are found, an error is logged that + * includes the list of supported vaidations. + */ + validationsConfig() { + const supportedValidations = Object.keys(this).filter( + (k) => !["enabled", "disable"].includes(k) + ); + supportedValidations.push("default"); + for (const key of Object.keys(this.enabled)) { + if (!supportedValidations.includes(key)) { + throw new ValidationError( + "ERR_ERL_UNKNOWN_VALIDATION", + `options.validate.${key} is not recognized. Supported validate options are: ${supportedValidations.join( + ", " + )}.` + ); + } + } + }, + /** + * Checks to see if the instance was created inside of a request handler, + * which would prevent it from working correctly, with the default memory + * store (or any other store with localKeys.) + */ + creationStack(store) { + const { stack } = new Error( + "express-rate-limit validation check (set options.validate.creationStack=false to disable)" + ); + if (stack?.includes("Layer.handle [as handle_request]")) { + if (!store.localKeys) { + throw new ValidationError( + "ERR_ERL_CREATED_IN_REQUEST_HANDLER", + "express-rate-limit instance should *usually* be created at app initialization, not when responding to a request." + ); + } + throw new ValidationError( + "ERR_ERL_CREATED_IN_REQUEST_HANDLER", + `express-rate-limit instance should be created at app initialization, not when responding to a request.` + ); + } + } +}; +var getValidations = (_enabled) => { + let enabled; + if (typeof _enabled === "boolean") { + enabled = { + default: _enabled + }; + } else { + enabled = { + default: true, + ..._enabled + }; + } + const wrappedValidations = { + enabled + }; + for (const [name, validation] of Object.entries(validations)) { + if (typeof validation === "function") + wrappedValidations[name] = (...args) => { + if (!(enabled[name] ?? enabled.default)) { + return; + } + try { + ; + validation.apply( + wrappedValidations, + args + ); + } catch (error) { + if (error instanceof ChangeWarning) + console.warn(error); + else + console.error(error); + } + }; + } + return wrappedValidations; +}; + +// source/memory-store.ts +var MemoryStore = class { + constructor() { + /** + * These two maps store usage (requests) and reset time by key (for example, IP + * addresses or API keys). + * + * They are split into two to avoid having to iterate through the entire set to + * determine which ones need reset. Instead, `Client`s are moved from `previous` + * to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients + * left in `previous`, i.e., those that have not made any recent requests, are + * known to be expired and can be deleted in bulk. + */ + this.previous = /* @__PURE__ */ new Map(); + this.current = /* @__PURE__ */ new Map(); + /** + * Confirmation that the keys incremented in once instance of MemoryStore + * cannot affect other instances. + */ + this.localKeys = true; + } + /** + * Method that initializes the store. + * + * @param options {Options} - The options used to setup the middleware. + */ + init(options) { + this.windowMs = options.windowMs; + if (this.interval) + clearInterval(this.interval); + this.interval = setInterval(() => { + this.clearExpired(); + }, this.windowMs); + if (this.interval.unref) + this.interval.unref(); + } + /** + * Method to fetch a client's hit count and reset time. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client. + * + * @public + */ + async get(key) { + return this.current.get(key) ?? this.previous.get(key); + } + /** + * Method to increment a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client. + * + * @public + */ + async increment(key) { + const client = this.getClient(key); + const now = Date.now(); + if (client.resetTime.getTime() <= now) { + this.resetClient(client, now); + } + client.totalHits++; + return client; + } + /** + * Method to decrement a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @public + */ + async decrement(key) { + const client = this.getClient(key); + if (client.totalHits > 0) + client.totalHits--; + } + /** + * Method to reset a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @public + */ + async resetKey(key) { + this.current.delete(key); + this.previous.delete(key); + } + /** + * Method to reset everyone's hit counter. + * + * @public + */ + async resetAll() { + this.current.clear(); + this.previous.clear(); + } + /** + * Method to stop the timer (if currently running) and prevent any memory + * leaks. + * + * @public + */ + shutdown() { + clearInterval(this.interval); + void this.resetAll(); + } + /** + * Recycles a client by setting its hit count to zero, and reset time to + * `windowMs` milliseconds from now. + * + * NOT to be confused with `#resetKey()`, which removes a client from both the + * `current` and `previous` maps. + * + * @param client {Client} - The client to recycle. + * @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client. + * + * @return {Client} - The modified client that was passed in, to allow for chaining. + */ + resetClient(client, now = Date.now()) { + client.totalHits = 0; + client.resetTime.setTime(now + this.windowMs); + return client; + } + /** + * Retrieves or creates a client, given a key. Also ensures that the client being + * returned is in the `current` map. + * + * @param key {string} - The key under which the client is (or is to be) stored. + * + * @returns {Client} - The requested client. + */ + getClient(key) { + if (this.current.has(key)) + return this.current.get(key); + let client; + if (this.previous.has(key)) { + client = this.previous.get(key); + this.previous.delete(key); + } else { + client = { totalHits: 0, resetTime: /* @__PURE__ */ new Date() }; + this.resetClient(client); + } + this.current.set(key, client); + return client; + } + /** + * Move current clients to previous, create a new map for current. + * + * This function is called every `windowMs`. + */ + clearExpired() { + this.previous = this.current; + this.current = /* @__PURE__ */ new Map(); + } +}; + +// source/lib.ts +var isLegacyStore = (store) => ( + // Check that `incr` exists but `increment` does not - store authors might want + // to keep both around for backwards compatibility. + 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 legacyStore.decrement(key); + } + async resetKey(key) { + return legacyStore.resetKey(key); + } + /* istanbul ignore next */ + async resetAll() { + if (typeof legacyStore.resetAll === "function") + return legacyStore.resetAll(); + } + } + return new PromisifiedStore(); +}; +var getOptionsFromConfig = (config) => { + const { validations: validations2, ...directlyPassableEntries } = config; + return { + ...directlyPassableEntries, + validate: validations2.enabled + }; +}; +var omitUndefinedOptions = (passedOptions) => { + const omittedOptions = {}; + for (const k of Object.keys(passedOptions)) { + const key = k; + if (passedOptions[key] !== void 0) { + omittedOptions[key] = passedOptions[key]; + } + } + return omittedOptions; +}; +var parseOptions = (passedOptions) => { + const notUndefinedOptions = omitUndefinedOptions(passedOptions); + const validations2 = getValidations(notUndefinedOptions?.validate ?? true); + validations2.validationsConfig(); + validations2.draftPolliHeaders( + // @ts-expect-error see the note above. + notUndefinedOptions.draft_polli_ratelimit_headers + ); + validations2.onLimitReached(notUndefinedOptions.onLimitReached); + let standardHeaders = notUndefinedOptions.standardHeaders ?? false; + if (standardHeaders === true) + standardHeaders = "draft-6"; + const config = { + windowMs: 60 * 1e3, + limit: passedOptions.max ?? 5, + // `max` is deprecated, but support it anyways. + message: "Too many requests, please try again later.", + statusCode: 429, + legacyHeaders: passedOptions.headers ?? true, + identifier(request, _response) { + let duration = ""; + const property = config.requestPropertyName; + const { limit } = request[property]; + const seconds = config.windowMs / 1e3; + const minutes = config.windowMs / (1e3 * 60); + const hours = config.windowMs / (1e3 * 60 * 60); + const days = config.windowMs / (1e3 * 60 * 60 * 24); + if (seconds < 60) + duration = `${seconds}sec`; + else if (minutes < 60) + duration = `${minutes}min`; + else if (hours < 24) + duration = `${hours}hr${hours > 1 ? "s" : ""}`; + else + duration = `${days}day${days > 1 ? "s" : ""}`; + return `${limit}-in-${duration}`; + }, + requestPropertyName: "rateLimit", + skipFailedRequests: false, + skipSuccessfulRequests: false, + requestWasSuccessful: (_request, response) => response.statusCode < 400, + skip: (_request, _response) => false, + keyGenerator(request, _response) { + validations2.ip(request.ip); + validations2.trustProxy(request); + validations2.xForwardedForHeader(request); + return request.ip; + }, + async handler(request, response, _next, _optionsUsed) { + response.status(config.statusCode); + const message = typeof config.message === "function" ? await config.message( + request, + response + ) : config.message; + if (!response.writableEnded) { + response.send(message); + } + }, + passOnStoreError: false, + // Allow the default options to be overriden by the passed options. + ...notUndefinedOptions, + // `standardHeaders` is resolved into a draft version above, use that. + standardHeaders, + // Note that this field is declared after the user's options are spread in, + // so that this field doesn't get overriden with an un-promisified store! + store: promisifyStore(notUndefinedOptions.store ?? new MemoryStore()), + // Print an error to the console if a few known misconfigurations are detected. + validations: validations2 + }; + if (typeof config.store.increment !== "function" || typeof config.store.decrement !== "function" || typeof config.store.resetKey !== "function" || config.store.resetAll !== void 0 && typeof config.store.resetAll !== "function" || config.store.init !== void 0 && typeof config.store.init !== "function") { + throw new TypeError( + "An invalid store was passed. Please ensure that the store is a class that implements the `Store` interface." + ); + } + return config; +}; +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 config = parseOptions(passedOptions ?? {}); + const options = getOptionsFromConfig(config); + config.validations.creationStack(config.store); + config.validations.unsharedStore(config.store); + if (typeof config.store.init === "function") + config.store.init(options); + const middleware = handleAsyncErrors( + async (request, response, next) => { + const skip = await config.skip(request, response); + if (skip) { + next(); + return; + } + const augmentedRequest = request; + const key = await config.keyGenerator(request, response); + let totalHits = 0; + let resetTime; + try { + const incrementResult = await config.store.increment(key); + totalHits = incrementResult.totalHits; + resetTime = incrementResult.resetTime; + } catch (error) { + if (config.passOnStoreError) { + console.error( + "express-rate-limit: error from store, allowing request without rate-limiting.", + error + ); + next(); + return; + } + throw error; + } + config.validations.positiveHits(totalHits); + config.validations.singleCount(request, config.store, key); + const retrieveLimit = typeof config.limit === "function" ? config.limit(request, response) : config.limit; + const limit = await retrieveLimit; + config.validations.limit(limit); + const info = { + limit, + used: totalHits, + remaining: Math.max(limit - totalHits, 0), + resetTime + }; + Object.defineProperty(info, "current", { + configurable: false, + enumerable: false, + value: totalHits + }); + augmentedRequest[config.requestPropertyName] = info; + if (config.legacyHeaders && !response.headersSent) { + setLegacyHeaders(response, info); + } + if (config.standardHeaders && !response.headersSent) { + switch (config.standardHeaders) { + case "draft-6": { + setDraft6Headers(response, info, config.windowMs); + break; + } + case "draft-7": { + config.validations.headersResetTime(info.resetTime); + setDraft7Headers(response, info, config.windowMs); + break; + } + case "draft-8": { + const retrieveName = typeof config.identifier === "function" ? config.identifier(request, response) : config.identifier; + const name = await retrieveName; + config.validations.headersResetTime(info.resetTime); + setDraft8Headers(response, info, config.windowMs, name, key); + break; + } + default: { + config.validations.headersDraftVersion(config.standardHeaders); + break; + } + } + } + if (config.skipFailedRequests || config.skipSuccessfulRequests) { + let decremented = false; + const decrementKey = async () => { + if (!decremented) { + await config.store.decrement(key); + decremented = true; + } + }; + if (config.skipFailedRequests) { + response.on("finish", async () => { + if (!await config.requestWasSuccessful(request, response)) + await decrementKey(); + }); + response.on("close", async () => { + if (!response.writableEnded) + await decrementKey(); + }); + response.on("error", async () => { + await decrementKey(); + }); + } + if (config.skipSuccessfulRequests) { + response.on("finish", async () => { + if (await config.requestWasSuccessful(request, response)) + await decrementKey(); + }); + } + } + config.validations.disable(); + if (totalHits > limit) { + if (config.legacyHeaders || config.standardHeaders) { + setRetryAfterHeader(response, info, config.windowMs); + } + config.handler(request, response, next, options); + return; + } + next(); + } + ); + const getThrowFn = () => { + throw new Error("The current store does not support the get/getKey method"); + }; + middleware.resetKey = config.store.resetKey.bind(config.store); + middleware.getKey = typeof config.store.get === "function" ? config.store.get.bind(config.store) : getThrowFn; + return middleware; +}; +var lib_default = rateLimit; +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + MemoryStore, + rateLimit +}); +module.exports = rateLimit; module.exports.default = rateLimit; module.exports.rateLimit = rateLimit; module.exports.MemoryStore = MemoryStore; diff --git a/node_modules/express-rate-limit/dist/index.d.cts b/node_modules/express-rate-limit/dist/index.d.cts new file mode 100644 index 0000000..62fe123 --- /dev/null +++ b/node_modules/express-rate-limit/dist/index.d.cts @@ -0,0 +1,584 @@ +// Generated by dts-bundle-generator v8.0.1 + +import { NextFunction, Request, RequestHandler, Response } from 'express'; + +declare const validations: { + enabled: { + [key: string]: boolean; + }; + disable(): void; + /** + * Checks whether the IP address is valid, and that it does not have a port + * number in it. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_invalid_ip_address. + * + * @param ip {string | undefined} - The IP address provided by Express as request.ip. + * + * @returns {void} + */ + ip(ip: string | undefined): void; + /** + * Makes sure the trust proxy setting is not set to `true`. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_permissive_trust_proxy. + * + * @param request {Request} - The Express request object. + * + * @returns {void} + */ + trustProxy(request: Request): void; + /** + * Makes sure the trust proxy setting is set in case the `X-Forwarded-For` + * header is present. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_unset_trust_proxy. + * + * @param request {Request} - The Express request object. + * + * @returns {void} + */ + xForwardedForHeader(request: Request): void; + /** + * Ensures totalHits value from store is a positive integer. + * + * @param hits {any} - The `totalHits` returned by the store. + */ + positiveHits(hits: any): void; + /** + * Ensures a single store instance is not used with multiple express-rate-limit instances + */ + unsharedStore(store: Store): void; + /** + * 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: Request, store: Store, key: string): void; + /** + * Warns the user that the behaviour for `max: 0` / `limit: 0` is + * changing in the next major release. + * + * @param limit {number} - The maximum number of hits per client. + * + * @returns {void} + */ + limit(limit: number): void; + /** + * Warns the user that the `draft_polli_ratelimit_headers` option is deprecated + * and will be removed in the next major release. + * + * @param draft_polli_ratelimit_headers {any | undefined} - The now-deprecated setting that was used to enable standard headers. + * + * @returns {void} + */ + draftPolliHeaders(draft_polli_ratelimit_headers?: any): void; + /** + * Warns the user that the `onLimitReached` option is deprecated and + * will be removed in the next major release. + * + * @param onLimitReached {any | undefined} - The maximum number of hits per client. + * + * @returns {void} + */ + onLimitReached(onLimitReached?: any): void; + /** + * Warns the user when an invalid/unsupported version of the draft spec is passed. + * + * @param version {any | undefined} - The version passed by the user. + * + * @returns {void} + */ + headersDraftVersion(version?: any): void; + /** + * Warns the user when the selected headers option requires a reset time but + * the store does not provide one. + * + * @param resetTime {Date | undefined} - The timestamp when the client's hit count will be reset. + * + * @returns {void} + */ + headersResetTime(resetTime?: Date): void; + /** + * Checks the options.validate setting to ensure that only recognized + * validations are enabled or disabled. + * + * If any unrecognized values are found, an error is logged that + * includes the list of supported vaidations. + */ + validationsConfig(): void; + /** + * Checks to see if the instance was created inside of a request handler, + * which would prevent it from working correctly, with the default memory + * store (or any other store with localKeys.) + */ + creationStack(store: Store): void; +}; +export type Validations = typeof validations; +declare const SUPPORTED_DRAFT_VERSIONS: string[]; +/** + * 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 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 type ValueDeterminingMiddleware = (request: Request, response: Response) => T | Promise; +/** + * 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 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 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 type ClientRateLimitInfo = { + totalHits: number; + resetTime: Date | undefined; +}; +export type IncrementResponse = ClientRateLimitInfo; +/** + * A modified Express request handler with the rate limit functions. + */ +export type RateLimitRequestHandler = RequestHandler & { + /** + * Method to reset a client's hit counter. + * + * @param key {string} - The identifier for a client. + */ + resetKey: (key: string) => void; + /** + * Method to fetch a client's hit count and reset time. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client. + */ + getKey: (key: string) => Promise | ClientRateLimitInfo | undefined; +}; +/** + * An interface that all hit counter stores must implement. + * + * @deprecated 6.x - Implement the `Store` interface instead. + */ +export type 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 type 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 fetch a client's hit count and reset time. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client. + */ + get?: (key: string) => Promise | ClientRateLimitInfo | undefined; + /** + * Method to increment a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @returns {IncrementResponse | undefined} - The number of hits and reset time for that client. + */ + increment: (key: string) => Promise | IncrementResponse; + /** + * Method to decrement a client's hit counter. + * + * @param key {string} - The identifier for a client. + */ + decrement: (key: string) => Promise | void; + /** + * Method to reset a client's hit counter. + * + * @param key {string} - The identifier for a client. + */ + resetKey: (key: string) => Promise | void; + /** + * Method to reset everyone's hit counter. + */ + resetAll?: () => Promise | void; + /** + * Method to shutdown the store, stop timers, and release all resources. + */ + shutdown?: () => Promise | 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; + /** + * Optional value that the store prepends to keys + * + * Used by the double-count check to avoid false-positives when a key is counted twice, but with different prefixes + */ + prefix?: string; +}; +export type DraftHeadersVersion = (typeof SUPPORTED_DRAFT_VERSIONS)[number]; +/** + * Validate configuration object for enabling or disabling specific validations. + * + * The keys must also be keys in the validations object, except `enable`, `disable`, + * and `default`. + */ +export type EnabledValidations = { + [key in keyof Omit | "default"]?: boolean; +}; +/** + * The configuration options for the rate limiter. + */ +export type Options = { + /** + * How long we should remember the requests. + * + * Defaults to `60000` ms (= 1 minute). + */ + windowMs: number; + /** + * The maximum number of connections 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. + * + * Defaults to `5`. + */ + limit: number | ValueDeterminingMiddleware; + /** + * The response body to send back when a client is rate limited. + * + * Defaults to `'Too many requests, please try again later.'` + */ + message: any | ValueDeterminingMiddleware; + /** + * The HTTP status code to send back when a client is rate limited. + * + * Defaults to `HTTP 429 Too Many Requests` (RFC 6585). + */ + statusCode: number; + /** + * Whether to send `X-RateLimit-*` headers with the rate limit and the number + * of requests. + * + * Defaults to `true` (for backward compatibility). + */ + legacyHeaders: boolean; + /** + * Whether to enable support for the standardized rate limit headers (`RateLimit-*`). + * + * Defaults to `false` (for backward compatibility, but its use is recommended). + */ + standardHeaders: boolean | DraftHeadersVersion; + /** + * The name used to identify the quota policy in the `RateLimit` headers as per + * the 8th draft of the IETF specification. + * + * Defaults to `{limit}-in-{window}`. + */ + identifier: string | ValueDeterminingMiddleware; + /** + * The name of the property on the request object to store the rate limit info. + * + * Defaults to `rateLimit`. + */ + requestPropertyName: string; + /** + * If `true`, the library will (by default) skip all requests that have a 4XX + * or 5XX status. + * + * Defaults to `false`. + */ + skipFailedRequests: boolean; + /** + * If `true`, the library will (by default) skip all requests that have a + * status code less than 400. + * + * Defaults to `false`. + */ + skipSuccessfulRequests: boolean; + /** + * Method to generate custom identifiers for clients. + * + * By default, the client's IP address is used. + */ + keyGenerator: ValueDeterminingMiddleware; + /** + * Express request handler that sends back a response when a client is + * rate-limited. + * + * By default, sends back the `statusCode` and `message` set via the options. + */ + handler: RateLimitExceededEventHandler; + /** + * Method (in the form of middleware) to determine whether or not this request + * counts towards a client's quota. + * + * By default, skips no requests. + */ + skip: ValueDeterminingMiddleware; + /** + * Method to determine whether or not the request counts as 'succesful'. Used + * when either `skipSuccessfulRequests` or `skipFailedRequests` is set to true. + * + * By default, requests with a response status code less than 400 are considered + * successful. + */ + requestWasSuccessful: ValueDeterminingMiddleware; + /** + * The `Store` to use to store the hit count for each client. + * + * By default, the built-in `MemoryStore` will be used. + */ + store: Store | LegacyStore; + /** + * The list of validation checks that should run. + */ + validate: boolean | EnabledValidations; + /** + * 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; + /** + * The maximum number of connections 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. + * + * @deprecated 7.x - This option was renamed to `limit`. However, it will not + * be removed from the library in the foreseeable future. + */ + max?: number | ValueDeterminingMiddleware; + /** + * If the Store generates an error, allow the request to pass. + */ + passOnStoreError: boolean; +}; +/** + * The extended request object that includes information about the client's + * rate limit. + */ +export type AugmentedRequest = Request & { + [key: string]: RateLimitInfo; +}; +/** + * The rate limit related information for each client included in the + * Express request object. + */ +export type RateLimitInfo = { + limit: number; + used: number; + remaining: number; + resetTime: Date | undefined; +}; +/** + * + * Create an instance of IP rate-limiting middleware for Express. + * + * @param passedOptions {Options} - Options to configure the rate limiter. + * + * @returns {RateLimitRequestHandler} - The middleware that rate-limits clients based on your configuration. + * + * @public + */ +export declare const rateLimit: (passedOptions?: Partial) => RateLimitRequestHandler; +/** + * The record that stores information about a client - namely, how many times + * they have hit the endpoint, and when their hit count resets. + * + * Similar to `ClientRateLimitInfo`, except `resetTime` is a compulsory field. + */ +export type Client = { + totalHits: number; + resetTime: Date; +}; +/** + * A `Store` that stores the hit count for each client in memory. + * + * @public + */ +export declare class MemoryStore implements Store { + /** + * The duration of time before which all hit counts are reset (in milliseconds). + */ + windowMs: number; + /** + * These two maps store usage (requests) and reset time by key (for example, IP + * addresses or API keys). + * + * They are split into two to avoid having to iterate through the entire set to + * determine which ones need reset. Instead, `Client`s are moved from `previous` + * to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients + * left in `previous`, i.e., those that have not made any recent requests, are + * known to be expired and can be deleted in bulk. + */ + previous: Map; + current: Map; + /** + * A reference to the active timer. + */ + interval?: NodeJS.Timeout; + /** + * Confirmation that the keys incremented in once instance of MemoryStore + * cannot affect other instances. + */ + localKeys: boolean; + /** + * Method that initializes the store. + * + * @param options {Options} - The options used to setup the middleware. + */ + init(options: Options): void; + /** + * Method to fetch a client's hit count and reset time. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client. + * + * @public + */ + get(key: string): Promise; + /** + * Method to increment a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client. + * + * @public + */ + increment(key: string): Promise; + /** + * Method to decrement a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @public + */ + decrement(key: string): Promise; + /** + * Method to reset a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @public + */ + resetKey(key: string): Promise; + /** + * Method to reset everyone's hit counter. + * + * @public + */ + resetAll(): Promise; + /** + * Method to stop the timer (if currently running) and prevent any memory + * leaks. + * + * @public + */ + shutdown(): void; + /** + * Recycles a client by setting its hit count to zero, and reset time to + * `windowMs` milliseconds from now. + * + * NOT to be confused with `#resetKey()`, which removes a client from both the + * `current` and `previous` maps. + * + * @param client {Client} - The client to recycle. + * @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client. + * + * @return {Client} - The modified client that was passed in, to allow for chaining. + */ + private resetClient; + /** + * Retrieves or creates a client, given a key. Also ensures that the client being + * returned is in the `current` map. + * + * @param key {string} - The key under which the client is (or is to be) stored. + * + * @returns {Client} - The requested client. + */ + private getClient; + /** + * Move current clients to previous, create a new map for current. + * + * This function is called every `windowMs`. + */ + private clearExpired; +} + +export { + rateLimit as default, +}; + +export {}; diff --git a/node_modules/express-rate-limit/dist/index.d.mts b/node_modules/express-rate-limit/dist/index.d.mts new file mode 100644 index 0000000..62fe123 --- /dev/null +++ b/node_modules/express-rate-limit/dist/index.d.mts @@ -0,0 +1,584 @@ +// Generated by dts-bundle-generator v8.0.1 + +import { NextFunction, Request, RequestHandler, Response } from 'express'; + +declare const validations: { + enabled: { + [key: string]: boolean; + }; + disable(): void; + /** + * Checks whether the IP address is valid, and that it does not have a port + * number in it. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_invalid_ip_address. + * + * @param ip {string | undefined} - The IP address provided by Express as request.ip. + * + * @returns {void} + */ + ip(ip: string | undefined): void; + /** + * Makes sure the trust proxy setting is not set to `true`. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_permissive_trust_proxy. + * + * @param request {Request} - The Express request object. + * + * @returns {void} + */ + trustProxy(request: Request): void; + /** + * Makes sure the trust proxy setting is set in case the `X-Forwarded-For` + * header is present. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_unset_trust_proxy. + * + * @param request {Request} - The Express request object. + * + * @returns {void} + */ + xForwardedForHeader(request: Request): void; + /** + * Ensures totalHits value from store is a positive integer. + * + * @param hits {any} - The `totalHits` returned by the store. + */ + positiveHits(hits: any): void; + /** + * Ensures a single store instance is not used with multiple express-rate-limit instances + */ + unsharedStore(store: Store): void; + /** + * 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: Request, store: Store, key: string): void; + /** + * Warns the user that the behaviour for `max: 0` / `limit: 0` is + * changing in the next major release. + * + * @param limit {number} - The maximum number of hits per client. + * + * @returns {void} + */ + limit(limit: number): void; + /** + * Warns the user that the `draft_polli_ratelimit_headers` option is deprecated + * and will be removed in the next major release. + * + * @param draft_polli_ratelimit_headers {any | undefined} - The now-deprecated setting that was used to enable standard headers. + * + * @returns {void} + */ + draftPolliHeaders(draft_polli_ratelimit_headers?: any): void; + /** + * Warns the user that the `onLimitReached` option is deprecated and + * will be removed in the next major release. + * + * @param onLimitReached {any | undefined} - The maximum number of hits per client. + * + * @returns {void} + */ + onLimitReached(onLimitReached?: any): void; + /** + * Warns the user when an invalid/unsupported version of the draft spec is passed. + * + * @param version {any | undefined} - The version passed by the user. + * + * @returns {void} + */ + headersDraftVersion(version?: any): void; + /** + * Warns the user when the selected headers option requires a reset time but + * the store does not provide one. + * + * @param resetTime {Date | undefined} - The timestamp when the client's hit count will be reset. + * + * @returns {void} + */ + headersResetTime(resetTime?: Date): void; + /** + * Checks the options.validate setting to ensure that only recognized + * validations are enabled or disabled. + * + * If any unrecognized values are found, an error is logged that + * includes the list of supported vaidations. + */ + validationsConfig(): void; + /** + * Checks to see if the instance was created inside of a request handler, + * which would prevent it from working correctly, with the default memory + * store (or any other store with localKeys.) + */ + creationStack(store: Store): void; +}; +export type Validations = typeof validations; +declare const SUPPORTED_DRAFT_VERSIONS: string[]; +/** + * 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 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 type ValueDeterminingMiddleware = (request: Request, response: Response) => T | Promise; +/** + * 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 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 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 type ClientRateLimitInfo = { + totalHits: number; + resetTime: Date | undefined; +}; +export type IncrementResponse = ClientRateLimitInfo; +/** + * A modified Express request handler with the rate limit functions. + */ +export type RateLimitRequestHandler = RequestHandler & { + /** + * Method to reset a client's hit counter. + * + * @param key {string} - The identifier for a client. + */ + resetKey: (key: string) => void; + /** + * Method to fetch a client's hit count and reset time. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client. + */ + getKey: (key: string) => Promise | ClientRateLimitInfo | undefined; +}; +/** + * An interface that all hit counter stores must implement. + * + * @deprecated 6.x - Implement the `Store` interface instead. + */ +export type 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 type 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 fetch a client's hit count and reset time. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client. + */ + get?: (key: string) => Promise | ClientRateLimitInfo | undefined; + /** + * Method to increment a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @returns {IncrementResponse | undefined} - The number of hits and reset time for that client. + */ + increment: (key: string) => Promise | IncrementResponse; + /** + * Method to decrement a client's hit counter. + * + * @param key {string} - The identifier for a client. + */ + decrement: (key: string) => Promise | void; + /** + * Method to reset a client's hit counter. + * + * @param key {string} - The identifier for a client. + */ + resetKey: (key: string) => Promise | void; + /** + * Method to reset everyone's hit counter. + */ + resetAll?: () => Promise | void; + /** + * Method to shutdown the store, stop timers, and release all resources. + */ + shutdown?: () => Promise | 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; + /** + * Optional value that the store prepends to keys + * + * Used by the double-count check to avoid false-positives when a key is counted twice, but with different prefixes + */ + prefix?: string; +}; +export type DraftHeadersVersion = (typeof SUPPORTED_DRAFT_VERSIONS)[number]; +/** + * Validate configuration object for enabling or disabling specific validations. + * + * The keys must also be keys in the validations object, except `enable`, `disable`, + * and `default`. + */ +export type EnabledValidations = { + [key in keyof Omit | "default"]?: boolean; +}; +/** + * The configuration options for the rate limiter. + */ +export type Options = { + /** + * How long we should remember the requests. + * + * Defaults to `60000` ms (= 1 minute). + */ + windowMs: number; + /** + * The maximum number of connections 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. + * + * Defaults to `5`. + */ + limit: number | ValueDeterminingMiddleware; + /** + * The response body to send back when a client is rate limited. + * + * Defaults to `'Too many requests, please try again later.'` + */ + message: any | ValueDeterminingMiddleware; + /** + * The HTTP status code to send back when a client is rate limited. + * + * Defaults to `HTTP 429 Too Many Requests` (RFC 6585). + */ + statusCode: number; + /** + * Whether to send `X-RateLimit-*` headers with the rate limit and the number + * of requests. + * + * Defaults to `true` (for backward compatibility). + */ + legacyHeaders: boolean; + /** + * Whether to enable support for the standardized rate limit headers (`RateLimit-*`). + * + * Defaults to `false` (for backward compatibility, but its use is recommended). + */ + standardHeaders: boolean | DraftHeadersVersion; + /** + * The name used to identify the quota policy in the `RateLimit` headers as per + * the 8th draft of the IETF specification. + * + * Defaults to `{limit}-in-{window}`. + */ + identifier: string | ValueDeterminingMiddleware; + /** + * The name of the property on the request object to store the rate limit info. + * + * Defaults to `rateLimit`. + */ + requestPropertyName: string; + /** + * If `true`, the library will (by default) skip all requests that have a 4XX + * or 5XX status. + * + * Defaults to `false`. + */ + skipFailedRequests: boolean; + /** + * If `true`, the library will (by default) skip all requests that have a + * status code less than 400. + * + * Defaults to `false`. + */ + skipSuccessfulRequests: boolean; + /** + * Method to generate custom identifiers for clients. + * + * By default, the client's IP address is used. + */ + keyGenerator: ValueDeterminingMiddleware; + /** + * Express request handler that sends back a response when a client is + * rate-limited. + * + * By default, sends back the `statusCode` and `message` set via the options. + */ + handler: RateLimitExceededEventHandler; + /** + * Method (in the form of middleware) to determine whether or not this request + * counts towards a client's quota. + * + * By default, skips no requests. + */ + skip: ValueDeterminingMiddleware; + /** + * Method to determine whether or not the request counts as 'succesful'. Used + * when either `skipSuccessfulRequests` or `skipFailedRequests` is set to true. + * + * By default, requests with a response status code less than 400 are considered + * successful. + */ + requestWasSuccessful: ValueDeterminingMiddleware; + /** + * The `Store` to use to store the hit count for each client. + * + * By default, the built-in `MemoryStore` will be used. + */ + store: Store | LegacyStore; + /** + * The list of validation checks that should run. + */ + validate: boolean | EnabledValidations; + /** + * 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; + /** + * The maximum number of connections 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. + * + * @deprecated 7.x - This option was renamed to `limit`. However, it will not + * be removed from the library in the foreseeable future. + */ + max?: number | ValueDeterminingMiddleware; + /** + * If the Store generates an error, allow the request to pass. + */ + passOnStoreError: boolean; +}; +/** + * The extended request object that includes information about the client's + * rate limit. + */ +export type AugmentedRequest = Request & { + [key: string]: RateLimitInfo; +}; +/** + * The rate limit related information for each client included in the + * Express request object. + */ +export type RateLimitInfo = { + limit: number; + used: number; + remaining: number; + resetTime: Date | undefined; +}; +/** + * + * Create an instance of IP rate-limiting middleware for Express. + * + * @param passedOptions {Options} - Options to configure the rate limiter. + * + * @returns {RateLimitRequestHandler} - The middleware that rate-limits clients based on your configuration. + * + * @public + */ +export declare const rateLimit: (passedOptions?: Partial) => RateLimitRequestHandler; +/** + * The record that stores information about a client - namely, how many times + * they have hit the endpoint, and when their hit count resets. + * + * Similar to `ClientRateLimitInfo`, except `resetTime` is a compulsory field. + */ +export type Client = { + totalHits: number; + resetTime: Date; +}; +/** + * A `Store` that stores the hit count for each client in memory. + * + * @public + */ +export declare class MemoryStore implements Store { + /** + * The duration of time before which all hit counts are reset (in milliseconds). + */ + windowMs: number; + /** + * These two maps store usage (requests) and reset time by key (for example, IP + * addresses or API keys). + * + * They are split into two to avoid having to iterate through the entire set to + * determine which ones need reset. Instead, `Client`s are moved from `previous` + * to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients + * left in `previous`, i.e., those that have not made any recent requests, are + * known to be expired and can be deleted in bulk. + */ + previous: Map; + current: Map; + /** + * A reference to the active timer. + */ + interval?: NodeJS.Timeout; + /** + * Confirmation that the keys incremented in once instance of MemoryStore + * cannot affect other instances. + */ + localKeys: boolean; + /** + * Method that initializes the store. + * + * @param options {Options} - The options used to setup the middleware. + */ + init(options: Options): void; + /** + * Method to fetch a client's hit count and reset time. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client. + * + * @public + */ + get(key: string): Promise; + /** + * Method to increment a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client. + * + * @public + */ + increment(key: string): Promise; + /** + * Method to decrement a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @public + */ + decrement(key: string): Promise; + /** + * Method to reset a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @public + */ + resetKey(key: string): Promise; + /** + * Method to reset everyone's hit counter. + * + * @public + */ + resetAll(): Promise; + /** + * Method to stop the timer (if currently running) and prevent any memory + * leaks. + * + * @public + */ + shutdown(): void; + /** + * Recycles a client by setting its hit count to zero, and reset time to + * `windowMs` milliseconds from now. + * + * NOT to be confused with `#resetKey()`, which removes a client from both the + * `current` and `previous` maps. + * + * @param client {Client} - The client to recycle. + * @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client. + * + * @return {Client} - The modified client that was passed in, to allow for chaining. + */ + private resetClient; + /** + * Retrieves or creates a client, given a key. Also ensures that the client being + * returned is in the `current` map. + * + * @param key {string} - The key under which the client is (or is to be) stored. + * + * @returns {Client} - The requested client. + */ + private getClient; + /** + * Move current clients to previous, create a new map for current. + * + * This function is called every `windowMs`. + */ + private clearExpired; +} + +export { + rateLimit as default, +}; + +export {}; diff --git a/node_modules/express-rate-limit/dist/index.d.ts b/node_modules/express-rate-limit/dist/index.d.ts new file mode 100644 index 0000000..62fe123 --- /dev/null +++ b/node_modules/express-rate-limit/dist/index.d.ts @@ -0,0 +1,584 @@ +// Generated by dts-bundle-generator v8.0.1 + +import { NextFunction, Request, RequestHandler, Response } from 'express'; + +declare const validations: { + enabled: { + [key: string]: boolean; + }; + disable(): void; + /** + * Checks whether the IP address is valid, and that it does not have a port + * number in it. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_invalid_ip_address. + * + * @param ip {string | undefined} - The IP address provided by Express as request.ip. + * + * @returns {void} + */ + ip(ip: string | undefined): void; + /** + * Makes sure the trust proxy setting is not set to `true`. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_permissive_trust_proxy. + * + * @param request {Request} - The Express request object. + * + * @returns {void} + */ + trustProxy(request: Request): void; + /** + * Makes sure the trust proxy setting is set in case the `X-Forwarded-For` + * header is present. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_unset_trust_proxy. + * + * @param request {Request} - The Express request object. + * + * @returns {void} + */ + xForwardedForHeader(request: Request): void; + /** + * Ensures totalHits value from store is a positive integer. + * + * @param hits {any} - The `totalHits` returned by the store. + */ + positiveHits(hits: any): void; + /** + * Ensures a single store instance is not used with multiple express-rate-limit instances + */ + unsharedStore(store: Store): void; + /** + * 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: Request, store: Store, key: string): void; + /** + * Warns the user that the behaviour for `max: 0` / `limit: 0` is + * changing in the next major release. + * + * @param limit {number} - The maximum number of hits per client. + * + * @returns {void} + */ + limit(limit: number): void; + /** + * Warns the user that the `draft_polli_ratelimit_headers` option is deprecated + * and will be removed in the next major release. + * + * @param draft_polli_ratelimit_headers {any | undefined} - The now-deprecated setting that was used to enable standard headers. + * + * @returns {void} + */ + draftPolliHeaders(draft_polli_ratelimit_headers?: any): void; + /** + * Warns the user that the `onLimitReached` option is deprecated and + * will be removed in the next major release. + * + * @param onLimitReached {any | undefined} - The maximum number of hits per client. + * + * @returns {void} + */ + onLimitReached(onLimitReached?: any): void; + /** + * Warns the user when an invalid/unsupported version of the draft spec is passed. + * + * @param version {any | undefined} - The version passed by the user. + * + * @returns {void} + */ + headersDraftVersion(version?: any): void; + /** + * Warns the user when the selected headers option requires a reset time but + * the store does not provide one. + * + * @param resetTime {Date | undefined} - The timestamp when the client's hit count will be reset. + * + * @returns {void} + */ + headersResetTime(resetTime?: Date): void; + /** + * Checks the options.validate setting to ensure that only recognized + * validations are enabled or disabled. + * + * If any unrecognized values are found, an error is logged that + * includes the list of supported vaidations. + */ + validationsConfig(): void; + /** + * Checks to see if the instance was created inside of a request handler, + * which would prevent it from working correctly, with the default memory + * store (or any other store with localKeys.) + */ + creationStack(store: Store): void; +}; +export type Validations = typeof validations; +declare const SUPPORTED_DRAFT_VERSIONS: string[]; +/** + * 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 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 type ValueDeterminingMiddleware = (request: Request, response: Response) => T | Promise; +/** + * 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 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 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 type ClientRateLimitInfo = { + totalHits: number; + resetTime: Date | undefined; +}; +export type IncrementResponse = ClientRateLimitInfo; +/** + * A modified Express request handler with the rate limit functions. + */ +export type RateLimitRequestHandler = RequestHandler & { + /** + * Method to reset a client's hit counter. + * + * @param key {string} - The identifier for a client. + */ + resetKey: (key: string) => void; + /** + * Method to fetch a client's hit count and reset time. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client. + */ + getKey: (key: string) => Promise | ClientRateLimitInfo | undefined; +}; +/** + * An interface that all hit counter stores must implement. + * + * @deprecated 6.x - Implement the `Store` interface instead. + */ +export type 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 type 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 fetch a client's hit count and reset time. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client. + */ + get?: (key: string) => Promise | ClientRateLimitInfo | undefined; + /** + * Method to increment a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @returns {IncrementResponse | undefined} - The number of hits and reset time for that client. + */ + increment: (key: string) => Promise | IncrementResponse; + /** + * Method to decrement a client's hit counter. + * + * @param key {string} - The identifier for a client. + */ + decrement: (key: string) => Promise | void; + /** + * Method to reset a client's hit counter. + * + * @param key {string} - The identifier for a client. + */ + resetKey: (key: string) => Promise | void; + /** + * Method to reset everyone's hit counter. + */ + resetAll?: () => Promise | void; + /** + * Method to shutdown the store, stop timers, and release all resources. + */ + shutdown?: () => Promise | 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; + /** + * Optional value that the store prepends to keys + * + * Used by the double-count check to avoid false-positives when a key is counted twice, but with different prefixes + */ + prefix?: string; +}; +export type DraftHeadersVersion = (typeof SUPPORTED_DRAFT_VERSIONS)[number]; +/** + * Validate configuration object for enabling or disabling specific validations. + * + * The keys must also be keys in the validations object, except `enable`, `disable`, + * and `default`. + */ +export type EnabledValidations = { + [key in keyof Omit | "default"]?: boolean; +}; +/** + * The configuration options for the rate limiter. + */ +export type Options = { + /** + * How long we should remember the requests. + * + * Defaults to `60000` ms (= 1 minute). + */ + windowMs: number; + /** + * The maximum number of connections 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. + * + * Defaults to `5`. + */ + limit: number | ValueDeterminingMiddleware; + /** + * The response body to send back when a client is rate limited. + * + * Defaults to `'Too many requests, please try again later.'` + */ + message: any | ValueDeterminingMiddleware; + /** + * The HTTP status code to send back when a client is rate limited. + * + * Defaults to `HTTP 429 Too Many Requests` (RFC 6585). + */ + statusCode: number; + /** + * Whether to send `X-RateLimit-*` headers with the rate limit and the number + * of requests. + * + * Defaults to `true` (for backward compatibility). + */ + legacyHeaders: boolean; + /** + * Whether to enable support for the standardized rate limit headers (`RateLimit-*`). + * + * Defaults to `false` (for backward compatibility, but its use is recommended). + */ + standardHeaders: boolean | DraftHeadersVersion; + /** + * The name used to identify the quota policy in the `RateLimit` headers as per + * the 8th draft of the IETF specification. + * + * Defaults to `{limit}-in-{window}`. + */ + identifier: string | ValueDeterminingMiddleware; + /** + * The name of the property on the request object to store the rate limit info. + * + * Defaults to `rateLimit`. + */ + requestPropertyName: string; + /** + * If `true`, the library will (by default) skip all requests that have a 4XX + * or 5XX status. + * + * Defaults to `false`. + */ + skipFailedRequests: boolean; + /** + * If `true`, the library will (by default) skip all requests that have a + * status code less than 400. + * + * Defaults to `false`. + */ + skipSuccessfulRequests: boolean; + /** + * Method to generate custom identifiers for clients. + * + * By default, the client's IP address is used. + */ + keyGenerator: ValueDeterminingMiddleware; + /** + * Express request handler that sends back a response when a client is + * rate-limited. + * + * By default, sends back the `statusCode` and `message` set via the options. + */ + handler: RateLimitExceededEventHandler; + /** + * Method (in the form of middleware) to determine whether or not this request + * counts towards a client's quota. + * + * By default, skips no requests. + */ + skip: ValueDeterminingMiddleware; + /** + * Method to determine whether or not the request counts as 'succesful'. Used + * when either `skipSuccessfulRequests` or `skipFailedRequests` is set to true. + * + * By default, requests with a response status code less than 400 are considered + * successful. + */ + requestWasSuccessful: ValueDeterminingMiddleware; + /** + * The `Store` to use to store the hit count for each client. + * + * By default, the built-in `MemoryStore` will be used. + */ + store: Store | LegacyStore; + /** + * The list of validation checks that should run. + */ + validate: boolean | EnabledValidations; + /** + * 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; + /** + * The maximum number of connections 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. + * + * @deprecated 7.x - This option was renamed to `limit`. However, it will not + * be removed from the library in the foreseeable future. + */ + max?: number | ValueDeterminingMiddleware; + /** + * If the Store generates an error, allow the request to pass. + */ + passOnStoreError: boolean; +}; +/** + * The extended request object that includes information about the client's + * rate limit. + */ +export type AugmentedRequest = Request & { + [key: string]: RateLimitInfo; +}; +/** + * The rate limit related information for each client included in the + * Express request object. + */ +export type RateLimitInfo = { + limit: number; + used: number; + remaining: number; + resetTime: Date | undefined; +}; +/** + * + * Create an instance of IP rate-limiting middleware for Express. + * + * @param passedOptions {Options} - Options to configure the rate limiter. + * + * @returns {RateLimitRequestHandler} - The middleware that rate-limits clients based on your configuration. + * + * @public + */ +export declare const rateLimit: (passedOptions?: Partial) => RateLimitRequestHandler; +/** + * The record that stores information about a client - namely, how many times + * they have hit the endpoint, and when their hit count resets. + * + * Similar to `ClientRateLimitInfo`, except `resetTime` is a compulsory field. + */ +export type Client = { + totalHits: number; + resetTime: Date; +}; +/** + * A `Store` that stores the hit count for each client in memory. + * + * @public + */ +export declare class MemoryStore implements Store { + /** + * The duration of time before which all hit counts are reset (in milliseconds). + */ + windowMs: number; + /** + * These two maps store usage (requests) and reset time by key (for example, IP + * addresses or API keys). + * + * They are split into two to avoid having to iterate through the entire set to + * determine which ones need reset. Instead, `Client`s are moved from `previous` + * to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients + * left in `previous`, i.e., those that have not made any recent requests, are + * known to be expired and can be deleted in bulk. + */ + previous: Map; + current: Map; + /** + * A reference to the active timer. + */ + interval?: NodeJS.Timeout; + /** + * Confirmation that the keys incremented in once instance of MemoryStore + * cannot affect other instances. + */ + localKeys: boolean; + /** + * Method that initializes the store. + * + * @param options {Options} - The options used to setup the middleware. + */ + init(options: Options): void; + /** + * Method to fetch a client's hit count and reset time. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client. + * + * @public + */ + get(key: string): Promise; + /** + * Method to increment a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client. + * + * @public + */ + increment(key: string): Promise; + /** + * Method to decrement a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @public + */ + decrement(key: string): Promise; + /** + * Method to reset a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @public + */ + resetKey(key: string): Promise; + /** + * Method to reset everyone's hit counter. + * + * @public + */ + resetAll(): Promise; + /** + * Method to stop the timer (if currently running) and prevent any memory + * leaks. + * + * @public + */ + shutdown(): void; + /** + * Recycles a client by setting its hit count to zero, and reset time to + * `windowMs` milliseconds from now. + * + * NOT to be confused with `#resetKey()`, which removes a client from both the + * `current` and `previous` maps. + * + * @param client {Client} - The client to recycle. + * @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client. + * + * @return {Client} - The modified client that was passed in, to allow for chaining. + */ + private resetClient; + /** + * Retrieves or creates a client, given a key. Also ensures that the client being + * returned is in the `current` map. + * + * @param key {string} - The key under which the client is (or is to be) stored. + * + * @returns {Client} - The requested client. + */ + private getClient; + /** + * Move current clients to previous, create a new map for current. + * + * This function is called every `windowMs`. + */ + private clearExpired; +} + +export { + rateLimit as default, +}; + +export {}; diff --git a/node_modules/express-rate-limit/dist/index.mjs b/node_modules/express-rate-limit/dist/index.mjs new file mode 100644 index 0000000..f8b70ce --- /dev/null +++ b/node_modules/express-rate-limit/dist/index.mjs @@ -0,0 +1,809 @@ +// source/headers.ts +import { Buffer } from "buffer"; +import { createHash } from "crypto"; +var SUPPORTED_DRAFT_VERSIONS = ["draft-6", "draft-7", "draft-8"]; +var getResetSeconds = (resetTime, windowMs) => { + let resetSeconds = void 0; + if (resetTime) { + const deltaSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1e3); + resetSeconds = Math.max(0, deltaSeconds); + } else if (windowMs) { + resetSeconds = Math.ceil(windowMs / 1e3); + } + return resetSeconds; +}; +var getPartitionKey = (key) => { + const hash = createHash("sha256"); + hash.update(key); + const partitionKey = hash.digest("hex").slice(0, 12); + return Buffer.from(partitionKey).toString("base64"); +}; +var setLegacyHeaders = (response, info) => { + if (response.headersSent) + return; + response.setHeader("X-RateLimit-Limit", info.limit.toString()); + response.setHeader("X-RateLimit-Remaining", info.remaining.toString()); + if (info.resetTime instanceof Date) { + response.setHeader("Date", (/* @__PURE__ */ new Date()).toUTCString()); + response.setHeader( + "X-RateLimit-Reset", + Math.ceil(info.resetTime.getTime() / 1e3).toString() + ); + } +}; +var setDraft6Headers = (response, info, windowMs) => { + if (response.headersSent) + return; + const windowSeconds = Math.ceil(windowMs / 1e3); + const resetSeconds = getResetSeconds(info.resetTime); + response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`); + response.setHeader("RateLimit-Limit", info.limit.toString()); + response.setHeader("RateLimit-Remaining", info.remaining.toString()); + if (resetSeconds) + response.setHeader("RateLimit-Reset", resetSeconds.toString()); +}; +var setDraft7Headers = (response, info, windowMs) => { + if (response.headersSent) + return; + const windowSeconds = Math.ceil(windowMs / 1e3); + const resetSeconds = getResetSeconds(info.resetTime, windowMs); + response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`); + response.setHeader( + "RateLimit", + `limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}` + ); +}; +var setDraft8Headers = (response, info, windowMs, name, key) => { + if (response.headersSent) + return; + const windowSeconds = Math.ceil(windowMs / 1e3); + const resetSeconds = getResetSeconds(info.resetTime, windowMs); + const partitionKey = getPartitionKey(key); + const policy = `q=${info.limit}; w=${windowSeconds}; pk=:${partitionKey}:`; + const header = `r=${info.remaining}; t=${resetSeconds}`; + response.append("RateLimit-Policy", `"${name}"; ${policy}`); + response.append("RateLimit", `"${name}"; ${header}`); +}; +var setRetryAfterHeader = (response, info, windowMs) => { + if (response.headersSent) + return; + const resetSeconds = getResetSeconds(info.resetTime, windowMs); + response.setHeader("Retry-After", resetSeconds.toString()); +}; + +// source/validations.ts +import { isIP } from "net"; +var ValidationError = class extends Error { + /** + * The code must be a string, in snake case and all capital, that starts with + * the substring `ERR_ERL_`. + * + * The message must be a string, starting with an uppercase character, + * describing the issue in detail. + */ + constructor(code, message) { + const url = `https://express-rate-limit.github.io/${code}/`; + super(`${message} See ${url} for more information.`); + this.name = this.constructor.name; + this.code = code; + this.help = url; + } +}; +var ChangeWarning = class extends ValidationError { +}; +var usedStores = /* @__PURE__ */ new Set(); +var singleCountKeys = /* @__PURE__ */ new WeakMap(); +var validations = { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + enabled: { + default: true + }, + // Should be EnabledValidations type, but that's a circular reference + disable() { + for (const k of Object.keys(this.enabled)) + this.enabled[k] = false; + }, + /** + * Checks whether the IP address is valid, and that it does not have a port + * number in it. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_invalid_ip_address. + * + * @param ip {string | undefined} - The IP address provided by Express as request.ip. + * + * @returns {void} + */ + ip(ip) { + if (ip === void 0) { + throw new ValidationError( + "ERR_ERL_UNDEFINED_IP_ADDRESS", + `An undefined 'request.ip' was detected. This might indicate a misconfiguration or the connection being destroyed prematurely.` + ); + } + if (!isIP(ip)) { + throw new ValidationError( + "ERR_ERL_INVALID_IP_ADDRESS", + `An invalid 'request.ip' (${ip}) was detected. Consider passing a custom 'keyGenerator' function to the rate limiter.` + ); + } + }, + /** + * Makes sure the trust proxy setting is not set to `true`. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_permissive_trust_proxy. + * + * @param request {Request} - The Express request object. + * + * @returns {void} + */ + trustProxy(request) { + if (request.app.get("trust proxy") === true) { + throw new ValidationError( + "ERR_ERL_PERMISSIVE_TRUST_PROXY", + `The Express 'trust proxy' setting is true, which allows anyone to trivially bypass IP-based rate limiting.` + ); + } + }, + /** + * Makes sure the trust proxy setting is set in case the `X-Forwarded-For` + * header is present. + * + * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_unset_trust_proxy. + * + * @param request {Request} - The Express request object. + * + * @returns {void} + */ + xForwardedForHeader(request) { + if (request.headers["x-forwarded-for"] && request.app.get("trust proxy") === false) { + throw new ValidationError( + "ERR_ERL_UNEXPECTED_X_FORWARDED_FOR", + `The 'X-Forwarded-For' header is set but the Express 'trust proxy' setting is false (default). This could indicate a misconfiguration which would prevent express-rate-limit from accurately identifying users.` + ); + } + }, + /** + * Ensures totalHits value from store is a positive integer. + * + * @param hits {any} - The `totalHits` returned by the store. + */ + positiveHits(hits) { + if (typeof hits !== "number" || hits < 1 || hits !== Math.round(hits)) { + throw new ValidationError( + "ERR_ERL_INVALID_HITS", + `The totalHits value returned from the store must be a positive integer, got ${hits}` + ); + } + }, + /** + * Ensures a single store instance is not used with multiple express-rate-limit instances + */ + unsharedStore(store) { + if (usedStores.has(store)) { + const maybeUniquePrefix = store?.localKeys ? "" : " (with a unique prefix)"; + throw new ValidationError( + "ERR_ERL_STORE_REUSE", + `A Store instance must not be shared across multiple rate limiters. Create a new instance of ${store.constructor.name}${maybeUniquePrefix} for each limiter instead.` + ); + } + usedStores.add(store); + }, + /** + * 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) { + let storeKeys = singleCountKeys.get(request); + if (!storeKeys) { + storeKeys = /* @__PURE__ */ new Map(); + singleCountKeys.set(request, storeKeys); + } + const storeKey = store.localKeys ? store : store.constructor.name; + let keys = storeKeys.get(storeKey); + if (!keys) { + keys = []; + storeKeys.set(storeKey, keys); + } + const prefixedKey = `${store.prefix ?? ""}${key}`; + if (keys.includes(prefixedKey)) { + throw new ValidationError( + "ERR_ERL_DOUBLE_COUNT", + `The hit count for ${key} was incremented more than once for a single request.` + ); + } + keys.push(prefixedKey); + }, + /** + * Warns the user that the behaviour for `max: 0` / `limit: 0` is + * changing in the next major release. + * + * @param limit {number} - The maximum number of hits per client. + * + * @returns {void} + */ + limit(limit) { + if (limit === 0) { + throw new ChangeWarning( + "WRN_ERL_MAX_ZERO", + `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` + ); + } + }, + /** + * Warns the user that the `draft_polli_ratelimit_headers` option is deprecated + * and will be removed in the next major release. + * + * @param draft_polli_ratelimit_headers {any | undefined} - The now-deprecated setting that was used to enable standard headers. + * + * @returns {void} + */ + draftPolliHeaders(draft_polli_ratelimit_headers) { + if (draft_polli_ratelimit_headers) { + throw new ChangeWarning( + "WRN_ERL_DEPRECATED_DRAFT_POLLI_HEADERS", + `The draft_polli_ratelimit_headers configuration option is deprecated and has been removed in express-rate-limit v7, please set standardHeaders: 'draft-6' instead.` + ); + } + }, + /** + * Warns the user that the `onLimitReached` option is deprecated and + * will be removed in the next major release. + * + * @param onLimitReached {any | undefined} - The maximum number of hits per client. + * + * @returns {void} + */ + onLimitReached(onLimitReached) { + if (onLimitReached) { + throw new ChangeWarning( + "WRN_ERL_DEPRECATED_ON_LIMIT_REACHED", + `The onLimitReached configuration option is deprecated and has been removed in express-rate-limit v7.` + ); + } + }, + /** + * Warns the user when an invalid/unsupported version of the draft spec is passed. + * + * @param version {any | undefined} - The version passed by the user. + * + * @returns {void} + */ + headersDraftVersion(version) { + if (typeof version !== "string" || !SUPPORTED_DRAFT_VERSIONS.includes(version)) { + const versionString = SUPPORTED_DRAFT_VERSIONS.join(", "); + throw new ValidationError( + "ERR_ERL_HEADERS_UNSUPPORTED_DRAFT_VERSION", + `standardHeaders: only the following versions of the IETF draft specification are supported: ${versionString}.` + ); + } + }, + /** + * Warns the user when the selected headers option requires a reset time but + * the store does not provide one. + * + * @param resetTime {Date | undefined} - The timestamp when the client's hit count will be reset. + * + * @returns {void} + */ + headersResetTime(resetTime) { + if (!resetTime) { + throw new ValidationError( + "ERR_ERL_HEADERS_NO_RESET", + `standardHeaders: 'draft-7' requires a 'resetTime', but the store did not provide one. The 'windowMs' value will be used instead, which may cause clients to wait longer than necessary.` + ); + } + }, + /** + * Checks the options.validate setting to ensure that only recognized + * validations are enabled or disabled. + * + * If any unrecognized values are found, an error is logged that + * includes the list of supported vaidations. + */ + validationsConfig() { + const supportedValidations = Object.keys(this).filter( + (k) => !["enabled", "disable"].includes(k) + ); + supportedValidations.push("default"); + for (const key of Object.keys(this.enabled)) { + if (!supportedValidations.includes(key)) { + throw new ValidationError( + "ERR_ERL_UNKNOWN_VALIDATION", + `options.validate.${key} is not recognized. Supported validate options are: ${supportedValidations.join( + ", " + )}.` + ); + } + } + }, + /** + * Checks to see if the instance was created inside of a request handler, + * which would prevent it from working correctly, with the default memory + * store (or any other store with localKeys.) + */ + creationStack(store) { + const { stack } = new Error( + "express-rate-limit validation check (set options.validate.creationStack=false to disable)" + ); + if (stack?.includes("Layer.handle [as handle_request]")) { + if (!store.localKeys) { + throw new ValidationError( + "ERR_ERL_CREATED_IN_REQUEST_HANDLER", + "express-rate-limit instance should *usually* be created at app initialization, not when responding to a request." + ); + } + throw new ValidationError( + "ERR_ERL_CREATED_IN_REQUEST_HANDLER", + `express-rate-limit instance should be created at app initialization, not when responding to a request.` + ); + } + } +}; +var getValidations = (_enabled) => { + let enabled; + if (typeof _enabled === "boolean") { + enabled = { + default: _enabled + }; + } else { + enabled = { + default: true, + ..._enabled + }; + } + const wrappedValidations = { + enabled + }; + for (const [name, validation] of Object.entries(validations)) { + if (typeof validation === "function") + wrappedValidations[name] = (...args) => { + if (!(enabled[name] ?? enabled.default)) { + return; + } + try { + ; + validation.apply( + wrappedValidations, + args + ); + } catch (error) { + if (error instanceof ChangeWarning) + console.warn(error); + else + console.error(error); + } + }; + } + return wrappedValidations; +}; + +// source/memory-store.ts +var MemoryStore = class { + constructor() { + /** + * These two maps store usage (requests) and reset time by key (for example, IP + * addresses or API keys). + * + * They are split into two to avoid having to iterate through the entire set to + * determine which ones need reset. Instead, `Client`s are moved from `previous` + * to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients + * left in `previous`, i.e., those that have not made any recent requests, are + * known to be expired and can be deleted in bulk. + */ + this.previous = /* @__PURE__ */ new Map(); + this.current = /* @__PURE__ */ new Map(); + /** + * Confirmation that the keys incremented in once instance of MemoryStore + * cannot affect other instances. + */ + this.localKeys = true; + } + /** + * Method that initializes the store. + * + * @param options {Options} - The options used to setup the middleware. + */ + init(options) { + this.windowMs = options.windowMs; + if (this.interval) + clearInterval(this.interval); + this.interval = setInterval(() => { + this.clearExpired(); + }, this.windowMs); + if (this.interval.unref) + this.interval.unref(); + } + /** + * Method to fetch a client's hit count and reset time. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client. + * + * @public + */ + async get(key) { + return this.current.get(key) ?? this.previous.get(key); + } + /** + * Method to increment a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client. + * + * @public + */ + async increment(key) { + const client = this.getClient(key); + const now = Date.now(); + if (client.resetTime.getTime() <= now) { + this.resetClient(client, now); + } + client.totalHits++; + return client; + } + /** + * Method to decrement a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @public + */ + async decrement(key) { + const client = this.getClient(key); + if (client.totalHits > 0) + client.totalHits--; + } + /** + * Method to reset a client's hit counter. + * + * @param key {string} - The identifier for a client. + * + * @public + */ + async resetKey(key) { + this.current.delete(key); + this.previous.delete(key); + } + /** + * Method to reset everyone's hit counter. + * + * @public + */ + async resetAll() { + this.current.clear(); + this.previous.clear(); + } + /** + * Method to stop the timer (if currently running) and prevent any memory + * leaks. + * + * @public + */ + shutdown() { + clearInterval(this.interval); + void this.resetAll(); + } + /** + * Recycles a client by setting its hit count to zero, and reset time to + * `windowMs` milliseconds from now. + * + * NOT to be confused with `#resetKey()`, which removes a client from both the + * `current` and `previous` maps. + * + * @param client {Client} - The client to recycle. + * @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client. + * + * @return {Client} - The modified client that was passed in, to allow for chaining. + */ + resetClient(client, now = Date.now()) { + client.totalHits = 0; + client.resetTime.setTime(now + this.windowMs); + return client; + } + /** + * Retrieves or creates a client, given a key. Also ensures that the client being + * returned is in the `current` map. + * + * @param key {string} - The key under which the client is (or is to be) stored. + * + * @returns {Client} - The requested client. + */ + getClient(key) { + if (this.current.has(key)) + return this.current.get(key); + let client; + if (this.previous.has(key)) { + client = this.previous.get(key); + this.previous.delete(key); + } else { + client = { totalHits: 0, resetTime: /* @__PURE__ */ new Date() }; + this.resetClient(client); + } + this.current.set(key, client); + return client; + } + /** + * Move current clients to previous, create a new map for current. + * + * This function is called every `windowMs`. + */ + clearExpired() { + this.previous = this.current; + this.current = /* @__PURE__ */ new Map(); + } +}; + +// source/lib.ts +var isLegacyStore = (store) => ( + // Check that `incr` exists but `increment` does not - store authors might want + // to keep both around for backwards compatibility. + 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 legacyStore.decrement(key); + } + async resetKey(key) { + return legacyStore.resetKey(key); + } + /* istanbul ignore next */ + async resetAll() { + if (typeof legacyStore.resetAll === "function") + return legacyStore.resetAll(); + } + } + return new PromisifiedStore(); +}; +var getOptionsFromConfig = (config) => { + const { validations: validations2, ...directlyPassableEntries } = config; + return { + ...directlyPassableEntries, + validate: validations2.enabled + }; +}; +var omitUndefinedOptions = (passedOptions) => { + const omittedOptions = {}; + for (const k of Object.keys(passedOptions)) { + const key = k; + if (passedOptions[key] !== void 0) { + omittedOptions[key] = passedOptions[key]; + } + } + return omittedOptions; +}; +var parseOptions = (passedOptions) => { + const notUndefinedOptions = omitUndefinedOptions(passedOptions); + const validations2 = getValidations(notUndefinedOptions?.validate ?? true); + validations2.validationsConfig(); + validations2.draftPolliHeaders( + // @ts-expect-error see the note above. + notUndefinedOptions.draft_polli_ratelimit_headers + ); + validations2.onLimitReached(notUndefinedOptions.onLimitReached); + let standardHeaders = notUndefinedOptions.standardHeaders ?? false; + if (standardHeaders === true) + standardHeaders = "draft-6"; + const config = { + windowMs: 60 * 1e3, + limit: passedOptions.max ?? 5, + // `max` is deprecated, but support it anyways. + message: "Too many requests, please try again later.", + statusCode: 429, + legacyHeaders: passedOptions.headers ?? true, + identifier(request, _response) { + let duration = ""; + const property = config.requestPropertyName; + const { limit } = request[property]; + const seconds = config.windowMs / 1e3; + const minutes = config.windowMs / (1e3 * 60); + const hours = config.windowMs / (1e3 * 60 * 60); + const days = config.windowMs / (1e3 * 60 * 60 * 24); + if (seconds < 60) + duration = `${seconds}sec`; + else if (minutes < 60) + duration = `${minutes}min`; + else if (hours < 24) + duration = `${hours}hr${hours > 1 ? "s" : ""}`; + else + duration = `${days}day${days > 1 ? "s" : ""}`; + return `${limit}-in-${duration}`; + }, + requestPropertyName: "rateLimit", + skipFailedRequests: false, + skipSuccessfulRequests: false, + requestWasSuccessful: (_request, response) => response.statusCode < 400, + skip: (_request, _response) => false, + keyGenerator(request, _response) { + validations2.ip(request.ip); + validations2.trustProxy(request); + validations2.xForwardedForHeader(request); + return request.ip; + }, + async handler(request, response, _next, _optionsUsed) { + response.status(config.statusCode); + const message = typeof config.message === "function" ? await config.message( + request, + response + ) : config.message; + if (!response.writableEnded) { + response.send(message); + } + }, + passOnStoreError: false, + // Allow the default options to be overriden by the passed options. + ...notUndefinedOptions, + // `standardHeaders` is resolved into a draft version above, use that. + standardHeaders, + // Note that this field is declared after the user's options are spread in, + // so that this field doesn't get overriden with an un-promisified store! + store: promisifyStore(notUndefinedOptions.store ?? new MemoryStore()), + // Print an error to the console if a few known misconfigurations are detected. + validations: validations2 + }; + if (typeof config.store.increment !== "function" || typeof config.store.decrement !== "function" || typeof config.store.resetKey !== "function" || config.store.resetAll !== void 0 && typeof config.store.resetAll !== "function" || config.store.init !== void 0 && typeof config.store.init !== "function") { + throw new TypeError( + "An invalid store was passed. Please ensure that the store is a class that implements the `Store` interface." + ); + } + return config; +}; +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 config = parseOptions(passedOptions ?? {}); + const options = getOptionsFromConfig(config); + config.validations.creationStack(config.store); + config.validations.unsharedStore(config.store); + if (typeof config.store.init === "function") + config.store.init(options); + const middleware = handleAsyncErrors( + async (request, response, next) => { + const skip = await config.skip(request, response); + if (skip) { + next(); + return; + } + const augmentedRequest = request; + const key = await config.keyGenerator(request, response); + let totalHits = 0; + let resetTime; + try { + const incrementResult = await config.store.increment(key); + totalHits = incrementResult.totalHits; + resetTime = incrementResult.resetTime; + } catch (error) { + if (config.passOnStoreError) { + console.error( + "express-rate-limit: error from store, allowing request without rate-limiting.", + error + ); + next(); + return; + } + throw error; + } + config.validations.positiveHits(totalHits); + config.validations.singleCount(request, config.store, key); + const retrieveLimit = typeof config.limit === "function" ? config.limit(request, response) : config.limit; + const limit = await retrieveLimit; + config.validations.limit(limit); + const info = { + limit, + used: totalHits, + remaining: Math.max(limit - totalHits, 0), + resetTime + }; + Object.defineProperty(info, "current", { + configurable: false, + enumerable: false, + value: totalHits + }); + augmentedRequest[config.requestPropertyName] = info; + if (config.legacyHeaders && !response.headersSent) { + setLegacyHeaders(response, info); + } + if (config.standardHeaders && !response.headersSent) { + switch (config.standardHeaders) { + case "draft-6": { + setDraft6Headers(response, info, config.windowMs); + break; + } + case "draft-7": { + config.validations.headersResetTime(info.resetTime); + setDraft7Headers(response, info, config.windowMs); + break; + } + case "draft-8": { + const retrieveName = typeof config.identifier === "function" ? config.identifier(request, response) : config.identifier; + const name = await retrieveName; + config.validations.headersResetTime(info.resetTime); + setDraft8Headers(response, info, config.windowMs, name, key); + break; + } + default: { + config.validations.headersDraftVersion(config.standardHeaders); + break; + } + } + } + if (config.skipFailedRequests || config.skipSuccessfulRequests) { + let decremented = false; + const decrementKey = async () => { + if (!decremented) { + await config.store.decrement(key); + decremented = true; + } + }; + if (config.skipFailedRequests) { + response.on("finish", async () => { + if (!await config.requestWasSuccessful(request, response)) + await decrementKey(); + }); + response.on("close", async () => { + if (!response.writableEnded) + await decrementKey(); + }); + response.on("error", async () => { + await decrementKey(); + }); + } + if (config.skipSuccessfulRequests) { + response.on("finish", async () => { + if (await config.requestWasSuccessful(request, response)) + await decrementKey(); + }); + } + } + config.validations.disable(); + if (totalHits > limit) { + if (config.legacyHeaders || config.standardHeaders) { + setRetryAfterHeader(response, info, config.windowMs); + } + config.handler(request, response, next, options); + return; + } + next(); + } + ); + const getThrowFn = () => { + throw new Error("The current store does not support the get/getKey method"); + }; + middleware.resetKey = config.store.resetKey.bind(config.store); + middleware.getKey = typeof config.store.get === "function" ? config.store.get.bind(config.store) : getThrowFn; + return middleware; +}; +var lib_default = rateLimit; +export { + MemoryStore, + lib_default as default, + lib_default as rateLimit +}; diff --git a/node_modules/express-rate-limit/license.md b/node_modules/express-rate-limit/license.md new file mode 100644 index 0000000..468dc35 --- /dev/null +++ b/node_modules/express-rate-limit/license.md @@ -0,0 +1,20 @@ +# MIT License + +Copyright 2023 Nathan Friedly, Vedant K + +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. diff --git a/node_modules/express-rate-limit/package.json b/node_modules/express-rate-limit/package.json new file mode 100644 index 0000000..1783725 --- /dev/null +++ b/node_modules/express-rate-limit/package.json @@ -0,0 +1,133 @@ +{ + "name": "express-rate-limit", + "version": "7.5.0", + "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/express-rate-limit/express-rate-limit", + "repository": { + "type": "git", + "url": "git+https://github.com/express-rate-limit/express-rate-limit.git" + }, + "funding": "https://github.com/sponsors/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": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + } + }, + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "files": [ + "dist/", + "tsconfig.json" + ], + "engines": { + "node": ">= 16" + }, + "scripts": { + "clean": "del-cli dist/ coverage/ *.log *.tmp *.bak *.tgz", + "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", + "build:esm": "esbuild --platform=node --bundle --target=es2022 --format=esm --outfile=dist/index.mjs source/index.ts", + "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", + "compile": "run-s clean build:*", + "docs": "cd docs && mintlify dev", + "lint:code": "xo", + "lint:rest": "prettier --check .", + "lint": "run-s lint:*", + "format:code": "xo --fix", + "format:rest": "prettier --write .", + "format": "run-s format:*", + "test:lib": "jest", + "test:ext": "cd test/external/ && bash run-all-tests", + "test": "run-s lint test:lib", + "pre-commit": "lint-staged", + "prepare": "run-s compile && husky install config/husky" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + }, + "devDependencies": { + "@express-rate-limit/prettier": "1.1.1", + "@express-rate-limit/tsconfig": "1.0.2", + "@jest/globals": "29.7.0", + "@types/express": "4.17.20", + "@types/jest": "29.5.6", + "@types/node": "20.8.7", + "@types/supertest": "2.0.15", + "del-cli": "5.1.0", + "dts-bundle-generator": "8.0.1", + "esbuild": "0.19.5", + "express": "4.21.1", + "husky": "8.0.3", + "jest": "29.7.0", + "lint-staged": "15.0.2", + "mintlify": "4.0.63", + "npm-run-all": "4.1.5", + "ratelimit-header-parser": "0.1.0", + "supertest": "6.3.3", + "ts-jest": "29.1.1", + "ts-node": "10.9.1", + "typescript": "5.2.2", + "xo": "0.56.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" + ], + "n/no-unsupported-features/es-syntax": 0 + }, + "overrides": [ + { + "files": "test/library/*.ts", + "rules": { + "@typescript-eslint/no-unsafe-argument": 0, + "@typescript-eslint/no-unsafe-assignment": 0 + } + } + ], + "ignore": [ + "test/external" + ] + }, + "prettier": "@express-rate-limit/prettier", + "lint-staged": { + "{source,test}/**/*.ts": "xo --fix", + "**/*.{json,yaml,md}": "prettier --write " + } +} diff --git a/node_modules/express-rate-limit/readme.md b/node_modules/express-rate-limit/readme.md new file mode 100644 index 0000000..20fed3a --- /dev/null +++ b/node_modules/express-rate-limit/readme.md @@ -0,0 +1,147 @@ +

express-rate-limit

+ +
+ +[![tests](https://img.shields.io/github/actions/workflow/status/express-rate-limit/express-rate-limit/ci.yaml)](https://github.com/express-rate-limit/express-rate-limit/actions/workflows/ci.yaml) +[![npm version](https://img.shields.io/npm/v/express-rate-limit.svg)](https://npmjs.org/package/express-rate-limit 'View this project on NPM') +[![npm downloads](https://img.shields.io/npm/dm/express-rate-limit)](https://www.npmjs.com/package/express-rate-limit) +[![license](https://img.shields.io/npm/l/express-rate-limit)](license.md) + +
+ +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) and +[ratelimit-header-parser](https://www.npmjs.com/package/ratelimit-header-parser). + +## Usage + +The [full documentation](https://express-rate-limit.mintlify.app/overview) is +available on-line. + +```ts +import { rateLimit } from 'express-rate-limit' + +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + limit: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes). + standardHeaders: 'draft-8', // draft-6: `RateLimit-*` headers; draft-7 & draft-8: combined `RateLimit` header + legacyHeaders: false, // Disable the `X-RateLimit-*` headers. + // store: ... , // Redis, Memcached, etc. See below. +}) + +// Apply the rate limiting middleware to all requests. +app.use(limiter) +``` + +### Data Stores + +The rate limiter comes with a built-in memory store, and supports a variety of +[external data stores](https://express-rate-limit.mintlify.app/reference/stores). + +### Configuration + +All function options may be async. Click the name for additional info and +default values. + +| Option | Type | Remarks | +| -------------------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------- | +| [`windowMs`] | `number` | How long to remember requests for, in milliseconds. | +| [`limit`] | `number` \| `function` | How many requests to allow. | +| [`message`] | `string` \| `json` \| `function` | Response to return after limit is reached. | +| [`statusCode`] | `number` | HTTP status code after limit is reached (default is 429). | +| [`handler`] | `function` | Function to run after limit is reached (overrides `message` and `statusCode` settings, if set). | +| [`legacyHeaders`] | `boolean` | Enable the `X-Rate-Limit` header. | +| [`standardHeaders`] | `'draft-6'` \| `'draft-7'` \| `'draft-8'` | Enable the `Ratelimit` header. | +| [`identifier`] | `string` \| `function` | Name associated with the quota policy enforced by this rate limiter. | +| [`store`] | `Store` | Use a custom store to share hit counts across multiple nodes. | +| [`passOnStoreError`] | `boolean` | Allow (`true`) or block (`false`, default) traffic if the store becomes unavailable. | +| [`keyGenerator`] | `function` | Identify users (defaults to IP address). | +| [`requestPropertyName`] | `string` | Add rate limit info to the `req` object. | +| [`skip`] | `function` | Return `true` to bypass the limiter for the given request. | +| [`skipSuccessfulRequests`] | `boolean` | Uncount 1xx/2xx/3xx responses. | +| [`skipFailedRequests`] | `boolean` | Uncount 4xx/5xx responses. | +| [`requestWasSuccessful`] | `function` | Used by `skipSuccessfulRequests` and `skipFailedRequests`. | +| [`validate`] | `boolean` \| `object` | Enable or disable built-in validation checks. | + +## Thank You + +Sponsored by [Zuplo](https://zuplo.link/express-rate-limit) a fully-managed API +Gateway for developers. Add +[dynamic rate-limiting](https://zuplo.link/dynamic-rate-limiting), +authentication and more to any API in minutes. Learn more at +[zuplo.com](https://zuplo.link/express-rate-limit) + +

+ + + + zuplo-logo + + +

+ +--- + +Thanks to Mintlify for hosting the documentation at +[express-rate-limit.mintlify.app](https://express-rate-limit.mintlify.app) + +

+ + Create your docs today + +

+ +--- + +Finally, thank you to everyone who's contributed to this project in any way! 🫶 + +## 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/express-rate-limit/express-rate-limit/issues/new)! +If you need help with something, feel free to +[start a discussion](https://github.com/express-rate-limit/express-rate-limit/discussions/new)! + +If you wish to contribute to the library, thanks! First, please read +[the contributing guide](https://express-rate-limit.mintlify.app/docs/guides/contributing.mdx). +Then you can pick up any issue and fix/implement it! + +## License + +MIT © [Nathan Friedly](http://nfriedly.com/), +[Vedant K](https://github.com/gamemaker1) + +[`windowMs`]: + https://express-rate-limit.mintlify.app/reference/configuration#windowms +[`limit`]: https://express-rate-limit.mintlify.app/reference/configuration#limit +[`message`]: + https://express-rate-limit.mintlify.app/reference/configuration#message +[`statusCode`]: + https://express-rate-limit.mintlify.app/reference/configuration#statuscode +[`handler`]: + https://express-rate-limit.mintlify.app/reference/configuration#handler +[`legacyHeaders`]: + https://express-rate-limit.mintlify.app/reference/configuration#legacyheaders +[`standardHeaders`]: + https://express-rate-limit.mintlify.app/reference/configuration#standardheaders +[`identifier`]: + https://express-rate-limit.mintlify.app/reference/configuration#identifier +[`store`]: https://express-rate-limit.mintlify.app/reference/configuration#store +[`passOnStoreError`]: + https://express-rate-limit.mintlify.app/reference/configuration#passOnStoreError +[`keyGenerator`]: + https://express-rate-limit.mintlify.app/reference/configuration#keygenerator +[`requestPropertyName`]: + https://express-rate-limit.mintlify.app/reference/configuration#requestpropertyname +[`skip`]: https://express-rate-limit.mintlify.app/reference/configuration#skip +[`skipSuccessfulRequests`]: + https://express-rate-limit.mintlify.app/reference/configuration#skipsuccessfulrequests +[`skipFailedRequests`]: + https://express-rate-limit.mintlify.app/reference/configuration#skipfailedrequests +[`requestWasSuccessful`]: + https://express-rate-limit.mintlify.app/reference/configuration#requestwassuccessful +[`validate`]: + https://express-rate-limit.mintlify.app/reference/configuration#validate diff --git a/node_modules/express-rate-limit/tsconfig.json b/node_modules/express-rate-limit/tsconfig.json new file mode 100644 index 0000000..52b6ff1 --- /dev/null +++ b/node_modules/express-rate-limit/tsconfig.json @@ -0,0 +1,8 @@ +{ + "include": ["source/"], + "exclude": ["node_modules/"], + "extends": "@express-rate-limit/tsconfig", + "compilerOptions": { + "target": "ES2020" + } +} diff --git a/package-lock.json b/package-lock.json index 1df1096..fbe8c15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^5.1.0", + "express-rate-limit": "^7.5.0", "jsonwebtoken": "^9.0.2", "multer": "^1.4.5-lts.2", "pg": "^8.15.1" @@ -363,6 +364,20 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, "node_modules/finalhandler": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", diff --git a/package.json b/package.json index dd7a0ca..10b62ce 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^5.1.0", + "express-rate-limit": "^7.5.0", "jsonwebtoken": "^9.0.2", "multer": "^1.4.5-lts.2", "pg": "^8.15.1"