import { BrowserOAuthClient } from '@atproto/oauth-client-browser' import { Agent } from '@atproto/api' import { Tile, TileMothership } from '@dasl/tile-loader' import { ATTileLoader } from '@dasl/tile-loader/at' import { FedproxyCubit } from './fedproxy-cubit.js' import { pageClientId } from 'atlibs/oauth/index.js' import { createPluginContext } from 'atlibs/plugin-context/index.js' import { loadModule, localModule } from 'atlibs/run-module/index.js' // The host (web tiles' "mothership"): holds the OAuth session and runs the fedproxy wasm module, // with the same FedproxyCubit as web/fedproxy-ssh-public-keys. The UI is a web tile // (https://dasl.ing/tiles.html): a frame on a random origin, no network, resources from its // manifest. It gets cubit state over tp-data and sends { createKey } back; nothing else crosses. // ?module=local|/|at://... wasm module, as in fedproxy-ssh-public-keys // ?tile=local|at://... tile; local serves ./tile/ through the same sandbox // ?load= tile loading server, default load.webtil.es const params = new URLSearchParams(location.search) const MODULE = params.get('module') ?? 'bobvmbuilder.bsky.social/fedproxy' // atlibs publish tile web/fedproxy-webtiles/tile fedproxy-webtiles const TILE = params.get('tile') ?? 'at://did:plc:lpfuqerea3deuoyrn7ojser4/ing.dasl.masl/3mzt2ys2vqc3l' const ctx = createPluginContext({ id: 'fedproxy' }) const MODULES = new URL('./modules/', import.meta.url) // Loads "local" from ./tile/: manifest.json as the MASL, files as its resources. const localTiles = { async load(url, mothership) { if (url !== 'local') return false const manifest = await (await fetch('./tile/manifest.json')).json() return new Tile(mothership, url, manifest, { async resolvePath(path) { const response = await fetch(`./tile${path === '/' ? '/index.html' : path}`) if (!response.ok) return { ok: false, status: response.status } return { ok: true, status: 200, headers: { 'content-type': response.headers.get('content-type') }, body: new Uint8Array(await response.arrayBuffer()) } }, }) }, } // As fedproxy-ssh-public-keys: the loopback client on localhost; deployed, the client `atlibs stage // --target spa` wrote next to the page, else fedproxy.com's own. const SCOPE = 'atproto repo:com.fedproxy.sshPublicKey?action=create' const clientId = pageClientId(SCOPE, { fallback: `https://${location.host}/oauth-client-metadata.json` }) const cubit = new FedproxyCubit({ client: clientId.then((clientId) => BrowserOAuthClient.load({ clientId, handleResolver: 'https://bsky.social' })), makeAgent: (session) => new Agent(session), module: MODULE === 'local' ? localModule(new URL('fedproxy.wasm', MODULES), new URL('fedproxy.json', MODULES)) : loadModule(ctx, MODULE), ctx, }) const mothership = new TileMothership({ loadDomain: params.get('load') ?? 'load.webtil.es' }) mothership.init() mothership.addLoader(localTiles) mothership.addLoader(new ATTileLoader()) const show = (id, on) => (document.getElementById(id).style.display = on ? 'inherit' : 'none') function renderPage(state) { show('loading-spinner', state.session === 'loading' || state.session === 'error') show('loading-error', state.session === 'error') document.getElementById('loading-error').innerText = state.error ?? '' show('login-container', state.session === 'signed-out') show('logout-nav', state.session === 'signed-in') const button = document.getElementById('login-button') if (state.login.busy) button.setAttribute('aria-busy', 'true') else button.removeAttribute('aria-busy') document.getElementById('login-form-error').innerText = state.login.error ?? '' } // What the tile may see: what it renders, no session errors or login state. const forTile = ({ session, handle, keys, create }) => ({ session, handle, keys, create }) async function init() { document.getElementById('login-form').onsubmit = (e) => { e.preventDefault() cubit.login(e.target.username.value) } document.getElementById('logout-nav').onclick = (e) => { e.preventDefault() cubit.logout() } const tile = await mothership.loadTile(TILE) if (!tile) throw new Error(`No tile at ${TILE}`) // tp-data delivers only to handlers the tile has added, so answer every ready, then send on change. let ready = false tile.addEventListener('data-ready', () => { ready = true tile.sendData(forTile(cubit.state)) }) // The tile is untrusted: take only the one request it may make, as strings. The module validates them. tile.addEventListener('data', ({ payload }) => { const { name, service, key } = payload?.createKey ?? {} if ([name, service, key].every((v) => typeof v === 'string')) cubit.createKey({ name, service, key }) }) document.getElementById('tile').append(tile.renderContent(tile.manifest.sizing?.height ?? 900)) cubit.subscribe((state) => { renderPage(state) if (ready) tile.sendData(forTile(state)) }) await cubit.init() } document.addEventListener('DOMContentLoaded', init)