~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs-fedproxy

fedproxy and ingress modules for atlibs: SSH public keys on your PDS, did:key ingress; pages and web tiles

0
0
main
108 lines · 4.9 KB Raw History
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|<handle>/<name>|at://...  wasm module, as in fedproxy-ssh-public-keys
//   ?tile=local|at://...    tile; local serves ./tile/ through the same sandbox
//   ?load=<host>            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)