@nitida/asset-client 0.20.4 → 0.23.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
@@ -47,6 +47,13 @@ schemas, not against a hand-kept list.
47
47
  *guesses* — the server keys the original off the **uploaded filename**, which the mime does not
48
48
  determine — and with neither it emits the `-o.bin` sentinel and 404s forever.
49
49
 
50
+ The ladder itself — **generated from `TRANSFORM_WIDTHS`, do not edit by hand.**
51
+ `bun run gen:docs` rewrites it; `--check` fails the build if it drifts:
52
+
53
+ <!-- BEGIN GENERATED: transform-widths · bun run gen:docs -->
54
+ `96, 128, 160, 180, 240, 256, 320, 400, 480, 600, 640, 800, 960, 1080, 1200, 1280, 1440, 1600, 1920, 2560, 3840` — 21 widths.
55
+ <!-- END GENERATED: transform-widths -->
56
+
50
57
  ## Existence checks
51
58
 
52
59
  Use `hasPreset(asset, preset)` against `dto.presets` (the compact code string). It is on every
package/README.md CHANGED
@@ -43,10 +43,15 @@ getTransformUrl({ sha }, { format: "webp", width: 640 });
43
43
  getTransformSrcSet({ sha }, [640, 960, 1280], { format: "webp" });
44
44
  ```
45
45
 
46
- ⚠️ **Widths must be on the unsigned ladder** `TRANSFORM_WIDTHS`
47
- (`96,128,160,240,256,320,400,480,600,640,800,960,1080,1200,1280,1440,1600,1920,2560,3840` 20 widths). Anything else
48
- is **HTTP 400** at the edge — it is a DoS guard, not a bug. Import the `TransformWidth` type and an
49
- off-ladder number becomes a compile error instead of a runtime 400.
46
+ ⚠️ **Widths must be on the unsigned ladder** `TRANSFORM_WIDTHS` — generated from the source, do not
47
+ edit by hand:
48
+
49
+ <!-- BEGIN GENERATED: transform-widths · bun run gen:docs -->
50
+ `96, 128, 160, 180, 240, 256, 320, 400, 480, 600, 640, 800, 960, 1080, 1200, 1280, 1440, 1600, 1920, 2560, 3840` — 21 widths.
51
+ <!-- END GENERATED: transform-widths -->
52
+
53
+ Anything else is **HTTP 400** at the edge — it is a DoS guard, not a bug. Import the `TransformWidth`
54
+ type and an off-ladder number becomes a compile error instead of a runtime 400.
50
55
 
51
56
  ## Video — the stored variant, not a transform
52
57
 
package/dist/index.cjs CHANGED
@@ -20,6 +20,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ MAX_SIGNED_TRANSFORM_TTL_SECONDS: () => MAX_SIGNED_TRANSFORM_TTL_SECONDS,
24
+ MAX_SIGNED_URL_TTL_SECONDS: () => MAX_SIGNED_URL_TTL_SECONDS,
25
+ MIN_SIGNED_TRANSFORM_TTL_SECONDS: () => MIN_SIGNED_TRANSFORM_TTL_SECONDS,
23
26
  PRESET_EXT: () => PRESET_EXT,
24
27
  PRESET_LONG: () => PRESET_LONG,
25
28
  PRESET_MAX_DIM: () => PRESET_MAX_DIM,
@@ -34,6 +37,7 @@ __export(index_exports, {
34
37
  configureSlotResolver: () => configureSlotResolver,
35
38
  contrastRatio: () => contrastRatio,
36
39
  deriveAccessKey: () => deriveAccessKey,
40
+ deriveTransformKid: () => deriveTransformKid,
37
41
  extractAssetSha: () => extractAssetSha,
38
42
  getAmbientGradient: () => getAmbientGradient,
39
43
  getAssetDimensions: () => getAssetDimensions,
@@ -66,7 +70,8 @@ __export(index_exports, {
66
70
  setTenantId: () => setTenantId,
67
71
  signAccessUrl: () => signAccessUrl,
68
72
  signTransformUrl: () => signTransformUrl,
69
- toRequestablePresets: () => toRequestablePresets
73
+ toRequestablePresets: () => toRequestablePresets,
74
+ transformMessage: () => transformMessage
70
75
  });
71
76
  module.exports = __toCommonJS(index_exports);
72
77
 
@@ -97,12 +102,18 @@ function accessMessage(tenantPrefix, exp, resourcePath) {
97
102
  ${exp}
98
103
  ${resourcePath.replace(/^\/+/, "")}`;
