import type { Middleware } from "@nifrajs/core/server"; export interface RateLimitResult { /** Hits recorded in the current window, including this one. */ readonly count: number; /** Epoch-ms when the current window resets. */ readonly resetAt: number; } /** * Counter backend. Production deploys MUST use a shared store (Redis, etc.) so the * limit holds across instances - that's a user dependency, not ours, hence the * interface. {@link MemoryStore} is for dev / single-instance only. */ export interface RateLimitStore { hit(key: string, windowMs: number): Promise; } export interface MemoryStoreOptions { /** Allow the in-memory store in production. Off by default - a per-instance limiter is unsafe across instances. */ readonly allowInProduction?: boolean; /** Hard cap on tracked client keys; expired keys are evicted first, then oldest active keys. Default `100_000`. * Bounds memory against an unbounded key space (bot scans, per-IP buckets). */ readonly maxKeys?: number; /** Minimum interval (ms) between amortized sweeps of expired windows. Default `30_000`. */ readonly sweepIntervalMs?: number; } /** * In-process fixed-window store. Refuses to run in production unless explicitly allowed. * * Bounded against unbounded growth: expired windows are swept lazily (amortized, at most once per * `sweepIntervalMs`) and the key set is hard-capped at `maxKeys` (expired keys are evicted first, * then oldest active keys). * Without this, expired entries for keys never seen again - and the unbounded key space of a bot scan * - would accumulate in the map forever and OOM a single-instance deploy. */ export declare class MemoryStore implements RateLimitStore { private readonly windows; private readonly maxKeys; private readonly sweepIntervalMs; private lastSweep; private evictionScans; /** Cumulative entries inspected by the eviction scan since construction - an eviction-pressure * gauge. The bounded scan keeps this ~O(1) per over-cap insert (≤ {@link MAX_EVICTION_SCAN}); a * regressed full O(n) sweep would make it ~maxKeys per insert (what the regression test asserts). */ get evictionScanCount(): number; constructor(options?: MemoryStoreOptions); hit(key: string, windowMs: number): Promise; private sweepExpired; private enforceMaxKeys; } export interface RateLimitOptions { /** Where counters live. `MemoryStore` for dev; a shared store in production. */ readonly store: RateLimitStore; /** Max requests allowed per window. */ readonly max: number; /** Window length, in milliseconds. */ readonly windowMs: number; /** * How many trusted reverse proxies sit in front of the app and append to `X-Forwarded-For`. * Default `0`. * * The default key reads the client IP from `X-Forwarded-For` as the address your **edge** proxy * observed - the entry `trustedProxies` from the right (1 proxy → the rightmost hop; 2 → the * second-from-right; …). Your proxies append on the right, so a client can only inject fake hops on * the *left*, which this skips → not spoofable when `trustedProxies` matches your topology. * * ⚠️ With the default `0`, `X-Forwarded-For` is treated as fully client-controlled and **ignored**. * Reading the * *first* XFF hop - the old behavior - let any client mint a fresh bucket per request and defeat the * limiter. Set `trustedProxies` (only safe behind a proxy you control that appends XFF), configure a * trusted single-IP {@link header}, or supply a custom {@link key} (e.g. an authenticated user id). */ readonly trustedProxies?: number; /** Exact trusted single-IP header, e.g. an infra-set `x-real-ip`. Not read unless configured. */ readonly header?: string; /** * Allow one shared bucket when no per-request key can be derived. Off by default because it lets one * client consume the quota for everyone. Enable only for intentional global throttles. */ readonly allowGlobalKey?: boolean; /** * Bucket key for a request. Overrides the default XFF-based key entirely - set this for accurate * per-client limiting (e.g. an authenticated user id, or a header your proxy sets). A `Middleware` * can't see the socket IP (that needs the server instance). */ readonly key?: (req: Request) => string; } /** * Rate limiting as a {@link Middleware}. Runs in `onRequest` (before routing, so it * also covers 404s); over the limit → `429` + `Retry-After`. Every response carries * `RateLimit-Limit/Remaining/Reset` (added in `onResponse`, keyed off the request). */ export declare function rateLimit(options: RateLimitOptions): Middleware; //# sourceMappingURL=rate-limit.d.ts.map