/** * Per-IP / per-token rate limiter for the public MCP HTTP endpoint. * * Backend: Upstash Redis REST API. We use raw fetch (no SDK dep) because: * - It runs cleanly on Vercel serverless without bundler tweaks. * - Upstash's REST surface is small and stable. * * Algorithm: fixed window. INCR a counter keyed by `prefix:bucket:windowStart` * and set EXPIRE on first increment. Cheap, accurate within ±1 window, and * Upstash bills per command — INCR is a single command. * * Fail-open policy: if UPSTASH_REDIS_REST_URL or UPSTASH_REDIS_REST_TOKEN is * unset (local dev, tests, preview deploys without secrets), the limiter * returns { ok: true } without contacting any backend. This is intentional: * a misconfigured limiter must NOT block production traffic, only relax it. * * Configuration (all env, all optional): * UPSTASH_REDIS_REST_URL e.g. https://us1-foo-12345.upstash.io * UPSTASH_REDIS_REST_TOKEN bearer token * RATE_LIMIT_ANON_PER_MIN default 60 * RATE_LIMIT_BEARER_PER_MIN default 200 * RATE_LIMIT_STRICT_PER_MIN default 5 (credential endpoints) * * Vercel's "Upstash for Redis" Marketplace integration with a custom prefix * injects env vars in the form UPSTASH_REDIS_KV_REST_API_URL / * UPSTASH_REDIS_KV_REST_API_TOKEN. Those names are also accepted as a * fallback so the limiter works without manual env-var aliasing. */ export type RateKind = "anon" | "bearer" | "strict"; export interface RateLimitResult { ok: boolean; /** When ok=false, suggested seconds until the caller can retry. */ retryAfterSec?: number; /** Configured limit for this kind. Undefined when limiter is disabled. */ limit?: number; /** Remaining quota in the current window. Undefined when disabled. */ remaining?: number; } export interface RateLimitDeps { /** Defaults to globalThis.fetch — injectable for tests. */ fetch?: typeof fetch; /** Defaults to Date.now — injectable for tests. */ now?: () => number; /** Defaults to process.env. */ env?: NodeJS.ProcessEnv; } /** * Check (and increment) the rate limit for `kind`+`identifier`. * Returns { ok: true } if the request is allowed, { ok: false, retryAfterSec } * if it should be rejected with HTTP 429. */ export declare function checkRateLimit(kind: RateKind, identifier: string, deps?: RateLimitDeps): Promise; /** * Stable, low-PII identifier for an anonymous caller. Falls back to "unknown" * when no IP-bearing header is present (e.g. local stdin tests). * * Vercel sets x-forwarded-for; the first comma-separated value is the client. */ export declare function anonIdentifier(headers: Record): string; /** * Identifier derived from a Bearer token. We hash the token (FNV-1a 32-bit) so * the rate-limit key never contains the secret — Upstash logs and our own * structured logs would otherwise leak it. * * FNV-1a is non-cryptographic but is sufficient as a key-bucketing function: * an attacker cannot use a hash collision to bypass the limiter (collisions * land in the same bucket, increasing the count). */ export declare function bearerIdentifier(authorization: string | undefined): string;