Closed general

readme.html sandbox: lint for the 5 silent traps (storage, 4MB cap, blob:, relative paths, height) + inject the storage shim platform-side

John Lauer · 2mo ago ·closed by Colby Knox

Problem

readme.html is now the way rich pages are built, and the sandbox it renders in breaks a predictable, finite set of things. Every one of them fails silently: no console error, no lint warning, and a page that looks fine to the author locally renders blank or broken once published.

The knowledge exists — issues #47, #60, #134, #135 each cover a piece — but it is spread across closed issues and one project's skillpack, so each new author rediscovers it from zero. Concretely: every adom-tsci board page currently ships a permanently blank 3D tab, and I burned over an hour today re-deriving the cause of #60 before I found #60.

This is about to get worse, not better: component pages are moving to readme.html with embedded viewers and applets, so the same five traps are about to be hit by people who are not debugging 3D engines.

Ask: repo push / pkg lint already emit good advisory hints (README_HTML_REGISTERED, HERO_ASPECT, broken_images). Extend that with checks for the sandbox traps. They are all cheap static checks on files the linter already has in hand.

Versions: adom-wiki 1.7.4 · adom-tsci 1.4.24 · evidence from adom/evan-led-nameplate.

Proposed checks

1. Unguarded storage access — highest value. Grep readme.html and any first-party .js it ships for localStorage/sessionStorage not wrapped in try/catch. In a sandboxed null-origin document the property read itself throws SecurityError, and a typeof window !== "undefined" guard does NOT help — window exists, the origin is null. If that throw lands in an un-awaited async init, everything after it is silently abandoned.

Measured, published page, cold profile: lights: 0, environmentTexture: false, activeRenderLoops: 0, getCamera(): null → after an in-memory shim, unchanged otherwise: lights: ["light","spotLight"], environmentTexture: true, activeRenderLoops: 1.

SANDBOX_STORAGE: readme.html or js/foo.js reads localStorage without a try/catch. A sandboxed readme has a null origin and the read throws SecurityError. Shim both storages before your bundle loads.

2. Files over the render cap. render/ 413s over ~4 MB with {"error":"File too large to render"}. A <script src> cannot surface that; it just fails. Flag any pushed file over the cap that the readme references.

RENDER_SIZE_CAP: viewer/js/app.min.js is 13.0 MB; /render/ refuses over ~4 MB. Split it and reassemble client-side.

3. blob: usage against the CSP. img-src/media-src/script-src allow data: and https: but not blob:. Any URL.createObjectURL in shipped JS is a likely runtime CSP refusal. Bites harder than it sounds: Babylon's glTF loader blobs embedded GLB textures, which throws and fails the whole model load, and the obvious fix (externalise the images to sibling .png files) does not work either, because a relative URI still gets blobbed in a null-origin document. data: URIs are the only accepted form.

4. Relative asset paths in readme.html. Anything not absolute under https://wiki.adom.inc/<owner>/<slug>/render/… will not resolve. Trivially detectable, currently discovered by seeing broken images on a published page.

5. Missing readme-height postMessage. If readme.html contains no postMessage({type:'readme-height'…}), everything below the fold is unreachable. The hint text already explains this beautifully — but only after the push succeeds. Make it a pre-flight check.

6. @font-face in readme.html. adom.inc font files fail CORS from a null origin every load. One-line grep.

7. .glb orientation on hardware pages. The wiki's native 3D viewer is Z-up; glTF is Y-up by spec and that is what tscircuit emits. Publishing an unrotated board.glb stands the board on edge like a wall, which looks like a modelling mistake rather than a convention mismatch. The thin axis of a board-like mesh is detectable from the bounding box.

GLB_ORIENTATION: board.glb has its thinnest axis on Y (Y-up). The wiki viewer is Z-up and will render it edge-on. Bake a +90° X rotation.

Two non-lint asks

A. Inject the storage shim into the readme frame yourselves. The wiki controls the frame wrapper. A no-op storage shim injected ahead of author content would fix every existing broken page at once, with zero author effort, and remove trap #1 permanently. This seems strictly better than asking every author to remember it.

B. Document the sandbox contract in one place. The README_HTML_REGISTERED hint is genuinely excellent and covers height, connect-src and the lightbox. Adding storage, the size cap, blob:, absolute render/ paths and @font-face to that same hint would put the whole contract in front of the author at exactly the right moment.

Happy to help write the check implementations or test them against adom/evan-led-nameplate, which now exercises every one of these.

1 Reply

Colby Knox · 2mo ago

Both non-lint asks and six of the seven checks are live on prod; the seventh is closed by a stronger mechanism than a lint.

Ask A, the storage shim: shipped first, because you were right that it is strictly better. The server injects an in-memory storage shim as the FIRST script of every sandboxed HTML document, on both serving paths (the /readme render and the render/ route that readmes iframe their viewers from). Verified end to end on a real browser against a previously-broken page: the #60 camera-mode read no longer throws, the viewer global initializes, getCamera() is non-null, and the walkthrough narrates. Every existing page got this with zero author action, which is why check 1 ships as a documented guarantee instead of a lint: SANDBOX_STORAGE would now be warning about a closed trap.

The lint, checks 2-7, all live at push time (advisory, never blocking, each pinned positive AND negative so the channel does not over-fire):

  • RENDER_SIZE_CAP: any pushed file over the 4 MB render cap on a page with a readme.html, including when viewer assets arrive in a later push than the readme.
  • SANDBOX_BLOB: URL.createObjectURL in readme.html or shipped js, with your Babylon embedded-texture consequence in the message.
  • SANDBOX_RELATIVE_PATHS: bare-relative src/href in readme.html, offenders named; root-absolute, https:, data:, and fragments pass.
  • README_HEIGHT_MISSING: the pre-flight you asked for.
  • SANDBOX_FONT_FACE: remote @font-face; data: fonts pass.
  • GLB_ORIENTATION: on hardware pages, a board-like .glb whose thin axis is Y (bounding box from the GLB's own accessor min/max), with the +90 X rotation remedy.

Ask B: the README_HTML_REGISTERED hint now carries the whole sandbox contract in one paragraph: the storage guarantee, the size cap, blob:, absolute render/ paths, and fonts, alongside the existing height/connect-src/lightbox text. All new codes are registered in docs/hint-codes.md.

Verified on staging by pushing a deliberately trap-laden fixture (relative img + remote font + createObjectURL + no height post + a Y-up board.glb): all five applicable hints fired on one push.

On evan-led-nameplate and the tsci fleet: the shim fixes the storage half everywhere, and I confirmed on alex-nameplate that what remains is page-side (a <script src> to repo js, which the MIME downgrade blocks by design and the docs say to inline, plus a failing cubemap load). Those authors now get the exact hints above on their next push.

Log in to reply.