W WRNexusJS
Core

@wrnexus/core

Contexts, middleware, security, sessions, caching, JSX, and realtime.

bun add @wrnexus/core@0.2.15
The framework core: the request Context, middleware contract, and the security, session, caching, streaming, realtime, and JSX primitives every other WRNexusJS package builds on.

Part of the WRNexusJS framework — an SSR-first, Bun-native full-stack web framework.

Overview

@wrnexus/core is the shared foundation of WRNexusJS. It defines the Context object that flows through every middleware, page, and API route, plus the Middleware/Next contract they implement. On top of that it ships the building blocks a real app needs: cookie-backed sessions, password auth, CSRF protection, rate limiting, request logging, HTTP + in-memory caching, file uploads, streaming/SSE responses, WebSocket "rooms", security headers/CORS, and a server-side JSX runtime that renders to HTML strings. Everything here is server-side and Bun-native (it uses Bun.password, Bun.write, the web-standard Request/Response, and crypto). You depend on it directly and transitively through the rest of the framework.

Installation

bun add @wrnexus/core
Private package — the machine must be authenticated to the wrnexus npm org
(a read token in ~/.npmrc). Requires Bun (Node is not supported).

API

Context & middleware — @wrnexus/core

The Context (ctx) is the single value passed to middleware and handlers.

ExportKindDescription
ContexttypePer-request object: req, url, lang, t, params, locals, user?, ip?, cookies, session, localStorage.
Nexttype`() => Promise<Response> \Response` — invokes the next middleware/handler.
Middlewaretype`(ctx, next) => Promise<Response> \Response. Return next() to continue, or a Response` to short-circuit.
createContext(req, url)fnBuild a fresh Context for an incoming request (wires up cookies, session, localStorage snapshot).
withContextHeaders(ctx, res)fnApply accumulated headers (e.g. Set-Cookie) from the context onto a response.
PageComponenttype`(ctx) => string \Promise<string>` — a page module's default export.
PageMeta / SeoConfigtype<head> metadata: title, description, canonical, robots, image, twitterCard, themeColor, …
TFunctiontype(key, params?) => string — translate a key for ctx.lang, interpolating {param} placeholders.

Key Context fields:

  • ctx.locals — per-request scratch space for passing values between middleware.
  • ctx.user — the authenticated user (populated by sessionAuth/logIn), or null.
  • ctx.ip — the direct socket peer IP (not spoofable via headers).
  • ctx.cookies / ctx.session / ctx.localStorage — see Storage below.

Authentication — @wrnexus/core

Passwords are hashed with argon2id via Bun.password; sessions ride the cookie-backed SessionStore.

ExportSignatureNotes
hashPassword(password)(string) => Promise<string>argon2id hash to store.
verifyPassword(password, hash)(string, string) => Promise<boolean>Constant-safe; returns false on bad/empty hash.
logIn(ctx, user)(Context, U) => voidRegenerates the session id (fixation defense), stores the user, sets ctx.user.
logOut(ctx)(Context) => voidClears the session and ctx.user.
getUser(ctx)`(Context) => U \null`Current user from ctx.user, falling back to the session.
sessionAuth()() => MiddlewareHydrates ctx.user from the session each request. Register early.
requireAuth(options?)(RequireAuthOptions?) => MiddlewareGuard: API/fetch requests get 401 JSON, page navigations get 302 to loginPath (default /login) with ?next=.
SESSION_USER_KEY"user"Session key holding the user.

RequireAuthOptions: { loginPath?: string }.

CSRF — @wrnexus/core

Double-submit cookie pattern: a readable wire-csrf cookie is echoed in an x-csrf-token header on unsafe requests.

ExportSignatureNotes
csrfToken(ctx)(Context) => stringEnsures the CSRF cookie exists and returns its token.
verifyCsrf(ctx)(Context) => booleanSafe methods (GET/HEAD/OPTIONS) pass; otherwise header/ctx.locals._csrf must match the cookie (constant-time).
csrfProtection()() => Middleware403s unsafe requests with a missing/mismatched token.
CSRF_COOKIE / CSRF_HEADER"wire-csrf" / "x-csrf-token"Cookie & header names.

Rate limiting — @wrnexus/core

Fixed-window limiter that returns 429 with Retry-After and emits RateLimit-Limit/-Remaining/-Reset headers.

ExportSignatureNotes
rateLimit(options?)(RateLimitOptions?) => MiddlewareMain middleware.
peerKey(ctx)(Context) => stringNon-spoofable key from ctx.ip (default).
proxyKey(ctx)(Context) => stringTrusts x-forwarded-for/x-real-ip. Use only behind a trusted proxy.
defaultKeyDeprecated alias of proxyKey.

RateLimitOptions: windowMs (default 60_000), max (default 60), key, trustProxy (default false → keys on peerKey; trueproxyKey), message, headers (default true), store.

RateLimitStore is pluggable — implement hit(key, windowMs, now) => Bucket | Promise<Bucket> (a Bucket is { count, resetAt }) to back limits with Redis/SQL across instances. The default store is process-local memory.

Request logging — @wrnexus/core

ExportSignatureNotes
requestLogger(options?)(RequestLoggerOptions?) => MiddlewareOne record per request with a request id (stored on ctx.locals[requestIdKey]).

RequestLoggerOptions: format ("pretty" default \| "json"), sink(line, record) (default console.log), requestIdKey (default "requestId"), now. RequestRecord = { time, id, method, path, status, durationMs }.

Caching — @wrnexus/core

ExportKindNotes
TTLCache<V>classIn-memory TTL cache: get, set, getOrLoad(key, loader, ttlMs?), delete, clear, size. Constructor takes a default ttlMs (60s).
cacheControl(options)fnBuild a Cache-Control value from CacheControlOptions.
withCacheControl(res, options)fnApply Cache-Control to a response.
etag(body, weak?)fnStable quoted FNV-1a ETag (weak by default).
notModified(req, tag)fntrue when If-None-Match matches — send a 304.

CacheControlOptions: maxAge, sMaxAge, private, noStore, noCache, staleWhileRevalidate, immutable.

File uploads — @wrnexus/core

