/**
 * Plugin integrity enforcement (PLUGIN-INTEGRITY-01).
 *
 * Load-time SHA-256 integrity checking for plugins.
 * Every plugin is verified against a trusted manifest (plugins.lock.json)
 * before it is admitted into the governance runtime.
 *
 * Fail-closed by default: a plugin whose hash is absent from the manifest
 * or whose hash does not match is rejected. The `disableIntegrity` escape
 * hatch exists for development convenience but emits a WARN audit event so
 * auditors can see the bypass on the chain.
 *
 * Scope (PLUGIN-INTEGRITY-01): load-time only.
 * Runtime per-dispatch re-verification is tracked under PLUGIN-INTEGRITY-02.
 *
 * @connexum/ai-governance
 */
import * as crypto from 'crypto';
import * as fs from 'fs';
/**
 * Thrown when a plugin fails load-time integrity verification.
 * Extends Error directly (same base as GovernanceTamperError / PluginValidationError)
 * so catch(err instanceof Error) paths handle it without importing the hierarchy.
 */
export class PluginIntegrityError extends Error {
    code = 'PLUGIN_INTEGRITY_FAILED';
    reason;
    pluginName;
    expected;
    actual;
    constructor(opts) {
        const detail = opts.reason === 'hash-mismatch'
            ? ` (expected ${opts.expected ?? '?'}, got ${opts.actual ?? '?'})`
            : opts.reason === 'unregistered-plugin'
                ? ' (not present in trusted manifest)'
                : ' (manifest map is empty -- all plugins are untrusted)';
        super(`Plugin integrity check failed for '${opts.pluginName}': ${opts.reason}${detail}`);
        this.name = 'PluginIntegrityError';
        this.reason = opts.reason;
        this.pluginName = opts.pluginName;
        this.expected = opts.expected;
        this.actual = opts.actual;
    }
}
// ---------------------------------------------------------------------------
// Core functions
// ---------------------------------------------------------------------------
/**
 * Compute the hex SHA-256 digest of plugin source code.
 * Input may be a string (UTF-8 encoded internally) or a raw Buffer.
 */
export function computePluginSha256(pluginCode) {
    const buf = typeof pluginCode === 'string' ? Buffer.from(pluginCode, 'utf-8') : pluginCode;
    return crypto.createHash('sha256').update(buf).digest('hex');
}
/**
 * Verify a plugin's source against its trusted manifest entry.
 *
 * Returns `{ ok: true }` when the hash matches.
 * Returns a typed failure record otherwise -- callers decide whether to throw.
 */
export function verifyPluginIntegrity(plugin, manifestEntry, manifestMap) {
    // Empty-manifest guard: if a map was supplied and it is empty, all plugins
    // are by definition unregistered. Surface this as a distinct reason so
    // operators can distinguish "manifest file missing" from "plugin just not
    // listed."
    if (manifestMap !== undefined && manifestMap.size === 0) {
        return { ok: false, reason: 'empty-manifest' };
    }
    if (!manifestEntry) {
        return { ok: false, reason: 'unregistered-plugin' };
    }
    const actual = computePluginSha256(plugin.source);
    if (actual !== manifestEntry.sha256) {
        return {
            ok: false,
            reason: 'hash-mismatch',
            expected: manifestEntry.sha256,
            actual,
        };
    }
    return { ok: true };
}
/**
 * Read `.governance/plugins.lock.json` (or an arbitrary path) and parse it
 * into a map keyed by plugin name.
 *
 * Rejects on:
 *   - File read failure
 *   - Malformed JSON
 *   - Root value that is not an array
 *   - Array entries that are missing `name` or `sha256`
 *
 * Returns an empty Map (not an error) when the file does not exist -- a
 * missing lockfile is a valid first-boot state. Callers that require a
 * non-empty manifest should call `assertPluginTrusted`, which will surface
 * `empty-manifest`.
 */
export async function loadTrustedPluginManifest(manifestPath) {
    let raw;
    try {
        raw = await fs.promises.readFile(manifestPath, 'utf-8');
    }
    catch (err) {
        const code = err.code;
        if (code === 'ENOENT') {
            // File missing is NOT an error -- treat as empty manifest.
            return new Map();
        }
        throw new Error(`Failed to read plugin manifest at ${manifestPath}: ${err.message ?? String(err)}`);
    }
    let parsed;
    try {
        parsed = JSON.parse(raw);
    }
    catch (err) {
        throw new Error(`Plugin manifest at ${manifestPath} contains malformed JSON: ${err.message ?? String(err)}`);
    }
    if (!Array.isArray(parsed)) {
        throw new Error(`Plugin manifest at ${manifestPath} must be a JSON array, got ${typeof parsed}`);
    }
    const map = new Map();
    for (const entry of parsed) {
        if (typeof entry !== 'object' ||
            entry === null ||
            typeof entry.name !== 'string' ||
            typeof entry.sha256 !== 'string') {
            throw new Error(`Plugin manifest at ${manifestPath} contains an entry missing required fields 'name' (string) and 'sha256' (string)`);
        }
        const typed = entry;
        map.set(typed.name, typed);
    }
    return map;
}
/**
 * Assert that a plugin is trusted according to the manifest map.
 * Throws `PluginIntegrityError` if the check fails.
 *
 * This is the high-level convenience used by `PluginRegistry.register()`.
 */
export function assertPluginTrusted(plugin, manifestMap) {
    const entry = manifestMap.get(plugin.name);
    const result = verifyPluginIntegrity(plugin, entry, manifestMap);
    if (!result.ok) {
        throw new PluginIntegrityError({
            reason: result.reason,
            pluginName: plugin.name,
            expected: result.expected,
            actual: result.actual,
        });
    }
}
//# sourceMappingURL=plugin-integrity.js.map