@tokenoftrust/storefront-runner 6.2.1 → 6.3.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.
@@ -5,7 +5,7 @@
5
5
  * than a grey box, we synthesize a tasteful, aspect-correct SVG keyed by a
6
6
  * stable seed (image id or product handle) so each product gets a consistent,
7
7
  * on-brand gradient + monogram. The <Img> component swaps these out for real
8
- * rehosted URLs when CatalogImage.rehosted === true.
8
+ * rehosted URLs or content-addressed pool URLs.
9
9
  *
10
10
  * Palette is pulled from the theme's CSS vars at render time where possible, but
11
11
  * the SVG is self-contained (inlines colors) so it works as a data URI / <img src>.
@@ -91,6 +91,9 @@ export function placeholderDataUri(opts: PlaceholderOpts): string {
91
91
  export function isUsableImageUrl(url: string | undefined, rehosted?: boolean): boolean {
92
92
  if (!url) return false;
93
93
  if (rehosted) return true;
94
+ // Capture replaces image locators without changing catalog metadata. Pool
95
+ // originals render directly on both CDNs, including images with rehosted:false.
96
+ if (/^\/_a\/[0-9a-f]{64}(?:\.(?:avif|gif|jpe?g|png|svg|webp))?$/.test(url)) return true;
94
97
  // The synthetic apparel fixtures use a fake shop id (…/s/files/1/0001/…) whose
95
98
  // URLs 404 — draw placeholders for those. REAL store CDN URLs (e.g. a live POC
96
99
  // migration) are usable as-is.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "6.2.1",
3
+ "version": "6.3.1",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
6
  "packageManager": "pnpm@11.9.0",
@@ -19,7 +19,7 @@
19
19
 
20
20
  export const TOT_CONFIG_KEYS = [
21
21
  "$schema", "tenant", "scope", "mappings", "hostPlatform", "sample", "seed",
22
- "displayName", "siteType", "compliance", "capabilities", "features", "embeds", "seo", "currency", "timezone", "shipping",
22
+ "displayName", "siteType", "compliance", "capabilities", "features", "embeds", "seo", "currency", "timezone", "shipping", "catalog",
23
23
  ];
24
24
  // Keys other tooling has written that the platform does NOT read, with the key it does.
25
25
  const RENAMED_CONFIG_KEYS = { appDomain: "scope", tenantId: "tenant" };
@@ -232,6 +232,37 @@ export function seoDeclarationProblems(seo) {
232
232
  return problems;
233
233
  }
234
234
 
235
+ /** Every `catalog` key a tenant may declare. `serving` stays a platform field. */
236
+ export const CATALOG_KEYS = Object.freeze(["cdnSegment"]);
237
+
238
+ /** One DNS label. Anything else is not a catalog object-key segment. */
239
+ const CATALOG_SEGMENT = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
240
+
241
+ /**
242
+ * What is wrong with a declared `catalog` value, one sentence per problem; `[]` when it is valid.
243
+ * A tenant cannot set `catalog.serving` here: that field belongs to the platform record.
244
+ * @param {unknown} catalog
245
+ * @returns {string[]}
246
+ */
247
+ export function catalogDeclarationProblems(catalog) {
248
+ if (!isObject(catalog)) return ["`catalog` must be an object"];
249
+ const source = /** @type {Record<string, unknown>} */ (catalog);
250
+ const problems = [];
251
+ for (const key of Object.keys(source)) {
252
+ if (!CATALOG_KEYS.includes(key)) problems.push(`\`catalog.${key}\` is not a catalog key (${CATALOG_KEYS.join(", ")})`);
253
+ }
254
+ if (source.cdnSegment !== undefined && (typeof source.cdnSegment !== "string" || !CATALOG_SEGMENT.test(source.cdnSegment))) {
255
+ problems.push("`catalog.cdnSegment` must be one lowercase DNS label");
256
+ }
257
+ return problems;
258
+ }
259
+
260
+ /** The owner-facing sentence for an invalid `catalog` declaration. */
261
+ function catalogProblemMessage(problems) {
262
+ return `\`catalog\` is not a valid catalog declaration (${problems.join("; ")}). ` +
263
+ "Owned catalog images stay refused until it is fixed.";
264
+ }
265
+
235
266
  /** Every `shipping` key (mirrors packages/public-runtime/src/declared-tenant-config.mjs). */
236
267
  export const SHIPPING_KEYS = Object.freeze(["tiers"]);
237
268
 
@@ -690,6 +721,8 @@ export function declarationFindings(current, previous) {
690
721
  }
691
722
  const shipping = current.shipping === undefined ? [] : shippingDeclarationProblems(current.shipping);
692
723
  if (shipping.length > 0) out.push(`CONFIG: ${shippingProblemMessage(shipping)}`);
724
+ const catalog = current.catalog === undefined ? [] : catalogDeclarationProblems(current.catalog);
725
+ if (catalog.length > 0) out.push(`CONFIG: ${catalogProblemMessage(catalog)}`);
693
726
  }
694
727
  for (const { key, notice } of droppedMandatedNotices(previous, current)) {
695
728
  out.push(
@@ -790,6 +823,13 @@ export function validateDeclaredConfig(config, file = ".tot/config.json") {
790
823
  'declare e.g. `"shipping": { "tiers": [ { "rate": 5.99 }, { "atOrAbove": 100, "rate": 0, "label": "Free shipping" } ] }`, or remove `shipping` to charge none'));
791
824
  }
792
825
  }