Bun parses multipart/form-data via Request.formData(); these helpers validate and persist the resulting Files.

ExportSignatureNotes
collectUploads(form)(FormData) => { field, file }[]Every non-empty File in a parsed form.
saveUpload(file, options)(File, SaveUploadOptions) => Promise<SavedUpload>Validates size/type, sanitizes the name, writes via Bun.write. Throws UploadError.
sanitizeFilename(name)(string) => stringStrips separators, traversal, control/illegal chars; caps at 255.
UploadErrorclassThrown on rejected uploads.

SaveUploadOptions: dir (required), maxBytes, allowedTypes (MIME types like "image/png" and/or extensions like ".png"), filename(file). SavedUpload = { path, filename, size, type }.

Streaming & SSE — @wrnexus/core

ExportSignatureNotes
streamResponse(source, init?)`(Iterable\AsyncIterable<string\Uint8Array>, StreamResponseInit?) => Response`Streaming Response from a chunk source (basis for streaming SSR).
sse(source)`(Iterable\AsyncIterable<ServerSentEvent>) => Response`text/event-stream response.

StreamResponseInit: status, headers, contentType (default "text/html; charset=utf-8"). ServerSentEvent: { data, event?, id?, retry? }.

Realtime rooms — @wrnexus/core

WebSocket rooms. A file in app/realtime/ exports default defineRoom({ ... }) and is served at ws://host/realtime/<name>.

ExportSignatureNotes
defineRoom(handlers)(RoomHandlers) => RoomDefinitionDefine a room. Export the result as default.
isRoomDefinition(value)(unknown) => booleanType guard for a room definition.
createRealtimeRegistry()() => RealtimeRegistryServer-side connection manager mapping sockets ↔ rooms.
bridgeRealtime(registry, bus, topic?)(RealtimeRegistry, RealtimeBus, string?) => () => voidBridge broadcasts/toUser sends across processes via a pub/sub bus.

RoomHandlers: authorize(info) => boolean (gate before accept — return false to reject with 403), onConnect(client), onMessage(client, message) (JSON auto-parsed), onLeave(client). A handler receives a RoomClient with id, user, query, data, room, and send / broadcast / to(id) / toUser(user) / close. The Room API adds state, clients(), count(), and broadcast. RealtimeBus is structurally satisfied by @wrnexus/pubsub. Legacy RealtimeHandler/RealtimeSocket raw handlers are still exported. Connection-targeted sends (send, to(id)) stay local; room broadcasts and toUser cross the bridge.

Error pages — @wrnexus/core

ExportSignatureNotes
renderError(err, mode)(unknown, Mode) => ResponseDev page (with stack) or generic prod page by mode.
renderDevError(err, status?)(unknown, number?) => ResponseReadable HTML error page including the stack trace.
renderProdError(status?)(number?) => ResponseGeneric page that never leaks file paths.
renderNotFound()() => ResponseSimple 404 page.

Mode = "development" | "production".

Security headers & CORS — @wrnexus/core

ExportSignatureNotes
withSecurityHeaders(req, res, mode, security?, nonce?)ResponseApplies CORS + CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, COOP, Trusted Types, and extraHeaders.
createCorsPreflightResponse(req, security?)→ `Response \null`Builds a 204/403 preflight response for CORS OPTIONS requests.
isWebSocketOriginAllowed(req, security?)booleanGuards WS upgrades against cross-site hijacking (allows same-origin, configured CORS origins, and non-browser clients).

Config types: SecurityConfig (top-level), CorsConfig/CorsOrigin, ContentSecurityPolicyConfig/CspDirectiveValue, HstsConfig, TrustedTypesConfig, PermissionsPolicyConfig. WRNexusJS applies sensible defaults (self-only CSP, frame-ancestors 'none', restrictive Permissions-Policy, HSTS in production, Trusted Types in production); each is individually overridable or disable-able via false.

Storage: cookies, sessions, localStorage — @wrnexus/core

These back the ctx.cookies, ctx.session, and ctx.localStorage fields.

ExportKindNotes
setSessionBackend(backend)fnSwap the sync session persistence backend (SessionBackend) — e.g. bun:sqlite. Default is process-local memory. Call once at startup.
loadSession(backend, options?)fn → MiddlewareBack ctx.session with an async store (AsyncSessionBackend: load/save/destroy) — loads before the request, saves after. options.ttlMs default 24h.
CookieStoretypeget/getAll/has/set(name, value, opts?)/delete/headers.
SessionStoretypeid/get/getAll/set/delete/regenerate/clear.
LocalStorageSnapshottypeRead-only view of the browser's localStorage sent via header for CSR bindings.
CookieOptionstypepath, domain, maxAge, expires, httpOnly, secure, sameSite.
SessionEntry / SessionBackend / AsyncSessionBackendtypesSession persistence contracts.

Low-level security helpers — @wrnexus/core

ExportSignatureNotes
escapeHtml(value)(string) => stringEscape for HTML text/attributes.
isSafeIslandName(name)(string) => booleanAllow only a conservative [A-Za-z0-9_-]+ charset.
isSafeRequestPath(pathname)(string) => booleanReject NULs, .. traversal, and backslashes.

JSX runtime — @wrnexus/core, @wrnexus/core/jsx-runtime, @wrnexus/core/jsx-dev-runtime

A server-side JSX runtime that renders to HTML strings (no virtual DOM). Point tsconfig's jsxImportSource at @wrnexus/core.

ExportKindNotes
jsx / jsxsfnThe runtime factory (TypeScript calls these automatically). Returns an Html instance.
FragmentsymbolJSX fragment marker.
HtmlclassWraps a raw, already-safe HTML string (toString() returns it).
mustache(expr)fnEmit a {{expr}} placeholder (tagged-template or string form) for the client binder.
JSXComponent / JSXProps / RenderabletypesComponent signature and renderable value types.

Values interpolated as children are HTML-escaped unless they are an Html instance; use dangerouslySetInnerHTML={{ __html }} for trusted markup. Void elements render without a closing tag; classNameclass, htmlForfor, and style objects are serialized to CSS text.

The subpath exports map to the runtime TypeScript's JSX transform expects:

// tsconfig.json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "@wrnexus/core",
  },
}

