@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 +5 -4
- package/dist/index.cjs +65 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +101 -38
- package/dist/index.d.ts +101 -38
- package/dist/index.js +63 -14
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +2 -0
- package/src/slots.ts +56 -13
- package/src/transform.ts +165 -38
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
|
|
15
|
-
|
|
16
|
-
installed the SDK, importing from its root works and needs no second
|
|
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
|
|
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
|
|
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,
|