"use strict";
/**
 * HashManifest -- HMAC-signed file integrity manifests.
 *
 * Creates and verifies SHA-256 hash manifests for governance files.
 * Signs the manifest with HMAC-SHA256 to prevent tampering.
 * Used to detect unauthorized modifications to hooks, configs,
 * and compliance pack files.
 *
 * T-045: Sign hash manifests with HMAC.
 *
 * @connexum/ai-governance
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.HashManifest = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const crypto = __importStar(require("crypto"));
const log_1 = require("../util/log");
const log = (0, log_1.createLogger)('hash-manifest');
class HashManifest {
    config;
    manifestFile;
    constructor(config) {
        this.config = {
            includePatterns: ['.ts', '.js', '.sh', '.json', '.md'],
            excludePatterns: ['.test.ts', '.spec.ts', 'node_modules', '.git'],
            ...config,
        };
        if (!fs.existsSync(config.manifestDir)) {
            fs.mkdirSync(config.manifestDir, { recursive: true });
        }
        this.manifestFile = path.join(config.manifestDir, 'governance.manifest.json');
    }
    /**
     * Generate a new signed manifest from the configured directories.
     */
    generate() {
        const entries = [];
        for (const dir of this.config.scanDirs) {
            if (!fs.existsSync(dir))
                continue;
            const files = this.scanDir(dir);
            for (const file of files) {
                try {
                    const stat = fs.statSync(file);
                    const content = fs.readFileSync(file);
                    const hash = crypto.createHash('sha256').update(content).digest('hex');
                    entries.push({
                        file: path.relative(this.config.scanDirs[0], file) || file,
                        sha256: hash,
                        sizeBytes: stat.size,
                        lastModified: stat.mtime.toISOString(),
                    });
                }
                catch {
                    // Skip unreadable files
                }
            }
        }
        entries.sort((a, b) => a.file.localeCompare(b.file));
        const manifest = {
            version: 1,
            createdAt: new Date().toISOString(),
            entries,
            signature: '',
        };
        manifest.signature = this.sign(manifest);
        fs.writeFileSync(this.manifestFile, JSON.stringify(manifest, null, 2));
        log.info('Manifest generated', { files: entries.length });
        return manifest;
    }
    /**
     * Verify the current files against the stored manifest.
     */
    verify() {
        if (!fs.existsSync(this.manifestFile)) {
            return {
                valid: false,
                signatureValid: false,
                totalFiles: 0,
                changedFiles: [],
                missingFiles: [],
                newFiles: [],
            };
        }
        const manifest = JSON.parse(fs.readFileSync(this.manifestFile, 'utf-8'));
        // Verify HMAC signature first
        const signatureValid = this.verifySignature(manifest);
        if (!signatureValid) {
            log.error('Manifest signature verification FAILED -- manifest may be tampered');
            return {
                valid: false,
                signatureValid: false,
                totalFiles: manifest.entries.length,
                changedFiles: [],
                missingFiles: [],
                newFiles: [],
            };
        }
        const changedFiles = [];
        const missingFiles = [];
        // Check each manifest entry against current files
        for (const entry of manifest.entries) {
            const fullPath = this.resolveFile(entry.file);
            if (!fs.existsSync(fullPath)) {
                missingFiles.push(entry.file);
                continue;
            }
            try {
                const content = fs.readFileSync(fullPath);
                const currentHash = crypto.createHash('sha256').update(content).digest('hex');
                if (currentHash !== entry.sha256) {
                    changedFiles.push(entry.file);
                }
            }
            catch {
                missingFiles.push(entry.file);
            }
        }
        // Check for new files not in manifest
        const manifestFiles = new Set(manifest.entries.map(e => e.file));
        const currentFiles = [];
        for (const dir of this.config.scanDirs) {
            if (!fs.existsSync(dir))
                continue;
            for (const file of this.scanDir(dir)) {
                const relative = path.relative(this.config.scanDirs[0], file) || file;
                currentFiles.push(relative);
            }
        }
        const newFiles = currentFiles.filter(f => !manifestFiles.has(f));
        const valid = signatureValid && changedFiles.length === 0 && missingFiles.length === 0;
        if (!valid) {
            log.warn('Manifest verification found changes', {
                changed: changedFiles.length,
                missing: missingFiles.length,
                new: newFiles.length,
            });
        }
        return {
            valid,
            signatureValid,
            totalFiles: manifest.entries.length,
            changedFiles,
            missingFiles,
            newFiles,
        };
    }
    /**
     * Get the stored manifest.
     */
    getManifest() {
        if (!fs.existsSync(this.manifestFile))
            return null;
        try {
            return JSON.parse(fs.readFileSync(this.manifestFile, 'utf-8'));
        }
        catch {
            return null;
        }
    }
    /**
     * Get hash of a specific file from the manifest.
     */
    getFileHash(relativePath) {
        const manifest = this.getManifest();
        if (!manifest)
            return undefined;
        return manifest.entries.find(e => e.file === relativePath)?.sha256;
    }
    // --- Private ---
    sign(manifest) {
        const data = JSON.stringify({
            version: manifest.version,
            createdAt: manifest.createdAt,
            entries: manifest.entries,
        });
        return crypto
            .createHmac('sha256', this.config.secret)
            .update(data)
            .digest('hex');
    }
    verifySignature(manifest) {
        const expected = this.sign({
            ...manifest,
            signature: '', // Exclude signature from verification payload
        });
        // F-NEW-CRIT-35: length-check before timingSafeEqual to avoid
        // ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH on malformed signature.
        const sigBuf = Buffer.from(manifest.signature, 'hex');
        const expBuf = Buffer.from(expected, 'hex');
        if (sigBuf.length !== expBuf.length)
            return false;
        return crypto.timingSafeEqual(sigBuf, expBuf);
    }
    resolveFile(relativePath) {
        // Try each scan dir
        for (const dir of this.config.scanDirs) {
            const fullPath = path.join(dir, relativePath);
            if (fs.existsSync(fullPath))
                return fullPath;
        }
        return path.join(this.config.scanDirs[0], relativePath);
    }
    scanDir(dir, maxDepth = 5, depth = 0) {
        if (depth >= maxDepth)
            return [];
        const files = [];
        try {
            const entries = fs.readdirSync(dir, { withFileTypes: true });
            for (const entry of entries) {
                const fullPath = path.join(dir, entry.name);
                // Check excludes
                if (this.config.excludePatterns.some(p => entry.name.includes(p)))
                    continue;
                if (entry.isDirectory()) {
                    files.push(...this.scanDir(fullPath, maxDepth, depth + 1));
                }
                else if (entry.isFile()) {
                    // Check includes
                    if (this.config.includePatterns.some(p => entry.name.endsWith(p))) {
                        files.push(fullPath);
                    }
                }
            }
        }
        catch { /* skip */ }
        return files;
    }
}
exports.HashManifest = HashManifest;
//# sourceMappingURL=hash-manifest.js.map