Usage

A minimal middleware chain

import {
  createContext,
  withContextHeaders,
  sessionAuth,
  requireAuth,
  requestLogger,
  rateLimit,
  csrfProtection,
  type Middleware,
} from "@wrnexus/core";

const chain: Middleware[] = [
  requestLogger({ format: "json" }),
  rateLimit({ max: 100, windowMs: 60_000 }),
  csrfProtection(),
  sessionAuth(),
  requireAuth({ loginPath: "/login" }),
];

Password auth

import { hashPassword, verifyPassword, logIn, getUser } from "@wrnexus/core";

// Registration
const passwordHash = await hashPassword(form.password);

// Login
if (await verifyPassword(form.password, user.passwordHash)) {
  logIn(ctx, { id: user.id, email: user.email });
}

const current = getUser<{ id: string }>(ctx); // or null

HTTP caching with ETags

import { etag, notModified, withCacheControl } from "@wrnexus/core";

const body = JSON.stringify(data);
const tag = etag(body);
if (notModified(ctx.req, tag)) {
  return new Response(null, { status: 304, headers: { ETag: tag } });
}
const res = new Response(body, { headers: { ETag: tag, "content-type": "application/json" } });
return withCacheControl(res, { maxAge: 60, staleWhileRevalidate: 300 });

Streaming SSE

import { sse } from "@wrnexus/core";

async function* ticks() {
  for (let n = 0; ; n++) {
    yield { event: "tick", data: String(n) };
    await Bun.sleep(1000);
  }
}
export default (ctx) => sse(ticks());

A realtime room

// app/realtime/chat.ts
import { defineRoom } from "@wrnexus/core";

export default defineRoom({
  authorize: (info) => !!info.user, // require auth
  onConnect(client) {
    client.user = client.query.user;
    client.room.broadcast({ type: "join", id: client.id });
  },
  onMessage(client, msg) {
    client.broadcast({ type: "say", from: client.id, text: msg.text });
  },
});

Scale it across processes:

import { createRealtimeRegistry, bridgeRealtime } from "@wrnexus/core";
import { createPubSub } from "@wrnexus/pubsub";
import { redisDriver } from "@wrnexus/pubsub/redis";

const registry = createRealtimeRegistry();
bridgeRealtime(registry, createPubSub(redisDriver(process.env.REDIS_URL)));

JSX rendering

import { Html } from "@wrnexus/core";

function Card({ title, body }: { title: string; body: string }) {
  return (
    <article class="card">
      <h2>{title}</h2>
      <p>{body}</p>
    </article>
  );
}

const html: Html = <Card title="Hi" body="<b>escaped</b> automatically" />;
return new Response(html.toString(), { headers: { "content-type": "text/html" } });

Requirements / Notes

  • Bun-only. Uses Bun.password (argon2id), Bun.write, web-standard
  • Request/Response/FormData/ReadableStream, and the global crypto. Node is not supported.

  • Session and rate-limit backends default to process-local memory. For
  • multi-instance deployments, swap in a shared backend: setSessionBackend (sync, e.g. bun:sqlite) or loadSession (async, e.g. Redis) for sessions, a custom RateLimitStore for limits, and bridgeRealtime for realtime.

  • Works with the rest of the framework: realtime bridging is structurally
  • compatible with [@wrnexus/pubsub](../pubsub); the security, auth, and JSX primitives here are consumed by the WRNexusJS server/router packages.

  • Subpath exports: @wrnexus/core/jsx-runtime and @wrnexus/core/jsx-dev-runtime
  • for TypeScript's automatic JSX transform.

Complete TypeScript API

This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.

export { Fragment, Html, Component as JSXComponent, Props as JSXProps, Renderable, jsx, jsxs, mustache } from './jsx-runtime.js';

interface CookieOptions {
    path?: string;
    domain?: string;
    maxAge?: number;
    expires?: Date | string;
    httpOnly?: boolean;
    secure?: boolean;
    sameSite?: "Strict" | "Lax" | "None" | "strict" | "lax" | "none";
}
interface CookieStore {
    get(name: string): string | undefined;
    getAll(): Record<string, string>;
    has(name: string): boolean;
    set(name: string, value: string, options?: CookieOptions): void;
    delete(name: string, options?: CookieOptions): void;
    headers(): string[];
}
interface SessionStore {
    id(): string;
    get<T = unknown>(key: string): T | undefined;
    getAll(): Record<string, unknown>;
    set(key: string, value: unknown): void;
    delete(key: string): void;
    /** Issue a fresh session id, keeping the data — defends against fixation. */
    regenerate(): void;
    clear(): void;
}
interface LocalStorageSnapshot {
    get(key: string): string | undefined;
    getAll(): Record<string, string>;
    has(key: string): boolean;
}
/** A stored session: its data plus an absolute expiry timestamp (ms). */
interface SessionEntry {
    data: Record<string, unknown>;
    expiresAt: number;
}
/**
 * Pluggable session persistence. The default is process-local memory; swap in a
 * shared backend (Redis, SQL, etc.) via `setSessionBackend` so sessions survive
 * restarts and work across multiple instances. Methods are synchronous, so a
 * backend must be sync (e.g. `bun:sqlite`); async stores need a load/save
 * wrapper around the request (future work).
 */
interface SessionBackend {
    get(id: string): SessionEntry | undefined;
    set(id: string, entry: SessionEntry): void;
    delete(id: string): void;
    /** Optional: drop expired entries. Called periodically by the store. */
    gc?(now: number): void;
}
/** Replace the session persistence backend (call once at startup). */
declare function setSessionBackend(backend: SessionBackend): void;
/**
 * An ASYNC session store (Redis, a remote DB). Use it via the `loadSession`
 * middleware, which loads the session before the request and saves it after —
 * keeping the `ctx.session` API synchronous while persistence is shared across
 * instances.
 */
interface AsyncSessionBackend {
    load(id: string): Promise<SessionEntry | undefined>;
    save(id: string, entry: SessionEntry): Promise<void>;
    destroy(id: string): Promise<void>;
}
/**
 * Back `ctx.session` with an async store. Register early (before anything reads
 * `ctx.session`). Loads once at the start of the request and saves once at the
 * end; regenerate/clear destroy the old id.
 */
