@nitida/sdk 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,33 +1,68 @@
1
1
  ---
2
2
  name: nitida-sdk
3
- description: How to consume the nitida media platform (formerly aquienpz) (@nitida/sdk + @nitida/asset-client, CDN 8ok.uk) from an app — build image/video URLs, resolve stored refs, migrate an app off a legacy CDN, and provision a tenant. Use when wiring images/videos through aquienpz, debugging 400/404/410 on 8ok.uk URLs, or onboarding a new tenant. Captures the gotchas that bit real migrations (base36 video prefix, /t/ vs /v/, the width ladder, audio, the 0-cdn build gate).
3
+ description: How to consume the nitida media platform (@nitida/sdk + @nitida/asset-client, API api.nitida.gofuture.space, CDN 8ok.uk) from an app — build image/video URLs, resolve stored refs, migrate an app off a legacy CDN, and provision a tenant. Use when wiring images/videos through nitida, debugging 400/404/410 on 8ok.uk URLs, or onboarding a new tenant. Captures the gotchas that bit real migrations (base36 video prefix, /t/ vs /v/, the width ladder, audio, the 0-cdn build gate).
4
4
  ---
5
5
 
6
6
  # Consuming the nitida media platform
7
7
 
8
- > Durable source of truth: `aquienpz` repo `docs/SDK_CONSUMER_GUIDE.md`. This is a local copy for skill activation; update the repo doc when these learnings change.
8
+ > **The product is called nitida.** It used to be called *aquienpz*, and that name survives on
9
+ > purpose in three places that were NOT renamed: the GitHub repo (`aquienpz`), the CDN host
10
+ > (`8ok.uk`), and three packages that still publish under `@aquienpz/*` (`tenant-config`,
11
+ > `asset-uploader-web`, `asset-uploader-expo`). Everything else you install or import is
12
+ > `@nitida/*`.
9
13
 
10
- The platform stores assets in R2 and serves them through the CDN **`https://8ok.uk`**.
14
+ > Durable source of truth: **the published docs at <https://nitida.gofuture.space>** 187 pages, 166
15
+ > of them generated from the types, so the API reference cannot rot. Agents can download it: the site
16
+ > serves `/agents/skill.md`, `/agents/index.json`, `llms.txt` / `llms-small.txt` / `llms-full.txt`,
17
+ > and every page as raw `.md`. Source lives in `apps/nitida-docs`.
18
+ >
19
+ > ⚠️ An earlier version of this line pointed at `docs/SDK_CONSUMER_GUIDE.md`. **That file does not
20
+ > exist** (verified 2026-08-17) and did not when the line was written — a citation to nothing is
21
+ > worse than a wrong one, because it sends the reader hunting instead of correcting them. The three
22
+ > other docs this file cites (`ASSET_MANAGER_V2.md`, `CLIENT_OPERATIONS.md`, `TENANT_ONBOARDING.md`)
23
+ > were checked and DO exist. This file is mirrored to `~/.claude/skills/nitida-sdk/SKILL.md` for skill
24
+ > activation; update both when these learnings change.
25
+
26
+ The platform stores assets in R2, is driven through the API **`https://api.nitida.gofuture.space`**
27
+ and serves bytes through the CDN **`https://8ok.uk`**.
11
28
  Apps are **external consumers**: install the npm packages, never vendor neo's in-tree copies.
12
29
 
13
30
  ```bash
14
31
  bun add @nitida/sdk @nitida/asset-client
15
32
  ```
16
33
 
