~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
86 lines · 4.6 KB Raw History
// The page's pure parts: the did:key and what load shows, which DNS records a CNAME replaces, and a handler
// run as the relay's response.

// The ingress did:key: its JWK sealed in the plugin-context secret setting `didkeys` (slot `ingress`), made
// on first load. One tab at a time reads and writes the map (Web Locks). Storage that fails (a private
// window, blocked site data) gives a key for this load only: kept false.
export async function ingressKey(settings, createDidKey, locks = globalThis.navigator?.locks) {
	const locked = (fn) => (locks ? locks.request('atlibs-didkeys', fn) : fn())
	try {
		return await locked(async () => {
			const keys = (await settings.get('didkeys')) ?? {}
			const key = await createDidKey(keys.ingress)
			if (!keys.ingress) await settings.set('didkeys', { ...keys, ingress: key.jwk })
			return { key, kept: true }
		})
	} catch {
		return { key: await createDidKey(), kept: false }
	}
}

// Load, signed in or out: the tab serves at its did:key host (start(key, aliases) runs the ingress), and that URL
// shows. The domain form (CNAME + _xrpc TXT through Marque) needs a session: signed out it is disabled.
//   $         document.querySelector
//   hostOf    did -> the relay's host name for it
export async function boot({ $, settings, createDidKey, hostOf, start, signedIn, locks }) {
	const { key, kept } = await ingressKey(settings, createDidKey, locks)
	const host = hostOf(key.did)
	Object.assign($('#urls .ingress'), { href: `https://${host}/`, textContent: `https://${host}/` })
	$('#urls .kept').textContent = kept ? '' : '(this browser keeps no site data: a new did:key, and URL, each load)'
	$('#urls').hidden = false
	$('#serve fieldset').disabled = !signedIn
	$('#serve .signin').hidden = signedIn
	start(key, [])
	return { key, host }
}

// The handler the code view starts with. run.input is the forwarded request.
export const DEFAULT_HANDLER = `// One run per request to the ingress. run.input is the request:
//   { method, path, params, headers, body, callerDid }
// Finish with a string (200, text/plain) or { status, body, contentType, headers }, e.g.
//   run.finish(Json.object()
//     .set("contentType", Json.string("text/html"))
//     .set("body", Json.string("<h1>" + run.input.get("path").str + "</h1>")));
export function main(run: Run): void {
  run.finish(Json.string("hello world"));
}
`

export const fqdn = (name, domain) => (name === '@' ? domain : `${name}.${domain}`)

// A CNAME is alone at its name (RFC 1034 3.6.2): every other record there goes. `create` is false when
// the CNAME is already in the zone. Names and hostnames compare as Marque stores them (lowercase, no dot).
export function plan(records, name, target) {
	const here = records.filter((r) => r.name === name)
	const same = (r) => r.recordType === 'CNAME' && r.value === target
	return { remove: here.filter((r) => !same(r)), create: !here.some(same) }
}

// The relay maps a custom host by the TXT at _xrpc.<host>, whose value is the ingress did:key (Marque
// flattens the CNAME, so the relay cannot follow it). A did:key TXT there that is not ours (an older key) is
// updated; other TXT records at the name stay.
export const txtName = (name) => (name === '@' ? '_xrpc' : `_xrpc.${name}`)

export function txtPlan(records, name, did) {
	const here = records.filter((r) => r.name === name && r.recordType === 'TXT')
	if (here.some((r) => r.value === did)) return { update: null, create: false }
	const stale = here.find((r) => r.value.startsWith('did:key:')) ?? null
	return { update: stale, create: !stale }
}

// runModule's { result, error } for the handler -> the forward call's { status, body, contentType }.
export function toResponse({ result, error }) {
	const text = (status, body) => ({ status, body, contentType: 'text/plain; charset=utf-8' })
	if (error !== undefined) return text(500, `handler failed: ${error}`)
	const value = result?.value ?? null
	if (value === null) return text(500, 'the handler did not call run.finish')
	if (typeof value === 'string') return text(200, value)
	if (typeof value === 'object' && !Array.isArray(value) && ('status' in value || 'body' in value || 'contentType' in value || 'headers' in value)) {
		const body = value.body ?? ''
		const response = { status: value.status ?? 200, body, contentType: value.contentType ?? (typeof body === 'string' ? 'text/plain; charset=utf-8' : 'application/json') }
		// name -> string, or -> [string] for Set-Cookie; the ingress module and the relay drop the rest
		if (value.headers && typeof value.headers === 'object' && !Array.isArray(value.headers)) response.headers = value.headers
		return response
	}
	return { status: 200, body: value, contentType: 'application/json' }
}