@khalidadil/filebot-client (0.1.0)
Installation
@khalidadil:registry=npm install @khalidadil/filebot-client@0.1.0"@khalidadil/filebot-client": "0.1.0"About this package
@khalidadil/filebot-client
Browser Filebot client: a plan-driven Uppy uploader (direct presigned multipart PUTs, resume, live status), the host-route client, an upload API, an SSE subscriber, a list + gallery store, <img> srcset helpers, and a QR desktop helper. It holds no server secrets — it only ever sees host-minted grants, per-upload fuk_ keys and server-issued signed bases.
Publishes to the private Gitea registry https://git.kadil.dev/api/packages/khalidadil/npm/ (never GitHub), installable with no token.
pnpm add @khalidadil/filebot-client
Node >=20.19 for the toolchain; ships browser ESM. Runtime deps: @uppy/core, @uppy/aws-s3, @uppy/golden-retriever (all v5), uqr, plus the two internal packages.
The uploader
createUploader wires an Uppy instance to your host's file router: a pre-processor batches an init call, @uppy/aws-s3 runs the presigned part PUTs from the returned plan, a post-processor finalizes, and Golden Retriever + a reconciliation pass resume interrupted uploads across reloads. Pause / resume / retry / remove stay on Uppy itself (uppy.pauseAll(), resumeAll(), retryUpload(), removeFile(); removing a file aborts its Filebot upload).
import { createUploader, routeTokenSource } from "@khalidadil/filebot-client";
const uploader = createUploader({
getToken: routeTokenSource({ mount: "/api/filebot", route: "avatars" }), // POSTs your host route
storeName: "avatars", // Golden Retriever namespace (= Uppy id); ^[A-Za-z0-9_:.-]{1,64}$, default "filebot"
maxFilesPerInit: 1, // integer 1..20, default 20; 1 = one Idempotency-Key per file
});
uploader.onUploadAccepted((e) => console.log("accepted", e.fileId, e.uploadId));
uploader.onRejected((e) => console.warn("rejected", e.uppyFileId, e.code, e.retryable));
uploader.onRestored((files) => reconcile(files)); // after a reload, ghosts included
uploader.onFileStatus((e) => setStatus(e.uppyFileId, e.status)); // ready / failed / quarantined
uploader.uppy.addFile({ name: file.name, type: file.type, data: file });
// later
uploader.destroy(); // idempotent: removes listeners, stops timers + SSE, destroys an Uppy it created
token-tier uploads cannot choose preserveQuality — the bucket default applies. HEIC and other files a browser reports with an empty type are typed by inferMime(name, type) before init.
The embedder surface (web-apps X-4)
This is the exact surface the apparel-designer capture flow (upload-engine.ts / uploader-adapter.ts) is built on — no host-specific extensions were needed. The mapping:
| Uploader concept | X-4 embedder use |
|---|---|
storeName |
one namespace per capture surface (isolates Golden Retriever state) |
maxFilesPerInit: 1 |
one file per init, one Idempotency-Key per file |
getToken |
routeTokenSource (host route) or staticTokenSource (a pre-minted token) |
onUploadAccepted(e) |
Filebot accepted the file — fires once per (uppyFileId, uploadId); a completing retry, resume or replayed init never double-fires |
onRejected(e) |
init refused the file (code, retryable); a Filebot ErrorCode, client_outdated or invalid_state |
onRestored(files) |
resume reconciliation result: each file's outcome is resumable / completed / reinit / pending_init, ghost: true when the blob must be re-selected |
onFileStatus(e) |
post-ready lifecycle: ready / failed / quarantined |
Uppy pauseAll / resumeAll / retryUpload / removeFile |
the host's own pause / resume / retry / remove controls |
Every on* returns an unsubscribe; a throwing listener never breaks an upload.
Live status without a host route
The token-tier init response carries a sess.<sessionId> events ticket, so the uploader streams its own session over SSE with no host /events-ticket call (security-14). Node has no global EventSource; inject one where needed:
import { EventSource } from "eventsource";
createUploader({ getToken, EventSource });
Subscribe directly with subscribeEvents({ url, ticket, EventSource }) for custom flows.
Lists, galleries and media URLs
import { listFiles, createGalleryStore, mediaSrcset, mediaUrl } from "@khalidadil/filebot-client";
const page = await listFiles({ mount, identifier: "album:42" }); // one host-minted list token
const store = createGalleryStore({ mount, identifier: "album:42" }); // paging + subscribe
const { src, srcSet } = mediaSrcset(signedBase, oid, { preset: "card" }); // falls back to a ladder width
const full = mediaUrl(signedBase, oid, { rendition: "o" });
QR desktop (phone → desktop)
import { startQrSession } from "@khalidadil/filebot-client";
const qr = await startQrSession({ mount, route: "avatars" });
render(qr.url); // the phone scans this
qr.subscribe((state) => update(state.counts)); // uploaded / ready counts stream over the QR ticket
Errors
FilebotClientError carries a numeric HTTP-ish status plus a code, detail and retryable; parseErrorResponse(res) builds one from a host/API error body. Secrets are never placed in message text.
Testing
pnpm --filter @khalidadil/filebot-client test runs the unit suite under happy-dom (no services). test/pack.test.ts asserts the published tarball ships dist/** + README.md + package.json only with the exact runtime deps. uppy-pin.test.ts fails loudly if an Uppy bump changes the chunk loop or processor API. See Features/SDKs.md in the vault for the full testing and journey-1 story.
Release
Part of the five-package lockstep set; packages/filebot-server/scripts/lockstep-check.ts prints (never runs) the gated publish. Publishing needs the lead's go and Khalid's approval.
Dependencies
Dependencies
| ID | Version |
|---|---|
| @khalidadil/filebot-contracts | 0.1.0 |
| @khalidadil/filebot-signing | 0.1.0 |
| @uppy/aws-s3 | ^5.1.0 |
| @uppy/core | ^5.2.0 |
| @uppy/golden-retriever | ^5.2.1 |
| uqr | 0.1.3 |
Development Dependencies
| ID | Version |
|---|---|
| @types/node | ^22.10.0 |
| happy-dom | ^20.14.5 |
| tsx | ^4.19.2 |
| typescript | ^5.7.0 |