@nitida/asset-client 0.17.0 → 0.18.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/AGENTS.md +26 -0
- package/README.md +6 -3
- package/dist/index.cjs +265 -99
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +266 -85
- package/dist/index.d.ts +266 -85
- package/dist/index.js +258 -99
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/access.ts +232 -0
- package/src/index.ts +219 -1
- package/src/transform.ts +44 -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,14 @@ __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,
|
|
30
|
+
assertSha: () => assertSha,
|
|
28
31
|
bestTextContrast: () => bestTextContrast,
|
|
29
32
|
computeVariantDimensions: () => computeVariantDimensions,
|
|
30
33
|
configureSlotResolver: () => configureSlotResolver,
|
|
31
34
|
contrastRatio: () => contrastRatio,
|
|
35
|
+
deriveAccessKey: () => deriveAccessKey,
|
|
32
36
|
extractAssetSha: () => extractAssetSha,
|
|
33
37
|
getAmbientGradient: () => getAmbientGradient,
|
|
34
38
|
getAssetDimensions: () => getAssetDimensions,
|
|
@@ -39,6 +43,8 @@ __export(index_exports, {
|
|
|
39
43
|
getHlsStreamingUrl: () => getHlsStreamingUrl,
|
|
40
44
|
getPaletteBlurBackground: () => getPaletteBlurBackground,
|
|
41
45
|
getPaletteCssVars: () => getPaletteCssVars,
|
|
46
|
+
getPrivateAssetUrl: () => getPrivateAssetUrl,
|
|
47
|
+
getPrivateTransformUrl: () => getPrivateTransformUrl,
|
|
42
48
|
getSignedTransformUrl: () => getSignedTransformUrl,
|
|
43
49
|
getTenantId: () => getTenantId,
|
|
44
50
|
getTextColorForBackground: () => getTextColorForBackground,
|
|
@@ -56,10 +62,89 @@ __export(index_exports, {
|
|
|
56
62
|
serializeTransform: () => serializeTransform,
|
|
57
63
|
setCdnBase: () => setCdnBase,
|
|
58
64
|
setTenantId: () => setTenantId,
|
|
65
|
+
signAccessUrl: () => signAccessUrl,
|
|
59
66
|
signTransformUrl: () => signTransformUrl
|
|
60
67
|
});
|
|
61
68
|
module.exports = __toCommonJS(index_exports);
|
|
62
69
|
|
|
70
|
+
// src/access.ts
|
|
71
|
+
var ACCESS_KEY_INFO = "nitida/access/v1";
|
|
72
|
+
async function hmac(key, message) {
|
|
73
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
74
|
+
"raw",
|
|
75
|
+
key,
|
|
76
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
77
|
+
false,
|
|
78
|
+
["sign"]
|
|
79
|
+
);
|
|
80
|
+
return new Uint8Array(
|
|
81
|
+
await crypto.subtle.sign(
|
|
82
|
+
"HMAC",
|
|
83
|
+
cryptoKey,
|
|
84
|
+
new TextEncoder().encode(message)
|
|
85
|
+
)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
var toHex = (b) => [...b].map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
89
|
+
async function deriveAccessKey(signingKey) {
|
|
90
|
+
return hmac(new TextEncoder().encode(signingKey), ACCESS_KEY_INFO);
|
|
91
|
+
}
|
|
92
|
+
function accessMessage(tenantPrefix, exp, resourcePath) {
|
|
93
|
+
return `${tenantPrefix}
|
|
94
|
+
${exp}
|
|
95
|
+
${resourcePath.replace(/^\/+/, "")}`;
|
|
96
|
+
}
|
|
97
|
+
async function signAccessUrl(publicUrl, signingKey, opts) {
|
|
98
|
+
if (!Number.isFinite(opts.expiresInSeconds) || opts.expiresInSeconds <= 0) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
"signAccessUrl: `expiresInSeconds` must be a positive number \u2014 a signed URL without an expiry is a public URL the moment it is forwarded."
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
const u = new URL(publicUrl);
|
|
104
|
+
const segments = u.pathname.split("/").filter(Boolean);
|
|
105
|
+
if (segments[0] === "a" && segments[2] && /^[vrt]$/.test(segments[2])) {
|
|
106
|
+
segments.shift();
|
|
107
|
+
}
|
|
108
|
+
const tenantPrefix = segments.shift();
|
|
109
|
+
if (!tenantPrefix || segments.length === 0) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`signAccessUrl: expected a tenant-prefixed CDN path like /<tenant>/v/<sha>-<preset>.<ext>, got ${u.pathname}`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (!/^[vrt]$/.test(segments[0])) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`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}.`)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const resourcePath = segments.join("/");
|
|
120
|
+
const now = opts.nowSeconds ?? Math.floor(Date.now() / 1e3);
|
|
121
|
+
const exp = now + Math.floor(opts.expiresInSeconds);
|
|
122
|
+
const sig = toHex(
|
|
123
|
+
await hmac(
|
|
124
|
+
await deriveAccessKey(signingKey),
|
|
125
|
+
accessMessage(tenantPrefix, exp, resourcePath)
|
|
126
|
+
)
|
|
127
|
+
);
|
|
128
|
+
u.pathname = `/a/${tenantPrefix}/${resourcePath}`;
|
|
129
|
+
u.searchParams.set("exp", String(exp));
|
|
130
|
+
u.searchParams.set("sig", sig);
|
|
131
|
+
return u.toString();
|
|
132
|
+
}
|
|
133
|
+
function assertPublic(asset, fn, escape) {
|
|
134
|
+
if (asset.visibility !== "private") return;
|
|
135
|
+
throw new Error(
|
|
136
|
+
`${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.`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
function assertSha(asset, fn) {
|
|
140
|
+
const sha = asset?.sha;
|
|
141
|
+
if (typeof sha === "string" && /^[0-9a-f]{16,64}$/i.test(sha)) return;
|
|
142
|
+
const hint = asset && typeof asset === "object" && "sha256" in asset ? " The value you passed has `sha256` but not `sha` \u2014 that is the shape `upload()` returns. Use `{ sha: result.sha256.slice(0, 16) }`, or fetch the DTO with `assets.get(id)`." : ` Got ${JSON.stringify(sha)}.`;
|
|
143
|
+
throw new Error(
|
|
144
|
+
`${fn}: no usable \`sha\` on the value you passed, so the URL would contain "undefined" and 404 somewhere else.${hint}`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
63
148
|
// src/palette.ts
|
|
64
149
|
function resolveSwatch(palette, ...keys) {
|
|
65
150
|
if (!palette) return null;
|
|
@@ -77,9 +162,9 @@ function pickAmbientBackground(palette) {
|
|
|
77
162
|
function getAmbientGradient(palette, opts = {}) {
|
|
78
163
|
if (!palette) return void 0;
|
|
79
164
|
const fromHex = palette[opts.from ?? "lm"] ?? palette.m ?? palette.d;
|
|
80
|
-
const
|
|
81
|
-
if (!fromHex || !
|
|
82
|
-
return `linear-gradient(${opts.angle ?? "135deg"}, ${fromHex}, ${
|
|
165
|
+
const toHex2 = palette[opts.to ?? "m"] ?? palette.dm ?? palette.d;
|
|
166
|
+
if (!fromHex || !toHex2) return void 0;
|
|
167
|
+
return `linear-gradient(${opts.angle ?? "135deg"}, ${fromHex}, ${toHex2})`;
|
|
83
168
|
}
|
|
84
169
|
function getTextColorForBackground(swatch) {
|
|
85
170
|
if (!swatch) return "#000000";
|
|
@@ -164,102 +249,6 @@ function getPaletteBlurBackground(palette) {
|
|
|
164
249
|
return layers.length > 0 ? `${layers.join(", ")}, ${base}` : base;
|
|
165
250
|
}
|
|
166
251
|
|
|
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
252
|
// src/transform.ts
|
|
264
253
|
var TRANSFORM_WIDTHS = [
|
|
265
254
|
96,
|
|
@@ -331,12 +320,24 @@ function extForOptions(opts) {
|
|
|
331
320
|
}
|
|
332
321
|
}
|
|
333
322
|
function getVideoTransformUrl(asset, opts) {
|
|
323
|
+
assertSha(asset, "getVideoTransformUrl");
|
|
324
|
+
assertPublic(
|
|
325
|
+
asset,
|
|
326
|
+
"getVideoTransformUrl",
|
|
327
|
+
"getPrivateTransformUrl(asset, opts, signingKey, { expiresInSeconds: 300 })"
|
|
328
|
+
);
|
|
334
329
|
const dsl = serializeTransform(opts);
|
|
335
330
|
if (!dsl) return null;
|
|
336
331
|
const ext = opts.format === "webm" ? "webm" : "mp4";
|
|
337
332
|
return `${getCdnBase()}/t/${dsl}/${asset.sha}.${ext}`;
|
|
338
333
|
}
|
|
339
334
|
function getHlsStreamingUrl(asset, opts = {}) {
|
|
335
|
+
assertSha(asset, "getHlsStreamingUrl");
|
|
336
|
+
assertPublic(
|
|
337
|
+
asset,
|
|
338
|
+
"getHlsStreamingUrl",
|
|
339
|
+
'getPrivateAssetUrl(asset, "hls", signingKey, { expiresInSeconds: 300 }) \u2014 the worker re-signs the playlist children'
|
|
340
|
+
);
|
|
340
341
|
const merged = { ...opts, format: "hls" };
|
|
341
342
|
const dsl = serializeTransform(merged);
|
|
342
343
|
return `${getCdnBase()}/t/${dsl}/${asset.sha}.m3u8`;
|
|
@@ -348,9 +349,21 @@ function buildTransformUrl(asset, opts) {
|
|
|
348
349
|
return `${getCdnBase()}/t/${dsl}/${asset.sha}.${ext}`;
|
|
349
350
|
}
|
|
350
351
|
function getTransformUrl(asset, opts) {
|
|
352
|
+
assertSha(asset, "getTransformUrl");
|
|
353
|
+
assertPublic(
|
|
354
|
+
asset,
|
|
355
|
+
"getTransformUrl",
|
|
356
|
+
"getPrivateTransformUrl(asset, opts, signingKey, { expiresInSeconds: 300 })"
|
|
357
|
+
);
|
|
351
358
|
return buildTransformUrl(asset, opts);
|
|
352
359
|
}
|
|
353
360
|
function getSignedTransformUrl(asset, opts, signingKey) {
|
|
361
|
+
assertSha(asset, "getSignedTransformUrl");
|
|
362
|
+
assertPublic(
|
|
363
|
+
asset,
|
|
364
|
+
"getSignedTransformUrl",
|
|
365
|
+
"getPrivateTransformUrl(asset, opts, signingKey, { expiresInSeconds: 300 })"
|
|
366
|
+
);
|
|
354
367
|
const url = buildTransformUrl(asset, opts);
|
|
355
368
|
if (!url) return null;
|
|
356
369
|
return signTransformUrl(url, signingKey);
|
|
@@ -381,12 +394,114 @@ async function hmacSha256Hex(key, message) {
|
|
|
381
394
|
return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
382
395
|
}
|
|
383
396
|
function getTransformSrcSet(asset, widths, extraOpts = {}) {
|
|
397
|
+
assertSha(asset, "getTransformSrcSet");
|
|
398
|
+
assertPublic(
|
|
399
|
+
asset,
|
|
400
|
+
"getTransformSrcSet",
|
|
401
|
+
"getPrivateTransformUrl(asset, opts, signingKey, { expiresInSeconds: 300 }) per width"
|
|
402
|
+
);
|
|
384
403
|
return widths.map((w) => {
|
|
385
404
|
const url = buildTransformUrl(asset, { ...extraOpts, width: w });
|
|
386
405
|
return url ? `${url} ${w}w` : null;
|
|
387
406
|
}).filter((s) => s != null).join(", ");
|
|
388
407
|
}
|
|
389
408
|
|
|
409
|
+
// src/slots.ts
|
|
410
|
+
var DEFAULT_TTL_MS = 6e4;
|
|
411
|
+
var cache = /* @__PURE__ */ new Map();
|
|
412
|
+
var endpoint = "https://api.nitida.gofuture.space";
|
|
413
|
+
var apiKey = null;
|
|
414
|
+
var tenantCode = null;
|
|
415
|
+
function configureSlotResolver(opts) {
|
|
416
|
+
if (opts.endpoint) endpoint = opts.endpoint.replace(/\/+$/, "");
|
|
417
|
+
if (opts.apiKey !== void 0) apiKey = opts.apiKey;
|
|
418
|
+
if (opts.tenantCode !== void 0) tenantCode = opts.tenantCode;
|
|
419
|
+
}
|
|
420
|
+
function invalidateSlotCache(slotKey) {
|
|
421
|
+
if (slotKey === void 0) cache.clear();
|
|
422
|
+
else
|
|
423
|
+
for (const k of cache.keys())
|
|
424
|
+
if (k.endsWith(`:${slotKey}`)) cache.delete(k);
|
|
425
|
+
}
|
|
426
|
+
var baseHeaders = () => {
|
|
427
|
+
const h = {};
|
|
428
|
+
if (apiKey) h.Authorization = `Bearer ${apiKey}`;
|
|
429
|
+
if (tenantCode) h["X-Tenant-Code"] = tenantCode;
|
|
430
|
+
return h;
|
|
431
|
+
};
|
|
432
|
+
async function fetchSlot(slotKey) {
|
|
433
|
+
const r = await fetch(`${endpoint}/slots/${encodeURIComponent(slotKey)}`, {
|
|
434
|
+
headers: baseHeaders()
|
|
435
|
+
});
|
|
436
|
+
if (r.status === 404) return null;
|
|
437
|
+
if (!r.ok) throw new Error(`slot fetch ${r.status}: ${await r.text()}`);
|
|
438
|
+
return await r.json();
|
|
439
|
+
}
|
|
440
|
+
async function fetchSlotsBulk(slotKeys) {
|
|
441
|
+
if (slotKeys.length === 0) return {};
|
|
442
|
+
const r = await fetch(`${endpoint}/slots/resolve`, {
|
|
443
|
+
method: "POST",
|
|
444
|
+
headers: { ...baseHeaders(), "Content-Type": "application/json" },
|
|
445
|
+
body: JSON.stringify({ keys: slotKeys })
|
|
446
|
+
});
|
|
447
|
+
if (!r.ok) throw new Error(`slots resolve ${r.status}: ${await r.text()}`);
|
|
448
|
+
const body = await r.json();
|
|
449
|
+
return body.resolved;
|
|
450
|
+
}
|
|
451
|
+
async function resolveSlot(slotKey, opts = {}) {
|
|
452
|
+
const ttl = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
453
|
+
const cacheKey = `${tenantCode ?? "_"}:${slotKey}`;
|
|
454
|
+
const now = Date.now();
|
|
455
|
+
let dto;
|
|
456
|
+
const hit = cache.get(cacheKey);
|
|
457
|
+
if (hit && now - hit.fetchedAt < ttl) {
|
|
458
|
+
dto = hit.value;
|
|
459
|
+
} else {
|
|
460
|
+
dto = await fetchSlot(slotKey);
|
|
461
|
+
cache.set(cacheKey, { fetchedAt: now, value: dto });
|
|
462
|
+
}
|
|
463
|
+
return materializeResolution(dto, opts.preset);
|
|
464
|
+
}
|
|
465
|
+
async function resolveSlots(slotKeys, opts = {}) {
|
|
466
|
+
if (slotKeys.length === 0) return {};
|
|
467
|
+
const ttl = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
468
|
+
const now = Date.now();
|
|
469
|
+
const missing = [];
|
|
470
|
+
const out = {};
|
|
471
|
+
for (const k of slotKeys) {
|
|
472
|
+
const cacheKey = `${tenantCode ?? "_"}:${k}`;
|
|
473
|
+
const hit = cache.get(cacheKey);
|
|
474
|
+
if (hit && now - hit.fetchedAt < ttl) {
|
|
475
|
+
out[k] = materializeResolution(hit.value, opts.preset);
|
|
476
|
+
} else {
|
|
477
|
+
missing.push(k);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (missing.length > 0) {
|
|
481
|
+
const resolved = await fetchSlotsBulk(missing);
|
|
482
|
+
for (const k of missing) {
|
|
483
|
+
const dto = resolved[k] ?? null;
|
|
484
|
+
cache.set(`${tenantCode ?? "_"}:${k}`, { fetchedAt: now, value: dto });
|
|
485
|
+
out[k] = materializeResolution(dto, opts.preset);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return out;
|
|
489
|
+
}
|
|
490
|
+
function defaultPresetFor(asset) {
|
|
491
|
+
if (!asset) return "lg";
|
|
492
|
+
return asset.kind === "video" ? "video" : "lg";
|
|
493
|
+
}
|
|
494
|
+
function materializeResolution(dto, overridePreset) {
|
|
495
|
+
if (!dto) return { slot: null, preset: overridePreset ?? "lg", url: null };
|
|
496
|
+
const effective = overridePreset ?? dto.preset ?? defaultPresetFor(dto.asset);
|
|
497
|
+
const finalPreset = hasPreset(dto.asset, effective) ? effective : defaultPresetFor(dto.asset);
|
|
498
|
+
return {
|
|
499
|
+
slot: dto,
|
|
500
|
+
preset: finalPreset,
|
|
501
|
+
url: getAssetUrl(dto.asset, finalPreset)
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
390
505
|
// src/index.ts
|
|
391
506
|
var PRESET_SHORT = {
|
|
392
507
|
thumb: "q",
|
|
@@ -513,6 +628,25 @@ function originalExtForMime(mime) {
|
|
|
513
628
|
return (mime ? ORIGINAL_EXT_BY_MIME[mime] : void 0) ?? PRESET_EXT.original;
|
|
514
629
|
}
|
|
515
630
|
function getAssetUrl(asset, preset) {
|
|
631
|
+
assertSha(asset, "getAssetUrl");
|
|
632
|
+
assertPublic(
|
|
633
|
+
asset,
|
|
634
|
+
"getAssetUrl",
|
|
635
|
+
`getPrivateAssetUrl(asset, "${preset}", signingKey, { expiresInSeconds: 300 })`
|
|
636
|
+
);
|
|
637
|
+
const fallback = transformFallbackFor(asset, preset);
|
|
638
|
+
if (fallback) return fallback;
|
|
639
|
+
return buildPublicAssetUrl(asset, preset);
|
|
640
|
+
}
|
|
641
|
+
function transformFallbackFor(asset, preset) {
|
|
642
|
+
if (typeof asset.presets !== "string") return null;
|
|
643
|
+
if (hasPreset({ presets: asset.presets }, preset)) return null;
|
|
644
|
+
const maxDim = PRESET_MAX_DIM[preset];
|
|
645
|
+
if (maxDim == null) return null;
|
|
646
|
+
return `${cdnBaseUrl}/t/format=webp,width=${maxDim}/${asset.sha}.webp`;
|
|
647
|
+
}
|
|
648
|
+
function buildPublicAssetUrl(asset, preset) {
|
|
649
|
+
assertSha(asset, "getPrivateAssetUrl");
|
|
516
650
|
if (preset === "original") {
|
|
517
651
|
const stored = asset.variants?.find((v) => v.preset === "original")?.url;
|
|
518
652
|
if (stored) return stored;
|
|
@@ -526,6 +660,25 @@ function getAssetUrl(asset, preset) {
|
|
|
526
660
|
}
|
|
527
661
|
return `${cdnBaseUrl}/${variantPrefix()}${asset.sha}-${PRESET_SHORT[preset]}.${PRESET_EXT[preset]}`;
|
|
528
662
|
}
|
|
663
|
+
async function getPrivateAssetUrl(asset, preset, signingKey, opts) {
|
|
664
|
+
return signAccessUrl(buildPublicAssetUrl(asset, preset), signingKey, opts);
|
|
665
|
+
}
|
|
666
|
+
async function getPrivateTransformUrl(asset, opts, signingKey, signOpts) {
|
|
667
|
+
const url = buildTransformUrl(asset, opts);
|
|
668
|
+
if (!url) return null;
|
|
669
|
+
const tid = getTenantId();
|
|
670
|
+
if (tid == null) {
|
|
671
|
+
throw new Error(
|
|
672
|
+
"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."
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
const u = new URL(url);
|
|
676
|
+
return signAccessUrl(
|
|
677
|
+
`${u.origin}/${tid.toString(36)}${u.pathname}`,
|
|
678
|
+
signingKey,
|
|
679
|
+
signOpts
|
|
680
|
+
);
|
|
681
|
+
}
|
|
529
682
|
function hasPreset(asset, preset) {
|
|
530
683
|
if (preset === "mp3") return asset.presets.includes("mp3");
|
|
531
684
|
return stripMultiCharTokens(asset.presets).includes(PRESET_SHORT[preset]);
|
|
@@ -535,6 +688,12 @@ function stripMultiCharTokens(presets) {
|
|
|
535
688
|
}
|
|
536
689
|
var IMAGE_PRESETS = ["thumb", "sm", "md", "lg", "xl"];
|
|
537
690
|
function getAssetSrcSet(asset) {
|
|
691
|
+
assertSha(asset, "getAssetSrcSet");
|
|
692
|
+
assertPublic(
|
|
693
|
+
asset,
|
|
694
|
+
"getAssetSrcSet",
|
|
695
|
+
"getPrivateAssetUrl(asset, preset, signingKey, { expiresInSeconds: 300 }) per preset"
|
|
696
|
+
);
|
|
538
697
|
return IMAGE_PRESETS.filter(
|
|
539
698
|
(p) => hasPreset(asset, p) && PRESET_MAX_DIM[p] != null
|
|
540
699
|
).map((p) => `${getAssetUrl(asset, p)} ${PRESET_MAX_DIM[p]}w`).join(", ");
|
|
@@ -561,10 +720,14 @@ function getAssetDimensions(asset) {
|
|
|
561
720
|
PRESET_MAX_DIM,
|
|
562
721
|
PRESET_SHORT,
|
|
563
722
|
TRANSFORM_WIDTHS,
|
|
723
|
+
accessMessage,
|
|
724
|
+
assertPublic,
|
|
725
|
+
assertSha,
|
|
564
726
|
bestTextContrast,
|
|
565
727
|
computeVariantDimensions,
|
|
566
728
|
configureSlotResolver,
|
|
567
729
|
contrastRatio,
|
|
730
|
+
deriveAccessKey,
|
|
568
731
|
extractAssetSha,
|
|
569
732
|
getAmbientGradient,
|
|
570
733
|
getAssetDimensions,
|
|
@@ -575,6 +738,8 @@ function getAssetDimensions(asset) {
|
|
|
575
738
|
getHlsStreamingUrl,
|
|
576
739
|
getPaletteBlurBackground,
|
|
577
740
|
getPaletteCssVars,
|
|
741
|
+
getPrivateAssetUrl,
|
|
742
|
+
getPrivateTransformUrl,
|
|
578
743
|
getSignedTransformUrl,
|
|
579
744
|
getTenantId,
|
|
580
745
|
getTextColorForBackground,
|
|
@@ -592,6 +757,7 @@ function getAssetDimensions(asset) {
|
|
|
592
757
|
serializeTransform,
|
|
593
758
|
setCdnBase,
|
|
594
759
|
setTenantId,
|
|
760
|
+
signAccessUrl,
|
|
595
761
|
signTransformUrl
|
|
596
762
|
});
|
|
597
763
|
//# sourceMappingURL=index.cjs.map
|