/** * Generate a signed CSRF token: `.`. * The random half is the token proper; the HMAC binds it to the server secret * so an attacker who plants a fake cookie can't forge the tag. * * @param {string | Buffer} secret — at least 32 bytes of entropy * @param {{ length?: number }} [options] * @returns {string} */ declare function generate(secret: string | Buffer, options?: { length?: number; }): string; /** * Verify a signed CSRF token. Returns true iff: * - both values are present and structurally valid, * - the two values are identical (timing-safe), * - the HMAC tag matches the current secret. * * Never throws for invalid input — a malformed or missing token is just an * unauthenticated request. Only throws `SecurityError` on programmer error * (bad secret). * * @param {unknown} fromCookie * @param {unknown} fromHeader * @param {string | Buffer} secret * @returns {boolean} */ declare function verify(fromCookie: unknown, fromHeader: unknown, secret: string | Buffer): boolean; /** * Generate an unsigned random token. Use only when there is no server secret * available — the check is a plain equality between cookie and header, so a * planted cookie will pass. `generate` is preferred. * * @param {{ length?: number }} [options] * @returns {string} */ declare function generateUnsigned(options?: { length?: number; }): string; /** * Verify an unsigned CSRF token — plain timing-safe equality of cookie and * form/header value. * * @param {unknown} fromCookie * @param {unknown} fromForm * @returns {boolean} */ declare function verifyUnsigned(fromCookie: unknown, fromForm: unknown): boolean; /** * Generate a session-bound CSRF token: `hmacBase64Url(sessionId, secret)`. * Requires no per-request storage — the value is derived from state the * server already has. When the session expires, so does the token. * * @param {string} sessionId — non-empty session identifier * @param {string | Buffer} secret * @returns {string} */ declare function generateForSession(sessionId: string, secret: string | Buffer): string; /** * Verify a session-bound CSRF token against the current session. * * @param {unknown} token * @param {string} sessionId * @param {string | Buffer} secret * @returns {boolean} */ declare function verifyForSession(token: unknown, sessionId: string, secret: string | Buffer): boolean; declare const index_d_generate: typeof generate; declare const index_d_generateForSession: typeof generateForSession; declare const index_d_generateUnsigned: typeof generateUnsigned; declare const index_d_verify: typeof verify; declare const index_d_verifyForSession: typeof verifyForSession; declare const index_d_verifyUnsigned: typeof verifyUnsigned; declare namespace index_d { export { index_d_generate as generate, index_d_generateForSession as generateForSession, index_d_generateUnsigned as generateUnsigned, index_d_verify as verify, index_d_verifyForSession as verifyForSession, index_d_verifyUnsigned as verifyUnsigned, }; } /** * Fixed-window rate limiter. * * Every window boundary resets the counter to zero. Cheap (one INCR per * request) but permits burst at boundary edges — a caller can spend the * whole budget in the last second of one window and the whole budget in * the first second of the next. Prefer sliding for user-facing APIs; fixed * is fine for "cheap-and-lax" scenarios like debounce or dedup. * * @param {import('../index.js').WindowLimiterConfig} config * @returns {import('../index.js').Limiter} */ declare function fixed(config: WindowLimiterConfig): Limiter; /** * Interpolated sliding-window rate limiter. * * Approximates a true sliding window using two fixed buckets — the current * window's counter plus a weighted slice of the previous window's counter, * where the weight is the fraction of the previous window still overlapping * with "now". This costs the same as fixed (~1 write) but eliminates the * boundary burst that plagues fixed windows. * * Accuracy: ~1% off ground truth in the worst case (Cloudflare / Kong / * Envoy all ship this variant as their default). For strict per-user API * quotas this is normally within a few requests of the true count. * * @param {import('../index.js').WindowLimiterConfig} config * @returns {import('../index.js').Limiter} */ declare function sliding(config: WindowLimiterConfig): Limiter; /** * Token-bucket rate limiter. * * A bucket of `capacity` tokens refills at `refillRate` tokens per second. * Every request consumes one token; if the bucket is empty, the request is * rejected. Allows *controlled burst* — an idle caller accumulates a full * bucket and can spend it at once, then drops to the steady rate. * * State is stored as a single JSON blob per key: `{ tokens, updatedAt }`. * Tokens are recomputed on read from the elapsed time — no background * timer needed. * * Because token accounting is more than a simple counter, this reads the * state, recomputes, and writes it back. When the store exposes the * optional atomic `compareAndSet` (the bundled memory and Redis stores * both do), the write is a CAS retried on contention — concurrent * requests can never double-spend a token. Stores without it fall back * to a last-writer-wins `set`, which can race under concurrency. * * @param {import('../index.js').BucketLimiterConfig} config * @returns {import('../index.js').Limiter} */ declare function tokenBucket(config: BucketLimiterConfig): Limiter; /** * Leaky-bucket rate limiter. * * The bucket accumulates requests up to `capacity`. Water leaks out at a * constant `leakRate` requests/second — the effective steady-state rate. * When the bucket is full, incoming requests are rejected. Unlike * token-bucket, there is **no burst tolerance**: outgoing rate is * strictly bounded by `leakRate`. * * Use for traffic shaping and protecting downstream services with a hard * throughput ceiling (SMTP relays, upstream APIs with quota-per-second). * For user-facing endpoints, prefer `sliding` or `tokenBucket`. * * State encoding matches token-bucket: `"|"`. * Writes go through the store's optional atomic `compareAndSet` when * available (bundled memory + Redis stores both provide it) so * concurrent requests can't race the level; stores without it fall * back to last-writer-wins `set`. * * @param {import('../index.js').BucketLimiterConfig} config * @returns {import('../index.js').Limiter} */ declare function leakyBucket(config: BucketLimiterConfig): Limiter; /** * Combine multiple limiters into one. A request is allowed only if **every** * inner limiter allows it; when any denies, the request is rejected with * the strictest `retryAfter` (max over deniers). * * Use for API-key style layered quotas — e.g. 100/min AND 1k/hour AND 10k/day. * Each inner limiter is a fully-formed limiter object; they can differ in * algorithm, window, or even backing store. * * @param {{ limiters: Array }} config * @returns {import('./index.js').Limiter} */ declare function multi(config: { limiters: Array; }): Limiter; /** * Wrap any limiter with a violation-count ban policy. When a caller * triggers `threshold` denials within `trackingWindow`, we bump them to * a hard ban for `banDuration` — subsequent `.check()` calls short-circuit * to denied without touching the base limiter. * * Sits nicely on top of a stricter-than-you-need limiter: the base * limiter catches ordinary abuse, `withBan` catches persistent abuse * cheaply (a single `store.get` per request while in the ban window, * no HMAC / no algorithm work). * * const limiter = rateLimit.withBan( * rateLimit.sliding({ requests: 20, window: '1m', store }), * { store, threshold: 5, banDuration: '1h' }, * ) * * State is written with the prefixes `bs:v:` (violation counter, * TTL = trackingWindow) and `bs:b:` (ban marker, TTL = banDuration). * You can share the base limiter's store or provide a dedicated one. * * @param {import('./index.js').Limiter} limiter * @param {{ * store: import('./index.js').RateLimitStore, * threshold: number, * banDuration: string | number, * trackingWindow?: string | number, * }} options * @returns {import('./index.js').Limiter} */ declare function withBan(limiter: Limiter, options: { store: RateLimitStore; threshold: number; banDuration: string | number; trackingWindow?: string | number; }): Limiter; declare function memoryStore(options?: {}): { get(key: any): Promise<{ count: number; expiresAt: number; } | null>; read(key: any): Promise<{ count: number; expiresAt: number; } | null>; incr(key: any, ttlMs: any): Promise<{ count: number; expiresAt: any; }>; set(key: any, count: any, ttlMs: any): Promise; decr(key: any): Promise; compareAndSet(key: any, expected: any, value: any, ttlMs: any): Promise; delete(key: any): Promise; reset(key: any): Promise; _size: () => number; _stop: () => void; }; /** * Wrap a user-supplied store into the interface the rate-limit algorithms * expect. Validates that the required methods exist so misconfigurations * surface at startup, not at the first blocked request. * * Required methods (all may be async): * - `get(key)` → { count, expiresAt } | null * - `incr(key, ttlMs)` → { count, expiresAt } (atomic) * - `set(key, count, ttlMs)` → void * - `delete(key)` → void * * Optional: * - `reset(key)` → void (defaults to `delete`) * - `decr(key)` → void — atomic decrement of an existing * key. When provided, `sliding` uses it for race-free rollback of a * rejected request's tentative increment. * - `compareAndSet(key, expected, value, ttlMs)` → boolean — atomic CAS * (`expected: null` = key must not exist). When provided, * `tokenBucket` / `leakyBucket` become race-free under concurrency. * * Atomicity guarantee: `incr` must be atomic across concurrent callers. On * Redis, wrap `INCR + EXPIRE` in a Lua script or MULTI/EXEC. On Mongo, use * `findOneAndUpdate` with upsert. Non-atomic implementations race and let * requests bypass the limit under load. * * @param {Partial} impl * @returns {import('../index.js').RateLimitStore} */ declare function customStore(impl: Partial): RateLimitStore; /** * Redis-compatible store. Works with any client that exposes: * - `eval(script, numkeys, ...args)` → number | string * - `pttl(key)` → number (ms; -2 missing, -1 no ttl) * - `get(key)` → string | null * - `set(key, value, 'PX', ms)` → 'OK' * - `del(key)` → number * * Verified compat: `ioredis`, `node-redis` (v4+), `@upstash/redis` (HTTP, * works on Cloudflare Workers / Vercel Edge / Deno Deploy). * * Atomicity: * - `incr` runs a single Lua script that INCR's the key and PEXPIRE's it * only when the key is fresh (count == 1). TTL anchors to the first * increment in the window — correct for fixed / token-bucket. * - `read` runs a Lua script (GET + PTTL) so the snapshot is consistent * even under contention. * * Optimization: when the client is `ioredis` (detected via `defineCommand`), * both scripts are registered once as named commands. Subsequent calls go * out as `EVALSHA`, saving the script body on every request. * * Namespacing: every key is prefixed with `rl:` by default so this adapter * doesn't collide with your application keys. Configurable via `prefix`. * * @param {object} client Redis-compatible client instance. * @param {{ prefix?: string }} [options] * @returns {import('../index.js').RateLimitStore} */ declare function redisStore(client: object, options?: { prefix?: string; }): RateLimitStore; declare namespace rateLimit { export { fixed }; export { sliding }; export { tokenBucket }; export { leakyBucket }; export { multi }; export { withBan }; export namespace stores { export { memoryStore as memory }; export { customStore as custom }; export { redisStore as redis }; } } /** * A store entry snapshot returned by `get` / `read` / `incr`. */ type StoreEntry = { /** * Current counter value for the key. */ count: number; /** * Absolute unix-ms timestamp when the key * stops being valid. */ expiresAt: number; }; /** * The store contract every rate-limit backend must satisfy. * * All methods are async. `incr` must be atomic across concurrent callers — * the algorithm layer relies on that guarantee. */ type RateLimitStore = { /** * Fetch current state. May refresh LRU position on stores that maintain one. */ get: (key: string) => Promise; /** * Non-mutating snapshot — does NOT refresh LRU / activity state. */ read: (key: string) => Promise; /** * Atomically increment (or create) the key and return the new state. */ incr: (key: string, ttlMs: number) => Promise; /** * Overwrite (or create) the key with an explicit count and TTL. */ set: (key: string, count: number, ttlMs: number) => Promise; delete: (key: string) => Promise; reset: (key: string) => Promise; /** * Optional: atomically decrement an existing key (no-op when absent; * never creates the key). `sliding` uses this to roll back its * tentative increment on rejection without racing concurrent `incr`s — * stores that omit it fall back to a read-modify-write rollback. */ decr?: ((key: string) => Promise) | undefined; /** * Optional: atomically write `value` only if the key's current stored * count equals `expected` (`null` = key must not exist). Returns * `true` when the write happened. `tokenBucket` / `leakyBucket` use * this as a CAS so concurrent requests can't double-spend a token — * stores that omit it fall back to last-writer-wins `set`. */ compareAndSet?: ((key: string, expected: string | null, value: string | number, ttlMs: number) => Promise) | undefined; }; type CheckInput = { key: string; }; type LimiterResult = { allowed: boolean; remaining: number; reset: Date | null; retryAfter: number | null; }; /** * A composable limiter. Every algorithm and `multi()` returns this shape. */ type Limiter = { check: (input: CheckInput) => Promise; }; type WindowLimiterConfig = { requests: number; /** * Duration string ('1m', '30s') or ms. */ window: string | number; store: RateLimitStore; }; type BucketLimiterConfig = { capacity: number; /** * Token-bucket only: tokens/second refill. */ refillRate?: number | undefined; /** * Leaky-bucket only: leak rate in req/sec. */ leakRate?: number | undefined; store: RateLimitStore; }; /** * Build a CORS decision function. * * const check = cors({ origin: ['https://app.example.com'], credentials: true }) * * // In your framework middleware: * const d = check({ * method: req.method, * origin: req.headers.origin, * requestMethod: req.headers['access-control-request-method'], * requestHeaders: req.headers['access-control-request-headers'], * }) * for (const [k, v] of Object.entries(d.headers)) res.setHeader(k, v) * if (d.preflight) { res.statusCode = d.status; res.end(); return } * if (!d.allowed) { res.statusCode = 403; res.end(); return } * * The returned function is pure — no state, safe to reuse across requests * and across workers. When the configured `origin` predicate is async * (returns a Promise), `check()` returns `Promise`; for sync * predicates it stays synchronous so hot paths don't pay a needless await. * * @param {CorsOptions} [options] * @returns {(input: CorsInput) => CorsDecision | Promise} */ declare function cors(options?: CorsOptions): (input: CorsInput) => CorsDecision | Promise; type OriginMatcher = string | RegExp; type CorsOptions = { /** * Which origins are allowed to make cross-origin requests. * - `true` → reflect any origin (echoes back the request's `Origin`). * - `false` → CORS disabled; every cross-origin request is denied. * - string → exact match against the request's `Origin`. * - RegExp → pattern match. * - Array → any-of match against the entries. * - Function → sync or async predicate; return true (or resolve to true) to allow. * When the predicate is async, `check()` returns a Promise; * for sync predicates it stays sync so consumers pay no async cost. */ origin?: boolean | OriginMatcher | OriginMatcher[] | ((origin: string | undefined) => boolean | Promise) | undefined; /** * Comma-separated string or array. Default: * `['GET','HEAD','PUT','PATCH','POST','DELETE']`. Sent as * `Access-Control-Allow-Methods` on preflight only. */ methods?: string | string[] | undefined; /** * Headers the browser may include on the actual request. Default `true` = * echo the request's `Access-Control-Request-Headers`. Sent on preflight. */ allowedHeaders?: string | true | string[] | undefined; /** * Response headers the browser may read via `getResponseHeader()`. Sent * on the actual response. */ exposedHeaders?: string | string[] | undefined; /** * When true, sets `Access-Control-Allow-Credentials: true`. Requires an * exact-echoed origin — cannot be combined with the `*` wildcard. */ credentials?: boolean | undefined; /** * Seconds the browser may cache the preflight decision. Sent on preflight. */ maxAge?: number | undefined; /** * HTTP status to end a preflight response with. Some legacy setups need * 200 instead — Chrome accepts either. */ optionsSuccessStatus?: number | undefined; }; type CorsInput = { /** * Request method (e.g. 'GET', 'OPTIONS'). */ method: string; /** * Value of the request's `Origin` header. */ origin: string | undefined; /** * `Access-Control-Request-Method` on preflight. */ requestMethod?: string | undefined; /** * `Access-Control-Request-Headers` on preflight. */ requestHeaders?: string | undefined; }; type CorsDecision = { /** * CORS response headers to merge onto the response. */ headers: Record; /** * Origin passed the policy check. */ allowed: boolean; /** * Request was an OPTIONS preflight. */ preflight: boolean; /** * Suggested response status for preflight. */ status?: number | undefined; }; /** * CSP directive map. Keys are camelCase; values are arrays of source * expressions. A value of `false` removes a default-provided directive. * @typedef {Record} CspDirectives */ /** * @typedef {object} CspOptions * @property {CspDirectives} [directives] * @property {boolean} [useDefaults=true] * @property {boolean} [reportOnly=false] */ /** * @typedef {object} HstsOptions * @property {number} [maxAge=15552000] Seconds. Default 180 days. * @property {boolean} [includeSubDomains=true] * @property {boolean} [preload=false] Requires maxAge >= 1y + includeSubDomains. */ /** * @typedef {'DENY' | 'SAMEORIGIN' | { action: 'DENY' | 'SAMEORIGIN' }} FrameguardOptions */ /** * @typedef {object} PermissionsPolicyOptions * @property {Record} [features] */ /** * A static-value policy option: * true → default value, false → skip, * string / { value } → verbatim override. * @typedef {boolean | string | { value: string } | undefined} StaticHeaderOption */ /** * @typedef {object} HeadersOptions * @property {boolean | CspOptions} [contentSecurityPolicy] * @property {boolean | HstsOptions} [hsts] * @property {boolean | HstsOptions} [strictTransportSecurity] Alias of `hsts`. * @property {StaticHeaderOption} [contentTypeOptions] * @property {StaticHeaderOption} [dnsPrefetchControl] * @property {StaticHeaderOption} [downloadOptions] * @property {StaticHeaderOption} [permittedCrossDomainPolicies] * @property {StaticHeaderOption} [originAgentCluster] * @property {StaticHeaderOption} [xssProtection] * @property {StaticHeaderOption} [crossOriginOpenerPolicy] * @property {StaticHeaderOption} [crossOriginEmbedderPolicy] * @property {StaticHeaderOption} [crossOriginResourcePolicy] * @property {StaticHeaderOption} [referrerPolicy] * @property {boolean | FrameguardOptions} [frameguard] * @property {boolean | PermissionsPolicyOptions} [permissionsPolicy] */ /** * Build a map of HTTP security headers. * * `headers()` with no options ships secure-by-default headers suitable for * an HTTPS API or SSR app. Each policy can be opted out with `false` or * customized via its own options object — see the individual `build*` * functions in `./policies.js` for supported shapes. * * import { headers } from '@exortek/security' * * const map = headers({ * hsts: { maxAge: 31536000, preload: true }, * contentSecurityPolicy: { * directives: { scriptSrc: ["'self'", "https://cdn.example.com"] }, * }, * crossOriginEmbedderPolicy: false, // COEP breaks many embeds * frameguard: 'SAMEORIGIN', * }) * // → { 'Content-Security-Policy': "...", 'Strict-Transport-Security': "...", ... } * * For per-request CSP nonces, use `cspNonce()` and template the resulting * string into `directives.scriptSrc` before calling `headers()`. * * Returns a plain `{ [name]: value }` object. Framework middleware iterates * and calls the framework's response setter; consumers can also assign the * map directly onto a Response. * * @param {HeadersOptions} [options] * @returns {Record} */ declare function headers(options?: HeadersOptions): Record; /** * Generate a fresh CSP nonce. * * A nonce is a random per-response value; embed the SAME nonce in your CSP * `script-src` (as `'nonce-'`) and on every inline `