johnandersen777.bsky.social/atlibs-socialweb-computer-requester
socialweb.computer market requester and policy modules for atlibs, with its web pages and lexicons
0
0
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();