99
104
  }
105
+ var MAX_SIGNED_URL_TTL_SECONDS = 7 * 24 * 60 * 60;
100
106
  async function signAccessUrl(publicUrl, signingKey, opts) {
101
107
  if (!Number.isFinite(opts.expiresInSeconds) || opts.expiresInSeconds <= 0) {
102
108
  throw new Error(
103
109
  "signAccessUrl: `expiresInSeconds` must be a positive number \u2014 a signed URL without an expiry is a public URL the moment it is forwarded."
104
110
  );
105
111
  }
112
+ if (opts.expiresInSeconds > MAX_SIGNED_URL_TTL_SECONDS) {
113
+ throw new Error(
114
+ `signAccessUrl: \`expiresInSeconds\` may not exceed ${MAX_SIGNED_URL_TTL_SECONDS} (7 days). Both verifiers refuse a longer one, so this would build a URL that 401s. An expiry that never arrives is not an expiry.`
115
+ );
116
+ }
106
117
  const u = new URL(publicUrl);
107
118
  const segments = u.pathname.split("/").filter(Boolean);
108
119
  if (segments[0] === "a" && segments[2] && /^[vrt]$/.test(segments[2])) {
@@ -258,6 +269,7 @@ var TRANSFORM_WIDTHS = [
258
269
  96,
259
270
  128,
260
271
  160,
272
+ 180,
261
273
  240,
262
274
  256,
263
275
  320,
@@ -361,7 +373,7 @@ function getTransformUrl(asset, opts) {
361
373
  );
362
374
  return buildTransformUrl(asset, opts);
363
375
  }
364
- function getSignedTransformUrl(asset, opts, signingKey) {
376
+ function getSignedTransformUrl(asset, opts, signingKey, signOpts) {
365
377
  assertSha(asset, "getSignedTransformUrl");
366
378
  assertPublic(
367
379
  asset,
@@ -370,9 +382,34 @@ function getSignedTransformUrl(asset, opts, signingKey) {
370
382
  );
371
383
  const url = buildTransformUrl(asset, opts);
372
384
  if (!url) return null;
373
- return signTransformUrl(url, signingKey);
374
- }
375
- async function signTransformUrl(unsignedUrl, signingKey) {
385
+ return signTransformUrl(url, signingKey, signOpts);
386
+ }
387
+ var TRANSFORM_SIG_DOMAIN = "nitida/transform/v2";
388
+ var KID_INFO = "nitida/kid/v1";
389
+ var MAX_SIGNED_TRANSFORM_TTL_SECONDS = 7 * 24 * 60 * 60;
390
+ var MIN_SIGNED_TRANSFORM_TTL_SECONDS = 120;
391
+ async function deriveTransformKid(signingKey) {
392
+ return (await hmacSha256Hex(signingKey, KID_INFO)).slice(0, 8);
393
+ }
394
+ function transformMessage(args) {
395
+ return `${TRANSFORM_SIG_DOMAIN}
396
+ ${args.tenantPrefix}
397
+ ${args.exp}
398
+ ${args.canonicalDsl}/${args.filename}`;
399
+ }
400
+ async function signTransformUrl(unsignedUrl, signingKey, opts) {
401
+ const ttl = Math.floor(opts.expiresInSeconds);
402
+ if (!Number.isFinite(ttl) || ttl < MIN_SIGNED_TRANSFORM_TTL_SECONDS || ttl > MAX_SIGNED_TRANSFORM_TTL_SECONDS) {
403
+ throw new Error(
404
+ `signTransformUrl: \`expiresInSeconds\` must be between ${MIN_SIGNED_TRANSFORM_TTL_SECONDS} and ${MAX_SIGNED_TRANSFORM_TTL_SECONDS}. The platform refuses a longer one on every verify, so a bigger number here just builds a URL that 401s.`
405
+ );
406
+ }
407
+ const tid = opts.tenantId ?? getTenantId();
408
+ if (tid == null) {
409
+ throw new Error(
410
+ "signTransformUrl: no tenant is configured. Call setTenantId(id) (or construct a NitidaClient with `tenantId`) \u2014 the tenant is part of what the signature covers, so this cannot be guessed."
411
+ );
412
+ }
376
413
  const u = new URL(unsignedUrl);
377
414
  const parts = u.pathname.split("/").filter(Boolean);
378
415
  if (parts[0] !== "t" || parts.length < 3) {
@@ -380,8 +417,19 @@ async function signTransformUrl(unsignedUrl, signingKey) {
380
417
  }
381
418
  const filename = parts[parts.length - 1];
382
419
  const dsl = parts.slice(1, -1).join("/");
383
- const message = `${dsl}/${filename}`;
384
- const sig = await hmacSha256Hex(signingKey, message);
420
+ const now = opts.nowSeconds ?? Math.floor(Date.now() / 1e3);
421
+ const exp = Math.floor((now + ttl) / 60) * 60;
422
+ const sig = await hmacSha256Hex(
423
+ signingKey,
424
+ transformMessage({
425
+ tenantPrefix: tid.toString(36),
426
+ exp,
427
+ canonicalDsl: dsl,
428
+ filename
429
+ })
430
+ );
431
+ u.searchParams.set("kid", await deriveTransformKid(signingKey));
432
+ u.searchParams.set("exp", String(exp));
385
433
  u.searchParams.set("sig", sig);
386
434
  return u.toString();
387
435
  }
@@ -508,16 +556,16 @@ function materializeResolution(dto, overridePreset) {
508
556
 
509
557
  // src/index.ts
510
558
  var REQUESTABLE_PRESETS = [
511
- "thumb",
512
- "sm",
513
- "md",
559
+ "aiproxy",
514
560
  "lg",
515
- "xl",
561
+ "md",
516
562
  "original",
517
563
  "poster",
564
+ "probe",
565
+ "sm",
566
+ "thumb",
518
567
  "video",
519
- "aiproxy",
520
- "probe"
568
+ "xl"
521
569
  ];
522
570
  var isRequestablePreset = (v) => REQUESTABLE_PRESETS.includes(v);
523
571
  var toRequestablePresets = (input) => {
@@ -687,6 +735,11 @@ function buildPublicAssetUrl(asset, preset, caller) {
687
735
  if (preset === "hls") {
688
736
  const stored = asset.variants?.find((v) => v.preset === "hls")?.url;
689
737
  if (stored) return stored;
738
+ if (asset.kind != null && asset.kind !== "video") {
739
+ throw new Error(
740
+ `${caller}: this asset is \`${asset.kind}\`, and only a video has an HLS ladder. Asking for \`hls\` here would build \`/t/format=hls/<sha>.m3u8\`, which does NOT 404 \u2014 the edge answers 200 with the image bytes under an .m3u8 name, so a player fails with no way to see why. For an image use a size preset (\`${caller}(asset, "md")\`) or a transform (\`getTransformUrl\`); for audio use \`mp3\`.`
741
+ );
742
+ }
690
743
  return `${cdnBaseUrl}/t/format=hls/${asset.sha}.m3u8`;
691
744
  }
692
745
  return `${cdnBaseUrl}/${variantPrefix(caller)}${asset.sha}-${PRESET_SHORT[preset]}.${PRESET_EXT[preset]}`;
@@ -751,6 +804,9 @@ function getAssetDimensions(asset) {
751
804
  }
752
805
  // Annotate the CommonJS export names for ESM import in node:
753
806
  0 && (module.exports = {
807
+ MAX_SIGNED_TRANSFORM_TTL_SECONDS,
808
+ MAX_SIGNED_URL_TTL_SECONDS,
809
+ MIN_SIGNED_TRANSFORM_TTL_SECONDS,
754
810
  PRESET_EXT,
755
811
  PRESET_LONG,
756
812
  PRESET_MAX_DIM,
@@ -765,6 +821,7 @@ function getAssetDimensions(asset) {
765
821
  configureSlotResolver,
766
822
  contrastRatio,
767
823
  deriveAccessKey,
824
+ deriveTransformKid,
768
825
  extractAssetSha,
769
826
  getAmbientGradient,
770
827
  getAssetDimensions,
@@ -797,6 +854,7 @@ function getAssetDimensions(asset) {
797
854
  setTenantId,
798
855
  signAccessUrl,
799
856
  signTransformUrl,
800
- toRequestablePresets
857
+ toRequestablePresets,
858
+ transformMessage
801
859
  });
802
860
  //# sourceMappingURL=index.cjs.map