1557 lines
96 KiB
Plaintext
1557 lines
96 KiB
Plaintext
page wrnexuscore {
|
|
seo {
|
|
title = "@wrnexus/core"
|
|
description = "Contexts, middleware, security, sessions, caching, JSX, and realtime."
|
|
}
|
|
|
|
view {
|
|
<div class="docs-shell">
|
|
<a href="#main" class="skip-link">Skip to content</a>
|
|
<header class="topbar">
|
|
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
|
<nav aria-label="Primary"><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="https://component.wrnexusjs.dev/">Components</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
|
<div class="topbar-actions"><a class="preview-pill" href="/access">Private preview · v0.8.0</a><button data-wire-theme-toggle class="theme-button" aria-label="Toggle color theme" title="Toggle color theme">◐</button></div>
|
|
</header>
|
|
<div class="mobile-doc-nav"><details><summary>Browse documentation</summary><nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="https://component.wrnexusjs.dev/">Components</a><a href="/language">Language</a><a href="/architecture">Architecture</a><a href="/tutorial">Tutorial</a><a href="/guides/project-structure">Guides</a><a href="/examples">Examples</a><a href="/search">Search</a></nav></details></div>
|
|
<main class="portal-main docs-layout">
|
|
<article id="main" class="documentation prose standalone package-document"><nav class="breadcrumbs" aria-label="Breadcrumb"><a href="/">Home</a><span>/</span><a href="/packages">Packages</a><span>/</span><span aria-current="page">@wrnexus/core</span></nav><section class="doc-intro"><span class="eyebrow">Core · Package reference</span><h1>@wrnexus/core</h1><p>Contexts, middleware, security, sessions, caching, JSX, and realtime.</p><div class="doc-meta"><span>v0.8.0</span><span>Private registry</span><span>Core</span></div><section id="access" class="access-callout"><h2>Install the package</h2><p>After WorkRoot approves private registry access, install the release-aligned package:</p><pre><code>bun add @wrnexus/core@0.8.0</code><button type="button" class="copy-button" aria-label="Copy installation command">Copy</button></pre><p><a href="/access">Request preview access</a>. Never put registry tokens in source control.</p></section></section><section id="guide"><blockquote>The framework core: the request <code>Context</code>, middleware contract, and the security, session, caching, streaming, realtime, and JSX primitives every other WRNexusJS package builds on.</blockquote>
|
|
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
|
|
<h3 id="overview">Overview</h3>
|
|
<p><code>@wrnexus/core</code> is the shared foundation of WRNexusJS. It defines the <code>Context</code> object that flows through every middleware, page, and API route, plus the <code>Middleware</code>/<code>Next</code> 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 <strong>server-side</strong> and Bun-native (it uses <code>Bun.password</code>, <code>Bun.write</code>, the web-standard <code>Request</code>/<code>Response</code>, and <code>crypto</code>). You depend on it directly and transitively through the rest of the framework.</p>
|
|
<pre data-language="bash"><code>bun add @wrnexus/core</code></pre>
|
|
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
|
|
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported).</blockquote>
|
|
<h3 id="api">API</h3>
|
|
<h4 id="context-middleware-wrnexus-core">Context & middleware — <code>@wrnexus/core</code></h4>
|
|
<p>The <code>Context</code> (<code>ctx</code>) is the single value passed to middleware and handlers.</p>
|
|
<div class="table-wrap"><table>
|
|
<thead><tr><th>Export</th><th>Kind</th><th>Description</th></tr></thead>
|
|
<tbody><tr><td><code>Context</code></td><td>type</td><td>Per-request object: <code>req</code>, <code>url</code>, <code>lang</code>, <code>t</code>, <code>params</code>, <code>locals</code>, <code>user?</code>, <code>ip?</code>, <code>cookies</code>, <code>session</code>, <code>localStorage</code>.</td></tr><tr><td><code>Next</code></td><td>type</td><td>`() => Promise<Response> \</td><td>Response` — invokes the next middleware/handler.</td></tr><tr><td><code>Middleware</code></td><td>type</td><td>`(ctx, next) => Promise<Response> \</td><td>Response<code>. Return </code>next()<code> to continue, or a </code>Response` to short-circuit.</td></tr><tr><td><code>createContext(req, url)</code></td><td>fn</td><td>Build a fresh <code>Context</code> for an incoming request (wires up cookies, session, localStorage snapshot).</td></tr><tr><td><code>withContextHeaders(ctx, res)</code></td><td>fn</td><td>Apply accumulated headers (e.g. <code>Set-Cookie</code>) from the context onto a response.</td></tr><tr><td><code>PageComponent</code></td><td>type</td><td>`(ctx) => string \</td><td>Promise<string>` — a page module's default export.</td></tr><tr><td><code>PageMeta</code> / <code>SeoConfig</code></td><td>type</td><td><code><head></code> metadata: <code>title</code>, <code>description</code>, <code>canonical</code>, <code>robots</code>, <code>image</code>, <code>twitterCard</code>, <code>themeColor</code>, …</td></tr><tr><td><code>TFunction</code></td><td>type</td><td><code>(key, params?) => string</code> — translate a key for <code>ctx.lang</code>, interpolating <code>{param}</code> placeholders.</td></tr></tbody></table></div>
|
|
<p>Key <code>Context</code> fields:</p>
|
|
<ul>
|
|
<li><code>ctx.locals</code> — per-request scratch space for passing values between middleware.</li>
|
|
<li><code>ctx.user</code> — the authenticated user (populated by <code>sessionAuth</code>/<code>logIn</code>), or <code>null</code>.</li>
|
|
<li><code>ctx.ip</code> — the direct socket peer IP (not spoofable via headers).</li>
|
|
<li><code>ctx.cookies</code> / <code>ctx.session</code> / <code>ctx.localStorage</code> — see <strong>Storage</strong> below.</li>
|
|
</ul>
|
|
<h4 id="authentication-wrnexus-core">Authentication — <code>@wrnexus/core</code></h4>
|
|
<p>Passwords are hashed with argon2id via <code>Bun.password</code>; sessions ride the cookie-backed <code>SessionStore</code>.</p>
|
|
<div class="table-wrap"><table>
|
|
<thead><tr><th>Export</th><th>Signature</th><th>Notes</th></tr></thead>
|
|
<tbody><tr><td><code>hashPassword(password)</code></td><td><code>(string) => Promise<string></code></td><td>argon2id hash to store.</td></tr><tr><td><code>verifyPassword(password, hash)</code></td><td><code>(string, string) => Promise<boolean></code></td><td>Constant-safe; returns <code>false</code> on bad/empty hash.</td></tr><tr><td><code>logIn(ctx, user)</code></td><td><code>(Context, U) => void</code></td><td>Regenerates the session id (fixation defense), stores the user, sets <code>ctx.user</code>.</td></tr><tr><td><code>logOut(ctx)</code></td><td><code>(Context) => void</code></td><td>Clears the session and <code>ctx.user</code>.</td></tr><tr><td><code>getUser(ctx)</code></td><td>`(Context) => U \</td><td>null`</td><td>Current user from <code>ctx.user</code>, falling back to the session.</td></tr><tr><td><code>sessionAuth()</code></td><td><code>() => Middleware</code></td><td>Hydrates <code>ctx.user</code> from the session each request. Register early.</td></tr><tr><td><code>requireAuth(options?)</code></td><td><code>(RequireAuthOptions?) => Middleware</code></td><td>Guard: API/fetch requests get <code>401 JSON</code>, page navigations get <code>302</code> to <code>loginPath</code> (default <code>/login</code>) with <code>?next=</code>.</td></tr><tr><td><code>SESSION_USER_KEY</code></td><td><code>"user"</code></td><td>Session key holding the user.</td></tr></tbody></table></div>
|
|
<p><code>RequireAuthOptions</code>: <code>{ loginPath?: string }</code>.</p>
|
|
<h4 id="csrf-wrnexus-core">CSRF — <code>@wrnexus/core</code></h4>
|
|
<p>Double-submit cookie pattern: a readable <code>wire-csrf</code> cookie is echoed in an <code>x-csrf-token</code> header on unsafe requests.</p>
|
|
<div class="table-wrap"><table>
|
|
<thead><tr><th>Export</th><th>Signature</th><th>Notes</th></tr></thead>
|
|
<tbody><tr><td><code>csrfToken(ctx)</code></td><td><code>(Context) => string</code></td><td>Ensures the CSRF cookie exists and returns its token.</td></tr><tr><td><code>verifyCsrf(ctx)</code></td><td><code>(Context) => boolean</code></td><td>Safe methods (GET/HEAD/OPTIONS) pass; otherwise header/<code>ctx.locals._csrf</code> must match the cookie (constant-time).</td></tr><tr><td><code>csrfProtection()</code></td><td><code>() => Middleware</code></td><td>403s unsafe requests with a missing/mismatched token.</td></tr><tr><td><code>CSRF_COOKIE</code> / <code>CSRF_HEADER</code></td><td><code>"wire-csrf"</code> / <code>"x-csrf-token"</code></td><td>Cookie & header names.</td></tr></tbody></table></div>
|
|
<h4 id="rate-limiting-wrnexus-core">Rate limiting — <code>@wrnexus/core</code></h4>
|
|
<p>Fixed-window limiter that returns <code>429</code> with <code>Retry-After</code> and emits <code>RateLimit-Limit</code>/<code>-Remaining</code>/<code>-Reset</code> headers.</p>
|
|
<div class="table-wrap"><table>
|
|
<thead><tr><th>Export</th><th>Signature</th><th>Notes</th></tr></thead>
|
|
<tbody><tr><td><code>rateLimit(options?)</code></td><td><code>(RateLimitOptions?) => Middleware</code></td><td>Main middleware.</td></tr><tr><td><code>peerKey(ctx)</code></td><td><code>(Context) => string</code></td><td>Non-spoofable key from <code>ctx.ip</code> (default).</td></tr><tr><td><code>proxyKey(ctx)</code></td><td><code>(Context) => string</code></td><td>Trusts <code>x-forwarded-for</code>/<code>x-real-ip</code>. Use only behind a trusted proxy.</td></tr><tr><td><code>defaultKey</code></td><td>—</td><td><strong>Deprecated</strong> alias of <code>proxyKey</code>.</td></tr></tbody></table></div>
|
|
<p><code>RateLimitOptions</code>: <code>windowMs</code> (default <code>60_000</code>), <code>max</code> (default <code>60</code>), <code>key</code>, <code>trustProxy</code> (default <code>false</code> → keys on <code>peerKey</code>; <code>true</code> → <code>proxyKey</code>), <code>message</code>, <code>headers</code> (default <code>true</code>), <code>store</code>.</p>
|
|
<p><code>RateLimitStore</code> is pluggable — implement <code>hit(key, windowMs, now) => Bucket | Promise<Bucket></code> (a <code>Bucket</code> is <code>{ count, resetAt }</code>) to back limits with Redis/SQL across instances. The default store is process-local memory.</p>
|
|
<h4 id="request-logging-wrnexus-core">Request logging — <code>@wrnexus/core</code></h4>
|
|
<div class="table-wrap"><table>
|
|
<thead><tr><th>Export</th><th>Signature</th><th>Notes</th></tr></thead>
|
|
<tbody><tr><td><code>requestLogger(options?)</code></td><td><code>(RequestLoggerOptions?) => Middleware</code></td><td>One record per request with a request id (stored on <code>ctx.locals[requestIdKey]</code>).</td></tr></tbody></table></div>
|
|
<p><code>RequestLoggerOptions</code>: <code>format</code> (<code>"pretty"</code> default \| <code>"json"</code>), <code>sink(line, record)</code> (default <code>console.log</code>), <code>requestIdKey</code> (default <code>"requestId"</code>), <code>now</code>. <code>RequestRecord</code> = <code>{ time, id, method, path, status, durationMs }</code>.</p>
|
|
<h4 id="resilience-wrnexus-core">Resilience — <code>@wrnexus/core</code></h4>
|
|
<p><code>resilientCall</code> 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 <code>CircuitBreaker</code>/<code>Bulkhead</code> instance, wherever calls must share health and capacity state.</p>
|
|
<pre data-language="ts"><code>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" }),
|
|
});</code></pre>
|
|
<p><code>CircuitBreaker.snapshot()</code> reports <code>closed</code>, <code>open</code>, or <code>half-open</code>, failure and success counts, and the remaining retry delay for health endpoints and development tooling. Fail-fast conditions use stable <code>WRN-RESILIENCE-*</code> codes. Core's existing <code>HealthRegistry</code>, <code>withIdempotency</code>, and pluggable stores/locks cover health reporting, idempotent requests, and distributed coordination.</p>
|
|
<h4 id="caching-wrnexus-core">Caching — <code>@wrnexus/core</code></h4>
|
|
<div class="table-wrap"><table>
|
|
<thead><tr><th>Export</th><th>Kind</th><th>Notes</th></tr></thead>
|
|
<tbody><tr><td><code>TTLCache<V></code></td><td>class</td><td>In-memory TTL cache: <code>get</code>, <code>set</code>, <code>getOrLoad(key, loader, ttlMs?)</code>, <code>delete</code>, <code>clear</code>, <code>size</code>. Constructor takes a default <code>ttlMs</code> (60s).</td></tr><tr><td><code>cacheControl(options)</code></td><td>fn</td><td>Build a <code>Cache-Control</code> value from <code>CacheControlOptions</code>.</td></tr><tr><td><code>withCacheControl(res, options)</code></td><td>fn</td><td>Apply <code>Cache-Control</code> to a response.</td></tr><tr><td><code>etag(body, weak?)</code></td><td>fn</td><td>Stable quoted FNV-1a ETag (weak by default).</td></tr><tr><td><code>notModified(req, tag)</code></td><td>fn</td><td><code>true</code> when <code>If-None-Match</code> matches — send a <code>304</code>.</td></tr></tbody></table></div>
|
|
<p><code>CacheControlOptions</code>: <code>maxAge</code>, <code>sMaxAge</code>, <code>private</code>, <code>noStore</code>, <code>noCache</code>, <code>staleWhileRevalidate</code>, <code>immutable</code>.</p>
|
|
<h4 id="file-uploads-wrnexus-core">File uploads — <code>@wrnexus/core</code></h4>
|
|
<p>Bun parses <code>multipart/form-data</code> via <code>Request.formData()</code>; these helpers validate and persist the resulting <code>File</code>s.</p>
|
|
<div class="table-wrap"><table>
|
|
<thead><tr><th>Export</th><th>Signature</th><th>Notes</th></tr></thead>
|
|
<tbody><tr><td><code>collectUploads(form)</code></td><td><code>(FormData) => { field, file }[]</code></td><td>Every non-empty <code>File</code> in a parsed form.</td></tr><tr><td><code>saveUpload(file, options)</code></td><td><code>(File, SaveUploadOptions) => Promise<SavedUpload></code></td><td>Validates size/type, sanitizes the name, writes via <code>Bun.write</code>. Throws <code>UploadError</code>.</td></tr><tr><td><code>sanitizeFilename(name)</code></td><td><code>(string) => string</code></td><td>Strips separators, traversal, control/illegal chars; caps at 255.</td></tr><tr><td><code>UploadError</code></td><td>class</td><td>Thrown on rejected uploads.</td></tr></tbody></table></div>
|
|
<p><code>SaveUploadOptions</code>: <code>dir</code> (required), <code>maxBytes</code>, <code>allowedTypes</code> (MIME types like <code>"image/png"</code> and/or extensions like <code>".png"</code>), <code>filename(file)</code>. <code>SavedUpload</code> = <code>{ path, filename, size, type }</code>.</p>
|
|
<h4 id="streaming-sse-wrnexus-core">Streaming & SSE — <code>@wrnexus/core</code></h4>
|
|
<div class="table-wrap"><table>
|
|
<thead><tr><th>Export</th><th>Signature</th><th>Notes</th></tr></thead>
|
|
<tbody><tr><td><code>streamResponse(source, init?)</code></td><td>`(Iterable\</td><td>AsyncIterable<string\</td><td>Uint8Array>, StreamResponseInit?) => Response`</td><td>Streaming <code>Response</code> from a chunk source (basis for streaming SSR).</td></tr><tr><td><code>sse(source)</code></td><td>`(Iterable\</td><td>AsyncIterable<ServerSentEvent>) => Response`</td><td><code>text/event-stream</code> response.</td></tr></tbody></table></div>
|
|
<p><code>StreamResponseInit</code>: <code>status</code>, <code>headers</code>, <code>contentType</code> (default <code>"text/html; charset=utf-8"</code>). <code>ServerSentEvent</code>: <code>{ data, event?, id?, retry? }</code>.</p>
|
|
<h4 id="realtime-rooms-wrnexus-core">Realtime rooms — <code>@wrnexus/core</code></h4>
|
|
<p>WebSocket rooms. A file in <code>app/realtime/</code> exports <code>default defineRoom({ ... })</code> and is served at <code>ws://host/realtime/<name></code>.</p>
|
|
<div class="table-wrap"><table>
|
|
<thead><tr><th>Export</th><th>Signature</th><th>Notes</th></tr></thead>
|
|
<tbody><tr><td><code>defineRoom(handlers)</code></td><td><code>(RoomHandlers) => RoomDefinition</code></td><td>Define a room. Export the result as <code>default</code>.</td></tr><tr><td><code>isRoomDefinition(value)</code></td><td><code>(unknown) => boolean</code></td><td>Type guard for a room definition.</td></tr><tr><td><code>createRealtimeRegistry()</code></td><td><code>() => RealtimeRegistry</code></td><td>Server-side connection manager mapping sockets ↔ rooms.</td></tr><tr><td><code>bridgeRealtime(registry, bus, topic?)</code></td><td><code>(RealtimeRegistry, RealtimeBus, string?) => () => void</code></td><td>Bridge broadcasts/<code>toUser</code> sends across processes via a pub/sub bus.</td></tr></tbody></table></div>
|
|
<p><code>RoomHandlers</code>: <code>authorize(info) => boolean</code> (gate before accept — return <code>false</code> to reject with 403), <code>onConnect(client)</code>, <code>onMessage(client, message)</code> (JSON auto-parsed), <code>onLeave(client)</code>. A handler receives a <code>RoomClient</code> with <code>id</code>, <code>user</code>, <code>query</code>, <code>data</code>, <code>room</code>, and <code>send</code> / <code>broadcast</code> / <code>to(id)</code> / <code>toUser(user)</code> / <code>close</code>. The <code>Room</code> API adds <code>state</code>, <code>clients()</code>, <code>count()</code>, and <code>broadcast</code>. <code>RealtimeBus</code> is structurally satisfied by <code>@wrnexus/pubsub</code>. Legacy <code>RealtimeHandler</code>/<code>RealtimeSocket</code> raw handlers are still exported. Connection-targeted sends (<code>send</code>, <code>to(id)</code>) stay local; room broadcasts and <code>toUser</code> cross the bridge.</p>
|
|
<h4 id="error-pages-wrnexus-core">Error pages — <code>@wrnexus/core</code></h4>
|
|
<div class="table-wrap"><table>
|
|
<thead><tr><th>Export</th><th>Signature</th><th>Notes</th></tr></thead>
|
|
<tbody><tr><td><code>renderError(err, mode)</code></td><td><code>(unknown, Mode) => Response</code></td><td>Dev page (with stack) or generic prod page by <code>mode</code>.</td></tr><tr><td><code>renderDevError(err, status?)</code></td><td><code>(unknown, number?) => Response</code></td><td>Readable HTML error page including the stack trace.</td></tr><tr><td><code>renderProdError(status?)</code></td><td><code>(number?) => Response</code></td><td>Generic page that never leaks file paths.</td></tr><tr><td><code>renderNotFound()</code></td><td><code>() => Response</code></td><td>Simple 404 page.</td></tr></tbody></table></div>
|
|
<p><code>Mode</code> = <code>"development" | "production"</code>.</p>
|
|
<h4 id="security-headers-cors-wrnexus-core">Security headers & CORS — <code>@wrnexus/core</code></h4>
|
|
<div class="table-wrap"><table>
|
|
<thead><tr><th>Export</th><th>Signature</th><th>Notes</th></tr></thead>
|
|
<tbody><tr><td><code>withSecurityHeaders(req, res, mode, security?, nonce?)</code></td><td>→ <code>Response</code></td><td>Applies CORS + CSP, HSTS, <code>X-Frame-Options</code>, <code>X-Content-Type-Options</code>, <code>Referrer-Policy</code>, <code>Permissions-Policy</code>, COOP, Trusted Types, and <code>extraHeaders</code>.</td></tr><tr><td><code>createCorsPreflightResponse(req, security?)</code></td><td>→ `Response \</td><td>null`</td><td>Builds a <code>204</code>/<code>403</code> preflight response for CORS <code>OPTIONS</code> requests.</td></tr><tr><td><code>isWebSocketOriginAllowed(req, security?)</code></td><td>→ <code>boolean</code></td><td>Guards WS upgrades against cross-site hijacking (allows same-origin, configured CORS origins, and non-browser clients).</td></tr></tbody></table></div>
|
|
<p>Config types: <code>SecurityConfig</code> (top-level), <code>CorsConfig</code>/<code>CorsOrigin</code>, <code>ContentSecurityPolicyConfig</code>/<code>CspDirectiveValue</code>, <code>HstsConfig</code>, <code>TrustedTypesConfig</code>, <code>PermissionsPolicyConfig</code>. WRNexusJS applies sensible defaults (self-only CSP, <code>frame-ancestors 'none'</code>, restrictive Permissions-Policy, HSTS in production, Trusted Types in production); each is individually overridable or disable-able via <code>false</code>.</p>
|
|
<h4 id="storage-cookies-sessions-localstorage-wrnexus-core">Storage: cookies, sessions, localStorage — <code>@wrnexus/core</code></h4>
|
|
<p>These back the <code>ctx.cookies</code>, <code>ctx.session</code>, and <code>ctx.localStorage</code> fields.</p>
|
|
<div class="table-wrap"><table>
|
|
<thead><tr><th>Export</th><th>Kind</th><th>Notes</th></tr></thead>
|
|
<tbody><tr><td><code>setSessionBackend(backend)</code></td><td>fn</td><td>Swap the <strong>sync</strong> session persistence backend (<code>SessionBackend</code>) — e.g. <code>bun:sqlite</code>. Default is process-local memory. Call once at startup.</td></tr><tr><td><code>loadSession(backend, options?)</code></td><td>fn → <code>Middleware</code></td><td>Back <code>ctx.session</code> with an <strong>async</strong> store (<code>AsyncSessionBackend</code>: <code>load</code>/<code>save</code>/<code>destroy</code>) — loads before the request, saves after. <code>options.ttlMs</code> default 24h.</td></tr><tr><td><code>CookieStore</code></td><td>type</td><td><code>get</code>/<code>getAll</code>/<code>has</code>/<code>set(name, value, opts?)</code>/<code>delete</code>/<code>headers</code>.</td></tr><tr><td><code>SessionStore</code></td><td>type</td><td><code>id</code>/<code>get</code>/<code>getAll</code>/<code>set</code>/<code>delete</code>/<code>regenerate</code>/<code>clear</code>.</td></tr><tr><td><code>LocalStorageSnapshot</code></td><td>type</td><td>Read-only view of the browser's localStorage sent via header for CSR bindings.</td></tr><tr><td><code>CookieOptions</code></td><td>type</td><td><code>path</code>, <code>domain</code>, <code>maxAge</code>, <code>expires</code>, <code>httpOnly</code>, <code>secure</code>, <code>sameSite</code>.</td></tr><tr><td><code>SessionEntry</code> / <code>SessionBackend</code> / <code>AsyncSessionBackend</code></td><td>types</td><td>Session persistence contracts.</td></tr></tbody></table></div>
|
|
<h4 id="low-level-security-helpers-wrnexus-core">Low-level security helpers — <code>@wrnexus/core</code></h4>
|
|
<div class="table-wrap"><table>
|
|
<thead><tr><th>Export</th><th>Signature</th><th>Notes</th></tr></thead>
|
|
<tbody><tr><td><code>escapeHtml(value)</code></td><td><code>(string) => string</code></td><td>Escape for HTML text/attributes.</td></tr><tr><td><code>isSafeIslandName(name)</code></td><td><code>(string) => boolean</code></td><td>Allow only a conservative <code>[A-Za-z0-9_-]+</code> charset.</td></tr><tr><td><code>isSafeRequestPath(pathname)</code></td><td><code>(string) => boolean</code></td><td>Reject NULs, <code>..</code> traversal, and backslashes.</td></tr></tbody></table></div>
|
|
<h4 id="jsx-runtime-wrnexus-core-wrnexus-core-jsx-runtime-wrnexus-core-jsx-dev-runtime">JSX runtime — <code>@wrnexus/core</code>, <code>@wrnexus/core/jsx-runtime</code>, <code>@wrnexus/core/jsx-dev-runtime</code></h4>
|
|
<p>A server-side JSX runtime that renders to HTML <strong>strings</strong> (no virtual DOM). Point <code>tsconfig</code>'s <code>jsxImportSource</code> at <code>@wrnexus/core</code>.</p>
|
|
<div class="table-wrap"><table>
|
|
<thead><tr><th>Export</th><th>Kind</th><th>Notes</th></tr></thead>
|
|
<tbody><tr><td><code>jsx</code> / <code>jsxs</code></td><td>fn</td><td>The runtime factory (TypeScript calls these automatically). Returns an <code>Html</code> instance.</td></tr><tr><td><code>Fragment</code></td><td>symbol</td><td>JSX fragment marker.</td></tr><tr><td><code>Html</code></td><td>class</td><td>Wraps a raw, already-safe HTML string (<code>toString()</code> returns it).</td></tr><tr><td><code>mustache(expr)</code></td><td>fn</td><td>Emit a <code>{{expr}}</code> placeholder (tagged-template or string form) for the client binder.</td></tr><tr><td><code>JSXComponent</code> / <code>JSXProps</code> / <code>Renderable</code></td><td>types</td><td>Component signature and renderable value types.</td></tr></tbody></table></div>
|
|
<p>Values interpolated as children are HTML-escaped unless they are an <code>Html</code> instance; use <code>dangerouslySetInnerHTML={{ __html }}</code> for trusted markup. Void elements render without a closing tag; <code>className</code>→<code>class</code>, <code>htmlFor</code>→<code>for</code>, and <code>style</code> objects are serialized to CSS text.</p>
|
|
<p>The subpath exports map to the runtime TypeScript's JSX transform expects:</p>
|
|
<pre data-language="jsonc"><code>// tsconfig.json
|
|
{
|
|
"compilerOptions": {
|
|
"jsx": "react-jsx",
|
|
"jsxImportSource": "@wrnexus/core",
|
|
},
|
|
}</code></pre>
|
|
<h3 id="usage">Usage</h3>
|
|
<h4 id="a-minimal-middleware-chain">A minimal middleware chain</h4>
|
|
<pre data-language="ts"><code>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" }),
|
|
];</code></pre>
|
|
<h4 id="password-auth">Password auth</h4>
|
|
<pre data-language="ts"><code>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</code></pre>
|
|
<h4 id="http-caching-with-etags">HTTP caching with ETags</h4>
|
|
<pre data-language="ts"><code>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 });</code></pre>
|
|
<h4 id="streaming-sse">Streaming SSE</h4>
|
|
<pre data-language="ts"><code>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());</code></pre>
|
|
<h4 id="a-realtime-room">A realtime room</h4>
|
|
<pre data-language="ts"><code>// 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 });
|
|
},
|
|
});</code></pre>
|
|
<p>Scale it across processes:</p>
|
|
<pre data-language="ts"><code>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)));</code></pre>
|
|
<h4 id="jsx-rendering">JSX rendering</h4>
|
|
<pre data-language="tsx"><code>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" } });</code></pre>
|
|
<h3 id="requirements-notes">Requirements / Notes</h3>
|
|
<ul>
|
|
<li><strong>Bun-only.</strong> Uses <code>Bun.password</code> (argon2id), <code>Bun.write</code>, web-standard</li>
|
|
<p><code>Request</code>/<code>Response</code>/<code>FormData</code>/<code>ReadableStream</code>, and the global <code>crypto</code>. Node is not supported.</p>
|
|
<li>Session and rate-limit backends default to <strong>process-local memory</strong>. For</li>
|
|
<p>multi-instance deployments, swap in a shared backend: <code>setSessionBackend</code> (sync, e.g. <code>bun:sqlite</code>) or <code>loadSession</code> (async, e.g. Redis) for sessions, a custom <code>RateLimitStore</code> for limits, and <code>bridgeRealtime</code> for realtime.</p>
|
|
<li>Works with the rest of the framework: realtime bridging is structurally</li>
|
|
<p>compatible with [<code>@wrnexus/pubsub</code>](../pubsub); the security, auth, and JSX primitives here are consumed by the WRNexusJS server/router packages.</p>
|
|
<li>Subpath exports: <code>@wrnexus/core/jsx-runtime</code> and <code>@wrnexus/core/jsx-dev-runtime</code></li>
|
|
<p>for TypeScript's automatic JSX transform.</p>
|
|
</ul></section><section id="api" class="api"><h2>Complete TypeScript API</h2><p>Generated from the exact installed package declarations.</p><pre data-language="typescript"><code>export { Fragment, Html, Component as JSXComponent, Props as JSXProps, Renderable, jsx, jsxs, mustache } from './jsx-runtime.js';
|
|
|
|
interface Tenant {
|
|
id: string;
|
|
slug?: string;
|
|
name?: string;
|
|
metadata?: Record<string, unknown>;
|
|
}
|
|
interface TenantResource {
|
|
tenantId: string;
|
|
}
|
|
interface TenantMembership {
|
|
tenantId: string;
|
|
userId: string;
|
|
roles?: string[];
|
|
workspaceIds?: string[];
|
|
}
|
|
interface TenantAuditEvent {
|
|
tenantId: string;
|
|
action: string;
|
|
actorId?: string;
|
|
resource?: string;
|
|
metadata?: Record<string, unknown>;
|
|
createdAt: number;
|
|
}
|
|
interface TenantQuota {
|
|
tenantId: string;
|
|
resource: string;
|
|
limit: number;
|
|
usage: number;
|
|
}
|
|
interface TenantDirectoryStore {
|
|
putMembership(value: TenantMembership): Promise<void>;
|
|
getMembership(tenantId: string, userId: string): Promise<TenantMembership | null>;
|
|
listMemberships(tenantId: string): Promise<TenantMembership[]>;
|
|
putQuota(value: TenantQuota): Promise<void>;
|
|
getQuota(tenantId: string, resource: string): Promise<TenantQuota | null>;
|
|
}
|
|
type TenantResolver = (ctx: Context) => Tenant | null | Promise<Tenant | null>;
|
|
interface TenantMiddlewareOptions {
|
|
required?: boolean;
|
|
status?: number;
|
|
}
|
|
declare function tenantMiddleware(resolveTenant: TenantResolver, options?: TenantMiddlewareOptions): Middleware;
|
|
declare function tenantFromSubdomain(lookup: (slug: string, ctx: Context) => Tenant | null | Promise<Tenant | null>, rootDomains?: string[]): TenantResolver;
|
|
declare function tenantFromDomain(lookup: (domain: string, ctx: Context) => Tenant | null | Promise<Tenant | null>): TenantResolver;
|
|
declare function tenantFromPath(lookup: (slug: string, ctx: Context) => Tenant | null | Promise<Tenant | null>, prefix?: string): TenantResolver;
|
|
/** Header resolution is intentionally opt-in and must only be used behind a trusted proxy. */
|
|
declare function tenantFromHeader(lookup: (id: string, ctx: Context) => Tenant | null | Promise<Tenant | null>, header?: string): TenantResolver;
|
|
declare function tenantFromSession(resolveId: (ctx: Context) => string | null | Promise<string | null>, lookup: (id: string, ctx: Context) => Tenant | null | Promise<Tenant | null>): TenantResolver;
|
|
declare function composeTenantResolvers(...resolvers: TenantResolver[]): TenantResolver;
|
|
declare function requireTenant(ctx: Context): Tenant;
|
|
/** Wrap a repository so every operation receives the current tenant id. */
|
|
declare function tenantScope<T extends object>(tenant: Tenant, repository: T): T & {
|
|
tenantId: string;
|
|
};
|
|
declare function assertTenantAccess(tenant: Tenant, resource: TenantResource): void;
|
|
declare function tenantKey(tenant: Tenant | string, ...parts: Array<string | number>): string;
|
|
declare function createTenantDirectory(options?: {
|
|
audit?: (event: TenantAuditEvent) => void | Promise<void>;
|
|
now?: () => number;
|
|
}): {
|
|
addMembership(membership: TenantMembership, actorId?: string): Promise<void>;
|
|
membership(tenantId: string, userId: string): TenantMembership | null;
|
|
switchWorkspace(tenantId: string, userId: string, workspaceId: string): Promise<{
|
|
tenantId: string;
|
|
workspaceId: string;
|
|
}>;
|
|
setQuota(tenantId: string, resource: string, limit: number): void;
|
|
enforceQuota(tenantId: string, resource: string, usage: number, requested?: number): {
|
|
usage: number;
|
|
requested: number;
|
|
limit: number | undefined;
|
|
};
|
|
};
|
|
declare function memoryTenantDirectoryStore(): TenantDirectoryStore;
|
|
declare function createPersistentTenantDirectory(store: TenantDirectoryStore, options?: {
|
|
audit?: (event: TenantAuditEvent) => void | Promise<void>;
|
|
now?: () => number;
|
|
}): {
|
|
addMembership(membership: TenantMembership, actorId?: string): Promise<void>;
|
|
membership: (tenantId: string, userId: string) => Promise<TenantMembership | null>;
|
|
memberships: (tenantId: string) => Promise<TenantMembership[]>;
|
|
switchWorkspace(tenantId: string, userId: string, workspaceId: string): Promise<{
|
|
tenantId: string;
|
|
workspaceId: string;
|
|
}>;
|
|
setQuota(tenantId: string, resource: string, limit: number, usage?: number): Promise<void>;
|
|
consumeQuota(tenantId: string, resource: string, requested: number): Promise<TenantQuota | null>;
|
|
};
|
|
interface TenantSqlClient {
|
|
query<T = Record<string, unknown>>(sql: string, parameters?: unknown[]): Promise<{
|
|
rows: T[];
|
|
}>;
|
|
}
|
|
declare function postgresTenantDirectoryStore(db: TenantSqlClient): TenantDirectoryStore;
|
|
declare const POSTGRES_TENANT_DIRECTORY_SCHEMA = "CREATE TABLE IF NOT EXISTS wrnexus_tenant_memberships (tenant_id text NOT NULL,user_id text NOT NULL,roles jsonb NOT NULL DEFAULT '[]',workspace_ids jsonb NOT NULL DEFAULT '[]',PRIMARY KEY (tenant_id,user_id)); CREATE TABLE IF NOT EXISTS wrnexus_tenant_quotas (tenant_id text NOT NULL,resource text NOT NULL,quota_limit bigint NOT NULL,usage bigint NOT NULL DEFAULT 0,PRIMARY KEY (tenant_id,resource));";
|
|
declare function migrateTenants<T extends Tenant>(tenants: T[], migrate: (tenant: T) => void | Promise<void>, options?: {
|
|
concurrency?: number;
|
|
continueOnError?: boolean;
|
|
}): Promise<{
|
|
migrated: string[];
|
|
failed: {
|
|
tenantId: string;
|
|
error: string;
|
|
}[];
|
|
}>;
|
|
|
|
interface SpanRecord {
|
|
name: string;
|
|
startTime: number;
|
|
endTime?: number;
|
|
durationMs?: number;
|
|
status?: "ok" | "error";
|
|
attributes: Record<string, string | number | boolean>;
|
|
error?: unknown;
|
|
}
|
|
interface Tracer {
|
|
startSpan(name: string, attributes?: SpanRecord["attributes"]): Span;
|
|
records(): readonly SpanRecord[];
|
|
}
|
|
interface Span {
|
|
setAttribute(name: string, value: string | number | boolean): void;
|
|
end(status?: "ok" | "error", error?: unknown): SpanRecord;
|
|
}
|
|
declare function createTracer(clock?: () => number): Tracer;
|
|
declare function withSpan<T>(tracer: Tracer, name: string, run: (span: Span) => T | Promise<T>, attributes?: SpanRecord["attributes"]): Promise<T>;
|
|
interface TracingMiddlewareOptions {
|
|
/** Include W3C Server-Timing response headers. Defaults to true. */
|
|
serverTiming?: boolean;
|
|
/** Fraction of requests to trace, from 0 to 1. Defaults to 1. */
|
|
sampleRate?: number;
|
|
/** Called after a traced response completes. */
|
|
onComplete?: (ctx: Context, records: readonly SpanRecord[]) => void | Promise<void>;
|
|
}
|
|
declare function tracingMiddleware(tracerFactory?: (ctx: Context) => Tracer, options?: TracingMiddlewareOptions): Middleware;
|
|
|
|
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;
|
|
}
|
|
interface SessionPolicy {
|
|
cookieName?: string;
|
|
idleTimeoutMs?: number;
|
|
absoluteTimeoutMs?: number;
|
|
sameSite?: NonNullable<CookieOptions["sameSite"]>;
|
|
secure?: boolean;
|
|
}
|
|
declare function setSessionPolicy(policy: SessionPolicy): void;
|
|
/** A stored session: its data plus an absolute expiry timestamp (ms). */
|
|
interface SessionEntry {
|
|
data: Record<string, unknown>;
|
|
expiresAt: number;
|
|
/** Creation time used for the absolute session lifetime. Optional for old backends. */
|
|
createdAt?: number;
|
|
lastAccessAt?: 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;
|
|
absoluteTtlMs?: number;
|
|
cookieName?: string;
|
|
sameSite?: NonNullable<CookieOptions["sameSite"]>;
|
|
secure?: boolean;
|
|
}): 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;
|
|
/** Active tenant/workspace resolved by tenant middleware. */
|
|
tenant?: Tenant;
|
|
/** Request tracer installed by observability middleware. */
|
|
tracer?: Tracer;
|
|
/**
|
|
* 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;
|
|
|
|
type ExecutionKind = "http" | "api" | "action" | "loader" | "middleware" | "realtime" | "queue" | "cron" | "webhook";
|
|
interface ResponseContext {
|
|
status: number;
|
|
headers: Headers;
|
|
setStatus(status: number): void;
|
|
}
|
|
interface ExecutionContext {
|
|
kind: ExecutionKind;
|
|
id: string;
|
|
request: Request;
|
|
response: ResponseContext;
|
|
user: unknown | null;
|
|
session: unknown | null;
|
|
tenant: Tenant | null;
|
|
locale: string;
|
|
timezone: string;
|
|
db?: unknown;
|
|
cache?: unknown;
|
|
logger?: unknown;
|
|
trace?: Tracer;
|
|
signal: AbortSignal;
|
|
deadline: Date | null;
|
|
metadata: Record<string, unknown>;
|
|
authorize(permission: string): void | Promise<void>;
|
|
}
|
|
interface ExecutionContextInput extends Partial<Omit<ExecutionContext, "kind" | "id" | "request" | "response" | "signal" | "deadline" | "metadata" | "authorize">> {
|
|
kind: ExecutionKind;
|
|
id?: string;
|
|
request?: Request;
|
|
response?: Partial<Pick<ResponseContext, "status">> & {
|
|
headers?: HeadersInit;
|
|
};
|
|
signal?: AbortSignal;
|
|
deadline?: Date | number | null;
|
|
timeoutMs?: number;
|
|
metadata?: Record<string, unknown>;
|
|
authorize?: (permission: string) => void | Promise<void>;
|
|
}
|
|
declare function createExecutionContext(input: ExecutionContextInput): ExecutionContext;
|
|
declare function executionContextFromHttp(context: Context, kind?: Extract<ExecutionKind, "http" | "api" | "action" | "loader" | "middleware" | "webhook">, input?: Omit<ExecutionContextInput, "kind" | "request" | "user" | "tenant" | "locale" | "trace">): ExecutionContext;
|
|
|
|
/**
|
|
* 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 plus origin/fetch
|
|
* metadata validation for unsafe requests.
|
|
*/
|
|
|
|
declare const CSRF_COOKIE = "wire-csrf";
|
|
declare const CSRF_HEADER = "x-csrf-token";
|
|
interface CsrfProtectionOptions {
|
|
/** Validate Origin when present. Defaults to true. */
|
|
verifyOrigin?: boolean;
|
|
/** Additional exact origins permitted for trusted cross-origin clients. */
|
|
trustedOrigins?: string[];
|
|
/** Reject Sec-Fetch-Site: cross-site on unsafe requests. Defaults to true. */
|
|
verifyFetchMetadata?: boolean;
|
|
}
|
|
/** Ensure the CSRF cookie exists (readable by JS) and return its token. */
|
|
declare function csrfToken(ctx: Context): string;
|
|
/** Verify an unsafe request's token, origin, and browser fetch metadata. */
|
|
declare function verifyCsrf(ctx: Context, options?: CsrfProtectionOptions): boolean;
|
|
/** Middleware that 403s unsafe requests with a missing/mismatched token. */
|
|
declare function csrfProtection(options?: CsrfProtectionOptions): 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;
|
|
/** stale-if-error window in seconds. */
|
|
staleIfError?: 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. The legacy `saveUpload` keeps the original sanitized
|
|
* filename for compatibility. New applications should use `saveUploadSecure`,
|
|
* which stores a random name and supports content inspection/scanning hooks.
|
|
*/
|
|
declare class UploadError extends Error {
|
|
readonly code: string;
|
|
constructor(message: string, code?: string);
|
|
}
|
|
interface UploadInspectionResult {
|
|
allowed: boolean;
|
|
detectedType?: string;
|
|
reason?: string;
|
|
}
|
|
type UploadInspector = (input: {
|
|
file: File;
|
|
bytes: Uint8Array;
|
|
filename: string;
|
|
}) => UploadInspectionResult | Promise<UploadInspectionResult>;
|
|
type UploadScanner = (input: {
|
|
file: File;
|
|
bytes: Uint8Array;
|
|
filename: string;
|
|
}) => boolean | {
|
|
clean: boolean;
|
|
reason?: string;
|
|
} | Promise<boolean | {
|
|
clean: boolean;
|
|
reason?: string;
|
|
}>;
|
|
interface SaveUploadOptions {
|
|
/** Destination directory. Keep this outside the public web root. */
|
|
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;
|
|
/** Content/magic-byte inspection hook. */
|
|
inspect?: UploadInspector;
|
|
/** Malware scanning hook. */
|
|
scan?: UploadScanner;
|
|
/** Called after validation but before persistence. */
|
|
beforeSave?: (input: {
|
|
file: File;
|
|
bytes: Uint8Array;
|
|
filename: string;
|
|
}) => void | Promise<void>;
|
|
}
|
|
interface SecureUploadOptions extends Omit<SaveUploadOptions, "filename"> {
|
|
/** Preserve the original sanitized name instead of a random server name. */
|
|
preserveOriginalName?: boolean;
|
|
/** Optional custom secure filename generator. */
|
|
filename?: (file: File) => string;
|
|
/** Preserve a conservative extension on random filenames. Defaults to true. */
|
|
preserveExtension?: boolean;
|
|
}
|
|
interface SavedUpload {
|
|
path: string;
|
|
filename: string;
|
|
size: number;
|
|
type: string;
|
|
detectedType?: string;
|
|
}
|
|
/** All `File` values in a parsed form, with their field names. */
|
|
declare function collectUploads(form: FormData, options?: {
|
|
maxFiles?: number;
|
|
maxTotalBytes?: number;
|
|
}): {
|
|
field: string;
|
|
file: File;
|
|
}[];
|
|
/** Validate and write one uploaded file using a compatibility filename policy. */
|
|
declare function saveUpload(file: File, options: SaveUploadOptions): Promise<SavedUpload>;
|
|
/** Store an upload under a random server-generated name by default. */
|
|
declare function saveUploadSecure(file: File, options: SecureUploadOptions): Promise<SavedUpload>;
|
|
/** Strip directory separators, traversal, and control chars from a filename. */
|
|
declare function sanitizeFilename(name: string): string;
|
|
declare function randomUploadFilename(originalName?: string, preserveExtension?: boolean): string;
|
|
declare function secureDownloadHeaders(filename: string, type?: string): Headers;
|
|
|
|
/**
|
|
* 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 RealtimeSecurityOptions {
|
|
/** Maximum inbound or outbound serialized message size. Defaults to 64 KiB. */
|
|
maxMessageBytes?: number;
|
|
/** Maximum messages accepted per connection per rolling second. Defaults to 30. */
|
|
maxMessagesPerSecond?: number;
|
|
/** Maximum live connections in one room. Defaults to 1,000. */
|
|
maxConnectionsPerRoom?: number;
|
|
/** Maximum connections for one authenticated user in a room. Defaults to 10. */
|
|
maxConnectionsPerUser?: number;
|
|
/** Reject anonymous connections before onConnect. */
|
|
requireUser?: boolean;
|
|
/** Maximum nested JSON depth. Defaults to 32. */
|
|
maxJsonDepth?: number;
|
|
/** Optional message schema/authorization predicate. */
|
|
validateMessage?(message: unknown, client: RoomClient): boolean | Promise<boolean>;
|
|
/** Called when a connection is rejected or closed for a policy violation. */
|
|
onViolation?(reason: string, client?: RoomClient): void;
|
|
}
|
|
interface RoomHandlers<TData = Record<string, unknown>, TMessage = any> {
|
|
/** Per-room abuse and payload controls. */
|
|
security?: RealtimeSecurityOptions;
|
|
/**
|
|
* 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: TMessage): void | Promise<void>;
|
|
/** A client disconnected. */
|
|
onLeave?(client: RoomClient<TData>): void | Promise<void>;
|
|
}
|
|
interface RoomDefinition<TData = Record<string, unknown>, TMessage = any> {
|
|
readonly __wrnexusRoom: true;
|
|
readonly handlers: RoomHandlers<TData, TMessage>;
|
|
}
|
|
/** Define a realtime room. Export the result as the `default` of a realtime file. */
|
|
declare function defineRoom<TData = Record<string, unknown>, TMessage = any>(handlers: RoomHandlers<TData, TMessage>): RoomDefinition<TData, TMessage>;
|
|
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 RealtimeRegistryOptions extends RealtimeSecurityOptions {
|
|
now?: () => number;
|
|
}
|
|
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(options?: RealtimeRegistryOptions): 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 RequestLimitsConfig {
|
|
maxUrlLength?: number;
|
|
maxHeaderCount?: number;
|
|
maxHeaderBytes?: number;
|
|
maxQueryParameters?: number;
|
|
maxBodyBytes?: number;
|
|
timeoutMs?: number;
|
|
maxConcurrent?: number;
|
|
trustedHosts?: string[];
|
|
fetchMetadata?: boolean;
|
|
}
|
|
interface SecurityConfig {
|
|
/** Set false to skip all framework security headers except explicitly enabled CORS. */
|
|
headers?: boolean;
|
|
/** Built-in request size, timeout, concurrency, host, and Fetch Metadata limits. */
|
|
requestLimits?: RequestLimitsConfig;
|
|
/**
|
|
* 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 "same-origin". */
|
|
crossOriginResourcePolicy?: false | "same-origin" | "same-site" | "cross-origin";
|
|
/** Isolate the origin in its own agent cluster. Defaults to true. */
|
|
originAgentCluster?: boolean;
|
|
/** Disable speculative DNS prefetching. Defaults to true. */
|
|
disableDnsPrefetch?: boolean;
|
|
/** 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;
|
|
|
|
interface SchemaLike<T> {
|
|
parse(input: unknown): T;
|
|
}
|
|
interface OutputSchemaLike<T> {
|
|
readonly __output: T;
|
|
parse(input: unknown): unknown;
|
|
}
|
|
type InferEndpointSchema<TSchema> = TSchema extends OutputSchemaLike<infer TValue> ? TValue : never;
|
|
interface EndpointErrorBody {
|
|
code: string;
|
|
message: string;
|
|
details?: unknown;
|
|
}
|
|
declare class EndpointError extends Error {
|
|
readonly status: number;
|
|
readonly code: string;
|
|
readonly details?: unknown | undefined;
|
|
constructor(status: number, code: string, message: string, details?: unknown | undefined);
|
|
}
|
|
interface EndpointDefinition<I, O> {
|
|
input?: SchemaLike<I> | OutputSchemaLike<I>;
|
|
output?: SchemaLike<O> | OutputSchemaLike<O>;
|
|
auth?: "optional" | "required";
|
|
description?: string;
|
|
tags?: string[];
|
|
handler(input: I, ctx: Context): O | Promise<O>;
|
|
}
|
|
interface DefinedEndpoint<I, O> {
|
|
readonly definition: EndpointDefinition<I, O>;
|
|
(ctx: Context, input?: unknown): Promise<Response>;
|
|
}
|
|
/** Define a validated, typed endpoint that can also drive SDK/OpenAPI generation. */
|
|
declare function defineEndpoint<InputSchema extends OutputSchemaLike<unknown>, OutputSchema extends OutputSchemaLike<unknown>>(definition: Omit<EndpointDefinition<InferEndpointSchema<InputSchema>, InferEndpointSchema<OutputSchema>>, "input" | "output"> & {
|
|
input: InputSchema;
|
|
output: OutputSchema;
|
|
}): DefinedEndpoint<InferEndpointSchema<InputSchema>, InferEndpointSchema<OutputSchema>>;
|
|
declare function defineEndpoint<I = unknown, O = unknown>(definition: EndpointDefinition<I, O>): DefinedEndpoint<I, O>;
|
|
interface RpcClientOptions {
|
|
baseUrl?: string;
|
|
fetch?: typeof globalThis.fetch;
|
|
headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
|
}
|
|
/** Create a tiny typed RPC caller for endpoints exposed by a WRNexusJS app. */
|
|
declare function createRpcClient(options?: RpcClientOptions): <I, O>(path: string, input: I) => Promise<O>;
|
|
|
|
interface CachePolicy {
|
|
ttlMs?: number;
|
|
staleWhileRevalidateMs?: number;
|
|
tags?: string[] | ((ctx: Context) => string[]);
|
|
}
|
|
interface LoaderDefinition<T> {
|
|
cache?: CachePolicy;
|
|
load(ctx: Context): T | Promise<T>;
|
|
}
|
|
interface ActionDefinition<I, O> {
|
|
csrf?: boolean;
|
|
run(input: I, ctx: Context): O | Promise<O>;
|
|
invalidate?: string[] | ((output: O, ctx: Context) => string[]);
|
|
}
|
|
interface DefinedLoader<T> {
|
|
readonly definition: LoaderDefinition<T>;
|
|
(ctx: Context): Promise<T>;
|
|
}
|
|
interface DefinedAction<I, O> {
|
|
readonly definition: ActionDefinition<I, O>;
|
|
(input: I, ctx: Context): Promise<O>;
|
|
}
|
|
declare function defineLoader<T>(definition: LoaderDefinition<T>): DefinedLoader<T>;
|
|
declare function defineAction<I, O>(definition: ActionDefinition<I, O>): DefinedAction<I, O>;
|
|
/** Request-local fetch deduplication keyed by a stable string. */
|
|
declare function dedupe<T>(ctx: Context, key: string, load: () => T | Promise<T>): Promise<T>;
|
|
|
|
type FeatureValue = boolean | string | number;
|
|
type FeatureRule = FeatureValue | ((ctx: Context) => FeatureValue | Promise<FeatureValue>);
|
|
interface FeatureFlags {
|
|
get(name: string, ctx: Context): Promise<FeatureValue | undefined>;
|
|
enabled(name: string, ctx: Context): Promise<boolean>;
|
|
}
|
|
declare function defineFeatureFlags(rules: Record<string, FeatureRule>): FeatureFlags;
|
|
|
|
interface PerformanceBudgets {
|
|
routeJsBytes?: number;
|
|
routeCssBytes?: number;
|
|
htmlBytes?: number;
|
|
imageBytes?: number;
|
|
hydrationMs?: number;
|
|
serverRenderMs?: number;
|
|
/** Largest Contentful Paint in milliseconds. Recommended <= 2500. */
|
|
lcpMs?: number;
|
|
/** Interaction to Next Paint in milliseconds. Recommended <= 200. */
|
|
inpMs?: number;
|
|
/** Cumulative Layout Shift score. Recommended <= 0.1. */
|
|
cls?: number;
|
|
/** Time to First Byte in milliseconds. */
|
|
ttfbMs?: number;
|
|
/** Longest main-thread task in milliseconds. Recommended <= 50. */
|
|
longTaskMs?: number;
|
|
/** Number of client hydration boundaries on the route. */
|
|
hydratedComponents?: number;
|
|
/** Total request count for the initial navigation. */
|
|
requests?: number;
|
|
}
|
|
interface PerformanceMeasurement {
|
|
routeJsBytes?: number;
|
|
routeCssBytes?: number;
|
|
htmlBytes?: number;
|
|
imageBytes?: number;
|
|
hydrationMs?: number;
|
|
serverRenderMs?: number;
|
|
lcpMs?: number;
|
|
inpMs?: number;
|
|
cls?: number;
|
|
ttfbMs?: number;
|
|
longTaskMs?: number;
|
|
hydratedComponents?: number;
|
|
requests?: number;
|
|
}
|
|
interface BudgetViolation {
|
|
metric: keyof PerformanceBudgets;
|
|
budget: number;
|
|
actual: number;
|
|
overBy: number;
|
|
}
|
|
declare const recommendedWebBudgets: Readonly<PerformanceBudgets>;
|
|
declare function checkPerformanceBudgets(budgets: PerformanceBudgets, measurement: PerformanceMeasurement): BudgetViolation[];
|
|
|
|
type Duration = number | `${number}${"ms" | "s" | "m" | "h"}`;
|
|
type BackoffStrategy = "fixed" | "exponential" | ((attempt: number) => Duration);
|
|
interface CircuitBreakerOptions {
|
|
failures: number;
|
|
resetAfter: Duration;
|
|
successesToClose?: number;
|
|
}
|
|
interface CircuitBreakerSnapshot {
|
|
state: "closed" | "open" | "half-open";
|
|
failures: number;
|
|
successes: number;
|
|
retryAfterMs: number;
|
|
}
|
|
declare class ResilienceError extends Error {
|
|
readonly code: "WRN-RESILIENCE-TIMEOUT" | "WRN-RESILIENCE-ABORTED" | "WRN-RESILIENCE-CIRCUIT-OPEN" | "WRN-RESILIENCE-BULKHEAD-FULL";
|
|
constructor(code: "WRN-RESILIENCE-TIMEOUT" | "WRN-RESILIENCE-ABORTED" | "WRN-RESILIENCE-CIRCUIT-OPEN" | "WRN-RESILIENCE-BULKHEAD-FULL", message: string, options?: ErrorOptions);
|
|
}
|
|
declare function durationMs(value: Duration): number;
|
|
declare class CircuitBreaker {
|
|
private readonly options;
|
|
private failures;
|
|
private successes;
|
|
private openedAt;
|
|
private probing;
|
|
constructor(options: CircuitBreakerOptions);
|
|
snapshot(now?: number): CircuitBreakerSnapshot;
|
|
execute<T>(operation: () => Promise<T>): Promise<T>;
|
|
}
|
|
interface BulkheadOptions {
|
|
concurrency: number;
|
|
queue?: number;
|
|
}
|
|
declare class Bulkhead {
|
|
private readonly options;
|
|
private active;
|
|
private readonly waiting;
|
|
constructor(options: BulkheadOptions);
|
|
get snapshot(): Readonly<{
|
|
active: number;
|
|
queued: number;
|
|
capacity: number;
|
|
}>;
|
|
execute<T>(operation: () => Promise<T>): Promise<T>;
|
|
}
|
|
interface ResilientCallOptions<T> {
|
|
run: (signal: AbortSignal, attempt: number) => Promise<T>;
|
|
timeout?: Duration;
|
|
retries?: number;
|
|
retryDelay?: Duration;
|
|
backoff?: BackoffStrategy;
|
|
circuitBreaker?: CircuitBreaker | CircuitBreakerOptions;
|
|
bulkhead?: Bulkhead | BulkheadOptions;
|
|
signal?: AbortSignal;
|
|
retryWhen?: (error: unknown, attempt: number) => boolean | Promise<boolean>;
|
|
fallback?: (error: unknown, signal: AbortSignal) => T | Promise<T>;
|
|
onRetry?: (error: unknown, attempt: number, delayMs: number) => void;
|
|
}
|
|
declare function resilientCall<T>(options: ResilientCallOptions<T>): Promise<T>;
|
|
|
|
interface ProblemDetails {
|
|
type: string;
|
|
title: string;
|
|
status: number;
|
|
detail?: string;
|
|
instance?: string;
|
|
code?: string;
|
|
[key: string]: unknown;
|
|
}
|
|
interface ProblemDetailsInput {
|
|
type?: string;
|
|
title: string;
|
|
status: number;
|
|
detail?: string;
|
|
instance?: string;
|
|
code?: string;
|
|
[key: string]: unknown;
|
|
}
|
|
declare function problem(details: ProblemDetailsInput, headers?: HeadersInit): Response;
|
|
type ServiceToken<T> = string | symbol | {
|
|
readonly key: symbol;
|
|
readonly __type?: T;
|
|
};
|
|
declare function serviceToken<T>(description: string): ServiceToken<T>;
|
|
declare class ServiceContainer {
|
|
#private;
|
|
private readonly parent?;
|
|
constructor(parent?: ServiceContainer | undefined);
|
|
set<T>(token: ServiceToken<T>, value: T): this;
|
|
has<T>(token: ServiceToken<T>): boolean;
|
|
get<T>(token: ServiceToken<T>): T;
|
|
tryGet<T>(token: ServiceToken<T>): T | undefined;
|
|
scope(): ServiceContainer;
|
|
}
|
|
type LifecyclePhase = "starting" | "started" | "stopping" | "stopped";
|
|
type LifecycleHandler = (signal: AbortSignal) => void | Promise<void>;
|
|
declare class ApplicationLifecycle {
|
|
#private;
|
|
on(phase: LifecyclePhase, handler: LifecycleHandler): () => void;
|
|
run(phase: LifecyclePhase): Promise<void>;
|
|
get signal(): AbortSignal;
|
|
}
|
|
interface HealthCheckResult {
|
|
status: "up" | "down" | "degraded";
|
|
message?: string;
|
|
details?: unknown;
|
|
durationMs?: number;
|
|
}
|
|
type HealthCheck = () => HealthCheckResult | Promise<HealthCheckResult>;
|
|
declare class HealthRegistry {
|
|
#private;
|
|
register(name: string, check: HealthCheck): () => void;
|
|
check(): Promise<{
|
|
status: "up" | "down" | "degraded";
|
|
checks: Record<string, HealthCheckResult>;
|
|
}>;
|
|
}
|
|
declare function requestId(headers: Headers, preferred?: string): string;
|
|
interface IdempotencyRecord<T = unknown> {
|
|
key: string;
|
|
value: T;
|
|
expiresAt: number;
|
|
}
|
|
interface IdempotencyStore<T = unknown> {
|
|
get(key: string): Promise<IdempotencyRecord<T> | null>;
|
|
set(record: IdempotencyRecord<T>): Promise<void>;
|
|
delete(key: string): Promise<void>;
|
|
}
|
|
declare function memoryIdempotencyStore<T = unknown>(now?: () => number): IdempotencyStore<T>;
|
|
declare function withIdempotency<T>(store: IdempotencyStore<T>, key: string, execute: () => Promise<T>, ttlMs?: number): Promise<{
|
|
value: T;
|
|
replayed: boolean;
|
|
}>;
|
|
|
|
export { type ActionDefinition, ApplicationLifecycle, type AsyncSessionBackend, type BackoffStrategy, type Bucket, type BudgetViolation, Bulkhead, type BulkheadOptions, CSRF_COOKIE, CSRF_HEADER, type CacheControlOptions, type CachePolicy, CircuitBreaker, type CircuitBreakerOptions, type CircuitBreakerSnapshot, type ContentSecurityPolicyConfig, type Context, type CookieOptions, type CookieStore, type CorsConfig, type CorsOrigin, type CspDirectiveValue, type CsrfProtectionOptions, type DefinedAction, type DefinedEndpoint, type DefinedLoader, type Duration, type EndpointDefinition, EndpointError, type EndpointErrorBody, type ExecutionContext, type ExecutionContextInput, type ExecutionKind, type FeatureFlags, type FeatureRule, type FeatureValue, type HealthCheck, type HealthCheckResult, HealthRegistry, type HstsConfig, type IdempotencyRecord, type IdempotencyStore, type InferEndpointSchema, type LifecycleHandler, type LifecyclePhase, type LoaderDefinition, type LocalStorageSnapshot, type Middleware, type Mode, type Next, type OutputSchemaLike, POSTGRES_TENANT_DIRECTORY_SCHEMA, type PageComponent, type PageMeta, type PerformanceBudgets, type PerformanceMeasurement, type PermissionsPolicyConfig, type ProblemDetails, type ProblemDetailsInput, type RateLimitOptions, type RateLimitStore, type RawSocket, type RealtimeBridge, type RealtimeBus, type RealtimeConnectMeta, type RealtimeEnvelope, type RealtimeHandler, type RealtimeRegistry, type RealtimeRegistryOptions, type RealtimeSecurityOptions, type RealtimeSocket, type RequestLimitsConfig, type RequestLoggerOptions, type RequestRecord, type RequireAuthOptions, ResilienceError, type ResilientCallOptions, type ResponseContext, type Room, type RoomAuthInfo, type RoomClient, type RoomDefinition, type RoomHandlers, type RpcClientOptions, SESSION_USER_KEY, type SaveUploadOptions, type SavedUpload, type SchemaLike, type SecureUploadOptions, type SecurityConfig, type SeoConfig, type ServerSentEvent, ServiceContainer, type ServiceToken, type SessionBackend, type SessionEntry, type SessionPolicy, type SessionStore, type Span, type SpanRecord, type StreamResponseInit, type TFunction, TTLCache, type Target, type Tenant, type TenantAuditEvent, type TenantDirectoryStore, type TenantMembership, type TenantMiddlewareOptions, type TenantQuota, type TenantResolver, type TenantResource, type TenantSqlClient, type Tracer, type TrustedTypesConfig, UploadError, type UploadInspectionResult, type UploadInspector, type UploadScanner, assertTenantAccess, bridgeRealtime, cacheControl, checkPerformanceBudgets, collectUploads, composeTenantResolvers, createContext, createCorsPreflightResponse, createExecutionContext, createPersistentTenantDirectory, createRealtimeRegistry, createRpcClient, createTenantDirectory, createTracer, csrfProtection, csrfToken, dedupe, defaultKey, defineAction, defineEndpoint, defineFeatureFlags, defineLoader, defineRoom, durationMs, escapeHtml, etag, executionContextFromHttp, getUser, hashPassword, isRoomDefinition, isSafeIslandName, isSafeRequestPath, isWebSocketOriginAllowed, loadSession, logIn, logOut, memoryIdempotencyStore, memoryTenantDirectoryStore, migrateTenants, notModified, peerKey, postgresTenantDirectoryStore, problem, proxyKey, randomUploadFilename, rateLimit, recommendedWebBudgets, renderDevError, renderError, renderNotFound, renderProdError, renderStatusPage, requestId, requestLogger, requireAuth, requireTenant, resilientCall, resolveRequestUrl, sanitizeFilename, saveUpload, saveUploadSecure, secureDownloadHeaders, serviceToken, sessionAuth, setSessionBackend, setSessionPolicy, sse, streamResponse, tenantFromDomain, tenantFromHeader, tenantFromPath, tenantFromSession, tenantFromSubdomain, tenantKey, tenantMiddleware, tenantScope, tracingMiddleware, verifyCsrf, verifyPassword, withCacheControl, withContextHeaders, withIdempotency, withSecurityHeaders, withSpan };
|
|
</code></pre></section><section id="examples" class="examples"><h2>Examples</h2><p>Copy-ready examples from the installed package documentation.</p><div class="example-grid"><article class="example-card"><h3>A minimal middleware chain</h3><pre data-language="ts"><code>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" }),
|
|
];</code></pre></article><article class="example-card"><h3>Password auth</h3><pre data-language="ts"><code>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</code></pre></article><article class="example-card"><h3>HTTP caching with ETags</h3><pre data-language="ts"><code>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 });</code></pre></article><article class="example-card"><h3>Streaming SSE</h3><pre data-language="ts"><code>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());</code></pre></article><article class="example-card"><h3>A realtime room</h3><pre data-language="ts"><code>// 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 });
|
|
},
|
|
});</code></pre></article><article class="example-card"><h3>A realtime room</h3><pre data-language="ts"><code>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)));</code></pre></article></div></section></article>
|
|
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-4" href="#context-middleware-wrnexus-core">Context & middleware — @wrnexus/core</a><a class="toc-level-4" href="#authentication-wrnexus-core">Authentication — @wrnexus/core</a><a class="toc-level-4" href="#csrf-wrnexus-core">CSRF — @wrnexus/core</a><a class="toc-level-4" href="#rate-limiting-wrnexus-core">Rate limiting — @wrnexus/core</a><a class="toc-level-4" href="#request-logging-wrnexus-core">Request logging — @wrnexus/core</a><a class="toc-level-4" href="#resilience-wrnexus-core">Resilience — @wrnexus/core</a><a class="toc-level-4" href="#caching-wrnexus-core">Caching — @wrnexus/core</a><a class="toc-level-4" href="#file-uploads-wrnexus-core">File uploads — @wrnexus/core</a><a class="toc-level-4" href="#streaming-sse-wrnexus-core">Streaming & SSE — @wrnexus/core</a><a class="toc-level-4" href="#realtime-rooms-wrnexus-core">Realtime rooms — @wrnexus/core</a><a class="toc-level-4" href="#error-pages-wrnexus-core">Error pages — @wrnexus/core</a><a class="toc-level-4" href="#security-headers-cors-wrnexus-core">Security headers & CORS — @wrnexus/core</a><a class="toc-level-4" href="#storage-cookies-sessions-localstorage-wrnexus-core">Storage: cookies, sessions, localStorage — @wrnexus/core</a><a class="toc-level-4" href="#low-level-security-helpers-wrnexus-core">Low-level security helpers — @wrnexus/core</a><a class="toc-level-4" href="#jsx-runtime-wrnexus-core-wrnexus-core-jsx-runtime-wrnexus-core-jsx-dev-runtime">JSX runtime — @wrnexus/core, @wrnexus/core/jsx-runtime, @wrnexus/core/jsx-dev-runtime</a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-4" href="#a-minimal-middleware-chain">A minimal middleware chain</a><a class="toc-level-4" href="#password-auth">Password auth</a><a class="toc-level-4" href="#http-caching-with-etags">HTTP caching with ETags</a><a class="toc-level-4" href="#streaming-sse">Streaming SSE</a><a class="toc-level-4" href="#a-realtime-room">A realtime room</a><a class="toc-level-4" href="#jsx-rendering">JSX rendering</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
|
</main>
|
|
<footer><div class="footer-brand"><span class="footer-mark" aria-hidden="true">W</span><p><strong>WRNexusJS 0.8.0</strong><span>Complete API documentation generated from installed package declarations.</span></p></div><nav aria-label="Footer"><a href="/packages">All packages</a><a href="/getting-started">Get started</a><a href="/security">Security</a><a href="/support">Support</a><a href="/llms.txt">AI guide</a></nav><p class="footer-meta">Private Developer Preview · Bun-native</p></footer>
|
|
</div>
|
|
}
|
|
}
|