@nitida/sdk 0.31.5 → 0.31.7

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 CHANGED
@@ -1403,6 +1403,43 @@ If you need a dimension that doesn't exist, two options: use the closest
1403
1403
  preset and let the browser scale, or file an issue to add it to the
1404
1404
  server-side pipeline (a platform-wide addition, not a per-tenant one).
1405
1405
 
1406
+ ### ⭐ What happens when two tenants upload the SAME image
1407
+
1408
+ Dedup is **content-addressed and cross-tenant**: the sha is a hash of the bytes,
1409
+ so if you and another customer upload the same logo, the same stock photo or the
1410
+ same placeholder, you share a candidate row. This is normal, not exotic.
1411
+
1412
+ A `/t/…` transform URL names **content**, not a tenant — there is no tenant
1413
+ segment in it. Three consequences follow, and all three are measured behaviour:
1414
+
1415
+ **1 · A signed transform is resolved by the SIGNATURE, not by tenant id.**
1416
+ The signature proves possession of one specific tenant's key, which is exactly
1417
+ the identity the URL lacks. Before `2026-08-25` the lowest tenant id won, so a
1418
+ correctly signed URL could be verified against someone else's key and answer
1419
+ `invalid_signature`. Nothing to do on your side — just know the signature is
1420
+ what disambiguates.
1421
+
1422
+ **2 · `strict_transforms` fails CLOSED across every tenant that shares the sha.**
1423
+ If any tenant holding those bytes requires signed URLs, the unsigned request is
1424
+ refused — because an unsigned request cannot say which tenant it belongs to, and
1425
+ picking the most permissive one would let a third party defeat your policy.
1426
+
1427
+ > ⚠️ The cost, stated plainly: if you do **not** use `strict_transforms` but you
1428
+ > share content with someone who does, that sha needs a signed URL from you too.
1429
+ > The alternative — failing open — means your own strict setting is silently
1430
+ > cancelled by a stranger. A 401 you fix by signing is the cheaper mistake.
1431
+
1432
+ **3 · Making an asset private does NOT unpublish another tenant's public copy.**
1433
+ `visibility: "private"` retracts **your** row. If another tenant uploaded the
1434
+ same bytes and left them public, that copy keeps serving, and a `/t/` request
1435
+ resolves to it. No new information leaks — those exact bytes were already public
1436
+ — but do not read "private" as "these bytes are now unreachable". It means
1437
+ "reachable through me only by signature".
1438
+
1439
+ > If that distinction matters for your content, the answer is not a flag: it is
1440
+ > not to share the bytes. Anything unique to you (a customer photo, a document,
1441
+ > a render) has a unique sha and never collides.
1442
+
1406
1443
  ### CDN URL format
1407
1444
 
1408
1445
  `<cdnBase>/<tenantId base36>/v/<sha16>-<presetCode>.<ext>`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitida/sdk",
3
- "version": "0.31.5",
3
+ "version": "0.31.7",
4
4
  "description": "nitida — the media SDK: browser and mobile upload with resume, client-side compression, on-the-fly transforms behind a CDN, video transcode, HLS ladders and AI proxies. Multi-tenant.",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: nitida-sdk
3
- description: How to consume the nitida media platform (@nitida/sdk + @nitida/asset-client, API api.nitida.gofuture.space, CDN 8ok.uk) from an app — build image/video URLs, resolve stored refs, migrate an app off a legacy CDN, and provision a tenant. Use when wiring images/videos through nitida, debugging 400/404/410 on 8ok.uk URLs, or onboarding a new tenant. Captures the gotchas that bit real migrations (base36 video prefix, /t/ vs /v/, the width ladder, audio, the 0-cdn build gate).
3
+ description: How to consume the nitida media platform (@nitida/sdk + @nitida/asset-client, API api.nitida.gofuture.space, CDN 8ok.uk) from an app — build image/video URLs, resolve stored refs, migrate an app off a legacy CDN, and provision a tenant. Use when wiring images/videos through nitida, debugging 400/404/410 on 8ok.uk URLs, or onboarding a new tenant. Also covers PRIVATE assets: `visibility`, the signed `/a/{tenant}/…?exp&sig` tree, `getPrivateAssetUrl` / `getPrivateTransformUrl`, and why a 404 on a private asset is the feature and not a missing file. Captures the gotchas that bit real migrations (base36 video prefix, /t/ vs /v/, the width ladder, audio, the 0-cdn build gate).
4
4
  ---
5
5
 
6
6
  # Consuming the nitida media platform
@@ -501,6 +501,31 @@ The progressive MP4 is capped unconditionally at 1920 wide, so `getAssetUrl(sha,
501
501
  never exceeds 1080p whatever you uploaded. A rung that weighs *more* than its own source (measured:
502
502
  5090 vs 4866 kbps) is the signature of the fallback path, not of a broken ladder.
503
503
 
504
+ ### 3c. ⭐ Dedup is CROSS-TENANT, and three things follow from it
505
+
506
+ The sha hashes the **bytes**, so two customers who upload the same logo, stock
507
+ photo or placeholder share a candidate row. A `/t/…` URL names content and
508
+ carries **no tenant segment**. Measured behaviour as of 2026-08-25:
509
+
510
+ | | |
511
+ |---|---|
512
+ | **signed** `/t/` | the **signature** picks the tenant — it proves possession of one key, which is the identity the URL lacks. (Before this, the lowest tenant id won and a valid signature could answer `invalid_signature`.) |
513
+ | **`strict_transforms`** | fails **closed** across every tenant sharing that sha. If any of them requires signing, the unsigned request is refused. |
514
+ | **`visibility: "private"`** | retracts **your** row only. Another tenant's public copy of the same bytes keeps serving, and `/t/` resolves to it. |
515
+
516
+ ⚠️ **Do not read `private` as "these bytes are unreachable".** It means
517
+ "reachable through me only by signature". Nothing new leaks — those bytes were
518
+ already public via the other tenant — but the promise is narrower than it looks.
519
+
520
+ ⚠️ And the cost of failing closed, stated plainly: a tenant that does **not**
521
+ use `strict_transforms` but shares content with one that does will need signed
522
+ URLs for that sha. Failing open would let a stranger cancel someone's policy;
523
+ a 401 you fix by signing is the cheaper of the two mistakes.
524
+
525
+ ⭐ **Anything unique to you has a unique sha and never collides** — a customer
526
+ photo, a document, a render. Collisions are for generic assets, which is exactly
527
+ the content where sharing costs you nothing.
528
+
504
529
  ## 4. Resolving a stored reference — `extractAssetSha`
505
530
 
506
531
  When a stored value is already an `8ok.uk` URL, pull its sha: