@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/dist/index.d.cts CHANGED
@@ -689,6 +689,12 @@ type SlotResolution = {
689
689
  /** The CDN URL the consumer should use. */
690
690
  url: string | null;
691
691
  };
692
+ /** Per-call resolver scope. When passed, it fully overrides the process globals. */
693
+ type SlotResolverConfig = {
694
+ endpoint?: string;
695
+ apiKey?: string | null;
696
+ tenantCode?: string | null;
697
+ };
692
698
  /**
693
699
  * Configure the resolver process-wide. Call once at boot from your
694
700
  * storefront layout / server entry / worker init.
@@ -711,6 +717,12 @@ type ResolveSlotOptions = {
711
717
  preset?: VariantPreset;
712
718
  /** TTL for the in-process cache. Default 60s. Set 0 to bypass. */
713
719
  ttlMs?: number;
720
+ /**
721
+ * Per-client auth/endpoint scope. Omit to use the process globals
722
+ * (configureSlotResolver). A NitidaClient passes its own here so its
723
+ * resolve() never uses another client's key (audit N15).
724
+ */
725
+ config?: SlotResolverConfig;
714
726
  };
715
727
  /**
716
728
  * Resolve a single slot to a CDN URL. Returns `{slot: null, url: null}`
package/dist/index.d.ts CHANGED
@@ -689,6 +689,12 @@ type SlotResolution = {
689
689
  /** The CDN URL the consumer should use. */
690
690
  url: string | null;
691
691
  };
692
+ /** Per-call resolver scope. When passed, it fully overrides the process globals. */
693
+ type SlotResolverConfig = {
694
+ endpoint?: string;
695
+ apiKey?: string | null;
696
+ tenantCode?: string | null;
697
+ };
692
698
  /**
693
699
  * Configure the resolver process-wide. Call once at boot from your
694
700
  * storefront layout / server entry / worker init.
@@ -711,6 +717,12 @@ type ResolveSlotOptions = {
711
717
  preset?: VariantPreset;
712
718
  /** TTL for the in-process cache. Default 60s. Set 0 to bypass. */
713
719
  ttlMs?: number;
720
+ /**
721
+ * Per-client auth/endpoint scope. Omit to use the process globals
722
+ * (configureSlotResolver). A NitidaClient passes its own here so its
723
+ * resolve() never uses another client's key (audit N15).
724
+ */
725
+ config?: SlotResolverConfig;
714
726
  };
715
727
  /**
716
728
  * Resolve a single slot to a CDN URL. Returns `{slot: null, url: null}`
package/dist/index.js CHANGED
@@ -219,10 +219,16 @@ function extractAssetSha(url) {
219
219
  function serializeTransform(opts) {
220
220
  const entries = [];
221
221
  const keys = Object.keys(opts).sort();
222
+ const VALUE_RE = /^[a-z0-9._-]+$/;
222
223
  for (const k of keys) {
223
224
  const v = opts[k];
224
225
  if (v == null) continue;
225
226
  const serialized = typeof v === "string" ? v.toLowerCase() : String(v);
227
+ if (!VALUE_RE.test(serialized)) {
228
+ throw new Error(
229
+ `invalid transform value for "${k}": ${JSON.stringify(serialized)} (only [a-z0-9._-] allowed)`
230
+ );
231
+ }
226
232
  entries.push([k, serialized]);
227
233
  }
228
234
  return entries.map(([k, v]) => `${k}=${v}`).join(",");
@@ -418,6 +424,14 @@ var cache = /* @__PURE__ */ new Map();
418
424
  var endpoint = "https://api.nitida.gofuture.space";
419
425
  var apiKey = null;
420
426
  var tenantCode = null;
