26 KiB
@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 (connects 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 wrn-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 |
"wrn-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.