Fix production dynamic-page modules and trusted email previews
This commit is contained in:
1 parent
bccbf1f4c9
commit
646814d6df
9 files changed
+149
-9
No files matched your search
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/block-editor",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.19",
|
||||
"type": "module",
|
||||
"description": "Hydrated accessible editor runtime for WRNexus block documents.",
|
||||
"exports": {
|
||||
@@ -25,11 +25,14 @@
|
||||
"@wrnexus/blocks": "workspace:*",
|
||||
"@wrnexus/dnd": "workspace:*",
|
||||
"@wrnexus/reactive": "workspace:*",
|
||||
"codemirror": "6.0.2"
|
||||
"codemirror": "6.0.2",
|
||||
"dompurify": "^3.4.16"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.4.0",
|
||||
"@types/jsdom": "^30.0.0",
|
||||
"happy-dom": "^20.11.2",
|
||||
"jsdom": "^30.1.1",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
@@ -10,3 +10,4 @@ export * from "./image-preview.ts";
|
||||
export * from "./announcements.ts";
|
||||
export * from "./kit.ts";
|
||||
export * from "./source-editor.ts";
|
||||
export { setBuilderPreview, builderPreviewHTML } from "./preview-html.ts";
|
||||
@@ -1,8 +1,9 @@
|
||||
import { builderPreviewHTML } from "./preview-html.ts";
|
||||
/** Updates the existing sandbox document without navigating/reloading its iframe. */
|
||||
export function updateBuilderCanvas(frame: HTMLIFrameElement, html: string): void {
|
||||
const canvas = frame.contentDocument;
|
||||
if (!canvas?.body) return;
|
||||
const parsed = new DOMParser().parseFromString(html, "text/html");
|
||||
const parsed = new DOMParser().parseFromString(builderPreviewHTML(html, frame.ownerDocument), "text/html");
|
||||
const scrolling = canvas.scrollingElement;
|
||||
const top = scrolling?.scrollTop ?? 0, left = scrolling?.scrollLeft ?? 0;
|
||||
canvas.head.replaceChildren(...Array.from(parsed.head.childNodes).map(node => canvas.importNode(node, true)));
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import createDOMPurify from "dompurify";
|
||||
|
||||
const sanitizers = new WeakMap<object, ReturnType<typeof createDOMPurify>>();
|
||||
/** Sanitize generated previews in the sink's realm, including Trusted Types. */
|
||||
export function builderPreviewHTML(html: string, doc: Document = document): string {
|
||||
const realm = doc.defaultView;
|
||||
if (!realm) throw new Error("Preview document has no browser realm");
|
||||
let sanitizer = sanitizers.get(realm);
|
||||
if (!sanitizer) {
|
||||
sanitizer = createDOMPurify(realm);
|
||||
sanitizers.set(realm, sanitizer);
|
||||
}
|
||||
return sanitizer.sanitize(html, {
|
||||
WHOLE_DOCUMENT: true,
|
||||
RETURN_TRUSTED_TYPE: true,
|
||||
FORBID_TAGS: ["script", "iframe", "object", "embed", "form", "base"],
|
||||
}) as unknown as string;
|
||||
}
|
||||
|
||||
/** Only script-free sandboxed frames may receive generated email previews. */
|
||||
export function setBuilderPreview(frame: HTMLIFrameElement, html: string): void {
|
||||
if (!frame.hasAttribute("sandbox") || frame.sandbox.contains("allow-scripts"))
|
||||
throw new Error("Email previews require a script-free sandbox");
|
||||
frame.srcdoc = builderPreviewHTML(html, frame.ownerDocument);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { JSDOM } from "jsdom";
|
||||
import { builderPreviewHTML } from "../src/preview-html.ts";
|
||||
|
||||
test("preview sanitization preserves email layout and editor markers but removes executable content", () => {
|
||||
const { window } = new JSDOM("");
|
||||
const result = String(builderPreviewHTML('<html><head><style>td{padding:12px}</style></head><body><table data-block-id="one"><tr><td><img src="https://example.com/a.png" onerror="alert(1)"><a href="javascript:alert(1)">Hello</a></td></tr></table><script>alert(1)</script><iframe src="https://example.com"></iframe></body></html>', window.document as unknown as Document));
|
||||
expect(result).toContain('data-block-id="one"');
|
||||
expect(result).toContain('<table');
|
||||
expect(result).toContain('<style>');
|
||||
expect(result).not.toMatch(/onerror|javascript:|<script|<iframe/);
|
||||
window.close();
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/cli",
|
||||
"version": "0.8.72",
|
||||
"version": "0.8.73",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -202,7 +202,10 @@ export async function runBuild(appRoot: string): Promise<void> {
|
||||
.update(source)
|
||||
.digest("hex")
|
||||
.slice(0, 16);
|
||||
const clientOut = join(clientModulesDir, `${basename(file, ".wrn")}-${clientHash}.mjs`);
|
||||
// Dynamic route names contain brackets, which the production asset handler
|
||||
// deliberately rejects. Keep public artifact names within its safe alphabet.
|
||||
const clientStem = basename(file, ".wrn").replace(/[^A-Za-z0-9._-]/g, "_");
|
||||
const clientOut = join(clientModulesDir, `${clientStem}-${clientHash}.mjs`);
|
||||
const clientUrl = `/__wrnexus/client/${basename(clientOut)}`;
|
||||
const clientEntry = join(compiledDir, `client${artifactId}.entry.mjs`);
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ export default {
|
||||
join(root, "app", "pages", "index.wrn"),
|
||||
`import RuntimeProbe from "../components/RuntimeProbe.wrn"\npage Home { view { <RuntimeProbe /> } }\n`,
|
||||
);
|
||||
writeFileSync(join(root, "app", "pages", "[id].wrn"),
|
||||
`page Dynamic { functions { client function selectPanel() { console.log('panel-ready') } } view { <button @click="selectPanel()">Panel</button> } }`);
|
||||
|
||||
await runBuild(root);
|
||||
|
||||
@@ -76,6 +78,11 @@ export default {
|
||||
|
||||
const page = await fetch(`http://127.0.0.1:${port}/`);
|
||||
expect(page.status).toBe(200);
|
||||
const dynamic = await fetch(`http://127.0.0.1:${port}/example`);
|
||||
const dynamicHtml = await dynamic.text();
|
||||
const modulePath = dynamicHtml.match(/\/__wrnexus\/client\/[^"\s<>]+\.mjs/)?.[0];
|
||||
expect(modulePath).toMatch(/^\/__wrnexus\/client\/[A-Za-z0-9._-]+\.mjs$/);
|
||||
expect((await fetch(`http://127.0.0.1:${port}${modulePath}`)).status).toBe(200);
|
||||
const html = await page.text();
|
||||
const assetPath = html.match(/(\/__wrnexus\/assets\/runtime-fixture\.[a-f0-9]+\.js)/)?.[1];
|
||||
expect(assetPath).toBeTruthy();
|
||||
|
||||
Reference in new issue
Block a user