Closed feature request

Applets can't reach a back-end from the readme sandbox (connect-src 'self'): two options + PR for B

Ray · 2mo ago ·closed by Colby Knox

Problem: readme applets can't reach a back-end

We're building applets: embeddable, server-backed page components (one button on the page calls an app's back-end, which does real work and returns a result). First three, all working locally: laser-etch (bakes an MPN into a chip STEP via adom-chip-laser), pin-1 picker (CDDB-style verification, 3 reviewers concur), chip-thumbnail (renders the chip's icon family via adom-glb2png).

The blocker is the readme sandbox. The /readme document is served under SANDBOXED_HTML_CSP (lib/router.js:506), and its network posture is:

  • sandbox allow-scripts allow-downloads allow-popups ... with no allow-same-origin -> opaque (null) origin.
  • connect-src 'self' data:

So fetch() is not blocked outright (the comments confirm a readme can already fetch its own repo files). It's pinned to the wiki origin. An applet's back-end lives on a different origin (a service container / proxy host), so the CSP blocks it. That's the whole problem: wrong origin, not "no network." The #53 custom-viewer slot is the same (connect-src 'self' data: blob:, server.js:6258), so there's no existing escape hatch.

A parent postMessage broker does not rescue this on its own: the parent wiki page is also wiki-origin and bound by its own connect-src 'self' ${bucketOrigin} (server.js:304), so it can't reach a foreign service either. It collapses into one of the two options below.

Option A: the wiki brokers the call (wiki-origin applet API)

Expose applet back-ends under the wiki origin, e.g. GET/POST /<owner>/<type>/<slug>/applet/<name>. The readme does a same-origin fetch('./applet/etch', ...), which connect-src 'self' already allows, and the wiki proxies server-side to the real app service.

  • Pros: strongest isolation (the iframe never touches a foreign host); the wiki can auth, rate-limit, cache, and hide the service topology; zero CSP change.
  • Cons: most backend work; the wiki becomes a proxy for every applet's API; needs an app -> service-endpoint registry (probably declared in page.json).

Option B: declared connect-src allowlist (per page)

page.json declares each applet's back-end origin(s); the wiki appends them to the readme document's connect-src. The readme calls the service directly. Our services already send Access-Control-Allow-Origin: * (no credentials), and the null origin is fine with a wildcard.

{ "applet_connect": ["https://chiplaser-xxxx.adom.cloud", "https://pin1-yyyy.adom.cloud"] }
  • Pros: small, self-contained change; app services stay standalone (no wiki proxy). Direct precedent: the main wiki page already widens connect-src 'self' ${bucketOrigin} to grant the S3 media bucket origin (server.js:304), so "add declared origins to connect-src" is an established pattern here.
  • Cons: the readme (publisher JS) can then connect to any declared origin (but those are the publisher's own hosts, that they declared); services must stay up and CORS-open. Slightly weaker than full brokering.
  • Safety: entries are normalized through new URL(x).origin (https only), so a path, query, space, or 'unsafe-*' token can never be injected into the CSP; default (no key) is byte-identical to today (strict, backward-compatible, same as the sections feature).

Recommendation

B to unblock now (matches the bucket-origin precedent, keeps services standalone), A as the eventual model once applets are common and we want the wiki mediating them. A PR for B is up (small, guarded, backward-compatible) for review.

While we're here: glossary term (John approved "applet")

Proposed definition for the definitions glossary:

applet - an embeddable, server-backed component that lives inside a wiki page and calls an app's back-end to do work on request (vs a widget, a pure front-end web component that just renders the data it's given). Users invoke applets; they don't author them. Example: the laser-etch applet POSTs the part's STEP to the adom-chip-laser app and renders the baked, marked GLB it gets back.

1 Reply

Colby Knox · 2mo ago

Option B shipped to prod (PR #279, tag 375afe2). page.json applet_connect widens the readme frame's connect-src to publisher-named PUBLIC https origins, SSRF-guarded to public DNS names only: every IP literal (including obfuscated 0x7f000001 / 2130706433 / IPv4-mapped IPv6), loopback/localhost, private TLDs (.internal/.local/...), non-https, and cloud metadata are rejected; https-only; capped at 8 and deduped. The readme sandbox stays opaque-origin with no allow-same-origin, so a widened connect-src only lets a readme reach hosts the PUBLISHER named, for anonymous credential-less CORS reads (your services already send Access-Control-Allow-Origin: *). Verified on staging and prod; laser-etch, pin-1, and chip-thumbnail can reach their own back-ends now.

Discoverability: the readme.html push hints mention applet_connect, a page.json push with applet_connect returns an APPLET_CONNECT_DECLARED hint echoing which origins were accepted vs dropped, and the adom/wiki skill (readme.html section) documents the contract. Thanks Ray for the two-option writeup and the PR for B.

Option A (the wiki brokers the call under ////applet/) was NOT built. It is the stronger-isolation path AND the only one that can also reach a reader's LOCAL machine (a bridge, an 'open in KiCad' button), which a connect-src allowlist by design cannot do (that goes through the parent). That is the parent-brokered Quick Actions / Web Control track, parked for now. Closing this since Option B is delivered and unblocks the applets; Option A rides that separate track.

Log in to reply.