@nitida/asset-client 0.17.0 → 0.18.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
@@ -66,6 +66,32 @@ now emits only the 1-char vocabulary, and `hasPreset` strips multi-char tokens b
66
66
  either half covers you — a hand-rolled `includes` covers neither. **`mp3` is a deliberate multi-char
67
67
  token that stays in the vocabulary**, so a naive test also reads its `m` as `md`.
68
68
 
69
+
70
+ ## Private assets — `visibility`
71
+
72
+ Default is `"public"`. Set `private` and **every public door answers 404** —
73
+ stored variants, the raw original, the HLS ladder, and `/t/` (including the
74
+ poster frame of a private video). The bytes come back only through a signed URL
75
+ that expires.
76
+
77
+ ```ts
78
+ import { getPrivateAssetUrl, getPrivateTransformUrl } from "@nitida/sdk";
79
+ // ON YOUR BACKEND, once you decided this viewer may see it:
80
+ await getPrivateAssetUrl(asset, "lg", signingKey, { expiresInSeconds: 300 });
81
+ await getPrivateTransformUrl(asset, { width: 1280 }, signingKey, { expiresInSeconds: 300 });
82
+ ```
83
+
84
+ - **A 404 on a private asset is NOT a missing file.** Check `visibility` on the
85
+ DTO before you check storage. It is the number-one support question.
86
+ - **Seven URL builders throw** rather than hand you a doomed URL — `getAssetUrl`,
87
+ `getAssetSrcSet`, `getTransformUrl`, `getTransformSrcSet`,
88
+ `getVideoTransformUrl`, `getHlsStreamingUrl`, `getSignedTransformUrl` — but
89
+ only when the value you pass carries `visibility`. `{ sha }` alone is never
90
+ refused.
91
+ - **`exp` is mandatory**; revocation is *"within a minute"* (60 s TTL at the edge).
92
+ - **The signing key is a backend secret** — it mints URLs for every private
93
+ asset the tenant owns.
94
+
69
95
  ## The endpoint
70
96
 
71
97
  The API this client's URLs belong to is `https://api.nitida.gofuture.space`. An older, longer
