@dasl/tile-loader #
Load DASL tiles into a web page — safely.
A tile is untrusted web content. This package renders it inside a sandboxed,
cross-origin iframe served by a @dasl/tile-server, where a
service worker answers the tile's resource requests from its manifest. The tile
can only talk to your page through postMessage; it can't reach your DOM,
your storage, or the network beyond what the loading server's CSP allows.
Browser only — it uses the DOM,
fetch, service workers, and iframes.
Install #
npm install @dasl/tile-loader
Usage #
import { TileMothership } from '@dasl/tile-loader';
import { ATTileLoader } from '@dasl/tile-loader/at';
const tl = new TileMothership({ loadDomain: 'load.webtiles.example' });
tl.init();
tl.addLoader(new ATTileLoader());
const tile = await tl.loadTile('at://did:plc:…/ing.dasl.masl/3mcjwwoqjqs2v');
document.body.append(await tile.renderCard()); // lightweight, metadata-only
// or, to run the tile:
document.body.append(tile.renderContent()); // sandboxed iframe
loadDomain must point at a running @dasl/tile-server — that's
the origin the sandboxed content is loaded from.
Architecture #
Three cooperating layers, all talking over postMessage:
- Mothership (this module, in your page) — the entry point. You configure it
with loaders, hand it a URL, and it produces a
Tile. It holds all the privileged capabilities (fetching, file access via loaders); the other layers stay generic. - Shuttle (served by the tile server) — a sandboxed iframe on a fresh random origin. It installs the service worker and hosts the inner iframe the tile renders into, shuttling messages between the worker and the mothership.
- Worker (a service worker, served by the tile server) — intercepts the inner iframe's requests and asks the mothership to resolve each path, so the tile only ever sees the resources its manifest declares.
API #
new TileMothership(conf?) #
-
conf.loadDomain: the host of your tile server (defaults toload.webtil.es); give a full origin (http://load.localhost:4321) when it is not https on 443. -
conf.webrtcGuard:'exhaust'(default) has the shuttle spend Chromium's per-process WebRTC budget before the tile loads (see Security model);'none'skips it, for hosts that block WebRTC themselves (an Electron app with the policy switches set, say). -
init()— wire up the message plumbing. Call once. -
addLoader(loader)/removeLoader(loader)— register a loader (below). Loaders are tried in order until one returns a tile. -
await loadTile(url)— returns aTile, orfalseif no loader matched. -
hardenResponse(headers)— the headers every tile document ships with: the resource's own plus the policy from@dasl/tile-lexicon. Override to add more, never to remove.
Tile #
await renderCard(options?)— a DOM element showing the tile's card (metadata only; safe to render many at once).options.contentHeightpresets the height used when the card is clicked to expand.renderContent(height?)— the sandboxed iframe that runs the tile.sendData(payload)/sendProtocol(protocol, kind, payload)— talk to the rendered tile over tiles protocols. What the tile sends up arrives asprotocolevents (ev.protocol,ev.kind,ev.payload) and, for tp-data, asdata/data-readyevents (ev.payload).url,manifest— the tile's URL and MASL metadata.
Loaders #
Each is a subpath export; add the ones you need:
| Import | Loads | Matches |
|---|---|---|
@dasl/tile-loader/at — ATTileLoader |
tiles from AT Protocol records | at://… |
@dasl/tile-loader/car — CARTileLoader |
CAR .tile files over HTTP |
http(s)://… |
@dasl/tile-loader/webxdc — WebXDCTileLoader |
WebXDC .xdc bundles over HTTP |
http(s)://… |
@dasl/tile-loader/memory — MemoryTileLoader |
in-memory manifests (testing/experiments) | memory://<id> |
@dasl/tile-loader/content-scheme exports ContentSchemeTileLoader, the base class
the HTTP-fetching loaders extend. The TileLoader and TilePathLoader interfaces
are exported from the package root for writing your own.
Security model #
A tile's origin is a dead end, by three layers that all read one policy
(@dasl/tile-lexicon):
- The worker answers everything. Every request a tile document makes goes
to its service worker, which asks the mothership to resolve the path against
the tile's manifest and answers with that, or a 404.
fetch, XHR, scripts, styles, images, fonts, frames: all tile content, never the network. - The document refuses what the worker cannot see. The mothership stamps
every response it resolves with the tile CSP:
connect-srcfor WebSocket, EventSource, beacons and WebTransport;frame-srcandform-actionfor navigations to other origins;webrtc 'block'for the day browsers implement it; thesandboxflags;X-DNS-Prefetch-Control: off. A CSP the tile declares stays and is enforced alongside. The worker's own improvised responses, and the server's 404s, carry adefault-src 'none'; sandboxdead end. - WebRTC is spent before the tile loads. No browser implements CSP's
webrtcdirective yet, and ICE is not a fetch. Chromium caps a renderer process at 500RTCPeerConnections and only frees a slot when the object is destroyed, so the shuttle creates and holds 500 closed ones (a few hundred milliseconds; the trick Delta Chat uses for webxdc). The tile, and any frame or popup it opens in that process, gets "Cannot create so many PeerConnections". Firefox and Safari have no such cap: there, WebRTC egress (STUN/TURN hostnames, TURN credentials) stays open from a browser page, and a host that can block it at the engine level should (mollusc'snetwork: "loopback", say).
The mothership's side of the contract is to only ever answer a worker's
resolve-path with { status, headers, body }, and to act on protocol
messages only from windows it created. @dasl/containment-tile
is the red-team tile that checks all of this end to end, in a real browser.
Part of DASL tiles #
@dasl/tiles— meta-package re-exporting the whole toolkit@dasl/tile-server— the browser tile-loading server@dasl/tile-writer— write tiles into.tilefiles@dasl/tile-car-reader— read tiles from a CAR file@dasl/tile-lexicon— MASL schema and shared types@dasl/atile— the tile-publishing CLI
License #
Apache-2.0