/** Root-only Contabo-side installer for a reviewed employee-resource plan. */ import { createHash } from 'node:crypto'; import fs from 'node:fs/promises'; import path from 'node:path'; import { spawnSync } from 'node:child_process'; const root = '/srv/www/azinstitute4autism.com'; const roles = new Set(['staff', 'management']); const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex'); const safeName = (value) => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]{0,240}$/.test(value) && value !== '.' && value !== '..'; function fail(message) { throw new Error(message); } function nginxGroup() { const result = spawnSync('getent', ['group', 'nginx'], { encoding: 'utf8' }); const gid = Number(result.stdout?.trim().split(':')[2]); if (!Number.isInteger(gid)) fail('nginx group is unavailable'); return gid; } async function readPlan(file) { const stat = await fs.lstat(file); if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 2_000_000) fail('Resource plan must be a bounded regular file'); const plan = JSON.parse(await fs.readFile(file, 'utf8')); if (plan.schema !== 1 || !Array.isArray(plan.files) || !/^[0-9a-f]{40}$/.test(plan.sourceCommit ?? '')) fail('Invalid resource installation plan'); const destinations = new Set(); for (const entry of plan.files) { if (!roles.has(entry.role) || !safeName(entry.name) || !safeName(entry.stageName) || !/^[0-9a-f]{64}$/.test(entry.sha256 ?? '') || !Number.isInteger(entry.size) || entry.size <= 0 || entry.size > 50 * 1024 * 1024) fail('Invalid resource plan entry'); const key = `${entry.role}/${entry.name}`; if (destinations.has(key)) fail('Duplicate resource destination'); destinations.add(key); } return plan; } async function verifyStage(stage, plan) { const expected = new Set(plan.files.map((entry) => `${entry.role}/${entry.stageName}`)); const actual = new Set(); for (const role of roles) { const directory = path.join(stage, role); const stat = await fs.lstat(directory).catch(() => null); if (!stat || !stat.isDirectory() || stat.isSymbolicLink()) fail(`Missing safe ${role} staging directory`); for (const name of await fs.readdir(directory)) { const file = path.join(directory, name), relative = `${role}/${name}`, meta = await fs.lstat(file); if (!meta.isFile() || meta.isSymbolicLink() || meta.nlink !== 1) fail(`Unsafe staged resource: ${relative}`); actual.add(relative); } } if (actual.size !== expected.size || [...actual].some((value) => !expected.has(value))) fail('Staged resources do not exactly match the reviewed plan'); for (const entry of plan.files) { const source = path.join(stage, entry.role, entry.stageName); const bytes = await fs.readFile(source); if (bytes.length !== entry.size || sha256(bytes) !== entry.sha256) fail(`Staged resource digest mismatch: ${entry.role}/${entry.name}`); } } async function install(stage, plan) { if (process.getuid?.() !== 0) fail('Resource installation requires root'); const expectedStage = path.join(root, 'staging', `portal-resources-${plan.sourceCommit}`); if (path.resolve(stage) !== expectedStage) fail('Resource stage is outside the approved root-controlled staging directory'); await verifyStage(stage, plan); const gid = nginxGroup(); const installed = []; for (const entry of plan.files) { const source = path.join(stage, entry.role, entry.stageName); const destinationDirectory = path.join(root, 'secure-resources', entry.role); const destination = path.join(destinationDirectory, entry.name); const existing = await fs.lstat(destination).catch(() => null); if (existing) { if (!existing.isFile() || existing.isSymbolicLink()) fail(`Unsafe existing resource destination: ${entry.role}/${entry.name}`); const bytes = await fs.readFile(destination); if (bytes.length !== entry.size || sha256(bytes) !== entry.sha256) fail(`Refusing to replace existing resource: ${entry.role}/${entry.name}`); installed.push({ role: entry.role, name: entry.name, result: 'already-verified' }); continue; } const temporary = path.join(destinationDirectory, `.${entry.name}.next-${process.pid}`); await fs.copyFile(source, temporary, fs.constants.COPYFILE_EXCL); await fs.chown(temporary, 0, gid); await fs.chmod(temporary, 0o640); await fs.rename(temporary, destination); installed.push({ role: entry.role, name: entry.name, result: 'installed' }); } return installed; } if (process.argv[1] && path.resolve(process.argv[1]) === new URL(import.meta.url).pathname) { const [stage, manifest] = process.argv.slice(2); (async () => { if (!stage || !manifest || process.argv.length !== 4) fail('Usage: portal-resource-install.mjs STAGING_DIRECTORY REVIEWED_PLAN.json'); const plan = await readPlan(manifest); const installed = await install(stage, plan); console.log(JSON.stringify({ sourceCommit: plan.sourceCommit, installed: installed.length, identifiers: installed }, null, 2)); })().catch((error) => { console.error(`Employee resource installation stopped: ${error.message}`); process.exitCode = 1; }); } export { readPlan, verifyStage, install };