import { type RequestContext } from '@basaltkit/core'; import type { HttpRequest } from './route.js'; export interface RateLimitResult { allowed: boolean; limit: number; remaining: number; resetAt: number; retryAfterMs: number; } /** * Backing store for the rate limiter (default in-memory; swap `RedisRateLimitStore` * to share limits across instances). Methods may be sync or async — the limiter * awaits them — so an in-process store stays synchronous while a Redis one doesn't. */ export interface RateLimitStore { hit(key: string, limit: number, windowMs: number): RateLimitResult | Promise; reset(key: string): void | Promise; } export interface MemoryRateLimitStoreOptions { clock?: () => number; /** * Most buckets kept at once (default 100 000). Past it, expired buckets are * swept and, if the store is still full, the oldest windows are evicted first * — so a flood of distinct client addresses (IPv6 makes them cheap) costs * bounded memory instead of growing the process until it dies. Evicting a * live window resets that client's count, so size it well above your real * distinct-client count per window; use `RedisRateLimitStore` across * instances. */ maxEntries?: number; /** How often, at most, a hit sweeps every expired bucket (default 60 000 ms). */ sweepIntervalMs?: number; } /** Default cap on in-memory rate-limit buckets. */ export declare const DEFAULT_RATE_LIMIT_MAX_ENTRIES = 100000; export declare class MemoryRateLimitStore implements RateLimitStore { private readonly windows; private readonly clock; private readonly maxEntries; private readonly sweepIntervalMs; private nextSweepAt; constructor(clockOrOptions?: (() => number) | MemoryRateLimitStoreOptions); /** Buckets currently held (live or not yet swept). */ get size(): number; hit(key: string, limit: number, windowMs: number): RateLimitResult; reset(key: string): void; private sweep; private makeRoom; } export interface RateLimitOptions { limit: number; windowMs: number; store?: RateLimitStore; key?: (request: HttpRequest) => string; skip?: (request: HttpRequest) => boolean; } export interface CorsOptions { origin?: boolean | string | string[] | ((origin: string | undefined) => boolean); methods?: string[]; allowedHeaders?: string[]; exposedHeaders?: string[]; credentials?: boolean; maxAge?: number; } export interface SecurityHeadersOptions { hsts?: boolean | { maxAge?: number; includeSubDomains?: boolean; preload?: boolean; }; contentTypeOptions?: boolean; frameOptions?: 'DENY' | 'SAMEORIGIN' | false; referrerPolicy?: string | false; /** * Content-Security-Policy value. Defaults to {@link DEFAULT_CSP} (a lock-down * policy fit for a JSON API); pass a string to use your own, or `false` to * omit the header entirely. */ contentSecurityPolicy?: string | false; crossOriginOpenerPolicy?: string | false; /** * Cache-Control value. Defaults to `no-store`: API responses carry session * tokens, API keys and MFA secrets that no browser or intermediary cache may * keep. A route that is safe to cache sets its own header (it replaces this * one); pass a string to change the default, or `false` to omit it. */ cacheControl?: string | false; } /** Default Cache-Control for API responses. */ export declare const DEFAULT_CACHE_CONTROL = "no-store"; /** Restrictive default CSP for a JSON API: it renders nothing and frames nothing. */ export declare const DEFAULT_CSP = "default-src 'none'; frame-ancestors 'none'"; /** * Who a per-route bucket belongs to (`meta.rateLimit.key`): * * - `'ip'` (default) — the client address (`request.ip`), as before. * - `'user'` — `ctx().user.id`: users behind one NAT/proxy no longer share a budget. * - `'tenant'` — `ctx().tenant.id`: every user of a tenant shares one budget. * - `'user+tenant'` — one budget per user per tenant. * - a function of `ctx()` returning the bucket id. * * Resolved after enrichers ran, so auth/tenancy have set `ctx()`. When the id * is missing (anonymous caller, no tenant resolved, the function returns * nothing) the bucket falls back to the client IP — never to one shared * bucket, and never mixed with identified callers' buckets. */ export type RateLimitKey = 'ip' | 'user' | 'tenant' | 'user+tenant' | ((context: RequestContext) => string | undefined | null); /** * Per-route rate-limit override, read from a route's `meta.rateLimit`. When set, * that route gets its own bucket (keyed by `key` + route) at these thresholds * instead of the global default — so login/reset can be stricter than the rest. */ export interface RouteRateLimit { limit: number; windowMs: number; /** Who the bucket belongs to. Default `'ip'`. See {@link RateLimitKey}. */ key?: RateLimitKey; } export interface SecurityPluginOptions { rateLimit?: RateLimitOptions | false; cors?: CorsOptions | false; headers?: SecurityHeadersOptions | boolean; } /** * Edge security — rate limiting, CORS and secure response headers — as a neutral * pre-hook, so it runs identically on Fastify, Express and Hono. */ export declare function securityPlugin(options?: SecurityPluginOptions): import("@basaltkit/core").BasaltPlugin;