@nitida/asset-client 0.18.0 → 0.19.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/dist/index.d.cts CHANGED
@@ -26,6 +26,20 @@
26
26
  *
27
27
  * A signed URL that never expires is a public URL as soon as someone forwards
28
28
  * it. There is no "no expiry" option here, and there will not be one.
29
+ *
30
+ * ## Where `signingKey` comes from
31
+ *
32
+ * The response that created your project. `POST /admin/projects` returns
33
+ * `signingKey` next to the three API keys, and the console shows it in the
34
+ * same panel — **once**. Save it with the keys.
35
+ *
36
+ * If it is gone — or you never saw one, which is the case for every project
37
+ * created before 2026-08-23 — the only endpoint that returns a key is
38
+ * `POST /admin/projects/:code/rotate-signing-key`, and rotating invalidates
39
+ * every URL already signed. That is free for a tenant with nothing in flight
40
+ * and expensive for a live one, which is exactly why the key is handed over at
41
+ * creation, when rotating would be free anyway. With URLs already circulating,
42
+ * ask the platform operator for the current key rather than rotating.
29
43
  */
30
44
  /**
31
45
  * The per-tenant access key, derived from `signing_key`. Same value the origin
@@ -79,12 +93,41 @@ declare function assertPublic(asset: VisibilityHint, fn: string,
79
93
  /**
80
94
  * The call to make instead — declared per call site, not guessed.
81
95
  *
96
+ * Named `escapeHatch`, not `escape`: the bare name shadows the deprecated
97
+ * global `escape`, which biome flags as an error. Nothing here calls that
98
+ * global, so this was never a defect — but it is a lint error standing in a
99
+ * PUBLISHED package, and a parameter name is not part of the API, so the
100
+ * cost of clearing it is zero.
101
+ *
82
102
  * It matters which one: `getPrivateAssetUrl` signs a STORED preset, and
83
103
  * pointing a transform caller at it sends them to a function that cannot do
84
104
  * what they asked for. The first version of this message named
85
105
  * `getPrivateAssetUrl` for all seven builders; a test caught it.
86
106
  */
87
- escape: string): void;
107
+ escapeHatch: string): void;
108
+ /**
109
+ * Refuse a value whose `sha` is missing or malformed, instead of interpolating
110
+ * it into a URL.
111
+ *
112
+ * ## Found by a Haiku agent, 2026-08-23
113
+ *
114
+ * It did the most natural thing there is — passed the result of `upload()`
115
+ * straight to `transform()` — and got:
116
+ *
117
+ * https://8ok.uk/t/width=1280/undefined.webp
118
+ *
119
+ * `UploadResult` carries `sha256`; every URL builder wants `sha`. TypeScript
120
+ * catches the mismatch, but an agent running through `bun` (or anyone in plain
121
+ * JS) sees no error at all: just a 200-shaped URL with the word `undefined` in
122
+ * it, which 404s later and somewhere else.
123
+ *
124
+ * The house rule applies exactly as it does to private assets: **a silence
125
+ * reads as "you can't"**. A builder that cannot name the asset must say so at
126
+ * the call site, not hand back a string that will fail far from here.
127
+ */
128
+ declare function assertSha(asset: {
129
+ sha?: unknown;
130
+ }, fn: string): void;
88
131
 
89
132
  /**
90
133
  * Color palette helpers — render harmonious ambient backgrounds behind
@@ -546,6 +589,38 @@ type VariantPreset = "thumb" | "sm" | "md" | "lg" | "xl" | "original" | "poster"
546
589
  * no drift between them.
547
590
  */
548
591
  type RequestablePreset = Exclude<VariantPreset, "hls" | "mp3"> | "probe";
