import { type LazyRedisClient } from "@voyant-travel/utils/redis-client"; import type { MiddlewareHandler } from "hono"; /** * Distributed-capable rate limiting (security finding C2). * * The limiter is split into two halves: * * - a {@link RateLimitStore} — the counting backend. Composition injects the * selected provider; an in-memory Map remains the zero-configuration * fallback for Node/dev/tests. * - the enforcement surface — the {@link rateLimit} Hono middleware and the * imperative {@link enforceRateLimit} helper that route packages * (storefront, bookings, …) call directly inside handlers. * * Keys always carry a client dimension: `lim::` where * `clientKey` is derived by {@link clientIpKey} (`cf-connecting-ip`, then * the first hop of `x-forwarded-for`, else `"anon"`). Window-keying is the * store's responsibility — the KV backend appends `:` so its * stored keys read `lim:::`; the CF binding * tracks its own configured period per key; the memory store keeps a * `resetAt` per key. * * `createApp` mounts default policies (tight on `/auth/*` POSTs, moderate * on unauthenticated public writes) — see `config.rateLimit` in types.ts. */ /** Result of a single limit check against a {@link RateLimitStore}. */ export interface RateLimitResult { allowed: boolean; /** Requests left in the current window, when the backend can tell. */ remaining?: number; /** Seconds until the caller should retry, when the backend can tell. */ retryAfterSeconds?: number; } /** * A counting backend for the limiter. `limit()` records one hit for `key` * and reports whether the caller is still within `max` per `windowSeconds`. */ export interface RateLimitStore { limit(key: string, opts: { max: number; windowSeconds: number; }): Promise; } /** A named limit applied to one logical traffic class. */ export interface RateLimitPolicy { /** * Namespace for the counter — requests in different buckets never share * a window (e.g. `"auth"`, `"public-write"`, `"booking-lookup"`). */ bucket: string; /** Maximum requests per window per client. */ max: number; /** Window length in seconds. */ windowSeconds: number; /** * Explicit backend. When omitted, {@link enforceRateLimit} resolves one * from the environment via {@link resolveRateLimitStore}. */ store?: RateLimitStore; /** Override the client dimension (defaults to {@link clientIpKey}). */ clientKey?: (c: RateLimitRequestContext) => string; } /** * `createApp({ rateLimit })` configuration. Defaults (when the key is * omitted entirely) are: `auth` = 10 POSTs/min/IP on `/auth/*`, * `publicWrite` = 60 writes/min/IP on `/v1/public/*` + `publicPaths`. * Set the whole config to `false` to disable, or set an individual * policy to `false` to disable just that policy. */ export interface RateLimitConfig { /** * Explicit store, or a function-of-bindings for stores built from env * bindings. When omitted — or when the function returns `undefined` — * resolution falls through to the injected `RATE_LIMIT_STORE`, then * in-memory. */ store?: RateLimitStore | ((env: unknown) => RateLimitStore | undefined); /** POSTs to `/auth/*`. Default `{ max: 10, windowSeconds: 60 }`. */ auth?: false | RateLimitRule; /** * Writes (POST/PUT/PATCH/DELETE) to `/v1/public/*` and to * `config.publicPaths`. Default `{ max: 60, windowSeconds: 60 }`. */ publicWrite?: false | RateLimitRule; } /** A bare max-per-window pair for the built-in `createApp` policies. */ export interface RateLimitRule { max: number; windowSeconds: number; } /** * Minimal structural view of a Hono context — enough for key derivation, * store resolution, and response headers — so route packages can call * {@link enforceRateLimit} without generics gymnastics. */ export interface RateLimitRequestContext { req: { method: string; url: string; header(name: string): string | undefined; }; env: unknown; header(name: string, value: string): void; } /** * Derive the client dimension for rate-limit keys: `cf-connecting-ip` * (set by Cloudflare, not spoofable through the edge), `x-real-ip`, then the * first hop of `x-forwarded-for`, else `"anon"`. Exported so route packages * key their own limiters and idempotency scopes consistently. */ export declare function clientIpKey(c: { req: { header(name: string): string | undefined; }; }): string; /** * In-memory fixed-window store for Node, dev, and tests. Per-isolate — * on Workers every isolate counts independently, so treat it as a last * resort (still vastly better than nothing: an abusive client hammering * one isolate is throttled by that isolate). Expired windows are pruned * periodically on access; a hard `maxEntries` cap bounds memory. */ export declare function createMemoryRateLimitStore(options?: { maxEntries?: number; }): RateLimitStore; export interface RedisRateLimitStoreOptions { client?: LazyRedisClient; keyPrefix?: string; } export declare function createRedisRateLimitStore(redisUrl: string, options?: RedisRateLimitStoreOptions): RateLimitStore; /** Test-only: re-arm the once-per-isolate missing-store warning. */ export declare function resetRateLimitWarningsForTests(): void; /** * Resolve the best available store from the environment: * Injected `c.env.RATE_LIMIT_STORE` → in-memory fallback. **Fails open into * the memory store** — * rate limiting must never break Node/headless deployments that bind * neither — but warns once per isolate outside dev/test so a * production deploy without a distributed backend is visible in logs. */ export declare function resolveRateLimitStore(c: { env: unknown; }, memoryFallback?: RateLimitStore): RateLimitStore; /** * Imperative limit check for route handlers. Records one hit for the * calling client against `policy` and returns `null` when allowed or a * ready-to-return `429` Response (with `Retry-After` and the * `X-RateLimit-*` headers the backend can populate) when over the limit. * * Fails open when the store itself errors — a broken KV namespace must * not take the API down. * * @example * const limited = await enforceRateLimit(c, { * bucket: "booking-lookup", * max: 20, * windowSeconds: 60, * }) * if (limited) return limited */ export declare function enforceRateLimit(c: RateLimitRequestContext, policy: RateLimitPolicy): Promise; /** * Hono middleware form of {@link enforceRateLimit}. Mount on a route or * group: * * app.post("/v1/public/leads", rateLimit({ bucket: "leads", max: 30, windowSeconds: 60 }), handler) */ export declare function rateLimit(policy: RateLimitPolicy): MiddlewareHandler; /** * @deprecated Legacy constants from the pre-C2 limiter, retained for * import compatibility. Configure limits per policy instead. */ export declare const LIVE_LIMITS: { readonly burst: 30; readonly rpm: 3000; };