@nitida/asset-client 0.24.0 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/index.cjs +30 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +30 -14
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/slots.ts +56 -13
- package/src/transform.ts +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitida/asset-client",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.1",
|
|
4
4
|
"description": "nitida URL builders — construct image, video and HLS URLs for the nitida CDN. No network, no key, no config beyond a tenant id.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"tsup": "^8.5.1",
|
|
45
|
-
"typescript": "
|
|
45
|
+
"typescript": "npm:@typescript/typescript6@6.0.2"
|
|
46
46
|
},
|
|
47
47
|
"keywords": [
|
|
48
48
|
"media",
|
package/src/slots.ts
CHANGED
|
@@ -61,6 +61,31 @@ let endpoint = "https://api.nitida.gofuture.space";
|
|
|
61
61
|
let apiKey: string | null = null;
|
|
62
62
|
let tenantCode: string | null = null;
|
|
63
63
|
|
|
64
|
+
/** Per-call resolver scope. When passed, it fully overrides the process globals. */
|
|
65
|
+
export type SlotResolverConfig = {
|
|
66
|
+
endpoint?: string;
|
|
67
|
+
apiKey?: string | null;
|
|
68
|
+
tenantCode?: string | null;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Effective config for one resolve call: explicit per-client config wins, else
|
|
73
|
+
* the process globals. This is what makes a per-client `slots.resolve()` use ITS
|
|
74
|
+
* OWN key instead of whatever client was constructed last (audit N15).
|
|
75
|
+
*/
|
|
76
|
+
function effectiveConfig(cfg?: SlotResolverConfig): {
|
|
77
|
+
endpoint: string;
|
|
78
|
+
apiKey: string | null;
|
|
79
|
+
tenantCode: string | null;
|
|
80
|
+
} {
|
|
81
|
+
if (!cfg) return { endpoint, apiKey, tenantCode };
|
|
82
|
+
return {
|
|
83
|
+
endpoint: cfg.endpoint ? cfg.endpoint.replace(/\/+$/, "") : endpoint,
|
|
84
|
+
apiKey: cfg.apiKey ?? null,
|
|
85
|
+
tenantCode: cfg.tenantCode ?? null,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
64
89
|
/**
|
|
65
90
|
* Configure the resolver process-wide. Call once at boot from your
|
|
66
91
|
* storefront layout / server entry / worker init.
|
|
@@ -93,16 +118,25 @@ export function invalidateSlotCache(slotKey?: string): void {
|
|
|
93
118
|
// Internal fetch helper
|
|
94
119
|
// ---------------------------------------------------------------------------
|
|
95
120
|
|
|
96
|
-
|
|
121
|
+
type ResolvedConfig = {
|
|
122
|
+
endpoint: string;
|
|
123
|
+
apiKey: string | null;
|
|
124
|
+
tenantCode: string | null;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const baseHeaders = (cfg: ResolvedConfig): Record<string, string> => {
|
|
97
128
|
const h: Record<string, string> = {};
|
|
98
|
-
if (apiKey) h.Authorization = `Bearer ${apiKey}`;
|
|
99
|
-
if (tenantCode) h["X-Tenant-Code"] = tenantCode;
|
|
129
|
+
if (cfg.apiKey) h.Authorization = `Bearer ${cfg.apiKey}`;
|
|
130
|
+
if (cfg.tenantCode) h["X-Tenant-Code"] = cfg.tenantCode;
|
|
100
131
|
return h;
|
|
101
132
|
};
|
|
102
133
|
|
|
103
|
-
async function fetchSlot(
|
|
104
|
-
|
|
105
|
-
|
|
134
|
+
async function fetchSlot(
|
|
135
|
+
slotKey: string,
|
|
136
|
+
cfg: ResolvedConfig,
|
|
137
|
+
): Promise<SlotDTO | null> {
|
|
138
|
+
const r = await fetch(`${cfg.endpoint}/slots/${encodeURIComponent(slotKey)}`, {
|
|
139
|
+
headers: baseHeaders(cfg),
|
|
106
140
|
});
|
|
107
141
|
if (r.status === 404) return null;
|
|
108
142
|
if (!r.ok) throw new Error(`slot fetch ${r.status}: ${await r.text()}`);
|
|
@@ -111,11 +145,12 @@ async function fetchSlot(slotKey: string): Promise<SlotDTO | null> {
|
|
|
111
145
|
|
|
112
146
|
async function fetchSlotsBulk(
|
|
113
147
|
slotKeys: string[],
|
|
148
|
+
cfg: ResolvedConfig,
|
|
114
149
|
): Promise<Record<string, SlotDTO | null>> {
|
|
115
150
|
if (slotKeys.length === 0) return {};
|
|
116
|
-
const r = await fetch(`${endpoint}/slots/resolve`, {
|
|
151
|
+
const r = await fetch(`${cfg.endpoint}/slots/resolve`, {
|
|
117
152
|
method: "POST",
|
|
118
|
-
headers: { ...baseHeaders(), "Content-Type": "application/json" },
|
|
153
|
+
headers: { ...baseHeaders(cfg), "Content-Type": "application/json" },
|
|
119
154
|
body: JSON.stringify({ keys: slotKeys }),
|
|
120
155
|
});
|
|
121
156
|
if (!r.ok) throw new Error(`slots resolve ${r.status}: ${await r.text()}`);
|
|
@@ -132,6 +167,12 @@ export type ResolveSlotOptions = {
|
|
|
132
167
|
preset?: VariantPreset;
|
|
133
168
|
/** TTL for the in-process cache. Default 60s. Set 0 to bypass. */
|
|
134
169
|
ttlMs?: number;
|
|
170
|
+
/**
|
|
171
|
+
* Per-client auth/endpoint scope. Omit to use the process globals
|
|
172
|
+
* (configureSlotResolver). A NitidaClient passes its own here so its
|
|
173
|
+
* resolve() never uses another client's key (audit N15).
|
|
174
|
+
*/
|
|
175
|
+
config?: SlotResolverConfig;
|
|
135
176
|
};
|
|
136
177
|
|
|
137
178
|
/**
|
|
@@ -146,14 +187,15 @@ export async function resolveSlot(
|
|
|
146
187
|
opts: ResolveSlotOptions = {},
|
|
147
188
|
): Promise<SlotResolution> {
|
|
148
189
|
const ttl = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
149
|
-
const
|
|
190
|
+
const cfg = effectiveConfig(opts.config);
|
|
191
|
+
const cacheKey = `${cfg.tenantCode ?? "_"}:${slotKey}`;
|
|
150
192
|
const now = Date.now();
|
|
151
193
|
let dto: SlotDTO | null;
|
|
152
194
|
const hit = cache.get(cacheKey);
|
|
153
195
|
if (hit && now - hit.fetchedAt < ttl) {
|
|
154
196
|
dto = hit.value;
|
|
155
197
|
} else {
|
|
156
|
-
dto = await fetchSlot(slotKey);
|
|
198
|
+
dto = await fetchSlot(slotKey, cfg);
|
|
157
199
|
cache.set(cacheKey, { fetchedAt: now, value: dto });
|
|
158
200
|
}
|
|
159
201
|
return materializeResolution(dto, opts.preset);
|
|
@@ -170,11 +212,12 @@ export async function resolveSlots(
|
|
|
170
212
|
): Promise<Record<string, SlotResolution>> {
|
|
171
213
|
if (slotKeys.length === 0) return {};
|
|
172
214
|
const ttl = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
215
|
+
const cfg = effectiveConfig(opts.config);
|
|
173
216
|
const now = Date.now();
|
|
174
217
|
const missing: string[] = [];
|
|
175
218
|
const out: Record<string, SlotResolution> = {};
|
|
176
219
|
for (const k of slotKeys) {
|
|
177
|
-
const cacheKey = `${tenantCode ?? "_"}:${k}`;
|
|
220
|
+
const cacheKey = `${cfg.tenantCode ?? "_"}:${k}`;
|
|
178
221
|
const hit = cache.get(cacheKey);
|
|
179
222
|
if (hit && now - hit.fetchedAt < ttl) {
|
|
180
223
|
out[k] = materializeResolution(hit.value, opts.preset);
|
|
@@ -183,10 +226,10 @@ export async function resolveSlots(
|
|
|
183
226
|
}
|
|
184
227
|
}
|
|
185
228
|
if (missing.length > 0) {
|
|
186
|
-
const resolved = await fetchSlotsBulk(missing);
|
|
229
|
+
const resolved = await fetchSlotsBulk(missing, cfg);
|
|
187
230
|
for (const k of missing) {
|
|
188
231
|
const dto = resolved[k] ?? null;
|
|
189
|
-
cache.set(`${tenantCode ?? "_"}:${k}`, { fetchedAt: now, value: dto });
|
|
232
|
+
cache.set(`${cfg.tenantCode ?? "_"}:${k}`, { fetchedAt: now, value: dto });
|
|
190
233
|
out[k] = materializeResolution(dto, opts.preset);
|
|
191
234
|
}
|
|
192
235
|
}
|
package/src/transform.ts
CHANGED
|
@@ -238,10 +238,21 @@ export function serializeTransform(
|
|
|
238
238
|
): string {
|
|
239
239
|
const entries: Array<[string, string]> = [];
|
|
240
240
|
const keys = Object.keys(opts).sort();
|
|
241
|
+
// DSL segments are joined with `,` and `/`; a value carrying either (or `=`,
|
|
242
|
+
// `%`, a space) breaks out of its segment and smuggles extra transform ops
|
|
243
|
+
// into the path (audit N17). Every legitimate DSL value is [a-z0-9._-] — reject
|
|
244
|
+
// anything else rather than emit it, keeping the canonical form byte-identical
|
|
245
|
+
// to what the server parses.
|
|
246
|
+
const VALUE_RE = /^[a-z0-9._-]+$/;
|
|
241
247
|
for (const k of keys) {
|
|
242
248
|
const v = (opts as Record<string, unknown>)[k];
|
|
243
249
|
if (v == null) continue;
|
|
244
250
|
const serialized = typeof v === "string" ? v.toLowerCase() : String(v);
|
|
251
|
+
if (!VALUE_RE.test(serialized)) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
`invalid transform value for "${k}": ${JSON.stringify(serialized)} (only [a-z0-9._-] allowed)`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
245
256
|
entries.push([k, serialized]);
|
|
246
257
|
}
|
|
247
258
|
return entries.map(([k, v]) => `${k}=${v}`).join(",");
|