427
+ function effectiveConfig(cfg) {
428
+ if (!cfg) return { endpoint, apiKey, tenantCode };
429
+ return {
430
+ endpoint: cfg.endpoint ? cfg.endpoint.replace(/\/+$/, "") : endpoint,
431
+ apiKey: cfg.apiKey ?? null,
432
+ tenantCode: cfg.tenantCode ?? null
433
+ };
434
+ }
421
435
  function configureSlotResolver(opts) {
422
436
  if (opts.endpoint) endpoint = opts.endpoint.replace(/\/+$/, "");
423
437
  if (opts.apiKey !== void 0) apiKey = opts.apiKey;
@@ -429,25 +443,25 @@ function invalidateSlotCache(slotKey) {
429
443
  for (const k of cache.keys())
430
444
  if (k.endsWith(`:${slotKey}`)) cache.delete(k);
431
445
  }
432
- var baseHeaders = () => {
446
+ var baseHeaders = (cfg) => {
433
447
  const h = {};
434
- if (apiKey) h.Authorization = `Bearer ${apiKey}`;
435
- if (tenantCode) h["X-Tenant-Code"] = tenantCode;
448
+ if (cfg.apiKey) h.Authorization = `Bearer ${cfg.apiKey}`;
449
+ if (cfg.tenantCode) h["X-Tenant-Code"] = cfg.tenantCode;
436
450
  return h;
437
451
  };
438
- async function fetchSlot(slotKey) {
439
- const r = await fetch(`${endpoint}/slots/${encodeURIComponent(slotKey)}`, {
440
- headers: baseHeaders()
452
+ async function fetchSlot(slotKey, cfg) {
453
+ const r = await fetch(`${cfg.endpoint}/slots/${encodeURIComponent(slotKey)}`, {
454
+ headers: baseHeaders(cfg)
441
455
  });
442
456
  if (r.status === 404) return null;
443
457
  if (!r.ok) throw new Error(`slot fetch ${r.status}: ${await r.text()}`);
444
458
  return await r.json();
445
459
  }
446
- async function fetchSlotsBulk(slotKeys) {
460
+ async function fetchSlotsBulk(slotKeys, cfg) {
447
461
  if (slotKeys.length === 0) return {};
448
- const r = await fetch(`${endpoint}/slots/resolve`, {
462
+ const r = await fetch(`${cfg.endpoint}/slots/resolve`, {
449
463
  method: "POST",
450
- headers: { ...baseHeaders(), "Content-Type": "application/json" },
464
+ headers: { ...baseHeaders(cfg), "Content-Type": "application/json" },
451
465
  body: JSON.stringify({ keys: slotKeys })
452
466
  });
453
467
  if (!r.ok) throw new Error(`slots resolve ${r.status}: ${await r.text()}`);
@@ -456,14 +470,15 @@ async function fetchSlotsBulk(slotKeys) {
456
470
  }
457
471
  async function resolveSlot(slotKey, opts = {}) {
458
472
  const ttl = opts.ttlMs ?? DEFAULT_TTL_MS;
459
- const cacheKey = `${tenantCode ?? "_"}:${slotKey}`;
473
+ const cfg = effectiveConfig(opts.config);
474
+ const cacheKey = `${cfg.tenantCode ?? "_"}:${slotKey}`;
460
475
  const now = Date.now();
461
476
  let dto;
462
477
  const hit = cache.get(cacheKey);
463
478
  if (hit && now - hit.fetchedAt < ttl) {
464
479
  dto = hit.value;
465
480
  } else {
466
- dto = await fetchSlot(slotKey);
481
+ dto = await fetchSlot(slotKey, cfg);
467
482
  cache.set(cacheKey, { fetchedAt: now, value: dto });
468
483
  }
469
484
  return materializeResolution(dto, opts.preset);
@@ -471,11 +486,12 @@ async function resolveSlot(slotKey, opts = {}) {
471
486
  async function resolveSlots(slotKeys, opts = {}) {
472
487
  if (slotKeys.length === 0) return {};
473
488
  const ttl = opts.ttlMs ?? DEFAULT_TTL_MS;
489
+ const cfg = effectiveConfig(opts.config);
474
490
  const now = Date.now();
475
491
  const missing = [];
476
492
  const out = {};
477
493
  for (const k of slotKeys) {
478
- const cacheKey = `${tenantCode ?? "_"}:${k}`;
494
+ const cacheKey = `${cfg.tenantCode ?? "_"}:${k}`;
479
495
  const hit = cache.get(cacheKey);
480
496
  if (hit && now - hit.fetchedAt < ttl) {
481
497
  out[k] = materializeResolution(hit.value, opts.preset);
@@ -484,10 +500,10 @@ async function resolveSlots(slotKeys, opts = {}) {
484
500
  }
485
501
  }
486
502
  if (missing.length > 0) {
487
- const resolved = await fetchSlotsBulk(missing);
503
+ const resolved = await fetchSlotsBulk(missing, cfg);
488
504
  for (const k of missing) {
489
505
  const dto = resolved[k] ?? null;
490
- cache.set(`${tenantCode ?? "_"}:${k}`, { fetchedAt: now, value: dto });
506
+ cache.set(`${cfg.tenantCode ?? "_"}:${k}`, { fetchedAt: now, value: dto });
491
507
  out[k] = materializeResolution(dto, opts.preset);
492
508
  }
493
509
  }