'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var routup = require('routup'); const symbol = Symbol.for('ReqRateLimit'); function useRequestRateLimitInfo(req, key) { if (symbol in req) { if (typeof key === 'string') { return req[symbol][key]; } return req[symbol]; } return {}; } function setRequestRateLimitInfo(req, key, value) { if (symbol in req) { if (typeof key === 'object') { req[symbol] = key; } else { req[symbol][key] = value; } return; } if (typeof key === 'object') { req[symbol] = key; return; } req[symbol] = { [key]: value }; } const RETRY_AGAIN_MESSAGE = 'Too many requests, please try again later.'; function calculateNextResetTime(windowMs) { const resetTime = new Date(); resetTime.setMilliseconds(resetTime.getMilliseconds() + windowMs); return resetTime; } class MemoryStore { /** * Method that initializes the store. * * @param options {Options} - The options used to setup the middleware. */ init(options) { // Get the duration of a window from the options. this.windowMs = options.windowMs; // Then calculate the reset time using that. this.resetTime = calculateNextResetTime(this.windowMs); // Initialise the hit counter map. this.hits = {}; // Reset hit counts for ALL clients every `windowMs` - this will also // re-calculate the `resetTime` this.interval = setInterval(async ()=>{ await this.resetAll(); }, this.windowMs); // Cleaning up the interval will be taken care of by the `shutdown` method. if (this.interval.unref) this.interval.unref(); } /** * Method to increment a client's hit counter. * * @param key {string} - The identifier for a client. * * @returns {IncrementResponse} - The number of hits and reset time for that client. * * @public */ async increment(key) { const totalHits = (this.hits[key] ?? 0) + 1; this.hits[key] = totalHits; return { totalHits, resetTime: this.resetTime }; } /** * Method to decrement a client's hit counter. * * @param key {string} - The identifier for a client. * * @public */ async decrement(key) { const current = this.hits[key]; if (current) this.hits[key] = current - 1; } /** * Method to reset a client's hit counter. * * @param key {string} - The identifier for a client. * * @public */ async reset(key) { delete this.hits[key]; } /** * Method to reset everyone's hit counter. * * @public */ /* istanbul ignore next */ async resetAll() { this.hits = {}; this.resetTime = calculateNextResetTime(this.windowMs); } } function buildHandlerOptions(input) { input = input || {}; const options = { windowMs: 60 * 1000, max: 5, message: RETRY_AGAIN_MESSAGE, statusCode: 429, skipFailedRequest: false, skipSuccessfulRequest: false, requestWasSuccessful: (request, response)=>response.statusCode < 400, skip: (_request, _response)=>false, keyGenerator: (request, _response)=>routup.getRequestIP(request, { trustProxy: true }), async handler (request, response, _next, _optionsUsed) { // Set the response status code response.statusCode = options.statusCode; // Call the `message` if it is a function. const message = typeof options.message === 'function' ? await options.message(request, response) : options.message; // Send the response if writable. if (!response.writableEnded) { routup.send(response, message ?? 'Too many requests, please try again later.'); } }, ...input, store: input.store || new MemoryStore() }; return options; } function createHandler(input) { const options = buildHandlerOptions({ ...input || {} }); if (typeof options.store.init === 'function') { options.store.init(options); } return routup.coreHandler(async (req, res, next)=>{ const skip = await options.skip(req, res); if (skip) { next(); return; } const key = await options.keyGenerator(req, res); const { totalHits, resetTime } = await options.store.increment(key); const retrieveQuota = typeof options.max === 'function' ? options.max(req, res) : options.max; const maxHits = await retrieveQuota; setRequestRateLimitInfo(req, { limit: maxHits, current: totalHits, remaining: Math.max(maxHits - totalHits, 0), resetTime }); if (!res.headersSent) { res.setHeader(routup.HeaderName.RATE_LIMIT_LIMIT, maxHits); res.setHeader(routup.HeaderName.RATE_LIMIT_REMAINING, useRequestRateLimitInfo(req, 'remaining')); if (resetTime) { const deltaSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1000); res.setHeader(routup.HeaderName.RATE_LIMIT_RESET, Math.max(0, deltaSeconds)); } } if (options.skipFailedRequest || options.skipSuccessfulRequest) { let decremented = false; const decrementKey = async ()=>{ if (!decremented) { await options.store.decrement(key); decremented = true; setRequestRateLimitInfo(req, 'remaining', Math.max(maxHits - totalHits - 1, 0)); } }; if (options.skipFailedRequest) { res.on('finish', async ()=>{ if (!options.requestWasSuccessful(req, res)) { await decrementKey(); } }); res.on('close', async ()=>{ if (!res.writableEnded) { await decrementKey(); } }); res.on('error', async ()=>{ await decrementKey(); }); } if (options.skipSuccessfulRequest) { res.on('finish', async ()=>{ if (options.requestWasSuccessful(req, res)) { await decrementKey(); } }); } } if (maxHits && totalHits > maxHits) { if (!res.headersSent) { res.setHeader(routup.HeaderName.RETRY_AFTER, Math.ceil(options.windowMs / 1000)); } options.handler(req, res, next, options); return; } next(); }); } function rateLimit(options) { return { name: 'rateLimit', install: (router)=>{ router.use(createHandler(options)); } }; } exports.MemoryStore = MemoryStore; exports.RETRY_AGAIN_MESSAGE = RETRY_AGAIN_MESSAGE; exports.buildHandlerOptions = buildHandlerOptions; exports.calculateNextResetTime = calculateNextResetTime; exports.createHandler = createHandler; exports.default = rateLimit; exports.rateLimit = rateLimit; exports.setRequestRateLimitInfo = setRequestRateLimitInfo; exports.useRequestRateLimitInfo = useRequestRateLimitInfo; module.exports = Object.assign(exports.default, exports); //# sourceMappingURL=index.cjs.map