@adonisjs/limiter
Version:
Rate limiting package for AdonisJS framework
319 lines (312 loc) • 8.7 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 "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
};