@nitida/asset-client 0.19.1 → 0.20.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
@@ -496,8 +496,27 @@ export function getCdnBase(): string {
496
496
  // `variantKey`). Variant URL builders MUST include that prefix or every
497
497
  // URL 404s. The tenant id is process-global (one tenant per client/app),
498
498
  // set once at boot — `NitidaClient` does this from its `tenantId` option;
499
- // standalone consumers call `setTenantId()` directly. Left unset, builders
500
- // fall back to the legacy pre-cutover bare path for back-compat.
499
+ // standalone consumers call `setTenantId()` directly.
500
+ //
501
+ // ⭐ THE BACK-COMPAT FALLBACK FELL BACK TO A 404, SO IT IS GONE
502
+ //
503
+ // This used to return `""` when no tenant was set, "for back-compat with the
504
+ // legacy pre-cutover bare path". Measured 2026-08-24: **nothing serves that
505
+ // shape.** The edge parser understands exactly two schemes —
506
+ // `<tid_b36>/<v|r>/<sha…>` and the literal `raw/…` prefix (`legacyRawSha16`)
507
+ // — and a bare `<sha16>-<preset>.<ext>` matches neither. So the fallback was
508
+ // not back-compat; it was a silent 404 generator wearing its name.
509
+ //
510
+ // It cost more than a 404. The README documented the bare shape as THE CDN
511
+ // URL format, with a worked example that 404s, and the sentence "the same
512
+ // bytes produce the same URL regardless of which tenant uploaded them" —
513
+ // which stopped being true at the cutover. Code and docs agreed with each
514
+ // other and disagreed with the CDN, which is the hardest kind of wrong to
515
+ // find: nothing contradicts you until a user opens the URL.
516
+ //
517
+ // Now it throws, like `getPrivateTransformUrl` already did for the same
518
+ // missing config. Same reason as `assertSha`: a builder that cannot produce a
519
+ // working URL must say so, not hand back a polite lie.
501
520
  // ---------------------------------------------------------------------------
502
521
 
503
522
  let tenantId: number | null = null;
@@ -509,9 +528,23 @@ export function setTenantId(id: number | null | undefined): void {
509
528
  export function getTenantId(): number | null {
510
529
  return tenantId;
511
530
  }
512
- /** Variant path prefix `<tid b36>/v/`, or "" when no tenant is configured. */
513
- function variantPrefix(): string {
514
- return tenantId != null ? `${tenantId.toString(36)}/v/` : "";
531
+ /**
532
+ * Variant path prefix `<tid b36>/v/`.
533
+ *
534
+ * @throws when no tenant is configured — see the note above for why this is
535
+ * not a "" fallback.
536
+ */
537
+ function variantPrefix(caller: string): string {
538
+ if (tenantId == null) {
539
+ throw new Error(
540
+ `${caller}: no tenant is configured, so every variant URL would 404. ` +
541
+ "Call setTenantId(id) once at boot, or construct a NitidaClient with " +
542
+ "`tenantId` (it does this for you). The tenant is part of the CDN " +
543
+ "path (`<cdn>/<tenantId base36>/v/<sha16>-<preset>.<ext>`) and cannot " +
544
+ "be guessed from the asset.",
545
+ );
546
+ }
547
+ return `${tenantId.toString(36)}/v/`;
515
548
  }
516
549
 
517
550
  // ---------------------------------------------------------------------------
@@ -646,7 +679,7 @@ export function getAssetUrl(
646
679
  // untouched and no existing behaviour changes.
647
680
  const fallback = transformFallbackFor(asset, preset);
648
681
  if (fallback) return fallback;
649
- return buildPublicAssetUrl(asset, preset);
682
+ return buildPublicAssetUrl(asset, preset, "getAssetUrl");
650
683
  }
651
684
 
652
685
  /**
@@ -689,6 +722,10 @@ function transformFallbackFor(
689
722
  function buildPublicAssetUrl(
690
723
  asset: Pick<AssetDTO, "sha"> & OriginalHints,
691
724
  preset: VariantPreset,
725
+ // Named by the PUBLIC entry point, not by this helper: the caller is the
726
+ // function the user actually typed, and it is the one whose name has to
727
+ // appear in the error for the fix to be obvious.
728
+ caller: string,
692
729
  ): string {
693
730
  // The guard lives HERE and not only in `getAssetUrl`, because
694
731
  // `getPrivateAssetUrl` reaches this function directly. Without it, passing an
@@ -696,14 +733,14 @@ function buildPublicAssetUrl(
696
733
  // **cryptographically valid signature over a path containing `undefined`**.
697
734
  // That is strictly worse than the public case: the signature makes it look
698
735
  // authoritative, and it passes shape checks at the edge before 404ing.
699
- assertSha(asset, "getPrivateAssetUrl");
736
+ assertSha(asset, caller);
700
737
  if (preset === "original") {
701
738
  // The stored URL beats every derivation, because it IS the key. Only fall
702
739
  // through to a guess when the caller gave us the sha and nothing else.
703
740
  const stored = asset.variants?.find((v) => v.preset === "original")?.url;
704
741
  if (stored) return stored;
705
742
  const ext = asset.oext || originalExtForMime(asset.mime);
706
- return `${cdnBaseUrl}/${variantPrefix()}${asset.sha}-${PRESET_SHORT.original}.${ext}`;
743
+ return `${cdnBaseUrl}/${variantPrefix(caller)}${asset.sha}-${PRESET_SHORT.original}.${ext}`;
707
744
  }
708
745
  if (preset === "hls") {
709
746
  // A ladder is a PREFIX (`<sha16>-hls<dslHash>/master.m3u8`), not a
@@ -718,7 +755,7 @@ function buildPublicAssetUrl(
718
755
  if (stored) return stored;
719
756
  return `${cdnBaseUrl}/t/format=hls/${asset.sha}.m3u8`;
720
757
  }
721
- return `${cdnBaseUrl}/${variantPrefix()}${asset.sha}-${PRESET_SHORT[preset]}.${PRESET_EXT[preset]}`;
758
+ return `${cdnBaseUrl}/${variantPrefix(caller)}${asset.sha}-${PRESET_SHORT[preset]}.${PRESET_EXT[preset]}`;
722
759
  }
723
760
 
724
761
  /**
@@ -751,7 +788,11 @@ export async function getPrivateAssetUrl(
751
788
  signingKey: string,
752
789
  opts: SignAccessOptions,
753
790
  ): Promise<string> {
754
- return signAccessUrl(buildPublicAssetUrl(asset, preset), signingKey, opts);
791
+ return signAccessUrl(
792
+ buildPublicAssetUrl(asset, preset, "getPrivateAssetUrl"),
793
+ signingKey,
794
+ opts,
795
+ );
755
796
  }
756
797
 
757
798
  /**