"use strict";
/**
 * PostToolAuditEnricher -- Policy-gated, field-aware enrichment of post-tool
 * audit events.
 *
 * Problem:
 *   The existing PostToolUse audit chain captures metadata (tool name, agent,
 *   cycle, duration) but NOT the params an agent passed in nor the results
 *   returned. This blinds forensics: auditors can see WHICH tool was called
 *   but not WHAT was passed or returned.
 *
 * Solution:
 *   Default-deny. No params/results are logged unless an operator explicitly
 *   configures an enrichment rule for a (orgId, agentId, packId, toolName)
 *   tuple. Rules specify per-field redaction modes so sensitive fields are
 *   hashed, tokenized, or redacted before they enter the audit chain.
 *
 * Redaction modes:
 *   redact    -- field replaced with '<REDACTED>'. Credentials, raw tokens.
 *   hash      -- field replaced with 'sha256:<16-hex-chars>'. PHI/PII.
 *               Same value => same hash. Operators can verify presence without
 *               seeing raw value.
 *   tokenize  -- field replaced with 'tok_<8-hex>_<seq>'. Per-org reversible.
 *               Operators retain the de-tokenization map. Auditors may request
 *               resolution via operator cooperation.
 *   log       -- explicit pass-through. Field logged unredacted.
 *
 * Default-deny semantic:
 *   If `enabledForTools` for the agent's bound packs does NOT list toolName,
 *   enricher falls back to existing metadata-only TOOL_INVOCATION_COMPLETED
 *   event. Operators must opt in tool-by-tool. Avoids accidental leakage from
 *   an unreviewed tool schema.
 *
 * FieldPath syntax:
 *   Dot notation: 'metadata.userId' resolves obj.metadata.userId.
 *   Array wildcard: 'attachments[*].content' applies to each element.
 *   Exact match only -- no fuzzy/partial matching (see PT-AUDIT-ENRICH-1).
 *
 * Tokenization (v0):
 *   Tokens stored in-memory Map per org. Same value -> same token within org.
 *   Cross-org lookup is rejected (PT-AUDIT-ENRICH-3).
 *   V1 plan: persist token map to operator-side tokenization vault (out of scope
 *   for this PR).
 *
 * Integration order (relative to PR #14 ToolRateLimiter):
 *   1. PreToolUse: ToolRateLimiter.hook() -- rate check (throws if exceeded)
 *   2. Tool executes
 *   3. PostToolUse: ToolRateLimiter implicitly recorded (counter already
 *      incremented in step 1); enrichAndEmit() fires AFTER the tool result
 *      is available. The rate-limit counter is updated before the audit event
 *      is emitted (no double-count risk).
 *
 * Pen-test gates:
 *   PT-AUDIT-ENRICH-1: redaction-bypass via field aliasing. Defence is exact
 *     fieldPath match only. Known limitation documented. Audit chain records
 *     unmatched field names so operators can spot alias attacks.
 *   PT-AUDIT-ENRICH-2: hash-collision lookup. SHA-256 full digest; 64-bit
 *     prefix is the logged representation. Pre-image resistance is computationally
 *     infeasible for SHA-256 (2^256 space). Full hash stored internally for
 *     deduplication; only 16-char prefix emitted in audit event.
 *   PT-AUDIT-ENRICH-3: tokenization cross-org leakage. Token map is keyed
 *     per orgId. Cross-org token lookup produces a NEW token (not org-A's value).
 *
 * Compliance references:
 *   HIPAA 164.312(b): Audit Controls -- tamper-evident activity logging
 *   GDPR Art. 5(2): Accountability -- demonstrable compliance records
 *   PCI DSS Req 10.2: Audit log events per security-related action
 *   DORA Art. 9: ICT risk -- tool-invocation traceability
 *   SOC 2 CC7.2: monitoring of system components
 *
 * BUILD_PLAN reference: line 237 "Post-tool-invocation audit enriched with
 * full parameter + result logging (currently metadata-only)".
 *
 * @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.PostToolAuditEnricher = exports.EnrichmentUnboundError = exports.ENRICHMENT_STRICT_MODE_PACKS = void 0;
const crypto = __importStar(require("node:crypto"));
// ---------------------------------------------------------------------------
// Strict-mode packs that require enrichment rules for every registered tool.
// If a strict-pack tool is registered but has no enrichment rule, the enricher
// emits ENRICHMENT_POLICY_VIOLATION severity ERROR.
// ---------------------------------------------------------------------------
exports.ENRICHMENT_STRICT_MODE_PACKS = new Set([
    'hipaa',
    'gdpr',
    'pci-dss',
    'hmda',
    'dora',
]);
// ---------------------------------------------------------------------------
// Error classes
// ---------------------------------------------------------------------------
class EnrichmentUnboundError extends Error {
    context;
    constructor(context) {
        super(`PostToolAuditEnricher: packBinding must not be empty. Context: ${context}. ` +
            `Per pack-binding-primacy governance: every audit record must carry at ` +
            `least one pack binding for retention policy derivation.`);
        this.name = 'EnrichmentUnboundError';
        this.context = context;
    }
}
exports.EnrichmentUnboundError = EnrichmentUnboundError;
// ---------------------------------------------------------------------------
// Tokenization store (v0 in-memory; v1 plan: persist to operator vault)
// ---------------------------------------------------------------------------
/**
 * PerOrgTokenStore -- isolated per-org token map.
 *
 * Token format: tok_<8-hex-of-sha256>_<seq>
 *   - 8 hex = first 4 bytes of SHA-256(orgId + fieldPath + value)
 *   - seq = per-(org, fieldPath) monotonically increasing counter
 *
 * Same value -> same token within org.
 * Different orgs -> different tokens for the same raw value (PT-AUDIT-ENRICH-3).
 *
 * V1 plan: replace the inner Map with a call to an operator-supplied
 * tokenization vault (e.g., Format-Preserving Encryption service, HashiCorp
 * Vault Transit secrets engine, or AWS Macie tokenization). The vault
 * persists the mapping across restarts, enabling auditors to request
 * resolution with operator cooperation.
 */
