All-purpose web tiles library
README.md

@dasl/tile-server #

The minimal server that makes browser tile loading safe.

Isolating untrusted DASL tiles in the browser needs two things a normal static host doesn't give you: a fresh origin per tile, and a service worker allowed to control that origin. This package is a tiny Express router (and a CLI) that does exactly that — it serves the loader runtime (the shuttle and worker from @dasl/tile-loader) under a hardened set of headers, and redirects each load onto its own random subdomain.

It never sees or stores tile content; it only serves the fixed runtime assets.

Install #

npm install @dasl/tile-server

As a library #

createTileLoadingRouter(baseHost) returns an Express router. Give it your base host; it listens on load.<baseHost> and redirects each request to a random <subdomain>.<baseHost>:

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

const app = express();
app.set('trust proxy', 'loopback'); // needed so req.hostname is right behind a proxy
app.use(createTileLoadingRouter('example.site'));
app.listen(1503);

You are responsible for pointing load.example.site and *.example.site at this app (wildcard DNS + TLS), and for setting loadDomain: 'load.example.site' on the mothership.

As a CLI #

# installed globally, or via npx
tiles-loading-server example.site 8080

Starts an Express app on the given port (default 1503) serving the router for the given base host.

What it enforces #

The policy itself lives in @dasl/tile-lexicon (runtimeHeaders, deadHeaders, tileCSP), so that this server, the loader and every other deployment read the same one and it is edited in one place.

  • Per-tile origin isolation: a request to load.<host> gets a 303 redirect to a fresh 20-character random subdomain (keeping the port, for a desktop app on *.localhost:<port>), so every tile loads on its own origin, cross-origin from your site and from every other tile.
  • The runtime, and nothing else, on tile origins. Under /.well-known/web-tiles/ the shuttle and worker are served with the runtime CSP (default-src 'self', connect-src 'none', form-action 'none', webrtc 'block', a sandbox that grants scripts, forms, modals and popups but not top navigation) and Service-Worker-Allowed: /. Every other path on a tile origin is a dead end: a 404 with default-src 'none'; sandbox; frame-ancestors 'none', so a document from it has an opaque origin the tile cannot script. That is what a tile gets if it unregisters its worker and navigates a frame to the network; it must never get a page of yours.
  • Never on your own host. The base host (and anything isTileOrigin does not claim) passes through untouched, so the runtime, and the worker it registers at scope /, cannot end up on a site origin. Mount the router before your own routes; it only answers for tile origins.
  • Hardening headers on everything it serves: Cross-Origin-Opener-Policy: same-origin, Cross-Origin-Resource-Policy: cross-origin, Origin-Agent-Cluster: ?1, X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer, X-DNS-Prefetch-Control: off, interest-cohort/browsing-topics disabled.

The documents a tile is made of are served by its worker, not by this server: the mothership stamps the tile CSP on each of them. createTileLoadingRouter(baseHost, { isTileOrigin }) lets a deployment that mints its own hostnames say which ones are tile origins.

Part of DASL tiles #

License #

Apache-2.0