@ibanzajoe/uploader
v1.12.0
Published
Drop-in React file-upload picker (drag-and-drop + dialog), headless upload client, in-picker image editor, and transform-URL builder.
Maintainers
Readme
@ibanzajoe/uploader
React picker and headless upload client for the Uploader platform.
📖 Full developer guide: docs/14 — SDK Developer Guide — install, the headless client, delivery/transform URLs, public/hotlink/signed protection, per-file protection, image editing, error handling, and an end-to-end example.
Installation
npm install @ibanzajoe/uploaderQuick start — React picker
import { useState } from 'react'
import { PickerOverlay } from '@ibanzajoe/uploader'
import '@ibanzajoe/uploader/styles.css'
function App() {
const [open, setOpen] = useState(false)
return (
<>
<button onClick={() => setOpen(true)}>Upload</button>
<PickerOverlay
apikey="pk_your_publishable_key"
apiUrl="https://your-api.example.com"
open={open}
onClose={() => setOpen(false)}
onUploadDone={(res) => console.log(res.filesUploaded)}
/>
</>
)
}Headless upload client (no React required)
The @ibanzajoe/uploader/core entry is a framework-free client — use it in a Node
script, a serverless function, a Vue/Svelte app, or behind your own UI. It has no
React dependency.
import { UploaderClient } from '@ibanzajoe/uploader/core'
const client = new UploaderClient({
apikey: 'pk_your_publishable_key', // required — publishable, safe in client code
apiUrl: 'https://your-api.example.com',
})
// Upload one file (a browser File/Blob, or a Node Blob/File):
const result = await client.upload(file, {
filename: 'photo.jpg',
onProgress: (pct) => console.log(`${pct}%`),
})
console.log(result.handle) // stable id, e.g. "abc123def456"
console.log(result.url) // delivery URL (cdn for public, edge for hotlink/signed)
console.log(result.size, result.mimetype)Upload many files with bounded concurrency:
const results = await client.uploadAll(files, { concurrency: 3 })new UploaderClient(options)
| Option | Type | Description |
|---|---|---|
| apikey | string | Required. Public key (pk_…). |
| apiUrl | string | Base URL of the API. Omit for same-origin. |
| security | { policy, signature } | Signed policy applied to every request (see Delivery protection). |
| deliveryProtection | 'public' \| 'hotlink' \| 'signed' | Client-wide default protection for uploads (see below). |
| allowedOrigins | string[] | Client-wide default per-file origin lock. |
| directUpload | boolean | Leave unset to auto-detect direct-to-bucket; false forces the proxied flow. |
client.upload(file, options?) → Promise<FileResult>
| Upload option | Type | Description |
|---|---|---|
| onProgress | (percent: number) => void | 0–100. Real byte progress on the direct + multipart paths. |
| filename | string | Override the stored filename. |
| path | string | Storage path prefix. |
| signal | AbortSignal | Cancel the upload (throws UploaderError code ABORTED). |
| chunkSize | number | Multipart chunk size in bytes (default ~5 MB). |
| deliveryProtection | 'public' \| 'hotlink' \| 'signed' | Per-upload protection for this file — overrides the client default. |
| allowedOrigins | string[] | Per-upload origin lock for this file. |
FileResult
type FileResult = {
handle: string // stable public id — build delivery/transform URLs from it
url: string // delivery URL for the original
filename: string
mimetype: string
size: number // bytes
status: 'Stored'
}When the account/plan allows it, bytes go direct to storage (they never transit the API): files up to 100 MB in one presigned PUT, bigger files in 64 MiB+ parts, 4 at a time, each part with its own short-lived URL and retries. Otherwise the client transparently falls back to a proxied upload (multipart above ~6 MiB).
Components
<PickerOverlay>
Full-screen modal picker with drag-and-drop, progress, and error UI.
| Prop | Type | Description |
|---|---|---|
| apikey | string | Publishable key (pk_…) — identifies your account, not a credential. Required. |
| apiUrl | string | Base URL of the API server |
| open | boolean | Whether the modal is open. Required. |
| onClose | () => void | Called when the modal should close (ESC, backdrop, cancel) |
| onUploadDone | (res: PickerResponse) => void | Called when all uploads complete |
| deliveryProtection | 'public' \| 'hotlink' \| 'signed' | Protection for every file this picker uploads (see Delivery protection). Omit → account default. |
| allowedOrigins | string[] | Per-file origin lock (see Delivery protection). Omit → account allowlist. |
| pickerOptions | PickerOptions | File constraints + sources — { accept?: string[], maxFiles?, maxSize?, fromSources?, cameraFacingMode? } |
| theme | UploaderTheme | Per-instance design tokens (see Theming) |
<DropPane> accepts the same apikey / apiUrl / deliveryProtection /
allowedOrigins / callback props.
Requires
@ibanzajoe/uploader≥ 1.3.0 fordeliveryProtection/allowedOriginson the picker components. (Earlier versions only honored them on the headless client.)
<DropPane>
Inline drop zone that can be embedded in a form.
usePicker(options)
Headless hook for building a fully custom picker UI. Takes the same options as the
components (all UploaderClientOptions incl. deliveryProtection/allowedOrigins,
plus pickerOptions and the onUpload* callbacks) and returns:
{ files, addFiles, removeFile, editFile, retryFile, upload, progress, isUploading, isDone }Delivery protection (public / hotlink / signed)
Every file is served under one of three protection modes. You choose the mode per upload (or set a client-wide default); it must be one your account's plan allows.
| Mode | Who can access | Delivery URL you get back |
|---|---|---|
| public | anyone with the link | https://cdn.…/<key> (CDN-cached, cheapest) |
| hotlink | requests from your allowed domains (Origin/Referer allowlist) | https://edge.…/file/<handle> |
| signed | only holders of a valid signed URL you mint server-side | https://edge.…/file/<handle> |
Set it per upload (headless or picker), or as a client-wide default:
// headless — per upload wins over the client default
await client.upload(logo, { deliveryProtection: 'public' })
await client.upload(productImg, { deliveryProtection: 'hotlink' })
await client.upload(idScan, { deliveryProtection: 'signed' })
// client-wide default
const client = new UploaderClient({ apikey, apiUrl, deliveryProtection: 'signed' })// React picker (≥ 1.3.0)
<PickerOverlay apikey="pk_…" deliveryProtection="signed" open={open} onClose={close} />Resolution order: per-upload → client default → the account default configured in
the dashboard. If you request a mode your plan doesn't allow, the API responds 403
DELIVERY_MODE_NOT_ALLOWED.
allowedOrigins (the domain list):
- For
hotlink: omit it and the file uses your account allowlist (set in the dashboard settings). Pass a per-fileallowedOriginsonly to override it for that file (it replaces, not merges). - For
signed: the signature is the gate; a per-fileallowedOriginsis an optional extra domain lock layered on top.
Viewing a signed file
A signed file's url is not directly loadable — each view needs a fresh signed URL,
minted server-side with your API key secret. Use the one-call helper from the
server entry (Node only — never ship the secret to the browser):
import { getSignedDeliveryUrl } from '@ibanzajoe/uploader/server'
// in an authenticated backend route:
const url = getSignedDeliveryUrl({
handle: file.handle,
secret: process.env.UPLOADER_API_SECRET, // the API key's secret
baseUrl: 'https://edge.your-domain.com', // origin of FileResult.url
expiresIn: 3600, // minimum seconds valid (default 300)
// stableWindow: 3600, // URL is byte-identical within this window
// ops: [resize({ w: 400 }), output({ format: 'webp' })], // optional transform
})
// → https://edge.…/file/<handle>?policy=…&signature=… (hand to <img src>)Calling this per view is fine and expected. The expiry is quantized to
stableWindow (defaults to expiresIn), so repeated calls inside one window
return the exact same URL string — which is what lets the browser cache the
image instead of re-fetching it on every render. Without that, each render
produces a different URL, and to a browser a different URL is a different
image.
Longer windows cache better; shorter windows revoke sooner. Pass
stableWindow: 0 to opt out and get an exact per-call expiry.
Prefer to build it yourself? withSignedPolicy(url, { policy, signature }) from
@ibanzajoe/uploader/core appends a policy/signature pair you produced.
Theming
The picker is styled entirely from --uploader-* CSS custom properties, so a
third-party app can restyle it to match its own brand. There are two ways to do
it — pick whichever fits your stack.
1. The theme prop (scoped, per-instance)
Pass your design tokens as a plain object. They are applied as inline CSS variables on that component's root, so the override is scoped to that instance — two pickers on one page can carry different themes and nothing leaks to the rest of your app. Only the keys you set change; everything else falls back to the shipped defaults.
import { PickerOverlay } from '@ibanzajoe/uploader'
import '@ibanzajoe/uploader/styles.css'
<PickerOverlay
apikey="pk_…"
open={open}
onClose={() => setOpen(false)}
theme={{
accent: '#4f46e5', // your brand color
accentSoft: '#eef2ff', // tinted hover/icon background
accentRing: 'rgba(79,70,229,.28)',
radius: '12px',
font: "'Inter', sans-serif",
}}
/>DropPane takes the same theme prop. See the UploaderTheme type for the
full token list (accent set, surfaces, text, borders, semantic colors, shadows,
radius, font, transition). A themeToVars(theme) helper is also exported if you
want to apply the same variables to your own wrapper element.
2. CSS variables (global / page-level)
Override the variables in your own stylesheet to theme every instance at once:
:root {
--uploader-accent: #4f46e5;
--uploader-accent-soft: #eef2ff;
--uploader-radius: 12px;
}Dark mode is handled automatically when any ancestor has data-theme="dark".
Camera capture
Both <PickerOverlay> and <DropPane> can capture a photo straight from the
device camera. A Take photo button appears next to Browse files whenever
the browser supports getUserMedia and the picker accepts images. The capture
flows through the exact same pipeline as a dropped or browsed file — it lands in
the queue with a preview, can be cropped/rotated in the in-picker editor, and is
then uploaded normally.
Front or rear camera. On phones and tablets the picker opens the rear
camera; on desktops it opens the front one. Where the device exposes more than
one camera, a flip control in the corner of the viewfinder switches between them,
so the default is only the starting side. Set cameraFacingMode to pin it.
<PickerOverlay
apikey="pk_…"
open={open}
onClose={() => setOpen(false)}
pickerOptions={{
accept: ['image/*'],
// Sources offered to the user. Omit to offer everything supported.
fromSources: ['local_file_system', 'camera'],
// Omit for the per-device default (rear on mobile, front on desktop).
// 'user' = front/selfie (mirrored) · 'environment' = rear camera
cameraFacingMode: 'environment',
}}
/>Notes:
- The camera requires a secure context (HTTPS or
localhost) and user permission. Permission/no-camera errors are surfaced inline with a retry. - Pass
fromSources: ['local_file_system']to hide the camera even where it is supported; omitfromSourcesto offer it by default. - The flip control appears only when
enumerateDevices()reports two or more video inputs (checked after permission is granted, when the list is accurate). - The front camera is mirrored in both the preview and the captured still; the rear camera is not.
- The
<CameraCapture>component and theisCameraSupported()/shouldOfferCamera()/isMobileDevice()/defaultCameraFacingMode()helpers are exported for fully custom pickers.
Signed policies
When the account serves files in signed delivery mode (or requires signed uploads), attach a signed policy at the client level — it rides as auth on every request from that client:
const client = new UploaderClient({
apikey: 'pk_…',
apiUrl: 'https://your-api.example.com',
security: {
policy: 'base64-encoded-policy', // from YOUR backend
signature: 'hmac-sha256-hex-signature', // from YOUR backend
},
})For building read URLs of signed files, prefer the one-call
getSignedDeliveryUrl() from @ibanzajoe/uploader/server (see
Delivery protection → Viewing a signed file). It signs and
assembles the URL for you. If you'd rather bring your own policy/signature
(produced server-side — HMAC over the API key's secret; the SDK never signs in
the browser), append them with withSignedPolicy(url, { policy, signature }). Full
recipe + example:
docs/14 §7.3.
Building
npm run build # outputs to dist/ (ESM + CJS + types)