@tokenoftrust/storefront-runner 2.4.71 → 2.4.72
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.
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-free, render-time sanitizer for catalog `description_html` (collections
|
|
3
|
+
* today; the same function covers products). The repo ships no runtime HTML-parsing
|
|
4
|
+
* library (see the "no new deps" convention and `blog/markdown.ts`'s own note that
|
|
5
|
+
* `sanitize-html` is a node-only devtool used by the migration pipeline), so this is
|
|
6
|
+
* a defense-in-depth pass that runs where the content is actually rendered with
|
|
7
|
+
* `set:html` — on top of, not instead of, the migration pipeline's own
|
|
8
|
+
* `sanitizeHtml(...)` pass at ingestion (`packages/migration/src/transform/transform.ts`).
|
|
9
|
+
*
|
|
10
|
+
* Scope matches `findUnsafeHtml`'s refusal list
|
|
11
|
+
* (`packages/private-controlplane/src/customization-richcontent.ts`): strip
|
|
12
|
+
* `<script>`/`<style>` elements entirely (tag + content), strip `<iframe>`/
|
|
13
|
+
* `<object>`/`<embed>` entirely, strip every `on*` event-handler attribute, and
|
|
14
|
+
* neutralize `javascript:`/`vbscript:`/`data:` URLs in `href`/`src`/`action`. Any
|
|
15
|
+
* other tag/attribute passes through unchanged — the allowed-tag narrowing already
|
|
16
|
+
* happened upstream in the migration transform.
|
|
17
|
+
*
|
|
18
|
+
* Returns "" for null/blank input so callers can render no block at all rather than
|
|
19
|
+
* an empty one.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const DANGEROUS_ELEMENTS = ["script", "style", "iframe", "object", "embed"];
|
|
23
|
+
|
|
24
|
+
function stripDangerousElements(html: string): string {
|
|
25
|
+
let out = html;
|
|
26
|
+
for (const tag of DANGEROUS_ELEMENTS) {
|
|
27
|
+
// Paired form: <script ...>...</script> (content dropped too — never just
|
|
28
|
+
// unwrapped, since the content of a <script>/<style> is not markup).
|
|
29
|
+
out = out.replace(new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}\\s*>`, "gi"), "");
|
|
30
|
+
// Self-closing / unpaired form: a stray <iframe src="..."> with no closing tag.
|
|
31
|
+
out = out.replace(new RegExp(`<${tag}\\b[^>]*\\/?>`, "gi"), "");
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Strips any `on<event>="..."`/`on<event>='...'`/`on<event>=bare` attribute from every tag. */
|
|
37
|
+
function stripEventHandlerAttributes(html: string): string {
|
|
38
|
+
return html.replace(
|
|
39
|
+
/\son[a-z]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi,
|
|
40
|
+
"",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Rewrites a `javascript:`/`vbscript:`/`data:` URL in href/src/action to "about:blank#blocked". */
|
|
45
|
+
function neutralizeScriptUrls(html: string): string {
|
|
46
|
+
return html.replace(
|
|
47
|
+
/\b(href|src|action)(\s*=\s*)("|')\s*(?:javascript|vbscript|data):[^"'>]*\3/gi,
|
|
48
|
+
(_m, attr: string, eq: string, quote: string) => `${attr}${eq}${quote}about:blank#blocked${quote}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Sanitize already-HTML-sanitized catalog `description_html` for render-time
|
|
54
|
+
* defense-in-depth. Idempotent and pure. Returns "" for null/undefined/blank input.
|
|
55
|
+
*/
|
|
56
|
+
export function sanitizeDescriptionHtml(html: string | null | undefined): string {
|
|
57
|
+
if (!html) return "";
|
|
58
|
+
const trimmed = html.trim();
|
|
59
|
+
if (!trimmed) return "";
|
|
60
|
+
const cleaned = neutralizeScriptUrls(stripEventHandlerAttributes(stripDangerousElements(trimmed))).trim();
|
|
61
|
+
return cleaned;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** True when sanitized output has no renderable content (e.g. input was only tags/whitespace). */
|
|
65
|
+
export function isBlankDescriptionHtml(sanitized: string): boolean {
|
|
66
|
+
return sanitized.replace(/<[^>]+>/g, "").trim().length === 0;
|
|
67
|
+
}
|
|
@@ -19,6 +19,7 @@ import { parseQuery, buildQueryString } from "@/lib/url";
|
|
|
19
19
|
import { breadcrumbLd, itemListLd } from "@/lib/jsonld";
|
|
20
20
|
import { truncate } from "@/lib/format";
|
|
21
21
|
import { withBase } from "@/lib/basePath";
|
|
22
|
+
import { isBlankDescriptionHtml, sanitizeDescriptionHtml } from "@/lib/content/sanitizeDescriptionHtml";
|
|
22
23
|
|
|
23
24
|
const { handle } = Astro.params;
|
|
24
25
|
const { tenant, tot, canonicalBase } = Astro.locals;
|
|
@@ -56,6 +57,13 @@ const crumbs = siteTrail(
|
|
|
56
57
|
|
|
57
58
|
const heroImg = collection.image;
|
|
58
59
|
const desc = collection.description_text || collection.description_html?.replace(/<[^>]+>/g, "");
|
|
60
|
+
// Full-fidelity body (brands/how-it-works/ingredients/FAQ sections on the merchant's
|
|
61
|
+
// live collection pages), sanitized for render-time defense-in-depth on top of the
|
|
62
|
+
// migration pipeline's own sanitize-html pass at ingestion (see
|
|
63
|
+
// sanitizeDescriptionHtml.ts). Rendered as its own block BELOW the product grid,
|
|
64
|
+
// matching the live site's layout — `desc` above stays the short hero excerpt.
|
|
65
|
+
const contentBlockHtml = sanitizeDescriptionHtml(collection.description_html);
|
|
66
|
+
const hasContentBlock = contentBlockHtml.length > 0 && !isBlankDescriptionHtml(contentBlockHtml);
|
|
59
67
|
|
|
60
68
|
// Zero-results recovery: differentiate the cause (search term vs. facet/price
|
|
61
69
|
// filters) and offer targeted "undo just this" links alongside the existing
|
|
@@ -183,4 +191,10 @@ if (query.priceMin != null || query.priceMax != null) {
|
|
|
183
191
|
)}
|
|
184
192
|
</div>
|
|
185
193
|
</div>
|
|
194
|
+
|
|
195
|
+
{hasContentBlock && (
|
|
196
|
+
<section class="container-prose pb-16">
|
|
197
|
+
<div class="rte mx-auto max-w-[68ch] text-[var(--color-text)]" set:html={contentBlockHtml} />
|
|
198
|
+
</section>
|
|
199
|
+
)}
|
|
186
200
|
</Layout>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/storefront-runner",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.72",
|
|
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",
|