@tokenoftrust/storefront-runner 3.0.1 → 3.1.0

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.
@@ -1,5 +1,5 @@
1
1
  import {
2
- isSafeRehostedAssetPathForTenant,
2
+ isPageBlockImageReference,
3
3
  isSafeStorefrontHref,
4
4
  } from "@tot/public-runtime";
5
5
  import { withBase } from "@/lib/publication/shared/basePath";
@@ -73,7 +73,7 @@ export function promoTilesView(
73
73
  .map((tile, index) => ({
74
74
  ...tile,
75
75
  href: withBase(basePath, tile.href),
76
- image: isSafeRehostedAssetPathForTenant(tile.image, tenantId) ? tile.image : undefined,
76
+ image: isPageBlockImageReference(tile.image, tenantId) ? tile.image : undefined,
77
77
  imageAlt: tile.imageAlt ?? tile.label,
78
78
  imageWidth: isLogoStrip ? 160 : 800,
79
79
  imageHeight: isLogoStrip ? 80 : isPortrait ? 1000 : 450,
@@ -95,12 +95,12 @@ export function promoTilesView(
95
95
  }
96
96
 
97
97
  /**
98
- * The editorial block's media column: the rehosted image, else generated art when
98
+ * The editorial block's media column: the block image, else generated art when
99
99
  * the block asks for it with `imageSeed`, else none — a text-only block.
100
100
  */
101
101
  export function editorialMediaView(block: EditorialBlock, tenantId: string) {
102
102
  if (
103
- isSafeRehostedAssetPathForTenant(block.image, tenantId) &&
103
+ isPageBlockImageReference(block.image, tenantId) &&
104
104
  typeof block.imageAlt === "string"
105
105
  ) {
106
106
  return { kind: "image" as const, src: block.image, alt: block.imageAlt };
@@ -125,7 +125,7 @@ export interface EditorialBlock {
125
125
  body_html: string;
126
126
  /** Generated placeholder art. With neither `image` nor `imageSeed` the block is text-only. */
127
127
  imageSeed?: string;
128
- /** Rehosted editorial photography; preferred over a generated placeholder. */
128
+ /** Editorial photography (a page-block image reference); preferred over a generated placeholder. */
129
129
  image?: string;
130
130
  imageAlt?: string;
131
131
  align?: "left" | "right";
@@ -14,8 +14,7 @@
14
14
  */
15
15
  import {
16
16
  isFabricatedTrustProof,
17
- isSafeRehostedAssetPath,
18
- isSafeRehostedAssetPathForTenant,
17
+ isPageBlockImageReference,
19
18
  } from "@tot/public-runtime";
20
19
 
21
20
  export type TrustStripItem =
@@ -82,9 +81,7 @@ export function sanitizeTrustStrip(
82
81
  out.push({
83
82
  kind: "claim",
84
83
  label,
85
- ...((tenantId
86
- ? isSafeRehostedAssetPathForTenant(item.icon, tenantId)
87
- : isSafeRehostedAssetPath(item.icon))
84
+ ...(isPageBlockImageReference(item.icon, tenantId || undefined)
88
85
  ? { icon: item.icon, iconAlt: item.iconAlt?.trim() || "" }
89
86
  : {}),
90
87
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "3.0.1",
3
+ "version": "3.1.0",
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",
@@ -16,6 +16,7 @@
16
16
  "./image-finding-acceptance": "./src/image-finding-acceptance.ts",
17
17
  "./image-preview-rewrite-overlay": "./src/image-preview-rewrite-overlay.ts",
18
18
  "./capture-rewrites": "./src/capture-rewrites.ts",
19
+ "./extension-contract-values": "./src/extension-contract-values.mjs",
19
20
  "./hosted-image-media-types": "./src/hosted-image-media-types.ts",
20
21
  "./durable-assets": "./src/durable-assets.ts",
21
22
  "./catalog-files": "./src/catalog-files.ts",
@@ -19,4 +19,14 @@ export declare const THEME_HEADING_LEVELS: readonly ["hero", "h1", "h2", "h3"];
19
19
  export declare function isThemeCssSize(value: unknown): value is string;
20
20
  export declare function isSafeRehostedAssetPath(value: unknown): value is string;
21
21
  export declare function isSafeRehostedAssetPathForTenant(value: unknown, tenantId: unknown): value is string;
22
+ export declare function isPlatformPoolImagePath(value: unknown): value is string;
23
+ export type ImageSourceUrlRefusal =
24
+ | "unsupported"
25
+ | "unsafe-scheme"
26
+ | "userinfo"
27
+ | "nonstandard-port"
28
+ | "blocked-address"
29
+ | "ip-literal";
30
+ export declare function imageSourceUrlRefusal(value: unknown): ImageSourceUrlRefusal | null;
31
+ export declare function isPageBlockImageReference(value: unknown, tenantId?: unknown): value is string;
22
32
  export declare function isSafeStorefrontHref(value: unknown): value is string;
@@ -58,6 +58,60 @@ export function isSafeRehostedAssetPathForTenant(value, tenantId) {
58
58
  return value.startsWith("/shared/") || value.startsWith(`/tenants/${tenantId}/`);
59
59
  }
60
60
 
61
+ /** A platform pool reference `/_a/<sha256>[.<ext>]`: content-addressed bytes the platform owns. */
62
+ export function isPlatformPoolImagePath(value) {
63
+ return typeof value === "string" && /^\/_a\/[0-9a-f]{64}(?:\.[a-z0-9]{1,8})?$/.test(value);
64
+ }
65
+
66
+ const IMAGE_SOURCE_DNS_NAME = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i;
67
+ const IMAGE_SOURCE_BLOCKED_NAME = /(?:^|\.)(?:localhost|localdomain|local|internal|intranet|lan|home|arpa)$/i;
68
+ const IMAGE_SOURCE_IPV4 = /^\d{1,3}(?:\.\d{1,3}){3}$/;
69
+
70
+ /**
71
+ * The locator half of remote image capture's refusal: why capture will not fetch `value`, or null
72
+ * when it will try. Capture (`original-image-capture.ts`) runs exactly this before it resolves the
73
+ * host and pins a public address; `"ip-literal"` hands an IPv4 literal back to that caller's
74
+ * address blocklist. Every other surface that accepts a source URL for capture uses this function,
75
+ * so authoring can never accept a locator capture refuses.
76
+ *
77
+ * @returns {null | "unsupported" | "unsafe-scheme" | "userinfo" | "nonstandard-port" | "blocked-address" | "ip-literal"}
78
+ */
79
+ export function imageSourceUrlRefusal(value) {
80
+ if (typeof value !== "string" || value.length === 0 || value.length > 2048) return "unsupported";
81
+ let url;
82
+ try {
83
+ url = new URL(value);
84
+ } catch {
85
+ return "unsupported";
86
+ }
87
+ if (url.protocol !== "https:") return "unsafe-scheme";
88
+ if (url.username || url.password) return "userinfo";
89
+ if (url.port) return "nonstandard-port";
90
+ const hostname = url.hostname;
91
+ if (!hostname || hostname.includes("%")) return "blocked-address";
92
+ if (IMAGE_SOURCE_IPV4.test(hostname)) return "ip-literal";
93
+ const name = hostname.endsWith(".") ? hostname.slice(0, -1) : hostname;
94
+ if (IMAGE_SOURCE_BLOCKED_NAME.test(name) || !IMAGE_SOURCE_DNS_NAME.test(hostname)) return "blocked-address";
95
+ return null;
96
+ }
97
+
98
+ /**
99
+ * A page-block image (hero image and slides, editorial image, promo tiles, trust-strip icons):
100
+ * a governed `/shared/` or active-tenant `/tenants/<id>/` path, a platform pool `/_a/<sha256>`
101
+ * reference, or an https source URL on a public DNS name that capture fetches into the pool at
102
+ * hosted preview and publish. A source URL is written as authored: no whitespace, quotes, angle
103
+ * brackets or backslashes. Without `tenantId`, any governed tenant path is accepted.
104
+ */
105
+ export function isPageBlockImageReference(value, tenantId) {
106
+ if (typeof value !== "string") return false;
107
+ if (tenantId === undefined || tenantId === null
108
+ ? isSafeRehostedAssetPath(value)
109
+ : isSafeRehostedAssetPathForTenant(value, tenantId)) return true;
110
+ if (isPlatformPoolImagePath(value)) return true;
111
+ if (!/^https:\/\//i.test(value) || /[\u0000-\u0020\u007f"'<>\\`]/.test(value)) return false;
112
+ return imageSourceUrlRefusal(value) === null;
113
+ }
114
+
61
115
  /** Navigation authored in a commerce block stays on the current storefront. */
62
116
  export function isSafeStorefrontHref(value) {
63
117
  if (typeof value !== "string" || !value.startsWith("/") || value.startsWith("//")) return false;
@@ -17,11 +17,15 @@ export {
17
17
  FEATURED_PRODUCT_PRESENTATIONS,
18
18
  PROMO_TILE_PRESENTATIONS,
19
19
  THEME_HEADING_LEVELS,
20
+ imageSourceUrlRefusal,
21
+ isPageBlockImageReference,
22
+ isPlatformPoolImagePath,
20
23
  isSafeRehostedAssetPath,
21
24
  isSafeRehostedAssetPathForTenant,
22
25
  isSafeStorefrontHref,
23
26
  isThemeCssSize,
24
27
  } from "./extension-contract-values.mjs";
28
+ export type { ImageSourceUrlRefusal } from "./extension-contract-values.mjs";
25
29
 
26
30
  export type PromoTilePresentation =
27
31
  (typeof PROMO_TILE_PRESENTATIONS)[number];
@@ -168,6 +168,16 @@ const DEFERRED_IMAGE_MESSAGE = "A CSS image form was skipped because hosted prev
168
168
  const BUILTIN_PLATFORM_PREFIXES = ["/_astro/", "/shared/"] as const;
169
169
  const DIRECT_IMAGE_KEY_RE = /^(?:image|images|featuredimage|featured_image|thumbnail|thumbnailurl|imageurl|image_url)$/i;
170
170
  const SCALAR_IMAGE_KEY_RE = /^(?:thumbnailurl|imageurl|image_url)$/i;
171
+ /**
172
+ * Identity and chrome image fields (trust-strip `icon`, chrome `head.favicon`, theme
173
+ * `brand.logoLight`/`logoDark`/`favicon`/`ogImage`, author `avatar`, checkout `logoUrl`). The same
174
+ * keys also carry inline SVG markup, icon names, emoji and booleans, so a value is a reference only
175
+ * when it is locator-shaped; anything else is not an image and is skipped without a finding.
176
+ */
177
+ const LOCATOR_IMAGE_KEY_RE = /^(?:icon|favicon|logo|logolight|logodark|avatar|ogimage|og_image|logourl|logo_url)$/i;
178
+ /** An object under one of these keys is an image whose `src`/`url` is the locator (chrome `brand.logo`, JSON-LD `logo`). */
179
+ const LOCATOR_IMAGE_OBJECT_KEY_RE = /^logo$/i;
180
+ const LOCATOR_SHAPED_RE = /^(?:\/|https?:|data:image\/)/i;
171
181
  const HTML_VALUE_KEYS = new Set(["contenthtml", "content_html", "descriptionhtml", "description_html", "html"]);
172
182
  const NON_IMAGE_ASSET_RE = /\.(?:css|m?js|woff2?|ttf|otf|eot|map|json|xml|txt|pdf|mp4|webm|mov)$/i;
173
183
  const CONTROL_RE = /[\u0000-\u001f\u007f]/;
@@ -935,6 +945,8 @@ function catalogReferences(content: string, artifactPath: string): ExtractedRefe
935
945
  else if (DIRECT_IMAGE_KEY_RE.test(key) || (imageContext && /^(?:src|url)$/i.test(key))) {
936
946
  if (trimAsciiWhitespace(value)) refs.push({ locator: value, surface: "catalog" });
937
947
  else malformed();
948
+ } else if (LOCATOR_IMAGE_KEY_RE.test(key) && LOCATOR_SHAPED_RE.test(trimAsciiWhitespace(value))) {
949
+ refs.push({ locator: value, surface: "catalog" });
938
950
  }
939
951
  continue;
940
952
  }
@@ -967,12 +979,19 @@ function catalogReferences(content: string, artifactPath: string): ExtractedRefe
967
979
  if (imageContext && !entries.some(([childKey, child]) => /^(?:src|url)$/i.test(childKey) && typeof child === "string" && trimAsciiWhitespace(child))) {
968
980
  malformed();
969
981
  }
982
+ const locatorImageObject = !imageContext && LOCATOR_IMAGE_OBJECT_KEY_RE.test(key);
970
983
  for (let index = entries.length - 1; index >= 0; index--) {
971
984
  const [childKey, child] = entries[index]!;
972
985
  if (imageContext && /^(?:src|url)$/i.test(childKey)) {
973
986
  if (typeof child === "string" && trimAsciiWhitespace(child)) stack.push({ kind: "reference", locator: child });
974
987
  continue;
975
988
  }
989
+ if (locatorImageObject && /^(?:src|url)$/i.test(childKey)) {
990
+ if (typeof child === "string" && LOCATOR_SHAPED_RE.test(trimAsciiWhitespace(child))) {
991
+ stack.push({ kind: "reference", locator: child });
992
+ }
993
+ continue;
994
+ }
976
995
  const nextImageContext = /^(?:image|images|featuredImage|featured_image|thumbnail)$/i.test(childKey);
977
996
  stack.push({ kind: "visit", value: child, key: childKey, imageContext: nextImageContext });
978
997
  }
@@ -35,8 +35,7 @@ import {
35
35
  FEATURED_PRODUCT_PRESENTATIONS,
36
36
  PROMO_TILE_PRESENTATIONS,
37
37
  THEME_HEADING_LEVELS,
38
- isSafeRehostedAssetPath,
39
- isSafeRehostedAssetPathForTenant,
38
+ isPageBlockImageReference,
40
39
  isSafeStorefrontHref,
41
40
  isThemeCssSize,
42
41
  } from "../../packages/public-runtime/src/extension-contract-values.mjs";
@@ -443,11 +442,8 @@ function validateProductSource(value, file, at) {
443
442
  }
444
443
 
445
444
  function validateVisualAsset(value, file, at, out, tenantId) {
446
- const safe = tenantId
447
- ? isSafeRehostedAssetPathForTenant(value, tenantId)
448
- : isSafeRehostedAssetPath(value);
449
- if (!safe) {
450
- out.push(mk(ERROR, "asset-path", file, `${at} must be a /shared/... asset or belong to the active tenant`));
445
+ if (!isPageBlockImageReference(value, tenantId || undefined)) {
446
+ out.push(mk(ERROR, "asset-path", file, `${at} must be a /shared/... or active-tenant asset, a /_a/<sha256> pool reference, or an https source URL on a public host (captured into the pool at preview and publish)`));
451
447
  }
452
448
  }
453
449