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: `/notify//notify.json`, // 0600, `{"notifyVersion": 1, "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": ""} 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": | null}`, 3); } return doc.binding; }