@tokenoftrust/storefront-runner 3.0.0 → 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.
- package/agency-kit/tools/lib/public-binaries.mjs +31 -15
- package/apps/storefront/src/lib/content/shared/homeVisualParity.ts +4 -4
- package/apps/storefront/src/lib/content/storyblok/content-model.ts +1 -1
- package/apps/storefront/src/lib/content/trustStrip.ts +2 -5
- package/package.json +1 -1
- package/packages/public-runtime/package.json +1 -0
- package/packages/public-runtime/src/extension-contract-values.d.mts +10 -0
- package/packages/public-runtime/src/extension-contract-values.mjs +54 -0
- package/packages/public-runtime/src/extension-contract.ts +4 -0
- package/packages/public-runtime/src/image-dependencies.ts +19 -0
- package/scripts/tenant/validate.mjs +6 -10
|
@@ -5,10 +5,13 @@
|
|
|
5
5
|
* catalog images, and the source store is never shut down before that check passes. This decides,
|
|
6
6
|
* from a listing of the store's `public/` tree, whether the repo is carrying binaries it must not.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* Findings, each naming its remedy:
|
|
9
9
|
*
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
10
|
+
* - one per entry of {@link FORBIDDEN_PUBLIC_DIRS}: any file under a directory a media download
|
|
11
|
+
* writes (`public/catalog-images/` is `catalog-images-in-repo`, `public/blog/wp-content/uploads/`
|
|
12
|
+
* is `blog-media-in-repo`). Decided from paths alone, so a caller without sizes still refuses
|
|
13
|
+
* it. This list is the one source the validators and the reconcile read; the Gitea
|
|
14
|
+
* pre-receive size guard names the same directories.
|
|
12
15
|
* - `public-binary-oversized` — one raster or media file over {@link PUBLIC_BINARY_LIMITS}
|
|
13
16
|
* `maxFileBytes`.
|
|
14
17
|
* - `public-tree-over-budget` — the whole `public/` tree over `maxTotalBytes`.
|
|
@@ -28,13 +31,25 @@ export const PUBLIC_BINARY_LIMITS = Object.freeze({
|
|
|
28
31
|
maxTotalBytes: 32 * 1024 * 1024,
|
|
29
32
|
});
|
|
30
33
|
|
|
31
|
-
/**
|
|
32
|
-
|
|
34
|
+
/**
|
|
35
|
+
* The directories under `public/` a media download writes. No file under one may be committed.
|
|
36
|
+
*
|
|
37
|
+
* @type {ReadonlyArray<Readonly<{ dir: string, code: string, noun: string }>>}
|
|
38
|
+
*/
|
|
39
|
+
export const FORBIDDEN_PUBLIC_DIRS = Object.freeze([
|
|
40
|
+
Object.freeze({ dir: "catalog-images/", code: "catalog-images-in-repo", noun: "catalog image" }),
|
|
41
|
+
Object.freeze({ dir: "blog/wp-content/uploads/", code: "blog-media-in-repo", noun: "blog media" }),
|
|
42
|
+
]);
|
|
33
43
|
|
|
34
|
-
/** The finding
|
|
35
|
-
export const
|
|
44
|
+
/** The finding codes decided from paths alone. */
|
|
45
|
+
export const PATH_DECIDED_FINDING_CODES = Object.freeze(new Set(FORBIDDEN_PUBLIC_DIRS.map((d) => d.code)));
|
|
46
|
+
|
|
47
|
+
/** True for a path (relative to `public/`, `/` separators) under a forbidden directory. */
|
|
48
|
+
export function isForbiddenPublicPath(rel) {
|
|
49
|
+
return FORBIDDEN_PUBLIC_DIRS.some((d) => rel.startsWith(d.dir));
|
|
50
|
+
}
|
|
36
51
|
|
|
37
|
-
/** The kit command that moves
|
|
52
|
+
/** The kit command that moves downloaded catalog images and blog media back to their source URLs. */
|
|
38
53
|
export const UNHOST_IMAGES_COMMAND = "node agency-kit/tools/import-catalog.mjs --store <checkout> --unhost-images --write";
|
|
39
54
|
|
|
40
55
|
const RASTER_OR_MEDIA = /\.(?:jpe?g|png|gif|webp|avif|bmp|tiff?|heic|heif|ico|jxl|mp4|m4v|mov|webm|mkv|avi|mpe?g|mp3|wav|ogg|oga|m4a|aac|flac|opus)$/i;
|
|
@@ -51,19 +66,20 @@ export function publicBinaryFindings(entries, limits = {}) {
|
|
|
51
66
|
const maxFileBytes = limits.maxFileBytes ?? PUBLIC_BINARY_LIMITS.maxFileBytes;
|
|
52
67
|
const maxTotalBytes = limits.maxTotalBytes ?? PUBLIC_BINARY_LIMITS.maxTotalBytes;
|
|
53
68
|
const findings = [];
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
69
|
+
for (const { dir, code, noun } of FORBIDDEN_PUBLIC_DIRS) {
|
|
70
|
+
const files = entries.filter((e) => e.rel.startsWith(dir));
|
|
71
|
+
if (files.length === 0) continue;
|
|
72
|
+
const bytes = files.reduce((n, e) => n + (e.bytes ?? 0), 0);
|
|
57
73
|
findings.push({
|
|
58
|
-
code
|
|
59
|
-
path: `public/${
|
|
60
|
-
message: `${
|
|
74
|
+
code,
|
|
75
|
+
path: `public/${dir}`,
|
|
76
|
+
message: `${files.length} ${noun} file(s)${bytes ? ` (${mb(bytes)})` : ""} are committed to the store repo; ` +
|
|
61
77
|
"images belong in the platform pool, not the repo; until platform capture owns them, keep each image at its source URL",
|
|
62
78
|
remedy: UNHOST_IMAGES_COMMAND,
|
|
63
79
|
});
|
|
64
80
|
}
|
|
65
81
|
for (const e of entries) {
|
|
66
|
-
if (e.rel
|
|
82
|
+
if (isForbiddenPublicPath(e.rel) || e.bytes === undefined) continue;
|
|
67
83
|
if (e.bytes > maxFileBytes && RASTER_OR_MEDIA.test(e.rel)) {
|
|
68
84
|
findings.push({
|
|
69
85
|
code: "public-binary-oversized",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
39
|
-
isSafeRehostedAssetPathForTenant,
|
|
38
|
+
isPageBlockImageReference,
|
|
40
39
|
isSafeStorefrontHref,
|
|
41
40
|
isThemeCssSize,
|
|
42
41
|
} from "../../packages/public-runtime/src/extension-contract-values.mjs";
|
|
@@ -45,7 +44,7 @@ import {
|
|
|
45
44
|
parseMappingTarget,
|
|
46
45
|
workspacePathForTenantRelative,
|
|
47
46
|
} from "../../agency-kit/tools/lib/tot-repo-mappings.mjs";
|
|
48
|
-
import {
|
|
47
|
+
import { isForbiddenPublicPath, publicBinaryFindings } from "../../agency-kit/tools/lib/public-binaries.mjs";
|
|
49
48
|
import { validateVisualParityClosure } from "../../packages/cli/src/visual-parity.mjs";
|
|
50
49
|
import { declaredObligations, isRegulatedCompliance, validateDeclaredConfig } from "../../packages/cli/src/declared-config.mjs";
|
|
51
50
|
|
|
@@ -443,11 +442,8 @@ function validateProductSource(value, file, at) {
|
|
|
443
442
|
}
|
|
444
443
|
|
|
445
444
|
function validateVisualAsset(value, file, at, out, tenantId) {
|
|
446
|
-
|
|
447
|
-
|
|
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
|
|
|
@@ -1367,8 +1363,8 @@ function checkAsset(src, file, publicDir, tenantId) {
|
|
|
1367
1363
|
function findLargePublicAssets(publicDir, rel) {
|
|
1368
1364
|
const out = [];
|
|
1369
1365
|
for (const p of walk(publicDir, () => true)) {
|
|
1370
|
-
//
|
|
1371
|
-
if (relative(publicDir, p).split("\\").join("/")
|
|
1366
|
+
// a forbidden media directory is already an error (catalog-images-in-repo, blog-media-in-repo); one line, not thousands.
|
|
1367
|
+
if (isForbiddenPublicPath(relative(publicDir, p).split("\\").join("/"))) continue;
|
|
1372
1368
|
const size = statSync(p).size;
|
|
1373
1369
|
if (size >= LARGE_PUBLIC_ASSET_BYTES) {
|
|
1374
1370
|
out.push(mk(WARN, "large-public-asset", rel(p), `${(size / 1024).toFixed(0)}KB`));
|