declare function loadSession(backend: AsyncSessionBackend, options?: {
    ttlMs?: number;
}): Middleware;

/**
 * Core request context and middleware contracts.
 *
 * The `Context` object is the single value that flows through middleware,
 * pages and API routes. It is intentionally small and framework-agnostic so
 * it can later be reused by the `.wrn` compiler output.
 */

/** Translate a key for the active language, interpolating `{param}` placeholders. */
type TFunction = (key: string, params?: Record<string, string | number>) => string;
type Context = {
    /** The raw incoming web-standard Request. */
    req: Request;
    /** Parsed URL of the request (pathname, query, etc.). */
    url: URL;
    /** Active language for this request (resolved by the runtime); "" if i18n is unused. */
    lang: string;
    /** Translate a key for the active language (identity until the runtime sets it). */
    t: TFunction;
    /** Dynamic route params, e.g. `/users/[id]` -> `{ id: "42" }`. */
    params: Record<string, string>;
    /**
     * Per-request scratch space. Middleware can attach values here
     * (e.g. the authenticated user) and downstream handlers can read them.
     */
    locals: Record<string, unknown>;
    /**
     * The authenticated user for this request, or null when anonymous. Populated
     * by the `sessionAuth` middleware (or `logIn`); read via `getUser(ctx)`.
     */
    user?: unknown;
    /**
     * The direct socket peer IP, set by the server from `server.requestIP`. This
     * is NOT spoofable by request headers — prefer it over `x-forwarded-for` for
     * rate limiting unless you run behind a trusted proxy.
     */
    ip?: string;
    /** Read/write HTTP cookies for the current response. */
    cookies: CookieStore;
    /** In-memory cookie-backed session store. */
    session: SessionStore;
    /** Read-only localStorage snapshot sent by the browser for CSR data bindings. */
    localStorage: LocalStorageSnapshot;
};
/** Calls the next middleware in the chain (or the final route handler). */
type Next = () => Promise<Response> | Response;
/**
 * Middleware runs before pages and API routes. It can:
 *  - inspect/modify `ctx`
 *  - short-circuit by returning a `Response` without calling `next()`
 *  - continue by returning `await next()`
 */
type Middleware = (ctx: Context, next: Next) => Promise<Response> | Response;
/** SEO metadata rendered into the document `<head>`. */
type SeoConfig = {
    /** BCP 47 document language used on `<html lang>` (default: `en`). */
    lang?: string;
    title?: string;
    titleTemplate?: string;
    description?: string;
    canonical?: string;
    canonicalBase?: string;
    robots?: string;
    keywords?: string | string[];
    image?: string;
    siteName?: string;
    type?: string;
    locale?: string;
    twitterCard?: string;
    twitterSite?: string;
    themeColor?: string;
};
/** Page metadata rendered into the document `<head>`. */
type PageMeta = SeoConfig;
/** A page module's default export. Returns an HTML string for the body. */
type PageComponent = (ctx: Context) => string | Promise<string>;
/** Create a fresh context for an incoming request. */
declare function createContext(req: Request, url: URL): Context;
/** Apply headers accumulated on the context, such as Set-Cookie. */
declare function withContextHeaders(ctx: Context, res: Response): Response;

/**
 * Small, dependency-free security helpers shared across packages.
 */
/**
 * Escape a string for safe interpolation into HTML text or attributes.
 * Used for page metadata (title/description) so untrusted values can't
 * break out of an attribute or inject markup.
 */
declare function escapeHtml(value: string): string;
declare function isSafeIslandName(name: string): boolean;
/**
 * Reject obvious path-traversal in a request path before it is ever used to
 * resolve a file. The router never builds file paths from request input
 * (routes are resolved against a pre-scanned table), but this is a cheap
 * defense-in-depth guard.
 */
declare function isSafeRequestPath(pathname: string): boolean;

/**
 * CSRF protection via the double-submit cookie pattern.
 *
 * The framework sets a readable `wire-csrf` cookie on page loads; the client
 * echoes it in an `x-csrf-token` header on unsafe requests (the Wire UI form
 * runtime does this automatically). The server checks header === cookie. A
 * cross-site attacker can't read the cookie to forge the header, so the request
 * is rejected — while same-origin requests pass.
 */

declare const CSRF_COOKIE = "wire-csrf";
declare const CSRF_HEADER = "x-csrf-token";
/** Ensure the CSRF cookie exists (readable by JS) and return its token. */
declare function csrfToken(ctx: Context): string;
/**
 * Verify an unsafe request's CSRF token against the cookie. Safe methods
 * (GET/HEAD/OPTIONS) always pass. The token may arrive in the `x-csrf-token`
 * header or a `_csrf` field already parsed onto `ctx.locals`.
 */
declare function verifyCsrf(ctx: Context): boolean;
/** Middleware that 403s unsafe requests with a missing/mismatched CSRF token. */
declare function csrfProtection(): Middleware;

/**
 * Authentication primitives.
 *
 * Passwords are hashed with argon2id via `Bun.password`. Sessions ride on the
 * existing cookie-backed `SessionStore`: logging a user in stores a serializable
 * user object under the "user" key, and `sessionAuth` hydrates `ctx.user` from
 * it on every request. `requireAuth` is a guard middleware for protected routes.
 */

/** Session key under which the authenticated user is stored. */
declare const SESSION_USER_KEY = "user";
/** Hash a plaintext password (argon2id). Store the returned string. */
declare function hashPassword(password: string): Promise<string>;
/** Verify a plaintext password against a stored hash. Safe against bad hashes. */
declare function verifyPassword(password: string, hash: string): Promise<boolean>;
/** Persist the authenticated user in the session and on the context. */
declare function logIn<U = unknown>(ctx: Context, user: U): void;
/** Clear the session and forget the current user. */
declare function logOut(ctx: Context): void;
/**
 * The currently-authenticated user, or null. Reads `ctx.user` first (set by
 * `sessionAuth`/`logIn`), falling back to the session store.
 */
