@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 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 (`96, 128, 160, 240, 256, 320, 400, 480, 600, 640, 800, 960,
879
- > 1080, 1200, 1280, 1440, 1600, 1920, 2560, 3840` — 20 widths, exported as
880
- > `TRANSFORM_WIDTHS`). An off-ladder width is a **compile error**.
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
- const signed = await aq.transform(asset, { width: 1280 }, { sign: true });
1005
- // https://8ok.uk/t/width=1280/<sha>.webp?sig=<64-hex>
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: `HMAC-SHA256(signingKey, "<canonical-DSL>/<filename>")`,
1017
- hex-encoded. The server canonicalizes the URL the same way the SDK does
1018
- (sort keys, lowercase strings), so two URLs with the same params in
1019
- different order accept the same signature.
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: number[], extraOpts: Omit<TransformOptions, "width">, signOpts: {
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) ?? Promise.resolve(this.urlFor(asset, "lg"));
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