Skip to content

embed: cross-origin web component broken -- module worker chunk fetched from app.simlin.com with no CORS/fallback #688

@bpowers

Description

@bpowers

Summary

The embeddable web component (sd-component.js) is broken on cross-origin (third-party) pages at HEAD. Embedded diagrams load their project data but cannot render or simulate, because the WASM engine now runs in a module Web Worker whose chunk is fetched from app.simlin.com with no CORS headers and no same-origin fallback. This regression class is invisible to every current test and deploy smoke step, because they are all same-origin.

Finding verified independently by two audit agents and re-verified against the source below.

Mechanism

  1. The engine rewrite moved WASM execution into a module Web Worker:

    • src/engine/src/backend-factory.browser.ts:23 does
      new Worker(new URL('./engine-worker.js', import.meta.url), { type: 'module' })
      with no Blob-URL / same-origin fallback. On any worker load failure, worker.onerror (lines 43-50) calls backend.handleWorkerError(...), which rejects all pending and subsequent engine operations.
  2. External sites hotlink the component by exact path:

    • https://app.simlin.com/static/js/sd-component.js is not content-hashed (src/app/config/rsbuild/rsbuild.component.config.js:31 fixed filename + :41 filenameHash: false).
    • Documented as a hard contract in docs/dev/deploy.md:103: "external sites <script src> this exact path."
  3. The component build's assetPrefix: 'auto' (rsbuild.component.config.js:51) resolves the worker chunk to an absolute app.simlin.com URL. The config comment itself states publicPath becomes https://app.simlin.com/static/js/ and that the worker URL resolves as new Worker(new URL(publicPath + chunkUrl, baseURI)). So even when sd-component.js itself is loaded from a third-party page, the worker chunk is fetched cross-origin from app.simlin.com.

  4. GAE serves /static via app.yaml (url: /static, static_dir: public/static, lines 8-9) with no http_headers block, hence no Access-Control-Allow-Origin. A cross-origin module worker fetch is subject to CORS; without the header the fetch fails, worker.onerror fires, and every engine op rejects.

Net effect on a third-party embedding page: the diagram loads project data (the Express /api surface does send CORS), but the engine never initializes, so the embed cannot render or simulate.

Why it matters

  • Broken product contract: the embeddable web component is an explicitly documented external-facing surface (docs/dev/deploy.md:103). Third-party embeds are its entire reason to exist, and they are non-functional at HEAD.
  • Silent / undetectable regression class: there is no test or deploy smoke step that exercises a cross-origin embed. The deploy smoke list (docs/dev/deploy.md:102-103) only does same-origin curl checks, which cannot catch this. Same-origin usage inside app.simlin.com keeps working, so the breakage is invisible to maintainers.
  • Historical nuance: in the 2022 production build the engine ran inline on the main thread. That older build also fetched WASM cross-origin, so embeds may already have been broken before this rewrite. Either way the contract has never been guarded.

Components affected

  • src/engine -- backend-factory.browser.ts (module worker instantiation, no fallback)
  • src/app -- config/rsbuild/rsbuild.component.config.js (assetPrefix: 'auto' -> absolute worker chunk URL; non-hashed sd-component.js)
  • deploy -- app.yaml (/static handler has no http_headers), docs/dev/deploy.md (smoke list)

Possible approaches (not implemented here)

These are candidate fixes for whoever picks this up; choose after design discussion. Do not treat as prescribed.

  1. Serve CORS headers for the worker/WASM chunks. Add http_headers with Access-Control-Allow-Origin to the /static handler in app.yaml (at least for the worker chunk and *.wasm). Simplest, but widens CORS on all of /static; scope carefully.
  2. Same-origin Blob-URL worker shim. In backend-factory.browser.ts, fetch() the worker script and wrap it in a same-origin Blob URL before new Worker(...) (the standard cross-origin worker workaround). The worker still needs to fetch its own WASM/imports cross-origin, so this likely must be combined with (1) and/or importScripts-style absolute-URL handling. A module worker created from a blob also needs care so its relative import.meta.url-based fetches resolve to app.simlin.com.
  3. Add a cross-origin embed check to the deploy smoke list so this regression class is caught going forward (e.g. load sd-component.js from a different origin in a headless browser and assert a diagram renders/simulates). This is orthogonal to 1/2 and worth doing regardless of which fix lands.

How it was discovered

Identified during a deploy-risk audit of HEAD; verified by two independent agents and re-confirmed by reading backend-factory.browser.ts, rsbuild.component.config.js, app.yaml, and docs/dev/deploy.md. Not currently tracked in GitHub issues or docs/tech-debt.md.

Metadata

Metadata

Assignees

No one assigned

    Labels

    backendInvolves the Google App Engine node appbugSomething isn't workingfrontendInvolved the React-based Typescript frontend

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions