#!/usr/bin/env node // Reach socialweb.computer guests and atirc over iroh. No deps; shells out to dumbpipe and ssh. // The host pseudo-call (irohHost) is js/index.js. // node examples/iroh.mjs ssh [command...] // ATIRC_TOKEN=... node examples/iroh.mjs irc import { spawn, spawnSync } from "node:child_process"; import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { knownHosts, sshArgs, ticket } from "../js/index.js"; export const ATIRC_ALPN = "atirc/irc/1"; // The atirc token's `endpoint` claim is the server's iroh ticket. export function atircTicket(token) { const claims = JSON.parse(Buffer.from(token.split(".")[1], "base64url")); return ticket(claims.endpoint); } function ssh(guestFile, key, command) { const guest = JSON.parse(readFileSync(guestFile, "utf8")); const dir = mkdtempSync(join(tmpdir(), "iroh-ssh-")); try { writeFileSync(join(dir, "known_hosts"), knownHosts("guest", guest.hostKeys)); return spawnSync("ssh", sshArgs(key, guest.iroh ?? guest.address, join(dir, "known_hosts"), command), { stdio: "inherit" }).status ?? 1; } finally { rmSync(dir, { recursive: true, force: true }); } } // IRC over the atirc server's iroh endpoint: register, post one line, PING/PONG round trip. function irc(text) { const token = process.env.ATIRC_TOKEN; if (!token) throw new Error("ATIRC_TOKEN is required"); const claims = JSON.parse(Buffer.from(token.split(".")[1], "base64url")); let nick = claims.nick; const channel = "#" + claims.handle.replace(/\./g, "_"); const pipe = spawn("dumbpipe", ["connect", "--custom-alpn", `utf8:${ATIRC_ALPN}`, atircTicket(token)], { stdio: ["pipe", "pipe", "inherit"] }); const send = (line) => pipe.stdin.write(line + "\r\n"); const t0 = Date.now(); const timer = setTimeout(() => { console.error("timeout"); pipe.kill(); process.exitCode = 1; }, 30000); let buf = ""; send(`PASS ${token}`); send(`NICK ${nick}`); send(`USER ${nick} 0 * :${nick}`); pipe.stdout.on("data", (d) => { buf += d; let i; while ((i = buf.indexOf("\n")) >= 0) { const line = buf.slice(0, i).trim(); buf = buf.slice(i + 1); const [, cmd] = line.startsWith(":") ? line.split(" ") : [null, line.split(" ")[0]]; if (cmd === "PING") send("PONG " + line.split(" ").slice(line.startsWith(":") ? 2 : 1).join(" ")); if (cmd === "433") send(`NICK ${(nick += "_")}`); // nick held by another session (e.g. atirc sub) if (cmd === "001") { console.log(`registered ${nick} in ${Date.now() - t0}ms`); send(`JOIN ${channel}`); // atirc drops PRIVMSG to a channel the session has not joined } if (cmd === "JOIN" && line.startsWith(`:${nick}!`)) { send(`PRIVMSG ${channel} :${text}`); send("PING :iroh-rt"); } if (cmd === "PONG" && line.endsWith(":iroh-rt")) { console.log(`posted to ${channel}; PONG round trip ${Date.now() - t0}ms`); clearTimeout(timer); send("QUIT :done"); pipe.stdin.end(); } if (/^(ERROR|464|465)$/.test(cmd)) console.error(line); } }); pipe.on("exit", () => clearTimeout(timer)); } if (process.argv[1] === fileURLToPath(import.meta.url)) { const [cmd, ...args] = process.argv.slice(2); if (cmd === "ssh") process.exitCode = ssh(args[0], args[1], args.slice(2)); else if (cmd === "irc") irc(args.join(" ")); else { console.error("usage: iroh.mjs ssh [cmd...] | irc "); process.exitCode = 2; } }