@routup/rate-limit
Version:
Routup rate limiter.
1 lines • 12.7 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","names":[],"sources":["../src/utils/is-object.ts","../src/constants.ts","../src/store/utils.ts","../src/store/memory.ts","../src/utils/options.ts","../src/request.ts","../src/handler.ts","../src/module.ts","../src/index.ts"],"sourcesContent":["export function isObject(item: unknown) : item is Record<string, any> {\n return (\n !!item &&\n typeof item === 'object' &&\n !Array.isArray(item)\n );\n}\n","export const RETRY_AGAIN_MESSAGE = 'Too many requests, please try again later.';\n","export function calculateNextResetTime(windowMs: number): Date {\n const resetTime = new Date();\n resetTime.setMilliseconds(resetTime.getMilliseconds() + windowMs);\n return resetTime;\n}\n","import type { IncrementResponse, Options } from '../type';\nimport type { Store } from './type';\nimport { calculateNextResetTime } from './utils';\n\nexport class MemoryStore implements Store {\n /**\n * The duration of time before which all hit counts are reset (in milliseconds).\n */\n windowMs!: number;\n\n /**\n * The map that stores the number of hits for each client in memory.\n */\n hits!: {\n [key: string]: number | undefined\n };\n\n /**\n * The time at which all hit counts will be reset.\n */\n resetTime!: Date;\n\n /**\n * Reference to the active timer.\n */\n interval?: NodeJS.Timeout;\n\n /**\n * Method that initializes the store.\n *\n * @param options {Options} - The options used to setup the middleware.\n */\n init(options: Options): void {\n // Get the duration of a window from the options.\n this.windowMs = options.windowMs;\n\n // Then calculate the reset time using that.\n this.resetTime = calculateNextResetTime(this.windowMs);\n\n // Initialise the hit counter map.\n this.hits = {};\n\n // Reset hit counts for ALL clients every `windowMs` - this will also\n // re-calculate the `resetTime`\n this.interval = setInterval(async () => {\n await this.resetAll();\n }, this.windowMs);\n\n // Cleaning up the interval will be taken care of by the `shutdown` method.\n if (this.interval.unref) this.interval.unref();\n }\n\n /**\n * Method to increment a client's hit counter.\n *\n * @param key {string} - The identifier for a client.\n *\n * @returns {IncrementResponse} - The number of hits and reset time for that client.\n *\n * @public\n */\n async increment(key: string): Promise<IncrementResponse> {\n const totalHits = (this.hits[key] ?? 0) + 1;\n this.hits[key] = totalHits;\n\n return {\n totalHits,\n resetTime: this.resetTime,\n };\n }\n\n /**\n * Method to decrement a client's hit counter.\n *\n * @param key {string} - The identifier for a client.\n *\n * @public\n */\n async decrement(key: string): Promise<void> {\n const current = this.hits[key];\n\n if (current) this.hits[key] = current - 1;\n }\n\n /**\n * Method to reset a client's hit counter.\n *\n * @param key {string} - The identifier for a client.\n *\n * @public\n */\n async reset(key: string): Promise<void> {\n delete this.hits[key];\n }\n\n /**\n * Method to reset everyone's hit counter.\n *\n * @public\n */\n /* istanbul ignore next */\n async resetAll(): Promise<void> {\n this.hits = {};\n this.resetTime = calculateNextResetTime(this.windowMs);\n }\n}\n","import type { IAppEvent } from 'routup';\nimport { getRequestIP } from 'routup';\nimport { RETRY_AGAIN_MESSAGE } from '../constants';\nimport { MemoryStore } from '../store';\nimport type { Options, OptionsInput, ValueDeterminingMiddleware } from '../type';\n\nexport function normalizeHandlerOptions(input: OptionsInput = {}) : Options {\n const options : Options = {\n windowMs: 60 * 1000,\n max: 5,\n message: RETRY_AGAIN_MESSAGE,\n statusCode: 429,\n skipFailedRequest: false,\n skipSuccessfulRequest: false,\n requestWasSuccessful: (_event: IAppEvent, response: Response): boolean => response.status < 400,\n skip: (_event: IAppEvent): boolean => false,\n keyGenerator: (event: IAppEvent): string => getRequestIP(event, { trustProxy: true }) || '127.0.0.1',\n async handler(\n event: IAppEvent,\n _optionsUsed: Options,\n ): Promise<unknown> {\n const message: unknown = typeof options.message === 'function' ?\n await (options.message as ValueDeterminingMiddleware<any>)(\n event,\n ) :\n options.message;\n\n event.response.status = options.statusCode;\n\n return message ?? 'Too many requests, please try again later.';\n },\n ...input,\n\n store: input.store || new MemoryStore(),\n };\n\n return options;\n}\n","import type { IAppEvent } from 'routup';\nimport type { RateLimitInfo } from './type';\nimport { isObject } from './utils';\n\nconst RateLimitSymbol = Symbol.for('@routup/rate-limit:ReqRateLimit');\n\nexport function useRequestRateLimitInfo(event: IAppEvent) : Partial<RateLimitInfo>;\nexport function useRequestRateLimitInfo<K extends keyof RateLimitInfo>(event: IAppEvent, key: K) : RateLimitInfo[K] | undefined;\nexport function useRequestRateLimitInfo(event: IAppEvent, key?: string) {\n if (RateLimitSymbol in event.store) {\n if (typeof key === 'string') {\n return (event.store[RateLimitSymbol] as Record<string, unknown>)[key];\n }\n\n return event.store[RateLimitSymbol];\n }\n\n return typeof key === 'string' ?\n undefined :\n {};\n}\n\nexport function setRequestRateLimitInfo<K extends keyof RateLimitInfo>(\n event: IAppEvent,\n key: K,\n value: RateLimitInfo[K],\n) : void;\nexport function setRequestRateLimitInfo(event: IAppEvent, record: RateLimitInfo) : void;\nexport function setRequestRateLimitInfo(event: IAppEvent, key: RateLimitInfo | string, value?: unknown) : void {\n const existing = RateLimitSymbol in event.store ?\n event.store[RateLimitSymbol] as Record<string, unknown> :\n undefined;\n\n if (isObject(key)) {\n event.store[RateLimitSymbol] = existing ? { ...existing, ...key } : key;\n } else if (existing) {\n existing[key] = value;\n } else {\n event.store[RateLimitSymbol] = { [key]: value };\n }\n}\n","import { HeaderName, defineCoreHandler } from 'routup';\nimport { setRequestRateLimitInfo } from './request';\nimport type { OptionsInput } from './type';\nimport { normalizeHandlerOptions } from './utils';\n\nexport function createHandler(input?: OptionsInput) {\n const options = normalizeHandlerOptions({ ...(input || {}) });\n\n if (typeof options.store.init === 'function') {\n options.store.init(options);\n }\n\n return defineCoreHandler(async (event) => {\n const skip = await options.skip(event);\n if (skip) {\n return event.next();\n }\n\n const key = await options.keyGenerator(event);\n\n const { totalHits, resetTime } = await options.store.increment(key);\n\n const retrieveQuota = typeof options.max === 'function' ?\n options.max(event) :\n options.max;\n\n const maxHits = await retrieveQuota;\n\n setRequestRateLimitInfo(event, {\n limit: maxHits,\n current: totalHits,\n remaining: Math.max(maxHits - totalHits, 0),\n resetTime,\n });\n\n event.response.headers.set(HeaderName.RATE_LIMIT_LIMIT, String(maxHits));\n event.response.headers.set(\n HeaderName.RATE_LIMIT_REMAINING,\n String(Math.max(maxHits - totalHits, 0)),\n );\n\n if (resetTime) {\n const deltaSeconds = Math.ceil(\n (resetTime.getTime() - Date.now()) / 1000,\n );\n event.response.headers.set(HeaderName.RATE_LIMIT_RESET, String(Math.max(0, deltaSeconds)));\n }\n\n if (\n maxHits &&\n totalHits > maxHits\n ) {\n event.response.headers.set(\n HeaderName.RETRY_AFTER,\n String(Math.ceil(options.windowMs / 1000)),\n );\n\n return options.handler(event, options);\n }\n\n const response = await event.next();\n\n if (\n options.skipFailedRequest ||\n options.skipSuccessfulRequest\n ) {\n const wasSuccessful = response ?\n options.requestWasSuccessful(event, response) :\n false;\n\n if (\n (options.skipFailedRequest && !wasSuccessful) ||\n (options.skipSuccessfulRequest && wasSuccessful)\n ) {\n await options.store.decrement(key);\n setRequestRateLimitInfo(event, 'remaining', Math.max(maxHits - totalHits + 1, 0));\n }\n }\n\n return response;\n });\n}\n","import type { Handler } from 'routup';\nimport { createHandler } from './handler';\nimport type { OptionsInput } from './type';\n\nexport function rateLimit(options?: OptionsInput) : Handler {\n return createHandler(options);\n}\n","import { rateLimit } from './module';\n\nexport * from './constants';\nexport * from './handler';\nexport * from './module';\nexport * from './request';\nexport * from './store';\nexport * from './type';\nexport * from './utils';\nexport default rateLimit;\n"],"mappings":"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"}