@nitida/asset-client 0.14.2
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 +25 -0
- package/README.md +100 -0
- package/dist/index.cjs +513 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +560 -0
- package/dist/index.d.ts +560 -0
- package/dist/index.js +455 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
- package/src/index.ts +413 -0
- package/src/palette.ts +182 -0
- package/src/slots.ts +217 -0
- package/src/transform.ts +410 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @nitida/asset-client — for coding agents
|
|
2
|
+
|
|
3
|
+
**Read `README.md` in this package, then `src/`.** This package publishes its TypeScript sources
|
|
4
|
+
(`files: ["dist/**", "src/**"]`), so the source is the reference — every public function carries
|
|
5
|
+
TSDoc, and the ones worth copying carry `@example` blocks. Read the source for the canonical,
|
|
6
|
+
version-pinned answer rather than recalling an API from training data.
|
|
7
|
+
|
|
8
|
+
This package **builds URLs**. It makes no network calls and takes no API key. If the task involves
|
|
9
|
+
uploading, that is `@nitida/sdk` — and its `skills/nitida-sdk/SKILL.md` is the deeper guide.
|
|
10
|
+
|
|
11
|
+
## The three that are wrong most often
|
|
12
|
+
|
|
13
|
+
1. **Image widths must be on `TRANSFORM_WIDTHS`.** Any other width is **HTTP 400** at the edge.
|
|
14
|
+
Import the `TransformWidth` type so an off-ladder number fails at compile time instead.
|
|
15
|
+
2. **Video URLs use a base36 tenant segment.** `setTenantId(12)` → `/c/v/…`; a decimal `/12/v/`
|
|
16
|
+
**404s**. Never hand-roll the path — call `getAssetUrl(asset, "video")`.
|
|
17
|
+
3. **`getAssetUrl(asset, "original")` needs the asset's `mime`**, or it emits the `-o.bin`
|
|
18
|
+
sentinel and 404s forever. Even then the stored extension comes from the *uploaded filename*, so
|
|
19
|
+
`image/jpeg` can be `-o.jpg` rather than `-o.jpeg` — verify with a HEAD when it must be right.
|
|
20
|
+
|
|
21
|
+
## Existence checks
|
|
22
|
+
|
|
23
|
+
Use `hasPreset(asset, preset)` against `dto.presets` (the compact code string). Do **not** read the
|
|
24
|
+
`variants` array: measured 2026-08-15 it comes back empty even on admin responses while the row
|
|
25
|
+
holds the variants.
|
package/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# @nitida/asset-client
|
|
2
|
+
|
|
3
|
+
URL builders for assets stored on the aquienpz platform and served from **`https://8ok.uk`**.
|
|
4
|
+
|
|
5
|
+
No network calls, no API key, no side effects — it turns a stored `sha` into a URL. If you also need
|
|
6
|
+
to **upload**, that is `@nitida/sdk` (which re-exports everything here).
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
bun add @nitida/asset-client
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Configure once, at module load
|
|
13
|
+
|
|
14
|
+
The builders read **process-global** config, so pin it where your helpers live — every Server and
|
|
15
|
+
Client Component that imports a builder is then configured.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { setCdnBase, setTenantId } from "@nitida/asset-client";
|
|
19
|
+
|
|
20
|
+
setCdnBase("https://8ok.uk"); // already the default
|
|
21
|
+
setTenantId(12); // REQUIRED for video/variant URLs — see below
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Images — on-the-fly transforms
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { getTransformSrcSet, getTransformUrl, type TransformWidth } from "@nitida/asset-client";
|
|
28
|
+
|
|
29
|
+
getTransformUrl({ sha }, { format: "webp", width: 640 });
|
|
30
|
+
// → https://8ok.uk/t/format=webp,width=640/<sha16>.webp
|
|
31
|
+
|
|
32
|
+
getTransformSrcSet({ sha }, [640, 960, 1280], { format: "webp" });
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
⚠️ **Widths must be on the unsigned ladder** `TRANSFORM_WIDTHS`
|
|
36
|
+
(`160,240,256,320,400,480,600,640,800,960,1080,1200,1280,1440,1600,1920,2560,3840`). Anything else
|
|
37
|
+
is **HTTP 400** at the edge — it is a DoS guard, not a bug. Import the `TransformWidth` type and an
|
|
38
|
+
off-ladder number becomes a compile error instead of a runtime 400.
|
|
39
|
+
|
|
40
|
+
## Video — the stored variant, not a transform
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { getAssetUrl, setTenantId } from "@nitida/asset-client";
|
|
44
|
+
|
|
45
|
+
setTenantId(tenantId);
|
|
46
|
+
getAssetUrl({ sha }, "video"); // → https://8ok.uk/<tenantId base36>/v/<sha16>-v.mp4
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
⚠️ **The tenant segment is base36**, not decimal. Tenant 10 is `/a/`, tenant 12 is `/c/`; `/12/v/`
|
|
50
|
+
**404s**. This is invisible for tenants ≤ 9 and bit a real migration exactly once — so always let
|
|
51
|
+
`getAssetUrl` build the path.
|
|
52
|
+
|
|
53
|
+
⚠️ **Never `getVideoTransformUrl` for a stored video.** That builds a `/t/…` transform URL, which
|
|
54
|
+
**410s** for a stored video sha. It is for on-the-fly re-encodes, a different feature.
|
|
55
|
+
|
|
56
|
+
## The `original` variant
|
|
57
|
+
|
|
58
|
+
Pass the asset's `mime` or you get the `"bin"` sentinel:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
getAssetUrl({ sha }, "original"); // → …-o.bin ❌ 404, always
|
|
62
|
+
getAssetUrl({ sha, mime }, "original"); // → …-o.webp ✓
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
⚠️ And the extension the SERVER stored comes from the **uploaded filename**, not the mime. For JPEG
|
|
66
|
+
those disagree (`image/jpeg` → `jpeg`, but a camera writes `.jpg`) — measured, `-o.jpeg` **404s**
|
|
67
|
+
while `-o.jpg` is 200. If the URL must be right, **verify it with a HEAD** before relying on it.
|
|
68
|
+
|
|
69
|
+
## Which presets exist
|
|
70
|
+
|
|
71
|
+
Read `dto.presets` — the compact code string (`"oq"` = original + thumb, `"pv"` = poster + video) —
|
|
72
|
+
via `hasPreset`, not the `variants` array:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { hasPreset } from "@nitida/asset-client";
|
|
76
|
+
if (hasPreset(asset, "thumb")) { /* … */ }
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The full `variants` array is documented as present on admin responses and, measured 2026-08-15, comes
|
|
80
|
+
back **empty** even with an admin key while the row holds the variants. `presets` is always populated.
|
|
81
|
+
|
|
82
|
+
| preset | code | ext | max dim |
|
|
83
|
+
|---|---|---|---|
|
|
84
|
+
| `thumb` | `q` | webp | 256 (square cover) |
|
|
85
|
+
| `sm` / `md` / `lg` / `xl` | `s` `m` `l` `x` | webp | 640 / 1280 / 1920 / 3840 |
|
|
86
|
+
| `original` | `o` | source ext | — |
|
|
87
|
+
| `poster` / `video` / `aiproxy` | `p` `v` `a` | webp / mp4 / mp4 | — |
|
|
88
|
+
| `mp3` | — | mp3 | — |
|
|
89
|
+
|
|
90
|
+
## Troubleshooting
|
|
91
|
+
|
|
92
|
+
| Symptom | Cause |
|
|
93
|
+
|---|---|
|
|
94
|
+
| **400** on an image URL | width not on `TRANSFORM_WIDTHS` |
|
|
95
|
+
| **410** on a video URL | used `/t/` (transform) for a stored video |
|
|
96
|
+
| **404** on a video URL | decimal tenant prefix instead of base36 |
|
|
97
|
+
| `original` 404s | no `mime` passed, or the stored ext came from the filename (`-o.jpg` vs `-o.jpeg`), or it was never written |
|
|
98
|
+
|
|
99
|
+
Deeper guidance — including uploads, tenant onboarding and the CORS boundary — lives in
|
|
100
|
+
`@nitida/sdk`'s `skills/nitida-sdk/SKILL.md`, which ships inside that package.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
PRESET_EXT: () => PRESET_EXT,
|
|
24
|
+
PRESET_LONG: () => PRESET_LONG,
|
|
25
|
+
PRESET_MAX_DIM: () => PRESET_MAX_DIM,
|
|
26
|
+
PRESET_SHORT: () => PRESET_SHORT,
|
|
27
|
+
TRANSFORM_WIDTHS: () => TRANSFORM_WIDTHS,
|
|
28
|
+
computeVariantDimensions: () => computeVariantDimensions,
|
|
29
|
+
configureSlotResolver: () => configureSlotResolver,
|
|
30
|
+
extractAssetSha: () => extractAssetSha,
|
|
31
|
+
getAmbientGradient: () => getAmbientGradient,
|
|
32
|
+
getAssetDimensions: () => getAssetDimensions,
|
|
33
|
+
getAssetSrcSet: () => getAssetSrcSet,
|
|
34
|
+
getAssetUrl: () => getAssetUrl,
|
|
35
|
+
getCdnBase: () => getCdnBase,
|
|
36
|
+
getHlsStreamingUrl: () => getHlsStreamingUrl,
|
|
37
|
+
getPaletteBlurBackground: () => getPaletteBlurBackground,
|
|
38
|
+
getPaletteCssVars: () => getPaletteCssVars,
|
|
39
|
+
getSignedTransformUrl: () => getSignedTransformUrl,
|
|
40
|
+
getTenantId: () => getTenantId,
|
|
41
|
+
getTextColorForBackground: () => getTextColorForBackground,
|
|
42
|
+
getTransformSrcSet: () => getTransformSrcSet,
|
|
43
|
+
getTransformUrl: () => getTransformUrl,
|
|
44
|
+
getVideoTransformUrl: () => getVideoTransformUrl,
|
|
45
|
+
hasPreset: () => hasPreset,
|
|
46
|
+
invalidateSlotCache: () => invalidateSlotCache,
|
|
47
|
+
iteratePaletteSwatches: () => iteratePaletteSwatches,
|
|
48
|
+
pickAmbientBackground: () => pickAmbientBackground,
|
|
49
|
+
resolveSlot: () => resolveSlot,
|
|
50
|
+
resolveSlots: () => resolveSlots,
|
|
51
|
+
serializeTransform: () => serializeTransform,
|
|
52
|
+
setCdnBase: () => setCdnBase,
|
|
53
|
+
setTenantId: () => setTenantId,
|
|
54
|
+
signTransformUrl: () => signTransformUrl
|
|
55
|
+
});
|
|
56
|
+
module.exports = __toCommonJS(index_exports);
|
|
57
|
+
|
|
58
|
+
// src/palette.ts
|
|
59
|
+
function resolveSwatch(palette, ...keys) {
|
|
60
|
+
if (!palette) return null;
|
|
61
|
+
for (const k of keys) {
|
|
62
|
+
const v = palette[k];
|
|
63
|
+
if (v) return v;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function pickAmbientBackground(palette) {
|
|
68
|
+
const hex = resolveSwatch(palette, "lm", "m", "lv", "d");
|
|
69
|
+
if (!hex) return null;
|
|
70
|
+
return { hex, textColor: textColorForHex(hex) };
|
|
71
|
+
}
|
|
72
|
+
function getAmbientGradient(palette, opts = {}) {
|
|
73
|
+
if (!palette) return void 0;
|
|
74
|
+
const fromHex = palette[opts.from ?? "lm"] ?? palette.m ?? palette.d;
|
|
75
|
+
const toHex = palette[opts.to ?? "m"] ?? palette.dm ?? palette.d;
|
|
76
|
+
if (!fromHex || !toHex) return void 0;
|
|
77
|
+
return `linear-gradient(${opts.angle ?? "135deg"}, ${fromHex}, ${toHex})`;
|
|
78
|
+
}
|
|
79
|
+
function getTextColorForBackground(swatch) {
|
|
80
|
+
if (!swatch) return "#000000";
|
|
81
|
+
const hex = typeof swatch === "string" ? swatch : swatch.hex;
|
|
82
|
+
return textColorForHex(hex);
|
|
83
|
+
}
|
|
84
|
+
function textColorForHex(hex) {
|
|
85
|
+
const h = hex.replace("#", "");
|
|
86
|
+
const r = Number.parseInt(h.slice(0, 2), 16) / 255;
|
|
87
|
+
const g = Number.parseInt(h.slice(2, 4), 16) / 255;
|
|
88
|
+
const b = Number.parseInt(h.slice(4, 6), 16) / 255;
|
|
89
|
+
const lin = (c) => c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
90
|
+
const L = 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
|
|
91
|
+
return L > 0.5 ? "#000000" : "#FFFFFF";
|
|
92
|
+
}
|
|
93
|
+
function getPaletteCssVars(palette) {
|
|
94
|
+
if (!palette) return {};
|
|
95
|
+
const bg = pickAmbientBackground(palette);
|
|
96
|
+
return {
|
|
97
|
+
"--asset-bg": bg?.hex ?? "transparent",
|
|
98
|
+
"--asset-fg": bg ? bg.textColor : "#000000",
|
|
99
|
+
"--asset-dominant": palette.d,
|
|
100
|
+
...palette.v && { "--asset-vibrant": palette.v },
|
|
101
|
+
...palette.m && { "--asset-muted": palette.m },
|
|
102
|
+
...palette.lv && { "--asset-light-vibrant": palette.lv },
|
|
103
|
+
...palette.dv && { "--asset-dark-vibrant": palette.dv },
|
|
104
|
+
...palette.lm && { "--asset-light-muted": palette.lm },
|
|
105
|
+
...palette.dm && { "--asset-dark-muted": palette.dm }
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function iteratePaletteSwatches(palette) {
|
|
109
|
+
if (!palette) return [];
|
|
110
|
+
const order = [
|
|
111
|
+
{ key: "d", label: "dominant" },
|
|
112
|
+
{ key: "v", label: "vibrant" },
|
|
113
|
+
{ key: "lv", label: "lightVibrant" },
|
|
114
|
+
{ key: "dv", label: "darkVibrant" },
|
|
115
|
+
{ key: "m", label: "muted" },
|
|
116
|
+
{ key: "lm", label: "lightMuted" },
|
|
117
|
+
{ key: "dm", label: "darkMuted" }
|
|
118
|
+
];
|
|
119
|
+
return order.map(({ key, label }) => {
|
|
120
|
+
const hex = palette[key];
|
|
121
|
+
return hex ? { key, label, hex } : null;
|
|
122
|
+
}).filter(
|
|
123
|
+
(s) => s != null
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
function getPaletteBlurBackground(palette) {
|
|
127
|
+
if (!palette) return void 0;
|
|
128
|
+
const corners = [
|
|
129
|
+
{ pos: "20% 20%", key: "lv" },
|
|
130
|
+
{ pos: "80% 25%", key: "v" },
|
|
131
|
+
{ pos: "25% 80%", key: "lm" },
|
|
132
|
+
{ pos: "80% 80%", key: "dv" }
|
|
133
|
+
];
|
|
134
|
+
const layers = corners.map(({ pos, key }) => {
|
|
135
|
+
const hex = palette[key];
|
|
136
|
+
if (!hex) return null;
|
|
137
|
+
return `radial-gradient(circle at ${pos}, ${hex} 0%, transparent 55%)`;
|
|
138
|
+
}).filter(Boolean);
|
|
139
|
+
const base = palette.d ?? palette.m ?? "#888";
|
|
140
|
+
return layers.length > 0 ? `${layers.join(", ")}, ${base}` : base;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/slots.ts
|
|
144
|
+
var DEFAULT_TTL_MS = 6e4;
|
|
145
|
+
var cache = /* @__PURE__ */ new Map();
|
|
146
|
+
var endpoint = "https://aquienpz-asset-manager-nlchzy26qa-uc.a.run.app";
|
|
147
|
+
var apiKey = null;
|
|
148
|
+
var tenantCode = null;
|
|
149
|
+
function configureSlotResolver(opts) {
|
|
150
|
+
if (opts.endpoint) endpoint = opts.endpoint.replace(/\/+$/, "");
|
|
151
|
+
if (opts.apiKey !== void 0) apiKey = opts.apiKey;
|
|
152
|
+
if (opts.tenantCode !== void 0) tenantCode = opts.tenantCode;
|
|
153
|
+
}
|
|
154
|
+
function invalidateSlotCache(slotKey) {
|
|
155
|
+
if (slotKey === void 0) cache.clear();
|
|
156
|
+
else
|
|
157
|
+
for (const k of cache.keys())
|
|
158
|
+
if (k.endsWith(`:${slotKey}`)) cache.delete(k);
|
|
159
|
+
}
|
|
160
|
+
var baseHeaders = () => {
|
|
161
|
+
const h = {};
|
|
162
|
+
if (apiKey) h.Authorization = `Bearer ${apiKey}`;
|
|
163
|
+
if (tenantCode) h["X-Tenant-Code"] = tenantCode;
|
|
164
|
+
return h;
|
|
165
|
+
};
|
|
166
|
+
async function fetchSlot(slotKey) {
|
|
167
|
+
const r = await fetch(`${endpoint}/slots/${encodeURIComponent(slotKey)}`, {
|
|
168
|
+
headers: baseHeaders()
|
|
169
|
+
});
|
|
170
|
+
if (r.status === 404) return null;
|
|
171
|
+
if (!r.ok) throw new Error(`slot fetch ${r.status}: ${await r.text()}`);
|
|
172
|
+
return await r.json();
|
|
173
|
+
}
|
|
174
|
+
async function fetchSlotsBulk(slotKeys) {
|
|
175
|
+
if (slotKeys.length === 0) return {};
|
|
176
|
+
const r = await fetch(`${endpoint}/slots/resolve`, {
|
|
177
|
+
method: "POST",
|
|
178
|
+
headers: { ...baseHeaders(), "Content-Type": "application/json" },
|
|
179
|
+
body: JSON.stringify({ keys: slotKeys })
|
|
180
|
+
});
|
|
181
|
+
if (!r.ok) throw new Error(`slots resolve ${r.status}: ${await r.text()}`);
|
|
182
|
+
const body = await r.json();
|
|
183
|
+
return body.resolved;
|
|
184
|
+
}
|
|
185
|
+
async function resolveSlot(slotKey, opts = {}) {
|
|
186
|
+
const ttl = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
187
|
+
const cacheKey = `${tenantCode ?? "_"}:${slotKey}`;
|
|
188
|
+
const now = Date.now();
|
|
189
|
+
let dto;
|
|
190
|
+
const hit = cache.get(cacheKey);
|
|
191
|
+
if (hit && now - hit.fetchedAt < ttl) {
|
|
192
|
+
dto = hit.value;
|
|
193
|
+
} else {
|
|
194
|
+
dto = await fetchSlot(slotKey);
|
|
195
|
+
cache.set(cacheKey, { fetchedAt: now, value: dto });
|
|
196
|
+
}
|
|
197
|
+
return materializeResolution(dto, opts.preset);
|
|
198
|
+
}
|
|
199
|
+
async function resolveSlots(slotKeys, opts = {}) {
|
|
200
|
+
if (slotKeys.length === 0) return {};
|
|
201
|
+
const ttl = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
202
|
+
const now = Date.now();
|
|
203
|
+
const missing = [];
|
|
204
|
+
const out = {};
|
|
205
|
+
for (const k of slotKeys) {
|
|
206
|
+
const cacheKey = `${tenantCode ?? "_"}:${k}`;
|
|
207
|
+
const hit = cache.get(cacheKey);
|
|
208
|
+
if (hit && now - hit.fetchedAt < ttl) {
|
|
209
|
+
out[k] = materializeResolution(hit.value, opts.preset);
|
|
210
|
+
} else {
|
|
211
|
+
missing.push(k);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (missing.length > 0) {
|
|
215
|
+
const resolved = await fetchSlotsBulk(missing);
|
|
216
|
+
for (const k of missing) {
|
|
217
|
+
const dto = resolved[k] ?? null;
|
|
218
|
+
cache.set(`${tenantCode ?? "_"}:${k}`, { fetchedAt: now, value: dto });
|
|
219
|
+
out[k] = materializeResolution(dto, opts.preset);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
function defaultPresetFor(asset) {
|
|
225
|
+
if (!asset) return "lg";
|
|
226
|
+
return asset.kind === "video" ? "video" : "lg";
|
|
227
|
+
}
|
|
228
|
+
function materializeResolution(dto, overridePreset) {
|
|
229
|
+
if (!dto) return { slot: null, preset: overridePreset ?? "lg", url: null };
|
|
230
|
+
const effective = overridePreset ?? dto.preset ?? defaultPresetFor(dto.asset);
|
|
231
|
+
const finalPreset = hasPreset(dto.asset, effective) ? effective : defaultPresetFor(dto.asset);
|
|
232
|
+
return {
|
|
233
|
+
slot: dto,
|
|
234
|
+
preset: finalPreset,
|
|
235
|
+
url: getAssetUrl(dto.asset, finalPreset)
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/transform.ts
|
|
240
|
+
var TRANSFORM_WIDTHS = [
|
|
241
|
+
96,
|
|
242
|
+
128,
|
|
243
|
+
160,
|
|
244
|
+
240,
|
|
245
|
+
256,
|
|
246
|
+
320,
|
|
247
|
+
400,
|
|
248
|
+
480,
|
|
249
|
+
600,
|
|
250
|
+
640,
|
|
251
|
+
800,
|
|
252
|
+
960,
|
|
253
|
+
1080,
|
|
254
|
+
1200,
|
|
255
|
+
1280,
|
|
256
|
+
1440,
|
|
257
|
+
1600,
|
|
258
|
+
1920,
|
|
259
|
+
2560,
|
|
260
|
+
3840
|
|
261
|
+
];
|
|
262
|
+
function extractAssetSha(url) {
|
|
263
|
+
if (!url) return null;
|
|
264
|
+
const m = url.match(/\/([0-9a-f]{16})(?:[-.]|$)/);
|
|
265
|
+
return m ? m[1] : null;
|
|
266
|
+
}
|
|
267
|
+
function serializeTransform(opts) {
|
|
268
|
+
const entries = [];
|
|
269
|
+
const keys = Object.keys(opts).sort();
|
|
270
|
+
for (const k of keys) {
|
|
271
|
+
const v = opts[k];
|
|
272
|
+
if (v == null) continue;
|
|
273
|
+
const serialized = typeof v === "string" ? v.toLowerCase() : String(v);
|
|
274
|
+
entries.push([k, serialized]);
|
|
275
|
+
}
|
|
276
|
+
return entries.map(([k, v]) => `${k}=${v}`).join(",");
|
|
277
|
+
}
|
|
278
|
+
function extForOptions(opts) {
|
|
279
|
+
if (opts.effect === "removebg") return "png";
|
|
280
|
+
if (opts.effect === "genfill") {
|
|
281
|
+
switch (opts.format) {
|
|
282
|
+
case "png":
|
|
283
|
+
return "png";
|
|
284
|
+
case "avif":
|
|
285
|
+
return "avif";
|
|
286
|
+
case "jpeg":
|
|
287
|
+
return "jpg";
|
|
288
|
+
default:
|
|
289
|
+
return "webp";
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
switch (opts.format) {
|
|
293
|
+
case "avif":
|
|
294
|
+
return "avif";
|
|
295
|
+
case "jpeg":
|
|
296
|
+
return "jpg";
|
|
297
|
+
case "png":
|
|
298
|
+
return "png";
|
|
299
|
+
case "mp4":
|
|
300
|
+
return "mp4";
|
|
301
|
+
case "webm":
|
|
302
|
+
return "webm";
|
|
303
|
+
case "hls":
|
|
304
|
+
return "m3u8";
|
|
305
|
+
default:
|
|
306
|
+
return "webp";
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function getVideoTransformUrl(asset, opts) {
|
|
310
|
+
const dsl = serializeTransform(opts);
|
|
311
|
+
if (!dsl) return null;
|
|
312
|
+
const ext = opts.format === "webm" ? "webm" : "mp4";
|
|
313
|
+
return `${getCdnBase()}/t/${dsl}/${asset.sha}.${ext}`;
|
|
314
|
+
}
|
|
315
|
+
function getHlsStreamingUrl(asset, opts = {}) {
|
|
316
|
+
const merged = { ...opts, format: "hls" };
|
|
317
|
+
const dsl = serializeTransform(merged);
|
|
318
|
+
return `${getCdnBase()}/t/${dsl}/${asset.sha}.m3u8`;
|
|
319
|
+
}
|
|
320
|
+
function buildTransformUrl(asset, opts) {
|
|
321
|
+
const dsl = serializeTransform(opts);
|
|
322
|
+
if (!dsl) return null;
|
|
323
|
+
const ext = extForOptions(opts);
|
|
324
|
+
return `${getCdnBase()}/t/${dsl}/${asset.sha}.${ext}`;
|
|
325
|
+
}
|
|
326
|
+
function getTransformUrl(asset, opts) {
|
|
327
|
+
return buildTransformUrl(asset, opts);
|
|
328
|
+
}
|
|
329
|
+
function getSignedTransformUrl(asset, opts, signingKey) {
|
|
330
|
+
const url = buildTransformUrl(asset, opts);
|
|
331
|
+
if (!url) return null;
|
|
332
|
+
return signTransformUrl(url, signingKey);
|
|
333
|
+
}
|
|
334
|
+
async function signTransformUrl(unsignedUrl, signingKey) {
|
|
335
|
+
const u = new URL(unsignedUrl);
|
|
336
|
+
const parts = u.pathname.split("/").filter(Boolean);
|
|
337
|
+
if (parts[0] !== "t" || parts.length < 3) {
|
|
338
|
+
throw new Error(`signTransformUrl: unexpected URL shape ${unsignedUrl}`);
|
|
339
|
+
}
|
|
340
|
+
const filename = parts[parts.length - 1];
|
|
341
|
+
const dsl = parts.slice(1, -1).join("/");
|
|
342
|
+
const message = `${dsl}/${filename}`;
|
|
343
|
+
const sig = await hmacSha256Hex(signingKey, message);
|
|
344
|
+
u.searchParams.set("sig", sig);
|
|
345
|
+
return u.toString();
|
|
346
|
+
}
|
|
347
|
+
async function hmacSha256Hex(key, message) {
|
|
348
|
+
const enc = new TextEncoder();
|
|
349
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
350
|
+
"raw",
|
|
351
|
+
enc.encode(key),
|
|
352
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
353
|
+
false,
|
|
354
|
+
["sign"]
|
|
355
|
+
);
|
|
356
|
+
const buf = await crypto.subtle.sign("HMAC", cryptoKey, enc.encode(message));
|
|
357
|
+
return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
358
|
+
}
|
|
359
|
+
function getTransformSrcSet(asset, widths, extraOpts = {}) {
|
|
360
|
+
return widths.map((w) => {
|
|
361
|
+
const url = buildTransformUrl(asset, { ...extraOpts, width: w });
|
|
362
|
+
return url ? `${url} ${w}w` : null;
|
|
363
|
+
}).filter((s) => s != null).join(", ");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// src/index.ts
|
|
367
|
+
var PRESET_SHORT = {
|
|
368
|
+
thumb: "q",
|
|
369
|
+
sm: "s",
|
|
370
|
+
md: "m",
|
|
371
|
+
lg: "l",
|
|
372
|
+
xl: "x",
|
|
373
|
+
original: "o",
|
|
374
|
+
poster: "p",
|
|
375
|
+
video: "v",
|
|
376
|
+
aiproxy: "a",
|
|
377
|
+
// 3 chars, NOT a 1-char alias: the asset-manager has no short-form for
|
|
378
|
+
// audio so its `shortPreset("mp3")` falls through to the literal token,
|
|
379
|
+
// and the deployed server already writes the `-mp3.mp3` variant + emits
|
|
380
|
+
// the bare `mp3` token in the wire `presets` string. Must stay in lockstep.
|
|
381
|
+
mp3: "mp3"
|
|
382
|
+
};
|
|
383
|
+
var PRESET_LONG = Object.fromEntries(
|
|
384
|
+
Object.entries(PRESET_SHORT).map(([k, v]) => [v, k])
|
|
385
|
+
);
|
|
386
|
+
var PRESET_EXT = {
|
|
387
|
+
thumb: "webp",
|
|
388
|
+
sm: "webp",
|
|
389
|
+
md: "webp",
|
|
390
|
+
lg: "webp",
|
|
391
|
+
xl: "webp",
|
|
392
|
+
original: "bin",
|
|
393
|
+
// overridden per-asset via mime when needed
|
|
394
|
+
poster: "webp",
|
|
395
|
+
video: "mp4",
|
|
396
|
+
aiproxy: "mp4",
|
|
397
|
+
mp3: "mp3"
|
|
398
|
+
};
|
|
399
|
+
var PRESET_MAX_DIM = {
|
|
400
|
+
thumb: 256,
|
|
401
|
+
sm: 640,
|
|
402
|
+
md: 1280,
|
|
403
|
+
lg: 1920,
|
|
404
|
+
xl: 3840,
|
|
405
|
+
original: null,
|
|
406
|
+
poster: null,
|
|
407
|
+
video: null,
|
|
408
|
+
aiproxy: null,
|
|
409
|
+
// audio has no pixel dimensions; `null` keeps mp3 out of the
|
|
410
|
+
// dimension-based `getAssetSrcSet` / `computeVariantDimensions` logic.
|
|
411
|
+
mp3: null
|
|
412
|
+
};
|
|
413
|
+
var cdnBaseUrl = "https://8ok.uk";
|
|
414
|
+
function setCdnBase(url) {
|
|
415
|
+
cdnBaseUrl = url.replace(/\/$/, "");
|
|
416
|
+
}
|
|
417
|
+
function getCdnBase() {
|
|
418
|
+
return cdnBaseUrl;
|
|
419
|
+
}
|
|
420
|
+
var tenantId = null;
|
|
421
|
+
function setTenantId(id) {
|
|
422
|
+
tenantId = typeof id === "number" && Number.isFinite(id) && id > 0 ? id : null;
|
|
423
|
+
}
|
|
424
|
+
function getTenantId() {
|
|
425
|
+
return tenantId;
|
|
426
|
+
}
|
|
427
|
+
function variantPrefix() {
|
|
428
|
+
return tenantId != null ? `${tenantId.toString(36)}/v/` : "";
|
|
429
|
+
}
|
|
430
|
+
var ORIGINAL_EXT_BY_MIME = {
|
|
431
|
+
"image/png": "png",
|
|
432
|
+
"image/jpeg": "jpeg",
|
|
433
|
+
"image/webp": "webp",
|
|
434
|
+
"image/gif": "gif",
|
|
435
|
+
"image/avif": "avif",
|
|
436
|
+
"image/svg+xml": "svg",
|
|
437
|
+
"image/heic": "heic",
|
|
438
|
+
"image/heif": "heif",
|
|
439
|
+
"image/bmp": "bmp",
|
|
440
|
+
"image/tiff": "tiff",
|
|
441
|
+
"application/pdf": "pdf",
|
|
442
|
+
"video/mp4": "mp4",
|
|
443
|
+
"video/webm": "webm",
|
|
444
|
+
"video/quicktime": "mov"
|
|
445
|
+
};
|
|
446
|
+
function originalExtForMime(mime) {
|
|
447
|
+
return (mime ? ORIGINAL_EXT_BY_MIME[mime] : void 0) ?? PRESET_EXT.original;
|
|
448
|
+
}
|
|
449
|
+
function getAssetUrl(asset, preset) {
|
|
450
|
+
const ext = preset === "original" ? originalExtForMime(asset.mime) : PRESET_EXT[preset];
|
|
451
|
+
return `${cdnBaseUrl}/${variantPrefix()}${asset.sha}-${PRESET_SHORT[preset]}.${ext}`;
|
|
452
|
+
}
|
|
453
|
+
function hasPreset(asset, preset) {
|
|
454
|
+
if (preset === "mp3") return asset.presets.includes("mp3");
|
|
455
|
+
return asset.presets.replace(/mp3/g, "").includes(PRESET_SHORT[preset]);
|
|
456
|
+
}
|
|
457
|
+
var IMAGE_PRESETS = ["thumb", "sm", "md", "lg", "xl"];
|
|
458
|
+
function getAssetSrcSet(asset) {
|
|
459
|
+
return IMAGE_PRESETS.filter(
|
|
460
|
+
(p) => hasPreset(asset, p) && PRESET_MAX_DIM[p] != null
|
|
461
|
+
).map((p) => `${getAssetUrl(asset, p)} ${PRESET_MAX_DIM[p]}w`).join(", ");
|
|
462
|
+
}
|
|
463
|
+
function computeVariantDimensions(asset, preset) {
|
|
464
|
+
const cap = PRESET_MAX_DIM[preset];
|
|
465
|
+
if (cap == null) return null;
|
|
466
|
+
if (preset === "thumb") return { width: cap, height: cap };
|
|
467
|
+
if (!asset.w || !asset.h) return null;
|
|
468
|
+
const scale = Math.min(cap / asset.w, cap / asset.h, 1);
|
|
469
|
+
return {
|
|
470
|
+
width: Math.round(asset.w * scale),
|
|
471
|
+
height: Math.round(asset.h * scale)
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
function getAssetDimensions(asset) {
|
|
475
|
+
if (asset.w && asset.h) return { width: asset.w, height: asset.h };
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
479
|
+
0 && (module.exports = {
|
|
480
|
+
PRESET_EXT,
|
|
481
|
+
PRESET_LONG,
|
|
482
|
+
PRESET_MAX_DIM,
|
|
483
|
+
PRESET_SHORT,
|
|
484
|
+
TRANSFORM_WIDTHS,
|
|
485
|
+
computeVariantDimensions,
|
|
486
|
+
configureSlotResolver,
|
|
487
|
+
extractAssetSha,
|
|
488
|
+
getAmbientGradient,
|
|
489
|
+
getAssetDimensions,
|
|
490
|
+
getAssetSrcSet,
|
|
491
|
+
getAssetUrl,
|
|
492
|
+
getCdnBase,
|
|
493
|
+
getHlsStreamingUrl,
|
|
494
|
+
getPaletteBlurBackground,
|
|
495
|
+
getPaletteCssVars,
|
|
496
|
+
getSignedTransformUrl,
|
|
497
|
+
getTenantId,
|
|
498
|
+
getTextColorForBackground,
|
|
499
|
+
getTransformSrcSet,
|
|
500
|
+
getTransformUrl,
|
|
501
|
+
getVideoTransformUrl,
|
|
502
|
+
hasPreset,
|
|
503
|
+
invalidateSlotCache,
|
|
504
|
+
iteratePaletteSwatches,
|
|
505
|
+
pickAmbientBackground,
|
|
506
|
+
resolveSlot,
|
|
507
|
+
resolveSlots,
|
|
508
|
+
serializeTransform,
|
|
509
|
+
setCdnBase,
|
|
510
|
+
setTenantId,
|
|
511
|
+
signTransformUrl
|
|
512
|
+
});
|
|
513
|
+
//# sourceMappingURL=index.cjs.map
|