import { test } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; const secret = 'sk-test-NOT-A-SECRET-1234'; function scratch(t) { const root = mkdtempSync(join(tmpdir(), 'r4-')); t.after(() => rmSync(root, { recursive: true, force: true })); return root; } function safe(label, Type, props = {}, omitted = secret) { return error => { assert.ok(error instanceof Type, error.stack); for (const [key, value] of Object.entries(props)) assert.equal(error[key], value); assert.ok(error.message.includes(label), error.message); assert.doesNotMatch(error.message + error.stack, /sk-test|NOT-A-SECRET|Unexpected token/); assert.ok(!error.message.includes(omitted), error.message); assert.equal(error.cause, undefined); return true; }; } import { checkAccess } from '../src/access.mjs'; import { checkAppearance } from '../src/appearance.mjs'; import { loadProject } from '../src/project.mjs'; import { validateRoleDocument } from '../src/role.mjs'; import { covers } from '../src/vocabulary.mjs'; import { BusinessError } from '../src/errors.mjs'; const context = { providers: ['test'], peers: [], businesses: { acme: { human: 'jason', instances: ['coder'] } }, ssoSubjects: [] }; const access = () => ({ accessVersion: 1, revision: 1, users: [{ id: 'bean', home: 'local', subjects: [{ provider: 'test', sub: 'user' }] }], grants: [{ id: 'grant', user: 'bean', business: 'acme', projects: ['*'], instances: ['coder'], level: 'viewer' }] }); for (const [field, edit] of [ ['users[0].home', d => { d.users[0].home = secret; }], ['users[0].subjects[0].provider', d => { d.users[0].subjects[0].provider = secret; }], ...['user', 'business', 'level'].map(k => [`grants[0].${k}`, d => { d.grants[0][k] = secret; }]), ['grants[0].projects[0]', d => { d.grants[0].projects = [secret]; }], ['accessVersion', d => { d.accessVersion = secret; }], ]) test(`access ${field} omits the refused value`, () => { const d = access(); edit(d); assert.throws(() => checkAccess(d, context), safe(field, BusinessError, { exitCode: 2 })); }); for (const k of ['theme', 'mode']) test(`appearance ${k} omits its value`, () => { assert.throws(() => checkAppearance({ [k]: secret }, 'appearance'), safe(k, BusinessError, { exitCode: 2 })); }); test('project root omits its configured value', () => assert.throws(() => loadProject(secret), safe('project root', BusinessError, { exitCode: 2 }))); test('covers names the level field, not its value', () => assert.throws(() => covers({}, {}, new Date(), { level: secret }), safe('level', TypeError))); const role = () => ({ roleVersion: 2, name: 'r', title: 'R', contract: 'r.md', tools: ['read'], network: 'none', authority: { withinRole: [], crossRole: [] }, credentials: [] }); for (const [field, edit] of [ ['skill', d => { d.skills = [secret + '/']; }], ['contract', d => { d.contract = secret; }], ['tool', d => { d.tools = [secret]; }], ['authority.withinRole', d => { d.authority.withinRole = [secret]; }], ['hireable', d => { d.hireable = [secret.toLowerCase()]; }], ['gitea scope', d => { d.credentials = [{ service: 'gitea', scopes: [secret] }]; }], ['vikunja route group', d => { d.credentials = [{ service: 'vikunja', scopes: { [secret]: ['read'] } }]; }], ['vikunja verb', d => { d.credentials = [{ service: 'vikunja', scopes: { tasks: [secret] } }]; }], ['name', d => { d.name = secret.toLowerCase(); }], ]) test(`role ${field} omits the configured value`, t => { const root = scratch(t); writeFileSync(join(root, 'r.md'), '# R\n\n## Authority\n'); const d = role(); edit(d); assert.throws(() => validateRoleDocument(d, join(root, 'r.json')), safe(field, BusinessError, { exitCode: 2 }, secret.toLowerCase())); }); // This branch is reachable only with already-validated service names; a sentinel service // is refused earlier. A known service is already displayed in the role's rails. test('creative service refusal names the validated service list', t => { const root = scratch(t); writeFileSync(join(root, 'companion.md'), '# R\n\n## Authority\n'); const d = { ...role(), name: 'companion', contract: 'companion.md', messaging: { sendTo: ['human'], receiveFrom: ['human'] }, credentials: [{ service: 'vikunja', scopes: { tasks: ['read_one'] } }] }; assert.throws(() => validateRoleDocument(d, join(root, 'companion.json')), named('credential', 'vikunja')); }); test('role duplicate skill omits the accepted list value', t => { const root = scratch(t); writeFileSync(join(root, 'r.md'), '# R\n\n## Authority\n'); const d = { ...role(), skills: [secret, secret] }; assert.throws(() => validateRoleDocument(d, join(root, 'r.json')), safe('role skills', BusinessError, { exitCode: 2 })); }); for (const at of ['user', 'grant', 'scope', 'human']) test(`access ${at} duplicate/reserved refusal names accepted ids`, () => { const value = secret.toLowerCase(), d = access(), ctx = structuredClone(context); if (at === 'user') { d.users[0].id = value; d.users.push({ ...structuredClone(d.users[0]), subjects: [{ provider: 'test', sub: 'second' }] }); } if (at === 'grant') { d.grants[0].id = value; d.grants.push(structuredClone(d.grants[0])); } if (at === 'scope') { ctx.businesses.acme.instances = [value]; d.grants[0].instances = [value, value]; } if (at === 'human') { ctx.businesses.acme.human = value; d.users[0].id = value; } assert.throws(() => checkAccess(d, ctx), named(at === 'scope' ? 'instances' : '.id', value)); }); function named(label, value) { return error => { assert.ok(error instanceof BusinessError); assert.equal(error.exitCode, 2); assert.ok(error.message.includes(label), error.message); assert.ok(error.message.includes(value), error.message); return true; }; }