Bumps all 47 packages, the root manifest and the VS Code extension to 0.8.6, and rebuilds the editor compiler, language server and extension bundles that embed the version. The release carries the output delivery fix: camelCase outputs now reach parent bindings, and 18 components emit through output.* instead of hand-built CustomEvents. See the 0.8.6 migration entry for what changes for consumers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@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
bun add @wrnexus/core
Private package — the machine must be authenticated to the
wrnexusnpm 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 bysessionAuth/logIn), ornull.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 }.
Resilience — @wrnexus/core
resilientCall standardizes cancellation-aware timeouts, controlled retries,
fixed or exponential backoff, fallback responses, circuit breaking, and bounded
concurrency. Reuse a declarative circuit/bulkhead options object, or an explicit
CircuitBreaker/Bulkhead instance, wherever calls must share health and
capacity state.
import { resilientCall } from "@wrnexus/core";
const paymentCircuit = { failures: 5, resetAfter: "30s" } as const;
const status = await resilientCall({
timeout: "5s",
retries: 3,
retryDelay: "100ms",
backoff: "exponential",
circuitBreaker: paymentCircuit,
bulkhead: { concurrency: 20, queue: 100 },
run: (signal) => paymentProvider.checkStatus({ signal }),
fallback: () => ({ state: "unavailable" }),
});
CircuitBreaker.snapshot() reports closed, open, or half-open, failure
and success counts, and the remaining retry delay for health endpoints and
development tooling. Fail-fast conditions use stable WRN-RESILIENCE-* codes.
Core's existing HealthRegistry, withIdempotency, and pluggable stores/locks
cover health reporting, idempotent requests, and distributed coordination.
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 Files.
| 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:
// 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-standardRequest/Response/FormData/ReadableStream, and the globalcrypto. 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) orloadSession(async, e.g. Redis) for sessions, a customRateLimitStorefor limits, andbridgeRealtimefor realtime. - Works with the rest of the framework: realtime bridging is structurally
compatible with
@wrnexus/pubsub; the security, auth, and JSX primitives here are consumed by the WrNexus server/router packages. - Subpath exports:
@wrnexus/core/jsx-runtimeand@wrnexus/core/jsx-dev-runtimefor TypeScript's automatic JSX transform.