@nitida/sdk 0.33.0 → 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 +5 -5
- package/dist/index.js +24 -4
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +24 -4
- package/dist/server.js.map +1 -1
- package/dist/web.d.ts +1 -1
- package/dist/web.js +24 -4
- package/dist/web.js.map +1 -1
- package/package.json +2 -2
- package/skills/nitida-sdk/SKILL.md +128 -6
- package/src/index.ts +38 -23
- package/src/server/index.ts +7 -0
- package/src/web/index.ts +7 -0
package/AGENTS.md
CHANGED
|
@@ -118,6 +118,32 @@ await getPrivateTransformUrl(asset, { width: 1280 }, signingKey, { expiresInSeco
|
|
|
118
118
|
instead. This is the wall the T5 agent hit, and the sentence that was
|
|
119
119
|
missing.
|
|
120
120
|
|
|
121
|
+
## ⚠️ EXIF orientation — two dimension pairs, and mixing them stretches the photo
|
|
122
|
+
|
|
123
|
+
A phone taking a portrait photo does not store portrait pixels: it writes the
|
|
124
|
+
sensor buffer landscape and tags it `Orientation` 5–8. Every such file has two
|
|
125
|
+
pairs, transposes of each other — `4032×3024` stored, `3024×4032` displayed.
|
|
126
|
+
|
|
127
|
+
Stored pair → only orientation-invariant quantities (area, a megapixel budget).
|
|
128
|
+
Displayed pair → anything geometric: a resize target, an aspect ratio, a layout
|
|
129
|
+
box, a coordinate you denormalise.
|
|
130
|
+
|
|
131
|
+
Mixing them shipped twice: `@nitida/asset-compressor-web` 0.6.0–0.6.2 stretched
|
|
132
|
+
every affected photo **1.78×** (fixed in **0.6.3**), and `asset-manager` recorded
|
|
133
|
+
the stored pair as the asset's `w`/`h` until 2026-08-28. Both needed orientation
|
|
134
|
+
5–8 plus a long side above the box, and **HEIC was immune** — which is why it
|
|
135
|
+
looked random. The damage is pre-upload, so affected photos must be re-uploaded,
|
|
136
|
+
not repaired.
|
|
137
|
+
|
|
138
|
+
Two traps worth naming:
|
|
139
|
+
|
|
140
|
+
- **`createImageBitmap` with BOTH resize axes does not preserve the ratio.** Pass
|
|
141
|
+
one axis; the spec derives the other and the proportions survive by
|
|
142
|
+
construction.
|
|
143
|
+
- **`sharp(x).rotate().metadata()` does NOT apply the rotation** — `.rotate()`
|
|
144
|
+
queues an operation, `.metadata()` reads the input. Measured on sharp 0.34.5:
|
|
145
|
+
it returns `4032×3024` while `.rotate().toBuffer()` returns `3024×4032`.
|
|
146
|
+
|
|
121
147
|
## Two ways an image gets smaller, and only one of them is yours to call
|
|
122
148
|
|
|
123
149
|
This is the question every programmatic caller gets wrong, so it is stated flat:
|
package/README.md
CHANGED
|
@@ -568,6 +568,28 @@ generated server-side by the asset-manager's variant pipeline. A tenant
|
|
|
568
568
|
cannot define custom dimensions through the SDK; they pick which preset a
|
|
569
569
|
slot defaults to and emit responsive `srcSet` for browser-side resizing.
|
|
570
570
|
|
|
571
|
+
### Preset short codes — generated, do not edit by hand
|
|
572
|
+
|
|
573
|
+
A variant URL carries the **code**, not the preset name (`/v/<sha>-l.webp`,
|
|
574
|
+
not `-lg.webp`). Copy the letter from here — `bun run gen:docs` regenerates
|
|
575
|
+
this table from the source, so it can't have the wrong one.
|
|
576
|
+
|
|
577
|
+
<!-- BEGIN GENERATED: preset-codes · bun run gen:docs -->
|
|
578
|
+
| Preset | Code |
|
|
579
|
+
|---|---|
|
|
580
|
+
| `thumb` | `q` |
|
|
581
|
+
| `sm` | `s` |
|
|
582
|
+
| `md` | `m` |
|
|
583
|
+
| `lg` | `l` |
|
|
584
|
+
| `xl` | `x` |
|
|
585
|
+
| `original` | `o` |
|
|
586
|
+
| `poster` | `p` |
|
|
587
|
+
| `video` | `v` |
|
|
588
|
+
| `aiproxy` | `a` |
|
|
589
|
+
| `hls` | `h` |
|
|
590
|
+
| `mp3` | `mp3` |
|
|
591
|
+
<!-- END GENERATED: preset-codes -->
|
|
592
|
+
|
|
571
593
|
### Image presets
|
|
572
594
|
|
|
573
595
|
| Preset | Code | Max-side | Typical use |
|
|
@@ -875,10 +897,13 @@ const asset = await aq.assets.byHash(sha256);
|
|
|
875
897
|
| `dpr` | `1` / `2` / `3` | `1` |
|
|
876
898
|
|
|
877
899
|
> **`width` is strongly typed.** `TransformOptions.width` is a **`TransformWidth`** — the
|
|
878
|
-
> predefined CDN ladder
|
|
879
|
-
>
|
|
880
|
-
|
|
881
|
-
|
|
900
|
+
> predefined CDN ladder below, exported as `TRANSFORM_WIDTHS`. An off-ladder width is a
|
|
901
|
+
> **compile error**.
|
|
902
|
+
|
|
903
|
+
<!-- BEGIN GENERATED: transform-widths · bun run gen:docs -->
|
|
904
|
+
`96, 128, 160, 180, 240, 256, 320, 400, 480, 600, 640, 800, 960, 1080, 1200, 1280, 1440, 1600, 1920, 2560, 3840` — 21 widths.
|
|
905
|
+
<!-- END GENERATED: transform-widths -->
|
|
906
|
+
|
|
882
907
|
> ⚠️ **The type is narrower than the edge, on purpose.** The edge whitelists a
|
|
883
908
|
> longer list (28 today — its own 400 response enumerates them, and `512`, `768`,
|
|
884
909
|
> `1800`, `2160`, `2400`, `2700`, `2880` and `3600` all serve 200 unsigned). So a
|
|
@@ -1001,22 +1026,49 @@ const aq = new NitidaClient({
|
|
|
1001
1026
|
});
|
|
1002
1027
|
|
|
1003
1028
|
// Async when { sign: true } is set — overload returns Promise<string>.
|
|
1004
|
-
|
|
1005
|
-
//
|
|
1029
|
+
// `expiresInSeconds` is REQUIRED: a signature with no expiry is a bearer
|
|
1030
|
+
// token with no expiry. 120 s minimum, 7 days maximum.
|
|
1031
|
+
const signed = await aq.transform(
|
|
1032
|
+
asset,
|
|
1033
|
+
{ width: 1280 },
|
|
1034
|
+
{ sign: true, expiresInSeconds: 3600 },
|
|
1035
|
+
);
|
|
1036
|
+
// → https://8ok.uk/t/width=1280/<sha>.webp?kid=<8-hex>&exp=<unix>&sig=<64-hex>
|
|
1006
1037
|
|
|
1007
1038
|
// Responsive
|
|
1008
1039
|
const srcset = await aq.transformSrcSet(
|
|
1009
1040
|
asset,
|
|
1010
1041
|
[640, 960, 1280, 1920],
|
|
1011
1042
|
{},
|
|
1012
|
-
{ sign: true },
|
|
1043
|
+
{ sign: true, expiresInSeconds: 3600 },
|
|
1013
1044
|
);
|
|
1014
1045
|
```
|
|
1015
1046
|
|
|
1016
|
-
Signature shape:
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1047
|
+
Signature shape:
|
|
1048
|
+
|
|
1049
|
+
```
|
|
1050
|
+
message = "nitida/transform/v2\n<tenantId base36>\n<exp>\n<canonical-DSL>/<filename>"
|
|
1051
|
+
sig = HMAC-SHA256(signingKey, message) // hex
|
|
1052
|
+
kid = HMAC-SHA256(signingKey, "nitida/kid/v1").slice(0, 8)
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
The server canonicalizes the URL the same way the SDK does (sort keys,
|
|
1056
|
+
lowercase strings), so two URLs with the same params in different order accept
|
|
1057
|
+
the same signature.
|
|
1058
|
+
|
|
1059
|
+
`kid` names the key so a rotation does not kill URLs already in flight: the
|
|
1060
|
+
outgoing key keeps verifying for 14 days
|
|
1061
|
+
(`POST /admin/projects/:code/rotate-signing-key` returns the new key, its `kid`,
|
|
1062
|
+
and when the previous one retires). See
|
|
1063
|
+
[the rotation runbook](https://github.com/espaciofuturoio/aquienpz/blob/main/docs/RUNBOOKS/signing-key-rotation.md).
|
|
1064
|
+
|
|
1065
|
+
> ⚠️ **A signature does NOT widen the ladder.** Off-ladder widths are 400 at
|
|
1066
|
+
> the edge whether or not the URL is signed. Until `@nitida/asset-client`
|
|
1067
|
+
> 0.22.0 a valid-looking `?sig=` skipped the edge whitelist entirely, so a
|
|
1068
|
+
> signature bought an arbitrary width up to 7680 px — one request measured at
|
|
1069
|
+
> 1 850 MB. What a signature buys now is IDENTITY: which tenant asked, proven
|
|
1070
|
+
> with its key, until `exp`. That is what `strict_transforms` and the
|
|
1071
|
+
> `effect=genfill` cost guard require.
|
|
1020
1072
|
|
|
1021
1073
|
## Private assets — `visibility`
|
|
1022
1074
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ResolveSlotOptions, SlotResolution, SlotDTO, VariantPreset, AssetDTO, AssetVariant, RequestablePreset, TransformOptions, SignedTransformOptions } from '@nitida/asset-client';
|
|
2
|
-
export { AssetDTO, AssetPalette, AssetVariant, HlsRung, PRESET_EXT, PRESET_LONG, PRESET_MAX_DIM, PRESET_SHORT, PaletteSwatch, REQUESTABLE_PRESETS, RequestablePreset, ResolveSlotOptions, SignAccessOptions, SignedTransformOptions, SlotDTO, SlotResolution, TRANSFORM_WIDTHS, TransformEffect, TransformFit, TransformFormat, TransformGravity, TransformOptions, TransformWidth, VariantEntryPreset, VariantPreset, VisibilityHint, accessMessage, assertPublic, assertSha, bestTextContrast, computeVariantDimensions, configureSlotResolver, contrastRatio, deriveAccessKey, extractAssetSha, getAmbientGradient, getAssetDimensions, getAssetSrcSet, getAssetUrl, getCdnBase, getHlsLadder, getHlsStreamingUrl, getPaletteBlurBackground, getPaletteCssVars, getPrivateAssetUrl, getPrivateTransformUrl, getSignedTransformUrl, getTenantId, getTextColorForBackground, getTransformSrcSet, getTransformUrl, getVideoTransformUrl, hasPreset, hlsLadderAlignment, invalidateSlotCache, isRequestablePreset, iteratePaletteSwatches, pickAmbientBackground, relativeLuminance, resolveSlot, resolveSlots, serializeTransform, setCdnBase, setTenantId, signAccessUrl, signTransformUrl, toRequestablePresets } from '@nitida/asset-client';
|
|
1
|
+
import { ResolveSlotOptions, SlotResolution, SlotDTO, VariantPreset, AssetDTO, AssetVariant, RequestablePreset, TransformOptions, SignedTransformOptions, SignTransformOptions, TransformWidth } from '@nitida/asset-client';
|
|
2
|
+
export { AssetDTO, AssetPalette, AssetVariant, HlsRung, MAX_SIGNED_TRANSFORM_TTL_SECONDS, MAX_SIGNED_URL_TTL_SECONDS, MIN_SIGNED_TRANSFORM_TTL_SECONDS, PRESET_EXT, PRESET_LONG, PRESET_MAX_DIM, PRESET_SHORT, PaletteSwatch, PrivateTransformOptions, REQUESTABLE_PRESETS, RequestablePreset, ResolveSlotOptions, SignAccessOptions, SignTransformOptions, SignedTransformOptions, SlotDTO, SlotResolution, TRANSFORM_WIDTHS, TransformEffect, TransformFit, TransformFormat, TransformGravity, TransformOptions, TransformWidth, VariantEntryPreset, VariantPreset, VisibilityHint, accessMessage, assertPublic, assertSha, bestTextContrast, computeVariantDimensions, configureSlotResolver, contrastRatio, deriveAccessKey, deriveTransformKid, extractAssetSha, getAmbientGradient, getAssetDimensions, getAssetSrcSet, getAssetUrl, getCdnBase, getHlsLadder, getHlsStreamingUrl, getPaletteBlurBackground, getPaletteCssVars, getPrivateAssetUrl, getPrivateTransformUrl, getSignedTransformUrl, getTenantId, getTextColorForBackground, getTransformSrcSet, getTransformUrl, getVideoTransformUrl, hasPreset, hlsLadderAlignment, invalidateSlotCache, isRequestablePreset, iteratePaletteSwatches, pickAmbientBackground, relativeLuminance, resolveSlot, resolveSlots, serializeTransform, setCdnBase, setTenantId, signAccessUrl, signTransformUrl, toRequestablePresets, transformMessage } from '@nitida/asset-client';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Which audio uploads already play everywhere — the single source of truth for
|
|
@@ -910,7 +910,7 @@ declare class NitidaClient {
|
|
|
910
910
|
transform(asset: Pick<AssetDTO, "sha">, opts?: TransformOptions): string;
|
|
911
911
|
transform(asset: Pick<AssetDTO, "sha">, opts: SignedTransformOptions, signOpts: {
|
|
912
912
|
sign: true;
|
|
913
|
-
}): Promise<string>;
|
|
913
|
+
} & SignTransformOptions): Promise<string>;
|
|
914
914
|
/**
|
|
915
915
|
* Build a responsive `srcSet` string. One transform URL per width; all
|
|
916
916
|
* other options apply to every URL.
|
|
@@ -919,9 +919,9 @@ declare class NitidaClient {
|
|
|
919
919
|
* call stays synchronous as before.
|
|
920
920
|
*/
|
|
921
921
|
transformSrcSet(asset: Pick<AssetDTO, "sha">, widths: number[], extraOpts?: Omit<TransformOptions, "width">): string;
|
|
922
|
-
transformSrcSet(asset: Pick<AssetDTO, "sha">, widths:
|
|
922
|
+
transformSrcSet(asset: Pick<AssetDTO, "sha">, widths: TransformWidth[], extraOpts: Omit<TransformOptions, "width">, signOpts: {
|
|
923
923
|
sign: true;
|
|
924
|
-
}): Promise<string>;
|
|
924
|
+
} & SignTransformOptions): Promise<string>;
|
|
925
925
|
/**
|
|
926
926
|
* Build an on-the-fly VIDEO transform URL — Phase 4.
|
|
927
927
|
*
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
configureSlotResolver as configureSlotResolver2,
|
|
33
33
|
contrastRatio,
|
|
34
34
|
deriveAccessKey,
|
|
35
|
+
deriveTransformKid,
|
|
35
36
|
extractAssetSha,
|
|
36
37
|
getAmbientGradient,
|
|
37
38
|
getAssetDimensions,
|
|
@@ -55,6 +56,9 @@ import {
|
|
|
55
56
|
invalidateSlotCache as invalidateSlotCache2,
|
|
56
57
|
isRequestablePreset as isRequestablePreset2,
|
|
57
58
|
iteratePaletteSwatches,
|
|
59
|
+
MAX_SIGNED_TRANSFORM_TTL_SECONDS,
|
|
60
|
+
MAX_SIGNED_URL_TTL_SECONDS,
|
|
61
|
+
MIN_SIGNED_TRANSFORM_TTL_SECONDS,
|
|
58
62
|
PRESET_EXT,
|
|
59
63
|
PRESET_LONG,
|
|
60
64
|
PRESET_MAX_DIM,
|
|
@@ -70,7 +74,8 @@ import {
|
|
|
70
74
|
signAccessUrl,
|
|
71
75
|
signTransformUrl,
|
|
72
76
|
TRANSFORM_WIDTHS,
|
|
73
|
-
toRequestablePresets as toRequestablePresets2
|
|
77
|
+
toRequestablePresets as toRequestablePresets2,
|
|
78
|
+
transformMessage
|
|
74
79
|
} from "@nitida/asset-client";
|
|
75
80
|
function endpointUrl(opts, path, searchParams) {
|
|
76
81
|
const endpoint = opts.endpoint.replace(/\/+$/, "");
|
|
@@ -600,7 +605,11 @@ var NitidaClient = class {
|
|
|
600
605
|
"aq.transform({ sign: true }) requires `signingKey` in NitidaClientOptions. No signingKey on this client. It is returned ONCE, in the response that creates your project (POST /admin/projects \u2192 `signingKey`, next to the three API keys; the console shows it in the same panel). If you never saw one \u2014 projects created before 2026-08-23 were not handed it \u2014 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."
|
|
601
606
|
);
|
|
602
607
|
}
|
|
603
|
-
return getSignedTransformUrl(asset, opts, this.opts.signingKey
|
|
608
|
+
return getSignedTransformUrl(asset, opts, this.opts.signingKey, {
|
|
609
|
+
expiresInSeconds: signOpts.expiresInSeconds,
|
|
610
|
+
tenantId: signOpts.tenantId ?? this.opts.tenantId,
|
|
611
|
+
nowSeconds: signOpts.nowSeconds
|
|
612
|
+
}) ?? Promise.resolve(this.urlFor(asset, "lg"));
|
|
604
613
|
}
|
|
605
614
|
transformSrcSet(asset, widths, extraOpts = {}, signOpts) {
|
|
606
615
|
if (!signOpts?.sign) return getTransformSrcSet(asset, widths, extraOpts);
|
|
@@ -610,12 +619,18 @@ var NitidaClient = class {
|
|
|
610
619
|
);
|
|
611
620
|
}
|
|
612
621
|
const key = this.opts.signingKey;
|
|
622
|
+
const sign = {
|
|
623
|
+
expiresInSeconds: signOpts.expiresInSeconds,
|
|
624
|
+
tenantId: signOpts.tenantId ?? this.opts.tenantId,
|
|
625
|
+
nowSeconds: signOpts.nowSeconds
|
|
626
|
+
};
|
|
613
627
|
return Promise.all(
|
|
614
628
|
widths.map(async (w) => {
|
|
615
629
|
const signed = await getSignedTransformUrl(
|
|
616
630
|
asset,
|
|
617
631
|
{ ...extraOpts, width: w },
|
|
618
|
-
key
|
|
632
|
+
key,
|
|
633
|
+
sign
|
|
619
634
|
);
|
|
620
635
|
return signed ? `${signed} ${w}w` : null;
|
|
621
636
|
})
|
|
@@ -952,6 +967,9 @@ var NitidaClient = class {
|
|
|
952
967
|
}
|
|
953
968
|
};
|
|
954
969
|
export {
|
|
970
|
+
MAX_SIGNED_TRANSFORM_TTL_SECONDS,
|
|
971
|
+
MAX_SIGNED_URL_TTL_SECONDS,
|
|
972
|
+
MIN_SIGNED_TRANSFORM_TTL_SECONDS,
|
|
955
973
|
NitidaClient,
|
|
956
974
|
PRESET_EXT,
|
|
957
975
|
PRESET_LONG,
|
|
@@ -967,6 +985,7 @@ export {
|
|
|
967
985
|
configureSlotResolver2 as configureSlotResolver,
|
|
968
986
|
contrastRatio,
|
|
969
987
|
deriveAccessKey,
|
|
988
|
+
deriveTransformKid,
|
|
970
989
|
extractAssetSha,
|
|
971
990
|
getAmbientGradient,
|
|
972
991
|
getAssetDimensions,
|
|
@@ -1001,6 +1020,7 @@ export {
|
|
|
1001
1020
|
setTenantId2 as setTenantId,
|
|
1002
1021
|
signAccessUrl,
|
|
1003
1022
|
signTransformUrl,
|
|
1004
|
-
toRequestablePresets2 as toRequestablePresets
|
|
1023
|
+
toRequestablePresets2 as toRequestablePresets,
|
|
1024
|
+
transformMessage
|
|
1005
1025
|
};
|
|
1006
1026
|
//# sourceMappingURL=index.js.map
|