// ATIRC_BIN= ATP_USERNAME= ATP_PASSWORD= deno run -A test/live.ts // // The account's real mesh: this host runs its own node (`atirc mesh run`, the bridge), headless Chrome // (test/web/live.html) joins the owner's channel over WebRTC, asks the first agent it sees, reads the answer. // The page's PDS writes go through an app-password session here; its records are deleted on exit. const ROOT = new URL("..", import.meta.url).pathname; const BIN = Deno.env.get("ATIRC_BIN") ?? "atirc"; const CHROME = Deno.env.get("CHROME") ?? (Deno.build.os === "darwin" ? "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" : "google-chrome"); const HANDLE = Deno.env.get("ATP_USERNAME") ?? ""; const PASSWORD = Deno.env.get("ATP_PASSWORD") ?? ""; const ASK = Deno.env.get("ASK") ?? "agents: please run uname -m and reply with its output"; const CHANNEL = Deno.env.get("CHANNEL") ?? ""; // Identities of this account the node (and page) should dial: e.g. a VM agent whose owner graph roots elsewhere. const PEERS = (Deno.env.get("PEERS") ?? "").split(",").filter(Boolean); if (!HANDLE || !PASSWORD) throw new Error("set ATP_USERNAME and ATP_PASSWORD"); const json = async (response: Response) => { if (!response.ok) throw new Error(`${response.url}: ${response.status} ${await response.text()}`); return response.json(); }; const pdsCache = new Map>(); const pdsOf = (did: string) => { if (!pdsCache.has(did)) { const url = did.startsWith("did:web:") ? `https://${did.slice(8)}/.well-known/did.json` : `https://plc.directory/${did}`; pdsCache.set(did, fetch(url).then(json).then((doc) => doc.service.find((s: { id: string }) => s.id.endsWith("#atproto_pds")).serviceEndpoint)); } return pdsCache.get(did)!; }; const did = (await json(await fetch(`https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=${HANDLE}`))).did as string; const pds = await pdsOf(did); const session = await json(await fetch(`${pds}/xrpc/com.atproto.server.createSession`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ identifier: HANDLE, password: PASSWORD }), })); const write = (method: string, body: Record) => fetch(`${pds}/xrpc/com.atproto.repo.${method}`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${session.accessJwt}` }, body: JSON.stringify({ ...body, repo: did }), }); const written = new Set(); let resolveResult: (value: unknown) => void; const result = new Promise((resolve) => (resolveResult = resolve)); const server = Deno.serve({ hostname: "127.0.0.1", port: 0, onListen: () => {} }, async (req) => { const url = new URL(req.url); if (url.pathname === "/xrpc/com.atproto.repo.getRecord") { const repo = url.searchParams.get("repo") ?? ""; const response = await fetch(`${await pdsOf(repo)}/xrpc/com.atproto.repo.getRecord${url.search}`); return new Response(response.body, { status: response.status, headers: { "content-type": "application/json" } }); } if (url.pathname === "/xrpc/com.atproto.repo.putRecord" || url.pathname === "/xrpc/com.atproto.repo.deleteRecord") { const body = await req.json(); const method = url.pathname.split(".").pop() as string; if (method === "putRecord") written.add(`${body.collection}/${body.rkey}`); else written.delete(`${body.collection}/${body.rkey}`); const response = await write(method, body); return new Response(response.body, { status: response.status, headers: { "content-type": "application/json" } }); } if (url.pathname === "/log") { console.error(`${new Date().toISOString()} page: ${await req.text()}`); return new Response("{}"); } if (url.pathname === "/result") { resolveResult(await req.json()); return new Response("{}"); } if (url.pathname.includes("..") || !/^\/(js|node_modules|test\/web)\//.test(url.pathname)) return new Response("not found", { status: 404 }); try { const type = url.pathname.endsWith(".html") ? "text/html" : "text/javascript"; return new Response(await Deno.readFile(`${ROOT}${url.pathname.slice(1)}`), { headers: { "content-type": type } }); } catch { return new Response("not found", { status: 404 }); } }); const tmp = await Deno.makeTempDir({ prefix: "atlibs-atirc-live-" }); const children: Deno.ChildProcess[] = []; const start = (args: string[], env: Record, log: string) => { const child = new Deno.Command(args[0], { args: args.slice(1), env, stdin: "null", stdout: "null", stderr: "piped" }).spawn(); children.push(child); const file = Deno.openSync(`${tmp}/${log}`, { write: true, create: true }); (async () => { for await (const chunk of child.stderr) file.writeSync(chunk); })().catch(() => {}); return child; }; let failed = false; try { // The owner's own node: publishes computer.socialweb.peer/ + its listen record, bridges the page. start([BIN, "mesh", "run", ...PEERS.flatMap((p) => ["--peer", p])], { ATIRC_CONFIG_DIR: `${tmp}/node`, ATP_USERNAME: HANDLE, ATP_PASSWORD: PASSWORD, RUST_LOG: Deno.env.get("RUST_LOG") ?? "info" }, "node.log"); for (let i = 0; i < 300 && !(await Deno.stat(`${tmp}/node/mesh.json`).then(() => true, () => false)); i++) await new Promise((r) => setTimeout(r, 100)); const page = `http://127.0.0.1:${server.addr.port}/test/web/live.html?${new URLSearchParams({ did, handle: HANDLE, ask: ASK, channel: CHANNEL, peers: PEERS.join(",") })}`; start([CHROME, "--headless=new", `--user-data-dir=${tmp}/chrome`, "--no-first-run", "--no-default-browser-check", "--disable-gpu", page], {}, "chrome.log"); const outcome = await Promise.race([result, new Promise((_, reject) => setTimeout(() => reject(new Error("the page reported nothing in 660 s")), 660000))]) as { ok: boolean; error?: string }; if (!outcome.ok) throw new Error(`page: ${outcome.error}`); console.log(`ok atlibs-atirc live: ${JSON.stringify(outcome)}`); } catch (error) { failed = true; console.error(`FAIL atlibs-atirc live: ${error instanceof Error ? error.message : error}`); console.error(`--- node.log\n${(await Deno.readTextFile(`${tmp}/node.log`).catch(() => "")).slice(-4000)}`); } finally { for (const child of children) { try { child.kill("SIGTERM"); } catch { /* exited */ } } await new Promise((r) => setTimeout(r, 1000)); // The node's records and any offer the page left. for (const key of [...written, `computer.socialweb.peer/${HANDLE}`, `computer.socialweb.ingress.listen/${HANDLE}`]) { const [collection, rkey] = key.split("/"); await write("deleteRecord", { collection, rkey }).catch(() => {}); } await server.shutdown(); await Deno.remove(tmp, { recursive: true }).catch(() => {}); } Deno.exit(failed ? 1 : 0);