@nitida/sdk 0.32.3 → 0.35.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/AGENTS.md +26 -0
- package/README.md +63 -11
- package/dist/index.d.ts +21 -5
- package/dist/index.js +34 -4
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +34 -4
- package/dist/server.js.map +1 -1
- package/dist/web.d.ts +1 -1
- package/dist/web.js +34 -4
- package/dist/web.js.map +1 -1
- package/package.json +2 -2
- package/skills/nitida-sdk/SKILL.md +131 -7
- package/src/index.ts +78 -23
- package/src/server/index.ts +7 -0
- package/src/web/index.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitida/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
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": {
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|
|
75
75
|
"react": ">=18",
|
|
76
|
-
"@nitida/asset-client": "^0.
|
|
76
|
+
"@nitida/asset-client": "^0.23.0",
|
|
77
77
|
"@nitida/asset-compressor-web": "^0.6.1",
|
|
78
78
|
"@nitida/asset-compressor-native": "^0.2.1"
|
|
79
79
|
},
|
|
@@ -94,7 +94,16 @@ const COVER_CROP = { fit: "cover", gravity: "auto" } as const;
|
|
|
94
94
|
|
|
95
95
|
Output URL shape: `https://8ok.uk/t/format=webp,width=640/<sha16>.webp`.
|
|
96
96
|
|
|
97
|
-
⚠️ **Widths MUST be on the unsigned ladder** `TRANSFORM_WIDTHS`
|
|
97
|
+
⚠️ **Widths MUST be on the unsigned ladder** `TRANSFORM_WIDTHS` — generated from the source, do not
|
|
98
|
+
edit by hand:
|
|
99
|
+
|
|
100
|
+
<!-- BEGIN GENERATED: transform-widths · bun run gen:docs -->
|
|
101
|
+
`96, 128, 160, 180, 240, 256, 320, 400, 480, 600, 640, 800, 960, 1080, 1200, 1280, 1440, 1600, 1920, 2560, 3840` — 21 widths.
|
|
102
|
+
<!-- END GENERATED: transform-widths -->
|
|
103
|
+
|
|
104
|
+
Any other width → **HTTP 400** at the edge (DoS guard). The `TransformWidth` type makes an off-ladder
|
|
105
|
+
width a compile error — import the type, don't hardcode magic numbers. (For a one-off custom width
|
|
106
|
+
you'd need signed URLs; not used here.)
|
|
98
107
|
|
|
99
108
|
⚠️ **`getTransformSrcSet` is NOT protected by that type.** Its parameter is `number[]` on purpose —
|
|
100
109
|
a responsive ladder may legitimately carry DPR widths — so `[641, 999]` compiles clean and fails in
|
|
@@ -206,6 +215,43 @@ Smaller **and** worse is generation loss, not a saving.
|
|
|
206
215
|
so there is no downscale in between to hide the first pass. At 640 and 1920
|
|
207
216
|
the laddered output is *heavier* too.
|
|
208
217
|
- **`["original"]`-only assets are unaffected** — one pass, always.
|
|
218
|
+
- **`quality: "auto"` is a NO-OP** — byte-for-byte identical to omitting the key.
|
|
219
|
+
Measured 2026-08-31 on a production laddered asset at `width=1920`: both
|
|
220
|
+
answered **136 680 B, sha256 `3a9ba57d…`, `x-transform-source: original`**.
|
|
221
|
+
⇒ The whole option is unnecessary, which is why `TransformOptions.quality`
|
|
222
|
+
carries an `@deprecated` tag since `@nitida/asset-client` 0.23.0. Your editor
|
|
223
|
+
strikes it through; that is on purpose.
|
|
224
|
+
- **A second corpus, where there is not even a byte saving.** 5056 px
|
|
225
|
+
architectural renders (tenant `suenos-del-mar`, 2026-08-31), referenced
|
|
226
|
+
against `/t/format=png,width=W/` — the lossless auto path at the same width:
|
|
227
|
+
|
|
228
|
+
| width | pinned source | bytes | PSNR |
|
|
229
|
+
|---|---|---|---|
|
|
230
|
+
| 3840 | `xl` | **+7.2 %** | **−0.50 dB** |
|
|
231
|
+
| 1920 | `lg` | **+8.1 %** | **−0.64 dB** |
|
|
232
|
+
| 1280 | `md` | **+2.0 %** | **−0.85 dB** |
|
|
233
|
+
|
|
234
|
+
Heavier **and** worse, 3 of 3. The photograph corpus above at least came back
|
|
235
|
+
smaller — that is exactly what hid this for four months.
|
|
236
|
+
|
|
237
|
+
### ✅ The one-line check: `x-transform-source`
|
|
238
|
+
|
|
239
|
+
**Do not reason about which path you got — read it off the response.** The
|
|
240
|
+
header names the variant the edge actually decoded:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
curl -sI 'https://8ok.uk/t/format=webp,width=1920/<sha16>.webp' \
|
|
244
|
+
| grep -i x-transform-source
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
| it says | you got |
|
|
248
|
+
|---|---|
|
|
249
|
+
| `original` | **one** lossy pass ✅ |
|
|
250
|
+
| `md` / `lg` / `xl` / `sm` | **two** — a numeric `quality` is in your URL ✖ |
|
|
251
|
+
| `poster` | you asked `/t/` for a video sha; it transformed the poster frame |
|
|
252
|
+
|
|
253
|
+
Run it once per surface after a change. It costs nothing and it is the only
|
|
254
|
+
statement about compression that is not an inference.
|
|
209
255
|
|
|
210
256
|
### What running this recipe caught
|
|
211
257
|
|
|
@@ -574,7 +620,9 @@ That replaces the older hand-run provisioning scripts and SQL that used to live
|
|
|
574
620
|
| Poster | `getAssetUrl({sha}, 'poster')` | `/<tid b36>/v/<sha>-p.webp` |
|
|
575
621
|
| Sha from URL | `extractAssetSha(url)` | — |
|
|
576
622
|
|
|
577
|
-
Variant preset short codes — **all of them**: `thumb=q, sm=s, md=m, lg=l, xl=x, original=o, poster=p, video=v, aiproxy=a,
|
|
623
|
+
Variant preset short codes — **all of them**: `thumb=q, sm=s, md=m, lg=l, xl=x, original=o, poster=p, video=v, aiproxy=a, mp3=mp3`. ⚠️ `mp3` is the one irregular case: three characters, not one. Exts: images `webp`, video `mp4`.
|
|
624
|
+
|
|
625
|
+
⚠️ **`hls` is deliberately NOT in that list.** It has an internal short code (`h`), but it is never a URL you can fetch: a ladder is a PREFIX (`<sha16>-hls<dslHash>/master.m3u8`) whose `<dslHash>` is computed server-side, so a hand-built `<sha16>-h.m3u8` **404s on every asset** (measured). Use `getAssetUrl(asset, "hls")` or `getHlsStreamingUrl({ sha })` — see §3c — and never derive the path yourself.
|
|
578
626
|
|
|
579
627
|
|
|
580
628
|
## Private assets — `visibility`
|
|
@@ -600,7 +648,13 @@ await getPrivateTransformUrl(asset, { width: 1280 }, signingKey, { expiresInSeco
|
|
|
600
648
|
`"public"`, or a bare `{ sha }`, is never refused — and this is a DIFFERENT
|
|
601
649
|
rule from the missing-preset fallback below, which is about presets, not
|
|
602
650
|
privacy.
|
|
603
|
-
- **`exp` is mandatory
|
|
651
|
+
- **`exp` is mandatory and CAPPED at 7 days.** `expiresInSeconds` over that
|
|
652
|
+
throws in the SDK and 401s (`exp_too_far`) at the platform — an expiry that
|
|
653
|
+
never arrives is not an expiry. Revocation is *"within a minute"* (60 s TTL
|
|
654
|
+
at the edge).
|
|
655
|
+
- **⚠️ A signature does NOT widen the width ladder.** Off-ladder widths are 400
|
|
656
|
+
whether or not the URL is signed. Signing buys IDENTITY (which tenant asked,
|
|
657
|
+
until `exp`), which is what `strict_transforms` and `effect=genfill` need.
|
|
604
658
|
- **The signing key is a backend secret** — it mints URLs for every private
|
|
605
659
|
asset the tenant owns.
|
|
606
660
|
- **⭐ Where the signing key comes from: the response that CREATED your
|
|
@@ -608,10 +662,15 @@ await getPrivateTransformUrl(asset, { width: 1280 }, signingKey, { expiresInSeco
|
|
|
608
662
|
three API keys, and the console shows it in the same panel. Nothing else
|
|
609
663
|
hands it out — `GET /admin/projects/:code` does **not** include it. If it is
|
|
610
664
|
lost, the only endpoint that returns a key is
|
|
611
|
-
`POST /admin/projects/:code/rotate-signing-key`,
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
665
|
+
`POST /admin/projects/:code/rotate-signing-key`, and it needs the
|
|
666
|
+
system-scope key the platform operator holds.
|
|
667
|
+
- **⭐ Rotating no longer kills your `/t/` URLs.** Since `@nitida/sdk` 0.31.0 a
|
|
668
|
+
signed transform URL carries `kid` (which key signed it) and `exp`, and the
|
|
669
|
+
OUTGOING key keeps verifying for 14 days after a rotation. ⚠️ The exception
|
|
670
|
+
is `/a/…` ACCESS URLs: their key is derived from `signing_key` with no `kid`,
|
|
671
|
+
so those 401 immediately on rotation. They are minted per view with
|
|
672
|
+
minute-scale expiries, so the burst heals itself — but rotate at a quiet hour
|
|
673
|
+
if the tenant serves private assets.
|
|
615
674
|
- **⭐ Already have a project and never saw a signing key?** Then you never got
|
|
616
675
|
one — projects created before 2026-08-23 were not handed it, and no endpoint
|
|
617
676
|
shows you the current one. **If you have not signed any URLs yet, ask us to
|
|
@@ -620,6 +679,69 @@ await getPrivateTransformUrl(asset, { width: 1280 }, signingKey, { expiresInSeco
|
|
|
620
679
|
instead. This is the wall the T5 agent hit, and the sentence that was
|
|
621
680
|
missing.
|
|
622
681
|
|
|
682
|
+
## ⚠️ EXIF orientation — two dimension pairs, and mixing them stretches the photo
|
|
683
|
+
|
|
684
|
+
**A phone taking a portrait photo does not store portrait pixels.** It writes the
|
|
685
|
+
sensor buffer landscape and tags it `Orientation` 5–8 meaning "rotate before
|
|
686
|
+
showing". So every such file has **two** pairs, transposes of each other:
|
|
687
|
+
|
|
688
|
+
```
|
|
689
|
+
4032×3024 the STORED buffer (what a header parser reads)
|
|
690
|
+
3024×4032 what it DISPLAYS as (what every decoder hands you)
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
Use the **stored** pair only for orientation-invariant quantities — area, a
|
|
694
|
+
megapixel budget. Use the **displayed** pair for anything geometric: a resize
|
|
695
|
+
target, an aspect ratio, a layout box, a coordinate you denormalise.
|
|
696
|
+
|
|
697
|
+
**This exact confusion shipped twice, on both sides of the wire:**
|
|
698
|
+
|
|
699
|
+
| where | what happened | fixed |
|
|
700
|
+
|---|---|---|
|
|
701
|
+
| `@nitida/asset-compressor-web` 0.6.0–0.6.2 | resize target from the stored pair, fed to a decode that had already rotated ⇒ **1.78× stretch**, measured | **0.6.3** |
|
|
702
|
+
| `asset-manager` (server), until 2026-08-28 | recorded the stored pair as the asset's `w`/`h` while serving rotated variants ⇒ transposed aspect on the DTO | 2026-08-28 |
|
|
703
|
+
|
|
704
|
+
Needed all three, which is why it looked random: a parseable header
|
|
705
|
+
(**HEIC is immune** — the ISOBMFF box walk isn't implemented, so no dims, so no
|
|
706
|
+
forced resize), orientation **5–8**, and a long side **above** the box.
|
|
707
|
+
|
|
708
|
+
⇒ **Damage is pre-upload and unrecoverable.** The raw bucket faithfully stores the
|
|
709
|
+
already-stretched bytes. Affected photos must be re-uploaded from the original;
|
|
710
|
+
no backfill exists.
|
|
711
|
+
|
|
712
|
+
### The two traps, concretely
|
|
713
|
+
|
|
714
|
+
**Browser — `createImageBitmap` with BOTH resize axes does not preserve ratio.**
|
|
715
|
+
|
|
716
|
+
```ts
|
|
717
|
+
// 🔴 any disagreement with the decoded bitmap becomes a stretch
|
|
718
|
+
createImageBitmap(blob, { imageOrientation: "from-image", resizeWidth: w, resizeHeight: h })
|
|
719
|
+
// ✅ one axis: the spec derives the other, ratio survives BY CONSTRUCTION
|
|
720
|
+
createImageBitmap(blob, { imageOrientation: "from-image", resizeWidth: w })
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
**Node — `sharp(x).rotate().metadata()` does NOT apply the rotation.** `.rotate()`
|
|
724
|
+
queues an operation; `.metadata()` still reads the input. Measured, sharp 0.34.5:
|
|
725
|
+
|
|
726
|
+
```
|
|
727
|
+
sharp(buf).metadata() → 4032×3024, orientation 6
|
|
728
|
+
sharp(buf).rotate().metadata() → 4032×3024 ← the false idiom
|
|
729
|
+
sharp(buf).rotate().toBuffer() → info → 3024×4032 ← the truth
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
It read so plausibly it was written three times in one repo, once directly under
|
|
733
|
+
the comment *"Source dims (EXIF-rotated for correctness)"*. To get display dims:
|
|
734
|
+
transpose the raw pair yourself when `orientation` is 5–8, or measure the output
|
|
735
|
+
buffer.
|
|
736
|
+
|
|
737
|
+
**And the lesson that cost the most:** the stretch had been suspected once and
|
|
738
|
+
declared *"refuted with production data"* because storage held four distinct
|
|
739
|
+
aspect ratios — "a pipeline that forced a ratio would have produced ONE". It
|
|
740
|
+
never forced one ratio; it forced **each photo's own transpose**. Counting
|
|
741
|
+
distinct ratios could not detect it, so finding several was evidence of nothing.
|
|
742
|
+
**An invariant you cannot state is an invariant you cannot test** — compare
|
|
743
|
+
against the *displayed* ratio, or you are comparing stored ratios to each other.
|
|
744
|
+
|
|
623
745
|
## Two ways an image gets smaller, and only one of them is yours to call
|
|
624
746
|
|
|
625
747
|
This is the question every programmatic caller gets wrong, so it is stated flat:
|
|
@@ -694,6 +816,8 @@ needed. Reproduce there before debugging any of these by hand.
|
|
|
694
816
|
|
|
695
817
|
| Symptom | Cause |
|
|
696
818
|
|---|---|
|
|
819
|
+
| **a portrait photo comes back stretched wide**, and only some do | `@nitida/asset-compressor-web` **0.6.0–0.6.2**: stored (pre-EXIF) dims forced onto an already-rotated decode ⇒ 1.78×. Needs a parseable header (**HEIC immune**) + orientation 5–8 + long side above the box. **Fixed in 0.6.3**; pre-upload damage, so re-upload, don't repair — see §EXIF orientation |
|
|
820
|
+
| **`w`/`h` on the DTO disagree with the image you receive** | `asset-manager` older than 2026-08-28 recorded the stored pair for EXIF-rotated sources. The **variant** dims were always right — trust those until the asset is re-processed |
|
|
697
821
|
| **400** on an image URL | width not on `TRANSFORM_WIDTHS` ladder |
|
|
698
822
|
| **410** on a video `/t/` URL | that video has no `poster`. With one, `/t/` returns the poster as an image, never the video — use `getAssetUrl(...,'video')` |
|
|
699
823
|
| **404** on a video URL | decimal tenant prefix (`/10/v/`) instead of base36 (`/a/v/`) — call `setTenantId` + `getAssetUrl`. Tenant 12 → `/c/v/` |
|
package/src/index.ts
CHANGED
|
@@ -49,17 +49,20 @@ import {
|
|
|
49
49
|
hasPreset,
|
|
50
50
|
invalidateSlotCache,
|
|
51
51
|
isRequestablePreset,
|
|
52
|
+
type PrivateTransformOptions,
|
|
52
53
|
REQUESTABLE_PRESETS,
|
|
53
54
|
type RequestablePreset,
|
|
54
55
|
type ResolveSlotOptions,
|
|
55
56
|
resolveSlot,
|
|
56
57
|
resolveSlots,
|
|
57
58
|
type SignedTransformOptions,
|
|
59
|
+
type SignTransformOptions,
|
|
58
60
|
type SlotDTO,
|
|
59
61
|
type SlotResolution,
|
|
60
62
|
setCdnBase,
|
|
61
63
|
setTenantId,
|
|
62
64
|
type TransformOptions,
|
|
65
|
+
type TransformWidth,
|
|
63
66
|
toRequestablePresets,
|
|
64
67
|
type VariantEntryPreset,
|
|
65
68
|
type VariantPreset,
|
|
@@ -224,10 +227,12 @@ export type {
|
|
|
224
227
|
AssetVariant,
|
|
225
228
|
HlsRung,
|
|
226
229
|
PaletteSwatch,
|
|
230
|
+
PrivateTransformOptions,
|
|
227
231
|
RequestablePreset,
|
|
228
232
|
ResolveSlotOptions,
|
|
229
233
|
SignAccessOptions,
|
|
230
234
|
SignedTransformOptions,
|
|
235
|
+
SignTransformOptions,
|
|
231
236
|
SlotDTO,
|
|
232
237
|
SlotResolution,
|
|
233
238
|
TransformEffect,
|
|
@@ -249,6 +254,7 @@ export {
|
|
|
249
254
|
configureSlotResolver,
|
|
250
255
|
contrastRatio,
|
|
251
256
|
deriveAccessKey,
|
|
257
|
+
deriveTransformKid,
|
|
252
258
|
extractAssetSha,
|
|
253
259
|
getAmbientGradient,
|
|
254
260
|
getAssetDimensions,
|
|
@@ -272,6 +278,9 @@ export {
|
|
|
272
278
|
invalidateSlotCache,
|
|
273
279
|
isRequestablePreset,
|
|
274
280
|
iteratePaletteSwatches,
|
|
281
|
+
MAX_SIGNED_TRANSFORM_TTL_SECONDS,
|
|
282
|
+
MAX_SIGNED_URL_TTL_SECONDS,
|
|
283
|
+
MIN_SIGNED_TRANSFORM_TTL_SECONDS,
|
|
275
284
|
PRESET_EXT,
|
|
276
285
|
PRESET_LONG,
|
|
277
286
|
PRESET_MAX_DIM,
|
|
@@ -288,6 +297,7 @@ export {
|
|
|
288
297
|
signTransformUrl,
|
|
289
298
|
TRANSFORM_WIDTHS,
|
|
290
299
|
toRequestablePresets,
|
|
300
|
+
transformMessage,
|
|
291
301
|
} from "@nitida/asset-client";
|
|
292
302
|
|
|
293
303
|
// ---------------------------------------------------------------------------
|
|
@@ -1090,6 +1100,27 @@ export type UploadOptions = {
|
|
|
1090
1100
|
*/
|
|
1091
1101
|
const DEFAULT_UPLOAD_PRESETS: RequestablePreset[] = ["original"];
|
|
1092
1102
|
|
|
1103
|
+
/**
|
|
1104
|
+
* Fallback for `UploadResult.kind` when the server DTO does not carry one.
|
|
1105
|
+
*
|
|
1106
|
+
* Mirrors what the asset-manager itself does — it branches on the MIME prefix
|
|
1107
|
+
* (`video/` at `process.routes.ts:149`, `audio/` at `:332`, `image/` at
|
|
1108
|
+
* `:972`) and files everything else as `other`. Kept in step deliberately: a
|
|
1109
|
+
* fallback that disagreed with the server would hand a caller a `kind` the
|
|
1110
|
+
* platform never assigned, and `getAssetUrl` now makes decisions on it.
|
|
1111
|
+
*
|
|
1112
|
+
* This should almost never fire — every DTO the API returns carries `kind`.
|
|
1113
|
+
* It exists so the field is never `undefined`, because `undefined` is exactly
|
|
1114
|
+
* how a builder silently opts out of a refusal it should have honoured.
|
|
1115
|
+
*/
|
|
1116
|
+
function kindForMime(mime: string | undefined): AssetDTO["kind"] {
|
|
1117
|
+
const m = (mime ?? "").toLowerCase();
|
|
1118
|
+
if (m.startsWith("video/")) return "video";
|
|
1119
|
+
if (m.startsWith("audio/")) return "audio";
|
|
1120
|
+
if (m.startsWith("image/")) return "image";
|
|
1121
|
+
return "other";
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1093
1124
|
export type UploadResult = {
|
|
1094
1125
|
assetId: string;
|
|
1095
1126
|
sha256: string;
|
|
@@ -1165,6 +1196,22 @@ export type UploadResult = {
|
|
|
1165
1196
|
* objeto no lo llevaba. El resultado era una URL pública para un asset
|
|
1166
1197
|
* privado, servida sin una queja, que después da 404.
|
|
1167
1198
|
*/
|
|
1199
|
+
/**
|
|
1200
|
+
* What the asset IS (`image` · `video` · `audio` · `document` · `other`).
|
|
1201
|
+
*
|
|
1202
|
+
* ⭐ It exists because it did not, and the guard caught it before anyone
|
|
1203
|
+
* else did — the FIFTH member of this family, after `sha`, `mime`, `oext`
|
|
1204
|
+
* and `presets`. When `getAssetUrl` learned to refuse `hls` on something
|
|
1205
|
+
* that can never have a ladder, it started READING `kind`; an
|
|
1206
|
+
* `UploadResult` without it silently opted out of the refusal and went
|
|
1207
|
+
* right back to building `/t/format=hls/<sha>.m3u8` — which does not 404,
|
|
1208
|
+
* it answers 200 with the image bytes.
|
|
1209
|
+
*
|
|
1210
|
+
* The rule this keeps re-teaching: a return type that omits what the next
|
|
1211
|
+
* call reads is a silent 404 (or worse, a silent 200). The fix is always to
|
|
1212
|
+
* carry the field, never to write a better error message about its absence.
|
|
1213
|
+
*/
|
|
1214
|
+
kind: AssetDTO["kind"];
|
|
1168
1215
|
visibility: "public" | "private";
|
|
1169
1216
|
/**
|
|
1170
1217
|
* La cadena compacta de variantes que EXISTEN (`"lmoqs"`, `"o"`, …).
|
|
@@ -1440,27 +1487,21 @@ export class NitidaClient {
|
|
|
1440
1487
|
*/
|
|
1441
1488
|
// Overload 1: no signing — synchronous, on-ladder width only (strict).
|
|
1442
1489
|
transform(asset: Pick<AssetDTO, "sha">, opts?: TransformOptions): string;
|
|
1443
|
-
// Overload 2: with { sign: true } — async; returns
|
|
1444
|
-
//
|
|
1445
|
-
//
|
|
1490
|
+
// Overload 2: with { sign: true, expiresInSeconds } — async; returns a
|
|
1491
|
+
// `?kid&exp&sig` URL. The width ladder is the SAME as unsigned: a signature
|
|
1492
|
+
// names the caller, it does not widen the image (doc blindaje WS-6).
|
|
1446
1493
|
transform(
|
|
1447
1494
|
asset: Pick<AssetDTO, "sha">,
|
|
1448
1495
|
opts: SignedTransformOptions,
|
|
1449
|
-
signOpts: { sign: true },
|
|
1496
|
+
signOpts: { sign: true } & SignTransformOptions,
|
|
1450
1497
|
): Promise<string>;
|
|
1451
1498
|
transform(
|
|
1452
1499
|
asset: Pick<AssetDTO, "sha">,
|
|
1453
1500
|
opts: SignedTransformOptions = {},
|
|
1454
|
-
signOpts?: { sign: true },
|
|
1501
|
+
signOpts?: { sign: true } & SignTransformOptions,
|
|
1455
1502
|
): string | Promise<string> {
|
|
1456
1503
|
if (!signOpts?.sign) {
|
|
1457
|
-
|
|
1458
|
-
// public call site, so an off-ladder width can't reach here through the
|
|
1459
|
-
// typed API — narrow back to TransformOptions for the strict builder.
|
|
1460
|
-
return (
|
|
1461
|
-
getTransformUrl(asset, opts as TransformOptions) ??
|
|
1462
|
-
this.urlFor(asset, "lg")
|
|
1463
|
-
);
|
|
1504
|
+
return getTransformUrl(asset, opts) ?? this.urlFor(asset, "lg");
|
|
1464
1505
|
}
|
|
1465
1506
|
if (!this.opts.signingKey) {
|
|
1466
1507
|
throw new Error(
|
|
@@ -1468,11 +1509,15 @@ export class NitidaClient {
|
|
|
1468
1509
|
"No signingKey on this client. It is returned ONCE, in the response that creates your project (POST /admin/projects → `signingKey`, next to the three API keys; the console shows it in the same panel). If you never saw one — projects created before 2026-08-23 were not handed it — ask the platform operator to rotate: with no signed URLs in flight that invalidates nothing and is free. If you DO have signed URLs circulating, ask for the current key instead, because rotating would kill them. Then pass it to the SDK constructor on a SERVER-side instance only.",
|
|
1469
1510
|
);
|
|
1470
1511
|
}
|
|
1471
|
-
//
|
|
1472
|
-
//
|
|
1512
|
+
// Empty opts → no transform DSL, fall back to the unsigned `lg` variant
|
|
1513
|
+
// URL. `tenantId` defaults to the one this client was constructed with,
|
|
1514
|
+
// because the tenant is part of the signed message.
|
|
1473
1515
|
return (
|
|
1474
|
-
getSignedTransformUrl(asset, opts, this.opts.signingKey
|
|
1475
|
-
|
|
1516
|
+
getSignedTransformUrl(asset, opts, this.opts.signingKey, {
|
|
1517
|
+
expiresInSeconds: signOpts.expiresInSeconds,
|
|
1518
|
+
tenantId: signOpts.tenantId ?? this.opts.tenantId,
|
|
1519
|
+
nowSeconds: signOpts.nowSeconds,
|
|
1520
|
+
}) ?? Promise.resolve(this.urlFor(asset, "lg"))
|
|
1476
1521
|
);
|
|
1477
1522
|
}
|
|
1478
1523
|
|
|
@@ -1490,15 +1535,15 @@ export class NitidaClient {
|
|
|
1490
1535
|
): string;
|
|
1491
1536
|
transformSrcSet(
|
|
1492
1537
|
asset: Pick<AssetDTO, "sha">,
|
|
1493
|
-
widths:
|
|
1538
|
+
widths: TransformWidth[],
|
|
1494
1539
|
extraOpts: Omit<TransformOptions, "width">,
|
|
1495
|
-
signOpts: { sign: true },
|
|
1540
|
+
signOpts: { sign: true } & SignTransformOptions,
|
|
1496
1541
|
): Promise<string>;
|
|
1497
1542
|
transformSrcSet(
|
|
1498
1543
|
asset: Pick<AssetDTO, "sha">,
|
|
1499
1544
|
widths: number[],
|
|
1500
1545
|
extraOpts: Omit<TransformOptions, "width"> = {},
|
|
1501
|
-
signOpts?: { sign: true },
|
|
1546
|
+
signOpts?: { sign: true } & SignTransformOptions,
|
|
1502
1547
|
): string | Promise<string> {
|
|
1503
1548
|
if (!signOpts?.sign) return getTransformSrcSet(asset, widths, extraOpts);
|
|
1504
1549
|
if (!this.opts.signingKey) {
|
|
@@ -1508,15 +1553,22 @@ export class NitidaClient {
|
|
|
1508
1553
|
);
|
|
1509
1554
|
}
|
|
1510
1555
|
const key = this.opts.signingKey;
|
|
1556
|
+
const sign = {
|
|
1557
|
+
expiresInSeconds: signOpts.expiresInSeconds,
|
|
1558
|
+
tenantId: signOpts.tenantId ?? this.opts.tenantId,
|
|
1559
|
+
nowSeconds: signOpts.nowSeconds,
|
|
1560
|
+
};
|
|
1511
1561
|
return Promise.all(
|
|
1512
1562
|
widths.map(async (w) => {
|
|
1513
|
-
//
|
|
1514
|
-
//
|
|
1515
|
-
//
|
|
1563
|
+
// ⚠️ The overload types `widths` as `TransformWidth[]` on the signed
|
|
1564
|
+
// path. Off-ladder widths 400 at the edge whether or not they are
|
|
1565
|
+
// signed (doc blindaje WS-6), so a signed srcSet full of custom widths
|
|
1566
|
+
// is a srcSet of 400s — the type says so at compile time now.
|
|
1516
1567
|
const signed = await getSignedTransformUrl(
|
|
1517
1568
|
asset,
|
|
1518
|
-
{ ...extraOpts, width: w },
|
|
1569
|
+
{ ...extraOpts, width: w as TransformWidth },
|
|
1519
1570
|
key,
|
|
1571
|
+
sign,
|
|
1520
1572
|
);
|
|
1521
1573
|
return signed ? `${signed} ${w}w` : null;
|
|
1522
1574
|
}),
|
|
@@ -1756,6 +1808,7 @@ export class NitidaClient {
|
|
|
1756
1808
|
// Same shape as the presign branch below, for the same reason. This
|
|
1757
1809
|
// DTO does carry `sha` today — but relying on that is how the other
|
|
1758
1810
|
// branch broke, and the value we hashed ourselves is authoritative.
|
|
1811
|
+
kind: existing.kind ?? kindForMime(mime),
|
|
1759
1812
|
visibility: existing.visibility ?? "public",
|
|
1760
1813
|
presets: existing.presets ?? "",
|
|
1761
1814
|
variants: existing.variants ?? [],
|
|
@@ -1850,6 +1903,7 @@ export class NitidaClient {
|
|
|
1850
1903
|
// ACTUALLY has, now that we waited for it — not from
|
|
1851
1904
|
// `defaultPresetForMime`, which is a guess made before anything exists.
|
|
1852
1905
|
// Guessing was safe only while this branch never ran.
|
|
1906
|
+
kind: settled.kind ?? kindForMime(mime),
|
|
1853
1907
|
visibility: settled.visibility ?? "public",
|
|
1854
1908
|
presets: settled.presets ?? "",
|
|
1855
1909
|
variants: settled.variants ?? [],
|
|
@@ -1909,6 +1963,7 @@ export class NitidaClient {
|
|
|
1909
1963
|
sha: sha.slice(0, 16),
|
|
1910
1964
|
mime: final.mime ?? mime,
|
|
1911
1965
|
oext: final.oext ?? null,
|
|
1966
|
+
kind: final.kind ?? kindForMime(final.mime ?? mime),
|
|
1912
1967
|
visibility: final.visibility ?? "public",
|
|
1913
1968
|
presets: final.presets ?? "",
|
|
1914
1969
|
variants: final.variants ?? [],
|
package/src/server/index.ts
CHANGED
|
@@ -107,6 +107,7 @@ export {
|
|
|
107
107
|
configureSlotResolver,
|
|
108
108
|
contrastRatio,
|
|
109
109
|
deriveAccessKey,
|
|
110
|
+
deriveTransformKid,
|
|
110
111
|
extractAssetSha,
|
|
111
112
|
getAmbientGradient,
|
|
112
113
|
getAssetDimensions,
|
|
@@ -132,6 +133,9 @@ export {
|
|
|
132
133
|
isRequestablePreset,
|
|
133
134
|
isUniversallyPlayableAudio,
|
|
134
135
|
iteratePaletteSwatches,
|
|
136
|
+
MAX_SIGNED_TRANSFORM_TTL_SECONDS,
|
|
137
|
+
MAX_SIGNED_URL_TTL_SECONDS,
|
|
138
|
+
MIN_SIGNED_TRANSFORM_TTL_SECONDS,
|
|
135
139
|
mimeFromFileName,
|
|
136
140
|
type NitidaClientOptions,
|
|
137
141
|
type PaletteSwatch,
|
|
@@ -140,6 +144,7 @@ export {
|
|
|
140
144
|
PRESET_MAX_DIM,
|
|
141
145
|
PRESET_SHORT,
|
|
142
146
|
type PresignUploadUrlOptions,
|
|
147
|
+
type PrivateTransformOptions,
|
|
143
148
|
pickAmbientBackground,
|
|
144
149
|
REQUESTABLE_PRESETS,
|
|
145
150
|
type RegenerateResult,
|
|
@@ -150,6 +155,7 @@ export {
|
|
|
150
155
|
resolveSlots,
|
|
151
156
|
type SignAccessOptions,
|
|
152
157
|
type SignedTransformOptions,
|
|
158
|
+
type SignTransformOptions,
|
|
153
159
|
type SlotDTO,
|
|
154
160
|
type SlotHistoryEntry,
|
|
155
161
|
type SlotResolution,
|
|
@@ -166,6 +172,7 @@ export {
|
|
|
166
172
|
type TransformOptions,
|
|
167
173
|
type TransformWidth,
|
|
168
174
|
toRequestablePresets,
|
|
175
|
+
transformMessage,
|
|
169
176
|
type UploadOptions,
|
|
170
177
|
type UploadResult,
|
|
171
178
|
type UploadUrlResult,
|
package/src/web/index.ts
CHANGED
|
@@ -124,6 +124,7 @@ export {
|
|
|
124
124
|
configureSlotResolver,
|
|
125
125
|
contrastRatio,
|
|
126
126
|
deriveAccessKey,
|
|
127
|
+
deriveTransformKid,
|
|
127
128
|
extractAssetSha,
|
|
128
129
|
getAmbientGradient,
|
|
129
130
|
getAssetDimensions,
|
|
@@ -149,6 +150,9 @@ export {
|
|
|
149
150
|
isRequestablePreset,
|
|
150
151
|
isUniversallyPlayableAudio,
|
|
151
152
|
iteratePaletteSwatches,
|
|
153
|
+
MAX_SIGNED_TRANSFORM_TTL_SECONDS,
|
|
154
|
+
MAX_SIGNED_URL_TTL_SECONDS,
|
|
155
|
+
MIN_SIGNED_TRANSFORM_TTL_SECONDS,
|
|
152
156
|
mimeFromFileName,
|
|
153
157
|
type PaletteSwatch,
|
|
154
158
|
PRESET_EXT,
|
|
@@ -156,6 +160,7 @@ export {
|
|
|
156
160
|
PRESET_MAX_DIM,
|
|
157
161
|
PRESET_SHORT,
|
|
158
162
|
type PresignUploadUrlOptions,
|
|
163
|
+
type PrivateTransformOptions,
|
|
159
164
|
pickAmbientBackground,
|
|
160
165
|
REQUESTABLE_PRESETS,
|
|
161
166
|
type RegenerateResult,
|
|
@@ -166,6 +171,7 @@ export {
|
|
|
166
171
|
resolveSlots,
|
|
167
172
|
type SignAccessOptions,
|
|
168
173
|
type SignedTransformOptions,
|
|
174
|
+
type SignTransformOptions,
|
|
169
175
|
type SlotDTO,
|
|
170
176
|
type SlotHistoryEntry,
|
|
171
177
|
type SlotResolution,
|
|
@@ -182,6 +188,7 @@ export {
|
|
|
182
188
|
type TransformOptions,
|
|
183
189
|
type TransformWidth,
|
|
184
190
|
toRequestablePresets,
|
|
191
|
+
transformMessage,
|
|
185
192
|
type UploadOptions,
|
|
186
193
|
type UploadResult,
|
|
187
194
|
type UploadUrlResult,
|