#!/usr/bin/env node // Mac side of the Igneum relay (relay/ in this repo, https://relay.igneum.network). // node tools/relay.mjs the feed, newest first (last 50) // node tools/relay.mjs list [N] [--machine X] more of the feed (100 a call at most) // node tools/relay.mjs read print an item; its file is downloaded to --out (default $TMPDIR/igneum-relay) // node tools/relay.mjs drop "" | post a note or a file from the Mac [--to PC1] [--title "..."] [--body "..." with a file] // node tools/relay.mjs task "title" [file] [--body "..."] a task for a person or a Claude session on that PC // node tools/relay.mjs run "title" [--elevated] [--reboot-continue] [--reboot] // a script the igneum-agent runs: signed with ~/.config/igneum/relay-run-key // (Ed25519, checked by the relay) and tagged with the machine's secret // (~/.config/igneum/relay-machines/, checked by the agent); X23 // node tools/relay.mjs keygen make the run key pair once; prints the public key for RELAY_RUN_PUB // node tools/relay.mjs secret make that machine's secret, bind it on the relay, then make-clients.sh --machine // node tools/relay.mjs watch [--since ] poll every 10 s and print new items (results included) // node tools/relay.mjs inbox [--ack] what that machine has not read yet (--ack marks it read, a POST) // node tools/relay.mjs machines | role | name // node tools/relay.mjs ack | done | rm | url // Reads ~/.config/igneum/relay-token (sent as the x-relay-token header, never in a URL: X24), relay-run-key, // relay-machines/, dl-token (only to refuse a body that carries it: X26) and relay-url (optional, default // https://relay.igneum.network). Zero dependencies. import { readFileSync, writeFileSync, mkdirSync, existsSync, statSync, chmodSync } from 'node:fs'; import { homedir, tmpdir, hostname } from 'node:os'; import { basename, join, resolve, dirname } from 'node:path'; import { keygen as edKeygen, signRun, runCanon, machineTag, newNonce, newSecret, secretHash } from '../relay/lib/guard.mjs'; process.stdout.on('error', e => { if (e.code === 'EPIPE') process.exit(0); throw e; }); const CFG = join(homedir(), '.config', 'igneum'); const cfg = n => { try { return readFileSync(join(CFG, n), 'utf8').trim(); } catch { return ''; } }; const TOKEN = cfg('relay-token'); const DL = cfg('dl-token'); const BASE = (cfg('relay-url') || 'https://relay.igneum.network').replace(/\/$/, ''); if (!TOKEN) { console.error('no ~/.config/igneum/relay-token'); process.exit(1); } const API = `${BASE}/api/relay?fn=`; // the function itself; the token travels in the header const WEB = `${BASE}/r/${TOKEN}`; // the phone's page: the one place the token stays in the path const SHOWN = `${BASE}/r/`; // printed in place of WEB everywhere but `url` (round 4, X24: the token in every terminal) const HEADERS = { 'x-relay-token': TOKEN }; const argv = process.argv.slice(2); const flags = {}; const pos = []; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a.startsWith('--')) { const k = a.slice(2); const next = argv[i + 1]; if (next !== undefined && !next.startsWith('--') && !['elevated', 'reboot-continue', 'reboot', 'ack', 'all', 'rotate'].includes(k)) { flags[k] = next; i++; } else flags[k] = true; } else pos.push(a); } const cmd = pos[0] && !/^\d+$/.test(pos[0]) ? pos[0] : (pos[0] ? 'read' : 'list'); if (cmd === 'read' && /^\d+$/.test(pos[0])) pos.unshift('read'); async function api(fn, { q, body } = {}) { const r = await fetch(API + fn + (q ? '&' + new URLSearchParams(q) : ''), body === undefined ? { headers: HEADERS } : { method: 'POST', headers: { 'Content-Type': 'application/json', ...HEADERS }, body: JSON.stringify(body) }); const j = await r.json().catch(() => ({ ok: false, error: `http ${r.status}` })); if (!r.ok || j.ok === false) throw new Error(j.error || `http ${r.status}`); return j; } async function uploadFile(path) { const buf = readFileSync(path); const name = basename(path); const t = await api('upload', { body: { name, size: buf.length } }); if (buf.length > t.max) throw new Error(`${name} is ${buf.length} bytes, over the ${t.max} byte cap`); const r = await fetch(t.put_url, { method: 'PUT', body: buf, headers: { authorization: `Bearer ${t.token}`, 'x-api-version': t.api_version, 'x-add-random-suffix': '1', 'x-content-type': 'application/octet-stream' } }); const j = await r.json().catch(() => ({})); if (!r.ok || !j.url) throw new Error(`blob upload failed: ${r.status} ${JSON.stringify(j).slice(0, 200)}`); return { file_name: name, file_url: j.url, size: buf.length }; } const fmtSize = n => n < 1024 ? `${n} B` : n < 1048576 ? `${(n / 1024).toFixed(1)} KB` : `${(n / 1048576).toFixed(1)} MB`; const when = ts => new Date(ts).toISOString().replace('T', ' ').slice(5, 16); function line(it) { const route = it.to === 'all' ? it.from : `${it.from} > ${it.to}`; const st = it.done ? ' done' : it.read && (it.kind === 'task' || it.kind === 'run') ? ' read' : (it.kind === 'task' || it.kind === 'run') ? ' NEW' : ''; const first = (it.body || '').split('\n').find(l => l.trim()) || ''; const file = it.has_file ? ` [${it.file_name} ${fmtSize(it.size)}]` : ''; return `${('#' + it.id).padStart(5)} ${when(it.ts)} ${it.kind.padEnd(6)} ${route.padEnd(12)} ${it.title ? it.title + (first ? ': ' : '') : ''}${first.slice(0, 90)}${file}${st}`; } function printItem(it) { console.log(`===== #${it.id} ${it.kind} from ${it.from} to ${it.to} at ${it.ts}${it.title ? ` | ${it.title}` : ''} =====`); const fl = Object.entries(it.flags || {}).filter(([, v]) => v !== false).map(([k, v]) => v === true ? k : `${k}=${v}`).join(' '); if (fl || it.task_id) console.log(`[${[fl, it.task_id ? `task #${it.task_id}` : '', it.done ? 'done' : it.read ? 'read' : ''].filter(Boolean).join(' | ')}]`); if (it.body) process.stdout.write(it.body.endsWith('\n') ? it.body : it.body + '\n'); } const outDir = () => { const d = resolve(flags.out || process.env.RELAY_DOWNLOAD_DIR || join(tmpdir(), 'igneum-relay')); mkdirSync(d, { recursive: true }); return d; }; async function download(it) { const r = await fetch(`${API}file&id=${it.id}`, { headers: HEADERS, redirect: 'follow' }); if (!r.ok) throw new Error(`download http ${r.status}`); const buf = Buffer.from(await r.arrayBuffer()); const p = join(outDir(), `${it.id}-${it.file_name || 'file'}`); writeFileSync(p, buf); return p; } // X26: the body is posted as written. The downloads base reaches the script as $env:RELAY_DL_BASE (RELAY_DL_BASE in // bash), a value the agent holds; a body that still says __DL_BASE__ or carries the dl token is refused here. function playbook(path) { const s = readFileSync(path, 'utf8'); if (/__DL_BASE__/.test(s)) throw new Error(`${path} uses __DL_BASE__; write $env:RELAY_DL_BASE (PowerShell) or $RELAY_DL_BASE (bash) instead, the agent fills it in`); if (DL && s.includes(DL)) throw new Error(`${path} carries the dl token; it must never be in a task body`); return s; } const RUN_KEY = join(CFG, 'relay-run-key'); const machineSecretFile = m => join(CFG, 'relay-machines', m); // a run task: nonce, the machine's HMAC tag, the Ed25519 signature (relay/lib/guard.mjs, the same code the relay runs) function signRunTask(o) { const seed = cfg('relay-run-key'); if (!/^[0-9a-f]{64}$/.test(seed)) throw new Error(`no run key at ${RUN_KEY}: node tools/relay.mjs keygen (then set RELAY_RUN_PUB on the relay project)`); let secret = ''; try { secret = readFileSync(machineSecretFile(o.to), 'utf8').trim(); } catch {} if (!/^[0-9a-f]{64}$/.test(secret)) throw new Error(`no machine secret for ${o.to}: node tools/relay.mjs secret ${o.to} first, then relay/clients/make-clients.sh --machine ${o.to}`); o.flags.nonce = newNonce(); o.flags.mac = machineTag(secret, runCanon({ to: o.to, nonce: o.flags.nonce, body: o.body, flags: o.flags })); o.flags.sig = signRun(runCanon({ to: o.to, nonce: o.flags.nonce, body: o.body, flags: o.flags }), seed); return o; } try { if (cmd === 'url') { console.log(WEB); } else if (cmd === 'list') { const n = Number(pos[1]) || 50; const q = { limit: n }; if (flags.machine) q.machine = flags.machine; const j = await api('feed', { q }); const waiting = j.machines.filter(m => m.unread).map(m => `${m.name} ${m.unread}`).join(', '); console.log(`${SHOWN}\nmachines: ${j.machines.map(m => `${m.name}${m.role ? '/' + m.role : ''}${m.named === false ? ' (unnamed, hostname ' + m.hostname + ')' : ''}${m.last_seen ? ' seen ' + when(m.last_seen) : ''}`).join(' | ')}${waiting ? `\nwaiting: ${waiting}` : ''}`); if (!j.items.length) console.log('no items yet'); for (const it of j.items) console.log(line(it)); } else if (cmd === 'read') { const it = (await api('item', { q: { id: pos[1] } })).item; printItem(it); if (it.has_file) console.log(`file: ${await download(it)}`); } else if (cmd === 'drop') { const what = pos[1]; if (!what) throw new Error('drop "" or drop '); const o = { from: flags.from || 'Mac', to: flags.to || 'all', title: flags.title || '' }; if (existsSync(what) && statSync(what).isFile()) Object.assign(o, await uploadFile(what), { kind: 'file', body: typeof flags.body === 'string' ? flags.body : '' }); else { o.body = what; o.kind = flags.kind || 'text'; } const r = await api('drop', { body: o }); console.log(`sent #${r.id} (${r.kind})`); } else if (cmd === 'task' || cmd === 'run') { const [, to, title, file] = pos; if (!to || !title) throw new Error(`${cmd} "title" ${cmd === 'run' ? '