@nitida/asset-client
v0.24.1
Published
nitida URL builders — construct image, video and HLS URLs for the nitida CDN. No network, no key, no config beyond a tenant id.
Downloads
4,231
Maintainers
Readme
@nitida/asset-client
URL builders for assets stored on the nitida platform and served from https://8ok.uk.
No network calls, no API key, no side effects — it turns a stored sha into a URL. If you also need
to upload, that is @nitida/sdk (which re-exports everything here).
bun add @nitida/asset-clientWhere these symbols live
Everything documented here is exported by @nitida/asset-client, and
every one of those exports is re-exported by the root @nitida/sdk — if
you already installed the SDK, importing from its root works and needs no second
dependency.
@nitida/sdk/server and @nitida/sdk/web carry the full set as well, so any of
the three works. ⚠️ Not so before 2026-08-21 — /server was short 28 of the
root's exports and /web short 37. On an older SDK, import these from the root
or from this package.
Configure once, at module load
The builders read process-global config, so pin it where your helpers live — every Server and Client Component that imports a builder is then configured.
import { setCdnBase, setTenantId } from "@nitida/asset-client";
setCdnBase("https://8ok.uk"); // already the default
setTenantId(12); // REQUIRED for video/variant URLs — see belowImages — on-the-fly transforms
import { getTransformSrcSet, getTransformUrl, type TransformWidth } from "@nitida/asset-client";
getTransformUrl({ sha }, { format: "webp", width: 640 });
// → https://8ok.uk/t/format=webp,width=640/<sha16>.webp
getTransformSrcSet({ sha }, [640, 960, 1280], { format: "webp" });⚠️ Widths must be on the unsigned ladder TRANSFORM_WIDTHS — generated from the source, do not
edit by hand:
96, 128, 160, 180, 240, 256, 320, 400, 480, 600, 640, 800, 960, 1080, 1200, 1280, 1440, 1600, 1920, 2560, 3840 — 21 widths.
Anything else is HTTP 400 at the edge — it is a DoS guard, not a bug. Import the TransformWidth
type and an off-ladder number becomes a compile error instead of a runtime 400.
Video — the stored variant, not a transform
import { getAssetUrl, setTenantId } from "@nitida/asset-client";
setTenantId(tenantId);
getAssetUrl({ sha }, "video"); // → https://8ok.uk/<tenantId base36>/v/<sha16>-v.mp4⚠️ The tenant segment is base36, not decimal. Tenant 10 is /a/, tenant 12 is /c/; /12/v/
404s. This is invisible for tenants ≤ 9 and bit a real migration exactly once — so always let
getAssetUrl build the path.
⚠️ Never getVideoTransformUrl for a stored video. That builds a /t/… transform URL, which
does NOT give you the video. On a video sha it transforms the poster frame
and answers 200 image/webp (x-transform-source: poster) — useful, but an
image. If that video has no poster it answers 410. Either way you never get
playable video out of /t/; use getAssetUrl(asset, "video").
The original variant
Pass the asset's mime or you get the "bin" sentinel:
getAssetUrl({ sha }, "original"); // → …-o.bin ❌ 404, always
getAssetUrl({ sha, mime }, "original"); // → …-o.webp ✓⚠️ And the extension the SERVER stored comes from the uploaded filename, not the mime. For JPEG
those disagree (image/jpeg → jpeg, but a camera writes .jpg) — measured, -o.jpeg 404s
while -o.jpg is 200. If the URL must be right, verify it with a HEAD before relying on it.
Asking for a preset vs. a preset existing
VariantPreset is what a variant can be. RequestablePreset is what you
can ask for. They overlap in nine values and differ in three:
hlsandmp3exist but cannot be ordered — the ladder is built when a video transcodes, the mp3 is emitted alongside any audio original.probecan be ordered but never reaches the compactpresetsstring, so it is not aVariantPreset.
Sending either of the first two answers HTTP 400; the write methods now take
RequestablePreset[], so it is a compile error instead. A CI check reads the
server's own schemas and asserts the type still matches them.
Which presets exist
Read dto.presets — the compact code string ("oq" = original + thumb, "pv" = poster + video) —
via hasPreset, not the variants array:
import { hasPreset } from "@nitida/asset-client";
if (hasPreset(asset, "thumb")) { /* … */ }GET /assets/:id sends the full variants array (URLs included) as of the 2026-08-17 deploy, but the
slim list and slot-resolver shapes never carry it, and an older server deploy returns it empty on every
shape. presets is populated everywhere, on every version — so read existence from presets and use
variants only for URLs.
| preset | code | ext | max dim |
|---|---|---|---|
| thumb | q | webp | 256 (square cover) |
| sm / md / lg / xl | s m l x | webp | 640 / 1280 / 1920 / 3840 |
| original | o | source ext | — |
| poster / video / aiproxy | p v a | webp / mp4 / mp4 | — |
| mp3 | — | mp3 | — |
Troubleshooting
| Symptom | Cause |
|---|---|
| 400 on an image URL | width not on TRANSFORM_WIDTHS |
| 410 on a video /t/ URL | that video has no poster variant. With one, /t/ returns the poster as an image — never the video. Use getAssetUrl(asset, "video") |
| 404 on a video URL | decimal tenant prefix instead of base36 |
| original 404s | no mime passed, or the stored ext came from the filename (-o.jpg vs -o.jpeg), or it was never written |
Deeper guidance — including uploads, tenant onboarding and the CORS boundary — lives in
@nitida/sdk's skills/nitida-sdk/SKILL.md, which ships inside that package.
License
MIT — see LICENSE.
⚠️ The licence covers this client code, not the service. MIT lets you use, modify and redistribute the SDK; it does not grant access to nitida. The service is governed by its own terms and plan tiers, and every call still needs credentials issued to your project. Same shape as every other API client you already depend on.