declare function getUser<U = unknown>(ctx: Context): U | null;
/**
 * Hydrate `ctx.user` from the session for every request. Register this early in
 * the middleware chain so downstream pages and API routes can read `ctx.user`.
 */
declare function sessionAuth(): Middleware;
interface RequireAuthOptions {
    /** Where to redirect unauthenticated page requests. Default "/login". */
    loginPath?: string;
}
/**
 * Guard that requires an authenticated user. Unauthenticated requests that look
 * like an API/fetch call get a 401 JSON response; page navigations get a 302
 * redirect to the login page with the original target preserved as `?next=`.
 */
declare function requireAuth(options?: RequireAuthOptions): Middleware;

/**
 * Fixed-window rate limiting middleware. Keeps an in-memory counter per key
 * (client IP by default, read from `x-forwarded-for` / `x-real-ip`) and rejects
 * requests over the limit with a 429 and a `Retry-After` header. Sets the
 * `RateLimit-Limit` / `RateLimit-Remaining` / `RateLimit-Reset` headers.
 *
 * The store is process-local; behind multiple instances use a shared store
 * (out of scope here). Suitable as-is for single-process apps and dev.
 */

interface RateLimitOptions {
    /** Window length in milliseconds. Default 60_000 (1 minute). */
    windowMs?: number;
    /** Max requests allowed per key per window. Default 60. */
    max?: number;
    /** Derive the bucket key from the request. Default: client IP. */
    key?: (ctx: Context) => string;
    /**
     * Trust `x-forwarded-for` / `x-real-ip` for the client IP. Default false —
     * those headers are attacker-spoofable, so by default we key on the direct
     * socket peer (`ctx.ip`). Enable ONLY when behind a proxy that overwrites
     * these headers (nginx, a load balancer, Cloudflare).
     */
    trustProxy?: boolean;
    /** Body returned on 429. Default "Too Many Requests". */
    message?: string;
    /** Emit RateLimit-* headers. Default true. */
    headers?: boolean;
    /** Persistence for the counters. Default: process-local memory. */
    store?: RateLimitStore;
    /** Maximum in-memory keys before oldest buckets are evicted. Ignored for custom stores. */
    maxKeys?: number;
}
interface Bucket {
    count: number;
    resetAt: number;
}
/**
 * Pluggable rate-limit counter store. The default is process-local memory; swap
 * in a shared store (Redis/SQL) so limits hold across instances. `hit` records
 * one request for `key` in the current window and returns the running bucket.
 * It may be async (e.g. a Redis INCR + PEXPIRE) — the middleware awaits it.
 */
interface RateLimitStore {
    hit(key: string, windowMs: number, now: number): Bucket | Promise<Bucket>;
}
declare function rateLimit(options?: RateLimitOptions): Middleware;
/** Non-spoofable key: the direct socket peer IP (set by the server). */
declare function peerKey(ctx: Context): string;
/** Proxy-aware key: trusts `x-forwarded-for` / `x-real-ip`, else the peer IP. */
declare function proxyKey(ctx: Context): string;
/** @deprecated Use `peerKey` (default) or `proxyKey`. Kept for compatibility. */
declare const defaultKey: typeof proxyKey;

/**
 * Structured request logging middleware. Emits one record per request with a
 * request id, method, path, status, and duration — as pretty text (dev) or JSON
 * (production/log aggregation). The request id is stored on `ctx.locals` so
 * downstream handlers can correlate their own logs.
 */

interface RequestRecord {
    time: string;
    id: string;
    method: string;
    path: string;
    status: number;
    durationMs: number;
}
interface RequestLoggerOptions {
    /** "pretty" (default) for humans, "json" for machines. */
    format?: "pretty" | "json";
    /** Where each finished record goes. Default console.log. */
    sink?: (line: string, record: RequestRecord) => void;
    /** ctx.locals key for the request id. Default "requestId". */
    requestIdKey?: string;
    /** Clock injection for tests. Default Date.now. */
    now?: () => number;
}
declare function requestLogger(options?: RequestLoggerOptions): Middleware;

/**
 * Caching primitives:
 *  - `TTLCache` — a small in-memory time-to-live cache with `getOrLoad`, for
 *    memoising expensive data (query results, computed pages).
 *  - HTTP helpers — `cacheControl` to build a directive, `withCacheControl` to
 *    apply it, and `etag` / `notModified` for conditional requests (304s).
 */
declare class TTLCache<V = unknown> {
    private readonly ttlMs;
    private store;
    private loading;
    private revisions;
    private generation;
    constructor(ttlMs?: number);
    get(key: string): V | undefined;
    set(key: string, value: V, ttlMs?: number): void;
    /** Return the cached value or compute, cache, and return it. */
    getOrLoad(key: string, loader: () => Promise<V> | V, ttlMs?: number): Promise<V>;
    delete(key: string): void;
    clear(): void;
    get size(): number;
}
interface CacheControlOptions {
    /** max-age in seconds. */
    maxAge?: number;
    /** s-maxage (shared/CDN cache) in seconds. */
    sMaxAge?: number;
    /** Mark private (per-user) rather than public. */
    private?: boolean;
    /** no-store: never cache. Overrides other directives. */
    noStore?: boolean;
    /** no-cache: revalidate before use. */
    noCache?: boolean;
    /** stale-while-revalidate window in seconds. */
    staleWhileRevalidate?: number;
    immutable?: boolean;
}
/** Build a Cache-Control header value from options. */
declare function cacheControl(options: CacheControlOptions): string;
/** Apply a Cache-Control header to a response (returns the same response). */
declare function withCacheControl(res: Response, options: CacheControlOptions): Response;
/** A stable, quoted ETag for a string/bytes body (FNV-1a, weak by default). */
declare function etag(body: string | ArrayBuffer | Uint8Array, weak?: boolean): string;
/** True when the request's If-None-Match matches the given ETag (send a 304). */
declare function notModified(req: Request, tag: string): boolean;

/**
 * File upload helpers. Bun parses `multipart/form-data` natively via
 * `Request.formData()`, yielding web `File` objects; these helpers validate and
 * persist them safely (size/type limits, filename sanitisation to prevent path
 * traversal).
 */