17
- Both packages export the same URL builders (`@nitida/sdk` re-exports `@nitida/asset-client`). For plain URL-building in a Next.js app, importing from `@nitida/asset-client` is enough; use `@nitida/sdk`'s `AquienpzClient` only when you also upload.
34
+ Both packages export the same URL builders (`@nitida/sdk` re-exports `@nitida/asset-client`). For plain URL-building in a Next.js app, importing from `@nitida/asset-client` is enough; use `@nitida/sdk`'s `NitidaClient` only when you also upload.
35
+
36
+ ```ts
37
+ import { NitidaClient } from "@nitida/sdk";
38
+
39
+ const nt = new NitidaClient({
40
+ endpoint: "https://api.nitida.gofuture.space",
41
+ apiKey: process.env.NITIDA_API_KEY!, // amk_rt_… — server-only
42
+ tenantCode: "your-code",
43
+ tenantId: 12,
44
+ });
45
+ ```
46
+
47
+ :::note[The old Cloud Run address still works, and always will]
48
+ Until 2026-08-17 the published endpoint was
49
+ `https://aquienpz-asset-manager-nlchzy26qa-uc.a.run.app`. The branded name is an **additional**
50
+ mapping onto the *same* Cloud Run service — same instance, no extra hop, byte-identical responses —
51
+ and Cloud Run keeps a service's generated URL forever. Nothing breaks if you are still on it; point
52
+ new work at `api.nitida.gofuture.space` and move the rest whenever it suits you.
53
+ :::
18
54
 
19
55
  ## 1. Configure once at module load
20
56
 
21
- The URL builders read **process-global** config. Pin it where your helpers live (a `lib/aquienpz-images.ts`), so every Server/Client Component that imports a builder is configured:
57
+ The URL builders read **process-global** config. Pin it where your helpers live (a `lib/nitida-images.ts`), so every Server/Client Component that imports a builder is configured:
22
58
 
23
59
  ```ts
24
60
  import { setCdnBase, setTenantId } from "@nitida/asset-client";
25
61
 
26
- const CDN = process.env.NEXT_PUBLIC_AQUIENPZ_CDN || "https://8ok.uk";
27
- const TENANT_ID = Number(process.env.NEXT_PUBLIC_AQUIENPZ_TENANT_ID) || /* your id */ 0;
62
+ const TENANT_ID = Number(process.env.NEXT_PUBLIC_NITIDA_TENANT_ID) || /* your id */ 0;
28
63
 
29
- setCdnBase(CDN); // default is already "https://8ok.uk"
30
- setTenantId(TENANT_ID); // REQUIRED for video URLs (base36 variant prefix) — see §3
64
+ setCdnBase("https://8ok.uk"); // OPTIONAL — this is already the default; set it only if you were given another host
65
+ setTenantId(TENANT_ID); // REQUIRED for video URLs (base36 variant prefix) — see §3
31
66
  ```
32
67
 
33
68
  `NEXT_PUBLIC_*` so the tenant id reaches the client bundle (hero/about videos render client-side). The runtime read-only consumer needs only these public vars — the `amk_rt_*` runtime key is for **uploads** (server-only), not for building URLs.
@@ -77,7 +112,31 @@ Output: `https://8ok.uk/<tenantId.toString(36)>/v/<sha16>-v.mp4`.
77
112
  - ❌ Use `getVideoTransformUrl` — that builds a `/t/...` transform URL, which **410s** for a stored video sha. (`getVideoTransformUrl` is for on-the-fly re-encodes, a different feature.)
78
113
  - ❌ Hand-roll the path with the decimal tenant id. The path segment is **base36**: `tenantId.toString(36)`. **Tenant 10 → `/a/v/`**, and the decimal `/10/v/` **404s**. This is invisible for tenants ≤ 9 (`8`→`8`, `9`→`9`) and bit a real migration only at tenant 10. Always delegate to `getAssetUrl` so the encoding can't drift.
79
114
 
