johnandersen777.bsky.social/atlibs-socialweb-computer-app
app.socialweb.computer: socialweb.computer in one page, one atlibs composition
0
0
// The page's logic, DOM-free: load the staged composition, run an instance's entry as the user (each
// instance held to its own module's OAuth scope), the webrtc transport's signals, and what the views show.
import { bindParameters, loadComposition, runCompose, staged } from '@publicdomainrelay/atlibs/compose/index.js'
import { policyCall } from '@publicdomainrelay/atlibs/oauth/index.js'
import { hostCall, sessionXrpc } from '@publicdomainrelay/atlibs/run-module/host-call.js'
// a run ends when it is done: vm_create waits minutes for the bid, the receipt, the guest
const BUDGET = { calls: Infinity, events: Infinity, wallMs: 1_800_000 }
/** stage.json (sessions, scopes) and the composition `atlibs stage --target spa` staged beside the page. */
export async function loadApp(ctx, base, fetchImpl = globalThis.fetch) {
const json = async (name) => (await fetchImpl(new URL(name, base))).json()
const [stage, manifest] = await Promise.all([json('stage.json'), json('composition.json')])
const bound = bindParameters(manifest)
const composition = await loadComposition(ctx, bound, { load: staged(base, fetchImpl) })
return { stage, manifest: bound, composition }
}
/** A composition parameter's bound value (bindParameters wrote defaults into instance inputs). */
export const paramDefault = (manifest, key) => manifest.parameters?.find((p) => p.key === key)?.values?.default
/**
* run(instance, entry, input) -> { result, error }: runCompose as the user. Each instance's calls go through
* policyCall (its module's session); signed out, public reads only. vm: { attest, calls, fetchers }: the
* attestation key and the webrtc transport (webrtc.connect, and webrtc: URLs for process-mcp's http.fetch and the
* bidders' market endpoints).
*/
export function runner(ctx, { stage, composition }, session, vm = {}) {
const host = hostCall({ ctx, canHeader: false, fetchers: vm.fetchers })
const calls = session && Object.fromEntries(Object.keys(stage.sessions).map((name) => [name, hostCall({ ctx, xrpc: sessionXrpc(session), canHeader: false, attest: vm.attest, calls: vm.calls, fetchers: vm.fetchers })]))
const callFor = (inst) => (session ? policyCall(stage, inst.module, calls) : host)
return (instance, entry, input = {}, { signal, onEvent } = {}) =>
runCompose(ctx, composition, instance, entry, { ...input, ...(session && { did: session.did }) }, { callFor, signal, onEvent, budget: BUDGET, http: { 'webrtc://*': {} }, fetchers: vm.fetchers })
}
/** As run, but the result or a thrown error. */
export const must = (run) => async (instance, entry, input, options) => {
const ran = await run(instance, entry, input, options)
if (ran.error !== undefined) throw new Error(`${instance} ${entry}: ${ran.error}`)
return ran.result
}
/**
* atlibs-webrtc recordSignals' repo for a session: reads from each repo's own PDS; the offer (a fresh TID
* rkey) and its delete as the user.
*/
export function sessionRepo(ctx, session) {
const read = hostCall({ ctx, canHeader: false })
const xrpc = sessionXrpc(session)
return {
did: session.did,
getRecord: ({ repo, collection, rkey }) => read('com.atproto.repo.getRecord', { params: { repo, collection, rkey }, service: repo }).catch((e) => (/RecordNotFound|not found|404/i.test(e.message) ? null : Promise.reject(e))),
putRecord: ({ collection, rkey, record }) => xrpc('com.atproto.repo.createRecord', { input: { repo: session.did, collection, rkey, record } }),
deleteRecord: ({ collection, rkey }) => xrpc('com.atproto.repo.deleteRecord', { input: { repo: session.did, collection, rkey } }),
}
}
/** WebrtcHost's events and signal as requester (instance machine) runs: the guest's reports, the sealed offer. */
export const vmSignals = (run) => ({
events: async ({ receipt, tag }) => {
const bidder = receipt.bidder ?? receipt.uri.split('/')[2]
return (await run('machine', 'watch', { bidder, receipt: receipt.uri, tags: [tag], max_polls: 1 })).events
},
signal: (input) => run('machine', 'signal', input),
})
/** atirc's channel for a handle or subhandle. */
export const channelOf = (name) => `#${name.slice(0, 63).replace(/[.,\s]/g, '_').toLowerCase()}`
/**
* atlibs-atirc's guest for a VM you keep: its contract receipt (from the shell URL's ref, base64url JSON
* {receipt: {uri, cid}, bidder}) and the requester's watch and signal (vmSignals) for its "atirc" tunnel.
*/
export function guestOf(shell, signals) {
const ref = shell.split('/')[3] ?? ''
const { receipt, bidder } = JSON.parse(atob(ref.replace(/-/g, '+').replace(/_/g, '/')))
if (!receipt?.uri || !receipt?.cid) throw new Error(`no receipt in ${shell.slice(0, 40)}`)
return { receipt: { ...receipt, ...(bidder && { bidder }) }, ...signals }
}
/** A heartbeat's age: live within 5 minutes. */
export function liveness(beatAt, now = Date.now()) {
const t = Date.parse(beatAt ?? '')
if (Number.isNaN(t)) return { live: false, age: 'no heartbeat' }
const s = Math.max(0, Math.round((now - t) / 1000))
const age = s < 120 ? `${s}s ago` : s < 7200 ? `${Math.round(s / 60)}m ago` : s < 172800 ? `${Math.round(s / 3600)}h ago` : `${Math.round(s / 86400)}d ago`
return { live: s < 300, age }
}
/**
* The Running view's bidder groups from market-status rows: yours (you, or an operator confirmed to be you)
* and those of accounts you vouch for (the bidder, or its confirmed operator, vouched).
*/
export function groupBidders(rows, me, vouched) {
const who = (r) => r.operator ?? r.did
const has = (r) => r.offering || r.heartbeat
return {
mine: rows.filter((r) => has(r) && who(r) === me),
vouched: rows.filter((r) => has(r) && who(r) !== me && vouched.has(who(r))),
}
}
/** The DIDs whose bidders the Running view reads: the composition's, yours followed, you, and those you vouch for. */
export const bidderDids = (defaults, known, me, vouched) => [...new Set([...(defaults ?? []), ...known, me, ...vouched].filter((d) => d?.startsWith('did:')))].slice(0, 50)
/** Quick shell commands (the Shell view's buttons). */
export const QUICK = [
['uname', 'uname -a'],
['uptime', 'uptime'],
['disk', 'df -h /'],
['memory', 'free -m'],
['agents', 'systemctl --no-pager --type=service list-units "atirc-*"'],
['agent log', 'journalctl --no-pager -n 40 -u "atirc-agent-*"'],
]