Files
WRNexusJSDoc/app/pages/packages/uploader.wrn
T
2026-07-12 16:14:06 +05:30

420 lines
32 KiB
Plaintext

page wrnexusuploader {
seo {
title = "@wrnexus/uploader"
description = "Validated local/S3 uploads and secure file serving."
}
view {
<div class="docs-shell">
<header class="topbar">
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
</header>
<main class="page package-page">
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Data</span><h1>@wrnexus/uploader</h1><p>Validated local/S3 uploads and secure file serving.</p><code>bun add @wrnexus/uploader@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
<article class="documentation"><section class="doc-intro"><span class="eyebrow">Data</span><h1>@wrnexus/uploader</h1><p>Validated local/S3 uploads and secure file serving.</p><pre><code>bun add @wrnexus/uploader@0.2.12</code></pre></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="configure">Configure</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>
<h3 id="upload-server">Upload (server)</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>
<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>
<h3 id="widget-client">Widget (client)</h3>
<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>
<h3 id="serve-files">Serve files</h3>
<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 is generated from the exact published 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>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid"><article class="example-card"><h3>Example 1</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>Example 2</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>Example 3</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>Example 4</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></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="#configure">Configure</a><a class="toc-level-3" href="#upload-server">Upload (server)</a><a class="toc-level-3" href="#widget-client">Widget (client)</a><a class="toc-level-3" href="#serve-files">Serve files</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.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
</div>
}
}