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

johnandersen777.bsky.social/atlibs-atirc

atirc channels in a browser page: WebRTC to atirc mesh nodes, no server, no token

0
0
main
115 lines · 6.6 KB Raw History
// ATIRC_BIN=<atirc> ATP_USERNAME=<handle> ATP_PASSWORD=<app 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<string, Promise<string>>();
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<string, unknown>) =>
	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<string>();

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<string, string>, 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/<handle> + 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);