@nitida/sdk 0.31.7 → 0.32.1

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
@@ -1135,7 +1135,62 @@ export type UploadResult = {
1135
1135
  * can close (`.mpga`, `.docx`, `.m4a` all arrive as octet-stream).
1136
1136
  */
1137
1137
  oext?: string | null;
1138
- cdnUrl: string;
1138
+ /**
1139
+ * La URL pública de una variante razonable — o **`null` si el asset es
1140
+ * privado**, que no tiene ninguna.
1141
+ *
1142
+ * ⭐⭐ ERA `string`, Y ESO HACÍA QUE `upload()` PERDIERA EL HANDLE
1143
+ *
1144
+ * Los tres retornos de `upload()` arman este campo con `urlFor`, que llama a
1145
+ * `assertPublic` y **tira** sobre un asset privado. Medido 2026-08-25 por una
1146
+ * auditoría externa: subir bytes que deduplican contra una fila privada hacía
1147
+ * que `upload()` lanzara DESPUÉS del PUT — los bytes quedaban guardados y el
1148
+ * `assetId` se perdía, porque el error es un `Error` pelado sin `assetId`, sin
1149
+ * `sha` y sin `cause`.
1150
+ *
1151
+ * ⚠️ Y el mensaje nombraba `getAssetUrl` y un preset `"lg"` que el llamador
1152
+ * nunca pidió, así que iba a buscar en su código una función que no llamó.
1153
+ *
1154
+ * Un asset privado **no tiene** URL pública: eso es la feature. Lo que no
1155
+ * puede pasar es que no tenerla cueste el resultado de una subida que ya
1156
+ * ocurrió. Para servirlo, mirá `visibility` y usá `getPrivateAssetUrl` con la
1157
+ * signing key, en tu backend.
1158
+ */
1159
+ cdnUrl: string | null;
1160
+ /**
1161
+ * `"public"` o `"private"`, tal como quedó el asset.
1162
+ *
1163
+ * ⭐ Existe porque `getAssetUrl(up, …)` —el patrón que enseña toda la doc—
1164
+ * no podía detectar un privado: el guarda mira `asset.visibility`, y este
1165
+ * objeto no lo llevaba. El resultado era una URL pública para un asset
1166
+ * privado, servida sin una queja, que después da 404.
1167
+ */
1168
+ visibility: "public" | "private";
1169
+ /**
1170
+ * La cadena compacta de variantes que EXISTEN (`"lmoqs"`, `"o"`, …).
1171
+ *
1172
+ * ⭐⭐ EL QUINTO DEFECTO DE LA MISMA FAMILIA
1173
+ *
1174
+ * `getAssetUrl` **cambia de estrategia según este campo**. Con él, un preset
1175
+ * de imagen ausente cae a `/t/…width=N/` y el borde lo genera al vuelo:
1176
+ * **200**. Sin él, el builder no sabe qué existe, asume que todo existe, y
1177
+ * devuelve la clave de variante — que da **404 en silencio**.
1178
+ *
1179
+ * Medido 2026-08-25 sobre un asset con `presets: "lmoqs"`:
1180
+ *
1181
+ * sin `presets` → /f/v/<sha>-x.webp 404
1182
+ * con `presets` → /t/format=webp,width=3840/<sha>.webp 200
1183
+ *
1184
+ * `UploadResult` no lo llevaba, así que `getAssetUrl(up, "xl")` —el patrón
1185
+ * que enseña toda la doc— caía del lado malo.
1186
+ *
1187
+ * ⚠️ Es el mismo error que `sha`, `mime` y `oext`, por cuarta vez: **un tipo
1188
+ * de retorno que omite lo que la próxima llamada necesita convierte la
1189
+ * llamada obvia en un 404 silencioso.** Y el guarda que escribí para esto
1190
+ * fijaba los tres *hints* de `original`; `presets` no es un hint, es lo que
1191
+ * gobierna el fallback, así que quedaba afuera de lo que el guarda miraba.
1192
+ */
1193
+ presets: string;
1139
1194
  };
1140
1195
 
1141
1196
  async function computeSha256(
@@ -1687,7 +1742,9 @@ export class NitidaClient {
1687
1742
  // Same shape as the presign branch below, for the same reason. This
1688
1743
  // DTO does carry `sha` today — but relying on that is how the other
1689
1744
  // branch broke, and the value we hashed ourselves is authoritative.
1690
- cdnUrl: this.urlFor(
1745
+ visibility: existing.visibility ?? "public",
1746
+ presets: existing.presets ?? "",
1747
+ cdnUrl: this.publicUrlOrNull(
1691
1748
  { ...existing, sha: sha.slice(0, 16) },
1692
1749
  this.bestPresetForAsset(existing, mime),
1693
1750
  ),
@@ -1778,7 +1835,9 @@ export class NitidaClient {
1778
1835
  // ACTUALLY has, now that we waited for it — not from
1779
1836
  // `defaultPresetForMime`, which is a guess made before anything exists.
1780
1837
  // Guessing was safe only while this branch never ran.
1781
- cdnUrl: this.urlFor(
1838
+ visibility: settled.visibility ?? "public",
1839
+ presets: settled.presets ?? "",
1840
+ cdnUrl: this.publicUrlOrNull(
1782
1841
  { ...settled, sha: short },
1783
1842
  this.bestPresetForAsset(settled, mime),
1784
1843
  ),
@@ -1834,10 +1893,28 @@ export class NitidaClient {
1834
1893
  sha: sha.slice(0, 16),
1835
1894
  mime: final.mime ?? mime,
1836
1895
  oext: final.oext ?? null,
1837
- cdnUrl: this.urlFor(final, this.bestPresetForAsset(final, mime)),
1896
+ visibility: final.visibility ?? "public",
1897
+ presets: final.presets ?? "",
1898
+ cdnUrl: this.publicUrlOrNull(final, this.bestPresetForAsset(final, mime)),
1838
1899
  };
1839
1900
  }
1840
1901
 
1902
+ /**
1903
+ * `urlFor` sin la excepción: `null` cuando el asset es privado.
1904
+ *
1905
+ * `upload()` no puede fallar por no poder construir una URL pública. Los
1906
+ * bytes ya están; el handle tiene que volver igual.
1907
+ */
1908
+ private publicUrlOrNull(
1909
+ asset: Parameters<typeof getAssetUrl>[0] & {
1910
+ visibility?: "public" | "private";
1911
+ },
1912
+ preset: VariantPreset,
1913
+ ): string | null {
1914
+ if (asset.visibility === "private") return null;
1915
+ return this.urlFor(asset, preset);
1916
+ }
1917
+
1841
1918
  private defaultPresetForMime(mime: string): VariantPreset {
1842
1919
  if (mime.startsWith("video/")) return "video";
1843
1920
  // The `mp3` variant only exists AFTER /process transcodes it; at presign