// #1873 bus side of the Console proxy setup: proxy.init with a bus-generated key, the owner-only // proxy.fence.clear verb, and a lock clear that re-proves restart evidence before it clears. import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { Broker } from '../src/broker.mjs'; import { Store } from '../src/store.mjs'; import { createProxyLock } from '../src/proxy-lock.mjs'; import { REFUSALS } from '../src/refusals.mjs'; import { validManagementKey } from '../../model-proxy/src/common.mjs'; const role = { authority: { withinRole: ['message.send'], crossRole: [] } }; const code = name => error => error.code === name; function fixture(t, { proxyInit = null, proxyFenceClear = null } = {}) { const store = new Store(fs.mkdtempSync(join(tmpdir(), 'ps-bus-'))); t.after(() => store.close()); const b = new Broker({ store, proxyInit, proxyFenceClear, businesses: { demo: { id: 'demo', human: 'owner', arbiters: { technical: 'chief', delivery: 'chief' }, vars: { 'bus.tester': true }, roles: { chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: role } } } }); return { store, b, human: b.bindHuman({ business: 'demo', human: 'owner', via: 'cli', outsideAgent: true }), tester: b.bindTester({ business: 'demo' }), reader: b.bindReader({ business: 'demo' }), agent: b.bindLaunch({ business: 'demo', role: 'coder', run: 'ps-run', harness: 'pi' }), }; } const events = store => JSON.stringify(store.all('SELECT body FROM events')); const bodies = store => store.all('SELECT body FROM events').map(r => JSON.parse(r.body)); test('proxy.init generate:true makes a valid key on the bus, hands it only to init, and never returns or records it', async t => { const seen = []; const f = fixture(t, { proxyInit: async args => { seen.push(args); return { port: args.port ?? 18432, binary: '/fixture' }; } }); const result = await f.b.request(f.human, { verb: 'proxy.init', args: { generate: true, port: 18440, rotateGraceMinutes: 5 } }); assert.deepEqual(result, { port: 18440, binary: '/fixture' }); assert.equal(seen.length, 1); assert.deepEqual(Object.keys(seen[0]).sort(), ['key', 'port', 'rotateGraceMinutes']); assert.equal(validManagementKey(seen[0].key), true); assert.match(seen[0].key, /^[A-Za-z0-9_-]{64}$/, '48 CSPRNG bytes as base64url'); assert.equal(JSON.stringify(result).includes(seen[0].key), false); assert.equal(events(f.store).includes(seen[0].key), false); assert.deepEqual(bodies(f.store).filter(b => b.call === 'proxy.init'), [{ call: 'proxy.init', port: 18440, generated: true }]); await f.b.request(f.human, { verb: 'proxy.init', args: { generate: true } }); assert.notEqual(seen[1].key, seen[0].key, 'each init generates a fresh key'); }); test('proxy.init generate refuses agents and testers, and refuses a key with generate or generate other than true', async t => { let calls = 0; const f = fixture(t, { proxyInit: async () => { calls++; return { port: 18432, binary: '/fixture' }; } }); for (const cap of [f.agent, f.tester]) assert.throws(() => f.b.request(cap, { verb: 'proxy.init', args: { generate: true } }), code('human-required')); assert.throws(() => f.b.request(f.reader, { verb: 'proxy.init', args: { generate: true } }), code('read-only')); for (const args of [{ generate: true, key: 'ps-sentinel-key-0001' }, { generate: false }, { generate: 'yes' }, {}]) assert.throws(() => f.b.request(f.human, { verb: 'proxy.init', args }), code('invalid-request'), JSON.stringify(args)); assert.equal(calls, 0); assert.equal(events(f.store).includes('ps-sentinel-key-0001'), false); // The same verb the refusals guard does run for the owner. await f.b.request(f.human, { verb: 'proxy.init', args: { generate: true } }); assert.equal(calls, 1); }); test('proxy.fence.clear is owner-only, passes the evidence through, and records only the pid', async t => { const seen = []; const evidence = { unit: 'mosaic-proxy.service', confirmed: true, before: { pid: 1 }, after: { pid: 2 }, observedAt: new Date(0).toISOString() }; const f = fixture(t, { proxyFenceClear: async args => { seen.push(args); return { cleared: true, evidence: { after: { pid: 2 } } }; } }); for (const cap of [f.agent, f.tester]) assert.throws(() => f.b.request(cap, { verb: 'proxy.fence.clear', args: { evidence } }), code('human-required')); assert.throws(() => f.b.request(f.reader, { verb: 'proxy.fence.clear', args: { evidence } }), code('read-only')); assert.throws(() => f.b.request(f.human, { verb: 'proxy.fence.clear', args: {} }), code('invalid-request')); assert.throws(() => f.b.request(f.human, { verb: 'proxy.fence.clear', args: { evidence, restarted: true } }), code('invalid-request')); assert.equal(seen.length, 0); assert.deepEqual(await f.b.request(f.human, { verb: 'proxy.fence.clear', args: { evidence } }), { cleared: true, evidence: { after: { pid: 2 } } }); assert.deepEqual(seen, [{ evidence }]); assert.deepEqual(bodies(f.store).filter(b => b.call === 'proxy.fence.clear'), [{ call: 'proxy.fence.clear', cleared: true, pid: 2 }]); }); test('proxy.fence.clear maps a refusal to its proxy code and a missing runtime to not-configured', async t => { const f = fixture(t, { proxyFenceClear: async () => { throw Object.assign(Error('/home/x detail'), { code: 'proxy-restart-unconfirmed' }); } }); await assert.rejects(f.b.request(f.human, { verb: 'proxy.fence.clear', args: { evidence: {} } }), e => e.code === 'proxy-restart-unconfirmed' && !e.message.includes('/home/x')); const g = fixture(t); assert.throws(() => g.b.request(g.human, { verb: 'proxy.fence.clear', args: { evidence: {} } }), code('not-configured')); }); test('a lock clear with verify: the fence an uncertain write set clears only on proof, and the receipt holds the evidence', async t => { const dir = fs.mkdtempSync(join(tmpdir(), 'ps-lock-')); t.after(() => fs.rmSync(dir, { recursive: true, force: true })); const lock = createProxyLock({ stateDir: dir, identity: 'instance-one' }); await assert.rejects(lock.run(async () => { lock.fence({ org: 'acme', op: 'apikey.add' }); throw Error('outcome unknown'); })); assert.throws(() => lock.assertCertain(), code('proxy-uncertain')); let read = 0; await assert.rejects(lock.clear({ restarted: true, read: async () => { read++; }, verify: () => { throw Object.assign(Error('no'), { code: 'proxy-restart-unconfirmed' }); } }), code('proxy-restart-unconfirmed')); assert.equal(read, 0, 'an unproved restart never reads or clears'); assert.throws(() => lock.assertCertain(), code('proxy-uncertain')); const seenFence = []; const result = await lock.clear({ restarted: true, read: async () => { read++; }, verify: previous => { seenFence.push(previous); return { after: { pid: 42 } }; } }); assert.deepEqual(result, { cleared: true, evidence: { after: { pid: 42 } } }); assert.equal(seenFence[0].org, 'acme'); assert.equal(seenFence[0].op, 'apikey.add'); lock.assertCertain(); const receipt = fs.readFileSync(join(dir, 'mosaic-keys.receipts.jsonl'), 'utf8').trim().split('\n').map(JSON.parse).at(-1); assert.deepEqual(receipt.evidence, { after: { pid: 42 } }); assert.equal(receipt.previous.org, 'acme'); }); test('every #1873 refusal code has a refusal-table entry with a next step', () => { for (const c of ['proxy-admin-busy', 'proxy-key-transport', 'proxy-key-mismatch', 'proxy-restart-required', 'proxy-restart-unconfirmed', 'proxy-init-lock-held', 'proxy-init-lock-absent', 'proxy-unit-missing', 'proxy-unit-unhealthy', 'proxy-unit-failed', 'proxy-unit-exists', 'proxy-unit-path', 'proxy-unit-usage', 'proxy-install-missing', 'proxy-install-invalid', 'proxy-fetch-hash']) { const entry = REFUSALS[c]; assert.ok(entry, c); assert.equal(typeof entry.nextStep, 'string', c); assert.ok(entry.nextStep.length > 20, c); assert.equal(/\/(home|mnt|srv|tmp)\//.test(entry.nextStep), false, `${c} names no absolute path`); } });