/** * In-process request rate limiting keyed by principal or client address. * * Weft's request pipeline previously capped only body size (1 MB) and JSON-RPC * batch length (100 items); a single principal or IP could otherwise flood the * server with unbounded request volume. This limiter adds a configurable * fixed-window counter so a hostile or runaway caller is throttled with HTTP * 429 once it exceeds its budget. * * **Scope and trust posture.** This is a single-process, in-memory limiter — it * is a load-shedding guardrail, not a distributed quota. Behind multiple server * instances each process keeps its own counters; deployments that need a global * budget should still front Weft with a shared reverse-proxy limiter. The * in-process limiter exists so a single instance cannot be trivially flooded * even when no proxy is present. * * The window clock is injectable so tests advance time deterministically rather * than sleeping against the wall clock. * * @module server/authentication/rate-limiter */ /** * Configuration for the request rate limiter, supplied via * `serve({ rateLimit })`. Omitting `rateLimit` entirely disables limiting (the * historical behavior), so it is opt-in and never silently changes an existing * deployment. * * @example * ```ts * import { type RateLimitConfig } from '@lostgradient/weft/server'; * * const rateLimit: RateLimitConfig = { * maxRequests: 100, * windowMs: 60_000, * }; * void rateLimit; * ``` */ export type RateLimitConfig = { /** Maximum requests permitted per key within each `windowMs` window. Must be a positive integer. */ maxRequests: number; /** Length of the fixed window in milliseconds. Must be positive. */ windowMs: number; /** * Upper bound on the number of distinct keys tracked at once. When exceeded, * the oldest-expiring entries are evicted so a high-cardinality flood of * unique keys cannot grow memory without bound. Defaults to 10 000. */ maxTrackedKeys?: number; }; /** * Outcome of a single {@link RateLimiter.check} call. * * @example * ```ts * import { type RateLimitDecision } from '@lostgradient/weft/server'; * * function describe(decision: RateLimitDecision): string { * return decision.allowed ? 'ok' : `retry after ${decision.retryAfterSeconds}s`; * } * void describe; * ``` */ export type RateLimitDecision = { /** Whether the request is within budget and may proceed. */ allowed: boolean; /** Requests still available in the current window after this one. */ remaining: number; /** Seconds until the current window resets — surfaced as the `Retry-After` header on a 429. */ retryAfterSeconds: number; /** The configured per-window ceiling, surfaced as `X-RateLimit-Limit`. */ limit: number; }; /** * A request rate limiter. Call {@link RateLimiter.check} once per request with * a stable key (principal subject when authenticated, otherwise client * address). `dispose` clears the internal state and is invoked on server * shutdown. * * @example * ```ts * import { createRateLimiter, type RateLimiter } from '@lostgradient/weft/server'; * * const limiter: RateLimiter = createRateLimiter({ maxRequests: 2, windowMs: 1_000 }); * console.log(limiter.check('caller-1').allowed); // true * console.log(limiter.check('caller-1').allowed); // true * console.log(limiter.check('caller-1').allowed); // false * limiter.dispose(); * ``` */ export type RateLimiter = { check(key: string): RateLimitDecision; dispose(): void; }; /** * Validate a {@link RateLimitConfig}, throwing on invalid values so a * misconfiguration fails fast at `serve()` time rather than admitting an * accidentally-unlimited or always-zero limiter. * * @example * ```ts * import { validateRateLimitConfig } from '@lostgradient/weft/server'; * * validateRateLimitConfig({ maxRequests: 100, windowMs: 60_000 }); * console.log('config is valid'); * ``` */ export declare function validateRateLimitConfig(config: RateLimitConfig): void; /** * Create a fixed-window {@link RateLimiter}. The optional `now` clock is for * deterministic testing; production callers omit it and the limiter reads * `Date.now()`. * * @example * ```ts * import { createRateLimiter } from '@lostgradient/weft/server'; * * let clock = 0; * const limiter = createRateLimiter({ maxRequests: 1, windowMs: 1_000 }, () => clock); * console.log(limiter.check('a').allowed); // true * console.log(limiter.check('a').allowed); // false — window not yet reset * clock = 1_000; * console.log(limiter.check('a').allowed); // true — window rolled over * ``` */ export declare function createRateLimiter(config: RateLimitConfig, now?: () => number): RateLimiter;