592
+ /**
593
+ * The same set as {@link RequestablePreset}, at RUNTIME.
594
+ *
595
+ * The type stops the mistake in TypeScript. It cannot stop it anywhere else,
596
+ * and "anywhere else" is where it keeps happening: a preset list assembled
597
+ * from config, from a route body, from JSON, or from a script's argv arrives
598
+ * as `string[]`, and the only way past the type was a cast.
599
+ *
600
+ * Measured in neo-real-estate on 2026-08-23, in THREE independent files:
601
+ *
602
+ * presets: [...opts.presets] as VariantPreset[]
603
+ *
604
+ * — a blind cast, and to the wrong type at that: `VariantPreset` includes
605
+ * `hls` and `mp3`, which are precisely the two you may not ask for. Every one
606
+ * of those casts would have compiled a request the server answers 400.
607
+ *
608
+ * So the narrowing lives here, once, instead of being re-invented per repo.
609
+ */
610
+ declare const REQUESTABLE_PRESETS: readonly RequestablePreset[];
611
+ /** Type guard for a single value. */
612
+ declare const isRequestablePreset: (v: string) => v is RequestablePreset;
613
+ /**
614
+ * Narrow a `string[]` to the presets the server can actually produce, or throw
615
+ * naming the offender.
616
+ *
617
+ * Throws rather than filtering silently, for the same reason the seven URL
618
+ * builders throw on a private asset: **a silence reads as "you can't"**. A
619
+ * caller that asked for `hls` wants an HLS ladder; dropping it quietly returns
620
+ * a 200 and no ladder, and the 404 lands later and somewhere else. The error
621
+ * names the value, says why the server cannot make it, and lists what it can.
622
+ */
623
+ declare const toRequestablePresets: (input: readonly string[]) => RequestablePreset[];
549
624
  /** 1-char alias used in storage keys / wire `presets` string. */
550
625
  declare const PRESET_SHORT: Record<VariantPreset, string>;
551
626
  declare const PRESET_LONG: Record<string, VariantPreset>;
@@ -889,4 +964,4 @@ declare function getAssetDimensions(asset: Pick<AssetDTO, "w" | "h">): {
889
964
  height: number;
890
965
  } | null;
891
966
 
892
- export { type AssetDTO, type AssetPalette, type AssetVariant, type HlsRung, PRESET_EXT, PRESET_LONG, PRESET_MAX_DIM, PRESET_SHORT, type PaletteSwatch, type RequestablePreset, type ResolveSlotOptions, type SignAccessOptions, type SignedTransformOptions, type SlotDTO, type SlotResolution, TRANSFORM_WIDTHS, type TransformEffect, type TransformFit, type TransformFormat, type TransformGravity, type TransformOptions, type TransformWidth, type VariantEntryPreset, type VariantPreset, type VisibilityHint, accessMessage, assertPublic, 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, iteratePaletteSwatches, pickAmbientBackground, relativeLuminance, resolveSlot, resolveSlots, serializeTransform, setCdnBase, setTenantId, signAccessUrl, signTransformUrl };
967
+ export { type AssetDTO, type AssetPalette, type AssetVariant, type HlsRung, PRESET_EXT, PRESET_LONG, PRESET_MAX_DIM, PRESET_SHORT, type PaletteSwatch, REQUESTABLE_PRESETS, type RequestablePreset, type ResolveSlotOptions, type SignAccessOptions, type SignedTransformOptions, type SlotDTO, type SlotResolution, TRANSFORM_WIDTHS, type TransformEffect, type TransformFit, type TransformFormat, type TransformGravity, type TransformOptions, type TransformWidth, type VariantEntryPreset, type VariantPreset, type 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 };
package/dist/index.d.ts CHANGED
@@ -26,6 +26,20 @@
26
26
  *
27
27
  * A signed URL that never expires is a public URL as soon as someone forwards
28
28
  * it. There is no "no expiry" option here, and there will not be one.
29
+ *
30
+ * ## Where `signingKey` comes from
31
+ *
32
+ * The response that created your project. `POST /admin/projects` returns
33
+ * `signingKey` next to the three API keys, and the console shows it in the
34
+ * same panel — **once**. Save it with the keys.
35
+ *
36
+ * If it is gone — or you never saw one, which is the case for every project
37
+ * created before 2026-08-23 — the only endpoint that returns a key is
38
+ * `POST /admin/projects/:code/rotate-signing-key`, and rotating invalidates
39
+ * every URL already signed. That is free for a tenant with nothing in flight
40
+ * and expensive for a live one, which is exactly why the key is handed over at
41
+ * creation, when rotating would be free anyway. With URLs already circulating,
42
+ * ask the platform operator for the current key rather than rotating.
29
43
  */
