@beignet/core
Version:
Core framework primitives for Beignet
119 lines • 5.26 kB
TypeScript
/**
* Rate limit hooks for @beignet/core/server
*/
import type { RateLimitScope } from "../../contracts/index.js";
import { type ActivityActor, type RateLimitPort } from "../../ports/index.js";
import { type TrustedProxyClientIpSource, type TrustedProxyConfig } from "../trusted-proxy.js";
import type { HttpRequestLike, ServerHook } from "../types.js";
/**
* Ports required by rate-limit hooks.
*/
export type RateLimitPorts = {
rateLimit: RateLimitPort;
};
/**
* Minimal context shape required for user-scoped rate limits.
*/
export type CtxWithRateLimit = {
ports: RateLimitPorts;
actor?: ActivityActor;
};
type EarlyRateLimitScope = Exclude<RateLimitScope, "user">;
/**
* Strategy for resolving the client IP used by `ip`-scoped limits.
*
* - `"none"`: do not trust request headers for IP resolution. Every request
* to one contract shares that contract's unknown-client bucket; this is the
* explicit opt-out for apps that declare `ip` scopes without a trusted
* client-IP source.
* - `"x-forwarded-for-last"`: the last `x-forwarded-for` entry. Use this only
* when the app is always behind a trusted reverse proxy that appends the
* socket address.
* - `"x-forwarded-for-first"`: the first `x-forwarded-for` entry. This value
* is client-controlled, so only use it when a trusted edge normalizes the
* header before it reaches the app.
* - `"x-real-ip"` and `"cf-connecting-ip"`: dedicated platform headers.
* - A function receives the raw request and returns the client IP, for
* platform-specific resolution.
*/
export type RateLimitIpSource = "none" | TrustedProxyClientIpSource;
/**
* Options for `createRateLimitHooks(...)`.
*/
export interface RateLimitOptions<Ctx> {
/**
* Build a rate-limit key after context exists.
*
* This is used for user-scoped limits and any late key strategy. The
* returned value is the complete key and is not automatically namespaced by
* contract.
*/
key?: (args: {
ctx: Ctx;
req: HttpRequestLike;
scope: RateLimitScope;
}) => string;
/**
* Build a rate-limit key before request parsing and context creation.
*
* This is used for global and IP-scoped limits. The returned value is the
* complete key and is not automatically namespaced by contract.
*/
earlyKey?: (args: {
req: HttpRequestLike;
scope: EarlyRateLimitScope;
}) => string;
/**
* Resolve the client IP for `ip`-scoped limits.
*
* There is no default: when any contract declares an `ip`-scoped rate limit
* and neither the server-level `trustedProxy.clientIp`,
* this hook's `trustedProxy.clientIp`, `ipSource`, nor a custom `earlyKey`
* is configured, the hook fails at startup. Prefer the server-level policy
* for production proxy headers; keep these hook-local options for an
* intentional override or custom keying.
*/
ipSource?: RateLimitIpSource;
/**
* Hook-local trusted-proxy policy used to resolve client IPs when
* `ipSource` is not set. This overrides the server-level policy.
*
* Configure this only when the app is always behind a platform or reverse
* proxy that strips or normalizes forwarding headers. For `ip`-scoped rate
* limits, set `trustedProxy.clientIp` to the header source written by that
* trusted edge.
*/
trustedProxy?: TrustedProxyConfig;
}
/**
* Create metadata-driven rate-limit hooks.
*
* The hook reads `contract.metadata.rateLimit`. Global and IP-scoped limits run
* in `onRequest` before context creation; user-scoped limits run in
* `beforeHandle` after route hooks have resolved identity and `ctx.actor` is
* available. Default keys include the contract name so unrelated contracts do
* not share counters. A user-scoped limit without a resolved user actor fails
* with `AuthUnauthorizedError` instead of falling back to a global bucket.
* Exceeded limits throw the framework `TooManyRequests` app error
* with `scope`, `retryAfterSeconds`, and `resetAt` details, and the 429
* response carries a `Retry-After` header when the limiter reports a reset
* time. The bucket key is
* never sent to clients; denials emit a `rateLimit.denied` instrumentation
* event that carries the key for operators.
*
* `ip`-scoped limits require an explicit `trustedProxy.clientIp`, `ipSource`,
* or custom `earlyKey`: the hook's `validate` phase fails `createServer(...)`
* startup when a registered contract declares an `ip` scope without one,
* instead of silently collapsing all clients into one shared bucket.
* Contracts added later through `server.route(...)` are not visible to
* `validate`, so enforcing an `ip`-scoped limit without a client-IP source
* throws the same configuration error at request time as a backstop. Pass
* `ipSource: "none"` to explicitly opt in to one unknown-client bucket per
* contract.
*
* @param options - Optional key builders and client-IP source.
* @returns A server hook backed by `ctx.ports.rateLimit`.
*/
export declare function createRateLimitHooks<Ctx extends CtxWithRateLimit>(options?: RateLimitOptions<Ctx>): ServerHook<Ctx, RateLimitPorts>;
export {};
//# sourceMappingURL=rate-limit.d.ts.map