UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

119 lines 5.26 kB
/** * 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