From acffbcc3f29caf946ce5b93c461a7a2a23ebbbe2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 21:25:45 +0000 Subject: [PATCH 1/3] Self-host manifold-3d JS to remove unpkg.com from CSP manifold-3d's Emscripten-compiled JS calls locateFile for both the .wasm binary and any .js companion files (e.g. worker scripts). The .wasm was already redirected to the local wasm/ directory, but the .js file fell through and was resolved against the package's original unpkg.com base URL, requiring https://unpkg.com in both script-src and connect-src. Fix: - Copy node_modules/manifold-3d/manifold.js to wasm/ at build time via viteStaticCopy (alongside the existing manifold.wasm copy). - Extend locateFile in api/shapes.js to redirect .js files to wasm/ as well, so all manifold-3d assets are served from self. - Remove https://unpkg.com from script-src and connect-src in the CSP (vite.config.mjs, index.html, docs/CSP_POLICY.md). https://claude.ai/code/session_01AemiiYusMHWjBimfoovWBp --- api/shapes.js | 8 +++++--- docs/CSP_POLICY.md | 11 ++++------- index.html | 2 +- vite.config.mjs | 3 ++- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/api/shapes.js b/api/shapes.js index f567e1e5..05e020de 100644 --- a/api/shapes.js +++ b/api/shapes.js @@ -20,9 +20,11 @@ async function getManifold() { const wasm = await Module({ locateFile: (file) => { - if (file.endsWith('.wasm')) { - // Use base URL for both dev and production (GitHub Pages) - return `${baseUrl}wasm/manifold.wasm`; + if (file.endsWith('.wasm') || file.endsWith('.js')) { + // Serve both the WASM and its JS companion from the local wasm/ directory, + // preventing any fetch to unpkg.com (where the package was originally published). + const name = file.split('/').pop(); + return `${baseUrl}wasm/${name}`; } return file; } diff --git a/docs/CSP_POLICY.md b/docs/CSP_POLICY.md index 063ea864..b886b51e 100644 --- a/docs/CSP_POLICY.md +++ b/docs/CSP_POLICY.md @@ -10,8 +10,8 @@ Content Security Policy (CSP) is defined in two places: Core flows tested (app load, Blockly interaction, run code in sandbox, project export/import trigger, analytics path) observed these runtime source origins: - **document / stylesheet / image / font / xhr**: `self` (`http://127.0.0.1:4173` in local smoke run) -- **script**: `self`, `https://www.googletagmanager.com`, `https://unpkg.com` -- **fetch / connect**: `self`, `https://www.google-analytics.com`, `https://unpkg.com` +- **script**: `self`, `https://www.googletagmanager.com` +- **fetch / connect**: `self`, `https://www.google-analytics.com` ## Why each non-self origin is required @@ -23,9 +23,6 @@ Core flows tested (app load, Blockly interaction, run code in sandbox, project e - Optional Google Analytics transport endpoint used by some environments. - `https://region1.google-analytics.com` - Regional Google Analytics endpoint used by some environments. -- `https://unpkg.com` - - Used by `manifold-3d` runtime assets (`manifold.js` / `manifold.wasm`) in browser execution. - ## Current CSP Header policy (authoritative): @@ -35,11 +32,11 @@ default-src 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; -script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' https://www.googletagmanager.com https://unpkg.com; +script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://www.google-analytics.com https://www.googletagmanager.com; font-src 'self' data:; -connect-src 'self' https://www.googletagmanager.com https://www.google-analytics.com https://region1.google-analytics.com https://stats.g.doubleclick.net https://unpkg.com; +connect-src 'self' https://www.googletagmanager.com https://www.google-analytics.com https://region1.google-analytics.com https://stats.g.doubleclick.net; media-src 'self' data: blob:; worker-src 'self' blob:; frame-src 'self'; diff --git a/index.html b/index.html index e8ad94c1..229945ea 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ Flock XR - Creative coding in 3D diff --git a/vite.config.mjs b/vite.config.mjs index 984709d0..90db0b56 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -10,7 +10,7 @@ const isProduction = process.env.NODE_ENV === 'production'; const BASE_URL = process.env.VITE_BASE_URL || '/'; // `frame-ancestors` is only enforced from HTTP headers (ignored in CSP meta tags). -const CSP_META_POLICY = "default-src 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' https://www.googletagmanager.com https://unpkg.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://www.google-analytics.com https://www.googletagmanager.com; font-src 'self' data:; connect-src 'self' https: https://www.googletagmanager.com https://www.google-analytics.com https://region1.google-analytics.com https://stats.g.doubleclick.net https://unpkg.com; media-src 'self' data: blob:; worker-src 'self' blob:; frame-src 'self'; manifest-src 'self'"; +const CSP_META_POLICY = "default-src 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://www.google-analytics.com https://www.googletagmanager.com; font-src 'self' data:; connect-src 'self' https: https://www.googletagmanager.com https://www.google-analytics.com https://region1.google-analytics.com https://stats.g.doubleclick.net; media-src 'self' data: blob:; worker-src 'self' blob:; frame-src 'self'; manifest-src 'self'"; const CSP_HEADER_POLICY = `${CSP_META_POLICY}; frame-ancestors 'self'`; export default { @@ -30,6 +30,7 @@ export default { { src: 'textures/*.png', dest: 'textures' }, { src: 'fonts/*.{json,woff2,ttf}', dest: 'fonts' }, { src: 'node_modules/manifold-3d/manifold.wasm', dest: 'wasm' }, + { src: 'node_modules/manifold-3d/manifold.js', dest: 'wasm' }, { src: 'node_modules/blockly/media/*', dest: 'blockly/media' }, { src: 'images/dropdown-arrow.svg', dest: 'blockly/media' }, { From fd8f689440614adda41730f60799211b890fa83d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 15 Mar 2026 08:06:09 +0000 Subject: [PATCH 2/3] Fix manifold-3d unpkg.com CSP dependency via Vite transform patch Instead of redirecting manifold-3d's .js companion files through locateFile (which served a raw copy that still had the hardcoded unpkg.com scriptDirectory, breaking WASM loading in workers), strip the hardcoded URL at import time with a Vite transform plugin. The manifoldUnpkgPatchPlugin transform hook matches node_modules/manifold-3d/ manifold.js and replaces 'https://unpkg.com/manifold-3d...' with '' so Emscripten falls back to deriving scriptDirectory from the script's own URL (local server in dev, the bundle chunk in prod). This means no fetch ever leaves the origin for manifold-3d assets, making the https://unpkg.com CSP allowance unnecessary. Also reverts the broken locateFile .js redirect (caused 'ManifoldMesh is not a constructor' because the raw worker copy tried to load WASM from unpkg.com which was then blocked by CSP) and removes the now-unneeded raw manifold.js static copy from viteStaticCopy. https://claude.ai/code/session_01AemiiYusMHWjBimfoovWBp --- api/shapes.js | 8 +++----- vite.config.mjs | 24 +++++++++++++++++++++++- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/api/shapes.js b/api/shapes.js index 05e020de..f567e1e5 100644 --- a/api/shapes.js +++ b/api/shapes.js @@ -20,11 +20,9 @@ async function getManifold() { const wasm = await Module({ locateFile: (file) => { - if (file.endsWith('.wasm') || file.endsWith('.js')) { - // Serve both the WASM and its JS companion from the local wasm/ directory, - // preventing any fetch to unpkg.com (where the package was originally published). - const name = file.split('/').pop(); - return `${baseUrl}wasm/${name}`; + if (file.endsWith('.wasm')) { + // Use base URL for both dev and production (GitHub Pages) + return `${baseUrl}wasm/manifold.wasm`; } return file; } diff --git a/vite.config.mjs b/vite.config.mjs index 90db0b56..74a354e4 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -13,11 +13,34 @@ const BASE_URL = process.env.VITE_BASE_URL || '/'; const CSP_META_POLICY = "default-src 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://www.google-analytics.com https://www.googletagmanager.com; font-src 'self' data:; connect-src 'self' https: https://www.googletagmanager.com https://www.google-analytics.com https://region1.google-analytics.com https://stats.g.doubleclick.net; media-src 'self' data: blob:; worker-src 'self' blob:; frame-src 'self'; manifest-src 'self'"; const CSP_HEADER_POLICY = `${CSP_META_POLICY}; frame-ancestors 'self'`; +/** + * Strips the hardcoded https://unpkg.com/manifold-3d… scriptDirectory from + * the Emscripten-compiled manifold.js so Emscripten resolves assets relative + * to where the file is actually served, instead of reaching out to unpkg.com. + */ +function manifoldUnpkgPatchPlugin() { + const MANIFOLD_RE = /node_modules[/\\]manifold-3d[/\\]manifold\.js$/; + return { + name: 'manifold-unpkg-patch', + transform(code, id) { + if (!MANIFOLD_RE.test(id)) return null; + if (!code.includes('unpkg.com/manifold-3d')) return null; + // Replace every occurrence of the absolute unpkg.com origin so + // Emscripten falls back to the script's own URL as scriptDirectory. + return { + code: code.replace(/https:\/\/unpkg\.com\/manifold-3d[^'"\s]*/g, ''), + map: null, + }; + }, + }; +} + export default { // Ensure assets/chunk URLs are correct in standalone/PWA and under subpaths base: BASE_URL, plugins: [ + manifoldUnpkgPatchPlugin(), cssInjectedByJsPlugin(), viteStaticCopy({ targets: [ @@ -30,7 +53,6 @@ export default { { src: 'textures/*.png', dest: 'textures' }, { src: 'fonts/*.{json,woff2,ttf}', dest: 'fonts' }, { src: 'node_modules/manifold-3d/manifold.wasm', dest: 'wasm' }, - { src: 'node_modules/manifold-3d/manifold.js', dest: 'wasm' }, { src: 'node_modules/blockly/media/*', dest: 'blockly/media' }, { src: 'images/dropdown-arrow.svg', dest: 'blockly/media' }, { From 9e2dda4255a00a403e43e8d9c3b67e8c69b695d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 15 Mar 2026 08:18:42 +0000 Subject: [PATCH 3/3] Fix: inject manifold-3d wasm into InitializeCSG2Async to avoid unpkg.com MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: BABYLON.InitializeCSG2Async() with no arguments fetches manifold-3d from https://unpkg.com/manifold-3d@3.3.0 at runtime. With unpkg.com removed from the CSP, that fetch is blocked, leaving BabylonJS without an initialized manifold module. Any subsequent CSG2 operation then throws "ManifoldMesh is not a constructor". Fix: InitializeCSG2Async accepts an options object with manifoldInstance and manifoldMeshInstance. We pre-load the wasm module via the existing getManifold() helper in api/shapes.js (which already serves the WASM from the locally-hosted /wasm/manifold.wasm via locateFile) and inject the resulting Manifold and Mesh constructors into BabylonJS, so it never makes an external request. Also removes the manifoldUnpkgPatchPlugin Vite transform that was targeting the wrong file — the unpkg.com load was coming from BabylonJS, not from the manifold-3d npm package itself. https://claude.ai/code/session_01AemiiYusMHWjBimfoovWBp --- api/shapes.js | 2 ++ flock.js | 12 ++++++++++-- vite.config.mjs | 23 ----------------------- 3 files changed, 12 insertions(+), 25 deletions(-) diff --git a/api/shapes.js b/api/shapes.js index f567e1e5..5ff50103 100644 --- a/api/shapes.js +++ b/api/shapes.js @@ -45,6 +45,8 @@ async function getManifold() { return manifoldInitPromise; } +export { getManifold }; + export function setFlockReference(ref) { flock = ref; } diff --git a/flock.js b/flock.js index 3d1e531e..4b25ff1e 100644 --- a/flock.js +++ b/flock.js @@ -47,7 +47,7 @@ import { setFlockReference as setFlockMovement, } from "./api/movement"; import { flockModels, setFlockReference as setFlockModels } from "./api/models"; -import { flockShapes, setFlockReference as setFlockShapes } from "./api/shapes"; +import { flockShapes, setFlockReference as setFlockShapes, getManifold } from "./api/shapes"; import { flockTransform, setFlockReference as setFlockTransform, @@ -1258,7 +1258,15 @@ export const flock = { flock.abortController = new AbortController(); try { - await flock.BABYLON.InitializeCSG2Async(); + // Pre-load the manifold-3d wasm module (which already redirects + // its WASM fetch to the locally-served /wasm/manifold.wasm via + // locateFile) and inject it into BabylonJS so InitializeCSG2Async + // never fetches from unpkg.com. + const manifoldWasm = await getManifold(); + await flock.BABYLON.InitializeCSG2Async({ + manifoldInstance: manifoldWasm.Manifold, + manifoldMeshInstance: manifoldWasm.Mesh, + }); } catch (error) { console.error("Error initializing CSG2:", error); } diff --git a/vite.config.mjs b/vite.config.mjs index 74a354e4..b9ed0be0 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -13,34 +13,11 @@ const BASE_URL = process.env.VITE_BASE_URL || '/'; const CSP_META_POLICY = "default-src 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://www.google-analytics.com https://www.googletagmanager.com; font-src 'self' data:; connect-src 'self' https: https://www.googletagmanager.com https://www.google-analytics.com https://region1.google-analytics.com https://stats.g.doubleclick.net; media-src 'self' data: blob:; worker-src 'self' blob:; frame-src 'self'; manifest-src 'self'"; const CSP_HEADER_POLICY = `${CSP_META_POLICY}; frame-ancestors 'self'`; -/** - * Strips the hardcoded https://unpkg.com/manifold-3d… scriptDirectory from - * the Emscripten-compiled manifold.js so Emscripten resolves assets relative - * to where the file is actually served, instead of reaching out to unpkg.com. - */ -function manifoldUnpkgPatchPlugin() { - const MANIFOLD_RE = /node_modules[/\\]manifold-3d[/\\]manifold\.js$/; - return { - name: 'manifold-unpkg-patch', - transform(code, id) { - if (!MANIFOLD_RE.test(id)) return null; - if (!code.includes('unpkg.com/manifold-3d')) return null; - // Replace every occurrence of the absolute unpkg.com origin so - // Emscripten falls back to the script's own URL as scriptDirectory. - return { - code: code.replace(/https:\/\/unpkg\.com\/manifold-3d[^'"\s]*/g, ''), - map: null, - }; - }, - }; -} - export default { // Ensure assets/chunk URLs are correct in standalone/PWA and under subpaths base: BASE_URL, plugins: [ - manifoldUnpkgPatchPlugin(), cssInjectedByJsPlugin(), viteStaticCopy({ targets: [