UNPKG

@adonisjs/limiter

Version:

Rate limiting package for AdonisJS framework

319 lines (312 loc) 8.7 kB
import { __export } from "./chunk-MLKGABMK.js"; // src/stores/bridge.ts import string from "@adonisjs/core/helpers/string"; import { RateLimiterRes } from "rate-limiter-flexible"; // src/debug.ts import { debuglog } from "node:util"; var debug_default = debuglog("adonisjs:limiter"); // src/response.ts var LimiterResponse = class { /** * Allowed number of requests for a pre-defined * duration */ limit; /** * Requests remaining for the pre-defined duration */ remaining; /** * Requests consumed for the pre-defined duration */ consumed; /** * Number of seconds after which the requests count will * reset */ availableIn; constructor(rawResponse) { this.limit = rawResponse.limit; this.remaining = rawResponse.remaining; this.consumed = rawResponse.consumed; this.availableIn = rawResponse.availableIn; } toJSON() { return { limit: this.limit, remaining: this.remaining, consumed: this.consumed, availableIn: this.availableIn }; } }; // src/errors.ts var errors_exports = {}; __export(errors_exports, { E_TOO_MANY_REQUESTS: () => E_TOO_MANY_REQUESTS, ThrottleException: () => ThrottleException }); import { Exception } from "@adonisjs/core/exceptions"; var ThrottleException = class extends Exception { constructor(response, options) { super("Too many requests", options); this.response = response; } message = "Too many requests"; status = 429; code = "E_TOO_MANY_REQUESTS"; /** * Error identifier to lookup translation message */ identifier = "errors.E_TOO_MANY_REQUESTS"; /** * The response headers to set when converting exception * to response */ headers; /** * Translation identifier to use for creating throttle * response. */ translation; /** * Returns the default headers for the response */ getDefaultHeaders() { return { "X-RateLimit-Limit": this.response.limit, "X-RateLimit-Remaining": this.response.remaining, "Retry-After": this.response.availableIn, "X-RateLimit-Reset": new Date(Date.now() + this.response.availableIn * 1e3).toISOString() }; } /** * Returns the message to be sent in the HTTP response. * Feel free to override this method and return a custom * response. */ getResponseMessage(ctx) { if ("i18n" in ctx) { const identifier = this.translation?.identifier || this.identifier; const data = this.translation?.data || {}; return ctx.i18n.t(identifier, data, this.message); } return this.message; } /** * Update the default error message */ setMessage(message) { this.message = message; return this; } /** * Update the default error status code */ setStatus(status) { this.status = status; return this; } /** * Define custom response headers. Existing headers will * be removed */ setHeaders(headers) { this.headers = headers; return this; } /** * Define the translation identifier for the throttle response */ t(identifier, data) { this.translation = { identifier, data }; return this; } /** * Converts the throttle exception to an HTTP response */ async handle(error, ctx) { const status = error.status; const message = this.getResponseMessage(ctx); const headers = this.headers || this.getDefaultHeaders(); Object.keys(headers).forEach((header) => ctx.response.header(header, headers[header])); switch (ctx.request.accepts(["html", "application/vnd.api+json", "json"])) { case "html": case null: ctx.response.status(status).send(message); break; case "json": ctx.response.status(status).send({ errors: [ { message, retryAfter: this.response.availableIn } ] }); break; case "application/vnd.api+json": ctx.response.status(status).send({ errors: [ { code: this.code, title: message, meta: { retryAfter: this.response.availableIn } } ] }); break; } } }; var E_TOO_MANY_REQUESTS = ThrottleException; // src/stores/bridge.ts var RateLimiterBridge = class { rateLimiter; /** * The number of configured requests on the store */ get requests() { return this.rateLimiter.points; } /** * The duration (in seconds) for which the requests are configured */ get duration() { return this.rateLimiter.duration; } /** * The duration (in seconds) for which to block the key */ get blockDuration() { return this.rateLimiter.blockDuration; } constructor(rateLimiter) { this.rateLimiter = rateLimiter; } /** * Makes LimiterResponse from "node-rate-limiter-flexible" response * object */ makeLimiterResponse(response) { return new LimiterResponse({ limit: this.rateLimiter.points, remaining: response.remainingPoints, consumed: response.consumedPoints, availableIn: Math.ceil(response.msBeforeNext / 1e3) }); } /** * Consume 1 request for a given key. An exception is raised * when all the requests have already been consumed or if * the key is blocked. */ async consume(key) { try { const response = await this.rateLimiter.consume(key, 1); debug_default("request consumed for key %s", key); return this.makeLimiterResponse(response); } catch (errorResponse) { debug_default("unable to consume request for key %s, %O", key, errorResponse); if (errorResponse instanceof RateLimiterRes) { throw new E_TOO_MANY_REQUESTS(this.makeLimiterResponse(errorResponse)); } throw errorResponse; } } /** * Increment the number of consumed requests for a given key. * No errors are thrown when limit has reached */ async increment(key) { const response = await this.rateLimiter.penalty(key, 1); debug_default("increased requests count for key %s", key); return this.makeLimiterResponse(response); } /** * Decrement the number of consumed requests for a given key. */ async decrement(key) { const existingKey = await this.rateLimiter.get(key); if (!existingKey) { return this.set(key, 0, this.duration); } if (existingKey.consumedPoints <= 0) { return this.makeLimiterResponse(existingKey); } const response = await this.rateLimiter.reward(key, 1); debug_default("decreased requests count for key %s", key); return this.makeLimiterResponse(response); } /** * Block a given key for the given duration. The duration must be * a value in seconds or a string expression. */ async block(key, duration) { const response = await this.rateLimiter.block(key, string.seconds.parse(duration)); debug_default("blocked key %s", key); return this.makeLimiterResponse(response); } /** * Manually set the number of requests exhausted for * a given key for the given time duration. * * For example: "ip_127.0.0.1" has made "20 requests" in "1 minute". * Now, if you allow 25 requests in 1 minute, then only 5 requests * are left. * * The duration must be a value in seconds or a string expression. */ async set(key, requests, duration) { const response = await this.rateLimiter.set( key, requests, duration ? string.seconds.parse(duration) : this.duration ); debug_default("updated key %s with requests: %s, duration: %s", key, requests, duration); const remaining = this.requests - response.consumedPoints; const limiterResponse = this.makeLimiterResponse(response); limiterResponse.remaining = remaining < 0 ? 0 : remaining; return limiterResponse; } /** * Delete a given key */ delete(key) { debug_default("deleting key %s", key); return this.rateLimiter.delete(key); } /** * Delete all keys blocked within the memory */ deleteInMemoryBlockedKeys() { if ("deleteInMemoryBlockedAll" in this.rateLimiter) { return this.rateLimiter.deleteInMemoryBlockedAll(); } } /** * Get limiter response for a given key. Returns null when * key doesn't exist. */ async get(key) { const response = await this.rateLimiter.get(key); debug_default("fetching key %s, %O", key, response); if (!response || Number.isNaN(response.remainingPoints)) { return null; } return this.makeLimiterResponse(response); } }; export { E_TOO_MANY_REQUESTS, errors_exports, LimiterResponse, debug_default, RateLimiterBridge };