page wrnexusuploader {
  seo {
    title = "@wrnexus/uploader"
    description = "Validated local/S3 uploads and secure file serving."
  }

  view {
    <div class="docs-shell">
      <a class="skip-link" href="#main">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="/language">Language</a><a href="/architecture">Architecture</a></nav>
        <div class="topbar-actions"><a class="preview-pill" href="/access">Private preview · v0.2.31</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="/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="page package-page">
      <aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Data</span><h2>@wrnexus/uploader</h2><p>Validated local/S3 uploads and secure file serving.</p><span class="status status-beta">Private preview · 0.2.31</span><nav><a href="#access">Access</a><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
      <article id="main" class="documentation"><section class="doc-intro"><span class="eyebrow">Data · Preview</span><h1>@wrnexus/uploader</h1><p>Validated local/S3 uploads and secure file serving.</p><section id="access" class="access-callout"><h2>Private registry access required</h2><p>This package is not available from the public npm registry. After WorkRoot approves access and supplies private registry instructions, install the release-aligned package:</p><pre><code>bun add @wrnexus/uploader@0.2.31</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" class="prose"><p>Config-driven file uploads + serving for <a href="https://www.npmjs.com/org/wrnexus" rel="noreferrer">WRNexusJS</a>. Declare named <strong>storage stores</strong> (local disk or any S3-compatible backend) in <code>wrnexus.config.ts</code>, upload with one function call, drop a drag-and-drop widget on a page, and serve files back — public or private. Zero external dependencies (S3 is signed with a built-in AWS SigV4 implementation, like the rest of the framework).</p>
<h3 id="usage">Usage</h3>
<h4 id="configure-local-and-s3-stores">Configure local and S3 stores</h4>
<pre data-language="ts"><code>// wrnexus.config.ts
import type &#123; AppConfig &#125; from &quot;@wrnexus/styles&quot;;

const config: AppConfig = &#123;
  storage: &#123;
    default: &quot;public&quot;,
    stores: &#123;
      // Local disk, world-readable — served by the framework with a 1-year cache.
      public: &#123;
        driver: &quot;local&quot;,
        dir: &quot;uploads/public&quot;, // relative to the app root (dev) / cwd (prod)
        access: &quot;public&quot;,
        maxBytes: 10_000_000,
        accept: [&quot;image/*&quot;, &quot;.pdf&quot;], // MIME, &quot;type/*&quot; wildcards, or &quot;.ext&quot;
      &#125;,
      // Private S3 (works with AWS, Cloudflare R2, Backblaze B2, MinIO, DO Spaces).
      docs: &#123;
        driver: &quot;s3&quot;,
        access: &quot;private&quot;,
        bucket: &quot;my-bucket&quot;,
        region: &quot;auto&quot;,
        endpoint: &quot;https://&lt;acct&gt;.r2.cloudflarestorage.com&quot;,
        accessKeyId: process.env.S3_KEY!,
        secretAccessKey: process.env.S3_SECRET!,
      &#125;,
    &#125;,
  &#125;,
&#125;;
export default config;</code></pre>
<h4 id="upload-from-an-api-route-or-server-function">Upload from an API route or server function</h4>
<pre data-language="ts"><code>// app/api/upload.ts — one-liner
import &#123; handleUpload &#125; from &quot;@wrnexus/uploader&quot;;
export const POST = handleUpload(&#123; store: &quot;public&quot; &#125;);
// → &#123; ok: true, files: [&#123; key, url, name, type, size &#125;] &#125;</code></pre>
<pre data-language="ts"><code>// or drive it yourself, anywhere you have the request
import &#123; upload, getStore &#125; from &quot;@wrnexus/uploader&quot;;
const &#123; files &#125; = await upload(&quot;docs&quot;, ctx.req, &#123; prefix: &quot;invoices&quot; &#125;);
await getStore(&quot;docs&quot;).driver.delete(files[0].key);</code></pre>
<p>Uploads are validated (size + type), stored under a random, collision-proof, path-safe key (the client filename is never used as a path), and — for public stores — returned with a servable <code>url</code>.</p>
<h4 id="add-a-client-upload-widget">Add a client upload widget</h4>
<p>Drop the element anywhere; the runtime (drag-and-drop, per-file progress, success/failed states) is auto-injected on pages that contain <code>data-uploader</code>:</p>
<pre data-language="html"><code>&lt;div
  data-uploader=&quot;public&quot;
  data-endpoint=&quot;/api/upload&quot;
  data-accept=&quot;image/*&quot;
  data-max=&quot;10000000&quot;
  data-multiple
&gt;&lt;/div&gt;</code></pre>
<p>Or via the first-party UI component:</p>
<pre data-language="html"><code>&lt;div
  data-component=&quot;file-upload&quot;
  store=&quot;public&quot;
  endpoint=&quot;/api/upload&quot;
  accept=&quot;image/*&quot;
  multiple=&quot;true&quot;
&gt;&lt;/div&gt;</code></pre>
<p>It dispatches bubbling events you can listen for:</p>
<ul>
<li><code>wrnexus:upload</code> — <code>detail: &#123; file, result: &#123; key, url, name, size, type &#125; &#125;</code></li>
<li><code>wrnexus:upload-error</code> — <code>detail: &#123; file, error &#125;</code></li>
</ul>
<h4 id="serve-private-files-behind-application-authentication">Serve private files behind application authentication</h4>
<ul>
<li><strong>Public + local</strong> → served automatically at <code>/__wrnexus/uploads/&lt;store&gt;/&lt;key&gt;</code> (immutable cache).</li>
<li><strong>Public + S3</strong> → <code>url</code> is the bucket/CDN URL directly.</li>
<li><strong>Private</strong> (any driver) → mount a route and gate it with your auth middleware:</li>
</ul>
<pre data-language="ts"><code>// app/api/files/[key].ts
import &#123; serveFromStore &#125; from &quot;@wrnexus/uploader&quot;;
export const GET = serveFromStore(&quot;docs&quot;); // your middleware decides who gets in</code></pre>
<h3 id="api">API</h3>
<div class="table-wrap"><table>
<thead><tr><th>Export</th><th>What</th></tr></thead>
<tbody><tr><td><code>handleUpload(opts)</code></td><td>POST route handler → JSON <code>&#123; ok, files &#125;</code></td></tr><tr><td><code>upload(store, req, opts)</code></td><td>Parse + validate + store; returns <code>&#123; files &#125;</code></td></tr><tr><td><code>serveFromStore(store)</code></td><td>Route handler that streams an object back (gate it for private)</td></tr><tr><td><code>getStore(name?)</code> / <code>hasStorage(name?)</code></td><td>Reach a store's <code>driver</code> (<code>put</code>/<code>get</code>/<code>delete</code>/<code>publicUrl</code>)</td></tr><tr><td><code>configureStorage(config, root)</code></td><td>Build the registry (the framework calls this at startup)</td></tr><tr><td><code>s3Driver</code> / <code>localDriver</code> / <code>signS3</code></td><td>Lower-level building blocks</td></tr></tbody></table></div>
<h3 id="notes">Notes</h3>
<ul>
<li>Uploads count against the server's <code>maxBodyBytes</code>; per-file limits use each store's <code>maxBytes</code>.</li>
<li>SigV4 signing is implemented from scratch (no <code>@aws-sdk</code>); tested against local S3 semantics.</li>
<p>Live AWS/R2 connectivity depends on your credentials + bucket policy.</p>
<li>v1 buffers each file in memory up to its size cap (fine for images/docs up to tens of MB).</li>
</ul></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration comes from the exact installed package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>import &#123; Context &#125; from '@wrnexus/core';

/**
 * Storage driver contract + config types.
 *
 * A `StorageDriver` is the low-level object store (local disk, S3, …). It knows
 * how to put/get/delete raw bytes under a key — nothing about HTTP, multipart
 * parsing, validation, or URLs. The registry (`client.ts`) builds one driver per
 * configured store and the upload layer (`upload.ts`) drives them. This mirrors
 * `@wrnexus/db`'s driver/adapter split.
 */
/** Whether a store's objects are world-readable or served behind app auth. */
type StoreAccess = &quot;public&quot; | &quot;private&quot;;
/** An object read back from a store. */
interface StoredObject &#123;
    /** Object bytes as a web stream (preferred) or a buffer. */
    body: ReadableStream&lt;Uint8Array&gt; | Uint8Array;
    /** MIME type to serve with. */
    contentType: string;
    /** Size in bytes, when known. */
    size?: number;
&#125;
/** Metadata passed alongside the bytes on `put`. */
interface PutMeta &#123;
    contentType: string;
    /** Original client filename (informational only — NEVER used as a path). */
    filename?: string;
&#125;
/** The low-level object store. Implementations: `adapters/local.ts`, `adapters/s3.ts`. */
interface StorageDriver &#123;
    /** Persist `data` under `key` (overwrites). */
    put(key: string, data: Uint8Array, meta: PutMeta): Promise&lt;void&gt;;
    /** Fetch an object, or `null` if it doesn't exist. */
    get(key: string): Promise&lt;StoredObject | null&gt;;
    /** Remove an object. No error if it's already gone. */
    delete(key: string): Promise&lt;void&gt;;
    /**
     * A directly-servable absolute URL for a PUBLIC object (e.g. an S3/CDN URL), or
     * `null` when the framework should serve it (local public stores). Private
     * stores always return `null`.
     */
    publicUrl(key: string): string | null;
&#125;
/** Local-disk store. `dir` is resolved against the app root when relative. */
interface LocalStoreConfig &#123;
    driver: &quot;local&quot;;
    access: StoreAccess;
    /** Directory the files live under (e.g. &quot;uploads/public&quot;). */
    dir: string;
    /** Reject files larger than this many bytes (per file). */
    maxBytes?: number;
    /** Allowed types: MIME (`&quot;image/*&quot;`, `&quot;application/pdf&quot;`) and/or extensions (`&quot;.pdf&quot;`). */
    accept?: string[];
&#125;
/** S3 / S3-compatible store (AWS, Cloudflare R2, Backblaze B2, MinIO, DO Spaces). */
interface S3StoreConfig &#123;
    driver: &quot;s3&quot;;
    access: StoreAccess;
    bucket: string;
    region: string;
    accessKeyId: string;
    secretAccessKey: string;
    /**
     * Custom endpoint for non-AWS services, e.g.
     * `https://&lt;acct&gt;.r2.cloudflarestorage.com`. Omit for AWS S3.
     */
    endpoint?: string;
    /** Force path-style URLs (`/bucket/key`). Defaults on for custom endpoints. */
    forcePathStyle?: boolean;
    /** Public base URL for `publicUrl()` (a CDN or public bucket domain). */
    publicBaseUrl?: string;
    maxBytes?: number;
    accept?: string[];
&#125;
type StoreConfig = LocalStoreConfig | S3StoreConfig;
/** The `storage` block in `wrnexus.config.ts`. */
interface StorageConfig &#123;
    /** Name of the store used when a call omits one. Defaults to the first store. */
    default?: string;
    /** Named stores, reached with `getStore(&quot;&lt;name&gt;&quot;)` / `upload(&quot;&lt;name&gt;&quot;, …)`. */
    stores: Record&lt;string, StoreConfig&gt;;
&#125;

/**
 * Process-wide store registry, configured once at server startup from the
 * `storage` block in `wrnexus.config.ts` (mirrors `@wrnexus/db`'s registry).
 * Handlers then call `getStore(&quot;&lt;name&gt;&quot;)` — or omit the name for the default.
 */

interface Store &#123;
    name: string;
    access: StoreAccess;
    driver: StorageDriver;
    config: StoreConfig;
&#125;
/** Build a driver per configured store. Safe to call again (fully replaces). */
declare function configureStorage(config: StorageConfig | undefined, appRoot: string): void;
/** Whether the default (or a named) store is configured. */
declare function hasStorage(name?: string): boolean;
/** The default store, or a named one. Throws if it isn't configured. */
declare function getStore(name?: string): Store;
/** Names of all configured stores. */
declare function storeNames(): string[];

/**
 * The HTTP-facing upload + serve layer: parse multipart requests, validate,
 * store, and serve files back. Built on the store registry (`client.ts`).
 */

/** Reserved prefix the framework serves PUBLIC local objects from. */
declare const UPLOADS_PREFIX = &quot;/__wrnexus/uploads/&quot;;
interface UploadedFile &#123;
    /** Storage key — pass to `getStore().driver.get/delete` or a serve route. */
    key: string;
    /** A servable URL for public objects, or `null` for private stores. */
    url: string | null;
    /** Original (sanitized) client filename, for display. */
    name: string;
    type: string;
    size: number;
&#125;
interface UploadOptions &#123;
    /** Only read files from this form field (default: every file field). */
    field?: string;
    /** Override the store's `maxBytes`. */
    maxBytes?: number;
    /** Override the store's `accept` list. */
    accept?: string[];
    /** Key prefix, e.g. `&quot;avatars&quot;` → keys become `avatars/&lt;yyyy&gt;/&lt;mm&gt;/&lt;rand&gt;.&lt;ext&gt;`. */
    prefix?: string;
&#125;
/** A 4xx-carrying error so `handleUpload` can map it to a status. */
declare class UploadError extends Error &#123;
    readonly status: number;
    constructor(message: string, status?: number);
&#125;
/** The servable URL for a stored object (public → URL, private → null). */
declare function storedUrl(store: Store, key: string): string | null;
/**
 * Read multipart file(s) from a request and store them. Throws `UploadError`
 * (4xx) on validation failures. Call it directly, or use `handleUpload`.
 */
declare function upload(storeName: string | undefined, req: Request, opts?: UploadOptions): Promise&lt;&#123;
    files: UploadedFile[];
&#125;&gt;;
/**
 * Ready-made POST handler:
 *
 *   // app/api/upload.ts
 *   export const POST = handleUpload(&#123; store: &quot;public&quot; &#125;);
 *
 * Returns `&#123; ok:true, files:[…] &#125;` on success, or `&#123; ok:false, error &#125;` with a
 * 4xx/5xx status.
 */
declare function handleUpload(opts?: UploadOptions &amp; &#123;
    store?: string;
&#125;): (ctx: Context) =&gt; Promise&lt;Response&gt;;
/**
 * Serve an object from a store as a route handler — mount it behind your auth
 * middleware to gate PRIVATE files:
 *
 *   // app/api/files/[key].ts
 *   export const GET = serveFromStore(&quot;docs&quot;);
 *
 * Reads the key from `ctx.params.key` (or `ctx.params.path`); it may contain `/`.
 */
declare function serveFromStore(storeName?: string, opts?: &#123;
    param?: string;
&#125;): (ctx: Context) =&gt; Promise&lt;Response&gt;;
/**
 * Framework asset hook: serve PUBLIC local objects at
 * `/__wrnexus/uploads/&lt;store&gt;/&lt;key&gt;`. Returns `null` for anything it doesn't
 * own (unknown/private/S3-backed store) so the caller falls through. Wired into
 * the dev + prod asset servers.
 */
declare function serveStoredFile(pathname: string): Promise&lt;Response | null&gt;;

/**
 * Client runtime for `&lt;div data-uploader&gt;` elements — drag-and-drop + file
 * input, per-file progress bars, and success/failed states. Injected by
 * `collectScripts` only on pages that contain `data-uploader` (same mechanism as
 * `validate.js`). Self-contained: it injects its own themed stylesheet (using
 * `--wire-*` tokens) and posts each file via XHR so upload progress is live.
 *
 * Markup it enhances (also a valid no-JS `&lt;form&gt;` fallback if you wrap it):
 *   &lt;div data-uploader=&quot;public&quot; data-endpoint=&quot;/api/upload&quot;
 *        data-accept=&quot;image/*&quot; data-max=&quot;10000000&quot; data-multiple&gt;&lt;/div&gt;
 *
 * Events dispatched on the element (bubble):
 *   wrnexus:upload        detail: &#123; file, result: &#123; key, url, name, size, type &#125; &#125;
 *   wrnexus:upload-error  detail: &#123; file, error &#125;
 *
 * NOTE: written with single/double quotes + string concatenation only — no
 * backticks and no $&#123;...&#125;, so it embeds safely in the exported template string.
 */
declare const UPLOAD_JS_HREF = &quot;/__wrnexus/uploader.js&quot;;
declare const UPLOAD_RUNTIME = &quot;\n(function () &#123;\n  if (typeof document === \&quot;undefined\&quot;) return;\n  var CSRF_COOKIE = \&quot;wire-csrf\&quot;;\n\n  var CSS =\n    \&quot;.wire-uploader&#123;display:block&#125;\&quot; +\n    \&quot;.wire-uploader-zone&#123;display:flex;align-items:center;justify-content:center;text-align:center;\&quot; +\n      \&quot;min-height:8rem;padding:1.25rem;border:2px dashed var(--wire-border,#cbd5e1);border-radius:12px;\&quot; +\n      \&quot;background:var(--wire-surface,transparent);color:var(--wire-muted,#64748b);cursor:pointer;\&quot; +\n      \&quot;transition:border-color .15s ease,background-color .15s ease;position:relative&#125;\&quot; +\n    \&quot;.wire-uploader-zone:hover,.wire-uploader-zone:focus-visible&#123;border-color:var(--wire-brand,#3f7dff);outline:none&#125;\&quot; +\n    \&quot;.wire-uploader-zone.is-drag&#123;border-color:var(--wire-brand,#3f7dff);background:color-mix(in oklab,var(--wire-brand,#3f7dff) 8%,transparent)&#125;\&quot; +\n    \&quot;.wire-uploader-prompt&#123;font-size:.9rem;pointer-events:none&#125;\&quot; +\n    \&quot;.wire-uploader-input&#123;position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer&#125;\&quot; +\n    \&quot;.wire-uploader-list&#123;list-style:none;margin:.75rem 0 0;padding:0;display:flex;flex-direction:column;gap:.5rem&#125;\&quot; +\n    \&quot;.wire-uploader-item&#123;display:grid;grid-template-columns:1fr auto;gap:.15rem .75rem;align-items:center;\&quot; +\n      \&quot;font-size:.82rem;padding:.5rem .7rem;border:1px solid var(--wire-border,#e2e8f0);border-radius:8px&#125;\&quot; +\n    \&quot;.wire-uploader-name&#123;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--wire-text,#0f172a)&#125;\&quot; +\n    \&quot;.wire-uploader-meta&#123;color:var(--wire-muted,#94a3b8);font-variant-numeric:tabular-nums&#125;\&quot; +\n    \&quot;.wire-uploader-bar&#123;grid-column:1/-1;height:5px;border-radius:999px;background:var(--wire-border,#e2e8f0);overflow:hidden&#125;\&quot; +\n    \&quot;.wire-uploader-fill&#123;height:100%;width:0;border-radius:999px;background:var(--wire-brand,#3f7dff);transition:width .15s ease&#125;\&quot; +\n    \&quot;.wire-uploader-status&#123;grid-column:1/-1;font-size:.75rem;color:var(--wire-muted,#94a3b8);font-variant-numeric:tabular-nums&#125;\&quot; +\n    \&quot;.wire-uploader-item.is-done .wire-uploader-fill&#123;background:var(--wire-success,#16a34a)&#125;\&quot; +\n    \&quot;.wire-uploader-item.is-done .wire-uploader-status&#123;color:var(--wire-success,#16a34a)&#125;\&quot; +\n    \&quot;.wire-uploader-item.is-error .wire-uploader-fill&#123;background:var(--wire-danger,#dc2626)&#125;\&quot; +\n    \&quot;.wire-uploader-item.is-error .wire-uploader-status&#123;color:var(--wire-danger,#dc2626)&#125;\&quot;;\n\n  function injectCss() &#123;\n    if (document.getElementById(\&quot;wire-uploader-css\&quot;)) return;\n    var s = document.createElement(\&quot;style\&quot;);\n    s.id = \&quot;wire-uploader-css\&quot;;\n    s.textContent = CSS;\n    document.head.appendChild(s);\n  &#125;\n\n  function cookie(name) &#123;\n    var m = document.cookie.match(new RegExp(\&quot;(?:^|; )\&quot; + name + \&quot;=([^;]*)\&quot;));\n    return m ? decodeURIComponent(m[1]) : \&quot;\&quot;;\n  &#125;\n  function el(tag, cls, text) &#123;\n    var e = document.createElement(tag);\n    if (cls) e.className = cls;\n    if (text != null) e.textContent = text;\n    return e;\n  &#125;\n  function fmt(n) &#123;\n    if (n &lt; 1024) return n + \&quot; B\&quot;;\n    if (n &lt; 1048576) return (n / 1024).toFixed(1) + \&quot; KB\&quot;;\n    return (n / 1048576).toFixed(1) + \&quot; MB\&quot;;\n  &#125;\n  function accepts(accept, file) &#123;\n    var list = (accept || \&quot;\&quot;).split(\&quot;,\&quot;).map(function (s) &#123; return s.trim().toLowerCase(); &#125;).filter(Boolean);\n    if (!list.length) return true;\n    var type = (file.type || \&quot;\&quot;).toLowerCase();\n    var name = (file.name || \&quot;\&quot;).toLowerCase();\n    var ext = name.indexOf(\&quot;.\&quot;) &gt;= 0 ? name.slice(name.lastIndexOf(\&quot;.\&quot;)) : \&quot;\&quot;;\n    return list.some(function (rule) &#123;\n      if (rule.charAt(0) === \&quot;.\&quot;) return rule === ext;\n      if (rule.slice(-2) === \&quot;/*\&quot;) return type.indexOf(rule.slice(0, -1)) === 0;\n      return rule === type;\n    &#125;);\n  &#125;\n\n  function setup(root) &#123;\n    if (root.__wrnexusUploader) return;\n    root.__wrnexusUploader = true;\n\n    var endpoint = root.getAttribute(\&quot;data-endpoint\&quot;) || \&quot;/api/upload\&quot;;\n    var multipleAttr = root.getAttribute(\&quot;data-multiple\&quot;);\n    var multiple = root.hasAttribute(\&quot;data-multiple\&quot;) &amp;&amp; multipleAttr !== \&quot;false\&quot;;\n    var accept = root.getAttribute(\&quot;data-accept\&quot;) || \&quot;\&quot;;\n    var maxBytes = parseInt(root.getAttribute(\&quot;data-max\&quot;) || \&quot;0\&quot;, 10) || 0;\n    var field = root.getAttribute(\&quot;data-field\&quot;) || (multiple ? \&quot;files\&quot; : \&quot;file\&quot;);\n    var promptText = root.getAttribute(\&quot;data-label\&quot;) || \&quot;Drag files here or click to browse\&quot;;\n\n    root.classList.add(\&quot;wire-uploader\&quot;);\n    var zone = el(\&quot;div\&quot;, \&quot;wire-uploader-zone\&quot;);\n    zone.setAttribute(\&quot;role\&quot;, \&quot;button\&quot;);\n    zone.setAttribute(\&quot;tabindex\&quot;, \&quot;0\&quot;);\n    zone.appendChild(el(\&quot;div\&quot;, \&quot;wire-uploader-prompt\&quot;, promptText));\n    var input = document.createElement(\&quot;input\&quot;);\n    input.type = \&quot;file\&quot;;\n    input.className = \&quot;wire-uploader-input\&quot;;\n    if (multiple) input.multiple = true;\n    if (accept) input.accept = accept;\n    zone.appendChild(input);\n    var listEl = el(\&quot;ul\&quot;, \&quot;wire-uploader-list\&quot;);\n    root.appendChild(zone);\n    root.appendChild(listEl);\n\n    zone.addEventListener(\&quot;keydown\&quot;, function (e) &#123;\n      if (e.key === \&quot;Enter\&quot; || e.key === \&quot; \&quot;) &#123; e.preventDefault(); input.click(); &#125;\n    &#125;);\n    [\&quot;dragenter\&quot;, \&quot;dragover\&quot;].forEach(function (ev) &#123;\n      zone.addEventListener(ev, function (e) &#123; e.preventDefault(); zone.classList.add(\&quot;is-drag\&quot;); &#125;);\n    &#125;);\n    [\&quot;dragleave\&quot;, \&quot;drop\&quot;].forEach(function (ev) &#123;\n      zone.addEventListener(ev, function (e) &#123; e.preventDefault(); zone.classList.remove(\&quot;is-drag\&quot;); &#125;);\n    &#125;);\n    zone.addEventListener(\&quot;drop\&quot;, function (e) &#123;\n      if (e.dataTransfer &amp;&amp; e.dataTransfer.files) handle(e.dataTransfer.files);\n    &#125;);\n    input.addEventListener(\&quot;change\&quot;, function () &#123;\n      if (input.files) handle(input.files);\n      input.value = \&quot;\&quot;;\n    &#125;);\n\n    function handle(files) &#123;\n      var arr = Array.prototype.slice.call(files);\n      if (!multiple) arr = arr.slice(0, 1);\n      arr.forEach(uploadOne);\n    &#125;\n\n    function row(file) &#123;\n      var li = el(\&quot;li\&quot;, \&quot;wire-uploader-item\&quot;);\n      li.appendChild(el(\&quot;span\&quot;, \&quot;wire-uploader-name\&quot;, file.name));\n      li.appendChild(el(\&quot;span\&quot;, \&quot;wire-uploader-meta\&quot;, fmt(file.size)));\n      var bar = el(\&quot;div\&quot;, \&quot;wire-uploader-bar\&quot;);\n      var fill = el(\&quot;div\&quot;, \&quot;wire-uploader-fill\&quot;);\n      bar.appendChild(fill);\n      li.appendChild(bar);\n      var status = el(\&quot;span\&quot;, \&quot;wire-uploader-status\&quot;, \&quot;\&quot;);\n      li.appendChild(status);\n      listEl.appendChild(li);\n      return &#123; li: li, fill: fill, status: status &#125;;\n    &#125;\n\n    function uploadOne(file) &#123;\n      var ui = row(file);\n      if (maxBytes &amp;&amp; file.size &gt; maxBytes) return fail(ui, \&quot;Too large (max \&quot; + fmt(maxBytes) + \&quot;)\&quot;, file);\n      if (!accepts(accept, file)) return fail(ui, \&quot;Type not allowed\&quot;, file);\n\n      var fd = new FormData();\n      fd.append(field, file, file.name);\n      var xhr = new XMLHttpRequest();\n      xhr.open(\&quot;POST\&quot;, endpoint, true);\n      var token = cookie(CSRF_COOKIE);\n      if (token) xhr.setRequestHeader(\&quot;x-csrf-token\&quot;, token);\n      xhr.upload.addEventListener(\&quot;progress\&quot;, function (e) &#123;\n        if (e.lengthComputable) &#123;\n          var pct = Math.round((e.loaded / e.total) * 100);\n          ui.fill.style.width = pct + \&quot;%\&quot;;\n          ui.status.textContent = pct + \&quot;%\&quot;;\n        &#125;\n      &#125;);\n      xhr.addEventListener(\&quot;load\&quot;, function () &#123;\n        var data = null;\n        try &#123; data = JSON.parse(xhr.responseText); &#125; catch (e2) &#123;&#125;\n        if (xhr.status &gt;= 200 &amp;&amp; xhr.status &lt; 300 &amp;&amp; data &amp;&amp; data.ok) &#123;\n          done(ui, (data.files &amp;&amp; data.files[0]) || null, file);\n        &#125; else &#123;\n          fail(ui, (data &amp;&amp; data.error) || (\&quot;Upload failed (\&quot; + xhr.status + \&quot;)\&quot;), file);\n        &#125;\n      &#125;);\n      xhr.addEventListener(\&quot;error\&quot;, function () &#123; fail(ui, \&quot;Network error\&quot;, file); &#125;);\n      xhr.send(fd);\n    &#125;\n\n    function done(ui, info, file) &#123;\n      ui.li.classList.remove(\&quot;is-error\&quot;);\n      ui.li.classList.add(\&quot;is-done\&quot;);\n      ui.fill.style.width = \&quot;100%\&quot;;\n      ui.status.textContent = \&quot;\\u2713 Uploaded\&quot;;\n      root.dispatchEvent(new CustomEvent(\&quot;wrnexus:upload\&quot;, &#123; bubbles: true, detail: &#123; file: file, result: info &#125; &#125;));\n    &#125;\n    function fail(ui, msg, file) &#123;\n      ui.li.classList.add(\&quot;is-error\&quot;);\n      ui.status.textContent = \&quot;\\u2717 \&quot; + msg;\n      root.dispatchEvent(new CustomEvent(\&quot;wrnexus:upload-error\&quot;, &#123; bubbles: true, detail: &#123; file: file, error: msg &#125; &#125;));\n    &#125;\n  &#125;\n\n  function init() &#123;\n    injectCss();\n    var nodes = document.querySelectorAll(\&quot;[data-uploader]\&quot;);\n    for (var i = 0; i &lt; nodes.length; i++) setup(nodes[i]);\n  &#125;\n  if (document.readyState === \&quot;loading\&quot;) document.addEventListener(\&quot;DOMContentLoaded\&quot;, init);\n  else init();\n&#125;)();\n&quot;;

/**
 * Local-disk storage driver. Files live under a configured directory; keys map
 * to relative paths inside it. Path traversal is rejected — a key can never
 * escape the base dir.
 */

declare function localDriver(config: LocalStoreConfig, appRoot: string): StorageDriver;

/**
 * S3 (and S3-compatible) storage driver — zero deps, SigV4-signed `fetch`.
 * Works with AWS S3, Cloudflare R2, Backblaze B2, MinIO, DigitalOcean Spaces.
 *
 * Path-style vs virtual-hosted: AWS defaults to virtual-hosted
 * (`bucket.s3.region.amazonaws.com`); custom endpoints (R2/MinIO) default to
 * path-style (`endpoint/bucket/key`). Override with `forcePathStyle`.
 */

declare function s3Driver(config: S3StoreConfig): StorageDriver;

/**
 * AWS Signature Version 4 for S3 requests — zero external deps, built on
 * `node:crypto` + `fetch`. Matches the framework's zero-dep ethos (like
 * `@wrnexus/ai`) and works with any S3-compatible service (AWS, Cloudflare R2,
 * Backblaze B2, MinIO, DigitalOcean Spaces).
 *
 * Reference: docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
 */
/** Hex-encoded SHA-256 of a payload. */
declare function sha256Hex(data: Uint8Array | string): string;
/**
 * Percent-encode an S3 object key for the request path. Every character except
 * the RFC 3986 unreserved set is encoded; `/` between segments is preserved.
 */
declare function encodeKey(key: string): string;
interface SignInput &#123;
    method: string;
    host: string;
    /** Canonical URI — already `%`-encoded, begins with `/`. */
    path: string;
    region: string;
    accessKeyId: string;
    secretAccessKey: string;
    /** Hex SHA-256 of the body, or `&quot;UNSIGNED-PAYLOAD&quot;`. */
    payloadHash: string;
    /** Extra headers to sign (e.g. `content-type`). `host`/`x-amz-*` are added here. */
    headers?: Record&lt;string, string&gt;;
    date: Date;
    service?: string;
&#125;
/**
 * Compute the signed header set for an S3 request. Returns the headers to send
 * (lowercased names, including `authorization`, `host`, `x-amz-date`,
 * `x-amz-content-sha256`).
 */
declare function signS3(input: SignInput): Record&lt;string, string&gt;;

/**
 * Minimal extension ↔ MIME mapping + `accept` matching. Zero-dep: just a table
 * big enough for the common upload types (images, docs, media, archives).
 */
/** Lowercased extension WITHOUT the dot (e.g. &quot;png&quot;), or &quot;&quot; if none. */
declare function extOf(name: string): string;
/** MIME type for a filename/key by its extension, or a safe default. */
declare function contentTypeOf(name: string, fallback?: string): string;
/** The conventional extension for a MIME type, or &quot;&quot; (used to name S3 keys). */
declare function extForType(type: string): string;
/**
 * Does `file` (its MIME `type` + `name`) satisfy an `accept` list? Each accept
 * entry is a MIME type (`&quot;image/png&quot;`), a wildcard MIME (`&quot;image/*&quot;`), or a
 * dotted extension (`&quot;.pdf&quot;`). An empty/omitted list accepts everything.
 */
declare function accepts(accept: string[] | undefined, file: &#123;
    type: string;
    name: string;
&#125;): boolean;

export &#123; type LocalStoreConfig, type PutMeta, type S3StoreConfig, type StorageConfig, type StorageDriver, type Store, type StoreAccess, type StoreConfig, type StoredObject, UPLOADS_PREFIX, UPLOAD_JS_HREF, UPLOAD_RUNTIME, UploadError, type UploadOptions, type UploadedFile, accepts, configureStorage, contentTypeOf, encodeKey, extForType, extOf, getStore, handleUpload, hasStorage, localDriver, s3Driver, serveFromStore, serveStoredFile, sha256Hex, signS3, storeNames, storedUrl, upload &#125;;
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Examples are taken from this package's installed documentation and must be evaluated with its requirements and stability notes.</p><div class="example-grid"><article class="example-card"><h3>Configure local and S3 stores</h3><pre data-language="ts"><code>// wrnexus.config.ts
import type &#123; AppConfig &#125; from &quot;@wrnexus/styles&quot;;

const config: AppConfig = &#123;
  storage: &#123;
    default: &quot;public&quot;,
    stores: &#123;
      // Local disk, world-readable — served by the framework with a 1-year cache.
      public: &#123;
        driver: &quot;local&quot;,
        dir: &quot;uploads/public&quot;, // relative to the app root (dev) / cwd (prod)
        access: &quot;public&quot;,
        maxBytes: 10_000_000,
        accept: [&quot;image/*&quot;, &quot;.pdf&quot;], // MIME, &quot;type/*&quot; wildcards, or &quot;.ext&quot;
      &#125;,
      // Private S3 (works with AWS, Cloudflare R2, Backblaze B2, MinIO, DO Spaces).
      docs: &#123;
        driver: &quot;s3&quot;,
        access: &quot;private&quot;,
        bucket: &quot;my-bucket&quot;,
        region: &quot;auto&quot;,
        endpoint: &quot;https://&lt;acct&gt;.r2.cloudflarestorage.com&quot;,
        accessKeyId: process.env.S3_KEY!,
        secretAccessKey: process.env.S3_SECRET!,
      &#125;,
    &#125;,
  &#125;,
&#125;;
export default config;</code></pre></article><article class="example-card"><h3>Upload from an API route or server function</h3><pre data-language="ts"><code>// app/api/upload.ts — one-liner
import &#123; handleUpload &#125; from &quot;@wrnexus/uploader&quot;;
export const POST = handleUpload(&#123; store: &quot;public&quot; &#125;);
// → &#123; ok: true, files: [&#123; key, url, name, type, size &#125;] &#125;</code></pre></article><article class="example-card"><h3>Upload from an API route or server function</h3><pre data-language="ts"><code>// or drive it yourself, anywhere you have the request
import &#123; upload, getStore &#125; from &quot;@wrnexus/uploader&quot;;
const &#123; files &#125; = await upload(&quot;docs&quot;, ctx.req, &#123; prefix: &quot;invoices&quot; &#125;);
await getStore(&quot;docs&quot;).driver.delete(files[0].key);</code></pre></article><article class="example-card"><h3>Add a client upload widget</h3><pre data-language="html"><code>&lt;div
  data-uploader=&quot;public&quot;
  data-endpoint=&quot;/api/upload&quot;
  data-accept=&quot;image/*&quot;
  data-max=&quot;10000000&quot;
  data-multiple
&gt;&lt;/div&gt;</code></pre></article><article class="example-card"><h3>Add a client upload widget</h3><pre data-language="html"><code>&lt;div
  data-component=&quot;file-upload&quot;
  store=&quot;public&quot;
  endpoint=&quot;/api/upload&quot;
  accept=&quot;image/*&quot;
  multiple=&quot;true&quot;
&gt;&lt;/div&gt;</code></pre></article><article class="example-card"><h3>Serve private files behind application authentication</h3><pre data-language="ts"><code>// app/api/files/[key].ts
import &#123; serveFromStore &#125; from &quot;@wrnexus/uploader&quot;;
export const GET = serveFromStore(&quot;docs&quot;); // your middleware decides who gets in</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="#usage">Usage</a><a class="toc-level-4" href="#configure-local-and-s3-stores">Configure local and S3 stores</a><a class="toc-level-4" href="#upload-from-an-api-route-or-server-function">Upload from an API route or server function</a><a class="toc-level-4" href="#add-a-client-upload-widget">Add a client upload widget</a><a class="toc-level-4" href="#serve-private-files-behind-application-authentication">Serve private files behind application authentication</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-3" href="#notes">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>WRNexusJS 0.2.31 · Private Developer Preview · Bun-native · Documentation generated from installed package APIs.</footer>
    </div>
  }
}
