"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 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 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 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 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, 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 options passed to the middleware. ...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) { if (config.standardHeaders === "draft-6") { setDraft6Headers(response, info, config.windowMs); } else if (config.standardHeaders === "draft-7") { config.validations.headersResetTime(info.resetTime); setDraft7Headers(response, info, config.windowMs); } } 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;