import { R as RateLimitStore } from './types-BK5pGTGr.js'; /** * Minimal abstraction that every framework adapter must satisfy. * * The core rate-limiter only interacts with the request/response through * this interface, making it fully framework-agnostic. * * @example Express adapter * ```ts * const ctx: RateShieldContext = { * req, * setHeader: (name, value) => { if (!res.headersSent) res.setHeader(name, value) }, * isResponseSent: () => res.headersSent, * block: (_code, body) => { if (!res.headersSent) res.status(429).json(body) }, * pass: () => next(), * } * ``` */ interface RateShieldContext> { /** The raw framework request object — passed to all option callbacks. */ readonly req: TReq; /** Write a response header. Should no-op if the response is already sent. */ setHeader(name: string, value: string | number): void; /** Return `true` if the response has already been flushed. */ isResponseSent(): boolean; /** * Send the blocked response. * @param statusCode HTTP status (always 429). * @param body Serialisable payload to send as the response body. */ block(statusCode: number, body: unknown): void; /** Allow the request to proceed (call framework's `next()`). */ pass(): void; } type RateLimitStrategy = 'fixed' | 'sliding'; interface BlockedEvent> { /** The key that was blocked. */ key: string; req: TReq; /** Duration of this block in milliseconds. */ blockDuration: number; /** Cumulative block count for this key (including this one). */ blockCount: number; /** Epoch ms when the block expires. */ unblockTime: number; } interface LimitReachedEvent> { /** The key that just consumed the last available slot. */ key: string; req: TReq; /** Request count after this request (equals maxRequests). */ count: number; limit: number; } interface AllowedEvent> { key: string; req: TReq; /** Request count after this request. */ count: number; /** Slots remaining after this request. */ remaining: number; } /** * Full snapshot passed to `onRecord` after every request — allowed and blocked. * * @example * ```ts * createRateLimiter({ * onRecord: async (data) => { * await db.rateLimits.upsert({ id: data.key }, data) * }, * extractRequestData: (req) => ({ * userId: req.user?.id, * route: req.path, * }), * }) * ``` */ interface RecordData> { key: string; req: TReq; strategy: RateLimitStrategy; /** Requests counted in the current window (after this request). */ count: number; /** Slots remaining after this request. */ remaining: number; /** Epoch ms when the window or block resets. */ resetTime: number; isBlocked: boolean; /** * `true` when the request *would* have been blocked but `dryRun` is enabled. */ dryRunBlocked: boolean; blockCount?: number; blockExpiresAt?: number; /** Unix epoch ms when this request was processed. */ timestamp: number; /** * Custom fields extracted via `extractRequestData`. * `undefined` when `extractRequestData` is not configured. */ requestData?: Record; } interface RateLimitInfo { key: string; strategy: RateLimitStrategy; count: number; remaining: number; /** Epoch ms when the window or block resets. */ resetTime: number; isBlocked: boolean; blockExpiresAt?: number; blockCount?: number; } interface RateLimitStats { strategy: RateLimitStrategy; /** Unique keys currently being tracked (not blocked). */ trackedKeys: number; /** Keys currently serving an active penalty block. */ blockedKeys: number; windowMs: number; maxRequests: number; } interface RateLimitOptions> { /** * Rate-limiting algorithm. * - `'fixed'` — classic counter that resets every `windowMs`. * - `'sliding'` — rolling timestamp log; burst-safe and more accurate. * @default 'sliding' */ strategy?: RateLimitStrategy; /** Window size in milliseconds. @default 60_000 */ windowMs?: number; /** Maximum requests allowed inside the window. @default 15 */ maxRequests?: number; /** First-offense block duration in milliseconds. @default 1_200_000 (20 min) */ initialBlockMs?: number; /** * Response body sent with every 429. * Objects are JSON-serialized and merged with `message` and `retryAfter` fields. */ message?: string | object; /** * Derive a unique rate-limit key from the request. * When using `rate-shield/express` this defaults to the real client IP * (proxy-aware via `X-Forwarded-For`). * When using the core directly you must supply this. */ keyGenerator?: (req: TReq) => string; /** Print internal activity to stdout. @default false */ enableLogger?: boolean; /** * Return `true` to bypass the limiter for a specific request. * May be async. */ skip?: (req: TReq) => boolean | Promise; /** * Storage backend. * * Defaults to a new `MemoryStore` instance using `maxKeys`. * Provide your own implementation to share state across processes or persist * it across restarts (Redis, MongoDB, …). * * @example * ```ts * import { MemoryStore } from 'rate-shield/stores' * createRateLimiter({ store: new MemoryStore({ maxKeys: 50_000 }) }) * ``` */ store?: RateLimitStore; /** * Maximum unique keys in the default `MemoryStore`. * Ignored when a custom `store` is provided. * @default 10_000 */ maxKeys?: number; /** * Keys — or a predicate — that are never rate-limited. * * @example ['127.0.0.1', '::1'] * @example (key, req) => req.headers['x-internal'] === process.env.INTERNAL_SECRET */ whitelist?: string[] | ((key: string, req: TReq) => boolean); /** * Track state and fire callbacks without ever sending a 429. * @default false */ dryRun?: boolean; /** * Set to `false` to omit all `X-RateLimit-*` and `Retry-After` response headers. * @default true */ headers?: boolean; /** * Called after **every** request — allowed and blocked — with a complete * rate-limit snapshot. Use this to persist data to your own database. * * Errors thrown here are caught and logged; they never propagate. */ onRecord?: (data: RecordData) => void | Promise; /** * Extract custom fields from the request to include in the `onRecord` payload. * Only called when `onRecord` is configured. */ extractRequestData?: (req: TReq) => Record; /** Fired every time a key is newly blocked. */ onBlocked?: (event: BlockedEvent) => void | Promise; /** * Fired when a key takes the last available slot. * The *next* request from this key will be blocked. */ onLimitReached?: (event: LimitReachedEvent) => void | Promise; /** Fired for every request that is allowed through. */ onRequestAllowed?: (event: AllowedEvent) => void | Promise; } /** * Framework-agnostic rate-limiter instance returned by the core factory. * * To use with a specific framework, pass this to its adapter or use a * framework-specific factory (e.g. `rate-shield/express`). */ interface RateLimiterCore> { /** * Process a single request through the rate limiter. * Called by framework adapters with a populated `RateShieldContext`. */ handle(ctx: RateShieldContext): Promise; /** * Stop the background cleanup timer and destroy all store state. * Safe to call multiple times. */ destroy(): void | Promise; /** Clear all tracking and block data for a single key. */ reset(key: string): void | Promise; /** Clear all state — equivalent to restarting the limiter. */ resetAll(): void | Promise; /** * Current rate-limit snapshot for a key. * Returns `null` if the key has never made a request. */ getInfo(key: string): Promise; /** Aggregate runtime stats for this instance. */ getStats(): Promise; } export type { AllowedEvent as A, BlockedEvent as B, LimitReachedEvent as L, RateLimitOptions as R, RateLimiterCore as a, RateLimitInfo as b, RateLimitStats as c, RateLimitStrategy as d, RateShieldContext as e, RecordData as f };