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' import { WebrtcCubit } from './webrtc-cubit.js' import { createPluginContext } from 'atlibs/plugin-context/index.js' import { loadModule, localModule } from 'atlibs/run-module/index.js' import { pageClientId } from 'atlibs/oauth/index.js' // Wiring only: user events go to the cubit, cubit state goes to the views. // The requester wasm module plans every market call; ?module=/ (or at://...) runs a published record. const MODULE = new URLSearchParams(location.search).get('module') ?? 'local'; const MODULES = new URL('./modules/', import.meta.url); const T = 'com.publicdomainrelay.temp'; // The attestation key lives in this secret setting (AES-GCM, key non-extractable in IndexedDB). // managed_records is run-module's rkey ledger: without it the module may not delete what it // created (contract's cleanup was refused live, 2026-11-09). const ctx = createPluginContext({ id: 'webrtc', storage: { managed_records: {} }, settingsSchema: { attestKeys: { type: 'secret' } } }); const module = MODULE === 'local' ? localModule(new URL('requester.wasm', MODULES), new URL('requester.json', MODULES)) : loadModule(ctx, MODULE); // Create (and delete, for cleanup) on the requester's collections, service-auth for the bidder calls, and // WebRTC offers to a bidder's ingress (its offering's webrtc endpoint). // pageClientId: the loopback client on localhost, ./stage.json's clientId when deployed. 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']; const SCOPE = ['atproto', `repo:${T}.badgeBlueKeys?action=create`, ...WRITES.map((c) => `repo:${T}.${c}?action=create&action=delete`), ...RPCS.map((m) => `rpc:${T}.${m}?aud=*`), 'repo:computer.socialweb.ingress.offer?action=create&action=delete'].join(' '); const cubit = new WebrtcCubit({ client: pageClientId(SCOPE).then((clientId) => BrowserOAuthClient.load({ clientId, handleResolver: 'https://bsky.social' })), makeAgent: (session) => new Agent(session), secp256k1, age, makePeer: (config) => new RTCPeerConnection(config), ssh: { ...devTunnelsSsh, Buffer }, module, ctx, }); globalThis.webrtcCubit = cubit; const show = (id, on) => (document.getElementById(id).style.display = on ? "inherit" : "none"); function renderPage(state) { show("loading-error", state.session === "error"); document.getElementById("loading-error").innerText = state.error ?? ""; show("login-container", state.session === "signed-out"); show("logout-nav", state.session === "signed-in"); document.getElementById("whoami").innerText = state.handle ? `${state.handle} (${state.did})` : ""; document.getElementById("login-button").toggleAttribute("aria-busy", state.login.busy); document.getElementById("login-form-error").innerText = state.login.error ?? ""; } async function init() { const names = ["webrtc-connect", "webrtc-terminal", "requester-actions"]; await Promise.all(names.map((name) => customElements.whenDefined(name))); const [connect, terminal, actions] = names.map((name) => document.querySelector(name)); actions.setActions((await module).actions); document.getElementById("login-form").onsubmit = (e) => { e.preventDefault(); cubit.login(e.target.username.value); }; document.getElementById("logout-nav").onclick = (e) => { e.preventDefault(); cubit.logout(); }; document.addEventListener("run-entry", (e) => cubit.run(e.detail.entry, e.detail.input).catch(() => undefined)); document.addEventListener("find-guest", (e) => cubit.find(e.detail.receipt, e.detail.tag)); document.addEventListener("create-offer", () => cubit.offer()); document.addEventListener("wait-answer", () => cubit.answer()); document.addEventListener("send-line", (e) => cubit.send(e.detail.line)); document.addEventListener("ssh-key", () => cubit.sshKey()); cubit.subscribe((state, previous) => { renderPage(state); connect.render(state, previous); terminal.render(state, previous); actions.render(state, previous); }); await cubit.init(); } document.addEventListener('DOMContentLoaded', init);