@nitida/sdk 0.30.0 → 0.30.2

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.
@@ -159,6 +159,10 @@ everyone, and the ladder buys you nothing.
159
159
  that your sources can sustain.
160
160
  (There IS a paid, separate `POST /assets/:id/upscale` that genuinely enlarges
161
161
  with a model. It is not the transform route and it is not free — §8g.)
162
+ - ⚠️ **There is no upscale — the master is the ceiling.** Measured: a 2400 px
163
+ master asked for `width=2560` returns **2400 px**. Put widths in the `srcSet`
164
+ that your sources can sustain — asking for more is not an error, it is a
165
+ smaller image than you think you requested.
162
166
  - ⚠️ The **first** request of each width is generated cold, then cached at the
163
167
  edge immutably. Warm them after upload if the first visitor matters.
164
168
 
@@ -223,6 +227,11 @@ Output: `https://8ok.uk/<tenantId.toString(36)>/v/<sha16>-v.mp4`.
223
227
 
224
228
  **Do NOT:**
225
229
  - ❌ Use `getVideoTransformUrl` — that builds a `/t/...` transform URL, which never returns playable video. On a video sha `/t/` transforms the **poster frame**: `200 image/webp` with `x-transform-source: poster` when a poster exists, `410` when it does not. (`getVideoTransformUrl` is for on-the-fly re-encodes, a different feature.)
230
+ - ❌ Use `getVideoTransformUrl` for the HLS entry — that builds a `/t/…`
231
+ transform URL. ⚠️ **A 410 there means the SOURCE BYTES ARE GONE**
232
+ (`source_unavailable`), **not** that you used the wrong builder. Do not go
233
+ change the call site while data loss goes unnoticed. (`getVideoTransformUrl`
234
+ is for on-the-fly re-encodes — a different feature.)
226
235
  - ❌ 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.
227
236
 
228
237
  **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.
@@ -278,15 +287,45 @@ The client's `PRESET_EXT.original` is the literal `"bin"`, so `getAssetUrl({sha}
278
287
  > getAssetUrl(asset, "original"); // → the stored key, verbatim
