~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
159 lines · 9.4 KB Raw History
import { javascript } from '@codemirror/lang-javascript'
import { BrowserOAuthClient } from '@atproto/oauth-client-browser'
import { basicSetup, EditorView } from 'codemirror'
import { compile } from '@publicdomainrelay/atlibs-code-mode/index.js'
import { createDidKey, didkeyCall, ingressHost } from '@publicdomainrelay/atlibs/didkey/index.js'
import { pageClientId, policyCall, requestScope } from '@publicdomainrelay/atlibs/oauth/index.js'
import { createPluginContext } from '@publicdomainrelay/atlibs/plugin-context/index.js'
import { hostCall, sessionXrpc } from '@publicdomainrelay/atlibs/run-module/host-call.js'
import { localModule, runModule } from '@publicdomainrelay/atlibs/run-module/index.js'

import { boot, DEFAULT_HANDLER, fqdn, plan, toResponse, txtName, txtPlan } from './serve.js'

// The policy `atlibs stage --target spa` wrote beside the page: each module's session (marque's is the
// published com.fedfork.exec.oauth.clientMetadata/marque record: write at.marque.dns zones, nothing else)
// and that session's scope. The ingress has no session: it signs with its own did:key.
const POLICY = await (await fetch(new URL('./stage.json', import.meta.url))).json()
const FORWARD = 'com.fedfork.exec.ingress.request'
const $ = (selector) => document.querySelector(selector)
const show = (error) => ($('#error').textContent = error ? String(error.message ?? error) : '')

const ctx = createPluginContext({ id: 'marque-fedproxy-ingress', settingsSchema: { didkeys: { type: 'secret' } } })
// The wasm and module.json atlibs add fetched from the publisher's PDS (checked against atlibs.lock.json's pins).
const modules = new URL('./modules/', import.meta.url)
const marque = localModule(new URL('marque.wasm', modules), new URL('marque.json', modules))
const ingress = await localModule(new URL('ingress.wasm', modules), new URL('ingress.json', modules))

// On 127.0.0.1 the loopback client; deployed, the client `atlibs stage --target spa` wrote beside the page.
// Signed out (or OAuth unreachable) the tab still serves at its did:key host.
const client = await pageClientId(requestScope(POLICY, ['marque']))
	.then((clientId) => BrowserOAuthClient.load({ clientId, handleResolver: 'https://bsky.social' }))
	.catch(show)
const session = (await client?.init().catch(show))?.session

// The user's OAuth session, held to the marque session's scope.
async function dns(entry, input) {
	const call = policyCall(POLICY, 'marque', { marque: hostCall({ ctx, xrpc: sessionXrpc(session), canHeader: false }) })
	const { result, error } = await runModule(ctx, await marque, entry, { did: session.did, ...input }, call)
	if (error !== undefined) throw new Error(`${entry}: ${error}`)
	return result
}

// --- the handler: compiled once per Apply, run once per request (a fresh sandboxed instance each time)
const editor = new EditorView({ parent: $('#editor'), doc: DEFAULT_HANDLER, extensions: [basicSetup, javascript({ typescript: true })] })
let handler = null

async function apply() {
	$('#apply').disabled = true
	$('#compiled').textContent = 'compiling...'
	const built = await compile(editor.state.doc.toString())
	$('#apply').disabled = false
	if (built.error) return ($('#compiled').textContent = `compile failed, still serving the last handler:\n${built.error}`)
	handler = { name: 'handler', module: built.binary, calls: [] }
	$('#compiled').textContent = `applied at ${new Date().toLocaleTimeString()}`
}
$('#apply').onclick = apply
apply()

const noCalls = async (nsid) => Promise.reject(new Error(`${nsid}: the handler may make no calls`))
async function serve(request) {
	const started = performance.now()
	const response = handler ? toResponse(await runModule(ctx, handler, 'run', { input: request }, noCalls)) : { status: 503, body: 'the handler is compiling', contentType: 'text/plain' }
	const query = new URLSearchParams(request.params ?? {}).toString()
	const line = `${new Date().toISOString().slice(11, 19)} ${request.method} ${request.headers?.host ?? ''}${request.path}${query ? `?${query}` : ''} -> ${response.status} (${Math.round(performance.now() - started)} ms)${request.callerDid ? ` ${request.callerDid}` : ''}`
	$('#log').prepend(Object.assign(document.createElement('li'), { textContent: line }))
	while ($('#log').childElementCount > 200) $('#log').lastChild.remove()
	return response
}

