1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
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
}
}