Files
stack/packages/cli/src/config.mjs
T

141 lines
6.2 KiB
JavaScript

// What the host and the human commands read before they touch the bus:
// the system config (through scripts/mosaic-config.mjs, as `mosaic business`
// does), the business file, and the boot message for the broker process.
// Everything here reads and refuses; nothing writes.
import { spawnSync } from "node:child_process";
import { existsSync, lstatSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { BusinessError, accessFilePath, configDir, loadAccess, loadBusiness, loadProject, projectFilePath, resolveInstance, systemVars } from "../../business/src/index.mjs";
import { busBusiness } from "../../bus/src/business.mjs";
import { BusError } from "../../bus/src/broker.mjs";
import { CliError } from "./errors.mjs";
import { accessContextFor } from "./access.mjs";
export const REPO = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
// The broker's socket. One broker per data root: the bus store takes
// `<dataRoot>/bus/writer.lock` (packages/bus/src/store.mjs).
export const socketPath = (dataRoot) => join(dataRoot, "bus", "broker.sock");
export function loadSystem({ env = process.env } = {}) {
const proc = spawnSync(process.execPath, [join(REPO, "scripts", "mosaic-config.mjs"), "validate"], {
encoding: "utf8",
maxBuffer: 1024 * 1024,
env,
});
if (proc.status !== 0) throw new CliError(`system config problem (mosaic-config exit ${proc.status}): ${proc.stderr.trim()}`, 3);
try {
return JSON.parse(proc.stdout);
} catch {
throw new CliError("system config: mosaic-config printed something that isn't JSON", 3);
}
}
export function rolesDir(env = process.env) {
return resolve(env.MOSAIC_ROLES_DIR || join(REPO, "roles"));
}
// A business-package or adapter refusal is a config problem: exit 3.
function business(fn) {
try {
return fn();
} catch (error) {
if (error instanceof BusinessError) throw new CliError(error.message, 3);
if (error instanceof BusError) throw new CliError(`business adapter refused: ${error.code}`, 3);
throw error;
}
}
// config.trackers[<business>] (lead decision 70): plain data from the
// tracker.* variables. tracker.project is a project-layer variable, so the
// entry comes from the one declared project whose file sets it. No
// tracker.baseUrl, or no project naming a tracker project, means no entry
// and no task verbs; two projects naming one each refuse, since the boot
// shape holds one tracker project per business.
function trackerFor(system, b, warn) {
const first = Object.keys(b.roles)[0];
const base = resolveInstance({ system, business: b, instance: first }).vars;
if (base["tracker.baseUrl"] === undefined) return null;
const named = [];
for (const [id, entry] of Object.entries(b.projects)) {
if (entry.kind === 'library') continue;
if (!existsSync(projectFilePath(entry.root))) continue;
const project = loadProject(entry.root);
if (project.id !== id) throw new CliError(`project file ${project.file} has id ${project.id}, but business ${b.id} declares it as ${id}`, 3);
const vars = resolveInstance({ system, business: b, project, instance: first }).vars;
if (vars["tracker.project"] !== undefined) named.push({ id, vars });
}
if (named.length > 1) {
throw new CliError(`business ${b.id}: projects ${named.map((n) => n.id).join(", ")} each set tracker.project; the broker takes one tracker project per business`, 3);
}
if (named.length === 0) {
warn(`business ${b.id} sets tracker.baseUrl, but no project file sets tracker.project; the broker gets no task verbs`);
return null;
}
const v = named[0].vars;
return {
baseUrl: v["tracker.baseUrl"],
project: v["tracker.project"],
pollSeconds: v["tracker.pollSeconds"],
reconcileMinutes: v["tracker.reconcileMinutes"],
descendants: v["tracker.descendants"],
};
}
// FED R6 (#1800, sage-3142): the runtime's `access`, { file, context }. The broker reads the file
// with R1's loadAccess on a user request that finds the file's stat changed, and on its 30 s timer
// (FED R4, #1801), so a bad file leaves every user with no grant and the owner's bus still boots
// (sage-3153). The context is the cli's accessContextFor, peers included. No file, or a context
// that can't be built, means no `access`: no user binds, as before R6.
function accessFor(system, env, warn) {
const file = accessFilePath(configDir(env));
try {
lstatSync(file);
} catch {
return null;
}
let context;
try {
context = accessContextFor(system, env);
} catch (e) {
warn(`access.json is present but its context can't be read, so no granted user binds: ${e.message}`);
return null;
}
// sage-3153: one line at boot when the loader refuses the file, so the owner sees why no user
// binds. The loader's messages can quote the file, so the line names only the path and a code
// from its exit code: 4 is a file it could not open as a regular file, anything else invalid.
try {
loadAccess(file, context);
} catch (e) {
const code = e instanceof BusinessError && e.exitCode !== 4 ? "access-invalid" : "access-unreadable";
warn(`access file refused at boot (${code}), no granted user binds until it is fixed: ${file}`);
}
return { file, context };
}
// The `{op: 'boot'}` config for packages/bus/src/process.mjs, for one
// business. The host holds a reader capability for the notifier and binds
// launches later through bindLaunch, so `launches` starts empty.
export function bootConfig({ system, businessId, env = process.env, warn = () => {} }) {
return business(() => {
const vars = systemVars(system);
const b = loadBusiness(businessId, { dir: configDir(env), rolesDir: rolesDir(env) });
const resolved = {};
for (const instance of Object.keys(b.roles)) resolved[instance] = resolveInstance({ system: vars, business: b, instance });
const config = {
dataRoot: system.dataRoot,
businesses: { [b.id]: busBusiness(b, resolved) },
organization: {dir:configDir(env),rolesDir:rolesDir(env),system:vars},
launches: [],
readers: [b.id],
};
const tracker = trackerFor(vars, b, warn);
if (tracker) config.trackers = { [b.id]: tracker };
const access = accessFor(system, env, warn);
if (access) config.access = access;
return config;
});
}