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 } }