# @wrnexus/core

> The framework core: the request `Context`, middleware contract, and the security, session, caching, streaming, realtime, and JSX primitives every other WrNexus package builds on.

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

## Overview

`@wrnexus/core` is the shared foundation of WrNexus. 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

```bash
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.

| Export                         | Kind | Description                                                                                                              |
| ------------------------------ | ---- | ------------------------------------------------------------------------------------------------------------------------ |
| `Context`                      | type | Per-request object: `req`, `url`, `lang`, `t`, `params`, `locals`, `user?`, `ip?`, `cookies`, `session`, `localStorage`. |
| `Next`                         | type | `() => Promise<Response> \| Response` — invokes the next middleware/handler.                                             |
| `Middleware`                   | type | `(ctx, next) => Promise<Response> \| Response`. Return `next()` to continue, or a `Response` to short-circuit.           |
| `createContext(req, url)`      | fn   | Build a fresh `Context` for an incoming request (wires up cookies, session, localStorage snapshot).                      |
| `withContextHeaders(ctx, res)` | fn   | Apply accumulated headers (e.g. `Set-Cookie`) from the context onto a response.                                          |
| `PageComponent`                | type | `(ctx) => string \| Promise<string>` — a page module's default export.                                                   |
| `PageMeta` / `SeoConfig`       | type | `<head>` metadata: `title`, `description`, `canonical`, `robots`, `image`, `twitterCard`, `themeColor`, …                |
| `TFunction`                    | type | `(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`.

| Export                           | Signature                              | Notes                                                                                                                 |
| -------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `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) => void`                 | Regenerates the session id (fixation defense), stores the user, sets `ctx.user`.                                      |
| `logOut(ctx)`                    | `(Context) => void`                    | Clears the session and `ctx.user`.                                                                                    |
| `getUser(ctx)`                   | `(Context) => U \| null`               | Current user from `ctx.user`, falling back to the session.                                                            |
| `sessionAuth()`                  | `() => Middleware`                     | Hydrates `ctx.user` from the session each request. Register early.                                                    |
| `requireAuth(options?)`          | `(RequireAuthOptions?) => Middleware`  | Guard: 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.

| Export                        | Signature                        | Notes                                                                                                            |
| ----------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `csrfToken(ctx)`              | `(Context) => string`            | Ensures the CSRF cookie exists and returns its token.                                                            |
| `verifyCsrf(ctx)`             | `(Context) => boolean`           | Safe methods (GET/HEAD/OPTIONS) pass; otherwise header/`ctx.locals._csrf` must match the cookie (constant-time). |
| `csrfProtection()`            | `() => Middleware`               | 403s 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.

| Export                | Signature                           | Notes                                                                  |
| --------------------- | ----------------------------------- | ---------------------------------------------------------------------- |
| `rateLimit(options?)` | `(RateLimitOptions?) => Middleware` | Main middleware.                                                       |
| `peerKey(ctx)`        | `(Context) => string`               | Non-spoofable key from `ctx.ip` (default).                             |
| `proxyKey(ctx)`       | `(Context) => string`               | Trusts `x-forwarded-for`/`x-real-ip`. Use only behind a trusted proxy. |
| `defaultKey`          | —                                   | **Deprecated** alias of `proxyKey`.                                    |

`RateLimitOptions`: `windowMs` (default `60_000`), `max` (default `60`),
`key`, `trustProxy` (default `false` → keys on `peerKey`; `true` → `proxyKey`),
`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`

| Export                    | Signature                               | Notes                                                                            |
| ------------------------- | --------------------------------------- | -------------------------------------------------------------------------------- |
| `requestLogger(options?)` | `(RequestLoggerOptions?) => Middleware` | One 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`

| Export                           | Kind  | Notes                                                                                                                                      |
| -------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `TTLCache<V>`                    | class | In-memory TTL cache: `get`, `set`, `getOrLoad(key, loader, ttlMs?)`, `delete`, `clear`, `size`. Constructor takes a default `ttlMs` (60s). |
| `cacheControl(options)`          | fn    | Build a `Cache-Control` value from `CacheControlOptions`.                                                                                  |
| `withCacheControl(res, options)` | fn    | Apply `Cache-Control` to a response.                                                                                                       |
| `etag(body, weak?)`              | fn    | Stable quoted FNV-1a ETag (weak by default).                                                                                               |
| `notModified(req, tag)`          | fn    | `true` 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 `File`s.