declare class UploadError extends Error {
    constructor(message: string);
}
interface SaveUploadOptions {
    /** Destination directory. */
    dir: string;
    /** Reject files larger than this many bytes. */
    maxBytes?: number;
    /** Allowed MIME types (e.g. "image/png") and/or extensions (e.g. ".png"). */
    allowedTypes?: string[];
    /** Choose the stored filename. Default: the sanitised original name. */
    filename?: (file: File) => string;
}
interface SavedUpload {
    path: string;
    filename: string;
    size: number;
    type: string;
}
/** All `File` values in a parsed form, with their field names. */
declare function collectUploads(form: FormData): {
    field: string;
    file: File;
}[];
/** Validate and write one uploaded file to disk. Throws `UploadError` on reject. */
declare function saveUpload(file: File, options: SaveUploadOptions): Promise<SavedUpload>;
/** Strip directory separators, traversal, and control chars from a filename. */
declare function sanitizeFilename(name: string): string;

/**
 * Streaming response primitives.
 *
 * `streamResponse` turns a (sync or async) iterable of strings/bytes into a
 * streaming `Response` — the basis for streaming SSR (send the shell, then flush
 * page chunks as they render) and any progressively-generated output. `sse`
 * builds a Server-Sent Events stream from an async iterable of events.
 *
 * API routes and pages can already return a `Response` with a `ReadableStream`
 * body and the framework streams it unbuffered; these helpers just make the
 * common cases ergonomic.
 */
interface StreamResponseInit {
    status?: number;
    headers?: HeadersInit;
    /** Content-Type; default "text/html; charset=utf-8". */
    contentType?: string;
}
type Chunk = string | Uint8Array;
type ChunkSource = Iterable<Chunk> | AsyncIterable<Chunk>;
/** Build a streaming Response from an (async) iterable of chunks. */
declare function streamResponse(source: ChunkSource, init?: StreamResponseInit): Response;
interface ServerSentEvent {
    data: string;
    event?: string;
    id?: string;
    /** Client reconnection hint in milliseconds. */
    retry?: number;
}
/** Build a Server-Sent Events (text/event-stream) Response from events. */
declare function sse(source: Iterable<ServerSentEvent> | AsyncIterable<ServerSentEvent>): Response;

/**
 * Realtime rooms.
 *
 * A file in `app/realtime/` exports `default defineRoom({ onConnect, onMessage,
 * onLeave })` and is served at `ws://host/realtime/<name>`. The framework's
 * client runtime (`/__wrnexus/realtime.js`) handles the browser side, so pages
 * ship NO hand-written WebSocket code.
 *
 * Handlers get a `RoomClient` with everything you need:
 *   client.send(msg)                 → this connection
 *   client.broadcast(msg)            → everyone else in the room
 *   client.room.broadcast(msg)       → everyone (incl. sender)
 *   client.to(id | ids).send(msg)    → specific connection(s)
 *   client.toUser(u | users).send()  → a user / selected users (all their tabs)
 *   client.user = "u1"               → identify a connection for targeting
 *   client.data / client.room.state  → per-connection / shared room state
 *
 * The dynamic route `app/realtime/[room].ts` gives one handler many independent
 * rooms — `/realtime/lobby` and `/realtime/game-7` are separate room instances.
 */
interface RawSocket {
    send(data: string): unknown;
    close(code?: number, reason?: string): void;
}
interface RealtimeSocket<Data = unknown> {
    readonly data: Data;
    send(data: string | Uint8Array): number;
    subscribe(topic: string): void;
    unsubscribe(topic: string): void;
    publish(topic: string, data: string | Uint8Array): number;
    isSubscribed(topic: string): boolean;
    close(code?: number, reason?: string): void;
}
interface RealtimeHandler<Data = unknown> {
    open?(ws: RealtimeSocket<Data>): void | Promise<void>;
    message?(ws: RealtimeSocket<Data>, message: string | Uint8Array): void | Promise<void>;
    close?(ws: RealtimeSocket<Data>, code?: number, reason?: string): void | Promise<void>;
    drain?(ws: RealtimeSocket<Data>): void | Promise<void>;
}
interface Target {
    /** Send a message (objects are JSON-serialized). */
    send(message: unknown): void;
}
interface Room<TData = Record<string, unknown>> {
    readonly name: string;
    /** Shared, in-memory room state (lives while ≥1 client is connected). */
    readonly state: Record<string, unknown>;
    /** All connected clients. */
    clients(): RoomClient<TData>[];
    /** Number of connected clients. */
    count(): number;
    /** Send to everyone in the room, including the sender. */
    broadcast(message: unknown): void;
    /** Target specific connection id(s). */
    to(id: string | string[]): Target;
    /** Target a user / users by identity (reaches all their connections). */
    toUser(user: string | string[]): Target;
}
interface RoomClient<TData = Record<string, unknown>> {
    /** Unique per connection (a tab). */
    readonly id: string;
    /** App identity for targeting; assign it in `onConnect`. */
    user: string | undefined;
    /** Query params from the connection URL. */
    readonly query: Record<string, string>;
    /** Per-connection scratch state. */
    readonly data: TData;
    readonly room: Room<TData>;
    /** Send to THIS connection. */
    send(message: unknown): void;
    /** Send to everyone else in the room. */
    broadcast(message: unknown): void;
    /** Target specific connection id(s). */
    to(id: string | string[]): Target;
    /** Target a user / users by identity. */
    toUser(user: string | string[]): Target;
    /** Close this connection. */
    close(code?: number, reason?: string): void;
}
/** Info available when authorizing a connection, before it is accepted. */
interface RoomAuthInfo {
    /** Authenticated session user id, or `?user=` — undefined when anonymous. */
    user?: string;
    /** Connection URL query params. */
    query: Record<string, string>;
    /** The upgrade request's headers (cookies, etc.). */
    headers: Headers;
}
interface RoomHandlers<TData = Record<string, unknown>> {
    /**
     * Gate the connection BEFORE it is accepted. Return false to reject the
     * upgrade with 403 (e.g. `authorize: (info) => !!info.user` to require auth).
     */
    authorize?(info: RoomAuthInfo): boolean | Promise<boolean>;
    /** A client connected (a new tab joined the room). */
    onConnect?(client: RoomClient<TData>): void | Promise<void>;
    /** A message arrived (JSON is parsed; non-JSON arrives as a string). */
    onMessage?(client: RoomClient<TData>, message: any): void | Promise<void>;
    /** A client disconnected. */
    onLeave?(client: RoomClient<TData>): void | Promise<void>;
}
interface RoomDefinition<TData = Record<string, unknown>> {
    readonly __wrnexusRoom: true;
    readonly handlers: RoomHandlers<TData>;
}
/** Define a realtime room. Export the result as the `default` of a realtime file. */
declare function defineRoom<TData = Record<string, unknown>>(handlers: RoomHandlers<TData>): RoomDefinition<TData>;
declare function isRoomDefinition(value: unknown): value is RoomDefinition;
interface RealtimeConnectMeta {
    room: string;
    def: RoomDefinition;
    query?: Record<string, string>;
    user?: string;
}
/** One cross-instance message: a room broadcast, or a targeted user send. */
interface RealtimeEnvelope {
    room: string;
    /** If set, deliver only to these user identities; otherwise the whole room. */
    users?: string[];
    message: unknown;
}
/**
 * A pub/sub bridge for horizontal scaling. Wire the registry to a shared bus
 * (Redis pub/sub, NATS, …): local broadcasts/`toUser` sends are published to
 * peers, and messages received from peers are delivered via `registry.deliver`.
 * Connection-targeted sends (`send`, `to(id)`) stay local (ids are per-process).
 */
