/**
 * MemoryIntegrity -- Cryptographic verification for agent memory files.
 *
 * Every memory write gets a SHA-256 hash footer. Every read verifies
 * the hash. Tampered files are flagged, backed up copies are restored.
 * Addresses OWASP ASI06 (Memory Poisoning).
 *
 * @connexum/ai-governance
 */
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
const HASH_PATTERN = /<!-- HASH:(\w+):([a-f0-9]+) -->$/;
export class MemoryIntegrity {
    config;
    constructor(config) {
        this.config = {
            memoryDir: config.memoryDir,
            agentName: config.agentName,
            auditLogPath: config.auditLogPath || path.join(config.memoryDir, '..', 'audit', 'memory-integrity.jsonl'),
            protectedExtensions: config.protectedExtensions || ['.md'],
            algorithm: config.algorithm || 'sha256',
        };
    }
    /**
     * Write content to a memory file with integrity hash.
     * Creates a backup of the previous version.
     */
    writeMemory(filePath, content) {
        const resolved = this.resolveMemoryPath(filePath);
        const dir = path.dirname(resolved);
        if (!fs.existsSync(dir)) {
            fs.mkdirSync(dir, { recursive: true });
        }
        // Create backup of existing file
        let backupCreated = false;
        if (fs.existsSync(resolved)) {
            const backupPath = resolved + '.bak';
            fs.copyFileSync(resolved, backupPath);
            backupCreated = true;
        }
        // Strip any existing hash footer from content
        const cleanContent = content.replace(/\n<!-- HASH:\w+:[a-f0-9]+ -->\s*$/, '');
        // Compute hash of clean content
        const hash = crypto.createHash(this.config.algorithm)
            .update(cleanContent)
            .digest('hex');
        // Write content with hash footer
        const finalContent = cleanContent + `\n<!-- HASH:${this.config.algorithm}:${hash} -->`;
        fs.writeFileSync(resolved, finalContent);
        this.logEvent('write', resolved, { hash, backupCreated });
        return { file: resolved, hash, backupCreated };
    }
    /**
     * Read a memory file and verify its integrity.
     * Returns the content (without hash footer) if valid.
     * Attempts restoration from backup if tampered.
     */
    readMemory(filePath) {
        const resolved = this.resolveMemoryPath(filePath);
        const verification = this.verifyFile(resolved);
        if (!verification.valid && verification.restoredFromBackup) {
            // Re-read after restoration
            const raw = fs.readFileSync(resolved, 'utf-8');
            const content = raw.replace(/\n<!-- HASH:\w+:[a-f0-9]+ -->\s*$/, '');
            return { content, verification };
        }
        if (!verification.valid && !verification.restoredFromBackup) {
            // Tampered and no backup available
            this.logEvent('tamper-detected', resolved, {
                expectedHash: verification.expectedHash,
                actualHash: verification.actualHash,
                restored: false,
            });
            // Return content anyway but mark as tampered
            const raw = fs.existsSync(resolved) ? fs.readFileSync(resolved, 'utf-8') : '';
            const content = raw.replace(/\n<!-- HASH:\w+:[a-f0-9]+ -->\s*$/, '');
            return { content, verification };
        }
        const raw = fs.readFileSync(resolved, 'utf-8');
        const content = raw.replace(/\n<!-- HASH:\w+:[a-f0-9]+ -->\s*$/, '');
        return { content, verification };
    }
    /**
     * Verify integrity of a single memory file.
     */
    verifyFile(filePath) {
        const resolved = this.resolveMemoryPath(filePath);
        if (!fs.existsSync(resolved)) {
            return {
                file: resolved,
                valid: false,
                hasHash: false,
                restoredFromBackup: false,
                error: 'File does not exist',
            };
        }
        const raw = fs.readFileSync(resolved, 'utf-8');
        const match = raw.match(HASH_PATTERN);
        if (!match) {
            // No hash footer - file predates integrity system
            return {
                file: resolved,
                valid: true, // treat as valid but unhashed
                hasHash: false,
                restoredFromBackup: false,
            };
        }
        const [, algorithm, expectedHash] = match;
        const content = raw.replace(/\n<!-- HASH:\w+:[a-f0-9]+ -->\s*$/, '');
        const actualHash = crypto.createHash(algorithm)
            .update(content)
            .digest('hex');
        if (expectedHash === actualHash) {
            return {
                file: resolved,
                valid: true,
                hasHash: true,
                expectedHash,
                actualHash,
                restoredFromBackup: false,
            };
        }
        // Hash mismatch - attempt restore from backup
        const backupPath = resolved + '.bak';
        if (fs.existsSync(backupPath)) {
            this.logEvent('tamper-detected', resolved, {
                expectedHash,
                actualHash,
                restored: true,
            });
            fs.copyFileSync(backupPath, resolved);
            return {
                file: resolved,
                valid: false,
                hasHash: true,
                expectedHash,
                actualHash,
                restoredFromBackup: true,
            };
        }
        this.logEvent('tamper-detected', resolved, {
            expectedHash,
            actualHash,
            restored: false,
        });
        return {
            file: resolved,
            valid: false,
            hasHash: true,
            expectedHash,
            actualHash,
            restoredFromBackup: false,
        };
    }
    /**
     * Verify all memory files in the memory directory.
     */
    verifyAll() {
        const results = [];
        if (!fs.existsSync(this.config.memoryDir))
            return results;
        const files = this.listMemoryFiles();
        for (const file of files) {
            results.push(this.verifyFile(file));
        }
        return results;
    }
    /**
     * Add integrity hashes to all existing memory files that lack them.
     * Non-destructive: only adds hash footer, doesn't change content.
     */
    retroHashAll() {
        const results = [];
        if (!fs.existsSync(this.config.memoryDir))
            return results;
        const files = this.listMemoryFiles();
        for (const file of files) {
            const raw = fs.readFileSync(file, 'utf-8');
            if (!HASH_PATTERN.test(raw)) {
                // File has no hash - add one
                const result = this.writeMemory(file, raw);
                results.push(result);
            }
        }
        return results;
    }
    /**
     * Generate a manifest of all memory file hashes.
     */
    generateManifest() {
        const manifest = {};
        const files = this.listMemoryFiles();
        for (const file of files) {
            const raw = fs.readFileSync(file, 'utf-8');
            const content = raw.replace(/\n<!-- HASH:\w+:[a-f0-9]+ -->\s*$/, '');
            const hash = crypto.createHash(this.config.algorithm)
                .update(content)
                .digest('hex');
            const relative = path.relative(this.config.memoryDir, file);
            manifest[relative] = hash;
        }
        return manifest;
    }
    /**
     * Verify manifest against current files. Returns list of mismatches.
     */
    verifyManifest(manifest) {
        const mismatches = [];
        for (const [relative, expectedHash] of Object.entries(manifest)) {
            const filePath = path.join(this.config.memoryDir, relative);
            if (!fs.existsSync(filePath)) {
                mismatches.push({ file: relative, expected: expectedHash, actual: 'MISSING' });
                continue;
            }
            const raw = fs.readFileSync(filePath, 'utf-8');
            const content = raw.replace(/\n<!-- HASH:\w+:[a-f0-9]+ -->\s*$/, '');
            const actualHash = crypto.createHash(this.config.algorithm)
                .update(content)
                .digest('hex');
            if (expectedHash !== actualHash) {
                mismatches.push({ file: relative, expected: expectedHash, actual: actualHash });
            }
        }
        return mismatches;
    }
    // --- Private helpers ---
    resolveMemoryPath(filePath) {
        if (path.isAbsolute(filePath))
            return filePath;
        return path.join(this.config.memoryDir, filePath);
    }
    listMemoryFiles() {
        const files = [];
        const walk = (dir) => {
            const entries = fs.readdirSync(dir, { withFileTypes: true });
            for (const entry of entries) {
                const full = path.join(dir, entry.name);
                if (entry.isDirectory()) {
                    walk(full);
                }
                else if (this.config.protectedExtensions.some(ext => entry.name.endsWith(ext))) {
                    if (!entry.name.endsWith('.bak')) {
                        files.push(full);
                    }
                }
            }
        };
        walk(this.config.memoryDir);
        return files;
    }
    logEvent(event, file, details) {
        const logDir = path.dirname(this.config.auditLogPath);
        if (!fs.existsSync(logDir)) {
            fs.mkdirSync(logDir, { recursive: true });
        }
        const entry = {
            timestamp: new Date().toISOString(),
            agent: this.config.agentName,
            event: `memory:${event}`,
            file: path.relative(this.config.memoryDir, file),
            ...details,
        };
        fs.appendFileSync(this.config.auditLogPath, JSON.stringify(entry) + '\n');
    }
}
//# sourceMappingURL=memory-integrity.js.map