Web Tiles #
This repository lives on Tangled: https://tangled.org/robin.berjon.com/dasl-tiles (issues and pull requests there too). The copy on GitHub is archived and no longer updated.
Web Tiles are a DASL technology. They basically make content addressed resource work well in a web context, and make web content available safely and in privacy-friendly ways in non-web contexts such as social, commercial, or agentic protocols.
This library is a toolbox for tiles.
Packages #
This repository is a monorepo. The historical @dasl/tiles package is still
published and keeps its exact interface — every import and CLI command below
continues to work unchanged — but it is now a thin meta-package that re-exports a
set of smaller, independent libraries you can depend on directly:
| Package | What it is | Direct import |
|---|---|---|
@dasl/tile-loader |
Browser architecture for loading tiles (mothership/shuttle/worker) | @dasl/tile-loader, @dasl/tile-loader/at, .../car, .../memory, .../content-scheme, .../webxdc |
@dasl/tile-writer |
Write tiles into CAR-based .tile files |
@dasl/tile-writer |
@dasl/tile-car-reader |
Read tiles from a CAR file | @dasl/tile-car-reader |
@dasl/tile-server |
Minimal Express server for browser tile loading (bin: tiles-loading-server) |
@dasl/tile-server |
@dasl/atile |
CLI to publish/bundle tiles (bin: atile) |
— |
@dasl/tile-lexicon |
AT Protocol lexicon / MASL schema data, and the security policy for tile origins | @dasl/tile-lexicon |
@dasl/containment-tile |
A red-team tile that tries to escape the sandbox and reports what held, with its probe endpoints and host-side checker | @dasl/containment-tile/probe, .../check |
@dasl/tiles |
Backward-compatible meta-package re-exporting all of the above | @dasl/tiles/loader, @dasl/tiles/writer, … |
New code can depend on the small packages; existing code using @dasl/tiles/*
keeps working as-is. The mapping is one-to-one:
Legacy @dasl/tiles import |
Direct equivalent |
|---|---|
@dasl/tiles/loader |
@dasl/tile-loader |
@dasl/tiles/loader/at (and car, memory, content-scheme, webxdc) |
@dasl/tile-loader/at (etc.) |
@dasl/tiles/writer |
@dasl/tile-writer |
@dasl/tiles/car-reader |
@dasl/tile-car-reader |
@dasl/tiles/loading-server |
@dasl/tile-server |
All packages are written in TypeScript and published with compiled JavaScript
plus type declarations. JavaScript consumers import exactly as before; TypeScript
consumers get full type hints automatically (including the shared Masl,
ResourceEntry, and MaslResponse types from @dasl/tile-lexicon). No import
paths change.
Apps in the monorepo #
Blastile, the desktop app for opening, watching and publishing tiles (a Node server in a mollusc window), lives here too and uses the workspace packages directly.
Working in the monorepo #
The repo uses npm workspaces, with each package in a top-level directory (no
packages/ wrapper). Sources live in each package's src/*.ts and compile to
dist/ (.js + .d.ts). Run npm install once at the root to link everything,
npm run build to compile all packages (in dependency order), and npm test to
build then run every package's test suite.
Tile Loader #
In order to load a tile into a web page you need to use the tile loader with a specific loading configuration. It typically looks like this:
import { TileMothership } from '@dasl/tiles/loader';
import { ATTileLoader } from '@dasl/tiles/loader/at';
// AT Tiles!
const at = new ATTileLoader();
const tl = new TileMothership({ loadDomain: 'load.webtiles.bast' });
tl.init();
tl.addLoader(at);
const parent = document.createElement('div');
parent.style.display = 'inline-block';
parent.style.verticalAlign = 'top';
parent.style.padding = '50px';
parent.style.width = '570px';
document.body.append(parent);
const tile = await tl.loadTile(`at://did:plc:izttpdp3l6vss5crelt5kcux/ing.dasl.masl/3mcjwwoqjqs2v`); // Minesweeper
console.warn(`Tile`, tile);
parent.append(await tile.renderCard());
The tile-loading architecture has three levels that all communicate together:
- At the top, the MOTHERSHIP. This has access to things in the real world like fetching from the internet or reading from the file system. It's the interface to tile loading, it gets configured in ways that are appropriate for its context. This is the entry point: you give it a URL and it'll instantiate that tile. To the extent possible, this should contain all the intelligence and all the configurability so that the other components can be deployed in entirely generic ways.
- The mothership instantiates tiles by creating insulated contexts (a protected iframe, an incognito window…) and loading a SHUTTLE in it. The role of the shuttle is to set up a service worker and an iframe to load the root of the tile into. It only exists because you need something to carry a service worker in. The only other thing that it does is (drumroll) shuttle messages back and forth between the worker and the mothership.
- The WORKER is dispatched on a shuttle to handle resource loading for a tile. Apart from allow-listing some paths for itself and the shuttle, it passes all requests up, which the shuttle then hands over to the mothership.
First, you create a mothership using new TileMothership(). That mothership is then
initialised and configured with one or more loaders that support getting tiles
from AT Proto (ATTileLoader), CAR Tiles (CARTileLoader), WebXDC (WebXDCTileLoader), or
memory (MemoryTileLoader). If you add more than one loader, the mothership will try them
each in turn until one returns a tile (most match on URL scheme).
Once the mothership is configured, you call loadTile() on it with a tile URL. And after
the tile has been loaded, you can get a DOM element from either tile.renderCard() (for
the card overview) or tile.renderContent() that will render the actual tile.
Rendering a card only uses tile metadata and so is safe if you wish to render hundreds or even
thoughts. Rendering content is heavier, and may lead to resource contention. When content is
loaded, an intermediary iframe is created by the mothership, known as the shuttle. The
shuttle then sets up a service worker and a nested iframe into which the tile will render.
Due to browser limitations, the shuttle needs to be loaded from a website (see the Tiles Loading
Server section below), but that site doesn't get to see or know anything about the tiles.
TileMothership #
constructor: accepts{ loadDomain: 'load.webtiles.example' }which is the domain from which to hit the tile loading server.init(): must be called once, wires the mothership to make sure that it is ready to operateaddLoader(someTileLoader): adds a specific class that knows how to load a specific kind of tile. You need at least one.await loadTile(url): returns either aTileobject orfalseif no loader matched.
Tile #
This is the tile object produced when a tile is loaded.
renderCard(): renders the card for the tile, returning a DOM element that you can put wherever.renderContent(height?): renders the actual tile, returning a DOM element for you to insert. You can override the tile's height by passing your own.
atile tool #
The atile tool is used to publish tiles on AT proto or to bundle them into .tile files. Most
operations revolve around a tile source contained in a directory, with a manifest, that you want
to publish to AT or output to file.
Usage: atile [options] [command]
Manipulating tiles on the AT protocol
Options:
-V, --version output the version number
-h, --help display help for command
Commands:
login <handle> <appPassword> log a handle into AT so that you can post
logout <handle> log a specific handle out
default-user <handle> set the default handle to use when unspecified
list-users list all logged in handles you have
publish [options] <dir> publish a tile to the Atmosphere
delete [options] <dirOrATURL> delete a tile from the Atmosphere
bundle <dir> <out.tile> bundle a tile into a .tile file
help [command] display help for command
You can always atile bundle by giving it a source directory (with resources and manifest)
and an output file to save to, without logging into AT. This will give you a self-contained
tile file that can then be read from a tile reader.
In order to publish, you must be logged in. For that, you use the atile login
command, with an app password. Passwords are stored using keytar, which should
map to a safe option on your platform. Every time that you log in with a different
handle, it gets saved separately (and the latest one becomes the default handle).
You can then use a specific handle for other commands with the --user option.
You can then use atile logout to log a specific handle out, atile list-users
to list all the logged in handles, and atile default user to change the default
one.
To publish, use atile publish and give it the path to a directory that contains
a manifest.json, an index.html, and all the other files in the tile. It will
automatically populate the resources entry of the manifest with the correct
CID and guess the media type. If there is already an entry for that path in
resources, it will keep it and just update the CID. This makes it possible to
set your own HTTP headers there. You can change the handle you use with --user
and ask that atile saves the AT URL of the published tile with -s. In the latter
case, it will automatically reuse that URL whenever it's given the same path to
publish from.
To delete a tile, use atile delete. It acccepts either the AT URL of the tile,
or if you've saved the published URL with -s it will find it given the directory.
Tiles Loading Server #
Due to limitations in browser technology, supporting tiles in browsers requires a very minimalistic server set up so as to isolate tiles in their own origin, while maintaining the ability to run a service worker (otherwise it's either one or the other).
The tiles loader is expected to load from https://load.server/.well-known/web-tiles/.
The loading server handles that and redirects to
https://random-subdomain.server/.well-known/web-tiles/ with the right headers.
That's it!
You can use it either as a library or as a CLI tool.
As a library, there's a function that takes a domain and returns an Express
router configured correctly. Note that if you provide example.site for the
base host, it will set itself up to listen on load.example.site and redirect
to twenty-random-letters.example.site.
import express from 'express';
import { createTileLoadingRouter } from '@dasl/tiles/loading-server';
const app = express();
app.set('trust proxy', 'loopback'); // need this
app.use(createTileLoadingRouter('example.site'));
Or just install the global tool, or run it with npx:
tiles-loading-server example.site 8080
Writing #
import TileWriter from '@dasl/tiles/writer';
const tw = new TileWriter({
name: `My Cat`,
description: `This basic tile is a picture of my cat.`,
});
tw.addResource('/', { 'content-type': 'text/html' }, { path: '/path/to/index.html' });
tw.addResource('/img/kitsu.jpg', { 'content-type': 'image/jpeg' }, { path: '/path/to/rick.jpg' });
await tw.write('/path/to/output.tile');
Reading from a CAR #
The CAR reader indexes the whole tile and resolves paths to return the headers and the means of creating a stream that reads just that segment of the CAR. Note that the path resolution will correctly ignore query strings, hashes, etc.
import CarTileReader from '@dasl/tiles/car-reader';
const ctr = new CarTileReader('/path/to/a/car-based.tile');
await ctr.open();
const nf = ctr.resolvePath('/not/exists');
// nf.ok = false, nf.status = 404
const root = ctr.resolvePath('/');
// root.ok = true, root.status = 200
sendHeaders(root.headers); // { 'content-type': 'text/html' }
root.createReadStream().pipe(res); // sends the content