All-purpose web tiles library
README.md

@dasl/tile-host #

A hosted page that runs one DASL tile, for embedders that cannot run @dasl/tile-loader themselves: a native app's WebView, or a site that would rather drop a tile into an iframe than ship the loader. The package is the page (public/), the Express router that serves it, and the message schema the page and its embedder speak.

https://webtil.es/host/?tile=at://did:plc:…/ing.dasl.masl/3mcjwwoqjqs2v

The tile runs exactly as it would in a browser page: the mothership in the host page frames the shuttle from the loading server on a fresh random origin, the worker there answers the tile's requests from its own resources, and every response carries the policy from @dasl/tile-lexicon. The host page adds nothing to what the tile can reach; it only adds a way to be told what the tile is doing.

The page #

GET /host/?tile=<url>[&load=<loading server>]

  • tile: the tile to run. An at://…/ing.dasl.masl/… record, or the https:// URL of a .tile CAR file (served with Access-Control-Allow-Origin, as a PDS serves its blobs: the page fetches it from its own origin).
  • load: the loading server (load.webtil.es, or a full origin such as http://load.localhost:4321 off https). Defaults to load.<host of the page>, which is right when the page is mounted on the site that runs the loading server.

The tile fills the viewport: the embedder owns the size. The manifest's sizing is reported (below) so the embedder can pick a sensible one.

Messages #

Every message is the JSON of an object with tilesHost: 1, so an embedder can tell them from anything else on the channel. Types and parsers are in @dasl/tile-host/messages, which has no dependencies and no DOM.

Up, from the page, through window.ReactNativeWebView.postMessage when a React Native WebView injected it, and through window.parent.postMessage when the page is framed:

event fields when
manifest url, name, description, sizing the tile is known; always first
load the tile's runtime is up and the tile document is loading
title title the tile set a title
error message the tile could not be loaded (the page shows the message too)
protocol protocol, kind, payload a tiles protocol message from the tile

Down, from the embedder, as a message event on the page (what webView.postMessage() does in react-native-webview, on window on iOS and document on Android) or postMessage from the parent window:

action fields what
send-protocol protocol, kind, payload a tiles-protocol message down to the tile
send-data payload shorthand for tp-data payload

Commands are accepted from the WebView channel (events with no source) and the parent window only. The shuttle and the tile are windows of the page's own making and cannot speak for the embedder.

In a React Native app #

With react-native-webview:

<WebView
  source={{uri: `https://webtil.es/host/?tile=${encodeURIComponent(uri)}`}}
  onMessage={e => {
    const ev = parseTileHostEvent(e.nativeEvent.data)
    if (ev?.event === 'manifest') setHeight(heightFor(ev.sizing))
    if (ev?.event === 'load') setLoading(false)
    if (ev?.event === 'error') showError(ev.message)
  }}
  javaScriptEnabled
  domStorageEnabled
  limitsNavigationsToAppBoundDomains
  onShouldStartLoadWithRequest={req => req.isTopFrame ? req.url.startsWith(HOST_ORIGIN) : true}
  onOpenWindow={e => Linking.openURL(e.nativeEvent.targetUrl)}
  setSupportMultipleWindows
  scrollEnabled={false}
/>

What the embedder has to get right:

  • iOS: App-Bound Domains. WKWebView only provides service workers to app-bound domains, and the shuttle needs one. List the host page's domain (which the loading server's random subdomains fall under, webtil.es for the page above) in the app's WKAppBoundDomains and set limitsNavigationsToAppBoundDomains on the WebView. Without it the tile never loads. Up to ten domains fit the list.
  • Android needs nothing special; domStorageEnabled is prudent.
  • Navigation. Only the host page belongs in the top frame. The shuttle and the tile are subframes; a link a tile opens leaves the sandbox (allow-popups-to-escape-sandbox) and should go to the system browser, which is what onShouldStartLoadWithRequest and onOpenWindow above arrange.
  • WebRTC. The loader spends Chromium's peer-connection budget before a tile loads, which covers Android's WebView. WebKit has no such cap, so on iOS, as in Safari, WebRTC egress from a tile stays open. A host that can block it at the engine level should.
  • Treat events as data. name, title and protocol payloads come from the tile.

In an iframe #

<iframe src="https://webtil.es/host/?tile=at://…" sandbox="allow-scripts allow-same-origin allow-popups allow-forms" title="…"></iframe>

The same events arrive as message events on the parent window, with the iframe as source; commands go to iframe.contentWindow.postMessage(json, origin).

Hosting #

import express from 'express';
import { createTileLoadingRouter } from '@dasl/tile-server';
import { createTileHostRouter } from '@dasl/tile-host';

const app = express();
app.set('trust proxy', 'loopback');
app.use(createTileLoadingRouter('example.site'));   // load.example.site, *.example.site
app.use('/host', createTileHostRouter());           // https://example.site/host/

The router serves public/ with a CSP that confines the page (script-src 'self', connect-src and frame-src open to https: and http: by default, see TileHostOptions to narrow them) and without frame-ancestors or X-Frame-Options, since being embedded is the point. Mount it before any static middleware that sets X-Frame-Options on the site. tileHostDir is the directory, for serving it another way.

Developing #

npm run build compiles the router and the message module with tsc and bundles the page script, loader included, into public/host.js (not committed). npm test runs the message parsers in Node and the page end to end in a real Chromium (Google Chrome, Chromium or Brave in /Applications, or CHROME_PATH; skipped without one): a loading server on *.localhost, the page on 127.0.0.1, a CAR tile on a third origin, and a stand-in for the React Native channel.

Part of DASL tiles #

License #

Apache-2.0