@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 +26 -0
- package/README.md +6 -3
- package/dist/index.cjs +247 -99
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +243 -85
- package/dist/index.d.ts +243 -85
- package/dist/index.js +241 -99
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/access.ts +200 -0
- package/src/index.ts +208 -1
- package/src/transform.ts +39 -5
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
|
-
|
|
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 |
|
|
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
|
|
81
|
-
if (!fromHex || !
|
|
82
|
-
return `linear-gradient(${opts.angle ?? "135deg"}, ${fromHex}, ${
|
|
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
|