class PerOrgTokenStore {
    orgId;
    /** key: `${fieldPath}::${value}` -> token */
    tokenMap = new Map();
    /** key: fieldPath -> next sequence number */
    seqMap = new Map();
    constructor(orgId) {
        this.orgId = orgId;
    }
    tokenize(fieldPath, rawValue) {
        const key = `${fieldPath}::${rawValue}`;
        const existing = this.tokenMap.get(key);
        if (existing)
            return existing;
        const seq = (this.seqMap.get(fieldPath) ?? 0) + 1;
        this.seqMap.set(fieldPath, seq);
        const hashInput = `${this.orgId}::${fieldPath}::${rawValue}`;
        const hex8 = crypto.createHash('sha256').update(hashInput, 'utf8').digest('hex').slice(0, 8);
        const seqStr = String(seq).padStart(3, '0');
        const token = `tok_${hex8}_${seqStr}`;
        this.tokenMap.set(key, token);
        return token;
    }
    getOrgId() {
        return this.orgId;
    }
}
// ---------------------------------------------------------------------------
// PostToolAuditEnricher
// ---------------------------------------------------------------------------
class PostToolAuditEnricher {
    /**
     * rule registry key: `${orgId}::${agentId}::${toolName}`
     * Value: first registered rule wins. Multiple configure() calls for the same
     * key overwrite (last-write wins; operators should configure once at startup).
     */
    rules = new Map();
    /** Per-org token stores for tokenization. */
    tokenStores = new Map();
    /** SHA chain: tracks the SHA-256 of the previous event. */
    prevSha = '0'.repeat(64);
    /** Audit sink: receives all emitted events. */
    auditSink;
    constructor(auditSink) {
        this.auditSink = auditSink ?? (() => undefined);
    }
    // -------------------------------------------------------------------------
    // Public: configure
    // -------------------------------------------------------------------------
    /**
     * Register an enrichment policy for a specific (org, agent, tool) tuple.
     *
     * Operator-side call. Should be invoked once at agent startup for each tool
     * that should have enriched audit logging. Tools NOT registered here fall
     * back to metadata-only TOOL_INVOCATION_COMPLETED (default-deny).
     *
     * @param orgId      Tenant/org identifier.
     * @param agentId    Agent identifier.
     * @param packId     Compliance pack that owns this rule.
     * @param toolName   Tool name (e.g., 'filesystem.read', 'http.fetch').
     * @param paramRules Field redaction rules for invocation params.
     * @param resultRules Field redaction rules for tool result.
     */
    configure(orgId, agentId, packId, toolName, paramRules, resultRules) {
        const key = this.ruleKey(orgId, agentId, toolName);
        this.rules.set(key, { packId, paramRules, resultRules });
    }
    // -------------------------------------------------------------------------
    // Public: getEnrichmentStatus
    // -------------------------------------------------------------------------
    /**
     * Returns whether enrichment is enabled for a given (org, agent, tool) tuple.
     */
    getEnrichmentStatus(orgId, agentId, toolName) {
        return this.rules.has(this.ruleKey(orgId, agentId, toolName));
    }
    // -------------------------------------------------------------------------
    // Public: enrichAndEmit
    // -------------------------------------------------------------------------
    /**
     * Apply field-redaction rules and emit an audit event.
     *
     * If no rule is registered for the tool -> emits TOOL_INVOCATION_COMPLETED
     * (metadata-only, default-deny).
     *
     * If a rule is registered -> emits TOOL_INVOCATION_ENRICHED with redacted
     * params + result fields.
     *
     * Severity:
     *   NOTICE  -- success=true
     *   WARNING -- success=false (tool-side error)
     *
     * Strict-pack policy violation:
     *   If the agent is bound to a strict pack (HIPAA, GDPR, PCI DSS, HMDA, DORA)
     *   and the tool is NOT configured for enrichment, an
     *   ENRICHMENT_POLICY_VIOLATION event (ERROR) is emitted BEFORE falling back
     *   to the metadata-only event.
     *
     * @param orgId       Tenant/org identifier.
     * @param agentId     Agent identifier.
     * @param cycleId     Current governance cycle identifier.
     * @param toolName    Tool name.
     * @param params      Raw tool invocation params.
     * @param result      Raw tool result (may be error object).
     * @param durationMs  Tool execution duration in milliseconds.
     * @param packBinding Active compliance pack IDs for this agent.
     */
    enrichAndEmit(orgId, agentId, cycleId, toolName, params, result, durationMs, packBinding) {
        if (!packBinding || packBinding.length === 0) {
            throw new EnrichmentUnboundError(`enrichAndEmit called for tool '${toolName}' on agent '${agentId}' with empty packBinding`);
        }
        const key = this.ruleKey(orgId, agentId, toolName);
        const rule = this.rules.get(key);
        const success = !(result instanceof Error) && result !== null && result !== undefined
            ? true
            : (result instanceof Error ? false : true);
        const severity = success ? 'NOTICE' : 'WARNING';
        const tsNs = this.nowNs();
        if (!rule) {
            // Check strict-pack violation: agent bound to strict pack but tool not configured
            const strictViolatingPack = packBinding.find((p) => exports.ENRICHMENT_STRICT_MODE_PACKS.has(p));
            if (strictViolatingPack) {
                const violationEvent = {
                    type: 'ENRICHMENT_POLICY_VIOLATION',
                    severity: 'ERROR',
                    orgId,
                    agentId,
                    cycleId,
                    toolName,
                    violatingPackId: strictViolatingPack,
                    reason: `Agent '${agentId}' is bound to strict pack '${strictViolatingPack}' ` +
                        `but tool '${toolName}' has no enrichment rule configured. ` +
                        `Strict packs require operators to explicitly configure enrichment rules ` +
                        `or exclude the tool from audit scope.`,
                    packBinding,
                    timestampNs: tsNs,
                    prevSha: this.prevSha,
                };
                this.emitAndChain(violationEvent);
            }
            // Default-deny fallback: metadata-only event
            const metaEvent = {
                type: 'TOOL_INVOCATION_COMPLETED',
                severity,
                orgId,
                agentId,
                cycleId,
                toolName,
                success,
                durationMs,
                packBinding,
                timestampNs: tsNs,
                prevSha: this.prevSha,
            };
            this.emitAndChain(metaEvent);
            return;
        }
        // Enrichment path
        const resultObj = this.normalizeResult(result);
        const { enriched: enrichedParams, unmatched: unmatchedParamFields } = this.applyRules(orgId, params, rule.paramRules);
        const { enriched: enrichedResult, unmatched: unmatchedResultFields } = this.applyRules(orgId, resultObj, rule.resultRules);
        const enrichedEvent = {
            type: 'TOOL_INVOCATION_ENRICHED',
            severity,
            orgId,
            agentId,
            cycleId,
            toolName,
            success,
            durationMs,
            packBinding,
            enrichedParams,
            enrichedResult,
            unmatchedParamFields,
            unmatchedResultFields,
            timestampNs: tsNs,
            prevSha: this.prevSha,
        };
        this.emitAndChain(enrichedEvent);
    }
    // -------------------------------------------------------------------------
    // Private: rule application
    // -------------------------------------------------------------------------
    /**
     * Apply a set of FieldRedactionRules to a flat-or-nested object.
     *
     * Returns:
     *   enriched   -- copy of obj with rule-matching fields redacted/hashed/tokenized.
     *   unmatched  -- leaf field paths present in obj that had no matching rule.
     *                 Logged so operators can detect alias attacks (PT-AUDIT-ENRICH-1).
     */
    applyRules(orgId, obj, rules) {
        const ruleMap = new Map();
        for (const r of rules) {
            ruleMap.set(r.fieldPath, r.mode);
        }
        const enriched = this.deepClone(obj);
        const unmatched = [];
        this.visitLeaves(enriched, '', ruleMap, unmatched, orgId);
        return { enriched, unmatched };
    }
    /**
     * Recursively visit all leaf paths in obj.
     * For each leaf: if a matching rule exists, apply redaction mode.
     * Otherwise, push path to unmatched list (PT-AUDIT-ENRICH-1 alias detection).
     *
     * Supports:
     *   - Dot notation: 'metadata.userId'
     *   - Array wildcard: 'attachments[*].content' via '[*]' segment
     */
    visitLeaves(obj, prefix, ruleMap, unmatched, orgId, parent, parentKey) {
        if (Array.isArray(obj)) {
            for (let i = 0; i < obj.length; i++) {
                const elementPath = prefix ? `${prefix.replace(/\[\*\]$/, '')}[*]` : '[*]';
                // Try exact array element path e.g. attachments[*].content is handled by parent
                this.visitLeaves(obj[i], elementPath, ruleMap, unmatched, orgId, obj, i);
            }
            return;
        }
        if (obj !== null && typeof obj === 'object') {
            const record = obj;
            for (const [k, v] of Object.entries(record)) {
                const childPath = prefix ? `${prefix}.${k}` : k;
                // Also check array-wildcard path for nested array paths
                const arrayPath = prefix.includes('[*]') ? childPath : childPath;
                this.visitLeaves(v, arrayPath, ruleMap, unmatched, orgId, record, k);
            }
            return;
        }
        // Leaf value
        if (parent === undefined || parentKey === undefined)
            return;
        const paths = this.candidatePaths(prefix);
        let matched = false;
        for (const candidatePath of paths) {
            const mode = ruleMap.get(candidatePath);
            if (mode !== undefined) {
                matched = true;
                const raw = String(obj ?? '');
                if (Array.isArray(parent)) {
                    parent[parentKey] = this.applyMode(orgId, candidatePath, raw, mode);
                }
                else {
                    parent[parentKey] = this.applyMode(orgId, candidatePath, raw, mode);
                }
                break;
            }
        }
        if (!matched) {
            // Leaf had no matching rule -- log path so operators can detect aliases
            unmatched.push(prefix);
        }
    }
    /**
     * Return all candidate path strings to check in ruleMap for a given prefix.
     * Handles both exact paths and array-wildcard normalization.
     */
    candidatePaths(prefix) {
        // Return both the exact path and a normalized [*] version
        const paths = [prefix];
        // Normalize numeric indices to [*] wildcard for matching
        const wildcardPath = prefix.replace(/\[\d+\]/g, '[*]');
        if (wildcardPath !== prefix)
            paths.push(wildcardPath);
        return paths;
    }
    /**
     * Apply a single redaction mode to a raw string value.
     *
     * redact   -> '<REDACTED>'
     * hash     -> 'sha256:<16-hex>' (first 16 hex chars of SHA-256)
     * tokenize -> 'tok_<8-hex>_<seq>' (per-org reversible token)
     * log      -> raw value (explicit pass-through)
     */
    applyMode(orgId, fieldPath, rawValue, mode) {
        switch (mode) {
            case 'redact':
                return '<REDACTED>';
            case 'hash': {
                const full = crypto.createHash('sha256').update(rawValue, 'utf8').digest('hex');
                return `sha256:${full.slice(0, 16)}`;
            }
            case 'tokenize': {
                const store = this.getOrCreateTokenStore(orgId);
                return store.tokenize(fieldPath, rawValue);
            }
            case 'log':
                return rawValue;
        }
    }
    // -------------------------------------------------------------------------
    // Private: helpers
    // -------------------------------------------------------------------------
    ruleKey(orgId, agentId, toolName) {
        return `${orgId}::${agentId}::${toolName}`;
    }
    getOrCreateTokenStore(orgId) {
        let store = this.tokenStores.get(orgId);
        if (!store) {
            store = new PerOrgTokenStore(orgId);
            this.tokenStores.set(orgId, store);
        }
        return store;
    }
    normalizeResult(result) {
        if (result === null || result === undefined)
            return {};
        if (result instanceof Error)
            return { error: result.message };
        if (typeof result === 'object' && !Array.isArray(result)) {
            return result;
        }
        return { value: result };
    }
    deepClone(obj) {
        return JSON.parse(JSON.stringify(obj));
    }
    /**
     * Emit an audit event and advance the SHA-256 chain.
     * Chain ensures replay or modification is detectable.
     */
    emitAndChain(event) {
        this.auditSink(event);
        this.prevSha = crypto
            .createHash('sha256')
            .update(JSON.stringify(event), 'utf8')
            .digest('hex');
    }
    /**
     * Return current time as nanoseconds (string).
     */
    nowNs() {
        try {
            return String(process.hrtime.bigint());
        }
        catch {
            return String(BigInt(Date.now()) * 1000000n);
        }
    }
    /**
     * Expose current prevSha for chain verification in tests.
     */
    getCurrentChainHash() {
        return this.prevSha;
    }
}
exports.PostToolAuditEnricher = PostToolAuditEnricher;
//# sourceMappingURL=post-tool-audit-enrichment.js.map