@nitida/asset-client 0.21.0 → 0.24.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 +7 -0
- package/README.md +9 -4
- package/dist/index.cjs +102 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +237 -45
- package/dist/index.d.ts +237 -45
- package/dist/index.js +94 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/access.ts +21 -1
- package/src/index.ts +39 -30
- package/src/transform.ts +321 -35
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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,11 +37,13 @@ __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,
|
|
40
44
|
getAssetSrcSet: () => getAssetSrcSet,
|
|
41
45
|
getAssetUrl: () => getAssetUrl,
|
|
46
|
+
getByteBudgetTransformUrl: () => getByteBudgetTransformUrl,
|
|
42
47
|
getCdnBase: () => getCdnBase,
|
|
43
48
|
getHlsLadder: () => getHlsLadder,
|
|
44
49
|
getHlsStreamingUrl: () => getHlsStreamingUrl,
|
|
@@ -53,6 +58,7 @@ __export(index_exports, {
|
|
|
53
58
|
getTransformUrl: () => getTransformUrl,
|
|
54
59
|
getVideoTransformUrl: () => getVideoTransformUrl,
|
|
55
60
|
hasPreset: () => hasPreset,
|
|
61
|
+
hasSizeLadder: () => hasSizeLadder,
|
|
56
62
|
hlsLadderAlignment: () => hlsLadderAlignment,
|
|
57
63
|
invalidateSlotCache: () => invalidateSlotCache,
|
|
58
64
|
isRequestablePreset: () => isRequestablePreset,
|
|
@@ -66,7 +72,8 @@ __export(index_exports, {
|
|
|
66
72
|
setTenantId: () => setTenantId,
|
|
67
73
|
signAccessUrl: () => signAccessUrl,
|
|
68
74
|
signTransformUrl: () => signTransformUrl,
|
|
69
|
-
toRequestablePresets: () => toRequestablePresets
|
|
75
|
+
toRequestablePresets: () => toRequestablePresets,
|
|
76
|
+
transformMessage: () => transformMessage
|
|
70
77
|
});
|
|
71
78
|
module.exports = __toCommonJS(index_exports);
|
|
72
79
|
|
|
@@ -97,12 +104,18 @@ function accessMessage(tenantPrefix, exp, resourcePath) {
|
|
|
97
104
|
${exp}
|
|
98
105
|
${resourcePath.replace(/^\/+/, "")}`;
|
|
99
106
|
}
|
|
107
|
+
var MAX_SIGNED_URL_TTL_SECONDS = 7 * 24 * 60 * 60;
|
|
100
108
|
async function signAccessUrl(publicUrl, signingKey, opts) {
|
|
101
109
|
if (!Number.isFinite(opts.expiresInSeconds) || opts.expiresInSeconds <= 0) {
|
|
102
110
|
throw new Error(
|
|
103
111
|
"signAccessUrl: `expiresInSeconds` must be a positive number \u2014 a signed URL without an expiry is a public URL the moment it is forwarded."
|
|
104
112
|
);
|
|
105
113
|
}
|
|
114
|
+
if (opts.expiresInSeconds > MAX_SIGNED_URL_TTL_SECONDS) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`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.`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
106
119
|
const u = new URL(publicUrl);
|
|
107
120
|
const segments = u.pathname.split("/").filter(Boolean);
|
|
108
121
|
if (segments[0] === "a" && segments[2] && /^[vrt]$/.test(segments[2])) {
|
|
@@ -258,6 +271,7 @@ var TRANSFORM_WIDTHS = [
|
|
|
258
271
|
96,
|
|
259
272
|
128,
|
|
260
273
|
160,
|
|
274
|
+
180,
|
|
261
275
|
240,
|
|
262
276
|
256,
|
|
263
277
|
320,
|
|
@@ -361,7 +375,38 @@ function getTransformUrl(asset, opts) {
|
|
|
361
375
|
);
|
|
362
376
|
return buildTransformUrl(asset, opts);
|
|
363
377
|
}
|
|
364
|
-
|
|
378
|
+
var LADDER_PRESETS = ["thumb", "sm", "md", "lg", "xl"];
|
|
379
|
+
function hasSizeLadder(asset) {
|
|
380
|
+
const raw = asset.presets;
|
|
381
|
+
if (raw == null || raw.trim() === "") return null;
|
|
382
|
+
return LADDER_PRESETS.some((preset) => hasPreset(asset, preset));
|
|
383
|
+
}
|
|
384
|
+
function getByteBudgetTransformUrl(asset, opts) {
|
|
385
|
+
assertSha(asset, "getByteBudgetTransformUrl");
|
|
386
|
+
assertPublic(
|
|
387
|
+
asset,
|
|
388
|
+
"getByteBudgetTransformUrl",
|
|
389
|
+
"getPrivateTransformUrl(asset, opts, signingKey, { expiresInSeconds: 300 })"
|
|
390
|
+
);
|
|
391
|
+
if (!Number.isInteger(opts.quality) || opts.quality < 1 || opts.quality > 100) {
|
|
392
|
+
throw new Error(
|
|
393
|
+
`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.`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
const ladder = hasSizeLadder(asset);
|
|
397
|
+
if (ladder === null) {
|
|
398
|
+
throw new Error(
|
|
399
|
+
`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.`
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
if (ladder) {
|
|
403
|
+
throw new Error(
|
|
404
|
+
`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.`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
return buildTransformUrl(asset, opts);
|
|
408
|
+
}
|
|
409
|
+
function getSignedTransformUrl(asset, opts, signingKey, signOpts) {
|
|
365
410
|
assertSha(asset, "getSignedTransformUrl");
|
|
366
411
|
assertPublic(
|
|
367
412
|
asset,
|
|
@@ -370,9 +415,34 @@ function getSignedTransformUrl(asset, opts, signingKey) {
|
|
|
370
415
|
);
|
|
371
416
|
const url = buildTransformUrl(asset, opts);
|
|
372
417
|
if (!url) return null;
|
|
373
|
-
return signTransformUrl(url, signingKey);
|
|
374
|
-
}
|
|
375
|
-
|
|
418
|
+
return signTransformUrl(url, signingKey, signOpts);
|
|
419
|
+
}
|
|
420
|
+
var TRANSFORM_SIG_DOMAIN = "nitida/transform/v2";
|
|
421
|
+
var KID_INFO = "nitida/kid/v1";
|
|
422
|
+
var MAX_SIGNED_TRANSFORM_TTL_SECONDS = 7 * 24 * 60 * 60;
|
|
423
|
+
var MIN_SIGNED_TRANSFORM_TTL_SECONDS = 120;
|
|
424
|
+
async function deriveTransformKid(signingKey) {
|
|
425
|
+
return (await hmacSha256Hex(signingKey, KID_INFO)).slice(0, 8);
|
|
426
|
+
}
|
|
427
|
+
function transformMessage(args) {
|
|
428
|
+
return `${TRANSFORM_SIG_DOMAIN}
|
|
429
|
+
${args.tenantPrefix}
|
|
430
|
+
${args.exp}
|
|
431
|
+
${args.canonicalDsl}/${args.filename}`;
|
|
432
|
+
}
|
|
433
|
+
async function signTransformUrl(unsignedUrl, signingKey, opts) {
|
|
434
|
+
const ttl = Math.floor(opts.expiresInSeconds);
|
|
435
|
+
if (!Number.isFinite(ttl) || ttl < MIN_SIGNED_TRANSFORM_TTL_SECONDS || ttl > MAX_SIGNED_TRANSFORM_TTL_SECONDS) {
|
|
436
|
+
throw new Error(
|
|
437
|
+
`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.`
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
const tid = opts.tenantId ?? getTenantId();
|
|
441
|
+
if (tid == null) {
|
|
442
|
+
throw new Error(
|
|
443
|
+
"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."
|
|
444
|
+
);
|
|
445
|
+
}
|
|
376
446
|
const u = new URL(unsignedUrl);
|
|
377
447
|
const parts = u.pathname.split("/").filter(Boolean);
|
|
378
448
|
if (parts[0] !== "t" || parts.length < 3) {
|
|
@@ -380,8 +450,19 @@ async function signTransformUrl(unsignedUrl, signingKey) {
|
|
|
380
450
|
}
|
|
381
451
|
const filename = parts[parts.length - 1];
|
|
382
452
|
const dsl = parts.slice(1, -1).join("/");
|
|
383
|
-
const
|
|
384
|
-
const
|
|
453
|
+
const now = opts.nowSeconds ?? Math.floor(Date.now() / 1e3);
|
|
454
|
+
const exp = Math.floor((now + ttl) / 60) * 60;
|
|
455
|
+
const sig = await hmacSha256Hex(
|
|
456
|
+
signingKey,
|
|
457
|
+
transformMessage({
|
|
458
|
+
tenantPrefix: tid.toString(36),
|
|
459
|
+
exp,
|
|
460
|
+
canonicalDsl: dsl,
|
|
461
|
+
filename
|
|
462
|
+
})
|
|
463
|
+
);
|
|
464
|
+
u.searchParams.set("kid", await deriveTransformKid(signingKey));
|
|
465
|
+
u.searchParams.set("exp", String(exp));
|
|
385
466
|
u.searchParams.set("sig", sig);
|
|
386
467
|
return u.toString();
|
|
387
468
|
}
|
|
@@ -508,16 +589,16 @@ function materializeResolution(dto, overridePreset) {
|
|
|
508
589
|
|
|
509
590
|
// src/index.ts
|
|
510
591
|
var REQUESTABLE_PRESETS = [
|
|
511
|
-
"
|
|
512
|
-
"sm",
|
|
513
|
-
"md",
|
|
592
|
+
"aiproxy",
|
|
514
593
|
"lg",
|
|
515
|
-
"
|
|
594
|
+
"md",
|
|
516
595
|
"original",
|
|
517
596
|
"poster",
|
|
597
|
+
"probe",
|
|
598
|
+
"sm",
|
|
599
|
+
"thumb",
|
|
518
600
|
"video",
|
|
519
|
-
"
|
|
520
|
-
"probe"
|
|
601
|
+
"xl"
|
|
521
602
|
];
|
|
522
603
|
var isRequestablePreset = (v) => REQUESTABLE_PRESETS.includes(v);
|
|
523
604
|
var toRequestablePresets = (input) => {
|
|
@@ -756,6 +837,9 @@ function getAssetDimensions(asset) {
|
|
|
756
837
|
}
|
|
757
838
|
// Annotate the CommonJS export names for ESM import in node:
|
|
758
839
|
0 && (module.exports = {
|
|
840
|
+
MAX_SIGNED_TRANSFORM_TTL_SECONDS,
|
|
841
|
+
MAX_SIGNED_URL_TTL_SECONDS,
|
|
842
|
+
MIN_SIGNED_TRANSFORM_TTL_SECONDS,
|
|
759
843
|
PRESET_EXT,
|
|
760
844
|
PRESET_LONG,
|
|
761
845
|
PRESET_MAX_DIM,
|
|
@@ -770,11 +854,13 @@ function getAssetDimensions(asset) {
|
|
|
770
854
|
configureSlotResolver,
|
|
771
855
|
contrastRatio,
|
|
772
856
|
deriveAccessKey,
|
|
857
|
+
deriveTransformKid,
|
|
773
858
|
extractAssetSha,
|
|
774
859
|
getAmbientGradient,
|
|
775
860
|
getAssetDimensions,
|
|
776
861
|
getAssetSrcSet,
|
|
777
862
|
getAssetUrl,
|
|
863
|
+
getByteBudgetTransformUrl,
|
|
778
864
|
getCdnBase,
|
|
779
865
|
getHlsLadder,
|
|
780
866
|
getHlsStreamingUrl,
|
|
@@ -789,6 +875,7 @@ function getAssetDimensions(asset) {
|
|
|
789
875
|
getTransformUrl,
|
|
790
876
|
getVideoTransformUrl,
|
|
791
877
|
hasPreset,
|
|
878
|
+
hasSizeLadder,
|
|
792
879
|
hlsLadderAlignment,
|
|
793
880
|
invalidateSlotCache,
|
|
794
881
|
isRequestablePreset,
|
|
@@ -802,6 +889,7 @@ function getAssetDimensions(asset) {
|
|
|
802
889
|
setTenantId,
|
|
803
890
|
signAccessUrl,
|
|
804
891
|
signTransformUrl,
|
|
805
|
-
toRequestablePresets
|
|
892
|
+
toRequestablePresets,
|
|
893
|
+
transformMessage
|
|
806
894
|
});
|
|
807
895
|
//# sourceMappingURL=index.cjs.map
|