// M3: executable and instance boundaries. Presence is checked with lstat, so // broken links and unreadable metadata never turn a deployed path into dev. import { constants, lstatSync, openSync, fstatSync, readFileSync, closeSync, writeFileSync, fchmodSync, mkdirSync } from 'node:fs'; import { basename, dirname, join, resolve } from 'node:path'; import { homedir } from 'node:os'; const configDir = (env = process.env) => dirname(resolve(env.MOSAIC_CONFIG || join(homedir(), '.config/mosaic-dev/config.json'))); const CODE_ROOT = resolve(import.meta.dirname, '../../..'); export function present(file) { try { lstatSync(file); return true; } catch (e) { if (e.code === 'ENOENT') return false; throw e; } } function metadata(file, label, check) { let fd; try { fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK); const st = fstatSync(fd); if (!st.isFile() || st.size > 4096 || !check(st)) throw new Error('metadata refused'); return JSON.parse(readFileSync(fd, 'utf8')); } catch { throw new Error(`${label} is missing or invalid`); } finally { if (fd !== undefined) closeSync(fd); } } export function buildStamp(root = CODE_ROOT) { const file = join(root, '.stack-build.json'); if (!present(file)) return null; const doc = metadata(file, 'build stamp', st => !(st.mode & 0o222)); if (!doc || Object.keys(doc).length !== 1 || !/^[0-9a-f]{40}$/.test(doc.commit) || doc.commit !== basename(root)) throw new Error('build stamp does not name this copy commit'); return doc; } export function assertBuildStart({ serve = false, sso = false, devOwner = false, devBind = null, devNames = [] } = {}) { const stamp = buildStamp(); if (stamp && (devOwner || devBind !== null || devNames.length)) throw new Error('stamped build refuses dev-local-owner and LAN flags'); if (stamp && serve && !sso) throw new Error('stamped build serve requires --sso'); return stamp; } export function assertDevInstance(system, env = process.env) { if (present(join(configDir(env), 'accounts.json'))) throw new Error('--dev-local-owner refuses accounts.json'); if (system.console?.sso) throw new Error('--dev-local-owner refuses console.sso'); if (present(join(system.dataRoot, '.deployed'))) throw new Error('--dev-local-owner refuses .deployed'); } const instanceName = dir => /^mosaic-([a-z][a-z0-9]{1,15})$/.exec(basename(dir))?.[1] ?? 'dev'; export function assertDeployed(system, dir = configDir()) { const doc = metadata(join(system.dataRoot, '.deployed'), '.deployed', st => (st.mode & 0o777) === 0o444 && st.uid === process.geteuid()); if (!doc || Object.keys(doc).length !== 1 || doc.instance !== instanceName(dir)) throw new Error('.deployed does not name this instance'); } // Both managed host cases install through case2-install.sh. Existing metadata // must validate; an install never replaces it or follows a destination link. export function writeDeployment(system, dir = configDir()) { // Default-instance bootstrap creates the config before its data directory. mkdirSync(system.dataRoot, { recursive: true, mode: 0o700 }); const root = lstatSync(system.dataRoot); if (!root.isDirectory() || root.uid !== process.geteuid() || (root.mode & 0o022)) throw new Error('.deployed data root must be an owned directory without public write'); const file = join(system.dataRoot, '.deployed'); if (present(file)) { const doc = metadata(file, '.deployed', st => (st.mode & 0o777) === 0o444 && st.uid === process.geteuid()); if (!doc || Object.keys(doc).length !== 1 || doc.instance !== instanceName(dir)) throw new Error('.deployed does not name this instance'); return; } const fd = openSync(file, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o444); try { writeFileSync(fd, JSON.stringify({ instance: instanceName(dir) }) + '\n'); fchmodSync(fd, 0o444); } finally { closeSync(fd); } }