hono-rate-limiter
Version:
Rate limit middleware for Hono.
606 lines (597 loc) • 21.7 kB
text/typescript
import { Env, Input, Context, Next, MiddlewareHandler } from 'hono';
import { HandlerResponse, Env as Env$1, Input as Input$1 } from 'hono/types';
import { StatusCode } from 'hono/utils/http-status';
import { WSContext, WSEvents } from 'hono/ws';
/**
* Data returned from the `Store` when a client's hit counter is incremented.
*
* @property totalHits {number} - The number of hits for that client so far.
* @property resetTime {Date | undefined} - The time when the counter resets.
*/
type ClientRateLimitInfo = {
totalHits: number;
resetTime?: Date;
};
/**
* Promisify<T> is a utility type that represents a value of type T or a Promise<T>.
* This type is useful for converting synchronous functions to asynchronous functions.
* @example
* type getResult = Promisify<number>; // getResult can be number or Promise<number>
* type getUser = Promisify<User>; // getUser can be User or Promise<User>
*/
type Promisify<T> = T | Promise<T>;
/**
* The rate limit related information for each client included in the
* Hono context object.
*/
type RateLimitInfo = {
limit: number;
used: number;
remaining: number;
resetTime: Date | undefined;
};
/**
* Hono request handler that sends back a response when a client is
* rate-limited.
*
* @param context {Context} - The Hono context object.
* @param next {Next} - The Hono `next` function, can be called to skip responding.
* @param optionsUsed {ConfigType} - The options used to set up the middleware.
*/
type RateLimitExceededEventHandler<E extends Env = Env, P extends string = string, I extends Input = Input> = (c: Context<E, P, I>, next: Next, optionsUsed: ConfigType<E, P, I>) => void;
type KeyGeneratorType<E extends Env = Env, P extends string = string, I extends Input = Input> = {
/**
* Method to generate custom identifiers for clients.
*/
keyGenerator: (c: Context<E, P, I>) => Promisify<string>;
};
type CommonConfigType<E extends Env = Env, P extends string = string, I extends Input = Input> = {
/**
* The response body to send back when a client is rate limited.
*
* Defaults to `'Too many requests, please try again later.'`
*/
message: string | Record<string, unknown> | ((c: Context<E, P, I>) => Promisify<string | Record<string, unknown>>);
/**
* The HTTP status code to send back when a client is rate limited.
*
* Defaults to `HTTP 429 Too Many Requests` (RFC 6585).
*/
statusCode: StatusCode;
/**
* Hono request handler that sends back a response when a client is
* rate-limited.
*
* By default, sends back the `statusCode` and `message` set via the options.
*/
handler: RateLimitExceededEventHandler<E, P, I>;
/**
* Method (in the form of middleware) to determine whether or not this request
* counts towards a client's quota.
*
* By default, skips no requests.
*/
skip: (c: Context<E, P, I>) => Promisify<boolean>;
};
type RateLimit = {
limit: (options: {
key: string;
}) => Promisify<{
success: boolean;
}>;
};
type CloudflareConfigType<E extends Env = Env, P extends string = string, I extends Input = Input> = {
/**
* The Cloudflare rate limit binding to use.
*/
binding: RateLimit | ((c: Context<E, P, I>) => RateLimit);
} & KeyGeneratorType<E, P, I> & CommonConfigType<E, P, I>;
type CloudflareConfigProps<E extends Env = Env, P extends string = string, I extends Input = Input> = {
/**
* The Cloudflare rate limit binding to use.
*/
binding: RateLimit | ((c: Context<E, P, I>) => RateLimit);
} & KeyGeneratorType<E, P, I> & Partial<CommonConfigType<E, P, I>>;
type HonoConfigType<E extends Env = Env, P extends string = string, I extends Input = Input> = {
/**
* How long we should remember the requests.
*
* Defaults to `60000` ms (= 1 minute).
*/
windowMs: number;
/**
* The maximum number of connections to allow during the `window` before
* rate limiting the client.
*
* Can be the limit itself as a number or express middleware that parses
* the request and then figures out the limit.
*
* Defaults to `5`.
*/
limit: number | ((c: Context<E, P, I>) => Promisify<number>);
/**
* Whether to enable support for the standardized rate limit headers (`RateLimit-*`).
*
* Defaults to `draft-6`.
*/
standardHeaders: boolean | "draft-6" | "draft-7";
/**
* The name of the property on the context object to store the rate limit info.
*
* Defaults to `rateLimit`.
*/
requestPropertyName: string;
/**
* The name of the property on the context object to store the Data Store instance.
*
* Defaults to `rateLimitStore`.
*/
requestStorePropertyName: string;
/**
* If `true`, the library will (by default) skip all requests that have a 4XX
* or 5XX status.
*
* Defaults to `false`.
*/
skipFailedRequests: boolean;
/**
* If `true`, the library will (by default) skip all requests that have a
* status code less than 400.
*
* Defaults to `false`.
*/
skipSuccessfulRequests: boolean;
/**
* Method to determine whether or not the request counts as 'succesful'. Used
* when either `skipSuccessfulRequests` or `skipFailedRequests` is set to true.
*
* By default, requests with a response status code less than 400 are considered
* successful.
*/
requestWasSuccessful: (c: Context<E, P, I>) => Promisify<boolean>;
/**
* The `Store` to use to store the hit count for each client.
*
* By default, the built-in `MemoryStore` will be used.
*/
store: Store<E, P, I>;
} & KeyGeneratorType<E, P, I> & CommonConfigType<E, P, I>;
type HonoConfigProps<E extends Env = Env, P extends string = string, I extends Input = Input> = {
binding?: never;
} & KeyGeneratorType<E, P, I> & Partial<HonoConfigType<E, P, I>>;
type ConfigType<E extends Env = Env, P extends string = string, I extends Input = Input> = HonoConfigType<E, P, I> | CloudflareConfigType<E, P, I>;
type ConfigProps<E extends Env = Env, P extends string = string, I extends Input = Input> = HonoConfigProps<E, P, I> | CloudflareConfigProps<E, P, I>;
type WSStatusCode = 1000 | 1001 | 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | 1008 | 1009 | 1010;
/**
* Hono request handler that sends back a response when a client is
* rate-limited.
*
* @param context {Context} - The Hono context object.
* @param next {Next} - The Hono `next` function, can be called to skip responding.
* @param optionsUsed {ConfigType} - The options used to set up the middleware.
*/
type WSRateLimitExceededEventHandler<E extends Env = Env, P extends string = string, I extends Input = Input> = (event: unknown, ws: WSContext, optionsUsed: WSConfigType<E, P, I>) => void;
/**
* The configuration options for the rate limiter.
*/
interface WSConfigType<E extends Env = Env, P extends string = string, I extends Input = Input> extends Omit<HonoConfigType<E, P, I>, "statusCode" | "standardHeaders" | "requestWasSuccessful" | "handler" | "skip"> {
/**
* The response body to send back when a client is rate limited.
*
* Defaults to `'Too many requests, please try again later.'`
*/
message: string;
/**
* The ws status code to send back when a client is rate limited.
*
* Defaults to `HTTP 1008 Terminating The Connection` (RFC 6455).
*/
statusCode: WSStatusCode;
/**
* Hono ws request handler that sends back a response when a client is
* rate-limited.
*
* By default, sends back the `statusCode` and `message` set via the options.
*/
handler: WSRateLimitExceededEventHandler<E, P, I>;
/**
* Method (in the form of middleware) to determine whether or not this ws request
* counts towards a client's quota.
*
* By default, skips no requests.
*/
skip: (event: unknown, ws: WSContext) => Promisify<boolean>;
}
type WSConfigProps<E extends Env = Env, P extends string = string, I extends Input = Input> = KeyGeneratorType<E, P, I> & Partial<WSConfigType<E, P, I>>;
/**
* An interface that all hit counter stores must implement.
*/
type Store<E extends Env = Env, P extends string = string, I extends Input = Input> = {
/**
* Method that initializes the store, and has access to the options passed to
* the middleware too.
*
* @param options {HonoConfigType} - The options used to setup the middleware.
*/
init?: (options: HonoConfigType<E, P, I>) => void;
/**
* Method to fetch a client's hit count and reset time.
*
* @param key {string} - The identifier for a client.
*
* @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
*/
get?: (key: string) => Promisify<ClientRateLimitInfo | undefined>;
/**
* Method to increment a client's hit counter.
*
* @param key {string} - The identifier for a client.
*
* @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
*/
increment: (key: string) => Promisify<ClientRateLimitInfo>;
/**
* Method to decrement a client's hit counter.
*
* @param key {string} - The identifier for a client.
*/
decrement: (key: string) => Promisify<void>;
/**
* Method to reset a client's hit counter.
*
* @param key {string} - The identifier for a client.
*/
resetKey: (key: string) => Promisify<void>;
/**
* Method to reset everyone's hit counter.
*/
resetAll?: () => Promisify<void>;
/**
* Method to shutdown the store, stop timers, and release all resources.
*/
shutdown?: () => Promisify<void>;
/**
* Flag to indicate that keys incremented in one instance of this store can
* not affect other instances. Typically false if a database is used, true for
* MemoryStore.
*
* Used to help detect double-counting misconfigurations.
*/
localKeys?: boolean;
/**
* Optional value that the store prepends to keys
*
* Used by the double-count check to avoid false-positives when a key is counted twice, but with different prefixes
*/
prefix?: string;
};
/**
*
* Create an instance of rate-limiting middleware for Hono.
*
* @param config {ConfigProps} - Options to configure the rate limiter.
*
* @returns - The middleware that rate-limits clients based on your configuration.
*
* @public
*/
declare function rateLimiter<E extends Env = Env, P extends string = string, I extends Input = Input, R extends HandlerResponse<any> = Response>(config: ConfigProps<E, P, I>): MiddlewareHandler<E, P, I, R>;
/**
* A `Store` that stores the hit count for each client in memory.
*/
declare class MemoryStore<E extends Env$1 = Env$1, P extends string = string, I extends Input$1 = Input$1> implements Store<E, P, I> {
#private;
/**
* These two maps store usage (requests) and reset time by key (for example, IP
* addresses or API keys).
*
* They are split into two to avoid having to iterate through the entire set to
* determine which ones need reset. Instead, `Client`s are moved from `previous`
* to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients
* left in `previous`, i.e., those that have not made any recent requests, are
* known to be expired and can be deleted in bulk.
*/
previous: Map<string, Required<ClientRateLimitInfo>>;
current: Map<string, Required<ClientRateLimitInfo>>;
/**
* A reference to the active timer.
*/
interval?: any;
/**
* Method that initializes the store.
*
* @param options {HonoConfigType | WSConfigType} - The options used to setup the middleware.
*/
init(options: HonoConfigType<E, P, I> | WSConfigType<E, P, I>): void;
/**
* Method to fetch a client's hit count and reset time.
*
* @param key {string} - The identifier for a client.
*
* @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
*
* @public
*/
get(key: string): ClientRateLimitInfo | undefined;
/**
* Method to increment a client's hit counter.
*
* @param key {string} - The identifier for a client.
*
* @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
*
* @public
*/
increment(key: string): ClientRateLimitInfo;
/**
* Method to decrement a client's hit counter.
*
* @param key {string} - The identifier for a client.
*
* @public
*/
decrement(key: string): void;
/**
* Method to reset a client's hit counter.
*
* @param key {string} - The identifier for a client.
*
* @public
*/
resetKey(key: string): void;
/**
* Method to reset everyone's hit counter.
*
* @public
*/
resetAll(): void;
/**
* Method to stop the timer (if currently running) and prevent any memory
* leaks.
*
* @public
*/
shutdown(): void;
/**
* Recycles a client by setting its hit count to zero, and reset time to
* `windowMs` milliseconds from now.
*
* NOT to be confused with `#resetKey()`, which removes a client from both the
* `current` and `previous` maps.
*
* @param client {Client} - The client to recycle.
* @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client.
*
* @return {Client} - The modified client that was passed in, to allow for chaining.
*/
private resetClient;
/**
* Retrieves or creates a client, given a key. Also ensures that the client being
* returned is in the `current` map.
*
* @param key {string} - The key under which the client is (or is to be) stored.
*
* @returns {Client} - The requested client.
*/
private getClient;
/**
* Move current clients to previous, create a new map for current.
*
* This function is called every `windowMs`.
*/
private clearExpired;
}
type RedisClient = {
scriptLoad: (script: string) => Promise<string>;
evalsha: <TArgs extends unknown[], TData = unknown>(sha1: string, keys: string[], args: TArgs) => Promise<TData>;
decr: (key: string) => Promise<number>;
del: (key: string) => Promise<number>;
};
/**
* The type of data Redis might return to us.
*/
type Data = boolean | number | string;
type RedisReply = Data | Data[];
/**
* The configuration options for the store.
*/
type Options = {
/**
* The Redis client
*/
client: RedisClient;
/**
* The text to prepend to the key in Redis.
*/
readonly prefix?: string;
/**
* Whether to reset the expiry for a particular key whenever its hit count
* changes.
*/
readonly resetExpiryOnChange?: boolean;
};
declare class RedisStore<E extends Env$1 = Env$1, P extends string = string, I extends Input$1 = Input$1> implements Store<E, P, I> {
/**
* The text to prepend to the key in Redis.
*/
prefix: string;
/**
* Whether to reset the expiry for a particular key whenever its hit count
* changes.
*/
resetExpiryOnChange: boolean;
/**
* The Redis client to use.
*/
client: RedisClient;
/**
* The number of milliseconds to remember that user's requests.
*/
windowMs: number;
/**
* Stores the loaded SHA1s of the LUA scripts used for executing the increment
* and get key operations.
*/
incrementScriptSha: Promise<string>;
getScriptSha: Promise<string>;
/**
* @constructor for `RedisStore`.
*
* @param options {Options} - The configuration options for the store.
*/
constructor(options: Options);
/**
* Loads the script used to increment a client's hit count.
*/
loadIncrementScript(): Promise<string>;
/**
* Loads the script used to fetch a client's hit count and expiry time.
*/
loadGetScript(): Promise<string>;
/**
* Runs the increment command, and retries it if the script is not loaded.
*/
retryableIncrement(key: string): Promise<RedisReply>;
/**
* Method to prefix the keys with the given text.
*
* @param key {string} - The key.
*
* @returns {string} - The text + the key.
*/
prefixKey(key: string): string;
/**
* Method that actually initializes the store.
*
* @param options {RateLimitConfiguration} - The options used to setup the middleware.
*/
init(options: HonoConfigType<E, P, I>): void;
/**
* Method to fetch a client's hit count and reset time.
*
* @param key {string} - The identifier for a client.
*
* @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
*/
get(key: string): Promise<ClientRateLimitInfo | undefined>;
/**
* Method to increment a client's hit counter.
*
* @param key {string} - The identifier for a client
*
* @returns {ClientRateLimitInfo} - The number of hits and reset time for that client
*/
increment(key: string): Promise<ClientRateLimitInfo>;
/**
* Method to decrement a client's hit counter.
*
* @param key {string} - The identifier for a client
*/
decrement(key: string): Promise<void>;
/**
* Method to reset a client's hit counter.
*
* @param key {string} - The identifier for a client
*/
resetKey(key: string): Promise<void>;
}
type UnstorageInstance = {
get: (key: string) => Promise<any>;
set: (key: string, value: any) => Promise<void>;
remove: (key: string) => Promise<void>;
};
/**
* A `Store` that stores the hit count for each client using Unstorage
*
* {@link https://unstorage.unjs.io/}
*/
declare class UnstorageStore<E extends Env$1 = Env$1, P extends string = string, I extends Input$1 = Input$1> implements Store<E, P, I> {
/**
* The duration of time before which all hit counts are reset (in milliseconds).
*/
windowMs: number;
/**
* The text to prepend to the key in Unstorage.
*/
prefix: string;
/**
* The unstorage storage instance.
*/
storage: UnstorageInstance;
/**
* @constructor for `UnstorageStore`.
*
* @param options {Options} - The configuration options for the store.
*/
constructor(options: {
storage: UnstorageInstance;
prefix?: string;
});
/**
* Method to prefix the keys with the given text.
*
* @param key {string} - The key.
*
* @returns {string} - The text + the key.
*/
prefixKey(key: string): string;
/**
* Method that actually initializes the store.
*
* @param options {HonoConfigType} - The options used to setup the middleware.
*/
init(options: HonoConfigType<E, P, I>): void;
/**
* Method to fetch a client's hit count and reset time.
*
* @param key {string} - The identifier for a client.
*
* @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
*/
get(key: string): Promise<ClientRateLimitInfo | undefined>;
/**
* Method to increment a client's hit counter. If the current time is within an active window,
* it increments the existing hit count. Otherwise, it starts a new window with a hit count of 1.
*
* @param key {string} - The identifier for a client
*
* @returns {ClientRateLimitInfo} - An object containing:
* - totalHits: The updated number of hits for the client
* - resetTime: The time when the current rate limit window expires
*/
increment(key: string): Promise<ClientRateLimitInfo>;
/**
* Method to decrement a client's hit counter. Only decrements if there is an active time window.
* The hit counter will never go below 0.
*
* @param key {string} - The identifier for a client
* @returns {Promise<void>} - Returns void after attempting to decrement the counter
*/
decrement(key: string): Promise<void>;
/**
* Method to reset a client's hit counter.
*
* @param key {string} - The identifier for a client
*/
resetKey(key: string): Promise<void>;
/**
* Method to update a record.
*
* @param key {string} - The identifier for a client.
* @param payload {ClientRateLimitInfo} - The payload to update.
*/
private updateRecord;
}
/**
*
* Create an instance of ws based rate-limiting middleware for Hono.
*
* @param config {WSConfigProps} - Options to configure the rate limiter.
*
* @returns - The middleware that rate-limits clients based on your configuration.
*
* @public
*/
declare function webSocketLimiter<E extends Env = Env, P extends string = string, I extends Input = Input>(config: WSConfigProps<E, P, I>): (createEvents: (c: Context<E, P, I>) => WSEvents | Promise<WSEvents>) => (c: Context<E, P, I>) => Promise<WSEvents>;
export { MemoryStore, RedisStore, UnstorageStore, rateLimiter, webSocketLimiter };
export type { ClientRateLimitInfo, CloudflareConfigProps, CloudflareConfigType, ConfigProps, ConfigType, HonoConfigProps, HonoConfigType, Options, Promisify, RateLimitExceededEventHandler, RateLimitInfo, RedisClient, RedisReply, Store, UnstorageInstance, WSConfigProps, WSConfigType, WSRateLimitExceededEventHandler, WSStatusCode };