Files
jason.woltje 5dbd7e72d3 fix(org): copy binding review round, CB1 (#1873)
Security review P2s:
- A binding proposal keeps only its structured request fields. The
  free-text reason is never written to the private proposal or covered
  by its digest.
- A root copy's receipts are checked at prepare, at check, and again
  under the organization lock before the write and before publication.
  A copy removed, replaced or unreadable since boot refuses with
  instance-fired, copy-stale or root-policy-unreadable.

Build review P2:
- A root copy's admitted projects come from the project unions,
  recomputed from the current document and the boot's root agents. A
  project that excludes the copy, or whose union refuses, is never in
  reach; a request naming it refuses with credential-scope-exceeded.

Build review P3s: tests for proposal-time shadowing, the loader's
copyBindings validation, a pending record that covers every service,
a stricter path check on the refusal table, and refusal codes on every
bare rejects in the binding tests.
2026-11-11 14:27:08 -06:00

481 lines
35 KiB
JavaScript

// #1873 copy credential binding, slices CB0 and CB1 (design
// docs/plans/2026-11-11_copy-credential-binding-design.md section 8, rows CB0 and CB1).
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { createHash } from 'node:crypto';
import { hireDecisions, authorizeHire } from '../src/hire-decisions.mjs';
import { organizationWriter } from '../src/organization.mjs';
import { checkRootAgents } from '../src/agent-levels.mjs';
import { copyRefusal, loadWithCopies, materializeCopies } from '../src/agent-copies.mjs';
import { readTemplate } from '../src/definitions.mjs';
import { validateBusinessDocument } from '../src/organization-config.mjs';
import { activateOrg, ensureOrgIncarnation, orgIncarnation, orgRoot, readRegistry, reserveOrg, withRegistryWriter } from '../src/org-registry.mjs';
import { definitionPin, intersectScopes, resolveCopyBinding, validateCopyBindings } from '../src/copy-bindings.mjs';
import { businessDoc, rolesCopy, writeJson } from './helpers.mjs';
import { createOrg, showOrg, templateRender, updateOrg } from '../src/org-service.mjs';
const SECRET = 'sk-test-NOT-A-SECRET-1234';
const OWNER = { actor: 'jason', agent: false, rights: ['org.hire'], owner: true, instanceOwner: true, subject: { provider: 'test', iss: 'https://test', sub: 'owner' } };
const FUTURE = '2099-01-01T00:00:00.000Z';
const ID1 = '11111111-1111-4111-8111-111111111111', ID2 = '22222222-2222-4222-8222-222222222222', ID3 = '33333333-3333-4333-8333-333333333333';
const GITEA = { gitea: { scopes: ['read:repository'], projects: ['stack'] } };
const sha = b => createHash('sha256').update(b).digest('hex');
// ---- CB0: pure resolver fixtures ----
const rolesDir = join(new URL('../../..', import.meta.url).pathname, 'roles');
const rootOf = agents => ({ state: 'ok', ...checkRootAgents({ agentsVersion: 1, agents }, { rolesDir }) });
const reviewer = () => JSON.parse(readFileSync(join(rolesDir, 'reviewer.json'), 'utf8'));
const pinOf = (ref, definitions = {}) => definitionPin(ref, { definitions, rolesDir, readTemplate });
const INC_A = 'a'.repeat(32), INC_B = 'b'.repeat(32);
const docFor = (id, edit = d => d) => edit({ id, roles: {}, projects: { stack: {}, web: {} } });
const record = (over = {}) => ({
copyId: 'c'.repeat(32), orgIncarnation: INC_A, source: { level: 'root', name: 'rev', project: null },
definition: pinOf('reviewer'), generation: 2, state: 'bound',
services: { gitea: { slot: 'self', scopes: ['read:repository', 'write:issue'], resources: { projects: ['stack'] } },
vikunja: { slot: 'self', scopes: { tasks: ['read_one'] }, resources: { projects: ['stack'] } } },
decision: ID1, ...over,
});
const resolve = (over = {}) => {
const snapshot = over.snapshot ?? materializeCopies({ orgId: 'acme', doc: docFor('acme'), root: rootOf({ rev: { definition: 'reviewer' } }), rolesDir });
return resolveCopyBinding({ orgId: 'acme', incarnation: INC_A, snapshot, instance: 'rev', record: record(), pin: pinOf('reviewer'), role: reviewer(), projects: ['stack', 'web'], ...over });
};
test('CB0 a bound record resolves to effective = role intersect approved, only on admitted projects', () => {
const r = resolve();
assert.equal(r.ok, true);
// The role declares write:issue, write:repository and read:user; the owner approved less.
assert.deepEqual(r.services.gitea, { scopes: ['write:issue', 'read:repository'], projects: ['stack'] });
assert.deepEqual(r.services.vikunja, { scopes: { tasks: ['read_one'] }, projects: ['stack'] });
assert.deepEqual(intersectScopes('gitea', ['write:repository', 'read:issue'], ['read:repository', 'write:issue']), ['read:issue', 'read:repository']);
// A project no longer admitted drops out of the reach; none left, no binding.
assert.equal(resolve({ projects: ['web'] }).code, 'credential-unbound');
// Approved broader than the role (the role narrowed): never widened, refused.
const wide = record(); wide.services.gitea.scopes = ['write:admin'];
assert.equal(resolve({ record: wide }).ok, false);
const extra = record(); extra.services.gitea.scopes = ['write:repository', 'write:organization'];
assert.deepEqual([resolve({ record: extra }).code, resolve({ record: extra }).service], ['credential-scope-exceeded', 'gitea']);
});
test('CB0 pending, partial and tombstoned records never resolve', () => {
assert.equal(resolve({ record: record({ state: 'pending' }) }).code, 'credential-unbound');
assert.equal(resolve({ record: record({ state: 'unbound', services: {} }) }).code, 'credential-unbound');
const partial = record(); delete partial.services.vikunja;
assert.deepEqual([resolve({ record: partial }).code, resolve({ record: partial }).service], ['credential-unbound', 'vikunja']);
assert.equal(resolve({ record: undefined }).code, 'credential-unbound');
});
test('CB0 identical root copies in two organizations never share a binding', () => {
const root = rootOf({ rev: { definition: 'reviewer' } });
const a = materializeCopies({ orgId: 'acme', doc: docFor('acme'), root, rolesDir });
const b = materializeCopies({ orgId: 'beta', doc: docFor('beta'), root, rolesDir });
assert.deepEqual({ ...a.copies.rev }, { ...b.copies.rev }, 'the same copy shape in both');
assert.equal(resolve({ snapshot: a }).ok, true);
// A's record copied into B carries A's incarnation.
assert.equal(resolveCopyBinding({ orgId: 'beta', incarnation: INC_B, snapshot: b, instance: 'rev', record: record(), pin: pinOf('reviewer'), role: reviewer(), projects: ['stack'] }).code, 'credential-unbound');
// A's snapshot never answers for B.
assert.equal(resolveCopyBinding({ orgId: 'beta', incarnation: INC_A, snapshot: a, instance: 'rev', record: record(), pin: pinOf('reviewer'), role: reviewer(), projects: ['stack'] }).code, 'copy-unavailable');
});
test('CB0 project dotted-name collisions never attach a record', () => {
const root = rootOf({});
// project "a.b" agent "c" and project "b" agent "c.a" both make instance c.a.b: both refuse.
const both = materializeCopies({ orgId: 'acme', doc: docFor('acme', d => ({ ...d, projects: { 'a.b': { agents: { c: { definition: 'reviewer' } } }, b: { agents: { 'c.a': { definition: 'reviewer' } } } } })), root, rolesDir });
assert.equal(both.copies['c.a.b'].state, 'refused');
const old = record({ source: { level: 'project', name: 'c.a', project: 'b' }, services: { gitea: { slot: 'self', scopes: ['read:repository'], resources: { projects: ['b'] } } } });
const args = { orgId: 'acme', incarnation: INC_A, instance: 'c.a.b', record: old, pin: pinOf('reviewer'), role: reviewer(), projects: ['a.b', 'b'] };
assert.equal(resolveCopyBinding({ ...args, snapshot: both }).code, 'copy-refused');
// Project b drops its agent; project a.b's "c" now owns c.a.b alone. b's record never attaches.
const one = materializeCopies({ orgId: 'acme', doc: docFor('acme', d => ({ ...d, projects: { 'a.b': { agents: { c: { definition: 'reviewer' } } }, b: {} } })), root, rolesDir });
assert.equal(one.copies['c.a.b'].state, 'unbound');
assert.equal(resolveCopyBinding({ ...args, snapshot: one, record: { ...old, state: 'bound' } }).code, 'credential-unbound');
// A root agent named like a project copy: the root record never attaches to the project copy.
const proj = materializeCopies({ orgId: 'acme', doc: docFor('acme', d => ({ ...d, projects: { stack: { agents: { checker: { definition: 'reviewer' } } } } })), root, rolesDir });
const rootRecord = record({ source: { level: 'root', name: 'checker.stack', project: null } });
assert.equal(resolveCopyBinding({ ...args, instance: 'checker.stack', snapshot: proj, record: rootRecord, projects: ['stack'] }).code, 'credential-unbound');
});
test('CB0 an organization instance shadows the copy; its record never resolves', () => {
const snapshot = materializeCopies({ orgId: 'acme', doc: docFor('acme', d => ({ ...d, roles: { rev: { definition: 'reviewer' } } })), root: rootOf({ rev: { definition: 'reviewer' } }), rolesDir });
assert.equal(Object.hasOwn(snapshot.copies, 'rev'), false);
assert.equal(resolve({ snapshot }).code, 'copy-unavailable');
});
test('CB0 a removed and recreated organization has a new incarnation; the old record never resolves', async t => {
const base = mkdtempSync(join(tmpdir(), 'cb0-')); t.after(() => rmSync(base, { recursive: true, force: true }));
const c = { configDir: join(base, 'config'), dataRoot: join(base, 'data'), instance: 'test' };
mkdirSync(c.configDir, { mode: 0o700 }); mkdirSync(c.dataRoot, { mode: 0o700 });
const first = await reserveOrg(c, 'acme');
assert.match(first.incarnation, /^[0-9a-f]{32}$/);
assert.notEqual((await reserveOrg(c, 'beta')).incarnation, first.incarnation);
withRegistryWriter(c.configDir, (registry, publish) => { delete registry.orgs.acme; publish(); });
rmSync(orgRoot(c.dataRoot, 'acme'), { recursive: true });
const again = await reserveOrg(c, 'acme');
assert.notEqual(again.incarnation, first.incarnation);
assert.equal(orgIncarnation(await readRegistry(c), 'acme'), again.incarnation);
assert.equal(resolve({ incarnation: again.incarnation, record: record({ orgIncarnation: first.incarnation }) }).code, 'credential-unbound');
assert.equal(resolve({ incarnation: null }).code, 'credential-unbound', 'no incarnation, no binding');
// An invalid incarnation in the registry refuses the registry.
withRegistryWriter(c.configDir, (registry, publish) => { registry.orgs.acme.incarnation = 'x'; publish(); });
await assert.rejects(readRegistry(c), { code: 'org-mismatch' });
});
test('CB0 a reused source name with another definition or level never inherits a record', () => {
// Root "rev" was a reviewer; it is removed and a new root "rev" is a coder.
const snapshot = materializeCopies({ orgId: 'acme', doc: docFor('acme'), root: rootOf({ rev: { definition: 'coder' } }), rolesDir });
const coder = JSON.parse(readFileSync(join(rolesDir, 'coder.json'), 'utf8'));
assert.equal(resolve({ snapshot, pin: pinOf('coder'), role: coder }).code, 'credential-role-changed');
// An org definition changed in place (same name, new rev or text) changes the pin.
const defs = { d: { rev: 1, role: { template: 'reviewer' } } };
const before = pinOf('org:d', defs), after = pinOf('org:d', { d: { ...defs.d, instructions: 'other' } });
assert.ok(before && after && before.sha256 !== after.sha256);
// The copyId is kept only with its own record: a record for another copyId does not move.
assert.equal(resolve({ record: record({ source: { level: 'project', name: 'rev', project: 'stack' } }) }).code, 'credential-unbound');
});
test('CB0 copyBindings schema: ids and scope names only, every refusal positional', () => {
const ok = { rev: record() };
assert.doesNotThrow(() => validateCopyBindings(ok));
const cases = [
{ rev: { ...record(), token: SECRET } },
{ rev: { ...record(), services: { gitea: { ...record().services.gitea, file: '/x/token' } } } },
{ rev: { ...record(), services: { gitea: { ...record().services.gitea, slot: '/srv/token' } } } },
{ rev: { ...record(), copyId: SECRET } },
{ rev: { ...record(), orgIncarnation: 'z' } },
{ rev: { ...record(), generation: 0 } },
{ rev: { ...record(), state: 'active' } },
{ rev: { ...record(), source: { level: 'root', name: 'other', project: null } } },
{ rev: { ...record(), state: 'unbound' } },
{ rev: record(), other: { ...record(), source: { level: 'root', name: 'other', project: null } } },
];
for (const value of cases) {
assert.throws(() => validateCopyBindings(value), e => e.name === 'BusinessError' && !e.message.includes(SECRET) && !e.message.includes('/'));
}
});
// ---- CB1: decision subtype, guarded writer ----
function setup(t, { incarnation = true, edit = d => d } = {}) {
const root = mkdtempSync(join(tmpdir(), 'cb1-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
const roles = rolesCopy(root);
const doc = { ...businessDoc(root, 'demo'), plugins: { harness: ['pi'] } };
doc.projects.stack.agents = { checker: { definition: 'reviewer' } };
edit(doc);
const file = writeJson(join(root, 'businesses/demo.json'), doc);
const dataRoot = join(root, 'data'); mkdirSync(dataRoot, { mode: 0o700 });
const ctx = { configDir: root, dataRoot, instance: 'test' };
const ready = (async () => {
await reserveOrg(ctx, 'demo'); await activateOrg(ctx, 'demo');
if (!incarnation) withRegistryWriter(root, (registry, publish) => { delete registry.orgs.demo.incarnation; publish(); });
})();
const snapshot = materializeCopies({ orgId: 'demo', doc, root: { state: 'ok', ...checkRootAgents({ agentsVersion: 1, agents: { rev: { definition: 'reviewer' } } }, { rolesDir: roles }) }, rolesDir: roles });
const writer = organizationWriter({ dir: root, rolesDir: roles });
const service = hireDecisions({ writer, rolesDir: roles, configDir: root, dataRoot, assertClean: v => { if (JSON.stringify(v).includes(SECRET)) throw Object.assign(new Error('x'), { code: 'definition-holds-credential' }); }, copies: id => snapshot.org === id ? snapshot : null });
const bind = (services = GITEA, extra = {}) => ({ op: 'change', level: 'root', name: 'rev', reason: 'Bind the reviewer copy', binding: { services }, ...extra });
const prepare = async (input = bind(), id = ID1, expiresAt = FUTURE) => { await ready; return service.prepare('demo', input, { id, expiresAt }); };
const bytes = () => readFileSync(file);
return { root, roles, file, writer, service, snapshot, bind, prepare, bytes, ready, ctx, dataRoot };
}
test('CB1 owner binds and unbinds; the record holds ids only and stays pending; receipts are paths only', async t => {
const f = setup(t);
const p = await f.prepare();
assert.equal(p.binding.instance, 'rev');
assert.deepEqual(p.credentialLines, [{ service: 'gitea', scope: 'read:repository' }]);
await f.service.apply('demo', p.id, { ...OWNER, digest: p.digest });
const rec = f.writer.read('demo').document.copyBindings.rev;
assert.equal(rec.state, 'pending');
assert.equal(rec.generation, 1);
assert.deepEqual(rec.services, { gitea: { slot: 'self', scopes: ['read:repository'], resources: { projects: ['stack'] } } });
assert.deepEqual(rec.source, { level: 'root', name: 'rev', project: null });
assert.equal(rec.orgIncarnation, orgIncarnation(await readRegistry(f.ctx), 'demo'));
assert.deepEqual(rec.definition, definitionPin('reviewer', { rolesDir: f.roles, readTemplate }));
assert.equal(rec.decision, p.id);
const text = JSON.stringify(rec);
for (const bad of [SECRET, f.root, '/', 'token']) assert.ok(!text.includes(bad), bad);
const log = readFileSync(join(f.root, 'businesses/.history/demo/changes.jsonl'), 'utf8');
assert.ok(log.includes(p.id)); assert.ok(log.includes('binding.apply'));
assert.ok(!log.includes('read:repository'), 'paths only: the receipt holds no scope value');
// The owner unbinds through the same decision kind; a tombstone stays with the same copyId.
const u = await f.prepare(f.bind(null), ID2);
await f.service.apply('demo', u.id, { ...OWNER, digest: u.digest });
const tomb = f.writer.read('demo').document.copyBindings.rev;
assert.deepEqual({ state: tomb.state, services: tomb.services, generation: tomb.generation, copyId: tomb.copyId }, { state: 'unbound', services: {}, generation: 2, copyId: rec.copyId });
assert.ok(readFileSync(join(f.root, 'businesses/.history/demo/changes.jsonl'), 'utf8').includes('binding.unbind'));
// Unbinding a tombstone refuses.
await assert.rejects(f.prepare(f.bind(null), ID3), { code: 'credential-unbound' });
});
test('CB1 a project copy binds only its own project; a role-exceeding request refuses, never trims', async t => {
const f = setup(t);
const project = (services, id) => f.prepare({ op: 'change', level: 'project', project: 'stack', name: 'checker', reason: 'Bind', binding: { services } }, id);
const p = await project(GITEA, ID1);
assert.equal(p.binding.instance, 'checker.stack');
await assert.rejects(project({ gitea: { scopes: ['write:admin'], projects: ['stack'] } }, ID2), { code: 'invalid-request' });
await assert.rejects(project({ gitea: { scopes: ['write:organization'], projects: ['stack'] } }, ID2), { code: 'credential-scope-exceeded' });
await assert.rejects(project({ gitea: { scopes: ['read:repository'], projects: ['other'] } }, ID2), { code: 'credential-scope-exceeded' });
await assert.rejects(f.prepare(f.bind({ gitea: { scopes: ['read:repository'], projects: ['stack'], token: SECRET } }), ID2), e => !JSON.stringify(e).includes(SECRET) && !e.message.includes(SECRET));
// A copy the bus did not boot, and an org instance, are not copies.
await assert.rejects(f.prepare({ ...f.bind(), name: 'nobody' }, ID2), { code: 'copy-unavailable' });
await assert.rejects(f.prepare({ ...f.bind(), name: 'coder' }, ID2), { code: 'copy-unavailable' });
// A binding is only ever a change of a root or project copy, with no role, route or credentials.
for (const extra of [{ op: 'hire' }, { level: 'org' }, { route: { harness: 'pi', model: 'm' } }, { credentials: {} }]) {
await assert.rejects(f.prepare({ ...f.bind(), ...extra }, ID2), { code: 'invalid-request' });
}
});
test('CB1 only the owner resolves: member with org.hire or decide, chief, arbiter, subject, tester and reader all fail', async t => {
const plain = { binding: {}, flagged: [], credentialLines: [], level: 'root', id: ID1, name: 'rev' };
const member = { ...OWNER, owner: false, instanceOwner: false };
assert.throws(() => authorizeHire(plain, member), { code: 'human-required' });
assert.throws(() => authorizeHire(plain, { ...member, instanceOwner: true }), { code: 'human-required' }, 'an instance operator alone is not the owner');
assert.throws(() => authorizeHire(plain, { ...OWNER, rights: ['decide'] }), { code: 'right-required' });
assert.throws(() => authorizeHire(plain, { ...OWNER, rights: [] }), { code: 'right-required' }, 'reader');
for (const agent of ['chief', 'cto', 'rev', 'tester']) {
// An agent, even one carrying an authority decision and the authority right, never resolves.
assert.throws(() => authorizeHire(plain, { actor: agent, agent: true, rights: ['org.hire', 'decision.resolve.authority'], owner: true, authorityDecision: ID1, resolver: agent }), { code: 'human-required' });
}
assert.doesNotThrow(() => authorizeHire(plain, OWNER));
// Through the service: each refused caller leaves the file byte-identical.
const f = setup(t);
const p = await f.prepare();
const before = f.bytes();
for (const caller of [member, { ...OWNER, rights: ['decide'] }, { ...OWNER, rights: [] },
{ actor: 'chief', agent: true, rights: ['org.hire'], owner: true }, { actor: 'cto', agent: true, rights: ['decision.resolve.authority', 'org.hire'], authorityDecision: p.id, resolver: 'cto' },
{ actor: 'rev', agent: true, rights: ['org.hire'] }, { actor: 'tester', agent: true, rights: ['org.hire'] }]) {
await assert.rejects(f.service.apply('demo', p.id, { ...caller, digest: p.digest }));
}
assert.deepEqual(f.bytes(), before);
// The writer refuses the tester and every agent identity even when told owner:true.
const full = { ...JSON.parse(readFileSync(join(f.dataRoot, 'authority-proposals', `${p.id}.json`), 'utf8')), digest: p.digest };
for (const actor of ['tester', 'chief', 'rev', 'checker.stack', 'reviewer', 'pm']) {
assert.throws(() => f.writer.applyCopyBindingDecision('demo', full, { actor, owner: true }), { code: 'human-required' }, actor);
}
assert.throws(() => f.writer.applyCopyBindingDecision('demo', full, { actor: 'jason', owner: false }), { code: 'human-required' });
assert.deepEqual(f.bytes(), before);
});
test('CB1 generic writes never change copyBindings, whatever options they pass', async t => {
const f = setup(t); await f.ready;
const { document, revision } = f.writer.read('demo');
const forged = { ...document, copyBindings: { rev: { ...record(), orgIncarnation: orgIncarnation(await readRegistry(f.ctx), 'demo'), state: 'bound' } } };
assert.doesNotThrow(() => validateBusinessDocument(forged, f.file, { rolesDir: f.roles }), 'a valid document is still refused by the writer');
const before = f.bytes();
for (const options of [{}, { capability: Symbol('copy-binding-write') }, { capability: Symbol.for('copy-binding-write'), incarnation: forged.copyBindings.rev.orgIncarnation }, { guard: () => {} }]) {
assert.throws(() => f.writer.write('demo', forged, { revision, actor: 'jason', ...options }), { code: 'organization-bindings-protected' });
}
assert.throws(() => f.writer.write('demo', old => ({ ...old, copyBindings: forged.copyBindings }), { revision, actor: 'jason' }), { code: 'organization-bindings-protected' });
assert.deepEqual(f.bytes(), before);
// Once a record exists, a generic write can neither remove nor edit it.
const p = await f.prepare(); await f.service.apply('demo', p.id, { ...OWNER, digest: p.digest });
const now = f.writer.read('demo');
const { copyBindings: _, ...stripped } = now.document;
assert.throws(() => f.writer.write('demo', stripped, { revision: now.revision, actor: 'jason' }), { code: 'organization-bindings-protected' });
const promoted = structuredClone(now.document); promoted.copyBindings.rev.state = 'bound';
assert.throws(() => f.writer.write('demo', promoted, { revision: now.revision, actor: 'jason' }), { code: 'organization-bindings-protected' });
// An unrelated write keeps the record.
const edited = structuredClone(now.document); edited.vars['tracker.pollSeconds'] = 61;
f.writer.write('demo', edited, { revision: now.revision, actor: 'jason' });
assert.deepEqual(f.writer.read('demo').document.copyBindings, now.document.copyBindings);
});
test('CB1 stale revision, digest or generation, expiry, replay and a wrong incarnation activate nothing', async t => {
const f = setup(t);
// Digest.
const p = await f.prepare();
const before = f.bytes();
await assert.rejects(f.service.apply('demo', p.id, { ...OWNER, digest: '0'.repeat(64) }), { code: 'decision-stale' });
// Expiry.
await assert.rejects(f.service.apply('demo', p.id, { ...OWNER, digest: p.digest, now: () => Date.parse('2099-06-01T00:00:00Z') }), { code: 'decision-expired' });
assert.deepEqual(f.bytes(), before);
// Revision: any change to the file after the proposal.
const q = await f.prepare(f.bind(), ID2);
const r = f.writer.read('demo'), edited = structuredClone(r.document); edited.vars['tracker.pollSeconds'] = 61;
f.writer.write('demo', edited, { revision: r.revision, actor: 'jason' });
await assert.rejects(f.service.apply('demo', q.id, { ...OWNER, digest: q.digest }), { code: 'decision-stale' });
assert.equal(f.writer.read('demo').document.copyBindings, undefined);
// Replay: applying the same approval twice changes nothing the second time.
const s = await f.prepare(f.bind(), ID3);
await f.service.apply('demo', s.id, { ...OWNER, digest: s.digest });
const once = f.bytes();
await assert.rejects(f.service.apply('demo', s.id, { ...OWNER, digest: s.digest }), { code: 'decision-stale' });
const full = { ...JSON.parse(readFileSync(join(f.dataRoot, 'authority-proposals', `${s.id}.json`), 'utf8')), digest: s.digest };
// Generation: the writer refuses a proposal at an old generation even with a fresh revision.
assert.throws(() => f.writer.applyCopyBindingDecision('demo', { ...full, revision: f.writer.read('demo').revision }, { actor: 'jason', owner: true }), { code: 'credential-binding-changed' });
assert.deepEqual(f.bytes(), once);
// Wrong incarnation: the organization was recreated under the same id after approval.
const u = await f.prepare(f.bind(null), '44444444-4444-4444-8444-444444444444');
withRegistryWriter(f.root, (registry, publish) => { registry.orgs.demo.incarnation = 'f'.repeat(32); publish(); });
await assert.rejects(f.service.apply('demo', u.id, { ...OWNER, digest: u.digest }), { code: 'decision-stale' });
const uf = { ...JSON.parse(readFileSync(join(f.dataRoot, 'authority-proposals', `${u.id}.json`), 'utf8')), digest: u.digest };
assert.throws(() => f.writer.applyCopyBindingDecision('demo', uf, { actor: 'jason', owner: true }), { code: 'decision-stale' });
assert.deepEqual(f.bytes(), once);
// A changed definition pin refuses too.
assert.throws(() => f.writer.applyCopyBindingDecision('demo', { ...uf, binding: { ...uf.binding, orgIncarnation: 'f'.repeat(32), definition: { ...uf.binding.definition, sha256: '0'.repeat(64) } } }, { actor: 'jason', owner: true }), { code: 'credential-role-changed' });
// A binding is never reversed through the AU inverse, and never adjusted.
await assert.rejects(f.service.reverse('demo', s.id, OWNER), { code: 'decision-stale' });
await assert.rejects(f.service.check('demo', u.id, { ...OWNER, digest: u.digest, adjust: { role: {} } }), { code: 'invalid-request' });
});
test('CB1 a legacy organization gets its incarnation at the first proposal, under the registry lock', async t => {
const f = setup(t, { incarnation: false });
await f.ready;
assert.equal(orgIncarnation(await readRegistry(f.ctx), 'demo'), null);
const p = await f.prepare();
const inc = orgIncarnation(await readRegistry(f.ctx), 'demo');
assert.match(inc, /^[0-9a-f]{32}$/);
assert.equal(p.binding.orgIncarnation, inc);
assert.equal(ensureOrgIncarnation(f.root, 'demo'), inc, 'minted once');
assert.equal(ensureOrgIncarnation(f.root, 'nobody'), null);
});
test('CB1 a pending or partial binding never launches the copy', async t => {
const f = setup(t);
const p = await f.prepare();
await f.service.apply('demo', p.id, { ...OWNER, digest: p.digest });
const business = loadWithCopies('demo', { dir: f.root, rolesDir: f.roles, snapshot: f.snapshot });
assert.equal(copyRefusal(business, 'rev').code, 'credential-unbound');
assert.equal(Object.hasOwn(business.roles, 'rev'), false);
const rec = business.copyBindings.rev;
assert.equal(rec.state, 'pending');
const pin = definitionPin('reviewer', { rolesDir: f.roles, readTemplate });
const role = JSON.parse(readFileSync(join(f.roles, 'reviewer.json'), 'utf8'));
const args = { orgId: 'demo', incarnation: rec.orgIncarnation, snapshot: f.snapshot, instance: 'rev', pin, role, projects: ['stack'] };
assert.equal(resolveCopyBinding({ ...args, record: rec }).code, 'credential-unbound');
// Even forced to bound, gitea alone is partial for a role that also needs vikunja.
assert.deepEqual(resolveCopyBinding({ ...args, record: { ...rec, state: 'bound' } }), { ok: false, code: 'credential-unbound', service: 'vikunja' });
// A pending record that covers every service the role needs is still not a binding; bound, it is.
const whole = { ...rec, services: { ...rec.services, vikunja: { slot: 'self', scopes: { tasks: ['read_one'] }, resources: { projects: ['stack'] } } } };
assert.equal(resolveCopyBinding({ ...args, record: { ...whole, state: 'bound' } }).ok, true);
assert.deepEqual(resolveCopyBinding({ ...args, record: whole }), { ok: false, code: 'credential-unbound', service: 'gitea' });
});
test('CB1 no proposal, record, receipt or refusal names a token or a path', async t => {
const f = setup(t);
const p = await f.prepare();
await f.service.apply('demo', p.id, { ...OWNER, digest: p.digest });
const texts = [JSON.stringify(p), readFileSync(join(f.root, 'businesses/.history/demo/changes.jsonl'), 'utf8'),
...readdirSync(join(f.dataRoot, 'authority-proposals')).map(n => readFileSync(join(f.dataRoot, 'authority-proposals', n), 'utf8'))];
for (const text of texts) {
assert.ok(!text.includes(SECRET)); assert.ok(!text.includes(join(f.root, 'secrets')));
}
const refusals = [];
for (const args of [[f.bind({ gitea: { scopes: ['write:organization'], projects: ['stack'] } }), ID2], [{ ...f.bind(), name: 'gone' }, ID3]]) {
await f.prepare(...args).catch(e => refusals.push(e));
}
const t2 = { ...JSON.parse(readFileSync(join(f.dataRoot, 'authority-proposals', `${p.id}.json`), 'utf8')) };
try { f.writer.applyCopyBindingDecision('demo', { ...t2, revision: f.writer.read('demo').revision }, { actor: 'chief', owner: true }); } catch (e) { refusals.push(e); }
assert.equal(refusals.length, 3);
for (const e of refusals) assert.ok(!e.message.includes('/') && !e.message.includes(SECRET), e.message);
assert.equal(refusals[2].message, 'Only the organization owner can approve credential binding.');
void sha;
});
test('CB1 fix: the free-text reason is never written to a binding proposal', async t => {
const f = setup(t);
const leak = '/home/someone/.config/mosaic-dev/secrets/gitea tok-fragment-9f8e';
const p = await f.prepare(f.bind(GITEA, { reason: `Bind; see ${leak}` }));
const stored = readFileSync(join(f.dataRoot, 'authority-proposals', `${p.id}.json`), 'utf8');
assert.ok(!stored.includes(leak) && !stored.includes('tok-fragment') && !stored.includes('"reason"'), 'no reason text on disk');
assert.deepEqual(Object.keys(JSON.parse(stored).request).sort(), ['binding', 'level', 'name', 'op']);
assert.ok(!JSON.stringify(p).includes('tok-fragment'));
// The proposal still applies on its digest.
await f.service.apply('demo', p.id, { ...OWNER, digest: p.digest });
assert.equal(f.writer.read('demo').document.copyBindings.rev.state, 'pending');
assert.ok(!readFileSync(join(f.root, 'businesses/.history/demo/changes.jsonl'), 'utf8').includes('tok-fragment'));
});
test('CB1 fix: a root copy removed or replaced since boot refuses at prepare, at apply and before publication', async t => {
const receipts = root => join(root, 'agents-receipts.jsonl');
const add = (root, row) => writeFileSync(receipts(root), JSON.stringify(row) + '\n', { flag: 'a', mode: 0o600 });
// Removed since boot: prepare refuses instance-fired; replaced: copy-stale.
for (const [verb, code] of [['root.agent.remove', 'instance-fired'], ['root.agent.set', 'copy-stale']]) {
const f = setup(t); await f.ready;
add(f.root, { agent: 'rev', verb, at: FUTURE });
await assert.rejects(f.prepare(), { code }, verb);
let saved = []; try { saved = readdirSync(join(f.dataRoot, 'authority-proposals')); } catch (e) { if (e.code !== 'ENOENT') throw e; }
assert.deepEqual(saved, [], verb);
}
// Prepared while fresh, then the root copy is removed: apply refuses and writes nothing.
{
const f = setup(t); const p = await f.prepare(); const before = f.bytes();
add(f.root, { agent: 'rev', verb: 'root.agent.remove', at: FUTURE });
await assert.rejects(f.service.apply('demo', p.id, { ...OWNER, digest: p.digest }), { code: 'instance-fired' });
assert.deepEqual(f.bytes(), before);
}
// The change lands after checkBinding, under the lock: the publication fence still refuses.
{
const f = setup(t); const p = await f.prepare(); const before = f.bytes();
let wrote = false;
const late = () => { if (!wrote) { wrote = true; add(f.root, { agent: 'rev', verb: 'root.agent.set', at: FUTURE }); } };
await assert.rejects(f.service.apply('demo', p.id, { ...OWNER, digest: p.digest, beforeWrite: late }), { code: 'copy-stale' });
assert.equal(wrote, true);
assert.deepEqual(f.bytes(), before);
}
// A receipt for another root agent leaves this copy fresh; an unreadable receipts file refuses.
{
const f = setup(t); await f.ready;
add(f.root, { agent: 'other', verb: 'root.agent.remove', at: FUTURE });
const p = await f.prepare();
writeFileSync(receipts(f.root), 'not json\n', { mode: 0o600 });
await assert.rejects(f.service.apply('demo', p.id, { ...OWNER, digest: p.digest }), { code: 'root-policy-unreadable' });
}
// A project copy does not read root receipts.
{
const f = setup(t); await f.ready;
add(f.root, { agent: 'checker', verb: 'root.agent.remove', at: FUTURE });
const p = await f.prepare({ op: 'change', level: 'project', project: 'stack', name: 'checker', reason: 'Bind', binding: { services: GITEA } });
await f.service.apply('demo', p.id, { ...OWNER, digest: p.digest });
assert.equal(f.writer.read('demo').document.copyBindings['checker.stack'].state, 'pending');
}
});
test('CB1 the org service refuses copyBindings on create and on a whole-document update', async t => {
const root = mkdtempSync(join(tmpdir(), 'cb1-svc-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
const c = { configDir: join(root, 'config'), dataRoot: join(root, 'data'), instance: 'test', principal: { provider: 'local-dev', iss: 'local-dev', sub: 'local-dev-owner' } };
mkdirSync(c.configDir, { mode: 0o700 }); mkdirSync(c.dataRoot, { mode: 0o700 });
const forged = { rev: { ...record(), state: 'bound' } };
await assert.rejects(createOrg(c, 'alpha', { ...await templateRender('alpha'), copyBindings: forged }), { code: 'organization-bindings-protected' });
await createOrg(c, 'alpha', await templateRender('alpha'));
const current = await showOrg(c, 'alpha');
await assert.rejects(updateOrg(c, 'alpha', { ...current.doc, copyBindings: forged }, { ifRevision: current.revision }), { code: 'organization-bindings-protected' });
assert.equal((await showOrg(c, 'alpha')).doc.copyBindings, undefined);
await updateOrg(c, 'alpha', current.doc, { ifRevision: current.revision });
});
test('CB1 fix: a root copy binds only projects whose union still admits it', async t => {
const f = setup(t, { edit: doc => { doc.projects.web = { root: doc.projects.stack.root }; doc.projects.stack.agents.rev = { exclude: true }; } });
// stack excludes rev: a request naming it refuses whole, never trimmed to web.
await assert.rejects(f.prepare(f.bind({ gitea: { scopes: ['read:repository'], projects: ['stack', 'web'] } })), { code: 'credential-scope-exceeded' });
await assert.rejects(f.prepare(f.bind({ gitea: { scopes: ['read:repository'], projects: ['stack'] } }, {}), ID2), { code: 'credential-scope-exceeded' });
// web still admits it.
const p = await f.prepare(f.bind({ gitea: { scopes: ['read:repository'], projects: ['web'] } }), ID3);
assert.deepEqual(p.binding.after.gitea.resources.projects, ['web']);
});
test('CB1 fix: a named instance added after boot shadows the copy at proposal time', async t => {
const f = setup(t); await f.ready;
const r = f.writer.read('demo'), edited = structuredClone(r.document);
const like = Object.values(edited.roles).find(e => e.definition === 'reviewer');
edited.roles.rev = { ...structuredClone(like), tracker: { bot: 'bot-demo-rev', botId: 977 } };
f.writer.write('demo', edited, { revision: r.revision, actor: 'jason' });
await assert.rejects(f.prepare(), { code: 'copy-unavailable' });
});
test('CB1 fix: the loader refuses a copyBindings record holding a token, naming neither the value nor a path', t => {
const root = mkdtempSync(join(tmpdir(), 'cb1-load-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
const roles = rolesCopy(root);
const doc = { ...businessDoc(root, 'demo'), copyBindings: { rev: { ...record(), state: 'pending', token: SECRET } } };
const file = join(root, 'businesses/demo.json');
assert.throws(() => validateBusinessDocument(doc, file, { rolesDir: roles }), e => e.name === 'BusinessError' && !e.message.includes(SECRET) && !e.message.includes(root));
// The same document without the token loads.
const { token: _, ...clean } = doc.copyBindings.rev;
validateBusinessDocument({ ...doc, copyBindings: { rev: clean } }, file, { rolesDir: roles });
});