@mad-core/sdk
v0.1.1
Published
Typed TypeScript SDK for the MAD media server management API — upload, assets, exports and galleries, with full OpenAPI-contract coverage via a typed escape hatch. Isomorphic (Node 20+ and browser).
Maintainers
Readme
@mad-core/sdk
Typed TypeScript client for the MAD media server's management plane — upload, list,
edit, export and organise assets programmatically. It is the counterpart to
@mad-core/react (which builds delivery URLs and renders media): this package
talks to the API.
- Typed against the contract. Types are generated from the committed
openapi.json, so every one of the API's operations is reachable and typed via themad.rawescape hatch. - Ergonomic where it counts. A thin hand-written layer covers the integration happy-path —
upload/uploadMany,assets,exports,galleries— adding auth injection, a typed error (MadError), the 409-as-result upload semantics, multipart, and pagination as an async-iterator. - Isomorphic. Runs in Node 20+ and the browser. The only Node-specific piece (upload from a
file path) lives in the
@mad-core/sdk/nodesubpath; the main entry never importsnode:fs.
npm install @mad-core/sdkQuick start
import { createMadClient } from "@mad-core/sdk";
const mad = createMadClient({
baseUrl: "https://mad.example.com", // omit for same-origin (browser)
apiKey: process.env.MAD_API_KEY!,
});
// Upload a File/Blob (e.g. from an <input type="file">)
const result = await mad.upload({ file, namespace: "marketing", tags: ["hero"] });
if (result.duplicate) {
console.log("already exists:", result.existing.slug);
} else {
console.log("uploaded:", result.asset.slug);
}
// List with typed filters
const { images, pagination } = await mad.assets.list({ type: "image", liked: "true" });
// Iterate every matching asset across all pages
for await (const asset of mad.assets.listAll({ namespace: "marketing" })) {
console.log(asset.slug);
}Uploading
mad.upload(input) POSTs one File/Blob as multipart. A 409 duplicate is returned, not
thrown (the global content-hash dedup); any other non-ok status throws a MadError.
const r = await mad.upload({
file, // File | Blob
filename: "photo.png",// required for a bare Blob (the server derives the slug from it)
name: "Hero photo", // asset display name
namespace: "marketing",
tags: ["hero", "banner"],
altText: "A hero banner",
});mad.uploadMany(files, opts) orchestrates many single uploads client-side with bounded
concurrency, resilient per file — a duplicate or a single failure never aborts the batch — and
is resumable (re-running reports already-uploaded files as duplicates).
const summary = await mad.uploadMany(files, {
concurrency: 4,
namespace: "marketing",
onProgress: ({ completed, total }) => console.log(`${completed}/${total}`),
});
// summary: { uploaded, duplicates, failed, outcomes }From disk (Node)
import { uploadFile, uploadFiles } from "@mad-core/sdk/node";
await uploadFile(mad, "./photo.png", { namespace: "marketing" });
await uploadFiles(mad, ["./a.png", "./b.jpg"], { concurrency: 4 });Files are streamed from disk (fs.openAsBlob) — they are never read fully into memory.
Assets, exports, galleries
const detail = await mad.assets.get(id);
await mad.assets.update(id, { name: "Renamed", tags: ["x"] });
await mad.assets.delete(id); // soft-delete (trash)
await mad.assets.delete(id, { permanent: true });
const restored = await mad.assets.restore(id); // 409 conflict returned as { restored: false, conflict }
const job = await mad.exports.create({ namespace: "marketing", type: "image" });
const status = await mad.exports.get(job.jobId);
const zip = await mad.exports.download(job.jobId, token); // public token link, no auth header
const gallery = await mad.galleries.create({ name: "Spring 2026" });
await mad.galleries.setItems(gallery.id, [id1, id2]); // idempotent PUT
const fit = await mad.galleries.suggestions(gallery.id, [id1, id2]); // needs the semantic_search featureErrors
Every operation (except the documented 409-as-result cases) throws a MadError on a non-ok
response. Branch on the typed code, never the message:
import { MadError } from "@mad-core/sdk";
try {
await mad.assets.get("missing");
} catch (err) {
if (err instanceof MadError) {
console.log(err.status, err.code, err.message, err.details);
if (err.code === "VALIDATION_ERROR") console.log(err.issues); // [{ path, message }]
if (err.retryable) {/* 503 / STORAGE_UNAVAILABLE / SERVER_BUSY — safe to retry */}
}
}A non-JSON error body (e.g. an ALB HTML 502/504) yields a generic MadError carrying the
status, never a parse crash.
The escape hatch: mad.raw
Any operation the ergonomic layer doesn't wrap (folders, workflows, trash, features, watermark,
OG templates, API keys, LUTs, namespaces…) is reachable, fully typed, via the underlying
openapi-fetch client:
const { data, error } = await mad.raw.GET("/api/folders");
await mad.raw.POST("/api/folders", { body: { name: "Campaigns" } });mad.raw uses the same auth and base URL as the ergonomic methods. Unlike them, it follows the
openapi-fetch convention of returning { data, error } rather than throwing.
Cancellation
Every operation accepts an AbortSignal:
const controller = new AbortController();
const p = mad.assets.list({ type: "image" }, { signal: controller.signal });
controller.abort();