30
44
  /**
31
45
  * The per-tenant access key, derived from `signing_key`. Same value the origin
@@ -79,12 +93,41 @@ declare function assertPublic(asset: VisibilityHint, fn: string,
79
93
  /**
80
94
  * The call to make instead — declared per call site, not guessed.
81
95
  *
96
+ * Named `escapeHatch`, not `escape`: the bare name shadows the deprecated
97
+ * global `escape`, which biome flags as an error. Nothing here calls that
98
+ * global, so this was never a defect — but it is a lint error standing in a
99
+ * PUBLISHED package, and a parameter name is not part of the API, so the
100
+ * cost of clearing it is zero.
101
+ *
82
102
  * It matters which one: `getPrivateAssetUrl` signs a STORED preset, and
83
103
  * pointing a transform caller at it sends them to a function that cannot do
84
104
  * what they asked for. The first version of this message named
85
105
  * `getPrivateAssetUrl` for all seven builders; a test caught it.
86
106
  */
87
- escape: string): void;
107
+ escapeHatch: string): void;
108
+ /**
109
+ * Refuse a value whose `sha` is missing or malformed, instead of interpolating
110
+ * it into a URL.
111
+ *
112
+ * ## Found by a Haiku agent, 2026-08-23
113
+ *
114
+ * It did the most natural thing there is — passed the result of `upload()`
115
+ * straight to `transform()` — and got:
116
+ *
117
+ * https://8ok.uk/t/width=1280/undefined.webp
118
+ *
119
+ * `UploadResult` carries `sha256`; every URL builder wants `sha`. TypeScript
120
+ * catches the mismatch, but an agent running through `bun` (or anyone in plain
121
+ * JS) sees no error at all: just a 200-shaped URL with the word `undefined` in
122
+ * it, which 404s later and somewhere else.
123
+ *
124
+ * The house rule applies exactly as it does to private assets: **a silence
125
+ * reads as "you can't"**. A builder that cannot name the asset must say so at
126
+ * the call site, not hand back a string that will fail far from here.
127
+ */
128
+ declare function assertSha(asset: {
129
+ sha?: unknown;
130
+ }, fn: string): void;
88
131
 
89
132
  /**
90
133
  * Color palette helpers — render harmonious ambient backgrounds behind
@@ -546,6 +589,38 @@ type VariantPreset = "thumb" | "sm" | "md" | "lg" | "xl" | "original" | "poster"
546
589
  * no drift between them.
547
590
  */
548
591
  type RequestablePreset = Exclude<VariantPreset, "hls" | "mp3"> | "probe";
592
+ /**
593
+ * The same set as {@link RequestablePreset}, at RUNTIME.
594
+ *
595
+ * The type stops the mistake in TypeScript. It cannot stop it anywhere else,
596
+ * and "anywhere else" is where it keeps happening: a preset list assembled
597
+ * from config, from a route body, from JSON, or from a script's argv arrives
598
+ * as `string[]`, and the only way past the type was a cast.
599
+ *
600
+ * Measured in neo-real-estate on 2026-08-23, in THREE independent files:
601
+ *
602
+ * presets: [...opts.presets] as VariantPreset[]
603
+ *
604
+ * — a blind cast, and to the wrong type at that: `VariantPreset` includes
605
+ * `hls` and `mp3`, which are precisely the two you may not ask for. Every one
606
+ * of those casts would have compiled a request the server answers 400.
607
+ *
608
+ * So the narrowing lives here, once, instead of being re-invented per repo.
609
+ */
610
+ declare const REQUESTABLE_PRESETS: readonly RequestablePreset[];
611
+ /** Type guard for a single value. */
612
+ declare const isRequestablePreset: (v: string) => v is RequestablePreset;
613
+ /**
614
+ * Narrow a `string[]` to the presets the server can actually produce, or throw
615
+ * naming the offender.
616
+ *
617
+ * Throws rather than filtering silently, for the same reason the seven URL
618
+ * builders throw on a private asset: **a silence reads as "you can't"**. A
619
+ * caller that asked for `hls` wants an HLS ladder; dropping it quietly returns
620
+ * a 200 and no ladder, and the 404 lands later and somewhere else. The error
621
+ * names the value, says why the server cannot make it, and lists what it can.
622
+ */
623
+ declare const toRequestablePresets: (input: readonly string[]) => RequestablePreset[];
549
624
  /** 1-char alias used in storage keys / wire `presets` string. */
550
625
  declare const PRESET_SHORT: Record<VariantPreset, string>;
551
626
  declare const PRESET_LONG: Record<string, VariantPreset>;
@@ -889,4 +964,4 @@ declare function getAssetDimensions(asset: Pick<AssetDTO, "w" | "h">): {
889
964
  height: number;
890
965
  } | null;
891
966
 
