Closed feature request

Render registered ::tag{} widgets inline in the page as first-class citizens, not in a sandboxed iframe

John Lauer · 7d ago ·edited ·closed by Colby Knox

Ask. Render registered ::tag{} widgets inline in the README page, as first-class citizens, instead of in a sandboxed iframe.

The intended concept. A plain README.md renders inline in the page; a readme.html is the sandboxed, untrusted escape hatch. Registered widgets were meant to be on the trusted side of that line. Only Adom employees can add them, through the org-owned, reviewed registry (adom/widgets). That review is what makes them safe, so they shouldn't need the sandbox that untrusted page HTML needs.

What runs today. #152 shipped the tag into "the same sandboxed frame contract the readme already uses": an <iframe sandbox="allow-scripts"> with a CSP sandbox and an opaque origin. So a reviewed, first-party widget is treated exactly like an untrusted page author's HTML. That costs us:

  • Private pages break. The opaque frame is a third-party context, so browsers that block third-party cookies (Safari always, Chrome with that setting) drop the session cookie. ::adom-video cannot load a private page's video in a real browser, though it passes in pup (#228). Any widget that needs a private file hits the same wall.
  • No fullscreen (#226), no modals, and storage throws.
  • Glue code everywhere. Height has to be reported to the parent and can stick at a scrollbar equilibrium. The cross-highlight bus has to relay through postMessage. Keyboard focus starts outside the widget. Nested WebGL renders black.
  • One frame per tag, each with its own document and script load.

Proposal

  1. Registry widgets render inline: the page loads /widget-script/<name>.js once (registry-gated, as today), and the tag becomes the custom element in the README's DOM. Widgets already use Shadow DOM, so styles stay isolated.
  2. The registry stays the gate, and that is the whole trust model: org-owned, edited only by Adom employees, reviewed. Keep the attribute allowlist and escaping. Add the widget-script route to the page's script CSP.
  3. readme.html and declared section HTML stay sandboxed. They are page-author code, and that is where the sandbox belongs.

Effect. #228 (signed media URLs) and #226 (fullscreen) mostly go away for registry widgets. ::adom-video would play private videos with the reader's normal session, with seeking, in every browser. Widgets get simpler (no height reporter, no relay), and private data works without per-file tokens.

Wiki widgets are an Adom-employee domain, permanently. An inline widget script runs with the reader's session, so a registry entry is as trusted as wiki code. That is why only Adom employees build and register widgets. There is no third-party widget tier, now or later. Page authors who want their own code on a page already have readme.html and section HTML, which stay sandboxed.

6 Replies

John Lauer · 7d ago

Current state, measured on prod (served HTML plus a top-level DOM query in a browser):

Content Renders today Intended
README.md inline in the page (inside <div class="readme">, no iframe) inline ✅
readme.html, declared section HTML sandboxed iframe sandboxed iframe ✅
::tag{} registry widgets sandboxed opaque-origin iframe inline, like README.md ❌

Registry widgets are the only piece out of line with the design: they should render inline like the README they sit in. Note that the class name section-frame on the README wrapper is just a styled box, not a frame.

Colby Knox · 7d ago

Shipped on prod (32c7ba1), as an opt-in per registry entry:

  • An entry with "render": "inline", whose script lives in a page owned by the registry's own org (adom), renders as its custom element in the README itself. It has no iframe and runs with the reader's session, so private-page media and data load in every browser. Fullscreen, modals and storage work natively. The page loads /widget-script/<name>.js once, when the widget comes near the viewport ("load": "eager" loads it at once). adomWiki.widget() and viewers.list() drive inline elements directly. A backend widget's registered origin joins connect-src only on pages that render it, looked up by name in the registry. Event-handler, style and class attributes never reach an inline element.
  • Every other entry keeps the sandboxed frame, as do readme.html and section HTML. An inline request that can't be honored (script outside adom's pages) shows up as a registry error.

