PR #1916 rule: a refusal names the field or file, never a config value or a host path. readJsonFile's three refusals and parseJsonFileText now name the file by its `what` label only. Callers whose label alone is ambiguous add a name that already passed a strict pattern (business id, role name, org id, turn id, project key) or a fixed path under the data root. Exit codes, refusal codes and check order are unchanged. organization: util, access, project, config-command, library-seats, org-service deleteOrg receipt, agent-levels regex; T1 files role.mjs and organization-config.mjs change only the readJsonFile label. cli: access lock, accounts owner, host state/lock, notifier journal, notify-config, run-evidence, dev-fixture, git-socket, org-bus, org-console. discord: turn record label names the turn id. Sentinel tests (sk-test-NOT-A-SECRET-1234 as a path component) cover each changed refusal and fail on BASE; old path pins updated.
35 lines
1.7 KiB
JavaScript
35 lines
1.7 KiB
JavaScript
import { lstatSync, readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { BINDING_NAME } from "../../discord/src/binding.mjs";
|
|
import { CliError } from "./errors.mjs";
|
|
|
|
// The notifier's private config: `<dataRoot>/notify/<business>/notify.json`,
|
|
// 0600, `{"notifyVersion": 1, "binding": "<discord binding>" | null}`.
|
|
// Missing refuses, so a host never starts without someone deciding whether
|
|
// it DMs; null runs the host without a notifier. Refusals name the business
|
|
// and the path under the data root, never the host path (PR #1916).
|
|
export function readNotifyConfig(dataRoot, business) {
|
|
const file = join(dataRoot, "notify", business, "notify.json");
|
|
let st;
|
|
try {
|
|
st = lstatSync(file);
|
|
} catch {
|
|
throw new CliError(`no notifier config for ${business}: write {"notifyVersion": 1, "binding": "<discord binding>"} to notify/${business}/notify.json under the data root, mode 0600, or "binding": null to run without DMs`, 3);
|
|
}
|
|
if (!st.isFile() || st.uid !== process.getuid() || (st.mode & 0o777) !== 0o600) {
|
|
throw new CliError(`notifier config for ${business} must be a regular file, mode 0600, owned by this user`, 3);
|
|
}
|
|
let doc;
|
|
try {
|
|
doc = JSON.parse(readFileSync(file, "utf8"));
|
|
} catch {
|
|
throw new CliError(`notifier config for ${business} is not JSON`, 3);
|
|
}
|
|
const keys = doc && typeof doc === "object" && !Array.isArray(doc) ? Object.keys(doc).sort().join(",") : "";
|
|
if (keys !== "binding,notifyVersion" || doc.notifyVersion !== 1 || (doc.binding !== null && !(typeof doc.binding === "string" && BINDING_NAME.test(doc.binding)))) {
|
|
throw new CliError(`notifier config for ${business} must be exactly {"notifyVersion": 1, "binding": <binding name> | null}`, 3);
|
|
}
|
|
return doc.binding;
|
|
}
|
|
|