// node --test relay/test/clients.test.mjs Structural checks of the clients, the playbooks and the Mac tools (no pwsh // on the Mac: the PowerShell 5.1 parse is windows.yml's job; these catch the shapes the ledger names: X24, X25, X26, // X27, X28, X29). import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync, readdirSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); const read = p => readFileSync(join(ROOT, p), 'utf8'); const CLIENTS = ['relay/clients/igneum-agent.ps1', 'relay/clients/send.ps1', 'relay/clients/agent.sh', 'relay/clients/send.sh']; const TOOLS = ['tools/relay.mjs', 'tools/console.mjs', 'tools/build-job.mjs']; test('X24: every client and Mac tool sends x-relay-token as a header and never builds a tokened API path', () => { for (const f of [...CLIENTS, ...TOOLS]) { const s = read(f); assert.match(s, /x-relay-token/, `${f} sends no x-relay-token header`); assert.doesNotMatch(s, /\/r\/\$RelayToken\/api|\/r\/\$RELAY_TOKEN\/api|\/r\/\$\{token\b|\/r\/\$\{TOKEN\}\/(api|c)\//, `${f} still builds an API path with the token in it`); } // the one tokened path left is the phone's page, printed by `url` assert.match(read('tools/relay.mjs'), /const WEB = `\$\{BASE\}\/r\/\$\{TOKEN\}`/); assert.match(read('tools/relay.mjs'), /const API = `\$\{BASE\}\/api\/relay\?fn=`/); }); test('X29: the shell clients hand curl its secret headers through a config file, never on the command line', () => { for (const f of ['relay/clients/agent.sh', 'relay/clients/send.sh']) { const s = read(f); assert.match(s, /-K "\$HDR"/, `${f} does not use curl -K`); assert.doesNotMatch(s, /curl[^\n]*-H "x-(igneum-key|relay-token|machine-secret):/, `${f} puts a secret header on the curl command line`); } }); test('X25: the agent arms the logon task only on the reboot paths and disarms on start and in finally', () => { const s = read('relay/clients/igneum-agent.ps1'); const lines = s.split('\n'); const arms = lines.map((l, i) => [l, i]).filter(([l]) => /^\s*Arm-Restart\s*$/.test(l)); assert.equal(arms.length, 2, 'Arm-Restart is called exactly twice (the two reboot branches)'); for (const [, i] of arms) { assert.ok(lines.slice(i, i + 4).some(l => /shutdown\.exe \/r/.test(l)), `Arm-Restart at line ${i + 1} is not followed by the restart`); assert.ok(/^\s+/.test(lines[i]), 'Arm-Restart is never a top-level statement'); } assert.match(s, /^Disarm-Restart$/m, 'the agent disarms at start'); assert.match(s, /finally \{[\s\S]*Disarm-Restart[\s\S]*\}/, 'the agent disarms in finally'); assert.match(s, /schtasks\.exe \/Delete \/F \/TN 'IgneumRelayAgent'/); assert.match(s, /Remove-ItemProperty -Path \$k -Name 'IgneumRelayAgent'/); }); test('X23: the agent checks the machine tag and the nonce before Start-Process, and refuses with exit 77', () => { const s = read('relay/clients/igneum-agent.ps1'); const run = s.slice(s.indexOf('function Run-Task'), s.indexOf('Log ("igneum relay agent on')); const check = run.indexOf('$why = Check-Task $task'); const start = run.indexOf('Start-Process powershell.exe'); assert.ok(check > 0 && start > check, 'Check-Task runs before Start-Process'); assert.match(run, /Post-Result \$task 77 \$log \("refused: " \+ \$why\)/); assert.match(s, /HMACSHA256/); assert.match(s, /igneum-relay-run\/1`nto=/); const sh = read('relay/clients/agent.sh'); assert.ok(sh.indexOf('check_task "$it"') < sh.indexOf('bash "$STATE/tasks/task-$id.sh"'), 'agent.sh checks before it runs'); assert.match(sh, /hmac\.compare_digest/); }); test('X28: the reboot marker must stand on its own line and the task must be queued with a reboot flag; GET inbox is never acked', () => { const ps = read('relay/clients/igneum-agent.ps1'); assert.match(ps, /\(\?m\)\^RELAY-REBOOT\\r\?\$/); assert.match(ps, /\$reboot = \$asked -and \$rebootAllowed/); assert.match(ps, /Api-Post 'inbox' @\{ machine = \$script:Machine; kind = 'run'; ack = \$true \}/); assert.doesNotMatch(ps, /inbox\?machine=[^\n]*ack=1/); const sh = read('relay/clients/agent.sh'); assert.match(sh, /grep -qx 'RELAY-REBOOT' "\$logf" && \[ -n "\$rebootok" \]/); assert.doesNotMatch(sh, /inbox\?machine=[^\n]*ack=1/); for (const f of ['relay/clients/send.ps1', 'relay/clients/send.sh']) assert.doesNotMatch(read(f), /inbox\?machine=[^\n]*ack=1/, `${f} acks through a GET`); }); test('X28: the registration carries no username and no folder', () => { const ps = read('relay/clients/igneum-agent.ps1'); const info = ps.slice(ps.indexOf('function Collect-Info'), ps.indexOf('function Register-Machine')); assert.doesNotMatch(info, /user = |dir = /); const sh = read('relay/clients/agent.sh'); assert.doesNotMatch(sh, /"user":|"dir":/); }); test('X26: no playbook carries __DL_BASE__ or prints the download URL; the agents hand the base over as RELAY_DL_BASE', () => { for (const f of readdirSync(join(ROOT, 'relay/playbooks'))) { const s = read(`relay/playbooks/${f}`); assert.doesNotMatch(s, /__DL_BASE__/, `${f} still uses __DL_BASE__`); assert.doesNotMatch(s, /Write-Host "downloading \$zipUrl"/, `${f} prints the tokened URL`); } assert.match(read('relay/clients/igneum-agent.ps1'), /\$env:RELAY_DL_BASE = '\$DlBase'/); assert.match(read('relay/clients/agent.sh'), /export RELAY_DL_BASE=/); const mjs = read('tools/relay.mjs'); assert.doesNotMatch(mjs, /replace\(\/__DL_BASE__\/g/, 'tools/relay.mjs still substitutes the dl base into bodies'); assert.match(mjs, /carries the dl token; it must never be in a task body/); }); test('X27: results and registration carry the machine secret header; make-clients.sh bakes it per machine', () => { for (const f of CLIENTS) assert.match(read(f), /x-machine-secret/, `${f} never presents the machine secret`); const mk = read('relay/clients/make-clients.sh'); assert.match(mk, /--machine\) MACHINE=/); assert.match(mk, /machine-secret\.txt/); assert.match(mk, /__DL_BASE__#\$DL_BASE#g/); }); test('X28: the WSL playbook grants sudo for apt-get and dpkg only, and no password travels on a command line', () => { const w = read('relay/playbooks/wsl-setup.ps1'); assert.doesNotMatch(w, /NOPASSWD:ALL/); assert.match(w, /NOPASSWD:SETENV: \/usr\/bin\/apt-get, \/usr\/bin\/dpkg/); assert.doesNotMatch(read('relay/playbooks/prover-setup.ps1'), /echo igneum \| sudo -S/); }); test('PowerShell shape: balanced braces and here-strings in the two .ps1 clients (the 5.1 parse runs in windows.yml)', () => { for (const f of ['relay/clients/igneum-agent.ps1', 'relay/clients/send.ps1']) { const s = read(f); const open = (s.match(/\{/g) || []).length; const close = (s.match(/\}/g) || []).length; assert.equal(open, close, `${f}: ${open} { against ${close} }`); assert.equal((s.match(/@"\s*$/gm) || []).length, (s.match(/^"@\s*$/gm) || []).length, `${f}: here-string markers`); assert.doesNotMatch(s, /\$[A-Za-z_]+:\s[a-z]/, `${f}: a "$name: text" drive-qualified reference (the 5.1 class of 4 October)`); } });