page wrnexusvalidation {
  seo {
    title = "@wrnexus/validation"
    description = "Typed schemas, coercion, validation, and browser descriptors."
  }

  view {
    <div class="docs-shell">
      <SkipLink label="Skip to content" href="#main" class="docs-skip-link" />
      <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="/components">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.3.2</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="/components">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/validation</span></nav><section class="doc-intro"><span class="eyebrow">Security · Package reference</span><h1>@wrnexus/validation</h1><p>Typed schemas, coercion, validation, and browser descriptors.</p><div class="doc-meta"><span>v0.3.2</span><span>Private registry</span><span>Security</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/validation@0.3.2</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>One fluent schema, validated on the server (API bodies, env vars) and mirrored to an eval-free browser validator for forms.</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>Define a schema once with the fluent <code>v</code> builder, then reuse it in three places: <code>.parse()</code> runs server-side and returns coerced values plus per-field errors; <code>.describe()</code> emits a plain-JSON <code>SchemaDescriptor</code> that the browser runtime interprets (no <code>eval</code>, no bundled validator); and helpers like <code>parseBody</code> and <code>parseEnv</code> wire schemas straight into API routes and startup config. The server rule logic (<code>applyRule</code>/<code>checkField</code>) and the client runtime (<code>VALIDATE_RUNTIME</code>) mirror each other exactly, so a form validates identically in both places. Schemas are conventionally kept in <code>app/schemas/</code>.</p>
<pre data-language="bash"><code>bun add @wrnexus/validation</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="the-v-builder">The <code>v</code> builder</h4>
<pre data-language="ts"><code>import &#123; v &#125; from &quot;@wrnexus/validation&quot;;</code></pre>
<div class="table-wrap"><table>
<thead><tr><th>Factory</th><th>Returns</th><th>Field methods</th></tr></thead>
<tbody><tr><td><code>v.string()</code></td><td><code>StringSchema</code></td><td><code>email()</code>, <code>url()</code>, <code>uuid()</code>, <code>date()</code>, <code>length(n)</code>, <code>oneOf(string[])</code>, <code>pattern(re)</code>, <code>trim()</code>, <code>min(n)</code>, <code>max(n)</code></td></tr><tr><td><code>v.number()</code></td><td><code>NumberSchema</code></td><td><code>integer()</code>, <code>positive()</code>, <code>oneOf(number[])</code>, <code>min(n)</code>, <code>max(n)</code></td></tr><tr><td><code>v.boolean()</code></td><td><code>BooleanSchema</code></td><td>(base methods only)</td></tr><tr><td><code>v.object(fields)</code></td><td><code>ObjectSchema</code></td><td><code>parse(input)</code>, <code>describe()</code></td></tr></tbody></table></div>
<p>Every field schema is chainable and shares these base methods:</p>
<ul>
<li><code>min(n, message?)</code> / <code>max(n, message?)</code> — for strings, bounds the length; for numbers, bounds the value.</li>
<li><code>required(message?)</code> — require a non-empty value and optionally replace the default <code>&quot;Required&quot;</code> message on both server and browser validation.</li>
<li><code>optional()</code> — an empty/missing value passes instead of erroring <code>&quot;Required&quot;</code>.</li>
<li><code>label(text)</code> — human label carried into the descriptor.</li>
<li><code>default(value)</code> — value substituted when the field is absent (implies <code>optional</code>).</li>
<li><code>refine(fn, message?)</code> — <strong>server-only</strong> predicate. <code>fn</code> returns <code>true</code> (ok), <code>false</code> (use <code>message</code>), or a <code>string</code> (that error). Not serialized to the client.</li>
</ul>
<p>Each string rule accepts an optional trailing <code>message</code> to override the default error text.</p>
<h4 id="objectschema"><code>ObjectSchema</code></h4>
<pre data-language="ts"><code>schema.parse(input: unknown): ParseResult
schema.describe(): SchemaDescriptor</code></pre>
<p><code>parse</code> coerces each field (strings stay strings, <code>v.number()</code> runs <code>Number()</code>, <code>v.boolean()</code> treats <code>true</code> / <code>&quot;true&quot;</code> / <code>&quot;on&quot;</code> as true), applies its rules and refinements, fills in <code>default()</code> values, and returns:</p>
<pre data-language="ts"><code>interface ParseResult&lt;T = Record&lt;string, unknown&gt;&gt; &#123;
  ok: boolean; // true when errors is empty
  value: T; // coerced values (present pass or fail)
  errors: Record&lt;string, string&gt;; // field name → first failing message
&#125;</code></pre>
<p><code>describe()</code> returns the JSON bridge for the client:</p>
<pre data-language="ts"><code>interface SchemaDescriptor &#123;
  type: &quot;object&quot;;
  fields: Record&lt;string, FieldDescriptor&gt;;
&#125;
interface FieldDescriptor &#123;
  type: &quot;string&quot; | &quot;number&quot; | &quot;boolean&quot;;
  optional?: boolean;
  label?: string;
  trim?: boolean; // strings only
  rules: RuleDescriptor[];
&#125;</code></pre>
<h4 id="rules-and-coercion">Rules and coercion</h4>
<p><code>RuleDescriptor</code> is a discriminated union of the serializable rules — <code>min</code>, <code>max</code>, <code>length</code>, <code>email</code>, <code>url</code>, <code>uuid</code>, <code>date</code>, <code>oneOf</code>, <code>pattern</code>, <code>integer</code>. Two exported functions apply them and are shared by the server (the client runtime reimplements the same logic):</p>
<ul>
<li><code>applyRule(type, rule, value): string | null</code> — validate one already-coerced value against one rule.</li>
<li><code>checkField(desc, raw): &#123; value, error &#125;</code> — coerce and validate one field. Empty input (<code>undefined</code>/<code>null</code>/<code>&quot;&quot;</code>) is <code>&quot;Required&quot;</code> unless <code>optional</code>. Strings with <code>trim</code> are trimmed first. Numbers that fail <code>Number()</code> yield <code>&quot;Must be a number&quot;</code>.</li>
</ul>
<p>Notes on specific rules: <code>email</code>/<code>url</code>/<code>uuid</code> test built-in regexes; <code>date</code> uses <code>Date.parse</code>; <code>pattern</code> reconstructs a <code>RegExp</code> from its <code>source</code>/<code>flags</code> and passes silently if the pattern is invalid; <code>integer</code> requires <code>Number.isInteger</code>; <code>positive()</code> is implemented as <code>min(Number.MIN_VALUE)</code>.</p>
<h4 id="api-helpers">API helpers</h4>
<pre data-language="ts"><code>invalid(errors: Record&lt;string, string&gt;): Response   // ready 400 &#123; ok:false, errors &#125;

parseBody&lt;T&gt;(schema, req):
  Promise&lt;&#123; ok: true; value: T &#125; | &#123; ok: false; response: Response &#125;&gt;</code></pre>
<p><code>parseBody</code> reads the request body from JSON, <code>application/x-www-form-urlencoded</code>, or <code>multipart/form-data</code>, validates it, and on failure hands back a ready 400 <code>Response</code>.</p>
<h4 id="environment-config">Environment config</h4>
<pre data-language="ts"><code>parseEnv&lt;T&gt;(schema: ObjectSchema, source?): T</code></pre>
<p>Validates env vars (from <code>Bun.env</code>, falling back to <code>process.env</code>) against a schema and coerces them (<code>PORT</code> → number, <code>DEBUG</code> → boolean). On any problem it throws <strong>one</strong> error listing every offending variable, so misconfiguration fails fast at startup.</p>
<h4 id="client-runtime-from-runtime-ts">Client runtime (from <code>runtime.ts</code>)</h4>
<pre data-language="ts"><code>renderSchemasScript(descriptors: Record&lt;string, SchemaDescriptor&gt;): string
VALIDATE_RUNTIME: string</code></pre>
<ul>
<li><code>renderSchemasScript</code> produces <code>window.__wireSchemas = &#123; name: descriptor, … &#125;;</code> to inline in the page.</li>
<li><code>VALIDATE_RUNTIME</code> is a self-contained, eval-free IIFE string. Injected as a <code>&lt;script&gt;</code>, it binds every <code>form[data-schema]</code> and validates on submit and blur, writing messages into <code>[data-error=&quot;&lt;field&gt;&quot;]</code> elements and toggling <code>aria-invalid</code> / <code>.wire-invalid</code>. On a valid submit it <code>fetch</code>es the form <code>action</code> as JSON (attaching the <code>wire-csrf</code> cookie as an <code>x-csrf-token</code> header), then follows <code>data-redirect</code> / a <code>redirect</code> in the response, surfaces server-side field errors, and fires <code>wire:success</code> / <code>wire:error</code> events. It exposes <code>window.__wireValidate.init(root)</code> and self-initializes on <code>DOMContentLoaded</code>.</li>
</ul>
<h3 id="usage">Usage</h3>
<p>Define a schema and validate an API body:</p>
<pre data-language="ts"><code>import &#123; v, parseBody &#125; from &quot;@wrnexus/validation&quot;;

export const signupSchema = v.object(&#123;
  email: v.string().required(&quot;Enter your email address&quot;).trim().email(),
  password: v.string().required(&quot;Enter your password&quot;).min(8).max(200),
  age: v.number().integer().min(13).max(120).optional(),
  role: v.string().oneOf([&quot;user&quot;, &quot;admin&quot;]).default(&quot;user&quot;),
  agree: v.boolean(),
&#125;);

// inside a route handler
const result = await parseBody(signupSchema, req);
if (!result.ok) return result.response; // ready 400 with field errors
const &#123; email, password, role &#125; = result.value;</code></pre>
<p>Server-only refinement:</p>
<pre data-language="ts"><code>const schema = v.object(&#123;
  username: v
    .string()
    .min(3)
    .refine((name) =&gt; !RESERVED.has(String(name)), &quot;That name is taken&quot;),
&#125;);</code></pre>
<p>Validate environment at startup:</p>
<pre data-language="ts"><code>import &#123; v, parseEnv &#125; from &quot;@wrnexus/validation&quot;;

export const env = parseEnv(
  v.object(&#123;
    DATABASE_URL: v.string().min(1),
    PORT: v.number().integer().default(3000),
    DEBUG: v.boolean().optional(),
  &#125;),
);
// throws one readable error listing every bad variable if misconfigured</code></pre>
<p>Wire the same schema into the browser:</p>
<pre data-language="ts"><code>import &#123; renderSchemasScript, VALIDATE_RUNTIME &#125; from &quot;@wrnexus/validation&quot;;
import &#123; signupSchema &#125; from &quot;./app/schemas/signup.ts&quot;;

const head = `&lt;script&gt;$&#123;renderSchemasScript(&#123; signup: signupSchema.describe() &#125;)&#125;&lt;/script&gt;
&lt;script&gt;$&#123;VALIDATE_RUNTIME&#125;&lt;/script&gt;`;
// render a &lt;form data-schema=&quot;signup&quot;&gt; with [data-error=&quot;email&quot;] etc.</code></pre>
<h3 id="requirements-notes">Requirements / Notes</h3>
<ul>
<li><strong>Bun-only.</strong> <code>parseEnv</code> reads <code>Bun.env</code> (falling back to <code>process.env</code>); <code>parseBody</code> and <code>invalid</code> use the Web <code>Request</code>/<code>Response</code> APIs that back <code>Bun.serve</code>.</li>
<li>Refinements (<code>refine</code>) run only server-side and are never serialized — client and server agree on every other rule because both interpret the same <code>RuleDescriptor</code> list.</li>
<li>No runtime dependencies. Ships as TypeScript source (<code>src/index.ts</code>) executed directly by Bun.</li>
<li>Pairs with the WRNexusJS server (<code>@wrnexus/core</code>) for route handlers and the SSR layer that injects <code>renderSchemasScript</code> / <code>VALIDATE_RUNTIME</code>.</li>
</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>/**
 * Client-side validation. `renderSchemasScript` bakes the discovered schema
 * descriptors into `window.__wireSchemas`; `VALIDATE_RUNTIME` is a generic,
 * eval-free validator that reads them and validates every `form[data-schema]`
 * on submit and blur, writing messages into `[data-error=&quot;&lt;field&gt;&quot;]` elements.
 * The rule logic mirrors `checkField`/`applyRule` in index.ts.
 */

/** `window.__wireSchemas = &#123; name: descriptor, ... &#125;` for the client validator. */
declare function renderSchemasScript(descriptors: Record&lt;string, SchemaDescriptor&gt;): string;
declare const VALIDATE_RUNTIME: string;

/**
 * @wrnexus/validation — one schema, validated on the server (API) and the browser
 * (forms). A schema is a fluent builder; `.parse()` runs server-side and returns
 * coerced values + field errors, while `.describe()` emits a JSON descriptor the
 * eval-free client validator interprets. Define schemas once in `app/schemas/`.
 */
type RuleDescriptor = &#123;
    kind: &quot;min&quot;;
    n: number;
    message?: string;
&#125; | &#123;
    kind: &quot;max&quot;;
    n: number;
    message?: string;
&#125; | &#123;
    kind: &quot;length&quot;;
    n: number;
    message?: string;
&#125; | &#123;
    kind: &quot;email&quot;;
    message?: string;
&#125; | &#123;
    kind: &quot;url&quot;;
    message?: string;
&#125; | &#123;
    kind: &quot;uuid&quot;;
    message?: string;
&#125; | &#123;
    kind: &quot;date&quot;;
    message?: string;
&#125; | &#123;
    kind: &quot;oneOf&quot;;
    values: (string | number)[];
    message?: string;
&#125; | &#123;
    kind: &quot;pattern&quot;;
    source: string;
    flags?: string;
    message?: string;
&#125; | &#123;
    kind: &quot;integer&quot;;
    message?: string;
&#125;;
interface FieldDescriptor &#123;
    type: &quot;string&quot; | &quot;number&quot; | &quot;boolean&quot;;
    optional?: boolean;
    /** Message used when a required field is empty. Defaults to &quot;Required&quot;. */
    requiredMessage?: string;
    label?: string;
    /** Trim string input before validating. */
    trim?: boolean;
    rules: RuleDescriptor[];
&#125;
interface SchemaDescriptor &#123;
    type: &quot;object&quot;;
    fields: Record&lt;string, FieldDescriptor&gt;;
&#125;
interface ParseResult&lt;T = Record&lt;string, unknown&gt;&gt; &#123;
    ok: boolean;
    /** Coerced values (present whether or not validation passed). */
    value: T;
    /** Field name → message, only for fields that failed. */
    errors: Record&lt;string, string&gt;;
&#125;
/**
 * Apply one rule to an already-coerced value. Shared by the server; the client
 * runtime (runtime.ts) mirrors this exactly. Returns an error message or null.
 */
declare function applyRule(type: string, rule: RuleDescriptor, value: unknown): string | null;
/** Coerce + validate one field against its descriptor. */
declare function checkField(desc: FieldDescriptor, raw: unknown): &#123;
    value: unknown;
    error: string | null;
&#125;;
/** A server-only refinement (a predicate that can't be serialized to the client). */
type Refinement = &#123;
    fn: (value: unknown) =&gt; boolean | string;
    message?: string;
&#125;;
declare abstract class FieldSchema &#123;
    abstract readonly type: &quot;string&quot; | &quot;number&quot; | &quot;boolean&quot;;
    protected _optional: boolean;
    protected _requiredMessage?: string;
    protected _label?: string;
    protected _default?: unknown;
    protected rules: RuleDescriptor[];
    protected refinements: Refinement[];
    optional(): this;
    /** Require a non-empty value and optionally replace the default message. */
    required(message?: string): this;
    label(label: string): this;
    /** Value used when the field is absent (implies optional). */
    default(value: unknown): this;
    min(n: number, message?: string): this;
    max(n: number, message?: string): this;
    /**
     * Custom SERVER-side validation. `fn` returns true (ok), false (use `message`),
     * or a string (that error). Not mirrored to the client validator.
     */
    refine(fn: (value: unknown) =&gt; boolean | string, message?: string): this;
    getDefault(): unknown;
    runRefinements(value: unknown): string | null;
    describe(): FieldDescriptor;
&#125;
declare class StringSchema extends FieldSchema &#123;
    readonly type: &quot;string&quot;;
    private _trim;
    email(message?: string): this;
    url(message?: string): this;
    uuid(message?: string): this;
    date(message?: string): this;
    length(n: number, message?: string): this;
    oneOf(values: string[], message?: string): this;
    trim(): this;
    pattern(re: RegExp, message?: string): this;
    describe(): FieldDescriptor;
&#125;
declare class NumberSchema extends FieldSchema &#123;
    readonly type: &quot;number&quot;;
    integer(message?: string): this;
    positive(message?: string): this;
    oneOf(values: number[], message?: string): this;
&#125;
declare class BooleanSchema extends FieldSchema &#123;
    readonly type: &quot;boolean&quot;;
&#125;
declare class ObjectSchema &#123;
    private readonly fields;
    constructor(fields: Record&lt;string, FieldSchema&gt;);
    /** Validate an input object; returns coerced values + per-field errors. */
    parse(input: unknown): ParseResult;
    describe(): SchemaDescriptor;
&#125;
/** The fluent schema builder. */
declare const v: &#123;
    string: () =&gt; StringSchema;
    number: () =&gt; NumberSchema;
    boolean: () =&gt; BooleanSchema;
    object: (fields: Record&lt;string, FieldSchema&gt;) =&gt; ObjectSchema;
&#125;;
/**
 * Validate environment variables against a schema at startup. Values are read
 * from `Bun.env` / `process.env` by default and coerced by the schema (so
 * `PORT` becomes a number, `DEBUG` a boolean). On any problem it throws ONE
 * readable error listing every offending variable, so misconfiguration fails
 * fast with an actionable message instead of surfacing deep inside the app.
 *
 *   export const env = parseEnv(v.object(&#123;
 *     DATABASE_URL: v.string().min(1),
 *     PORT: v.number(),
 *   &#125;));
 */
declare function parseEnv&lt;T = Record&lt;string, unknown&gt;&gt;(schema: ObjectSchema, source?: Record&lt;string, string | undefined&gt;): T;
/** A 400 response carrying field errors, for API routes. */
declare function invalid(errors: Record&lt;string, string&gt;): Response;
/**
 * Parse a request's JSON body against a schema. On failure returns
 * `&#123; ok: false, response &#125;` (a ready 400); on success `&#123; ok: true, value &#125;`.
 */
declare function parseBody&lt;T = Record&lt;string, unknown&gt;&gt;(schema: ObjectSchema, req: Request): Promise&lt;&#123;
    ok: true;
    value: T;
&#125; | &#123;
    ok: false;
    response: Response;
&#125;&gt;;

export &#123; type FieldDescriptor, ObjectSchema, type ParseResult, type RuleDescriptor, type SchemaDescriptor, VALIDATE_RUNTIME, applyRule, checkField, invalid, parseBody, parseEnv, renderSchemasScript, v &#125;;
</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>Define a schema and validate an API body</h3><pre data-language="ts"><code>import &#123; v, parseBody &#125; from &quot;@wrnexus/validation&quot;;

export const signupSchema = v.object(&#123;
  email: v.string().required(&quot;Enter your email address&quot;).trim().email(),
  password: v.string().required(&quot;Enter your password&quot;).min(8).max(200),
  age: v.number().integer().min(13).max(120).optional(),
  role: v.string().oneOf([&quot;user&quot;, &quot;admin&quot;]).default(&quot;user&quot;),
  agree: v.boolean(),
&#125;);

// inside a route handler
const result = await parseBody(signupSchema, req);
if (!result.ok) return result.response; // ready 400 with field errors
const &#123; email, password, role &#125; = result.value;</code></pre></article><article class="example-card"><h3>Server-only refinement</h3><pre data-language="ts"><code>const schema = v.object(&#123;
  username: v
    .string()
    .min(3)
    .refine((name) =&gt; !RESERVED.has(String(name)), &quot;That name is taken&quot;),
&#125;);</code></pre></article><article class="example-card"><h3>Validate environment at startup</h3><pre data-language="ts"><code>import &#123; v, parseEnv &#125; from &quot;@wrnexus/validation&quot;;

export const env = parseEnv(
  v.object(&#123;
    DATABASE_URL: v.string().min(1),
    PORT: v.number().integer().default(3000),
    DEBUG: v.boolean().optional(),
  &#125;),
);
// throws one readable error listing every bad variable if misconfigured</code></pre></article><article class="example-card"><h3>Wire the same schema into the browser</h3><pre data-language="ts"><code>import &#123; renderSchemasScript, VALIDATE_RUNTIME &#125; from &quot;@wrnexus/validation&quot;;
import &#123; signupSchema &#125; from &quot;./app/schemas/signup.ts&quot;;

const head = `&lt;script&gt;$&#123;renderSchemasScript(&#123; signup: signupSchema.describe() &#125;)&#125;&lt;/script&gt;
&lt;script&gt;$&#123;VALIDATE_RUNTIME&#125;&lt;/script&gt;`;
// render a &lt;form data-schema=&quot;signup&quot;&gt; with [data-error=&quot;email&quot;] etc.</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="#the-v-builder">The v builder</a><a class="toc-level-4" href="#objectschema">ObjectSchema</a><a class="toc-level-4" href="#rules-and-coercion">Rules and coercion</a><a class="toc-level-4" href="#api-helpers">API helpers</a><a class="toc-level-4" href="#environment-config">Environment config</a><a class="toc-level-4" href="#client-runtime-from-runtime-ts">Client runtime (from runtime.ts)</a><a class="toc-level-3" href="#usage">Usage</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.3.2</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>
      <BackToTop />
    </div>
  }
}
