Relay (X23, X27): the intake key is its own tier (upload and file drops only, RELAY_INTAKE_COMPAT=0 closes it);
a run task needs an Ed25519 signature by the Mac run key over {to, nonce, body sha256, flags} (RELAY_RUN_PUB,
401 without) and an HMAC tag with the target's machine secret that the agent verifies before anything runs;
results and registration are bound to the machine the secret proves (403 on a forged from).
X24: every client and Mac tool sends x-relay-token as a header to /api/relay?fn=; the path token stays for the
phone page only. X25: the agent arms the logon task only for a restart a task asked for and disarms on start
and exit. X26: 30-day retention with blob deletion, feed capped at 100, the dl base as RELAY_DL_BASE held by the
agent, never in a body. X28: GET inbox never acks (POST inbox does), RELAY-REBOOT on its own line and only with a
reboot flag, 120/min and 10 failed auths/min per IP, no username or folder on register, WSL sudo scoped to
apt-get and dpkg with SETENV, no password on a command line. X29: the intake key reaches curl through -K in
upload.sh and both upload-log.bat; tools/ci/curl-header-check.sh fails the class. G14: TZ=UTC in ship-app.mjs
and publish-jobs.sh; tools/ci/commit-tz-check.sh fails the class; history-rewrite.md names the .old-2026-10-05
files as the values in the history. The handler moved to relay/lib/handler.mjs with injected sql and blobs
(relay/lib/blob.mjs holds @vercel/blob) so relay/test/handler.test.mjs drives it without a database:
47 tests across 6 suites, all green.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
113 lines
6.7 KiB
JavaScript
113 lines
6.7 KiB
JavaScript
// node --test relay/test/guard.test.mjs (no dependencies, no network)
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { authVia, runCanon, keygen, signRun, verifyRun, machineTag, sameTag, checkRun, wantsReboot, feedLimit, retentionCutoff, machineForSecret, secretHash, newNonce, newSecret, FEED_LIMIT_MAX, RETENTION_DAYS, POST_ALLOWED, DROP_KINDS, mayRead } from '../lib/guard.mjs';
|
|
|
|
const T = 'ABCDEFGHIJKLMNOPQRST'; // the token shape: 20 characters
|
|
const K = 'relaykeyrelaykeyrelaykeyrelaykeyrelaykeyrelayke'; // 48
|
|
const I = 'intakekeyintakekeyintakekeyinta'; // 32
|
|
const env = { RELAY_TOKEN: T, RELAY_KEY: K, LOG_INTAKE_KEY: I };
|
|
|
|
test('authVia: the header alone is the token tier (X24); the path token still works for the phone page', () => {
|
|
assert.equal(authVia({ headers: { 'x-relay-token': T } }, env), 'token');
|
|
assert.equal(authVia({ query: { token: T }, headers: {} }, env), 'token');
|
|
assert.equal(authVia({ headers: { 'x-relay-token': T.slice(0, 19) + 'x' } }, env), null);
|
|
assert.equal(authVia({ headers: {} }, env), null);
|
|
});
|
|
|
|
test('authVia: three tiers; the intake key is its own tier and RELAY_INTAKE_COMPAT=0 closes it (X23)', () => {
|
|
assert.equal(authVia({ headers: { 'x-igneum-key': K } }, env), 'key');
|
|
assert.equal(authVia({ headers: { 'x-igneum-key': I } }, env), 'intake');
|
|
assert.equal(authVia({ headers: { 'x-igneum-key': I } }, { ...env, LOG_INTAKE_KEY: '', LOG_INTAKE_KEY_NEXT: I }), 'intake');
|
|
assert.equal(authVia({ headers: { 'x-igneum-key': I } }, { ...env, RELAY_INTAKE_COMPAT: '0' }), null);
|
|
assert.equal(authVia({ headers: { 'x-igneum-key': 'wrongwrongwrongwrongwrongwrongwr' } }, env), null);
|
|
});
|
|
|
|
test('tiers: what each may post and read', () => {
|
|
assert.equal(POST_ALLOWED.token.has('task'), true);
|
|
assert.equal(POST_ALLOWED.key.has('task'), false);
|
|
assert.equal(POST_ALLOWED.key.has('secret'), false);
|
|
assert.deepEqual([...POST_ALLOWED.intake].sort(), ['drop', 'upload']);
|
|
assert.equal(DROP_KINDS.intake.has('result'), false);
|
|
assert.equal(DROP_KINDS.key.has('run'), false);
|
|
assert.equal(DROP_KINDS.token, null);
|
|
assert.deepEqual(['token', 'key', 'intake', null].map(mayRead), [true, true, false, false]);
|
|
});
|
|
|
|
test('runCanon: deterministic, names the machine, the nonce, the flags and the body hash', () => {
|
|
const a = runCanon({ to: 'PC1', nonce: 'a'.repeat(32), body: 'Write-Host hi', flags: { elevated: true } });
|
|
const b = runCanon({ to: 'PC1', nonce: 'a'.repeat(32), body: 'Write-Host hi', flags: { elevated: 1 } });
|
|
assert.equal(a, b);
|
|
assert.match(a, /^igneum-relay-run\/1\nto=PC1\nnonce=a{32}\nelevated=1\nreboot_continue=0\nreboot=0\nbody_sha256=[0-9a-f]{64}\n$/);
|
|
assert.notEqual(a, runCanon({ to: 'PC2', nonce: 'a'.repeat(32), body: 'Write-Host hi', flags: { elevated: true } }));
|
|
assert.notEqual(a, runCanon({ to: 'PC1', nonce: 'a'.repeat(32), body: 'Write-Host hi ', flags: { elevated: true } }));
|
|
assert.notEqual(a, runCanon({ to: 'PC1', nonce: 'a'.repeat(32), body: 'Write-Host hi', flags: {} }));
|
|
});
|
|
|
|
test('Ed25519: a good signature verifies; a changed byte, another key or a malformed signature does not', () => {
|
|
const { seed, pub } = keygen();
|
|
const other = keygen();
|
|
const canon = runCanon({ to: 'PC1', nonce: newNonce(), body: 'x' });
|
|
const sig = signRun(canon, seed);
|
|
assert.equal(sig.length, 128);
|
|
assert.equal(verifyRun(canon, sig, pub), true);
|
|
assert.equal(verifyRun(canon + ' ', sig, pub), false);
|
|
assert.equal(verifyRun(canon, sig, other.pub), false);
|
|
assert.equal(verifyRun(canon, sig.slice(0, 127) + (sig.endsWith('0') ? '1' : '0'), pub), false);
|
|
assert.equal(verifyRun(canon, 'nothex', pub), false);
|
|
assert.equal(verifyRun(canon, sig, 'nothex'), false);
|
|
});
|
|
|
|
test('machineTag: HMAC with the machine secret; sameTag compares in constant time and refuses malformed tags', () => {
|
|
const s = newSecret();
|
|
const canon = runCanon({ to: 'PC1', nonce: newNonce(), body: 'x' });
|
|
const t = machineTag(s, canon);
|
|
assert.equal(t.length, 64);
|
|
assert.equal(sameTag(t, machineTag(s, canon)), true);
|
|
assert.equal(sameTag(t, machineTag(newSecret(), canon)), false);
|
|
assert.equal(sameTag(t, machineTag(s, canon + 'x')), false);
|
|
assert.equal(sameTag(t, 'short'), false);
|
|
});
|
|
|
|
test('checkRun: a run without the signature, the tag or the nonce is refused; a complete one passes (X23)', () => {
|
|
const { seed, pub } = keygen();
|
|
const nonce = newNonce();
|
|
const body = 'Write-Host hi';
|
|
const flags = { elevated: true, nonce, mac: machineTag(newSecret(), runCanon({ to: 'PC1', nonce, body, flags: { elevated: true } })) };
|
|
flags.sig = signRun(runCanon({ to: 'PC1', nonce, body, flags }), seed);
|
|
assert.equal(checkRun({ to: 'PC1', body, flags }, pub), null);
|
|
assert.match(checkRun({ to: 'PC1', body, flags: {} }, pub), /nonce/);
|
|
assert.match(checkRun({ to: 'PC1', body, flags: { nonce } }, pub), /mac/);
|
|
assert.match(checkRun({ to: 'PC1', body, flags: { nonce, mac: flags.mac } }, pub), /sig/);
|
|
assert.match(checkRun({ to: 'PC1', body, flags }, ''), /RELAY_RUN_PUB/);
|
|
assert.match(checkRun({ to: 'PC1', body: body + ' ', flags }, pub), /does not verify/);
|
|
assert.match(checkRun({ to: 'PC2', body, flags }, pub), /does not verify/);
|
|
assert.match(checkRun({ to: 'PC1', body, flags: { ...flags, elevated: false } }, pub), /does not verify/);
|
|
assert.match(checkRun({ to: 'all', body, flags }, pub), /one named machine/);
|
|
});
|
|
|
|
test('wantsReboot: the marker on its own line only (X28)', () => {
|
|
assert.equal(wantsReboot('features enabled\nRELAY-REBOOT\n'), true);
|
|
assert.equal(wantsReboot('RELAY-REBOOT'), true);
|
|
assert.equal(wantsReboot('a\r\nRELAY-REBOOT\r\nb'), true);
|
|
assert.equal(wantsReboot('the script prints RELAY-REBOOT when it wants a restart\n'), false);
|
|
assert.equal(wantsReboot('RELAY-REBOOT-NOT\n'), false);
|
|
assert.equal(wantsReboot(''), false);
|
|
});
|
|
|
|
test('feedLimit and retention (X26)', () => {
|
|
assert.equal(feedLimit({}), 50);
|
|
assert.equal(feedLimit({ limit: '500' }), FEED_LIMIT_MAX);
|
|
assert.equal(feedLimit({ limit: '0' }), 50);
|
|
assert.equal(feedLimit({ limit: '7' }), 7);
|
|
assert.equal(RETENTION_DAYS, 30);
|
|
assert.equal(retentionCutoff(Date.UTC(2026, 9, 5, 22, 0, 0)), '2026-09-05T22:00:00.000Z');
|
|
});
|
|
|
|
test('machineForSecret: the stored sha256 names the machine; a wrong or malformed secret names nothing (X27)', () => {
|
|
const s = newSecret();
|
|
const rows = [{ name: 'PC1', secret_hash: secretHash(s) }, { name: 'PC2', secret_hash: secretHash(newSecret()) }, { name: 'Mac', secret_hash: null }];
|
|
assert.deepEqual(machineForSecret(s, rows), { name: 'PC1' });
|
|
assert.deepEqual(machineForSecret(newSecret(), rows), { error: 'unknown machine secret' });
|
|
assert.deepEqual(machineForSecret('short', rows), { error: 'x-machine-secret must be 64 hex' });
|
|
});
|