| Export                      | Signature                                           | Notes                                                                                  |
| --------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `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) => string`                                | Strips separators, traversal, control/illegal chars; caps at 255.                      |
| `UploadError`               | class                                               | Thrown 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`

| Export                          | Signature                                                                        | Notes                                                               |
| ------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `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>`.

| Export                                  | Signature                                                | Notes                                                                |
| --------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------------------- |
| `defineRoom(handlers)`                  | `(RoomHandlers) => RoomDefinition`                       | Define a room. Export the result as `default`.                       |
| `isRoomDefinition(value)`               | `(unknown) => boolean`                                   | Type guard for a room definition.                                    |
| `createRealtimeRegistry()`              | `() => RealtimeRegistry`                                 | Server-side connection manager mapping sockets ↔ rooms.              |
| `bridgeRealtime(registry, bus, topic?)` | `(RealtimeRegistry, RealtimeBus, string?) => () => void` | Bridge 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`

| Export                         | Signature                        | Notes                                                 |
| ------------------------------ | -------------------------------- | ----------------------------------------------------- |
| `renderError(err, mode)`       | `(unknown, Mode) => Response`    | Dev page (with stack) or generic prod page by `mode`. |
| `renderDevError(err, status?)` | `(unknown, number?) => Response` | Readable HTML error page including the stack trace.   |
| `renderProdError(status?)`     | `(number?) => Response`          | Generic page that never leaks file paths.             |
| `renderNotFound()`             | `() => Response`                 | Simple 404 page.                                      |

`Mode` = `"development" | "production"`.

### Security headers & CORS — `@wrnexus/core`

| Export                                                   | Signature            | Notes                                                                                                                                                    |
| -------------------------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `withSecurityHeaders(req, res, mode, security?, nonce?)` | → `Response`         | Applies 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?)`               | → `boolean`          | Guards 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`. WrNexus 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.

| Export                                                    | Kind              | Notes                                                                                                                                                             |
| --------------------------------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `setSessionBackend(backend)`                              | fn                | Swap the **sync** session persistence backend (`SessionBackend`) — e.g. `bun:sqlite`. Default is process-local memory. Call once at startup.                      |
| `loadSession(backend, options?)`                          | fn → `Middleware` | Back `ctx.session` with an **async** store (`AsyncSessionBackend`: `load`/`save`/`destroy`) — loads before the request, saves after. `options.ttlMs` default 24h. |
| `CookieStore`                                             | type              | `get`/`getAll`/`has`/`set(name, value, opts?)`/`delete`/`headers`.                                                                                                |
| `SessionStore`                                            | type              | `id`/`get`/`getAll`/`set`/`delete`/`regenerate`/`clear`.                                                                                                          |
| `LocalStorageSnapshot`                                    | type              | Read-only view of the browser's localStorage sent via header for CSR bindings.                                                                                    |
| `CookieOptions`                                           | type              | `path`, `domain`, `maxAge`, `expires`, `httpOnly`, `secure`, `sameSite`.                                                                                          |
| `SessionEntry` / `SessionBackend` / `AsyncSessionBackend` | types             | Session persistence contracts.                                                                                                                                    |

### Low-level security helpers — `@wrnexus/core`

| Export                        | Signature             | Notes                                               |
| ----------------------------- | --------------------- | --------------------------------------------------- |
| `escapeHtml(value)`           | `(string) => string`  | Escape for HTML text/attributes.                    |
| `isSafeIslandName(name)`      | `(string) => boolean` | Allow only a conservative `[A-Za-z0-9_-]+` charset. |
| `isSafeRequestPath(pathname)` | `(string) => boolean` | Reject 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`.

| Export                                     | Kind   | Notes                                                                                   |
| ------------------------------------------ | ------ | --------------------------------------------------------------------------------------- |
| `jsx` / `jsxs`                             | fn     | The runtime factory (TypeScript calls these automatically). Returns an `Html` instance. |
| `Fragment`                                 | symbol | JSX fragment marker.                                                                    |
| `Html`                                     | class  | Wraps a raw, already-safe HTML string (`toString()` returns it).                        |
| `mustache(expr)`                           | fn     | Emit a `{{expr}}` placeholder (tagged-template or string form) for the client binder.   |
| `JSXComponent` / `JSXProps` / `Renderable` | types  | Component 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; `className`→`class`, `htmlFor`→`for`, and
`style` objects are serialized to CSS text.

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

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

## Usage

### A minimal middleware chain

```ts
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

```ts
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

```ts
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

```ts
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

```ts
// 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:

```ts
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

```tsx
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 WrNexus server/router packages.
- Subpath exports: `@wrnexus/core/jsx-runtime` and `@wrnexus/core/jsx-dev-runtime`
  for TypeScript's automatic JSX transform.