package/README.md CHANGED
@@ -44,7 +44,7 @@ getTransformSrcSet({ sha }, [640, 960, 1280], { format: "webp" });
44
44
  ```
45
45
 
46
46
  ⚠️ **Widths must be on the unsigned ladder** `TRANSFORM_WIDTHS`
47
- (`160,240,256,320,400,480,600,640,800,960,1080,1200,1280,1440,1600,1920,2560,3840`). Anything else
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
48
  is **HTTP 400** at the edge — it is a DoS guard, not a bug. Import the `TransformWidth` type and an
49
49
  off-ladder number becomes a compile error instead of a runtime 400.
50
50
 
@@ -62,7 +62,10 @@ getAssetUrl({ sha }, "video"); // → https://8ok.uk/<tenantId base36>/v/<sha1
62
62
  `getAssetUrl` build the path.
63
63
 
64
64
  ⚠️ **Never `getVideoTransformUrl` for a stored video.** That builds a `/t/…` transform URL, which
65
- **410s** for a stored video sha. It is for on-the-fly re-encodes, a different feature.
65
+ does NOT give you the video. On a video sha it transforms the **poster frame**
66
+ and answers `200 image/webp` (`x-transform-source: poster`) — useful, but an
67
+ image. If that video has no poster it answers `410`. Either way you never get
68
+ playable video out of `/t/`; use `getAssetUrl(asset, "video")`.
66
69
 
67
70
  ## The `original` variant
68
71
 
@@ -119,7 +122,7 @@ shape. `presets` is populated everywhere, on every version — so read existence
119
122
  | Symptom | Cause |
120
123
  |---|---|
121
124
  | **400** on an image URL | width not on `TRANSFORM_WIDTHS` |
122
- | **410** on a video URL | used `/t/` (transform) for a stored video |
125
+ | **410** on a video `/t/` URL | that video has no `poster` variant. With one, `/t/` returns the poster as an image — never the video. Use `getAssetUrl(asset, "video")` |
123
126
  | **404** on a video URL | decimal tenant prefix instead of base36 |
124
127
  | `original` 404s | no `mime` passed, or the stored ext came from the filename (`-o.jpg` vs `-o.jpeg`), or it was never written |
125
128
 
package/dist/index.cjs CHANGED
@@ -25,10 +25,13 @@ __export(index_exports, {
25
25
  PRESET_MAX_DIM: () => PRESET_MAX_DIM,
26
26
  PRESET_SHORT: () => PRESET_SHORT,
27
27
  TRANSFORM_WIDTHS: () => TRANSFORM_WIDTHS,
28
+ accessMessage: () => accessMessage,
29
+ assertPublic: () => assertPublic,
28
30
  bestTextContrast: () => bestTextContrast,
29
31
  computeVariantDimensions: () => computeVariantDimensions,
30
32
  configureSlotResolver: () => configureSlotResolver,
31
33
  contrastRatio: () => contrastRatio,
34
+ deriveAccessKey: () => deriveAccessKey,
32
35
  extractAssetSha: () => extractAssetSha,
33
36
  getAmbientGradient: () => getAmbientGradient,
34
37
  getAssetDimensions: () => getAssetDimensions,
@@ -39,6 +42,8 @@ __export(index_exports, {
39
42
  getHlsStreamingUrl: () => getHlsStreamingUrl,
40
43
  getPaletteBlurBackground: () => getPaletteBlurBackground,
41
44
  getPaletteCssVars: () => getPaletteCssVars,
45
+ getPrivateAssetUrl: () => getPrivateAssetUrl,
46
+ getPrivateTransformUrl: () => getPrivateTransformUrl,
42
47
  getSignedTransformUrl: () => getSignedTransformUrl,
43
48
  getTenantId: () => getTenantId,
44
49
  getTextColorForBackground: () => getTextColorForBackground,
@@ -56,10 +61,81 @@ __export(index_exports, {
56
61
  serializeTransform: () => serializeTransform,
57
62
  setCdnBase: () => setCdnBase,
58
63
  setTenantId: () => setTenantId,
64
+ signAccessUrl: () => signAccessUrl,
59
65
  signTransformUrl: () => signTransformUrl
60
66
  });
61
67
  module.exports = __toCommonJS(index_exports);
62
68
 
69
+ // src/access.ts
70
+ var ACCESS_KEY_INFO = "nitida/access/v1";
71
+ async function hmac(key, message) {
72
+ const cryptoKey = await crypto.subtle.importKey(
73
+ "raw",
74
+ key,
75
+ { name: "HMAC", hash: "SHA-256" },
76
+ false,
77
+ ["sign"]
78
+ );
79
+ return new Uint8Array(
80
+ await crypto.subtle.sign(
81
+ "HMAC",
82
+ cryptoKey,
83
+ new TextEncoder().encode(message)
84
+ )
85
+ );
86
+ }
87
+ var toHex = (b) => [...b].map((x) => x.toString(16).padStart(2, "0")).join("");
88
+ async function deriveAccessKey(signingKey) {
89
+ return hmac(new TextEncoder().encode(signingKey), ACCESS_KEY_INFO);
90
+ }
91
+ function accessMessage(tenantPrefix, exp, resourcePath) {
92
+ return `${tenantPrefix}
93
+ ${exp}
94
+ ${resourcePath.replace(/^\/+/, "")}`;
95
+ }
96
+ async function signAccessUrl(publicUrl, signingKey, opts) {
97
+ if (!Number.isFinite(opts.expiresInSeconds) || opts.expiresInSeconds <= 0) {
98
+ throw new Error(
99
+ "signAccessUrl: `expiresInSeconds` must be a positive number \u2014 a signed URL without an expiry is a public URL the moment it is forwarded."
100
+ );
101
+ }
102
+ const u = new URL(publicUrl);
103
+ const segments = u.pathname.split("/").filter(Boolean);
104
+ if (segments[0] === "a" && segments[2] && /^[vrt]$/.test(segments[2])) {
105
+ segments.shift();
106
+ }
107
+ const tenantPrefix = segments.shift();
108
+ if (!tenantPrefix || segments.length === 0) {
109
+ throw new Error(
110
+ `signAccessUrl: expected a tenant-prefixed CDN path like /<tenant>/v/<sha>-<preset>.<ext>, got ${u.pathname}`
111
+ );
112
+ }
113
+ if (!/^[vrt]$/.test(segments[0])) {
114
+ throw new Error(
115
+ `signAccessUrl: expected /<tenant>/<v|r|t>/\u2026 but the segment after the tenant is "${segments[0]}". ` + (segments[0]?.includes("=") ? `That looks like a transform DSL, so the path is probably /t/<dsl>/<sha>.<ext> \u2014 which has no tenant in it (\`t\` here was read as tenant ${Number.parseInt(tenantPrefix, 36)}). Use getPrivateTransformUrl(asset, opts, signingKey, { expiresInSeconds }) instead.` : `Got ${u.pathname}.`)
116
+ );
117
+ }
118
+ const resourcePath = segments.join("/");
119
+ const now = opts.nowSeconds ?? Math.floor(Date.now() / 1e3);
120
+ const exp = now + Math.floor(opts.expiresInSeconds);
121
+ const sig = toHex(
122
+ await hmac(
123
+ await deriveAccessKey(signingKey),
124
+ accessMessage(tenantPrefix, exp, resourcePath)
125
+ )
126
+ );
127
+ u.pathname = `/a/${tenantPrefix}/${resourcePath}`;
128
+ u.searchParams.set("exp", String(exp));
129
+ u.searchParams.set("sig", sig);
130
+ return u.toString();
131
+ }
132
+ function assertPublic(asset, fn, escape) {
133
+ if (asset.visibility !== "private") return;
134
+ throw new Error(
135
+ `${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.`
136
+ );
137
+ }
138
+
63
139
  // src/palette.ts
