import test from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { Store } from '../src/store.mjs'; import { Broker } from '../src/broker.mjs'; test('only the owner Console reads and writes', t => { const root = mkdtempSync(join(tmpdir(), 'project-remote-bus-')), store = new Store(root), calls = []; t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); }); const business = { id: 'demo', human: 'jason', vars: { 'bus.tester': true }, arbiters: { technical: 'cto', delivery: 'pm' }, roles: { pm: { definition: 'pm', authority: { withinRole: [], crossRole: [] } }, cto: { definition: 'cto', authority: { withinRole: [], crossRole: [] } } }, projects: { stack: { root } } }; const broker = new Broker({ store, businesses: { demo: business }, organization: (...args) => { calls.push(args); return { project: 'stack', revision: 'a'.repeat(64), remote: null, firstSaveAllowed: true }; } }); const owner = broker.bindConsole({ business: 'demo', human: 'jason', subject: { provider: 'oidc', iss: 'https://id.test', sub: 'owner' } }); const cli = broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }); const reader = broker.bindReader({ business: 'demo' }), tester = broker.bindTester({ business: 'demo' }); const get = { verb: 'project.remote.get.console', args: { project: 'stack' } }; assert.equal(broker.request(owner, get).project, 'stack'); for (const cap of [cli, reader, tester]) assert.throws(() => broker.request(cap, get), /human-required|read-only|tester-refused/); assert.throws(() => broker.request(owner, { ...get, args: { project: 'stack', business: 'other' } }), /invalid-request/); assert.equal(broker.request(owner, { verb: 'project.remote.set.console', args: { project: 'stack', revision: 'a'.repeat(64), remote: 'https://git.example/a/b' } }).project, 'stack'); assert.equal(calls.length, 2); assert.throws(() => broker.bindConsole({ business: 'demo', human: 'other', subject: { provider: 'oidc', iss: 'https://id.test', sub: 'owner' } }), /human-required/); }); test('socket refuses tester, viewer, collaborator, agent and CLI human for both remote verbs', async t => { const { serve } = await import('../src/server.mjs'); const { Client } = await import('../src/client.mjs'); const root = mkdtempSync(join(tmpdir(), 'remote-socket-')), store = new Store(root); const access = { accessVersion: 1, revision: 1, users: [], grants: [ { id: 'g-1', user: 'viewer', business: 'demo', projects: ['*'], instances: ['pm'], level: 'viewer' }, { id: 'g-2', user: 'collab', business: 'demo', projects: ['*'], instances: ['pm'], level: 'collaborator' }, ] }; const business = { id: 'demo', human: 'jason', vars: { 'bus.tester': true }, arbiters: { technical: 'pm', delivery: 'pm' }, roles: { pm: { definition: 'pm', authority: { withinRole: [], crossRole: [] } } }, projects: { stack: { root } } }; const broker = new Broker({ store, businesses: { demo: business }, access: () => access, organization: () => { throw Error('unexpected writer'); } }); const caps = { human: broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }), tester: broker.bindTester({ business: 'demo' }), viewer: broker.bindUser({ business: 'demo', user: 'viewer' }), collaborator: broker.bindUser({ business: 'demo', user: 'collab' }), agent: broker.bindLaunch({ business: 'demo', role: 'pm', run: 'remote-test', harness: 'pi' }), }; const path = join(root, 'bus.sock'), server = await serve({ broker, path }); t.after(async () => { await server.close(); store.close(); rmSync(root, { recursive: true, force: true }); }); for (const [who, cap] of Object.entries(caps)) for (const [verb, args] of [ ['project.remote.get.console', { project: 'stack' }], ['project.remote.set.console', { project: 'stack', revision: 'a'.repeat(64), remote: 'https://git.example/a/b' }], ]) await assert.rejects(new Client({ path, cap }).call(verb, args), e => ['human-required', 'read-only', 'tester-refused', 'access-refused'].includes(e.code), `${who} ${verb}`); });