279
288
  > ```
280
289
  >
290
+ > ⭐ **And since `@nitida/sdk@0.30.0`, the result of `upload()` works too** —
291
+ > it carries `mime` and `oext`, so the obvious call is the correct one and you
292
+ > do not have to fetch the DTO first:
293
+ >
294
+ > ```ts
295
+ > const up = await nt.upload(file, { fileName, presets: ["original", "md"] });
296
+ > getAssetUrl(up, "original"); // → -o.jpg, not -o.bin
297
+ > ```
298
+ >
299
+ > Before 0.30.0 that call built `-o.bin` and 404'd over a file that was there.
300
+ > An agent running the getting-started doc verbatim found it on 2026-08-23.
301
+ >
281
302
  > `GET /assets/:id` now sends `variants` (the full list, URLs included) and `oext` (the extension
282
303
  > the original was really stored under). `getAssetUrl` prefers the stored URL, falls back to
283
304
  > `oext`, and only then guesses from the mime.
284
305
  >
285
306
  > **Why guessing could never work.** The server keys the original off the **uploaded filename's
286
- > extension**, which the mime does not determine. Measured over the 2 001 stored
287
- > originals in production: all **420** `image/jpeg` originals are `.jpg` and none are `.jpeg`, so
288
- > the old mime table 404'd on every JPEG; and **234** originals are `application/octet-stream`
289
- > (`.mpga`, `.docx`, `.m4a`), where no mime table can ever produce the right key.
307
+ > extension**, which the mime does not determine. `image/jpeg` originals are stored `.jpg` and
308
+ > never `.jpeg`, so a mime table that guessed `.jpeg` 404'd on every one of them.
309
+ >
310
+ > And a tail exists that **no** mime table can close: originals stored as
311
+ > `application/octet-stream`, where the mime says nothing about the extension.
312
+ > Re-measured 2026-08-24 over 3 211 live originals: **113** — and the split is
313
+ > the part worth carrying, because the total hides it:
314
+ >
315
+ > | | | |
316
+ > |---|---|---|
317
+ > | **109** | `.bin` in `sdk-e2e` | 29-byte degenerate PNG fixtures — test residue, and `bin` IS their extension, so the fallback is right |
318
+ > | **4** | `.docx` in `realtyone-cr` | the fallback builds `-o.bin` → **404**, while `-o.docx` → **200** |
319
+ >
320
+ > ⚠️ **This number used to read 234, and that was true until it wasn't.**
321
+ > `heal-misclassified-assets` (#245) reclassified 121 of them to their real
322
+ > mime — MP3s that had round-tripped through `.mpga`, WebPs uploaded with no
323
+ > declared type. 234 − 121 = 113. A published measurement with no date is a
324
+ > claim that decays silently; this one is dated, and so should the next.
325
+ >
326
+ > ⭐ **A fallback that is right 109 times out of 113 is the shape of bug that
327
+ > survives for months**, because almost every sample agrees with it. Only the
328
+ > four `.docx` ever fail, and only `oext` rescues them.
290
329
  >
291
330
  > Two things that did NOT change: existence still comes from `dto.presets` + `hasPreset` (the only
292
331
  > field on every response shape), and `getAssetUrl` was always correct for `video`/`poster`, whose
package/src/index.ts CHANGED
@@ -1684,7 +1684,13 @@ export class NitidaClient {
1684
1684
  sha: sha.slice(0, 16),
1685
1685
  mime: existing.mime ?? mime,
1686
1686
  oext: existing.oext ?? null,
1687
- cdnUrl: this.urlFor(existing, this.bestPresetForAsset(existing, mime)),
1687
+ // Same shape as the presign branch below, for the same reason. This
1688
+ // DTO does carry `sha` today — but relying on that is how the other
1689
+ // branch broke, and the value we hashed ourselves is authoritative.
1690
+ cdnUrl: this.urlFor(
1691
+ { ...existing, sha: sha.slice(0, 16) },
1692
+ this.bestPresetForAsset(existing, mime),
1693
+ ),
1688
1694
  };
1689
1695
  }
1690
1696
 
@@ -1706,13 +1712,38 @@ export class NitidaClient {
1706
1712
  ...(opts.video != null && { video: opts.video }),
1707
1713
  });
1708
1714
  if (presign.deduped) {
1715
+ const short = sha.slice(0, 16);
1709
1716
  return {
1710
1717
  assetId: presign.asset.id,
1711
1718
  sha256: sha,
1712
- sha: sha.slice(0, 16),
1719
+ sha: short,
1713
1720
  mime: presign.asset.mime ?? mime,
1714
1721
  oext: presign.asset.oext ?? null,
1715
- cdnUrl: this.urlFor(presign.asset, this.defaultPresetForMime(mime)),
1722
+ // ⭐ `{ ...asset, sha: short }`, never `asset` alone.
1723
+ //
1724
+ // The presign route answers a dedup hit with `sha256` and NO `sha` —
1725
+ // literally the shape `assertSha`'s message names. Handing it straight
1726
+ // to a URL builder threw:
1727
+ //
1728
+ // getAssetUrl: no usable `sha` … has `sha256` but not `sha` —
1729
+ // that is the shape `upload()` returns.
1730
+ //
1731
+ // …with `upload()` as BOTH the accuser and the caller. Found
1732
+ // 2026-08-24 by neo's hourly canary, ~16 alerts deep. Before
1733
+ // asset-client 0.18.1 the same line produced a `cdnUrl` with the word
1734
+ // `undefined` inside it, silently; the guard did its job and made a
1735
+ // latent defect loud.
1736
+ //
1737
+ // This branch is only reached when the asset EXISTS but is not
1738
+ // `ready` — the `byHash` branch above returns first otherwise — so
1739
+ // ordinary uploads never touched it and no test did either.
1740
+ //
1741
+ // The sha is not the DTO's to supply: we hashed the bytes ourselves at
1742
+ // the top of this method. Build from what we KNOW.
1743
+ cdnUrl: this.urlFor(
1744
+ { ...presign.asset, sha: short },
1745
+ this.defaultPresetForMime(mime),
1746
+ ),
1716
1747
  };
1717
1748
  }
1718
1749