@nitida/asset-client 0.23.0 → 0.24.1

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/README.md CHANGED
@@ -11,11 +11,12 @@ bun add @nitida/asset-client
11
11
 
12
12
  ## Where these symbols live
13
13
 
14
- Everything documented here is exported by **`@nitida/asset-client`**, and all
15
- 53 exports are re-exported by the root **`@nitida/sdk`** — if you already
16
- installed the SDK, importing from its root works and needs no second dependency.
14
+ Everything documented here is exported by **`@nitida/asset-client`**, and
15
+ **every one of those exports** is re-exported by the root **`@nitida/sdk`** — if
16
+ you already installed the SDK, importing from its root works and needs no second
17
+ dependency.
17
18
 
18
- `@nitida/sdk/server` and `@nitida/sdk/web` carry the same 53 as well, so any of
19
+ `@nitida/sdk/server` and `@nitida/sdk/web` carry the full set as well, so any of
19
20
  the three works. ⚠️ Not so before 2026-08-21 — `/server` was short 28 of the
20
21
  root's exports and `/web` short 37. On an older SDK, import these from the root
21
22
  or from this package.
package/dist/index.cjs CHANGED
@@ -43,6 +43,7 @@ __export(index_exports, {
43
43
  getAssetDimensions: () => getAssetDimensions,
44
44
  getAssetSrcSet: () => getAssetSrcSet,
45
45
  getAssetUrl: () => getAssetUrl,
46
+ getByteBudgetTransformUrl: () => getByteBudgetTransformUrl,
46
47
  getCdnBase: () => getCdnBase,
47
48
  getHlsLadder: () => getHlsLadder,
48
49
  getHlsStreamingUrl: () => getHlsStreamingUrl,
@@ -57,6 +58,7 @@ __export(index_exports, {
57
58
  getTransformUrl: () => getTransformUrl,
58
59
  getVideoTransformUrl: () => getVideoTransformUrl,
59
60
  hasPreset: () => hasPreset,
61
+ hasSizeLadder: () => hasSizeLadder,
60
62
  hlsLadderAlignment: () => hlsLadderAlignment,
61
63
  invalidateSlotCache: () => invalidateSlotCache,
62
64
  isRequestablePreset: () => isRequestablePreset,
@@ -296,10 +298,16 @@ function extractAssetSha(url) {
296
298
  function serializeTransform(opts) {
297
299
  const entries = [];
298
300
  const keys = Object.keys(opts).sort();
301
+ const VALUE_RE = /^[a-z0-9._-]+$/;
299
302
  for (const k of keys) {
300
303
  const v = opts[k];
301
304
  if (v == null) continue;
302
305
  const serialized = typeof v === "string" ? v.toLowerCase() : String(v);
306
+ if (!VALUE_RE.test(serialized)) {
307
+ throw new Error(
308
+ `invalid transform value for "${k}": ${JSON.stringify(serialized)} (only [a-z0-9._-] allowed)`
309
+ );
310
+ }
303
311
  entries.push([k, serialized]);
304
312
  }
305
313
  return entries.map(([k, v]) => `${k}=${v}`).join(",");
@@ -373,6 +381,37 @@ function getTransformUrl(asset, opts) {
373
381
  );
374
382
  return buildTransformUrl(asset, opts);
375
383
  }
384
+ var LADDER_PRESETS = ["thumb", "sm", "md", "lg", "xl"];
385
+ function hasSizeLadder(asset) {
386
+ const raw = asset.presets;
387
+ if (raw == null || raw.trim() === "") return null;
388
+ return LADDER_PRESETS.some((preset) => hasPreset(asset, preset));
389
+ }
390
+ function getByteBudgetTransformUrl(asset, opts) {
391
+ assertSha(asset, "getByteBudgetTransformUrl");
392
+ assertPublic(
393
+ asset,
394
+ "getByteBudgetTransformUrl",
395
+ "getPrivateTransformUrl(asset, opts, signingKey, { expiresInSeconds: 300 })"
396
+ );
397
+ if (!Number.isInteger(opts.quality) || opts.quality < 1 || opts.quality > 100) {
398
+ throw new Error(
399
+ `getByteBudgetTransformUrl: quality must be an integer 1..100, got ${String(opts.quality)}. If you do not have a measured byte budget, use getTransformUrl and omit quality entirely.`
400
+ );
401
+ }
402
+ const ladder = hasSizeLadder(asset);
403
+ if (ladder === null) {
404
+ throw new Error(
405
+ `getByteBudgetTransformUrl: asset ${asset.sha} carries no 'presets', so whether a pinned quality would re-compress it is UNKNOWN \u2014 and unknown is not permission. Fetch the full DTO (assets.get / assets.byHash) and pass it, or use getTransformUrl with no quality.`
406
+ );
407
+ }
408
+ if (ladder) {
409
+ throw new Error(
410
+ `getByteBudgetTransformUrl: asset ${asset.sha} has stored size variants (presets="${asset.presets}"). A pinned quality there does not set the encoder \u2014 it makes /t/ decode one of those already-compressed variants, so the output is a SECOND lossy generation: measured +2..+8% HEAVIER and -0.50..-0.85 dB. Use getTransformUrl with no quality (one pass from the master), or upload this asset with presets: ["original"] if the byte budget is real.`
411
+ );
412
+ }
413
+ return buildTransformUrl(asset, opts);
414
+ }
376
415
  function getSignedTransformUrl(asset, opts, signingKey, signOpts) {
377
416
  assertSha(asset, "getSignedTransformUrl");
378
417
  assertPublic(
@@ -464,6 +503,14 @@ var cache = /* @__PURE__ */ new Map();
464
503
  var endpoint = "https://api.nitida.gofuture.space";
465
504
  var apiKey = null;
466
505
  var tenantCode = null;
506
+ function effectiveConfig(cfg) {
507
+ if (!cfg) return { endpoint, apiKey, tenantCode };
508
+ return {
509
+ endpoint: cfg.endpoint ? cfg.endpoint.replace(/\/+$/, "") : endpoint,
510
+ apiKey: cfg.apiKey ?? null,
511
+ tenantCode: cfg.tenantCode ?? null
512
+ };
513
+ }
467
514
  function configureSlotResolver(opts) {
468
515
  if (opts.endpoint) endpoint = opts.endpoint.replace(/\/+$/, "");
469
516
  if (opts.apiKey !== void 0) apiKey = opts.apiKey;
@@ -475,25 +522,25 @@ function invalidateSlotCache(slotKey) {
475
522
  for (const k of cache.keys())
476
523
  if (k.endsWith(`:${slotKey}`)) cache.delete(k);
477
524
  }
478
- var baseHeaders = () => {
525
+ var baseHeaders = (cfg) => {
479
526
  const h = {};
480
- if (apiKey) h.Authorization = `Bearer ${apiKey}`;
481
- if (tenantCode) h["X-Tenant-Code"] = tenantCode;
527
+ if (cfg.apiKey) h.Authorization = `Bearer ${cfg.apiKey}`;
528
+ if (cfg.tenantCode) h["X-Tenant-Code"] = cfg.tenantCode;
482
529
  return h;
483
530
  };
484
- async function fetchSlot(slotKey) {
485
- const r = await fetch(`${endpoint}/slots/${encodeURIComponent(slotKey)}`, {
486
- headers: baseHeaders()
531
+ async function fetchSlot(slotKey, cfg) {
532
+ const r = await fetch(`${cfg.endpoint}/slots/${encodeURIComponent(slotKey)}`, {
533
+ headers: baseHeaders(cfg)
487
534
  });
488
535
  if (r.status === 404) return null;
489
536
  if (!r.ok) throw new Error(`slot fetch ${r.status}: ${await r.text()}`);
490
537
  return await r.json();
491
538
  }
492
- async function fetchSlotsBulk(slotKeys) {
539
+ async function fetchSlotsBulk(slotKeys, cfg) {
493
540
  if (slotKeys.length === 0) return {};
494
- const r = await fetch(`${endpoint}/slots/resolve`, {
541
+ const r = await fetch(`${cfg.endpoint}/slots/resolve`, {
495
542
  method: "POST",
496
- headers: { ...baseHeaders(), "Content-Type": "application/json" },
543
+ headers: { ...baseHeaders(cfg), "Content-Type": "application/json" },
497
544
  body: JSON.stringify({ keys: slotKeys })
498
545
  });
499
546
  if (!r.ok) throw new Error(`slots resolve ${r.status}: ${await r.text()}`);
@@ -502,14 +549,15 @@ async function fetchSlotsBulk(slotKeys) {
502
549
  }
503
550
  async function resolveSlot(slotKey, opts = {}) {
504
551
  const ttl = opts.ttlMs ?? DEFAULT_TTL_MS;
505
- const cacheKey = `${tenantCode ?? "_"}:${slotKey}`;
552
+ const cfg = effectiveConfig(opts.config);
553
+ const cacheKey = `${cfg.tenantCode ?? "_"}:${slotKey}`;
506
554
  const now = Date.now();
507
555
  let dto;
508
556
  const hit = cache.get(cacheKey);
509
557
  if (hit && now - hit.fetchedAt < ttl) {
510
558
  dto = hit.value;
511
559
  } else {
512
- dto = await fetchSlot(slotKey);
560
+ dto = await fetchSlot(slotKey, cfg);
513
561
  cache.set(cacheKey, { fetchedAt: now, value: dto });
514
562
  }
515
563
  return materializeResolution(dto, opts.preset);
@@ -517,11 +565,12 @@ async function resolveSlot(slotKey, opts = {}) {
517
565
  async function resolveSlots(slotKeys, opts = {}) {
518
566
  if (slotKeys.length === 0) return {};
519
567
  const ttl = opts.ttlMs ?? DEFAULT_TTL_MS;
568
+ const cfg = effectiveConfig(opts.config);
520
569
  const now = Date.now();
521
570
  const missing = [];
522
571
  const out = {};
523
572
  for (const k of slotKeys) {
524
- const cacheKey = `${tenantCode ?? "_"}:${k}`;
573
+ const cacheKey = `${cfg.tenantCode ?? "_"}:${k}`;
525
574
  const hit = cache.get(cacheKey);
526
575
  if (hit && now - hit.fetchedAt < ttl) {
527
576
  out[k] = materializeResolution(hit.value, opts.preset);
@@ -530,10 +579,10 @@ async function resolveSlots(slotKeys, opts = {}) {
530
579
  }
531
580
  }
532
581
  if (missing.length > 0) {
533
- const resolved = await fetchSlotsBulk(missing);
582
+ const resolved = await fetchSlotsBulk(missing, cfg);
534
583
  for (const k of missing) {
535
584
  const dto = resolved[k] ?? null;
536
- cache.set(`${tenantCode ?? "_"}:${k}`, { fetchedAt: now, value: dto });
585
+ cache.set(`${cfg.tenantCode ?? "_"}:${k}`, { fetchedAt: now, value: dto });
537
586
  out[k] = materializeResolution(dto, opts.preset);
538
587
  }
539
588
  }
@@ -827,6 +876,7 @@ function getAssetDimensions(asset) {
827
876
  getAssetDimensions,
828
877
  getAssetSrcSet,
829
878
  getAssetUrl,
879
+ getByteBudgetTransformUrl,
830
880
  getCdnBase,
831
881
  getHlsLadder,
832
882
  getHlsStreamingUrl,
@@ -841,6 +891,7 @@ function getAssetDimensions(asset) {
841
891
  getTransformUrl,
842
892
  getVideoTransformUrl,
843
893
  hasPreset,
894
+ hasSizeLadder,
844
895
  hlsLadderAlignment,
845
896
  invalidateSlotCache,
846
897
  isRequestablePreset,