@nitida/sdk 0.33.0 → 0.36.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 +5 -5
- package/dist/index.js +28 -4
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +28 -4
- package/dist/server.js.map +1 -1
- package/dist/web.d.ts +1 -1
- package/dist/web.js +28 -4
- package/dist/web.js.map +1 -1
- package/package.json +2 -2
- package/skills/nitida-sdk/SKILL.md +137 -6
- package/src/index.ts +40 -23
- package/src/server/index.ts +9 -0
- package/src/web/index.ts +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitida/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.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.24.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,52 @@ 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
|
+
- 🔴 **Since `@nitida/asset-client` 0.24.0 you CANNOT pass it.** `quality` is gone from
|
|
219
|
+
`TransformOptions`, so `getTransformUrl(a, { width, quality: 75 })` is a **compile error**.
|
|
220
|
+
There is nothing to remember and nothing to get wrong. If you are reading older code or older
|
|
221
|
+
docs that pass it, that code no longer compiles — delete the key, do not look for a replacement.
|
|
222
|
+
- **The one narrow legitimate case has its own door, and it cannot be called blind:**
|
|
223
|
+
`getByteBudgetTransformUrl(asset, { …, quality })`. It requires a `presets` string on the asset
|
|
224
|
+
(a bare `{ sha }` does not compile) and **throws** if the asset has any size variant, or if
|
|
225
|
+
`presets` is missing — *unknown is not permission*. Use it only for a byte budget somebody
|
|
226
|
+
measured, on an asset uploaded `presets: ["original"]`.
|
|
227
|
+
- **`quality: "auto"` was a NO-OP** — byte-for-byte identical to omitting the key.
|
|
228
|
+
Measured 2026-08-31 on a production laddered asset at `width=1920`: both
|
|
229
|
+
answered **136 680 B, sha256 `3a9ba57d…`, `x-transform-source: original`**.
|
|
230
|
+
⇒ The whole option is unnecessary, which is why `TransformOptions.quality`
|
|
231
|
+
carries an `@deprecated` tag since `@nitida/asset-client` 0.23.0. Your editor
|
|
232
|
+
strikes it through; that is on purpose.
|
|
233
|
+
- **A second corpus, where there is not even a byte saving.** 5056 px
|
|
234
|
+
architectural renders (tenant `suenos-del-mar`, 2026-08-31), referenced
|
|
235
|
+
against `/t/format=png,width=W/` — the lossless auto path at the same width:
|
|
236
|
+
|
|
237
|
+
| width | pinned source | bytes | PSNR |
|
|
238
|
+
|---|---|---|---|
|
|
239
|
+
| 3840 | `xl` | **+7.2 %** | **−0.50 dB** |
|
|
240
|
+
| 1920 | `lg` | **+8.1 %** | **−0.64 dB** |
|
|
241
|
+
| 1280 | `md` | **+2.0 %** | **−0.85 dB** |
|
|
242
|
+
|
|
243
|
+
Heavier **and** worse, 3 of 3. The photograph corpus above at least came back
|
|
244
|
+
smaller — that is exactly what hid this for four months.
|
|
245
|
+
|
|
246
|
+
### ✅ The one-line check: `x-transform-source`
|
|
247
|
+
|
|
248
|
+
**Do not reason about which path you got — read it off the response.** The
|
|
249
|
+
header names the variant the edge actually decoded:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
curl -sI 'https://8ok.uk/t/format=webp,width=1920/<sha16>.webp' \
|
|
253
|
+
| grep -i x-transform-source
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
| it says | you got |
|
|
257
|
+
|---|---|
|
|
258
|
+
| `original` | **one** lossy pass ✅ |
|
|
259
|
+
| `md` / `lg` / `xl` / `sm` | **two** — a numeric `quality` is in your URL ✖ |
|
|
260
|
+
| `poster` | you asked `/t/` for a video sha; it transformed the poster frame |
|
|
261
|
+
|
|
262
|
+
Run it once per surface after a change. It costs nothing and it is the only
|
|
263
|
+
statement about compression that is not an inference.
|
|
209
264
|
|
|
210
265
|
### What running this recipe caught
|
|
211
266
|
|
|
@@ -602,7 +657,13 @@ await getPrivateTransformUrl(asset, { width: 1280 }, signingKey, { expiresInSeco
|
|
|
602
657
|
`"public"`, or a bare `{ sha }`, is never refused — and this is a DIFFERENT
|
|
603
658
|
rule from the missing-preset fallback below, which is about presets, not
|
|
604
659
|
privacy.
|
|
605
|
-
- **`exp` is mandatory
|
|
660
|
+
- **`exp` is mandatory and CAPPED at 7 days.** `expiresInSeconds` over that
|
|
661
|
+
throws in the SDK and 401s (`exp_too_far`) at the platform — an expiry that
|
|
662
|
+
never arrives is not an expiry. Revocation is *"within a minute"* (60 s TTL
|
|
663
|
+
at the edge).
|
|
664
|
+
- **⚠️ A signature does NOT widen the width ladder.** Off-ladder widths are 400
|
|
665
|
+
whether or not the URL is signed. Signing buys IDENTITY (which tenant asked,
|
|
666
|
+
until `exp`), which is what `strict_transforms` and `effect=genfill` need.
|
|
606
667
|
- **The signing key is a backend secret** — it mints URLs for every private
|
|
607
668
|
asset the tenant owns.
|
|
608
669
|
- **⭐ Where the signing key comes from: the response that CREATED your
|
|
@@ -610,10 +671,15 @@ await getPrivateTransformUrl(asset, { width: 1280 }, signingKey, { expiresInSeco
|
|
|
610
671
|
three API keys, and the console shows it in the same panel. Nothing else
|
|
611
672
|
hands it out — `GET /admin/projects/:code` does **not** include it. If it is
|
|
612
673
|
lost, the only endpoint that returns a key is
|
|
613
|
-
`POST /admin/projects/:code/rotate-signing-key`,
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
674
|
+
`POST /admin/projects/:code/rotate-signing-key`, and it needs the
|
|
675
|
+
system-scope key the platform operator holds.
|
|
676
|
+
- **⭐ Rotating no longer kills your `/t/` URLs.** Since `@nitida/sdk` 0.31.0 a
|
|
677
|
+
signed transform URL carries `kid` (which key signed it) and `exp`, and the
|
|
678
|
+
OUTGOING key keeps verifying for 14 days after a rotation. ⚠️ The exception
|
|
679
|
+
is `/a/…` ACCESS URLs: their key is derived from `signing_key` with no `kid`,
|
|
680
|
+
so those 401 immediately on rotation. They are minted per view with
|
|
681
|
+
minute-scale expiries, so the burst heals itself — but rotate at a quiet hour
|
|
682
|
+
if the tenant serves private assets.
|
|
617
683
|
- **⭐ Already have a project and never saw a signing key?** Then you never got
|
|
618
684
|
one — projects created before 2026-08-23 were not handed it, and no endpoint
|
|
619
685
|
shows you the current one. **If you have not signed any URLs yet, ask us to
|
|
@@ -622,6 +688,69 @@ await getPrivateTransformUrl(asset, { width: 1280 }, signingKey, { expiresInSeco
|
|
|
622
688
|
instead. This is the wall the T5 agent hit, and the sentence that was
|
|
623
689
|
missing.
|
|
624
690
|
|
|
691
|
+
## ⚠️ EXIF orientation — two dimension pairs, and mixing them stretches the photo
|
|
692
|
+
|
|
693
|
+
**A phone taking a portrait photo does not store portrait pixels.** It writes the
|
|
694
|
+
sensor buffer landscape and tags it `Orientation` 5–8 meaning "rotate before
|
|
695
|
+
showing". So every such file has **two** pairs, transposes of each other:
|
|
696
|
+
|
|
697
|
+
```
|
|
698
|
+
4032×3024 the STORED buffer (what a header parser reads)
|
|
699
|
+
3024×4032 what it DISPLAYS as (what every decoder hands you)
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
Use the **stored** pair only for orientation-invariant quantities — area, a
|
|
703
|
+
megapixel budget. Use the **displayed** pair for anything geometric: a resize
|
|
704
|
+
target, an aspect ratio, a layout box, a coordinate you denormalise.
|
|
705
|
+
|
|
706
|
+
**This exact confusion shipped twice, on both sides of the wire:**
|
|
707
|
+
|
|
708
|
+
| where | what happened | fixed |
|
|
709
|
+
|---|---|---|
|
|
710
|
+
| `@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** |
|
|
711
|
+
| `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 |
|
|
712
|
+
|
|
713
|
+
Needed all three, which is why it looked random: a parseable header
|
|
714
|
+
(**HEIC is immune** — the ISOBMFF box walk isn't implemented, so no dims, so no
|
|
715
|
+
forced resize), orientation **5–8**, and a long side **above** the box.
|
|
716
|
+
|
|
717
|
+
⇒ **Damage is pre-upload and unrecoverable.** The raw bucket faithfully stores the
|
|
718
|
+
already-stretched bytes. Affected photos must be re-uploaded from the original;
|
|
719
|
+
no backfill exists.
|
|
720
|
+
|
|
721
|
+
### The two traps, concretely
|
|
722
|
+
|
|
723
|
+
**Browser — `createImageBitmap` with BOTH resize axes does not preserve ratio.**
|
|
724
|
+
|
|
725
|
+
```ts
|
|
726
|
+
// 🔴 any disagreement with the decoded bitmap becomes a stretch
|
|
727
|
+
createImageBitmap(blob, { imageOrientation: "from-image", resizeWidth: w, resizeHeight: h })
|
|
728
|
+
// ✅ one axis: the spec derives the other, ratio survives BY CONSTRUCTION
|
|
729
|
+
createImageBitmap(blob, { imageOrientation: "from-image", resizeWidth: w })
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
**Node — `sharp(x).rotate().metadata()` does NOT apply the rotation.** `.rotate()`
|
|
733
|
+
queues an operation; `.metadata()` still reads the input. Measured, sharp 0.34.5:
|
|
734
|
+
|
|
735
|
+
```
|
|
736
|
+
sharp(buf).metadata() → 4032×3024, orientation 6
|
|
737
|
+
sharp(buf).rotate().metadata() → 4032×3024 ← the false idiom
|
|
738
|
+
sharp(buf).rotate().toBuffer() → info → 3024×4032 ← the truth
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
It read so plausibly it was written three times in one repo, once directly under
|
|
742
|
+
the comment *"Source dims (EXIF-rotated for correctness)"*. To get display dims:
|
|
743
|
+
transpose the raw pair yourself when `orientation` is 5–8, or measure the output
|
|
744
|
+
buffer.
|
|
745
|
+
|
|
746
|
+
**And the lesson that cost the most:** the stretch had been suspected once and
|
|
747
|
+
declared *"refuted with production data"* because storage held four distinct
|
|
748
|
+
aspect ratios — "a pipeline that forced a ratio would have produced ONE". It
|
|
749
|
+
never forced one ratio; it forced **each photo's own transpose**. Counting
|
|
750
|
+
distinct ratios could not detect it, so finding several was evidence of nothing.
|
|
751
|
+
**An invariant you cannot state is an invariant you cannot test** — compare
|
|
752
|
+
against the *displayed* ratio, or you are comparing stored ratios to each other.
|
|
753
|
+
|
|
625
754
|
## Two ways an image gets smaller, and only one of them is yours to call
|
|
626
755
|
|
|
627
756
|
This is the question every programmatic caller gets wrong, so it is stated flat:
|
|
@@ -696,6 +825,8 @@ needed. Reproduce there before debugging any of these by hand.
|
|
|
696
825
|
|
|
697
826
|
| Symptom | Cause |
|
|
698
827
|
|---|---|
|
|
828
|
+
| **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 |
|
|
829
|
+
| **`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 |
|
|
699
830
|
| **400** on an image URL | width not on `TRANSFORM_WIDTHS` ladder |
|
|
700
831
|
| **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')` |
|
|
701
832
|
| **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,
|
|
@@ -259,6 +265,7 @@ export {
|
|
|
259
265
|
getHlsStreamingUrl,
|
|
260
266
|
getPaletteBlurBackground,
|
|
261
267
|
getPaletteCssVars,
|
|
268
|
+
getByteBudgetTransformUrl,
|
|
262
269
|
getPrivateAssetUrl,
|
|
263
270
|
getPrivateTransformUrl,
|
|
264
271
|
getSignedTransformUrl,
|
|
@@ -268,10 +275,14 @@ export {
|
|
|
268
275
|
getTransformUrl,
|
|
269
276
|
getVideoTransformUrl,
|
|
270
277
|
hasPreset,
|
|
278
|
+
hasSizeLadder,
|
|
271
279
|
hlsLadderAlignment,
|
|
272
280
|
invalidateSlotCache,
|
|
273
281
|
isRequestablePreset,
|
|
274
282
|
iteratePaletteSwatches,
|
|
283
|
+
MAX_SIGNED_TRANSFORM_TTL_SECONDS,
|
|
284
|
+
MAX_SIGNED_URL_TTL_SECONDS,
|
|
285
|
+
MIN_SIGNED_TRANSFORM_TTL_SECONDS,
|
|
275
286
|
PRESET_EXT,
|
|
276
287
|
PRESET_LONG,
|
|
277
288
|
PRESET_MAX_DIM,
|
|
@@ -288,6 +299,7 @@ export {
|
|
|
288
299
|
signTransformUrl,
|
|
289
300
|
TRANSFORM_WIDTHS,
|
|
290
301
|
toRequestablePresets,
|
|
302
|
+
transformMessage,
|
|
291
303
|
} from "@nitida/asset-client";
|
|
292
304
|
|
|
293
305
|
// ---------------------------------------------------------------------------
|
|
@@ -1477,27 +1489,21 @@ export class NitidaClient {
|
|
|
1477
1489
|
*/
|
|
1478
1490
|
// Overload 1: no signing — synchronous, on-ladder width only (strict).
|
|
1479
1491
|
transform(asset: Pick<AssetDTO, "sha">, opts?: TransformOptions): string;
|
|
1480
|
-
// Overload 2: with { sign: true } — async; returns
|
|
1481
|
-
//
|
|
1482
|
-
//
|
|
1492
|
+
// Overload 2: with { sign: true, expiresInSeconds } — async; returns a
|
|
1493
|
+
// `?kid&exp&sig` URL. The width ladder is the SAME as unsigned: a signature
|
|
1494
|
+
// names the caller, it does not widen the image (doc blindaje WS-6).
|
|
1483
1495
|
transform(
|
|
1484
1496
|
asset: Pick<AssetDTO, "sha">,
|
|
1485
1497
|
opts: SignedTransformOptions,
|
|
1486
|
-
signOpts: { sign: true },
|
|
1498
|
+
signOpts: { sign: true } & SignTransformOptions,
|
|
1487
1499
|
): Promise<string>;
|
|
1488
1500
|
transform(
|
|
1489
1501
|
asset: Pick<AssetDTO, "sha">,
|
|
1490
1502
|
opts: SignedTransformOptions = {},
|
|
1491
|
-
signOpts?: { sign: true },
|
|
1503
|
+
signOpts?: { sign: true } & SignTransformOptions,
|
|
1492
1504
|
): string | Promise<string> {
|
|
1493
1505
|
if (!signOpts?.sign) {
|
|
1494
|
-
|
|
1495
|
-
// public call site, so an off-ladder width can't reach here through the
|
|
1496
|
-
// typed API — narrow back to TransformOptions for the strict builder.
|
|
1497
|
-
return (
|
|
1498
|
-
getTransformUrl(asset, opts as TransformOptions) ??
|
|
1499
|
-
this.urlFor(asset, "lg")
|
|
1500
|
-
);
|
|
1506
|
+
return getTransformUrl(asset, opts) ?? this.urlFor(asset, "lg");
|
|
1501
1507
|
}
|
|
1502
1508
|
if (!this.opts.signingKey) {
|
|
1503
1509
|
throw new Error(
|
|
@@ -1505,11 +1511,15 @@ export class NitidaClient {
|
|
|
1505
1511
|
"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.",
|
|
1506
1512
|
);
|
|
1507
1513
|
}
|
|
1508
|
-
//
|
|
1509
|
-
//
|
|
1514
|
+
// Empty opts → no transform DSL, fall back to the unsigned `lg` variant
|
|
1515
|
+
// URL. `tenantId` defaults to the one this client was constructed with,
|
|
1516
|
+
// because the tenant is part of the signed message.
|
|
1510
1517
|
return (
|
|
1511
|
-
getSignedTransformUrl(asset, opts, this.opts.signingKey
|
|
1512
|
-
|
|
1518
|
+
getSignedTransformUrl(asset, opts, this.opts.signingKey, {
|
|
1519
|
+
expiresInSeconds: signOpts.expiresInSeconds,
|
|
1520
|
+
tenantId: signOpts.tenantId ?? this.opts.tenantId,
|
|
1521
|
+
nowSeconds: signOpts.nowSeconds,
|
|
1522
|
+
}) ?? Promise.resolve(this.urlFor(asset, "lg"))
|
|
1513
1523
|
);
|
|
1514
1524
|
}
|
|
1515
1525
|
|
|
@@ -1527,15 +1537,15 @@ export class NitidaClient {
|
|
|
1527
1537
|
): string;
|
|
1528
1538
|
transformSrcSet(
|
|
1529
1539
|
asset: Pick<AssetDTO, "sha">,
|
|
1530
|
-
widths:
|
|
1540
|
+
widths: TransformWidth[],
|
|
1531
1541
|
extraOpts: Omit<TransformOptions, "width">,
|
|
1532
|
-
signOpts: { sign: true },
|
|
1542
|
+
signOpts: { sign: true } & SignTransformOptions,
|
|
1533
1543
|
): Promise<string>;
|
|
1534
1544
|
transformSrcSet(
|
|
1535
1545
|
asset: Pick<AssetDTO, "sha">,
|
|
1536
1546
|
widths: number[],
|
|
1537
1547
|
extraOpts: Omit<TransformOptions, "width"> = {},
|
|
1538
|
-
signOpts?: { sign: true },
|
|
1548
|
+
signOpts?: { sign: true } & SignTransformOptions,
|
|
1539
1549
|
): string | Promise<string> {
|
|
1540
1550
|
if (!signOpts?.sign) return getTransformSrcSet(asset, widths, extraOpts);
|
|
1541
1551
|
if (!this.opts.signingKey) {
|
|
@@ -1545,15 +1555,22 @@ export class NitidaClient {
|
|
|
1545
1555
|
);
|
|
1546
1556
|
}
|
|
1547
1557
|
const key = this.opts.signingKey;
|
|
1558
|
+
const sign = {
|
|
1559
|
+
expiresInSeconds: signOpts.expiresInSeconds,
|
|
1560
|
+
tenantId: signOpts.tenantId ?? this.opts.tenantId,
|
|
1561
|
+
nowSeconds: signOpts.nowSeconds,
|
|
1562
|
+
};
|
|
1548
1563
|
return Promise.all(
|
|
1549
1564
|
widths.map(async (w) => {
|
|
1550
|
-
//
|
|
1551
|
-
//
|
|
1552
|
-
//
|
|
1565
|
+
// ⚠️ The overload types `widths` as `TransformWidth[]` on the signed
|
|
1566
|
+
// path. Off-ladder widths 400 at the edge whether or not they are
|
|
1567
|
+
// signed (doc blindaje WS-6), so a signed srcSet full of custom widths
|
|
1568
|
+
// is a srcSet of 400s — the type says so at compile time now.
|
|
1553
1569
|
const signed = await getSignedTransformUrl(
|
|
1554
1570
|
asset,
|
|
1555
|
-
{ ...extraOpts, width: w },
|
|
1571
|
+
{ ...extraOpts, width: w as TransformWidth },
|
|
1556
1572
|
key,
|
|
1573
|
+
sign,
|
|
1557
1574
|
);
|
|
1558
1575
|
return signed ? `${signed} ${w}w` : null;
|
|
1559
1576
|
}),
|
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,
|
|
@@ -119,6 +120,7 @@ export {
|
|
|
119
120
|
getPaletteCssVars,
|
|
120
121
|
getPrivateAssetUrl,
|
|
121
122
|
getPrivateTransformUrl,
|
|
123
|
+
getByteBudgetTransformUrl,
|
|
122
124
|
getSignedTransformUrl,
|
|
123
125
|
getTenantId,
|
|
124
126
|
getTextColorForBackground,
|
|
@@ -127,11 +129,15 @@ export {
|
|
|
127
129
|
getVideoTransformUrl,
|
|
128
130
|
type HlsRung,
|
|
129
131
|
hasPreset,
|
|
132
|
+
hasSizeLadder,
|
|
130
133
|
hlsLadderAlignment,
|
|
131
134
|
invalidateSlotCache,
|
|
132
135
|
isRequestablePreset,
|
|
133
136
|
isUniversallyPlayableAudio,
|
|
134
137
|
iteratePaletteSwatches,
|
|
138
|
+
MAX_SIGNED_TRANSFORM_TTL_SECONDS,
|
|
139
|
+
MAX_SIGNED_URL_TTL_SECONDS,
|
|
140
|
+
MIN_SIGNED_TRANSFORM_TTL_SECONDS,
|
|
135
141
|
mimeFromFileName,
|
|
136
142
|
type NitidaClientOptions,
|
|
137
143
|
type PaletteSwatch,
|
|
@@ -140,6 +146,7 @@ export {
|
|
|
140
146
|
PRESET_MAX_DIM,
|
|
141
147
|
PRESET_SHORT,
|
|
142
148
|
type PresignUploadUrlOptions,
|
|
149
|
+
type PrivateTransformOptions,
|
|
143
150
|
pickAmbientBackground,
|
|
144
151
|
REQUESTABLE_PRESETS,
|
|
145
152
|
type RegenerateResult,
|
|
@@ -150,6 +157,7 @@ export {
|
|
|
150
157
|
resolveSlots,
|
|
151
158
|
type SignAccessOptions,
|
|
152
159
|
type SignedTransformOptions,
|
|
160
|
+
type SignTransformOptions,
|
|
153
161
|
type SlotDTO,
|
|
154
162
|
type SlotHistoryEntry,
|
|
155
163
|
type SlotResolution,
|
|
@@ -166,6 +174,7 @@ export {
|
|
|
166
174
|
type TransformOptions,
|
|
167
175
|
type TransformWidth,
|
|
168
176
|
toRequestablePresets,
|
|
177
|
+
transformMessage,
|
|
169
178
|
type UploadOptions,
|
|
170
179
|
type UploadResult,
|
|
171
180
|
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,
|
|
@@ -136,6 +137,7 @@ export {
|
|
|
136
137
|
getPaletteCssVars,
|
|
137
138
|
getPrivateAssetUrl,
|
|
138
139
|
getPrivateTransformUrl,
|
|
140
|
+
getByteBudgetTransformUrl,
|
|
139
141
|
getSignedTransformUrl,
|
|
140
142
|
getTenantId,
|
|
141
143
|
getTextColorForBackground,
|
|
@@ -144,11 +146,15 @@ export {
|
|
|
144
146
|
getVideoTransformUrl,
|
|
145
147
|
type HlsRung,
|
|
146
148
|
hasPreset,
|
|
149
|
+
hasSizeLadder,
|
|
147
150
|
hlsLadderAlignment,
|
|
148
151
|
invalidateSlotCache,
|
|
149
152
|
isRequestablePreset,
|
|
150
153
|
isUniversallyPlayableAudio,
|
|
151
154
|
iteratePaletteSwatches,
|
|
155
|
+
MAX_SIGNED_TRANSFORM_TTL_SECONDS,
|
|
156
|
+
MAX_SIGNED_URL_TTL_SECONDS,
|
|
157
|
+
MIN_SIGNED_TRANSFORM_TTL_SECONDS,
|
|
152
158
|
mimeFromFileName,
|
|
153
159
|
type PaletteSwatch,
|
|
154
160
|
PRESET_EXT,
|
|
@@ -156,6 +162,7 @@ export {
|
|
|
156
162
|
PRESET_MAX_DIM,
|
|
157
163
|
PRESET_SHORT,
|
|
158
164
|
type PresignUploadUrlOptions,
|
|
165
|
+
type PrivateTransformOptions,
|
|
159
166
|
pickAmbientBackground,
|
|
160
167
|
REQUESTABLE_PRESETS,
|
|
161
168
|
type RegenerateResult,
|
|
@@ -166,6 +173,7 @@ export {
|
|
|
166
173
|
resolveSlots,
|
|
167
174
|
type SignAccessOptions,
|
|
168
175
|
type SignedTransformOptions,
|
|
176
|
+
type SignTransformOptions,
|
|
169
177
|
type SlotDTO,
|
|
170
178
|
type SlotHistoryEntry,
|
|
171
179
|
type SlotResolution,
|
|
@@ -182,6 +190,7 @@ export {
|
|
|
182
190
|
type TransformOptions,
|
|
183
191
|
type TransformWidth,
|
|
184
192
|
toRequestablePresets,
|
|
193
|
+
transformMessage,
|
|
185
194
|
type UploadOptions,
|
|
186
195
|
type UploadResult,
|
|
187
196
|
type UploadUrlResult,
|