~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs-socialweb-computer-requester

socialweb.computer market requester and policy modules for atlibs, with its web pages and lexicons

0
0
126 lines · 6.4 KB Raw History
import { BrowserOAuthClient } from '@atproto/oauth-client-browser'
import { Agent } from '@atproto/api'
import { secp256k1 } from '@noble/curves/secp256k1.js'
import * as age from 'age-encryption'
import * as devTunnelsSsh from '@microsoft/dev-tunnels-ssh'
import { Buffer } from 'buffer'

// The compute and atirc pages' components and cubits, imported through the webrtc/ and atirc/
// symlinks; this page adds the shared session and the agent features.
import './webrtc/requester-actions.js'
import './webrtc/webrtc-connect.js'
import './webrtc/webrtc-terminal.js'
import './atirc/atirc-post-form.js'
import './swc-sessions.js'
import './swc-channel.js'
import { WebrtcCubit } from './webrtc/webrtc-cubit.js'
import { AtircCubit } from './atirc/atirc-cubit.js'
import { SocialwebCubit, sharedSessions } from './socialweb-cubit.js'
import { pageClientId, requestScope } from 'atlibs/oauth/index.js'
import { createPluginContext } from 'atlibs/plugin-context/index.js'
import { localModule } from 'atlibs/run-module/index.js'

// Wiring only: user events go to the cubits, cubit state goes to the views.
const T = 'com.publicdomainrelay.temp';
const ATIRC = 'did:web:irc.fedfork.com%23atirc';
const POLL_MS = 5000;
const WRITES = ['compute.vm', 'market.rfp', 'market.accept', 'market.event', 'compute.events.vm.delete',
	'compute.events.guest.signalWebRTC', 'compute.events.guest.publicKey'];
const RPCS = ['market.submitRfp', 'market.submitAccept', 'market.submitEvent', 'requester.associateConfirm'];

// Module sessions are scope subsets of the page's one OAuth session; agents needs both.
const POLICY = {
	sessions: {
		requester: {
			label: 'compute (socialweb.computer requester)',
			scope: [`repo:${T}.badgeBlueKeys?action=create`, ...WRITES.map((c) => `repo:${T}.${c}?action=create&action=delete`),
				...RPCS.map((m) => `rpc:${T}.${m}?aud=*`)].join(' '),
		},
		atirc: { label: 'atirc (irc.fedfork.com)', scope: `rpc:chat.atirc.list?aud=${ATIRC} rpc:chat.atirc.post?aud=${ATIRC}` },
	},
	features: { agents: { requires: ['requester', 'atirc'] } },
};

const files = (dir, name) => {
	const base = new URL(`./${dir}/modules/`, import.meta.url);
	return localModule(new URL(`${name}.wasm`, base), new URL(`${name}.json`, base));
};
const requester = files('webrtc', 'requester');
const atircModule = files('atirc', 'atirc');

// On localhost the loopback client; deployed, the composite client `atlibs stage --target spa --name
// socialweb-computer-ui requester atirc` wrote next to the page.
const client = pageClientId(requestScope(POLICY, Object.keys(POLICY.sessions))).then((clientId) => BrowserOAuthClient.load({ clientId, handleResolver: 'https://bsky.social' }));
const sessions = sharedSessions(client, POLICY);
const makeAgent = (session) => new Agent(session);
const compute = new WebrtcCubit({
	client: Promise.resolve(sessions.client('requester')),
	makeAgent,
	secp256k1,
	age,
	makePeer: (config) => new RTCPeerConnection(config),
	ssh: { ...devTunnelsSsh, Buffer },
	module: requester,
	// the attestation key (bind_key once per browser origin) lives in this secret setting
	// managed_records: the rkey ledger, so the requester's cleanup may delete the records it created here
	ctx: createPluginContext({ id: 'socialweb-computer-ui', storage: { managed_records: {} }, settingsSchema: { attestKeys: { type: 'secret' } } }),
});
const atirc = new AtircCubit({ client: Promise.resolve(sessions.client('atirc')), makeAgent, module: atircModule, ctx: createPluginContext({ id: 'atirc' }) });
const cubit = new SocialwebCubit({ sessions, policy: POLICY, atirc, compute });
Object.assign(globalThis, { swcCubit: cubit, webrtcCubit: compute, atircCubit: atirc });

const show = (id, on) => (document.getElementById(id).style.display = on ? "block" : "none");

async function init() {
	const names = ["swc-sessions", "swc-channel", "atirc-post-form", "requester-actions", "webrtc-connect", "webrtc-terminal"];
	await Promise.all(names.map((name) => customElements.whenDefined(name)));
	const [sessionsView, channel, post, actions, connect, terminal] = names.map((name) => document.querySelector(name));
	sessionsView.setPolicy(POLICY);
	actions.setActions((await requester).actions);

	const on = (type, fn) => document.addEventListener(type, (e) => fn(e.detail ?? {}));
	on("login", ({ username, names }) => cubit.login(username, names));
	on("set-sessions", ({ names }) => cubit.setSessions(names));
	on("announce", () => cubit.announce());
	on("allow-agent-requests", ({ allow }) => cubit.allowAgentRequests(allow));
	on("approve", ({ seq }) => cubit.approve(seq));
	on("set-channel", ({ channel }) => atirc.setChannel(channel.trim()));
	on("post-line", ({ text }) => atirc.post(text));
	on("run-entry", ({ entry, input }) => compute.run(entry, input).catch(() => undefined));
	on("find-guest", ({ receipt, tag }) => compute.find(receipt, tag));
	on("create-offer", () => compute.offer());
	on("wait-answer", () => compute.answer());
	on("send-line", ({ line }) => compute.send(line));
	on("ssh-key", () => compute.sshKey());
	document.getElementById("logout-nav").onclick = (e) => {
		e.preventDefault();
		cubit.logout();
	};

	// Every view gets all three states; each renders its slice.
	let previous;
	const render = () => {
		const state = { ui: cubit.state, atirc: atirc.state, compute: compute.state };
		show("loading", state.ui.session === "loading");
		show("loading-error", state.ui.session === "error");
		document.getElementById("loading-error").innerText = state.ui.error ?? "";
		show("logout-nav", state.ui.session === "signed-in");
		document.getElementById("whoami").innerText = state.ui.did ? `${atirc.state.handle ?? compute.state.handle ?? ""} ${state.ui.did}` : "";
		show("compute-off", state.ui.session === "signed-in" && !state.ui.granted.includes("requester"));
		show("atirc-off", state.ui.session === "signed-in" && !state.ui.granted.includes("atirc"));
		sessionsView.render(state, previous);
		channel.render(state, previous);
		post.render(state.atirc, previous?.atirc);
		actions.render(state.compute, previous?.compute);
		connect.render(state.compute, previous?.compute);
		terminal.render(state.compute, previous?.compute);
		previous = state;
	};
	for (const c of [cubit, atirc, compute]) c.subscribe(render);
	await cubit.init();
	setInterval(() => atirc.state.session === "signed-in" && atirc.refresh(), POLL_MS);
}

// Modules run after parsing, and the imported components' top-level awaits finish after
// DOMContentLoaded, so start now.
init();