Why opt-in instead of flipping all 15 at once: all 15 registered scripts already live in adom pages, but some were written for the frame and would act on the wiki page itself once inline:

  • adom-3d and adom-prompt: if (window.self !== window.top) { document.documentElement.style.overflow = 'hidden'; document.body.style.margin = '0'; }. Inline, that's the wiki page, and in a Hydrogen webview the page is framed, so it would stop scrolling.
  • adom-3d: shimBlobUrls() replaces window.Blob and URL.createObjectURL for the frame's CSP, and shimStorage() patches window. Inline, they'd patch the wiki page. It also loads the viewer bundle from an absolute https://wiki.adom.inc/... URL, which is fine on prod but cross-origin on staging.
  • adom-chip-thumbnail / adom-component-lod / adom-table / pinout / footprint-variants / heatsink-via / molecule: they parent.postMessage height and bus messages. That's harmless inline (no frame listens), but it's dead code once a widget moves.
  • adom-video: page-safe already (its overflow change only runs on the /widget/ frame route). Add "render": "inline" to its entry in adom/widgets/widgets.json and it plays private videos with the reader's session, with seeking, in every browser.

Move the rest the same way once each is page-safe (drop the frame-only branches above). Leaving this open until the registry entries move.

Colby Knox · 7d ago

Update: we'll move the widgets ourselves, no action needed from you.

  • adom-video is inline on prod now ("render": "inline" in adom/widgets/widgets.json). It also needed a wiki fix (b905927): the page now inlines a widget's same-origin .json src as its <script type="application/json"> child before the script loads, the same contract the frame route gave it. Checked on adom/anthropic-mhs in a Hydrogen webview: plays and seeks with the reader's session.
  • The other 14 are being made page-safe (gating frame-only code on the /widget/ route, as adom-video already does) and flipped one at a time, each checked on the pages that use it. adom-3d needs the most work: its blob/storage shims and the Babylon bundle would otherwise run against the wiki page itself.

I'll post here as entries move and close this when all 15 are inline.

John Lauer · 7d ago

Inline rendering works well. Private videos play with the reader's session and real fullscreen is available. One follow-up for the inline loader:

Widget script updates reach readers late. The loader appends /widget-script/<name>.js with no version. The route sends cache-control: public, max-age=60, yet a real Chrome kept serving its cached copy (transferSize: 0) well past 60 s after a new adom-video was pushed, across new page loads. Readers kept the old widget, and a fix looked like it had not shipped until the cache entry was force-refreshed.

Ask: version the URL the way app.js?v=… already is, for example /widget-script/<name>.js?v=<registry blob sha>. Then a push changes the URL and the script can be cached long (immutable) instead of 60 s. The registry script field stays query-free as documented; only the URL the page requests carries the version.

Colby Knox · 7d ago

All 15 registry widgets now render inline. The adom/widgets registry has "render": "inline" on every entry.

What changed in the widget scripts (adom/component-widgets widgets/, adom/widgets widgets/stock-widget.js):

  • Frame-only code now runs only on the sandboxed widget frame route (/OWNER/SLUG/widget/NAME). That covers parent postMessage (height, bus relay, chip-thumbnail state), the overflow/margin reset, and adom-3d's Blob/storage shims and head style. It used to key on self !== top, which is also true for the wiki page itself in a Hydrogen webview.
  • adom-3d inline embeds the wiki's own /viewer/3d/... route instead of running Babylon in the page, so the page CSP is unchanged.
  • adom-stock inline: a bare ::adom-stock{} uses the host page's stock route, and requests carry the reader's session.
  • pinout, footprint-variants, heatsink-via and molecule follow the page's color-scheme (dark). Inline, they would otherwise follow the reader's OS setting.
  • adom-symbol-variants needed no change. Inline it uses its JS generator, because the page CSP blocks wasm.

Checked in a local wiki with every widget inline, top level and inside a parent frame: no page-level side effects or CSP errors. Also checked on prod in a Hydrogen webview: john/ksz9031rnx, adom/tmc2262, john/amplh5030s-150mt and ray/ws2812b (the drive contract answers inline).

Leaving this open for the versioned /widget-script/ URL ask above, which is a wiki change.

Colby Knox · 7d ago

Closing: everything here is live on prod.

  • All 15 registry widgets render inline, as custom elements in the README, with the reader's session ("render": "inline" on every entry in adom/widgets). readme.html and section HTML stay sandboxed.
  • Script versioning (your 17:01 ask), prod 9100ab7: the page loads /widget-script/<name>.js?v=<blob sha>, taken from the script's current blob. A matching v is served public, max-age=31536000, immutable; any other v, or none, keeps max-age=60, so an old URL can never pin old code. A push changes the URL, so readers get the new widget on their next page load. Example from adom/tmc2262: adom-verified loads ?v=463f91d41b45.
  • Related on prod: the /viewer/3d (and datasheet, symbol, footprint) routes accept the public type spelling hardware in the URL.

Reopen if anything about inline widgets misbehaves.

Log in to reply.