"use strict";Object.defineProperty(exports, "__esModule", { value: true });exports.default = register;var _promises = _interopRequireDefault(require("node:fs/promises")); var _nodePath = _interopRequireDefault(require("node:path"));function _interopRequireDefault(e) {return e && e.__esModule ? e : { default: e };} const STATE_VERSION = 2; const STATE_REL_PATH = ["plugins", "phone-control", "armed.json"]; const GROUP_COMMANDS = { camera: ["camera.snap", "camera.clip"], screen: ["screen.record"], writes: ["calendar.add", "contacts.add", "reminders.add", "sms.send"] }; function uniqSorted(values) { return [...new Set(values.map((v) => v.trim()).filter(Boolean))].toSorted(); } function resolveCommandsForGroup(group) { if (group === "all") { return uniqSorted(Object.values(GROUP_COMMANDS).flat()); } return uniqSorted(GROUP_COMMANDS[group]); } function formatGroupList() { return ["camera", "screen", "writes", "all"].join(", "); } function parseDurationMs(input) { if (!input) { return null; } const raw = input.trim().toLowerCase(); if (!raw) { return null; } const m = raw.match(/^(\d+)(s|m|h|d)$/); if (!m) { return null; } const n = Number.parseInt(m[1] ?? "", 10); if (!Number.isFinite(n) || n <= 0) { return null; } const unit = m[2]; const mult = unit === "s" ? 1000 : unit === "m" ? 60_000 : unit === "h" ? 3_600_000 : 86_400_000; return n * mult; } function formatDuration(ms) { const s = Math.max(0, Math.floor(ms / 1000)); if (s < 60) { return `${s}s`; } const m = Math.floor(s / 60); if (m < 60) { return `${m}m`; } const h = Math.floor(m / 60); if (h < 48) { return `${h}h`; } const d = Math.floor(h / 24); return `${d}d`; } function resolveStatePath(stateDir) { return _nodePath.default.join(stateDir, ...STATE_REL_PATH); } async function readArmState(statePath) { try { const raw = await _promises.default.readFile(statePath, "utf8"); // Type as unknown record first to allow property access during validation const parsed = JSON.parse(raw); if (parsed.version !== 1 && parsed.version !== 2) { return null; } if (typeof parsed.armedAtMs !== "number") { return null; } if (!(parsed.expiresAtMs === null || typeof parsed.expiresAtMs === "number")) { return null; } if (parsed.version === 1) { if ( !Array.isArray(parsed.removedFromDeny) || !parsed.removedFromDeny.every((v) => typeof v === "string")) { return null; } return parsed; } const group = typeof parsed.group === "string" ? parsed.group : ""; if (group !== "camera" && group !== "screen" && group !== "writes" && group !== "all") { return null; } if ( !Array.isArray(parsed.armedCommands) || !parsed.armedCommands.every((v) => typeof v === "string")) { return null; } if ( !Array.isArray(parsed.addedToAllow) || !parsed.addedToAllow.every((v) => typeof v === "string")) { return null; } if ( !Array.isArray(parsed.removedFromDeny) || !parsed.removedFromDeny.every((v) => typeof v === "string")) { return null; } return parsed; } catch { return null; } } async function writeArmState(statePath, state) { await _promises.default.mkdir(_nodePath.default.dirname(statePath), { recursive: true }); if (!state) { try { await _promises.default.unlink(statePath); } catch { // ignore }return; } await _promises.default.writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8"); } function normalizeDenyList(cfg) { return uniqSorted([...(cfg.gateway?.nodes?.denyCommands ?? [])]); } function normalizeAllowList(cfg) { return uniqSorted([...(cfg.gateway?.nodes?.allowCommands ?? [])]); } function patchConfigNodeLists( cfg, next) { return { ...cfg, gateway: { ...cfg.gateway, nodes: { ...cfg.gateway?.nodes, allowCommands: next.allowCommands, denyCommands: next.denyCommands } } }; } async function disarmNow(params) { const { api, stateDir, statePath, reason } = params; const state = await readArmState(statePath); if (!state) { return { changed: false, restored: [], removed: [] }; } const cfg = api.runtime.config.loadConfig(); const allow = new Set(normalizeAllowList(cfg)); const deny = new Set(normalizeDenyList(cfg)); const removed = []; const restored = []; if (state.version === 1) { for (const cmd of state.removedFromDeny) { if (!deny.has(cmd)) { deny.add(cmd); restored.push(cmd); } } } else { for (const cmd of state.addedToAllow) { if (allow.delete(cmd)) { removed.push(cmd); } } for (const cmd of state.removedFromDeny) { if (!deny.has(cmd)) { deny.add(cmd); restored.push(cmd); } } } if (removed.length > 0 || restored.length > 0) { const next = patchConfigNodeLists(cfg, { allowCommands: uniqSorted([...allow]), denyCommands: uniqSorted([...deny]) }); await api.runtime.config.writeConfigFile(next); } await writeArmState(statePath, null); api.logger.info(`phone-control: disarmed (${reason}) stateDir=${stateDir}`); return { changed: removed.length > 0 || restored.length > 0, removed: uniqSorted(removed), restored: uniqSorted(restored) }; } function formatHelp() { return [ "Phone control commands:", "", "/phone status", "/phone arm [duration]", "/phone disarm", "", "Groups:", `- ${formatGroupList()}`, "", "Duration format: 30s | 10m | 2h | 1d (default: 10m).", "", "Notes:", "- This only toggles what the gateway is allowed to invoke on phone nodes.", "- iOS will still ask for permissions (camera, photos, contacts, etc.) on first use."]. join("\n"); } function parseGroup(raw) { const value = (raw ?? "").trim().toLowerCase(); if (!value) { return null; } if (value === "camera" || value === "screen" || value === "writes" || value === "all") { return value; } return null; } function formatStatus(state) { if (!state) { return "Phone control: disarmed."; } const until = state.expiresAtMs == null ? "manual disarm required" : `expires in ${formatDuration(Math.max(0, state.expiresAtMs - Date.now()))}`; const cmds = uniqSorted( state.version === 1 ? state.removedFromDeny : state.armedCommands.length > 0 ? state.armedCommands : [...state.addedToAllow, ...state.removedFromDeny] ); const cmdLabel = cmds.length > 0 ? cmds.join(", ") : "none"; return `Phone control: armed (${until}).\nTemporarily allowed: ${cmdLabel}`; } function register(api) { let expiryInterval = null; const timerService = { id: "phone-control-expiry", start: async (ctx) => { const statePath = resolveStatePath(ctx.stateDir); const tick = async () => { const state = await readArmState(statePath); if (!state || state.expiresAtMs == null) { return; } if (Date.now() < state.expiresAtMs) { return; } await disarmNow({ api, stateDir: ctx.stateDir, statePath, reason: "expired" }); }; // Best effort; don't crash the gateway if state is corrupt. await tick().catch(() => {}); expiryInterval = setInterval(() => { tick().catch(() => {}); }, 15_000); expiryInterval.unref?.(); return; }, stop: async () => { if (expiryInterval) { clearInterval(expiryInterval); expiryInterval = null; } return; } }; api.registerService(timerService); api.registerCommand({ name: "phone", description: "Arm/disarm high-risk phone node commands (camera/screen/writes).", acceptsArgs: true, handler: async (ctx) => { const args = ctx.args?.trim() ?? ""; const tokens = args.split(/\s+/).filter(Boolean); const action = tokens[0]?.toLowerCase() ?? ""; const stateDir = api.runtime.state.resolveStateDir(); const statePath = resolveStatePath(stateDir); if (!action || action === "help") { const state = await readArmState(statePath); return { text: `${formatStatus(state)}\n\n${formatHelp()}` }; } if (action === "status") { const state = await readArmState(statePath); return { text: formatStatus(state) }; } if (action === "disarm") { const res = await disarmNow({ api, stateDir, statePath, reason: "manual" }); if (!res.changed) { return { text: "Phone control: disarmed." }; } const restoredLabel = res.restored.length > 0 ? res.restored.join(", ") : "none"; const removedLabel = res.removed.length > 0 ? res.removed.join(", ") : "none"; return { text: `Phone control: disarmed.\nRemoved allowlist: ${removedLabel}\nRestored denylist: ${restoredLabel}` }; } if (action === "arm") { const group = parseGroup(tokens[1]); if (!group) { return { text: `Usage: /phone arm [duration]\nGroups: ${formatGroupList()}` }; } const durationMs = parseDurationMs(tokens[2]) ?? 10 * 60_000; const expiresAtMs = Date.now() + durationMs; const commands = resolveCommandsForGroup(group); const cfg = api.runtime.config.loadConfig(); const allowSet = new Set(normalizeAllowList(cfg)); const denySet = new Set(normalizeDenyList(cfg)); const addedToAllow = []; const removedFromDeny = []; for (const cmd of commands) { if (!allowSet.has(cmd)) { allowSet.add(cmd); addedToAllow.push(cmd); } if (denySet.delete(cmd)) { removedFromDeny.push(cmd); } } const next = patchConfigNodeLists(cfg, { allowCommands: uniqSorted([...allowSet]), denyCommands: uniqSorted([...denySet]) }); await api.runtime.config.writeConfigFile(next); await writeArmState(statePath, { version: STATE_VERSION, armedAtMs: Date.now(), expiresAtMs, group, armedCommands: uniqSorted(commands), addedToAllow: uniqSorted(addedToAllow), removedFromDeny: uniqSorted(removedFromDeny) }); const allowedLabel = uniqSorted(commands).join(", "); return { text: `Phone control: armed for ${formatDuration(durationMs)}.\n` + `Temporarily allowed: ${allowedLabel}\n` + `To disarm early: /phone disarm` }; } return { text: formatHelp() }; } }); } /* v9-273f962f06c25113 */