@nitida/sdk 0.31.6 → 0.32.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/README.md +42 -1
- package/dist/index.d.ts +38 -1
- package/dist/index.js +16 -3
- package/dist/index.js.map +1 -1
- package/dist/server.js +16 -3
- package/dist/server.js.map +1 -1
- package/dist/web.js +16 -3
- package/dist/web.js.map +1 -1
- package/package.json +1 -1
- package/skills/nitida-sdk/SKILL.md +25 -0
- package/src/index.ts +53 -4
package/README.md
CHANGED
|
@@ -283,7 +283,11 @@ const result = await aq.upload(file, { fileName: "cover.jpg" });
|
|
|
283
283
|
await aq.upload(bytes, { fileName: "cover.webp" }); // MIME inferred from .webp ✓
|
|
284
284
|
await aq.upload(bytes, { contentType: "image/webp" }); // or be explicit ✓
|
|
285
285
|
// And request the variants you'll render — `presets` DEFAULTS TO ["original"] (just the raw
|
|
286
|
-
// bytes)
|
|
286
|
+
// bytes). ⚠️ Corrected 2026-08-25: a BARE sha does not 404 — it THROWS
|
|
287
|
+
// (`assertSha`); and with the full object, a missing IMAGE preset does not
|
|
288
|
+
// 404 either — it falls back to `/t/` and serves 200. What 404s is a
|
|
289
|
+
// hand-built variant key. Ask for the ladder anyway when you want
|
|
290
|
+
// materialised bytes instead of an on-the-fly transform:
|
|
287
291
|
await aq.upload(bytes, { contentType: "image/webp", presets: ["thumb", "sm", "md", "lg", "xl"] });
|
|
288
292
|
|
|
289
293
|
// Bind a slot (admin operation).
|
|
@@ -1403,6 +1407,43 @@ If you need a dimension that doesn't exist, two options: use the closest
|
|
|
1403
1407
|
preset and let the browser scale, or file an issue to add it to the
|
|
1404
1408
|
server-side pipeline (a platform-wide addition, not a per-tenant one).
|
|
1405
1409
|
|
|
1410
|
+
### ⭐ What happens when two tenants upload the SAME image
|
|
1411
|
+
|
|
1412
|
+
Dedup is **content-addressed and cross-tenant**: the sha is a hash of the bytes,
|
|
1413
|
+
so if you and another customer upload the same logo, the same stock photo or the
|
|
1414
|
+
same placeholder, you share a candidate row. This is normal, not exotic.
|
|
1415
|
+
|
|
1416
|
+
A `/t/…` transform URL names **content**, not a tenant — there is no tenant
|
|
1417
|
+
segment in it. Three consequences follow, and all three are measured behaviour:
|
|
1418
|
+
|
|
1419
|
+
**1 · A signed transform is resolved by the SIGNATURE, not by tenant id.**
|
|
1420
|
+
The signature proves possession of one specific tenant's key, which is exactly
|
|
1421
|
+
the identity the URL lacks. Before `2026-08-25` the lowest tenant id won, so a
|
|
1422
|
+
correctly signed URL could be verified against someone else's key and answer
|
|
1423
|
+
`invalid_signature`. Nothing to do on your side — just know the signature is
|
|
1424
|
+
what disambiguates.
|
|
1425
|
+
|
|
1426
|
+
**2 · `strict_transforms` fails CLOSED across every tenant that shares the sha.**
|
|
1427
|
+
If any tenant holding those bytes requires signed URLs, the unsigned request is
|
|
1428
|
+
refused — because an unsigned request cannot say which tenant it belongs to, and
|
|
1429
|
+
picking the most permissive one would let a third party defeat your policy.
|
|
1430
|
+
|
|
1431
|
+
> ⚠️ The cost, stated plainly: if you do **not** use `strict_transforms` but you
|
|
1432
|
+
> share content with someone who does, that sha needs a signed URL from you too.
|
|
1433
|
+
> The alternative — failing open — means your own strict setting is silently
|
|
1434
|
+
> cancelled by a stranger. A 401 you fix by signing is the cheaper mistake.
|
|
1435
|
+
|
|
1436
|
+
**3 · Making an asset private does NOT unpublish another tenant's public copy.**
|
|
1437
|
+
`visibility: "private"` retracts **your** row. If another tenant uploaded the
|
|
1438
|
+
same bytes and left them public, that copy keeps serving, and a `/t/` request
|
|
1439
|
+
resolves to it. No new information leaks — those exact bytes were already public
|
|
1440
|
+
— but do not read "private" as "these bytes are now unreachable". It means
|
|
1441
|
+
"reachable through me only by signature".
|
|
1442
|
+
|
|
1443
|
+
> If that distinction matters for your content, the answer is not a flag: it is
|
|
1444
|
+
> not to share the bytes. Anything unique to you (a customer photo, a document,
|
|
1445
|
+
> a render) has a unique sha and never collides.
|
|
1446
|
+
|
|
1406
1447
|
### CDN URL format
|
|
1407
1448
|
|
|
1408
1449
|
`<cdnBase>/<tenantId base36>/v/<sha16>-<presetCode>.<ext>`
|
package/dist/index.d.ts
CHANGED
|
@@ -671,7 +671,37 @@ type UploadResult = {
|
|
|
671
671
|
* can close (`.mpga`, `.docx`, `.m4a` all arrive as octet-stream).
|
|
672
672
|
*/
|
|
673
673
|
oext?: string | null;
|
|
674
|
-
|
|
674
|
+
/**
|
|
675
|
+
* La URL pública de una variante razonable — o **`null` si el asset es
|
|
676
|
+
* privado**, que no tiene ninguna.
|
|
677
|
+
*
|
|
678
|
+
* ⭐⭐ ERA `string`, Y ESO HACÍA QUE `upload()` PERDIERA EL HANDLE
|
|
679
|
+
*
|
|
680
|
+
* Los tres retornos de `upload()` arman este campo con `urlFor`, que llama a
|
|
681
|
+
* `assertPublic` y **tira** sobre un asset privado. Medido 2026-08-25 por una
|
|
682
|
+
* auditoría externa: subir bytes que deduplican contra una fila privada hacía
|
|
683
|
+
* que `upload()` lanzara DESPUÉS del PUT — los bytes quedaban guardados y el
|
|
684
|
+
* `assetId` se perdía, porque el error es un `Error` pelado sin `assetId`, sin
|
|
685
|
+
* `sha` y sin `cause`.
|
|
686
|
+
*
|
|
687
|
+
* ⚠️ Y el mensaje nombraba `getAssetUrl` y un preset `"lg"` que el llamador
|
|
688
|
+
* nunca pidió, así que iba a buscar en su código una función que no llamó.
|
|
689
|
+
*
|
|
690
|
+
* Un asset privado **no tiene** URL pública: eso es la feature. Lo que no
|
|
691
|
+
* puede pasar es que no tenerla cueste el resultado de una subida que ya
|
|
692
|
+
* ocurrió. Para servirlo, mirá `visibility` y usá `getPrivateAssetUrl` con la
|
|
693
|
+
* signing key, en tu backend.
|
|
694
|
+
*/
|
|
695
|
+
cdnUrl: string | null;
|
|
696
|
+
/**
|
|
697
|
+
* `"public"` o `"private"`, tal como quedó el asset.
|
|
698
|
+
*
|
|
699
|
+
* ⭐ Existe porque `getAssetUrl(up, …)` —el patrón que enseña toda la doc—
|
|
700
|
+
* no podía detectar un privado: el guarda mira `asset.visibility`, y este
|
|
701
|
+
* objeto no lo llevaba. El resultado era una URL pública para un asset
|
|
702
|
+
* privado, servida sin una queja, que después da 404.
|
|
703
|
+
*/
|
|
704
|
+
visibility: "public" | "private";
|
|
675
705
|
};
|
|
676
706
|
type SlotHistoryEntry = {
|
|
677
707
|
id: string;
|
|
@@ -946,6 +976,13 @@ declare class NitidaClient {
|
|
|
946
976
|
* ```
|
|
947
977
|
*/
|
|
948
978
|
upload(input: File | Blob | Uint8Array, opts?: UploadOptions): Promise<UploadResult>;
|
|
979
|
+
/**
|
|
980
|
+
* `urlFor` sin la excepción: `null` cuando el asset es privado.
|
|
981
|
+
*
|
|
982
|
+
* `upload()` no puede fallar por no poder construir una URL pública. Los
|
|
983
|
+
* bytes ya están; el handle tiene que volver igual.
|
|
984
|
+
*/
|
|
985
|
+
private publicUrlOrNull;
|
|
949
986
|
private defaultPresetForMime;
|
|
950
987
|
/**
|
|
951
988
|
* Pick a sensible preset to build a URL for, given the asset's actual
|
package/dist/index.js
CHANGED
|
@@ -777,7 +777,8 @@ var NitidaClient = class {
|
|
|
777
777
|
// Same shape as the presign branch below, for the same reason. This
|
|
778
778
|
// DTO does carry `sha` today — but relying on that is how the other
|
|
779
779
|
// branch broke, and the value we hashed ourselves is authoritative.
|
|
780
|
-
|
|
780
|
+
visibility: existing.visibility ?? "public",
|
|
781
|
+
cdnUrl: this.publicUrlOrNull(
|
|
781
782
|
{ ...existing, sha: sha.slice(0, 16) },
|
|
782
783
|
this.bestPresetForAsset(existing, mime)
|
|
783
784
|
)
|
|
@@ -834,7 +835,8 @@ var NitidaClient = class {
|
|
|
834
835
|
// ACTUALLY has, now that we waited for it — not from
|
|
835
836
|
// `defaultPresetForMime`, which is a guess made before anything exists.
|
|
836
837
|
// Guessing was safe only while this branch never ran.
|
|
837
|
-
|
|
838
|
+
visibility: settled.visibility ?? "public",
|
|
839
|
+
cdnUrl: this.publicUrlOrNull(
|
|
838
840
|
{ ...settled, sha: short },
|
|
839
841
|
this.bestPresetForAsset(settled, mime)
|
|
840
842
|
)
|
|
@@ -882,9 +884,20 @@ var NitidaClient = class {
|
|
|
882
884
|
sha: sha.slice(0, 16),
|
|
883
885
|
mime: final.mime ?? mime,
|
|
884
886
|
oext: final.oext ?? null,
|
|
885
|
-
|
|
887
|
+
visibility: final.visibility ?? "public",
|
|
888
|
+
cdnUrl: this.publicUrlOrNull(final, this.bestPresetForAsset(final, mime))
|
|
886
889
|
};
|
|
887
890
|
}
|
|
891
|
+
/**
|
|
892
|
+
* `urlFor` sin la excepción: `null` cuando el asset es privado.
|
|
893
|
+
*
|
|
894
|
+
* `upload()` no puede fallar por no poder construir una URL pública. Los
|
|
895
|
+
* bytes ya están; el handle tiene que volver igual.
|
|
896
|
+
*/
|
|
897
|
+
publicUrlOrNull(asset, preset) {
|
|
898
|
+
if (asset.visibility === "private") return null;
|
|
899
|
+
return this.urlFor(asset, preset);
|
|
900
|
+
}
|
|
888
901
|
defaultPresetForMime(mime) {
|
|
889
902
|
if (mime.startsWith("video/")) return "video";
|
|
890
903
|
if (mime.startsWith("audio/")) return "original";
|