// --- the ingress: registers the did:key with xrpc.fedproxy.com; with `forward`, every request but the
// relay's keepalives is one FORWARD call, answered by serve(). `aliases` are the custom host names the
// relay is asked to map to us; its answer (accepted, rejected) shows beside the custom URL. Load starts it
// with none; Serve restarts it with the domain.
let running = null
function showAliases({ accepted = [], rejected = {} }) {
	const parts = [...accepted.map((h) => `${h}: relay accepted`), ...Object.entries(rejected).map(([h, why]) => `${h}: relay rejected (${why}; it re-reads the TXT within a minute)`)]
	$('#urls .alias').textContent = parts.length ? `(${parts.join('; ')})` : ''
}
async function startIngress(key, aliases) {
	if (running) {
		running.abort.abort()
		await running.done
	}
	const run = { abort: new AbortController() }
	running = run
	$('#urls .status').textContent = '(serving)'
	const relay = didkeyCall(key, hostCall({ ctx, canHeader: false }), { services: ingress.didkey.services, fetch: (u, init) => ctx.http.fetch(u, init), canHeader: false })
	const call = (nsid, request) => (nsid === FORWARD ? serve(request.input) : relay(nsid, request))
	// A server runs until the tab closes: no total calls, events or wall time.
	const onEvent = (_, out) => out.result?.aliases && showAliases(out.result.aliases)
	const budget = { calls: Infinity, events: Infinity, wallMs: Infinity }
	run.done = runModule(ctx, ingress, 'serve', { forward: true, aliases }, call, { signal: run.abort.signal, budget, onEvent }).then((ran) => {
		if (running !== run) return
		running = null
		$('#urls .status').textContent = `(stopped: ${ran.error ?? 'done'}; reload to restart)`
	})
}

// Asks before removing what is at the name: resolves true to go ahead.
function confirmRemove(cname, remove) {
	$('#confirm .cname').textContent = cname
	$('#confirm .remove').replaceChildren(...remove.map((r) => Object.assign(document.createElement('li'), { textContent: `${r.name} ${r.recordType} ${r.value} (ttl ${r.ttl})` })))
	$('#confirm').showModal()
	return new Promise((resolve) => $('#confirm').addEventListener('close', () => resolve($('#confirm').returnValue === 'ok'), { once: true }))
}

async function pointDns(domain, name) {
	const status = $('#urls .dns')
	status.textContent = '(reading the zone...)'
	const { records } = await dns('list_records', { domain })
	const { remove, create } = plan(records, name, host)
	if (remove.length && !(await confirmRemove(`${name} CNAME ${host}`, remove))) return (status.textContent = '(DNS unchanged)')
	for (const r of remove) {
		status.textContent = `(removing ${r.name} ${r.recordType} ${r.value}...)`
		await dns('delete_record', { domain, name: r.name, type: r.recordType, value: r.value })
	}
	if (create) await dns('create_record', { domain, name, type: 'CNAME', value: host, ttl: 60 })
	const txt = txtName(name)
	const { update, create: createTxt } = txtPlan(records, txt, key.did)
	status.textContent = `(writing ${txt} TXT ${key.did}...)`
	if (update) await dns('update_record', { domain, name: txt, type: 'TXT', value: update.value, newValue: key.did, ttl: 60 })
	if (createTxt) await dns('create_record', { domain, name: txt, type: 'TXT', value: key.did, ttl: 60 })
	const wrote = (yes) => (yes ? 'written' : 'already there')
	status.textContent = `(${name} CNAME ${host}, ${wrote(create)}; ${txt} TXT ${key.did}, ${wrote(update || createTxt)}; ttl 60)`
}

// --- signed in or out. The did:key lives in this browser (sealed in the plugin-context secret setting), never
// in the wasm; either way the tab serves at its host from here on.
const hostOf = (did) => ingressHost(did, ingress.didkey.services[0])
const { key, host } = await boot({ $, settings: ctx.settings, createDidKey, hostOf, start: startIngress, signedIn: Boolean(session) })

$('#login').hidden = Boolean(session) || !client
$('#who').hidden = !session
$('#login').onsubmit = async (e) => {
	e.preventDefault()
	await client.signIn(new FormData(e.target).get('handle')).catch(show)
}
$('#logout').onclick = async () => (await client.revoke(session.did).catch(() => undefined), location.reload())

if (session) {
	$('#who span').textContent = `Signed in as ${session.did}`
	const { domains } = await dns('list_domains', {}).catch((error) => (show(error), { domains: [] }))
	$('#serve [name=domain]').replaceChildren(...domains.map((d) => new Option(d.domain, d.domain)))
	if (!domains.length) show('No at.marque.domain records in this account. Register a domain at marque.at.')
	$('#serve').hidden = false
	$('#serve').onsubmit = async (e) => {
		e.preventDefault()
		const form = new FormData(e.target)
		const domain = form.get('domain')
		const name = String(form.get('name')).trim().toLowerCase().replace(/\.$/, '') || '@'
		show()
		Object.assign($('#urls .custom'), { href: `https://${fqdn(name, domain)}/`, textContent: `https://${fqdn(name, domain)}/` })
		$('#urls .custom').parentElement.hidden = false
		e.submitter.disabled = true
		await pointDns(domain, name).catch((error) => (show(error), ($('#urls .dns').textContent = '(DNS write failed)')))
		// After the DNS write, so the relay's check of the TXT has the best chance to see it.
		await startIngress(key, [fqdn(name, domain)])
		e.submitter.disabled = false
	}
}