80
- **Audio IS supported (updated 2026-07-01 — verify against the SDK types, this used to say "not supported").** The platform now recognizes `kind: "image" | "video" | "document" | "audio" | "other"` and ships an **`mp3`** variant preset (`VariantPreset` in `@nitida/asset-client`). Upload raw audio bytes with an explicit audio MIME so it stores as `kind:"audio"` (not `"other"`): `await aq.upload(bytes, { contentType: "audio/mpeg", fileName: "track.mp3" })`. Uploads are hash-deduped (byte-identical re-uploads return the existing sha — that's *byte* dedup, NOT semantic "find a similar track"). `aq.upload` also accepts an `audioTrack` on video-composition calls. Serve via the `mp3` preset / `original`. (Legacy `.m4a/.wav` with no matching pipeline still fall back to `original` raw bytes.) Confirm the current preset/kind list in `node_modules/@nitida/asset-client/dist/index.d.ts` before relying on a specific ext.
115
+ **Audio IS supported (updated 2026-07-01 — verify against the SDK types, this used to say "not supported").** The platform recognizes `kind: "image" | "video" | "document" | "audio" | "other"` and ships an **`mp3`** variant preset (`VariantPreset` in `@nitida/asset-client`). Uploads are hash-deduped (byte-identical re-uploads return the existing sha — that's *byte* dedup, NOT semantic "find a similar track"). `nt.upload` also accepts an `audioTrack` on video-composition calls. Serve via the `mp3` preset / `original`. Confirm the current preset/kind list in `node_modules/@nitida/asset-client/dist/index.d.ts` before relying on a specific ext.
116
+
117
+ > ✅ **FIXED 2026-08-17 (aquienpz #230/#231): you no longer have to pass `contentType` to avoid
118
+ > `kind:"other"`.** This section used to insist on
119
+ > `upload(bytes, { contentType: "audio/mpeg", fileName: "track.mp3" })` as a *defence*. Two
120
+ > independent holes made that necessary and both are closed:
121
+ >
122
+ > - **The mime round-trip lost the type.** Presign derives the stored extension with
123
+ > `mime.extension(body.mime)`, so `audio/mpeg` became `.mpga` — and the server's hand-written
124
+ > ext→mime `switch` did not know `.mpga`, so it came back as `application/octet-stream` ⇒
125
+ > `kind:"other"` ⇒ no variants. That swallowed **104 MP3 uploads**. `detectMime` now derives from
126
+ > the same `mime-types` table that produced the extension, closing the whole class by construction
127
+ > (7 broken round-trips fixed: `mpga`, `heif`, `adts`, `tif`, `bmp`, `svg`, `flac`), and
128
+ > `test/mime-roundtrip.test.ts` keeps it closed.
129
+ > - **Genuinely untyped uploads are rescued by their bytes.** When the key implies
130
+ > `application/octet-stream`, `/process` sniffs the magic bytes (`sniffMagicMime`) and processes
131
+ > the file as what it actually is.
132
+ >
133
+ > Passing `contentType` + `fileName` is still *good practice* — it is the cheapest possible signal
134
+ > and it decides the stored extension. It is no longer load-bearing.
135
+ >
136
+ > ⚠️ **Against an `asset-manager` older than 2026-08-17 this bug is live**, so code that must run
137
+ > against an old deploy should keep declaring the MIME explicitly. Also note the owner's decision:
138
+ > the **230 rows already misclassified** as `kind:"other"` are NOT being repaired — the fix is
139
+ > forward-only.
81
140
 
82
141
  ## 3b. Keeping the ORIGINAL bytes (backup, not delivery) ← learned the hard way 2026-08-07
83
142
 
@@ -106,30 +165,35 @@ variantKey(tenantId, sha256, "original", ext)
106
165
 
107
166
  The client's `PRESET_EXT.original` is the literal `"bin"`, so `getAssetUrl({sha}, "original")` **without `mime`** emits `-o.bin` while a PNG is really stored at `-o.png` → 404, always.
108
167
 
109
- > ⚠️ **CORRECTED 2026-08-15 the advice below used to be "read it from `aq.assets.variants()`",
110
- > and that method is now the one that fails.** Measured against the live deployment while building
111
- > `neo/apps/media-harness`:
168
+ > **FIXED 2026-08-17 (doc 240 §4.3 + §4.4). Pass the whole DTO and it is simply right:**
169
+ >
170
+ > ```ts
171
+ > const asset = await nt.assets.get(id);
172
+ > getAssetUrl(asset, "original"); // → the stored key, verbatim
173
+ > ```
112
174
  >
113
- > - **`aq.assets.variants(id)` returns `[]`** with a runtime key *and* with an admin key — while
114
- > the database row for the same asset holds two variants. Believing the empty array makes a
115
- > healthy asset look like it is missing everything.
116
- > - **`getAssetUrl` now takes the asset's `mime`** and resolves a real extension:
117
- > `getAssetUrl({ sha, mime }, "original")` → `…-o.webp`, HTTP **200**. The old `-o.bin` sentinel
118
- > only appears when you omit `mime`.
119
- > - **…but the extension the SERVER stored comes from the uploaded FILENAME, not the mime.**
120
- > `variantKey(..., ext)` uses `rawKey.split(".").pop()`. For JPEG these disagree —
121
- > `image/jpeg` → `jpeg` while a camera writes `.jpg`. Measured: `…-o.jpeg` **404**, `…-o.jpg` 200.
175
+ > `GET /assets/:id` now sends `variants` (the full list, URLs included) and `oext` (the extension
176
+ > the original was really stored under). `getAssetUrl` prefers the stored URL, falls back to
177
+ > `oext`, and only then guesses from the mime.
122
178
  >
123
- > **What to do:** existence from `dto.presets` (the compact codes, always populated — use
124
- > `hasPreset`), URL from `getAssetUrl({sha, mime}, "original")` with the **uploaded filename's
125
- > extension** swapped in if it differs, and then **verify with a real HEAD before you rely on it**.
126
- > Reference implementation: `apps/media-harness/scripts/seed-fixtures.ts` (`resolveOriginalUrl`).
127
- > (`getAssetUrl` stays correct for `video`/`poster`, whose extensions are fixed.)
179
+ > **Why guessing could never work.** The server keys the original off the **uploaded filename**
180
+ > (`rawKey.split(".").pop()`), which the mime does not determine. Measured over the 2 001 stored
181
+ > originals in production: all **420** `image/jpeg` originals are `.jpg` and none are `.jpeg`, so
182
+ > the old mime table 404'd on every JPEG; and **234** originals are `application/octet-stream`
183
+ > (`.mpga`, `.docx`, `.m4a`), where no mime table can ever produce the right key.
184
+ >
185
+ > Two things that did NOT change: existence still comes from `dto.presets` + `hasPreset` (the only
186
+ > field on every response shape), and `getAssetUrl` was always correct for `video`/`poster`, whose
187
+ > extensions are fixed.
188
+ >
189
+ > ⚠️ **Against an `asset-manager` older than 2026-08-17**, `variants` is `[]` and `oext` is absent
190
+ > — the mime guess is all you have, so HEAD the URL before relying on it. Reference implementation
191
+ > of the old dance: `apps/media-harness/scripts/seed-fixtures.ts` (`resolveOriginalUrl`).
128
192
 
129
193
  **3. Re-uploading does NOT repair a missing preset.** Ingest dedupes by hash and returns `deduped: true` with the existing asset; no variants are regenerated. To add a preset to an existing asset use `regenerate`, which MERGES:
130
194
 
131
195
  ```ts
132
- await aq.assets.regenerate(assetId, { presets: ["original"] });
196
+ await nt.assets.regenerate(assetId, { presets: ["original"] });
133
197
  ```
134
198
 
135
199
  ⚠️ **There is a time window.** `admin.routes.ts` notes that once the cleanup job reaps `raw/`, regenerate reads from `variants/o.<ext>` — which is exactly what's missing. **If the first ingest didn't request `original`, the raw bytes may already be unrecoverable.** In the 2026-08-07 run, `regenerate({presets:["original"]})` returned without error and the `original` variant still did not appear — consistent with `raw/` already being gone. Get `original` right on the FIRST ingest.
@@ -139,7 +203,29 @@ await aq.assets.regenerate(assetId, { presets: ["original"] });
139
203
  | Goal | presets | Where the URL comes from |
140
204
  |---|---|---|
141
205
  | Deliver images on a site | `["thumb"]` (+ what you need) | `getTransformUrl` / `getTransformSrcSet` |
142
- | **Archive the source file** | **`["original", "thumb"]`** | **`aq.assets.variants(assetId)` → `preset:"original"`** |
206
+ | **Archive the source file** | **`["original", "thumb"]`** | **`getAssetUrl(await nt.assets.get(id), "original")`** |
207
+
208
+ ### 3b-bis. `hasPreset` is the existence check, and it stopped lying too
209
+
210
+ `presets` is documented as a concatenation of **one-character** codes, and membership is a
211
+ one-character `includes`. It used to carry multi-character tokens as well (`transform-3da0019…`,
212
+ `probe`), and every one of them answered `true` for presets that do not exist. Counted on live
213
+ rows: **16 299** assets were told they had an `original` they did not have — sending callers to
214
+ exactly the 404 that `oext` came to remove — plus 16 479 phantom `aiproxy` and 487 phantom `sm`/`md`.
215
+
216
+ ✅ **Fixed 2026-08-17 (#228) on both sides:** the server emits only the 1-char vocabulary (plus the
217
+ sanctioned `mp3` token, which is deliberate and stays), and `hasPreset` strips multi-char tokens
218
+ before the `includes`. **`hasPreset(dto, preset)` remains the recommended existence check** — it is
219
+ the only field present on every response shape, including the slim list/resolver one that carries no
220
+ `variants` at all.
221
+
222
+ ⚠️ **Against an `asset-manager` older than 2026-08-17 the contaminated strings are still being sent**
223
+ (62 % of live rows had one). The client-side strip covers you; a hand-rolled
224
+ `dto.presets.includes("o")` does not.
225
+
226
+ ⚠️ **`mp3` is a real, multi-char token in the vocabulary on purpose.** Any substring test you write
227
+ yourself must discount it — otherwise the `m` of `mp3` reads as `md`. That is a live bug class in
228
+ consumer code, not in the SDK.
143
229
 
144
230
  ## 3c. PLAYING a video — HLS, and the snippet that went stale in April 2026
145
231
 
@@ -226,33 +312,32 @@ const sha = extractAssetSha(storedUrl); // → "e0ef99988f4a3c9b" | null
226
312
 
227
313
  A robust app resolver tries, in order: an explicit `sha` on the row → a migrated-metadata blob → a migration-map lookup by old URL → `extractAssetSha` → raw fallback.
228
314
 
229
- ## 5. Migrating an app OFF a legacy CDN onto aquienpz
315
+ ## 5. Migrating an app OFF a legacy CDN onto nitida
230
316
 
231
317
  Pattern proven on carniceria, novatibas, sueños (all off `cdn.espaciofuturo.io`):
232
318
 
233
- 1. **Re-ingest is required, not a host rename.** Legacy hashes (64-hex sha256) ≠ aquienpz (16-hex sha). You must **fetch each legacy asset's bytes and re-upload** to the tenant (SDK server client + `amk_rt_*` key), producing a NEW sha. Emit an idempotent `old-URL → { sha, w, h, blur, kind }` map (`scripts/asset-migration-map.json`). Upload presets: images→`thumb`, videos→`poster`+`video`. Dedupe by sha; checkpoint (re-runnable).
319
+ 1. **Re-ingest is required, not a host rename.** Legacy hashes (64-hex sha256) ≠ nitida (16-hex sha). You must **fetch each legacy asset's bytes and re-upload** to the tenant (SDK server client + `amk_rt_*` key), producing a NEW sha. Emit an idempotent `old-URL → { sha, w, h, blur, kind }` map (`scripts/asset-migration-map.json`). Upload presets: images→`thumb`, videos→`poster`+`video`. Dedupe by sha; checkpoint (re-runnable).
234
320
  2. **Regenerate metadata** (`blur`/`w`/`h`/`aspectRatio`) from the uploaded asset (`assets.get`) — don't carry legacy values.
235
321
  3. **Render: bake resolved `8ok.uk` URLs at author time.** Do NOT `import` the map JSON into runtime code — its old-URL *keys* ship into the client/server bundle and fail a "0 legacy refs in built output" check. Resolve at build/author time and write the literal `8ok.uk` URL.
236
322
  4. **Next.js**: add `8ok.uk` to `images.remotePatterns`. With `images.unoptimized: true`, pass the CDN srcSet straight through. With Next optimization ON, set `unoptimized` **per slot** for CDN-backed images to avoid paying for double optimization.
237
323
  5. **Completeness sweep before declaring done**: `grep cdn-host` must be 0 in (a) rendered HTML of every route, (b) serialized/embedded JSON payloads (this is where a stale ref hides even when the *image* renders correctly), (c) JSON-LD / OG / preconnect, (d) any **DB mirror table** (an app DB's `assets`/asset tables are easy to miss), and (e) **cached API responses** (a CDN/data cache can persist a pre-cutover payload across deploys → bust the tag, don't assume redeploy clears it). "Images load from 8ok.uk" is NOT sufficient proof.
238
324
 
239
- ## 6. Provisioning a new tenant (ops / WS-0)
325
+ ## 6. Provisioning a new tenant (ops)
240
326
 
241
- ```bash
242
- # Secrets live in GCP Secret Manager, project `naye-tours`:
243
- # PLATFORM_DATABASE_URL (platform Neon), ASSET_AUTH_SECRET (better-auth)
244
- # 1) Insert tenant rows (idempotent) into the platform Neon:
245
- # SELECT MAX(id)+1 FROM public.tenants; -- pick next id
246
- # INSERT public.tenants (id, code, vertical, name) ...
247
- # INSERT public.tenant_config (tenant_id, code, vertical, display_name,
248
- # storefront_origins, brand_json, feature_flags, qdrant_tenant_id) ...
249
- # 2) Mint keys (prints runtime/admin/ci ONCE — store immediately):
250
- cd apps/asset-manager
251
- PLATFORM_DATABASE_URL=… ASSET_AUTH_SECRET=… bun run scripts/bootstrap-project.ts <tenant-code>
252
- ```
327
+ **Use the console: [`https://console.gofuture.space`](https://console.gofuture.space)** (sign in with
328
+ Google). *Tenants New* creates the tenant rows, and the tenant's page **issues, shows and rotates**
329
+ its runtime / admin / CI keys. A key's secret is displayed **once**, at issue time — store it then.
330
+
331
+ That replaces `apps/asset-manager/scripts/bootstrap-project.ts` and the hand-written SQL that used to
332
+ live here. The point is not convenience: **provisioning no longer requires the production database
333
+ URL** (`PLATFORM_DATABASE_URL`) or `ASSET_AUTH_SECRET` on somebody's laptop. Reach for the script
334
+ only if the console is down, and treat that as an incident, not a workflow.
253
335
 
254
- - Endpoint = the asset-manager Cloud Run URL. Verify: `GET /usage` with `Authorization: Bearer <amk_rt_*>` + `X-Tenant-Code: <code>` → `{ "tenant": { "id": N } }`.
255
- - The `@aquienpz/tenant-config` HTTP layer **caches tenant lookups for 60s** — a fresh tenant resolves in services within a minute (the `bootstrap` script reads the DB directly, so it works immediately).
336
+ - Endpoint = `https://api.nitida.gofuture.space` (the Cloud Run generated URL keeps working see the
337
+ note at the top). Verify a fresh tenant: `GET /usage` with `Authorization: Bearer <amk_rt_*>` +
338
+ `X-Tenant-Code: <code>` → `{ "tenant": { "id": N } }`.
339
+ - The `@aquienpz/tenant-config` HTTP layer (still published under the old scope) **caches tenant
340
+ lookups for 60s** — a fresh tenant resolves in services within a minute.
256
341
  - Asset-only consumers (a storefront with its own catalog DB) can skip the Qdrant/voice/scraper steps in `docs/TENANT_ONBOARDING.md` — only the two tenant rows + keys + `storefront_origins` are needed.
257
342
 
258
343
  ## Quick reference
@@ -297,11 +382,12 @@ Measured 2026-08-15 building `neo/apps/media-harness`, the bench for this SDK. R
297
382
  | `process returned no assetId` | fixed 2026-08-16 — you are on an `asset-manager` older than that deploy, §7.2 |
298
383
  | `waitReady timeout` on a video | a transcode + HLS ladder takes 1–2 min; the default `timeoutMs` is 5 min but a 4K source can beat it. Raise it, §7.2 |
299
384
  | `R2 PUT failed: network error` | your origin is not in the R2 BUCKET's CORS policy, §7.1 |
300
- | `aq.assets.variants()` returns `[]` | current deployment does not populate it, even for admin keys. Use `dto.presets` + `hasPreset` |
385
+ | `nt.assets.variants()` returns `[]` | fixed 2026-08-17 (#225) you are on an `asset-manager` older than that deploy. Existence from `dto.presets` + `hasPreset` works on every version |
386
+ | `hasPreset` says a preset exists and its URL 404s | fixed 2026-08-17 (#228) — an old server sends multi-char tokens in `presets`; a current `@nitida/asset-client` strips them, a hand-rolled `includes` does not, §3b-bis |
301
387
  | `tenant_not_found` from a service | tenant rows missing, or the 60s tenant-config cache is stale |
302
- | `original` 404s | (a) never written — `presets` omitted `"original"` at ingest, or the asset is a VIDEO (always dropped, §7.3); (b) `getAssetUrl(...,"original")` called WITHOUT `mime` → the `-o.bin` sentinel; (c) called WITH `mime` but the object was stored under the uploaded filename's extension (`-o.jpg`, not `-o.jpeg`). See §3b — HEAD it |
388
+ | `original` 404s | (a) never written — `presets` omitted `"original"` at ingest, or the asset is a VIDEO (always dropped, §7.3); (b) `getAssetUrl` called with a bare `{sha}` → the `-o.bin` sentinel; (c) called with `{sha, mime}` against an old server, so it guessed the extension. Pass the whole DTO from `nt.assets.get(id)` see §3b |
303
389
  | "uploaded OK" but nothing to restore | `["thumb"]`-style presets store no original; verify by SHA-256 round-trip, not by upload count — see §3b |
304
- | audio won't serve | stored as `kind:"other"` — re-upload with an explicit `contentType:"audio/mpeg"` so it's `kind:"audio"` + gets the `mp3` preset (audio IS supported as of 2026-07-01; see §3) |
390
+ | audio stored as `kind:"other"` | fixed 2026-08-17 (#230/#231) the ext→mime round-trip lost `.mpga`, and untyped bytes are now sniffed. Against an older `asset-manager`, upload with an explicit `contentType:"audio/mpeg"`. Rows misclassified **before** the fix stay that way by decision, §3 |
305
391
 
306
392
  > Source of truth for deeper detail: `docs/ASSET_MANAGER_V2.md`, `docs/CLIENT_OPERATIONS.md`, `docs/TENANT_ONBOARDING.md`, and `packages/asset-client/src/{transform.ts,index.ts}`.
307
393
 
package/src/index.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  * import { NitidaClient } from "@nitida/sdk";
14
14
  *
15
15
  * const aq = new NitidaClient({
16
- * endpoint: "https://aquienpz-asset-manager-xxx.run.app",
16
+ * endpoint: "https://api.nitida.gofuture.space",
17
17
  * apiKey: process.env.ASSET_MANAGER_RUNTIME_KEY!,
18
18
  * tenantCode: "realtyone-cr",
19
19
  * cdnBase: "https://8ok.uk", // optional override
@@ -126,7 +126,7 @@ export type NitidaClientOptions = {
126
126
  /**
127
127
  * Build a fully-qualified URL for an aquienpz endpoint path.
128
128
  *
129
- * Accepts both absolute endpoints (`https://aquienpz...run.app`) and
129
+ * Accepts both absolute endpoints (`https://api.nitida.gofuture.space`) and
130
130
  * relative ones (`/api/am`) — the latter only works in browser contexts
131
131
  * (resolved against `window.location.origin`). Node/Bun throws a clear
132
132
  * error if a relative endpoint is configured.
@@ -570,6 +570,12 @@ class AssetsApi {
570
570
  *
571
571
  * const v = await aq.assets.variants(logoId);
572
572
  * v.map((x) => x.preset); // → ("thumb" | "sm" | … | "original")[]
573
+ *
574
+ * ⚠️ Returns `[]` — not an error — against an `asset-manager` older than the
575
+ * 2026-08-17 deploy, which never sent the field (doc 240 §4.3). An empty
576
+ * array is therefore "no variants OR old server". For a plain existence
577
+ * check prefer `hasPreset(dto, preset)` on `dto.presets`, which every server
578
+ * version sends; use this when you need the URLs and sizes.
573
579
  */
574
580
  async variants(assetId: string): Promise<AssetVariant[]> {
575
581
  const dto = await this.get(assetId);