@nitida/asset-client 0.20.4 → 0.21.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.
package/src/index.ts CHANGED
@@ -378,6 +378,15 @@ export function hlsLadderAlignment(rungs: HlsRung[]): {
378
378
  */
379
379
  export type AssetDTO = {
380
380
  id: string;
381
+ /**
382
+ * ⭐ The same value as `id`, under the name the WRITE side uses.
383
+ *
384
+ * `upload()` returns `assetId`; the read endpoints returned only `id`, so
385
+ * what `assets.byHash()` handed back could not be fed to `assets.get()`
386
+ * without renaming a field. Optional here because a DTO produced by an older
387
+ * server will not carry it — read `dto.assetId ?? dto.id`.
388
+ */
389
+ assetId?: string;
381
390
  /** First 16 hex chars of sha256 — used to derive CDN URLs. */
382
391
  sha: string;
383
392
  kind: "image" | "video" | "document" | "audio" | "other";
@@ -611,6 +620,13 @@ type OriginalHints = {
611
620
  oext?: string | null;
612
621
  /** Full variant list — carries the stored URL verbatim. Authoritative. */
613
622
  variants?: AssetVariant[];
623
+ /**
624
+ * What the asset IS. Optional because these builders accept a minimal
625
+ * `{ sha }`, and absent means "we cannot know" — never "assume the worst".
626
+ * Used to refuse `hls` on something that can never have a ladder; see the
627
+ * `hls` branch of `getAssetUrl`.
628
+ */
629
+ kind?: AssetDTO["kind"];
614
630
  };
615
631
 
616
632
  /**
@@ -768,6 +784,33 @@ function buildPublicAssetUrl(
768
784
  // missing. Both are real; neither is a guess.
769
785
  const stored = asset.variants?.find((v) => v.preset === "hls")?.url;
770
786
  if (stored) return stored;
787
+ // No stored ladder. Before falling through to the transform route, refuse
788
+ // the kinds that can never HAVE one.
789
+ //
790
+ // `/t/format=hls/<sha>.m3u8` on an IMAGE does not 404 — measured against
791
+ // production 2026-08-25, it answers **200 with `content-type: image/webp`**
792
+ // (306 kB, cached `immutable` for a year) because the edge's transform
793
+ // planner short-circuits every image to the image pipeline before it ever
794
+ // looks at `format`. So the polite lie this branch was written to avoid
795
+ // comes back through the other door: not a 404, but a WebP wearing an
796
+ // `.m3u8` filename, which is worse — a player cannot even tell it failed.
797
+ //
798
+ // HLS is built when a VIDEO transcodes; audio gets `mp3`, images get the
799
+ // ladder of resized variants. The stored-URL check above runs FIRST on
800
+ // purpose: if a DTO really carries an `hls` variant, the data beats this
801
+ // inference and we hand it over whatever `kind` claims. We only refuse
802
+ // when we would otherwise fabricate a URL we already know is wrong.
803
+ if (asset.kind != null && asset.kind !== "video") {
804
+ throw new Error(
805
+ `${caller}: this asset is \`${asset.kind}\`, and only a video has an ` +
806
+ "HLS ladder. Asking for `hls` here would build " +
807
+ "`/t/format=hls/<sha>.m3u8`, which does NOT 404 — the edge answers " +
808
+ "200 with the image bytes under an .m3u8 name, so a player fails " +
809
+ "with no way to see why. For an image use a size preset " +
810
+ `(\`${caller}(asset, "md")\`) or a transform ` +
811
+ "(`getTransformUrl`); for audio use `mp3`.",
812
+ );
813
+ }
771
814
  return `${cdnBaseUrl}/t/format=hls/${asset.sha}.m3u8`;
772
815
  }
773
816
  return `${cdnBaseUrl}/${variantPrefix(caller)}${asset.sha}-${PRESET_SHORT[preset]}.${PRESET_EXT[preset]}`;