Core Philosophy#
Mol* Linker operates across several strict security boundaries imposed by the browser. Modern browsers enforce Content Security Policies (CSP) that block unsafe-eval in Manifest V3 extension pages, and restrict cross-origin requests (CORS) between websites and external servers.
To load multi-megabyte structural files from dynamic Single Page Applications (GitHub, GitLab, RCSB) into a high-performance WebGL visualizer (Mol*), the extension uses a 4-Layer Architecture where each layer has one clearly defined responsibility.
The 4-Layer Architecture#
1. Reconnaissance (The Content Script)#
- Source:
src/content.ts - Output:
content.js(bundled standalone, no imports) - Environment: The host webpage (GitHub, GitLab, RCSB, custom domains)
- Role: Scans the DOM for links that point to supported structural file formats. Uses a 3-ring scanner — URL, HTML attributes, and surrounding parent text — to detect structure links even on sites with opaque download URLs (Figshare, Zenodo).
- Action: Injects a native
<button>badge next to each valid link. Clicking the badge stops SPA navigation and sends anopen_viewermessage to the background router viachrome.runtime.sendMessage.
2. The Router (The Background Script)#
- Source:
src/background.ts - Output:
background.js - Environment: Extension service worker (Chrome MV3) or event page (Firefox MV2)
- Role: Acts as a traffic cop and security checkpoint. Validates that incoming
open_viewermessages carry a safe HTTPS URL and a known format string before acting. - Action: Opens a new isolated extension tab pointing to
viewer.html, passing the file URL and format via URL query parameters. Also handles the right-click context menu and dynamic content script registration for newly authorized custom domains.
3. The Privileged Shell (The Viewer)#
- Source:
src/viewer.ts - Output:
viewer.js - Environment: Extension context (
chrome-extension://...), full API access - Role: The security gatekeeper and data acquisition layer. Runs with elevated privileges that the sandbox explicitly does not have.
- Actions:
- Domain gating: Checks whether the requesting domain is a default (GitHub, RCSB, etc.) or an authorized custom domain. Halts with a UI prompt if not.
- Format gating: If format is unknown (right-click path), shows a format selector before proceeding.
- SSRF protection: Validates the URL against a blocklist of private IP ranges and loopback addresses before fetching.
- Fetch: Downloads the structure file using
fetch(), enforces a 25 MB size cap, and performs a Firefox tracking-protection sanity check on the first 150 bytes. - Schema gating: Merges storage settings with
AppConfig.getDefaults()using a strict allowlist of known keys before passing them to the sandbox, preventing rogue storage keys from reaching the MVS builder. - Handoff: Spawns the sandbox
<iframe>and passes the base64 data URI and validated settings viapostMessage, using ane.sourceguard instead of origin matching (sandboxed iframes always reportnullorigin). - Drag & drop: Handles local file loading for offline use.
4. The Engine (The Sandbox)#
- Source:
src/sandbox.ts,src/mvs-builder.ts - Output:
sandbox.js(bundles both modules) - Environment: Sandboxed iframe, origin
null, zero extension API access - Role: Mol* requires
eval()andnew Function()to compile WebGL shaders dynamically. Standard MV3 extension pages forbid this. The sandbox is explicitly declared in the Chrome manifest to allowunsafe-evalin isolation. - Action:
- Immediately posts
SANDBOX_READYto the parent on script load. - Validates the incoming
INIT_MOLSTARmessage (origin, URL scheme, format). - Converts the base64 data URI back into a short
blob:URL to avoid embedding multi-megabyte strings in the MVS JSON tree (anti-lag fix). - Translates the validated settings into a MolViewSpec JSON tree via
MvsBuilder._buildBaseTemplate(). - Renders the 3D scene via the Mol* viewer API.
- Immediately posts
The Build Pipeline#
A fifth layer that exists at development time rather than runtime.
src/*.ts ──→ tsc --noEmit ──→ (type errors only, no files)
src/*.ts ──→ esbuild ──→ dist/*.js (one file per entry point)
dist/*.js
public/ ──→ assemble.js ──→ dist/chrome/ (loadable extension)
manifests/ dist/firefox/- tsc acts as a quality gate: strict type checking with no file output (
noEmit: true). - esbuild bundles each entry point and all its imports into one self-contained JS file. Shared modules (
config,permissions,mvs-builder,types) are inlined — they do not appear as separate script files in the output. - assemble.js routes the correct manifest, the compiled JS, and the static assets into a browser-specific folder that can be loaded directly into the browser.
This design means content.js compiles to a plain classic script with no module syntax (it has no imports), while all other entry points also compile to classic scripts because esbuild resolves all imports at build time. The browser never needs to handle ES module resolution at runtime.
Cross-Browser Compatibility (Manifest Split)#
Chrome (MV3) and Firefox (MV2) have incompatible requirements for background workers, sandboxing, and CSP. Rather than a single manifest with conditional logic, two separate manifests are maintained:
manifests/chrome.json: Usesservice_workerfor the background,actionfor the popup, and thesandboxmanifest key to declare the sandboxed page.manifests/firefox.json: Usesbackground.scripts(event page),browser_actionfor the popup, and a permissivecontent_security_policystring. Firefox does not support thesandboxmanifest key but allowsunsafe-evalglobally for extension pages, achieving the same result.
The TypeScript source is identical for both targets — only the manifest differs. assemble.js copies the right one during the build.