/** * Config-Audit Rollback Engine * Restores configuration from backup with checksum verification. * Zero external dependencies. */ import { readFile, writeFile, readdir, stat, rm } from 'node:fs/promises'; import { join } from 'node:path'; import { getBackupDir, parseManifest, checksum } from './lib/backup.mjs'; /** * List all available backups. * @returns {Promise<{ backups: object[] }>} */ export async function listBackups() { const backupRoot = getBackupDir(); const backups = []; let entries; try { entries = await readdir(backupRoot, { withFileTypes: true }); } catch { return { backups: [] }; } for (const entry of entries) { if (!entry.isDirectory()) continue; const backupPath = join(backupRoot, entry.name); const manifestPath = join(backupPath, 'manifest.yaml'); try { const manifestContent = await readFile(manifestPath, 'utf-8'); const manifest = parseManifest(manifestContent); backups.push({ id: entry.name, createdAt: manifest.created_at, files: manifest.files.map(f => ({ originalPath: f.originalPath, backupPath: f.backupPath, checksum: f.checksum, sizeBytes: f.sizeBytes, })), }); } catch { // Skip backups without valid manifest continue; } } // Sort newest first backups.sort((a, b) => b.id.localeCompare(a.id)); return { backups }; } /** * Restore files from a backup. * @param {string} backupId * @param {object} [opts] * @param {boolean} [opts.dryRun=false] * @param {boolean} [opts.verify=true] * @returns {Promise<{ restored: object[], failed: object[] }>} */ export async function restoreBackup(backupId, opts = {}) { const verify = opts.verify !== false; const backupRoot = getBackupDir(); const backupPath = join(backupRoot, backupId); const manifestPath = join(backupPath, 'manifest.yaml'); // Read manifest let manifestContent; try { manifestContent = await readFile(manifestPath, 'utf-8'); } catch { throw new Error(`Backup not found: ${backupId}`); } const manifest = parseManifest(manifestContent); const restored = []; const failed = []; for (const fileEntry of manifest.files) { const backupFilePath = join(backupPath, fileEntry.backupPath); if (opts.dryRun) { restored.push({ originalPath: fileEntry.originalPath, status: 'dry-run', }); continue; } try { // Read backup file const content = await readFile(backupFilePath); // Verify checksum before restoring if (verify) { const hash = checksum(content); if (hash !== fileEntry.checksum) { failed.push({ originalPath: fileEntry.originalPath, status: 'checksum-mismatch', error: `Expected ${fileEntry.checksum}, got ${hash}`, }); continue; } } // Write to original path await writeFile(fileEntry.originalPath, content); // Verify after write if (verify) { const written = await readFile(fileEntry.originalPath); const writtenHash = checksum(written); if (writtenHash !== fileEntry.checksum) { failed.push({ originalPath: fileEntry.originalPath, status: 'checksum-mismatch', error: 'Checksum mismatch after write', }); continue; } } restored.push({ originalPath: fileEntry.originalPath, status: 'restored', }); } catch (err) { failed.push({ originalPath: fileEntry.originalPath, status: 'failed', error: err.message, }); } } return { restored, failed }; } /** * Delete a backup directory. * @param {string} backupId * @returns {Promise<{ deleted: boolean, error?: string }>} */ export async function deleteBackup(backupId) { const backupRoot = getBackupDir(); const backupPath = join(backupRoot, backupId); try { await stat(backupPath); } catch { return { deleted: false, error: `Backup not found: ${backupId}` }; } try { await rm(backupPath, { recursive: true, force: true }); return { deleted: true }; } catch (err) { return { deleted: false, error: err.message }; } }