@nitida/asset-client 0.21.0 → 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) => {
@@ -756,6 +804,9 @@ function getAssetDimensions(asset) {
756
804
  }
757
805
  // Annotate the CommonJS export names for ESM import in node:
758
806
  0 && (module.exports = {
807
+ MAX_SIGNED_TRANSFORM_TTL_SECONDS,
808
+ MAX_SIGNED_URL_TTL_SECONDS,
809
+ MIN_SIGNED_TRANSFORM_TTL_SECONDS,
759
810
  PRESET_EXT,
760
811
  PRESET_LONG,
761
812
  PRESET_MAX_DIM,
@@ -770,6 +821,7 @@ function getAssetDimensions(asset) {
770
821
  configureSlotResolver,
771
822
  contrastRatio,
772
823
  deriveAccessKey,
824
+ deriveTransformKid,
773
825
  extractAssetSha,
774
826
  getAmbientGradient,
775
827
  getAssetDimensions,
@@ -802,6 +854,7 @@ function getAssetDimensions(asset) {
802
854
  setTenantId,
803
855
  signAccessUrl,
804
856
  signTransformUrl,
805
- toRequestablePresets
857
+ toRequestablePresets,
858
+ transformMessage
806
859
  });
807
860
  //# sourceMappingURL=index.cjs.map