git mv packages/business -> packages/organization (@mosaic/organization). The old package stays as one-line re-export shims of each moved module, with src/business.mjs re-exporting src/organization-config.mjs and src/cli.mjs forwarding to the moved executable. BusinessError is OrganizationConfigError (one constructor, name kept BusinessError); OrganizationError stays distinct. The bus adapter gains organization-context.mjs (busOrganization), with business.mjs (busBusiness) as the same function. Import consumers, manifests, the lockfile, scripts/mosaic, stack-copy.sh COPY_PATHS (both roots), seat sandbox RUNNER_FILES and the copy closure walker (organization-imports.mjs, both roots required) move to the new path. v1 serialization, v1 writers and the stage A fixtures are unchanged; the stage A contract tests stay where they were and pass as before.
59 lines
3.5 KiB
JavaScript
59 lines
3.5 KiB
JavaScript
import { BusError } from './broker.mjs';
|
|
import { isHostKind } from '../../organization/src/vocabulary.mjs';
|
|
// Row 199 (#1697, design section 6): issue, rotate, revoke and delete of one agent's credentials, for
|
|
// the broker's human-only verbs. The broker checks the session, the keys and a running claim first.
|
|
// Each verb leaves one receipt in the organization's changes.jsonl: the actor and a detail naming the
|
|
// verb, agent, kinds, date or reason, never a value. Nothing here logs, and a result carries no token.
|
|
const DATE_KEY = Object.freeze({ gitea: 'rotateBy', vikunja: 'expires' });
|
|
// Row 210 (#1707, design D1): a host.<id> key takes a rotate date, as gitea does.
|
|
const dateKey = (service) => (isHostKind(service) ? 'rotateBy' : DATE_KEY[service]);
|
|
const deny = (code) => {
|
|
throw new BusError(code);
|
|
};
|
|
export function credentialVerbs({ credentials, writer }) {
|
|
const state = (instance, service) =>
|
|
credentials.status().find((e) => e.instance === instance && e.service === service)?.state;
|
|
return (id, verb, args, actor) => {
|
|
const { agent } = args;
|
|
const instance = id + '/' + agent;
|
|
const { document, revision } = writer.read(id);
|
|
const current = document.roles?.[agent]?.credentials;
|
|
if (!current) deny('credential-unknown');
|
|
if (verb === 'credentials.issue') {
|
|
const { service, token, date } = args;
|
|
if (!current[service]) deny('credential-unknown');
|
|
// rotate is an issue over an entry the bus holds now; the old value is gone when this returns.
|
|
const kind = ['not-issued', 'revoked', undefined].includes(state(instance, service)) ? 'issue' : 'rotate';
|
|
const next = structuredClone(document);
|
|
next.roles[agent].credentials[service] = { store: true, [dateKey(service)]: date };
|
|
const detail = { verb: kind, agent, kind: [service], date };
|
|
const status = credentials.issue(instance, service, token, date, () =>
|
|
writer.write(id, next, { revision, actor, detail }),
|
|
);
|
|
// Row 210 (design D9): a host key's public line and fingerprint, for ops-01's authorized_keys.
|
|
const host = status.fingerprint ? { publicKey: status.publicKey, fingerprint: status.fingerprint } : {};
|
|
return { agent, verb: kind, service, date: status.date, state: status.state, ...host };
|
|
}
|
|
const services = verb === 'credentials.revoke' && args.service ? [args.service] : Object.keys(current);
|
|
if (services.some((service) => !current[service])) deny('credential-unknown');
|
|
// Memory first, then the store file (Credentials.revoke), then the business file. A reference
|
|
// outside the store becomes `store` with its date, so a restart loads nothing for it. When that
|
|
// write fails, the entry stays revoked in memory and the refusal says to run the verb again.
|
|
if (verb === 'credentials.delete') credentials.remove(instance);
|
|
else for (const service of services) credentials.revoke(instance, service);
|
|
const detail = { verb: verb.slice('credentials.'.length), agent, kind: services, reason: args.reason };
|
|
const moved = services.filter((service) => current[service].store !== true);
|
|
if (moved.length) {
|
|
const next = structuredClone(document);
|
|
for (const service of moved)
|
|
next.roles[agent].credentials[service] = { store: true, [dateKey(service)]: current[service][dateKey(service)] };
|
|
writer.write(id, next, { revision, actor, detail });
|
|
} else writer.record(id, { actor, detail });
|
|
return {
|
|
agent,
|
|
verb: detail.verb,
|
|
services: services.map((service) => ({ service, state: state(instance, service) ?? 'not-issued' })),
|
|
};
|
|
};
|
|
}
|