import { readFileSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const DEFAULT = path.join(path.dirname(fileURLToPath(import.meta.url)), '../../config/actions.json'); const ID_RE = /^[a-z0-9-]+$/; const KINDS = new Set(['service_restart', 'guest_power']); const OPS = new Set(['start', 'stop', 'shutdown', 'reboot']); const TIERS = new Set(['safe', 'risky']); // Loads + validates the action whitelist. `raw` overrides file read (tests). export function loadActions(file = DEFAULT, raw) { const cfg = raw || JSON.parse(readFileSync(file, 'utf8')); const hosts = cfg.hosts || {}; const byId = new Map(); for (const a of (cfg.actions || [])) { if (!ID_RE.test(a.id || '')) throw new Error(`invalid action id: ${a.id}`); if (!KINDS.has(a.kind)) throw new Error(`unknown kind: ${a.kind}`); if (!TIERS.has(a.tier)) throw new Error(`invalid tier: ${a.tier}`); if (a.kind === 'guest_power' && !OPS.has(a.op)) throw new Error(`invalid op: ${a.op}`); if (byId.has(a.id)) throw new Error(`duplicate action id: ${a.id}`); byId.set(a.id, a); } return { list: () => [...byId.values()], get: (id) => byId.get(id), hostIp: (h) => hosts[h] }; }