892
- export { type AssetDTO, type AssetPalette, type AssetVariant, type HlsRung, PRESET_EXT, PRESET_LONG, PRESET_MAX_DIM, PRESET_SHORT, type PaletteSwatch, type RequestablePreset, type ResolveSlotOptions, type SignAccessOptions, type SignedTransformOptions, type SlotDTO, type SlotResolution, TRANSFORM_WIDTHS, type TransformEffect, type TransformFit, type TransformFormat, type TransformGravity, type TransformOptions, type TransformWidth, type VariantEntryPreset, type VariantPreset, type VisibilityHint, accessMessage, assertPublic, 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, iteratePaletteSwatches, pickAmbientBackground, relativeLuminance, resolveSlot, resolveSlots, serializeTransform, setCdnBase, setTenantId, signAccessUrl, signTransformUrl };
967
+ export { type AssetDTO, type AssetPalette, type AssetVariant, type HlsRung, PRESET_EXT, PRESET_LONG, PRESET_MAX_DIM, PRESET_SHORT, type PaletteSwatch, REQUESTABLE_PRESETS, type RequestablePreset, type ResolveSlotOptions, type SignAccessOptions, type SignedTransformOptions, type SlotDTO, type SlotResolution, TRANSFORM_WIDTHS, type TransformEffect, type TransformFit, type TransformFormat, type TransformGravity, type TransformOptions, type TransformWidth, type VariantEntryPreset, type VariantPreset, type 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 };
package/dist/index.js CHANGED
@@ -61,10 +61,18 @@ async function signAccessUrl(publicUrl, signingKey, opts) {
61
61
  u.searchParams.set("sig", sig);
62
62
  return u.toString();
63
63
  }
64
- function assertPublic(asset, fn, escape) {
64
+ function assertPublic(asset, fn, escapeHatch) {
65
65
  if (asset.visibility !== "private") return;
66
66
  throw new Error(
67
- `${fn}: this asset is private, so a public CDN URL for it will answer 404 \u2014 that is the feature, not a missing file. Mint a signed URL on your BACKEND instead: await ${escape}. Never ship the signing key to a browser.`
67
+ `${fn}: this asset is private, so a public CDN URL for it will answer 404 \u2014 that is the feature, not a missing file. Mint a signed URL on your BACKEND instead: await ${escapeHatch}. Never ship the signing key to a browser.`
68
+ );
69
+ }
70
+ function assertSha(asset, fn) {
71
+ const sha = asset?.sha;
72
+ if (typeof sha === "string" && /^[0-9a-f]{16,64}$/i.test(sha)) return;
73
+ const hint = asset && typeof asset === "object" && "sha256" in asset ? " The value you passed has `sha256` but not `sha` \u2014 that is the shape `upload()` returns. Use `{ sha: result.sha256.slice(0, 16) }`, or fetch the DTO with `assets.get(id)`." : ` Got ${JSON.stringify(sha)}.`;
74
+ throw new Error(
75
+ `${fn}: no usable \`sha\` on the value you passed, so the URL would contain "undefined" and 404 somewhere else.${hint}`
68
76
  );
69
77
  }
70
78
 
@@ -243,6 +251,7 @@ function extForOptions(opts) {
243
251
  }
244
252
  }