interface RealtimeBridge {
    publish(envelope: RealtimeEnvelope): void;
}
interface RealtimeRegistry {
    open(socket: RawSocket, meta: RealtimeConnectMeta): void | Promise<void>;
    message(socket: RawSocket, raw: string | Uint8Array): void | Promise<void>;
    close(socket: RawSocket): void | Promise<void>;
    /** Attach a cross-instance bridge (call once at startup). */
    setBridge(bridge: RealtimeBridge): void;
    /** Deliver an envelope received from a peer to LOCAL connections only. */
    deliver(envelope: RealtimeEnvelope): void;
    /** Number of live connections (across all rooms) — for tests/metrics. */
    size(): number;
}
/** Create the registry that maps sockets ↔ rooms and drives room handlers. */
declare function createRealtimeRegistry(): RealtimeRegistry;
/**
 * A minimal pub/sub bus (structurally satisfied by `@wrnexus/pubsub`). Used to
 * bridge realtime broadcasts across processes without a hard dependency.
 */
interface RealtimeBus {
    publish(topic: string, message: unknown): void | Promise<void>;
    subscribe(topic: string, handler: (message: unknown, topic: string) => void): () => void;
}
/**
 * Bridge a realtime registry across processes/instances via a pub/sub bus (use
 * the Redis driver so it crosses machines). After this, `client.room.broadcast`
 * and `client.toUser(...)` reach connected clients on **every** app process/
 * instance subscribed to the same bus — the foundation for realtime that works
 * with multiple running apps behind the gateway. Connection-targeted sends
 * (`send`, `to(id)`) stay local. Returns an unsubscribe function.
 *
 *   import { createRealtimeRegistry, bridgeRealtime } from "@wrnexus/core";
 *   import { createPubSub } from "@wrnexus/pubsub";
 *   import { redisDriver } from "@wrnexus/pubsub/redis";
 *   bridgeRealtime(registry, createPubSub(redisDriver(process.env.REDIS_URL)));
 */
declare function bridgeRealtime(registry: RealtimeRegistry, bus: RealtimeBus, topic?: string): () => void;

/**
 * Error + status pages. Every page here is a self-contained HTML document —
 * inline CSS only, no external stylesheet, no JavaScript (so it renders under the
 * strict CSP, even when the app's assets are what failed). Theme-aware via
 * `prefers-color-scheme`, styled in the WRNexusJS design language (ink-navy,
 * azure, a faint blueprint grid + glow). Development shows the stack trace;
 * production never leaks internal paths.
 */
type Mode = "development" | "production";
/** A beautiful, self-contained HTML page for any 4xx/5xx status. */
declare function renderStatusPage(status: number): Response;
/** Readable, styled development error page — includes the stack trace. */
declare function renderDevError(err: unknown, status?: number): Response;
/** Generic production error page — no stack, no file paths. */
declare function renderProdError(status?: number): Response;
/** Pick the right error page for the current mode. */
declare function renderError(err: unknown, mode: Mode): Response;
/** Beautiful 404 page. */
declare function renderNotFound(): Response;