64
140
  function resolveSwatch(palette, ...keys) {
65
141
  if (!palette) return null;
@@ -77,9 +153,9 @@ function pickAmbientBackground(palette) {
77
153
  function getAmbientGradient(palette, opts = {}) {
78
154
  if (!palette) return void 0;
79
155
  const fromHex = palette[opts.from ?? "lm"] ?? palette.m ?? palette.d;
80
- const toHex = palette[opts.to ?? "m"] ?? palette.dm ?? palette.d;
81
- if (!fromHex || !toHex) return void 0;
82
- return `linear-gradient(${opts.angle ?? "135deg"}, ${fromHex}, ${toHex})`;
156
+ const toHex2 = palette[opts.to ?? "m"] ?? palette.dm ?? palette.d;
157
+ if (!fromHex || !toHex2) return void 0;
158
+ return `linear-gradient(${opts.angle ?? "135deg"}, ${fromHex}, ${toHex2})`;
83
159
  }
84
160
  function getTextColorForBackground(swatch) {
85
161
  if (!swatch) return "#000000";
@@ -164,102 +240,6 @@ function getPaletteBlurBackground(palette) {
164
240
  return layers.length > 0 ? `${layers.join(", ")}, ${base}` : base;
165
241
  }
166
242
 
167
- // src/slots.ts
168
- var DEFAULT_TTL_MS = 6e4;
169
- var cache = /* @__PURE__ */ new Map();
170
- var endpoint = "https://api.nitida.gofuture.space";
171
- var apiKey = null;
172
- var tenantCode = null;
173
- function configureSlotResolver(opts) {
174
- if (opts.endpoint) endpoint = opts.endpoint.replace(/\/+$/, "");
175
- if (opts.apiKey !== void 0) apiKey = opts.apiKey;
176
- if (opts.tenantCode !== void 0) tenantCode = opts.tenantCode;
177
- }
178
- function invalidateSlotCache(slotKey) {
179
- if (slotKey === void 0) cache.clear();
180
- else
181
- for (const k of cache.keys())
182
- if (k.endsWith(`:${slotKey}`)) cache.delete(k);
183
- }
184
- var baseHeaders = () => {
185
- const h = {};
186
- if (apiKey) h.Authorization = `Bearer ${apiKey}`;
187
- if (tenantCode) h["X-Tenant-Code"] = tenantCode;
188
- return h;
189
- };
190
- async function fetchSlot(slotKey) {
191
- const r = await fetch(`${endpoint}/slots/${encodeURIComponent(slotKey)}`, {
192
- headers: baseHeaders()
193
- });
194
- if (r.status === 404) return null;
195
- if (!r.ok) throw new Error(`slot fetch ${r.status}: ${await r.text()}`);
196
- return await r.json();
197
- }
198
- async function fetchSlotsBulk(slotKeys) {
199
- if (slotKeys.length === 0) return {};
200
- const r = await fetch(`${endpoint}/slots/resolve`, {
201
- method: "POST",
202
- headers: { ...baseHeaders(), "Content-Type": "application/json" },
203
- body: JSON.stringify({ keys: slotKeys })
204
- });
205
- if (!r.ok) throw new Error(`slots resolve ${r.status}: ${await r.text()}`);
206
- const body = await r.json();
207
- return body.resolved;
208
- }
209
- async function resolveSlot(slotKey, opts = {}) {
210
- const ttl = opts.ttlMs ?? DEFAULT_TTL_MS;
211
- const cacheKey = `${tenantCode ?? "_"}:${slotKey}`;
212
- const now = Date.now();
213
- let dto;
214
- const hit = cache.get(cacheKey);
215
- if (hit && now - hit.fetchedAt < ttl) {
216
- dto = hit.value;
217
- } else {
218
- dto = await fetchSlot(slotKey);
219
- cache.set(cacheKey, { fetchedAt: now, value: dto });
220
- }
221
- return materializeResolution(dto, opts.preset);
222
- }
223
- async function resolveSlots(slotKeys, opts = {}) {
224
- if (slotKeys.length === 0) return {};
225
- const ttl = opts.ttlMs ?? DEFAULT_TTL_MS;
226
- const now = Date.now();
227
- const missing = [];
228
- const out = {};
229
- for (const k of slotKeys) {
230
- const cacheKey = `${tenantCode ?? "_"}:${k}`;
231
- const hit = cache.get(cacheKey);
232
- if (hit && now - hit.fetchedAt < ttl) {
233
- out[k] = materializeResolution(hit.value, opts.preset);
234
- } else {
235
- missing.push(k);
236
- }
237
- }
238
- if (missing.length > 0) {
239
- const resolved = await fetchSlotsBulk(missing);
240
- for (const k of missing) {
241
- const dto = resolved[k] ?? null;
242
- cache.set(`${tenantCode ?? "_"}:${k}`, { fetchedAt: now, value: dto });
243
- out[k] = materializeResolution(dto, opts.preset);
244
- }
245
- }
246
- return out;
247
- }
248
- function defaultPresetFor(asset) {
249
- if (!asset) return "lg";
250
- return asset.kind === "video" ? "video" : "lg";
251
- }
252
- function materializeResolution(dto, overridePreset) {
253
- if (!dto) return { slot: null, preset: overridePreset ?? "lg", url: null };
254
- const effective = overridePreset ?? dto.preset ?? defaultPresetFor(dto.asset);
255
- const finalPreset = hasPreset(dto.asset, effective) ? effective : defaultPresetFor(dto.asset);
256
- return {
257
- slot: dto,
258
- preset: finalPreset,
259
- url: getAssetUrl(dto.asset, finalPreset)
260
- };
261
- }
262
-
263
243
  // src/transform.ts
264
244
  var TRANSFORM_WIDTHS = [
265
245
  96,
@@ -331,12 +311,22 @@ function extForOptions(opts) {
331
311
  }
332
312
  }
333
313
  function getVideoTransformUrl(asset, opts) {
314
+ assertPublic(
315
+ asset,
316
+ "getVideoTransformUrl",
317
+ "getPrivateTransformUrl(asset, opts, signingKey, { expiresInSeconds: 300 })"
318
+ );
334
319
  const dsl = serializeTransform(opts);
335
320
  if (!dsl) return null;
336
321
  const ext = opts.format === "webm" ? "webm" : "mp4";
337
322
  return `${getCdnBase()}/t/${dsl}/${asset.sha}.${ext}`;
338
323
  }
339
324
  function getHlsStreamingUrl(asset, opts = {}) {
325
+ assertPublic(
326
+ asset,
327
+ "getHlsStreamingUrl",
328
+ 'getPrivateAssetUrl(asset, "hls", signingKey, { expiresInSeconds: 300 }) \u2014 the worker re-signs the playlist children'
329
+ );
340
330
  const merged = { ...opts, format: "hls" };
341
331
  const dsl = serializeTransform(merged);
342
332
  return `${getCdnBase()}/t/${dsl}/${asset.sha}.m3u8`;
@@ -348,9 +338,19 @@ function buildTransformUrl(asset, opts) {
348
338
  return `${getCdnBase()}/t/${dsl}/${asset.sha}.${ext}`;
349
339
  }
350
340
  function getTransformUrl(asset, opts) {
341
+ assertPublic(
342
+ asset,
343
+ "getTransformUrl",
344
+ "getPrivateTransformUrl(asset, opts, signingKey, { expiresInSeconds: 300 })"
345
+ );
351
346
  return buildTransformUrl(asset, opts);
352
347
  }
353
348
  function getSignedTransformUrl(asset, opts, signingKey) {
349
+ assertPublic(
350
+ asset,
351
+ "getSignedTransformUrl",
352
+ "getPrivateTransformUrl(asset, opts, signingKey, { expiresInSeconds: 300 })"
353
+ );
354
354
  const url = buildTransformUrl(asset, opts);
355
355
  if (!url) return null;
356
356
  return signTransformUrl(url, signingKey);
@@ -381,12 +381,113 @@ async function hmacSha256Hex(key, message) {
381
381
  return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
382
382
  }
383
383
  function getTransformSrcSet(asset, widths, extraOpts = {}) {
384
+ assertPublic(
385
+ asset,
386
+ "getTransformSrcSet",
387
+ "getPrivateTransformUrl(asset, opts, signingKey, { expiresInSeconds: 300 }) per width"
388
+ );
384
389
  return widths.map((w) => {
385
390
  const url = buildTransformUrl(asset, { ...extraOpts, width: w });
386
391
  return url ? `${url} ${w}w` : null;
387
392
  }).filter((s) => s != null).join(", ");
388
393
  }
389
394
 
395
+ // src/slots.ts
396
+ var DEFAULT_TTL_MS = 6e4;
397
+ var cache = /* @__PURE__ */ new Map();
398
+ var endpoint = "https://api.nitida.gofuture.space";
399
+ var apiKey = null;
400
+ var tenantCode = null;
401
+ function configureSlotResolver(opts) {
402
+ if (opts.endpoint) endpoint = opts.endpoint.replace(/\/+$/, "");
403
+ if (opts.apiKey !== void 0) apiKey = opts.apiKey;
404
+ if (opts.tenantCode !== void 0) tenantCode = opts.tenantCode;
405
+ }
406
+ function invalidateSlotCache(slotKey) {
407
+ if (slotKey === void 0) cache.clear();
408
+ else
409
+ for (const k of cache.keys())
410
+ if (k.endsWith(`:${slotKey}`)) cache.delete(k);
411
+ }
412
+ var baseHeaders = () => {
413
+ const h = {};
414
+ if (apiKey) h.Authorization = `Bearer ${apiKey}`;
415
+ if (tenantCode) h["X-Tenant-Code"] = tenantCode;
416
+ return h;
417
+ };
418
+ async function fetchSlot(slotKey) {
419
+ const r = await fetch(`${endpoint}/slots/${encodeURIComponent(slotKey)}`, {
420
+ headers: baseHeaders()
421
+ });
422
+ if (r.status === 404) return null;
423
+ if (!r.ok) throw new Error(`slot fetch ${r.status}: ${await r.text()}`);
424
+ return await r.json();
425
+ }
426
+ async function fetchSlotsBulk(slotKeys) {
427
+ if (slotKeys.length === 0) return {};
428
+ const r = await fetch(`${endpoint}/slots/resolve`, {
429
+ method: "POST",
430
+ headers: { ...baseHeaders(), "Content-Type": "application/json" },
431
+ body: JSON.stringify({ keys: slotKeys })
432
+ });
433
+ if (!r.ok) throw new Error(`slots resolve ${r.status}: ${await r.text()}`);
434
+ const body = await r.json();
435
+ return body.resolved;
436
+ }
437
+ async function resolveSlot(slotKey, opts = {}) {
438
+ const ttl = opts.ttlMs ?? DEFAULT_TTL_MS;
439
+ const cacheKey = `${tenantCode ?? "_"}:${slotKey}`;
440
+ const now = Date.now();
441
+ let dto;
442
+ const hit = cache.get(cacheKey);
443
+ if (hit && now - hit.fetchedAt < ttl) {
444
+ dto = hit.value;
445
+ } else {
446
+ dto = await fetchSlot(slotKey);
447
+ cache.set(cacheKey, { fetchedAt: now, value: dto });
448
+ }
449
+ return materializeResolution(dto, opts.preset);
450
+ }
451
+ async function resolveSlots(slotKeys, opts = {}) {
452
+ if (slotKeys.length === 0) return {};
453
+ const ttl = opts.ttlMs ?? DEFAULT_TTL_MS;
454
+ const now = Date.now();
455
+ const missing = [];
456
+ const out = {};
457
+ for (const k of slotKeys) {
458
+ const cacheKey = `${tenantCode ?? "_"}:${k}`;
459
+ const hit = cache.get(cacheKey);
460
+ if (hit && now - hit.fetchedAt < ttl) {
461
+ out[k] = materializeResolution(hit.value, opts.preset);
462
+ } else {
463
+ missing.push(k);
464
+ }
465
+ }
466
+ if (missing.length > 0) {
467
+ const resolved = await fetchSlotsBulk(missing);
468
+ for (const k of missing) {
469
+ const dto = resolved[k] ?? null;
470
+ cache.set(`${tenantCode ?? "_"}:${k}`, { fetchedAt: now, value: dto });
471
+ out[k] = materializeResolution(dto, opts.preset);
472
+ }
473
+ }
474
+ return out;
475
+ }
476
+ function defaultPresetFor(asset) {
477
+ if (!asset) return "lg";
478
+ return asset.kind === "video" ? "video" : "lg";
479
+ }
480
+ function materializeResolution(dto, overridePreset) {
481
+ if (!dto) return { slot: null, preset: overridePreset ?? "lg", url: null };
482
+ const effective = overridePreset ?? dto.preset ?? defaultPresetFor(dto.asset);
483
+ const finalPreset = hasPreset(dto.asset, effective) ? effective : defaultPresetFor(dto.asset);
484
+ return {
485
+ slot: dto,
486
+ preset: finalPreset,
487
+ url: getAssetUrl(dto.asset, finalPreset)
488
+ };
489
+ }
490
+
390
491
  // src/index.ts
391
492
  var PRESET_SHORT = {
392
493
  thumb: "q",
@@ -513,6 +614,23 @@ function originalExtForMime(mime) {
513
614
  return (mime ? ORIGINAL_EXT_BY_MIME[mime] : void 0) ?? PRESET_EXT.original;
514
615
  }
515
616
  function getAssetUrl(asset, preset) {
617
+ assertPublic(
618
+ asset,
619
+ "getAssetUrl",
620
+ `getPrivateAssetUrl(asset, "${preset}", signingKey, { expiresInSeconds: 300 })`
621
+ );
622
+ const fallback = transformFallbackFor(asset, preset);
623
+ if (fallback) return fallback;
624
+ return buildPublicAssetUrl(asset, preset);
625
+ }
626
+ function transformFallbackFor(asset, preset) {
627
+ if (typeof asset.presets !== "string") return null;
628
+ if (hasPreset({ presets: asset.presets }, preset)) return null;
629
+ const maxDim = PRESET_MAX_DIM[preset];
630
+ if (maxDim == null) return null;
631
+ return `${cdnBaseUrl}/t/format=webp,width=${maxDim}/${asset.sha}.webp`;
632
+ }
633
+ function buildPublicAssetUrl(asset, preset) {
516
634
  if (preset === "original") {
517
635
  const stored = asset.variants?.find((v) => v.preset === "original")?.url;
518
636
  if (stored) return stored;
@@ -526,6 +644,25 @@ function getAssetUrl(asset, preset) {
526
644
  }
527
645
  return `${cdnBaseUrl}/${variantPrefix()}${asset.sha}-${PRESET_SHORT[preset]}.${PRESET_EXT[preset]}`;
528
646
  }
647
+ async function getPrivateAssetUrl(asset, preset, signingKey, opts) {
648
+ return signAccessUrl(buildPublicAssetUrl(asset, preset), signingKey, opts);
649
+ }
650
+ async function getPrivateTransformUrl(asset, opts, signingKey, signOpts) {
651
+ const url = buildTransformUrl(asset, opts);
652
+ if (!url) return null;
653
+ const tid = getTenantId();
654
+ if (tid == null) {
655
+ throw new Error(
656
+ "getPrivateTransformUrl: 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."
657
+ );
658
+ }
659
+ const u = new URL(url);
660
+ return signAccessUrl(
661
+ `${u.origin}/${tid.toString(36)}${u.pathname}`,
662
+ signingKey,
663
+ signOpts
664
+ );
665
+ }
529
666
  function hasPreset(asset, preset) {
530
667
  if (preset === "mp3") return asset.presets.includes("mp3");
531
668
  return stripMultiCharTokens(asset.presets).includes(PRESET_SHORT[preset]);
@@ -535,6 +672,11 @@ function stripMultiCharTokens(presets) {
535
672
  }
536
673
  var IMAGE_PRESETS = ["thumb", "sm", "md", "lg", "xl"];
537
674
  function getAssetSrcSet(asset) {
675
+ assertPublic(
676
+ asset,
677
+ "getAssetSrcSet",
678
+ "getPrivateAssetUrl(asset, preset, signingKey, { expiresInSeconds: 300 }) per preset"
679
+ );
538
680
  return IMAGE_PRESETS.filter(
539
681
  (p) => hasPreset(asset, p) && PRESET_MAX_DIM[p] != null
540
682
  ).map((p) => `${getAssetUrl(asset, p)} ${PRESET_MAX_DIM[p]}w`).join(", ");
@@ -561,10 +703,13 @@ function getAssetDimensions(asset) {
561
703
  PRESET_MAX_DIM,
562
704
  PRESET_SHORT,
563
705
  TRANSFORM_WIDTHS,
706
+ accessMessage,
707
+ assertPublic,
564
708
  bestTextContrast,
565
709
  computeVariantDimensions,
566
710
  configureSlotResolver,
567
711
  contrastRatio,
712
+ deriveAccessKey,
568
713
  extractAssetSha,
569
714
  getAmbientGradient,
570
715
  getAssetDimensions,
@@ -575,6 +720,8 @@ function getAssetDimensions(asset) {
575
720
  getHlsStreamingUrl,
576
721
  getPaletteBlurBackground,
577
722
  getPaletteCssVars,
723
+ getPrivateAssetUrl,
724
+ getPrivateTransformUrl,
578
725
  getSignedTransformUrl,
579
726
  getTenantId,
580
727
  getTextColorForBackground,
@@ -592,6 +739,7 @@ function getAssetDimensions(asset) {
592
739
  serializeTransform,
593
740
  setCdnBase,
594
741
  setTenantId,
742
+ signAccessUrl,
595
743
  signTransformUrl
596
744
  });
597
745
  //# sourceMappingURL=index.cjs.map