page wrnexusuploader { seo { title = "@wrnexus/uploader" description = "Validated local/S3 uploads and secure file serving." } view {
W WRNexusJS
Browse documentation
Data · Preview

@wrnexus/uploader

Validated local/S3 uploads and secure file serving.

Private registry access required

This package is not available from the public npm registry. After WorkRoot approves access and supplies private registry instructions, install the release-aligned package:

bun add @wrnexus/uploader@0.2.59

Request preview access. Never put registry tokens in source control.

Config-driven file uploads + serving for WRNexusJS. Declare named storage stores (local disk or any S3-compatible backend) in wrnexus.config.ts, 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).

Usage

Configure local and S3 stores

// wrnexus.config.ts
import type { AppConfig } from "@wrnexus/styles";

const config: AppConfig = {
  storage: {
    default: "public",
    stores: {
      // Local disk, world-readable — served by the framework with a 1-year cache.
      public: {
        driver: "local",
        dir: "uploads/public", // relative to the app root (dev) / cwd (prod)
        access: "public",
        maxBytes: 10_000_000,
        accept: ["image/*", ".pdf"], // MIME, "type/*" wildcards, or ".ext"
      },
      // Private S3 (works with AWS, Cloudflare R2, Backblaze B2, MinIO, DO Spaces).
      docs: {
        driver: "s3",
        access: "private",
        bucket: "my-bucket",
        region: "auto",
        endpoint: "https://<acct>.r2.cloudflarestorage.com",
        accessKeyId: process.env.S3_KEY!,
        secretAccessKey: process.env.S3_SECRET!,
      },
    },
  },
};
export default config;

Upload from an API route or server function

// app/api/upload.ts — one-liner
import { handleUpload } from "@wrnexus/uploader";
export const POST = handleUpload({ store: "public" });
// → { ok: true, files: [{ key, url, name, type, size }] }
// or drive it yourself, anywhere you have the request
import { upload, getStore } from "@wrnexus/uploader";
const { files } = await upload("docs", ctx.req, { prefix: "invoices" });
await getStore("docs").driver.delete(files[0].key);

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 url.

Add a client upload widget

Drop the element anywhere; the runtime (drag-and-drop, per-file progress, success/failed states) is auto-injected on pages that contain data-uploader:

<div
  data-uploader="public"
  data-endpoint="/api/upload"
  data-accept="image/*"
  data-max="10000000"
  data-multiple
></div>

Or via the first-party UI component:

<div
  data-component="file-upload"
  store="public"
  endpoint="/api/upload"
  accept="image/*"
  multiple="true"
></div>

It dispatches bubbling events you can listen for:

  • wrnexus:uploaddetail: { file, result: { key, url, name, size, type } }
  • wrnexus:upload-errordetail: { file, error }

Serve private files behind application authentication

  • Public + local → served automatically at /__wrnexus/uploads/<store>/<key> (immutable cache).
  • Public + S3url is the bucket/CDN URL directly.
  • Private (any driver) → mount a route and gate it with your auth middleware:
// app/api/files/[key].ts
import { serveFromStore } from "@wrnexus/uploader";
export const GET = serveFromStore("docs"); // your middleware decides who gets in

API

ExportWhat
handleUpload(opts)POST route handler → JSON { ok, files }
upload(store, req, opts)Parse + validate + store; returns { files }
serveFromStore(store)Route handler that streams an object back (gate it for private)
getStore(name?) / hasStorage(name?)Reach a store's driver (put/get/delete/publicUrl)
configureStorage(config, root)Build the registry (the framework calls this at startup)
s3Driver / localDriver / signS3Lower-level building blocks

Notes

  • Uploads count against the server's maxBodyBytes; per-file limits use each store's maxBytes.
  • SigV4 signing is implemented from scratch (no @aws-sdk); tested against local S3 semantics.
  • Live AWS/R2 connectivity depends on your credentials + bucket policy.

  • v1 buffers each file in memory up to its size cap (fine for images/docs up to tens of MB).

Complete TypeScript API

This declaration comes from the exact installed package and lists its exported functions, classes, interfaces, and types.

import { Context } 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 = "public" | "private";
/** An object read back from a store. */
interface StoredObject {
    /** Object bytes as a web stream (preferred) or a buffer. */
    body: ReadableStream<Uint8Array> | Uint8Array;
    /** MIME type to serve with. */
    contentType: string;
    /** Size in bytes, when known. */
    size?: number;
}
/** Metadata passed alongside the bytes on `put`. */
interface PutMeta {
    contentType: string;
    /** Original client filename (informational only — NEVER used as a path). */
    filename?: string;
}
/** The low-level object store. Implementations: `adapters/local.ts`, `adapters/s3.ts`. */
interface StorageDriver {
    /** Persist `data` under `key` (overwrites). */
    put(key: string, data: Uint8Array, meta: PutMeta): Promise<void>;
    /** Fetch an object, or `null` if it doesn't exist. */
    get(key: string): Promise<StoredObject | null>;
    /** Remove an object. No error if it's already gone. */
    delete(key: string): Promise<void>;
    /**
     * 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;
}
/** Local-disk store. `dir` is resolved against the app root when relative. */
interface LocalStoreConfig {
    driver: "local";
    access: StoreAccess;
    /** Directory the files live under (e.g. "uploads/public"). */
    dir: string;
    /** Reject files larger than this many bytes (per file). */
    maxBytes?: number;
    /** Allowed types: MIME (`"image/*"`, `"application/pdf"`) and/or extensions (`".pdf"`). */
    accept?: string[];
}
/** S3 / S3-compatible store (AWS, Cloudflare R2, Backblaze B2, MinIO, DO Spaces). */
interface S3StoreConfig {
    driver: "s3";
    access: StoreAccess;
    bucket: string;
    region: string;
    accessKeyId: string;
    secretAccessKey: string;
    /**
     * Custom endpoint for non-AWS services, e.g.
     * `https://<acct>.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[];
}
type StoreConfig = LocalStoreConfig | S3StoreConfig;
/** The `storage` block in `wrnexus.config.ts`. */
interface StorageConfig {
    /** Name of the store used when a call omits one. Defaults to the first store. */
    default?: string;
    /** Named stores, reached with `getStore("<name>")` / `upload("<name>", …)`. */
    stores: Record<string, StoreConfig>;
}

/**
 * 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("<name>")` — or omit the name for the default.
 */

interface Store {
    name: string;
    access: StoreAccess;
    driver: StorageDriver;
    config: StoreConfig;
}
/** 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 = "/__wrnexus/uploads/";
interface UploadedFile {
    /** 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;
}
interface UploadOptions {
    /** 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. `"avatars"` → keys become `avatars/<yyyy>/<mm>/<rand>.<ext>`. */
    prefix?: string;
}
/** A 4xx-carrying error so `handleUpload` can map it to a status. */
declare class UploadError extends Error {
    readonly status: number;
    constructor(message: string, status?: number);
}
/** 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<{
    files: UploadedFile[];
}>;
/**
 * Ready-made POST handler:
 *
 *   // app/api/upload.ts
 *   export const POST = handleUpload({ store: "public" });
 *
 * Returns `{ ok:true, files:[…] }` on success, or `{ ok:false, error }` with a
 * 4xx/5xx status.
 */
declare function handleUpload(opts?: UploadOptions & {
    store?: string;
}): (ctx: Context) => Promise<Response>;
/**
 * 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("docs");
 *
 * Reads the key from `ctx.params.key` (or `ctx.params.path`); it may contain `/`.
 */
declare function serveFromStore(storeName?: string, opts?: {
    param?: string;
}): (ctx: Context) => Promise<Response>;
/**
 * Framework asset hook: serve PUBLIC local objects at
 * `/__wrnexus/uploads/<store>/<key>`. 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<Response | null>;

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

/**
 * 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 {
    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 `"UNSIGNED-PAYLOAD"`. */
    payloadHash: string;
    /** Extra headers to sign (e.g. `content-type`). `host`/`x-amz-*` are added here. */
    headers?: Record<string, string>;
    date: Date;
    service?: string;
}
/**
 * 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<string, string>;

/**
 * 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. "png"), or "" 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 "" (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 (`"image/png"`), a wildcard MIME (`"image/*"`), or a
 * dotted extension (`".pdf"`). An empty/omitted list accepts everything.
 */
declare function accepts(accept: string[] | undefined, file: {
    type: string;
    name: string;
}): boolean;

export { 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 };

Examples

Examples are taken from this package's installed documentation and must be evaluated with its requirements and stability notes.

Configure local and S3 stores

// wrnexus.config.ts
import type { AppConfig } from "@wrnexus/styles";

const config: AppConfig = {
  storage: {
    default: "public",
    stores: {
      // Local disk, world-readable — served by the framework with a 1-year cache.
      public: {
        driver: "local",
        dir: "uploads/public", // relative to the app root (dev) / cwd (prod)
        access: "public",
        maxBytes: 10_000_000,
        accept: ["image/*", ".pdf"], // MIME, "type/*" wildcards, or ".ext"
      },
      // Private S3 (works with AWS, Cloudflare R2, Backblaze B2, MinIO, DO Spaces).
      docs: {
        driver: "s3",
        access: "private",
        bucket: "my-bucket",
        region: "auto",
        endpoint: "https://<acct>.r2.cloudflarestorage.com",
        accessKeyId: process.env.S3_KEY!,
        secretAccessKey: process.env.S3_SECRET!,
      },
    },
  },
};
export default config;

Upload from an API route or server function

// app/api/upload.ts — one-liner
import { handleUpload } from "@wrnexus/uploader";
export const POST = handleUpload({ store: "public" });
// → { ok: true, files: [{ key, url, name, type, size }] }

Upload from an API route or server function

// or drive it yourself, anywhere you have the request
import { upload, getStore } from "@wrnexus/uploader";
const { files } = await upload("docs", ctx.req, { prefix: "invoices" });
await getStore("docs").driver.delete(files[0].key);

Add a client upload widget

<div
  data-uploader="public"
  data-endpoint="/api/upload"
  data-accept="image/*"
  data-max="10000000"
  data-multiple
></div>

Add a client upload widget

<div
  data-component="file-upload"
  store="public"
  endpoint="/api/upload"
  accept="image/*"
  multiple="true"
></div>

Serve private files behind application authentication

// app/api/files/[key].ts
import { serveFromStore } from "@wrnexus/uploader";
export const GET = serveFromStore("docs"); // your middleware decides who gets in
} }