@adonisjs/limiter
Version:
Rate limiting package for AdonisJS framework
434 lines (427 loc) • 12.1 kB
JavaScript
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 "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;
}
/**
* Returns a JSON representation of the limiter response.
*
* @example
* ```ts
* const response = limiter.get('user:1')
* console.log(response.toJSON())
* // { limit: 10, remaining: 5, consumed: 5, availableIn: 30 }
* ```
*/
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 rate limit headers that will be sent in the HTTP response.
* Includes limit information, remaining requests, retry-after time, and reset time.
*/
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.
* Supports i18n translations when the i18n package is available.
*
* @param ctx - The HTTP context
*/
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;
}
/**
* Updates the default error message.
*
* @param message - The new error message
*
* @example
* ```ts
* throw new ThrottleException(response)
* .setMessage('Rate limit exceeded. Please try again later.')
* ```
*/
setMessage(message) {
this.message = message;
return this;
}
/**
* Updates the default error status code.
*
* @param status - The HTTP status code
*
* @example
* ```ts
* throw new ThrottleException(response)
* .setStatus(503)
* ```
*/
setStatus(status) {
this.status = status;
return this;
}
/**
* Defines custom response headers. This will replace the default headers.
*
* @param headers - Custom headers to set
*
* @example
* ```ts
* throw new ThrottleException(response)
* .setHeaders({
* 'X-Custom-Header': 'value',
* 'Retry-After': 60
* })
* ```
*/
setHeaders(headers) {
this.headers = headers;
return this;
}
/**
* Defines the i18n translation identifier for the throttle response message.
*
* @param identifier - The translation key
* @param data - Optional translation data
*
* @example
* ```ts
* throw new ThrottleException(response)
* .t('errors.rate_limit_exceeded', { minutes: 5 })
* ```
*/
t(identifier, data) {
this.translation = { identifier, data };
return this;
}
/**
* Converts the throttle exception to an HTTP response.
* Automatically sets appropriate headers and formats the response
* based on the Accept header (HTML, JSON, or JSON:API).
*
* @param error - The throttle exception instance
* @param ctx - The HTTP context
*/
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;
}
/**
* Transforms a rate-limiter-flexible response into an AdonisJS LimiterResponse.
*
* @param response - Raw response from rate-limiter-flexible
*/
makeLimiterResponse(response) {
return new LimiterResponse({
limit: this.rateLimiter.points,
remaining: response.remainingPoints,
consumed: response.consumedPoints,
availableIn: Math.ceil(response.msBeforeNext / 1e3)
});
}
/**
* Consumes one request for the given key. Throws a ThrottleException
* when the rate limit is exceeded or the key is blocked.
*
* @param key - Unique identifier for the rate limit (e.g., user ID, IP address)
*
* @example
* ```ts
* const response = await limiter.consume('user:123')
* console.log(`Remaining: ${response.remaining}`)
* ```
*/
async consume(key, amount) {
const consumeAmount = amount !== void 0 && amount > 0 ? amount : 1;
try {
const response = await this.rateLimiter.consume(key, consumeAmount);
debug_default("request consumed for key %s with amount %d", key, consumeAmount);
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;
}
}
/**
* Increments the consumed request count for the given key.
* Unlike consume(), this method does not throw when the limit is reached.
*
* @param key - Unique identifier for the rate limit
*
* @example
* ```ts
* const response = await limiter.increment('user:123')
* ```
*/
async increment(key, amount = 1) {
if (amount <= 0) {
debug_default('invalid increment amount "%d" provided. Falling back to 1', amount);
amount = 1;
}
const response = await this.rateLimiter.penalty(key, amount);
debug_default("increased requests count for key %s", key);
return this.makeLimiterResponse(response);
}
/**
* Decrements the consumed request count for the given key.
* Will not decrement below zero.
*
* @param key - Unique identifier for the rate limit
*
* @example
* ```ts
* const response = await limiter.decrement('user:123')
* ```
*/
async decrement(key, amount = 1) {
const existingKey = await this.rateLimiter.get(key);
if (!existingKey) {
return this.set(key, 0, this.duration);
}
if (amount <= 0) {
debug_default('invalid decrement amount "%d" provided. Falling back to 1', amount);
amount = 1;
}
if (existingKey.consumedPoints <= 0) {
return this.makeLimiterResponse(existingKey);
}
if (amount > existingKey.consumedPoints) {
amount = existingKey.consumedPoints;
}
const response = await this.rateLimiter.reward(key, amount);
debug_default("decreased requests count for key %s", key);
return this.makeLimiterResponse(response);
}
/**
* Blocks the given key for the specified duration, preventing any requests.
*
* @param key - Unique identifier for the rate limit
* @param duration - Block duration in seconds or as a time expression (e.g., '5 mins')
*
* @example
* ```ts
* await limiter.block('user:123', '10 mins')
* await limiter.block('ip:192.168.1.1', 600)
* ```
*/
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 sets the number of consumed requests for a given key.
*
* @param key - Unique identifier for the rate limit
* @param requests - Number of requests consumed
* @param duration - Optional duration in seconds or time expression
*
* @example
* ```ts
* // Set that user has consumed 20 requests out of 25 allowed
* await limiter.set('user:123', 20, '1 minute')
* ```
*/
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;
}
/**
* Deletes the given key, resetting its rate limit state.
*
* @param key - Unique identifier for the rate limit
*
* @example
* ```ts
* await limiter.delete('user:123')
* ```
*/
delete(key) {
debug_default("deleting key %s", key);
return this.rateLimiter.delete(key);
}
/**
* Deletes all keys that are blocked in memory.
* Only applicable for stores with in-memory blocking enabled.
*/
deleteInMemoryBlockedKeys() {
if ("deleteInMemoryBlockedAll" in this.rateLimiter) {
return this.rateLimiter.deleteInMemoryBlockedAll();
}
}
/**
* Retrieves the current rate limit state for the given key.
*
* @param key - Unique identifier for the rate limit
*
* @example
* ```ts
* const response = await limiter.get('user:123')
* if (response) {
* console.log(`Remaining: ${response.remaining}`)
* }
* ```
*/
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
};