type CorsOrigin = "*" | string | string[];
interface CorsConfig {
    /** Enable CORS headers and preflight handling. Defaults to false. */
    enabled?: boolean;
    /** Allowed origins. Use "*" for public APIs. Defaults to "*". */
    origin?: CorsOrigin;
    /** Allowed methods for preflight responses. */
    methods?: string[];
    /** Allowed request headers. Defaults to the browser's requested headers. */
    allowedHeaders?: string[];
    /** Response headers exposed to browser JavaScript. */
    exposedHeaders?: string[];
    /** Whether to send Access-Control-Allow-Credentials. */
    credentials?: boolean;
    /** Access-Control-Max-Age, in seconds. */
    maxAge?: number;
}
type CspDirectiveValue = string | string[] | false | null | undefined;
interface ContentSecurityPolicyConfig {
    /** Defaults to true. */
    enabled?: boolean;
    /** Use Content-Security-Policy-Report-Only instead of enforcing. */
    reportOnly?: boolean;
    /** Merge or remove directives. Set a directive to false/null to remove it. */
    directives?: Record<string, CspDirectiveValue>;
    /** Set false to start from an empty policy instead of WRNexusJS defaults. */
    useDefaults?: boolean;
}
interface HstsConfig {
    /** Defaults to true in production, false in development. */
    enabled?: boolean;
    /** Defaults to 31536000 seconds (1 year). */
    maxAge?: number;
    /** Defaults to true. */
    includeSubDomains?: boolean;
    /** Defaults to true. */
    preload?: boolean;
}
interface TrustedTypesConfig {
    /** Defaults to true in production, false in development. */
    enabled?: boolean;
    /**
     * Defaults to ["*"] in production so browser extensions and dev tooling can
     * create their own policies without noisy console errors. Set this to a
     * concrete list, e.g. ["wrnexus", "default"], for stricter deployments.
     */
    policyNames?: string[];
    /** Defaults to true. */
    requireForScript?: boolean;
    /** Adds "allow-duplicates" to the trusted-types directive. */
    allowDuplicates?: boolean;
}
type PermissionsPolicyConfig = Record<string, string | string[] | false | null | undefined>;
interface SecurityConfig {
    /** Set false to skip all framework security headers except explicitly enabled CORS. */
    headers?: boolean;
    /**
     * Trust `X-Forwarded-Proto` / `X-Forwarded-Host` when building `ctx.url` — set
     * this when the app runs behind a TLS-terminating reverse proxy (nginx, the
     * WRNexusJS gateway, a load balancer). Without it, a proxied app sees the internal
     * `http://` request and marks cookies (e.g. CSRF/session) non-`Secure`. Default
     * false; enable ONLY when a trusted proxy actually sets these headers.
     */
    trustProxy?: boolean;
    cors?: boolean | CorsConfig;
    contentSecurityPolicy?: false | ContentSecurityPolicyConfig;
    hsts?: false | HstsConfig;
    trustedTypes?: false | TrustedTypesConfig;
    /** Defaults to "same-origin". */
    crossOriginOpenerPolicy?: false | "same-origin" | "same-origin-allow-popups" | "unsafe-none";
    /** Defaults to "DENY". */
    frameOptions?: false | "DENY" | "SAMEORIGIN";
    /** Defaults to "strict-origin-when-cross-origin". */
    referrerPolicy?: false | string;
    /** Defaults to a restrictive browser capability policy. */
    permissionsPolicy?: false | PermissionsPolicyConfig;
    /** Extra static headers applied last. */
    extraHeaders?: Record<string, string>;
}
/**
 * Guard a WebSocket upgrade against Cross-Site WebSocket Hijacking: browsers
 * always send an `Origin` header on a WS handshake, and — unlike fetch — WS is
 * NOT subject to CORS, so cookies would otherwise flow cross-site. We allow
 * same-origin (Origin host === Host header), configured CORS origins, and
 * non-browser clients (no Origin, which also carry no ambient cookies).
 */
declare function isWebSocketOriginAllowed(req: Request, security?: SecurityConfig): boolean;
declare function createCorsPreflightResponse(req: Request, security?: SecurityConfig): Response | null;
/**
 * Build the request URL, honoring `X-Forwarded-Proto` / `X-Forwarded-Host` when
 * `trustProxy` is set (app behind a TLS-terminating reverse proxy). This makes
 * `ctx.url.protocol` reflect the EXTERNAL scheme, so protocol-dependent logic —
 * `Secure` cookies, canonical URLs — is correct behind nginx / the gateway.
 * Security checks that compare the raw `Host`/`Origin` headers don't use this URL,
 * so they are unaffected. An invalid forwarded value is ignored by the URL setter.
 */
declare function resolveRequestUrl(req: Request, trustProxy?: boolean): URL;
declare function withSecurityHeaders(req: Request, res: Response, mode: Mode, security?: SecurityConfig, nonce?: string): Response;

export { type AsyncSessionBackend, type Bucket, CSRF_COOKIE, CSRF_HEADER, type CacheControlOptions, type ContentSecurityPolicyConfig, type Context, type CookieOptions, type CookieStore, type CorsConfig, type CorsOrigin, type CspDirectiveValue, type HstsConfig, type LocalStorageSnapshot, type Middleware, type Mode, type Next, type PageComponent, type PageMeta, type PermissionsPolicyConfig, type RateLimitOptions, type RateLimitStore, type RawSocket, type RealtimeBridge, type RealtimeBus, type RealtimeConnectMeta, type RealtimeEnvelope, type RealtimeHandler, type RealtimeRegistry, type RealtimeSocket, type RequestLoggerOptions, type RequestRecord, type RequireAuthOptions, type Room, type RoomAuthInfo, type RoomClient, type RoomDefinition, type RoomHandlers, SESSION_USER_KEY, type SaveUploadOptions, type SavedUpload, type SecurityConfig, type SeoConfig, type ServerSentEvent, type SessionBackend, type SessionEntry, type SessionStore, type StreamResponseInit, type TFunction, TTLCache, type Target, type TrustedTypesConfig, UploadError, bridgeRealtime, cacheControl, collectUploads, createContext, createCorsPreflightResponse, createRealtimeRegistry, csrfProtection, csrfToken, defaultKey, defineRoom, escapeHtml, etag, getUser, hashPassword, isRoomDefinition, isSafeIslandName, isSafeRequestPath, isWebSocketOriginAllowed, loadSession, logIn, logOut, notModified, peerKey, proxyKey, rateLimit, renderDevError, renderError, renderNotFound, renderProdError, renderStatusPage, requestLogger, requireAuth, resolveRequestUrl, sanitizeFilename, saveUpload, sessionAuth, setSessionBackend, sse, streamResponse, verifyCsrf, verifyPassword, withCacheControl, withContextHeaders, withSecurityHeaders };

Examples

Copy-ready examples taken from this package's published documentation.

Example 1

bun add @wrnexus/core

Example 2

// tsconfig.json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "@wrnexus/core",
  },
}

Example 3

import {
  createContext,
  withContextHeaders,
  sessionAuth,
  requireAuth,
  requestLogger,
  rateLimit,
  csrfProtection,
  type Middleware,
} from "@wrnexus/core";

const chain: Middleware[] = [
  requestLogger({ format: "json" }),
  rateLimit({ max: 100, windowMs: 60_000 }),
  csrfProtection(),
  sessionAuth(),
  requireAuth({ loginPath: "/login" }),
];

Example 4

import { hashPassword, verifyPassword, logIn, getUser } from "@wrnexus/core";

// Registration
const passwordHash = await hashPassword(form.password);

// Login
if (await verifyPassword(form.password, user.passwordHash)) {
  logIn(ctx, { id: user.id, email: user.email });
}

const current = getUser<{ id: string }>(ctx); // or null