245
253
  function getVideoTransformUrl(asset, opts) {
254
+ assertSha(asset, "getVideoTransformUrl");
246
255
  assertPublic(
247
256
  asset,
248
257
  "getVideoTransformUrl",
@@ -254,6 +263,7 @@ function getVideoTransformUrl(asset, opts) {
254
263
  return `${getCdnBase()}/t/${dsl}/${asset.sha}.${ext}`;
255
264
  }
256
265
  function getHlsStreamingUrl(asset, opts = {}) {
266
+ assertSha(asset, "getHlsStreamingUrl");
257
267
  assertPublic(
258
268
  asset,
259
269
  "getHlsStreamingUrl",
@@ -270,6 +280,7 @@ function buildTransformUrl(asset, opts) {
270
280
  return `${getCdnBase()}/t/${dsl}/${asset.sha}.${ext}`;
271
281
  }
272
282
  function getTransformUrl(asset, opts) {
283
+ assertSha(asset, "getTransformUrl");
273
284
  assertPublic(
274
285
  asset,
275
286
  "getTransformUrl",
@@ -278,6 +289,7 @@ function getTransformUrl(asset, opts) {
278
289
  return buildTransformUrl(asset, opts);
279
290
  }
280
291
  function getSignedTransformUrl(asset, opts, signingKey) {
292
+ assertSha(asset, "getSignedTransformUrl");
281
293
  assertPublic(
282
294
  asset,
283
295
  "getSignedTransformUrl",
@@ -313,6 +325,7 @@ async function hmacSha256Hex(key, message) {
313
325
  return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
314
326
  }
315
327
  function getTransformSrcSet(asset, widths, extraOpts = {}) {
328
+ assertSha(asset, "getTransformSrcSet");
316
329
  assertPublic(
317
330
  asset,
318
331
  "getTransformSrcSet",
@@ -421,6 +434,28 @@ function materializeResolution(dto, overridePreset) {
421
434
  }
422
435
 
423
436
  // src/index.ts
437
+ var REQUESTABLE_PRESETS = [
438
+ "thumb",
439
+ "sm",
440
+ "md",
441
+ "lg",
442
+ "xl",
443
+ "original",
444
+ "poster",
445
+ "video",
446
+ "aiproxy",
447
+ "probe"
448
+ ];
449
+ var isRequestablePreset = (v) => REQUESTABLE_PRESETS.includes(v);
450
+ var toRequestablePresets = (input) => {
451
+ const bad = input.filter((p) => !isRequestablePreset(p));
452
+ if (bad.length > 0) {
453
+ throw new Error(
454
+ `Cannot request ${bad.map((b) => `"${b}"`).join(", ")}. \`hls\` and \`mp3\` are things a variant can BE, not things you may ask for \u2014 the server derives them itself (hls when a video is transcoded, mp3 alongside any audio original). Requestable: ${REQUESTABLE_PRESETS.join(", ")}.`
455
+ );
456
+ }
457
+ return input;
458
+ };
424
459
  var PRESET_SHORT = {
425
460
  thumb: "q",
426
461
  sm: "s",
@@ -546,6 +581,7 @@ function originalExtForMime(mime) {
546
581
  return (mime ? ORIGINAL_EXT_BY_MIME[mime] : void 0) ?? PRESET_EXT.original;
547
582
  }
548
583
  function getAssetUrl(asset, preset) {
584
+ assertSha(asset, "getAssetUrl");
549
585
  assertPublic(
550
586
  asset,
551
587
  "getAssetUrl",
@@ -563,6 +599,7 @@ function transformFallbackFor(asset, preset) {
563
599
  return `${cdnBaseUrl}/t/format=webp,width=${maxDim}/${asset.sha}.webp`;
564
600
  }
565
601
  function buildPublicAssetUrl(asset, preset) {
602
+ assertSha(asset, "getPrivateAssetUrl");
566
603
  if (preset === "original") {
567
604
  const stored = asset.variants?.find((v) => v.preset === "original")?.url;
568
605
  if (stored) return stored;
@@ -604,6 +641,7 @@ function stripMultiCharTokens(presets) {
604
641
  }
605
642
  var IMAGE_PRESETS = ["thumb", "sm", "md", "lg", "xl"];
606
643
  function getAssetSrcSet(asset) {
644
+ assertSha(asset, "getAssetSrcSet");
607
645
  assertPublic(
608
646
  asset,
609
647
  "getAssetSrcSet",
@@ -633,9 +671,11 @@ export {
633
671
  PRESET_LONG,
634
672
  PRESET_MAX_DIM,
635
673
  PRESET_SHORT,
674
+ REQUESTABLE_PRESETS,
636
675
  TRANSFORM_WIDTHS,
637
676
  accessMessage,
638
677
  assertPublic,
678
+ assertSha,
639
679
  bestTextContrast,
640
680
  computeVariantDimensions,
641
681
  configureSlotResolver,
@@ -662,6 +702,7 @@ export {
662
702
  hasPreset,
663
703
  hlsLadderAlignment,
664
704
  invalidateSlotCache,
705
+ isRequestablePreset,
665
706
  iteratePaletteSwatches,
666
707
  pickAmbientBackground,
667
708
  relativeLuminance,
@@ -671,6 +712,7 @@ export {
671
712
  setCdnBase,
672
713
  setTenantId,
673
714
  signAccessUrl,
674
- signTransformUrl
715
+ signTransformUrl,
716
+ toRequestablePresets
675
717
  };
676
718
  //# sourceMappingURL=index.js.map