826
+ if (config.catalog !== undefined) {
827
+ const problems = catalogDeclarationProblems(config.catalog);
828
+ if (problems.length > 0) {
829
+ out.push(mk(ERROR, "config-catalog", `${file} catalog`, catalogProblemMessage(problems),
830
+ 'declare `"catalog": { "cdnSegment": "shop" }` as one lowercase DNS label, or remove `catalog`'));
831
+ }
832
+ }
793
833
  return out;
794
834
  }
795
835
 
@@ -29,6 +29,7 @@ export const DECLARED_TENANT_CONFIG_FIELDS = Object.freeze([
29
29
  "currency",
30
30
  "timezone",
31
31
  "shipping",
32
+ "catalog",
32
33
  ]);
33
34
 
34
35
  /**
@@ -279,6 +280,32 @@ export function seoDeclarationProblems(seo) {
279
280
  return problems;
280
281
  }
281
282
 
283
+ /** Every `catalog` key a tenant may declare. `serving` stays a platform field. */
284
+ export const CATALOG_KEYS = Object.freeze(["cdnSegment"]);
285
+
286
+ /** One DNS label. Anything else is not a catalog object-key segment. */
287
+ const CATALOG_SEGMENT = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
288
+
289
+ /**
290
+ * What is wrong with a declared `catalog` value, one sentence per problem; `[]` when it is valid.
291
+ * A tenant cannot set `catalog.serving` here: that field belongs to the platform record.
292
+ *
293
+ * @param {unknown} catalog
294
+ * @returns {string[]}
295
+ */
296
+ export function catalogDeclarationProblems(catalog) {
297
+ if (!isPlainObject(catalog)) return ["`catalog` must be an object"];
298
+ const source = /** @type {Record<string, unknown>} */ (catalog);
299
+ const problems = [];
300
+ for (const key of Object.keys(source)) {
301
+ if (!CATALOG_KEYS.includes(key)) problems.push(`\`catalog.${key}\` is not a catalog key (${CATALOG_KEYS.join(", ")})`);
302
+ }
303
+ if (source.cdnSegment !== undefined && (typeof source.cdnSegment !== "string" || !CATALOG_SEGMENT.test(source.cdnSegment))) {
304
+ problems.push("`catalog.cdnSegment` must be one lowercase DNS label");
305
+ }
306
+ return problems;
307
+ }
308
+
282
309
  /** Every `shipping` key (ShippingPolicy, tenant.ts). */
283
310
  export const SHIPPING_KEYS = Object.freeze(["tiers"]);
284
311
 
@@ -380,7 +407,7 @@ export function declaredTenantConfig(config) {
380
407
  /** @type {Record<string, unknown>} */
381
408
  const out = {};
382
409
  if (!isPlainObject(config)) return out;
383
- const { displayName, siteType, compliance, capabilities, features, embeds, seo, currency, timezone, shipping } = /** @type {Record<string, unknown>} */ (config);
410
+ const { displayName, siteType, compliance, capabilities, features, embeds, seo, currency, timezone, shipping, catalog } = /** @type {Record<string, unknown>} */ (config);
384
411
  // The store's name as visitors read it: the `<title>` suffix, og:site_name,
385
412
  // Organization JSON-LD, breadcrumbs, the default blog heading, the copyright line.
386
413
  if (typeof displayName === "string" && displayName.trim()) out.displayName = displayName.trim();
@@ -400,6 +427,11 @@ export function declaredTenantConfig(config) {
400
427
  if (shipping !== undefined && shippingDeclarationProblems(shipping).length === 0) {
401
428
  out.shipping = shippingPolicyOf(/** @type {{ tiers: Array<Record<string, unknown>> }} */ (shipping));
402
429
  }
430
+ // Only the declared segment. `catalog.serving` is the platform's and is refused by the validator.
431
+ if (catalog !== undefined && catalogDeclarationProblems(catalog).length === 0
432
+ && isPlainObject(catalog) && typeof catalog.cdnSegment === "string") {
433
+ out.catalog = { cdnSegment: catalog.cdnSegment };
434
+ }
403
435
  return out;
404
436
  }
405
437
 
@@ -713,6 +713,11 @@ export interface TenantConfig {
713
713
  */
714
714
  catalog?: {
715
715
  serving?: "files" | "d1";
716
+ /**
717
+ * Object-key segment for this tenant's catalog images on the owned CDN
718
+ * (`catalog/<cdnSegment>/<file>`). Set only by the tenant's declaration.
719
+ */
720
+ cdnSegment?: string;
716
721
  };
717
722
  /**
718
723
  * Availability (hot) provider (ADR 0033). Availability is never read from catalog