"use strict";
/**
 * RagRetrievalAuditLogger -- Durable forensic trail for every RAG retrieval call.
 *
 * This is the fourth gate in the P3-T1 RAG governance cluster:
 *
 *   PR #18 RagSourceAllowlist (pre-retrieval):
 *     Which sources may the agent query at all?
 *
 *   PR #20 RagCitationEnforcer (post-output, in-memory pool):
 *     Are citations in the per-cycle retrieval pool?
 *
 *   PR #23 SourceOutputChainVerifier (post-citation-enforcement):
 *     Does output text show evidence the cited chunk was actually used?
 *
 *   THIS FILE -- RagRetrievalAuditLogger (durable record, BUILD_PLAN line 249):
 *     Every retrieval call (allowed or blocked) is written to an immutable
 *     SHA-256 audit chain. Source identity is preserved via sourceHash and
 *     sourceKind. Raw URLs and query text NEVER enter the chain.
 *
 * Why this matters:
 *   - Auditors (HIPAA OCR, GDPR DPA, SOC 2 Type II) need "what did this agent
 *     see, when, from which source." The per-cycle pool (PR #20) is in-memory;
 *     this log is the DURABLE record after cycle teardown.
 *   - Memory-poisoning forensics: if PR #22's chain breaks, retrieval audit
 *     events point to which session ingested the rogue content.
 *   - Litigation hold: challenged agent output requires producing the retrieval
 *     trail. This log is that trail.
 *
 * Source identity design (operator mapping):
 *   sourceHash = SHA-256(source URL/URI).
 *   queryHash  = SHA-256(queryText).
 *   Raw values are NEVER logged. The operator retains the plaintext-to-hash
 *   mapping (e.g., in their internal vector-store catalog or source registry).
 *   Auditors with operator cooperation can deanonymize. Without the mapping,
 *   the hash is opaque -- this is intentional: the audit log can be safely
 *   shared with external reviewers without exposing proprietary source topology.
 *   See docs/runtime/rag-retrieval-audit-log.md for the integration recipe.
 *
 * Pairing:
 *   recordRetrievalAttempt() returns a retrievalId (UUID). The caller MUST
 *   pass that retrievalId to recordRetrievalResult() when the retrieval
 *   completes. If result is not recorded within retrievalAuditPairingTimeoutMs
 *   (default 30 s), the logger emits RAG_RETRIEVAL_AUDIT_PAIRING_VIOLATION ERROR.
 *
 * Cycle reconciler:
 *   At cycle teardown call cycleEnd(cycleId). Any retrievalId opened in that
 *   cycle that never received a result emits RAG_RETRIEVAL_AUDIT_INCOMPLETE ERROR.
 *   Strict-pack tenants (HIPAA, GDPR, PCI DSS, HMDA, DORA) MUST call cycleEnd.
 *
 * Cross-cycle and cross-agent pairing security:
 *   Each retrievalId is keyed by (cycleId, agentId, orgId). Attempts to call
 *   recordRetrievalResult() with a retrievalId from a different cycle, agent,
 *   or org throw RetrievalIdNotInCycleError (PT-RAG-AUDIT-2).
 *
 * Audit chain integrity:
 *   Every event carries prevSha (SHA-256 of the previous event's JSON). A
 *   replayed or modified event breaks the chain, detectable via chain verifier
 *   (PT-RAG-AUDIT-3).
 *
 * Hash function:
 *   SHA-256 (crypto.createHash('sha256')). PT-RAG-AUDIT-1 notes that collision
 *   resistance is computationally infeasible for SHA-256; MD5/SHA-1 are
 *   explicitly NOT used (see test assertion).
 *
 * Pack-binding mandatory:
 *   Per feedback_pack_binding_primacy.md: every call must carry at least one
 *   pack. Empty binding throws RagRetrievalAuditUnboundError.
 *
 * 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
 *   - HMDA Reg C 1003.4: data-collection audit trail
 *   - DORA Art. 9: ICT risk -- retrieval traceability for incident forensics
 *   - SOC 2 CC7.2: monitoring of system components
 *
 * BUILD_PLAN reference: line 249 "Retrieval audit logging with source identity
 * preserved".
 *
 * @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.RagRetrievalAuditLogger = exports.RetrievalIdNotInCycleError = exports.RagRetrievalAuditUnboundError = exports.RAG_AUDIT_PAIRING_TIMEOUT_MS = exports.RAG_AUDIT_STRICT_MODE_PACKS = void 0;
const crypto = __importStar(require("node:crypto"));
// ---------------------------------------------------------------------------
// Strict-mode packs (mirrors PR #18, #20, #23)
// ---------------------------------------------------------------------------
/**
 * Packs for which retrieval audit logging is MANDATORY.
 * Matches SOURCE_OUTPUT_STRICT_MODE_PACKS in rag-source-output-chain.ts.
 *
 * hipaa   -- HIPAA 164.312(b): Audit Controls
 * gdpr    -- GDPR Art. 5(2): Accountability
 * pci-dss -- PCI DSS Req 10: Track and monitor all access
 * hmda    -- HMDA Reg C 1003.4: Data collection audit
 * dora    -- DORA Art. 9: ICT risk traceability
 */
exports.RAG_AUDIT_STRICT_MODE_PACKS = new Set([
    'hipaa',
    'gdpr',
    'pci-dss',
    'hmda',
    'dora',
]);
/** Default pairing timeout in milliseconds (30 s). */
exports.RAG_AUDIT_PAIRING_TIMEOUT_MS = 30_000;
// ---------------------------------------------------------------------------
// Error classes
// ---------------------------------------------------------------------------
/**
 * Thrown when packBinding is empty on any audit call.
 * Every retrieval audit record must carry at least one pack.
 */
class RagRetrievalAuditUnboundError extends Error {
    context;
    constructor(context) {
        super(`RagRetrievalAuditLogger: packBinding must not be empty. Context: ${context}. ` +
            `Per pack-binding-primacy governance: every retrieval audit record must ` +
            `carry at least one pack binding for retention policy derivation.`);
        this.name = 'RagRetrievalAuditUnboundError';
        this.context = context;
    }
}
exports.RagRetrievalAuditUnboundError = RagRetrievalAuditUnboundError;
/**
 * Thrown when recordRetrievalResult() is called with a retrievalId that does
 * not belong to the given (cycleId, agentId, orgId) scope.
 *
 * PT-RAG-AUDIT-2: prevents cross-cycle and cross-agent result injection.
 */
class RetrievalIdNotInCycleError extends Error {
    retrievalId;
    orgId;
    agentId;
    cycleId;
    constructor(opts) {
        super(`RagRetrievalAuditLogger: retrievalId '${opts.retrievalId}' is not registered ` +
            `in scope (org='${opts.orgId}', agent='${opts.agentId}', cycle='${opts.cycleId}'). ` +
            `Reason: ${opts.reason}. ` +
            `This may indicate a cross-cycle or cross-agent result-injection attempt (PT-RAG-AUDIT-2). ` +
            `Only the caller that received the retrievalId from recordRetrievalAttempt() ` +
            `may submit the matching result.`);
        this.name = 'RetrievalIdNotInCycleError';
        this.retrievalId = opts.retrievalId;
        this.orgId = opts.orgId;
        this.agentId = opts.agentId;
        this.cycleId = opts.cycleId;
    }
}
exports.RetrievalIdNotInCycleError = RetrievalIdNotInCycleError;
/**
 * Durable forensic audit logger for RAG retrieval calls.
 *
 * Integration recipe:
 *
 *   const auditLogger = new RagRetrievalAuditLogger(sink, config);
 *
 *   // Step 1 -- call AFTER PR #18 allowlist decision, BEFORE actual retrieval.
 *   const retrievalId = auditLogger.recordRetrievalAttempt(
 *     orgId, agentId, cycleId,
 *     {
 *       source: 'https://vectordb.internal/collections/patient-notes',
 *       sourceKind: 'vector-store',
 *       queryText: 'elevated glucose levels for patient X',
 *       allowedByAllowlist: true,
 *     },
 *     ['hipaa'],
 *   );
 *
 *   // Step 2 -- dispatch actual retrieval call ...
 *   const chunks = await vectorDb.query(queryText, k=5);
 *
 *   // Step 3 -- call immediately after retrieval returns (or errors/times out).
 *   auditLogger.recordRetrievalResult(
 *     orgId, agentId, cycleId, retrievalId,
 *     {
 *       chunksReturned: chunks.length,
 *       chunkRefs: chunks.map(c => c.id),
 *       tookMs: elapsed,
 *       providerStatus: 'ok',
 *     },
 *     ['hipaa'],
 *   );
 *
 *   // Step 4 -- at cycle teardown (after PR #20 and PR #23 gates run):
 *   auditLogger.cycleEnd(orgId, agentId, cycleId);
 */
class RagRetrievalAuditLogger {
    config;
    auditSink;
    /**
     * SHA-256 chain head for tamper-evident linking.
     * Every emitted event advances this value.
     */
    prevSha = '0'.repeat(64);
    /**
     * Pending attempts keyed by retrievalId.
     * Entry exists from recordRetrievalAttempt() until recordRetrievalResult()
     * is called (or cycleEnd() clears unpaired entries).
     */
    pendingAttempts = new Map();
    /**
     * Timer handles for pairing-timeout enforcement.
     * Keyed by retrievalId.
     */
    pairingTimers = new Map();
    constructor(auditSink, config = {}) {
        this.auditSink = auditSink ?? (() => undefined);
        this.config = {
            retrievalAuditPairingTimeoutMs: config.retrievalAuditPairingTimeoutMs ?? exports.RAG_AUDIT_PAIRING_TIMEOUT_MS,
        };
    }
    // ---------------------------------------------------------------------------
    // recordRetrievalAttempt()
    // ---------------------------------------------------------------------------
    /**
     * Record a retrieval attempt (allowed or blocked).
     *
     * Call AFTER the PR #18 allowlist decision and BEFORE dispatching the actual
     * retrieval call. This ensures every retrieval -- whether it proceeds or is
     * blocked -- has a durable audit record.
     *
     * @returns retrievalId (UUID). Pass to recordRetrievalResult() when the
     *          retrieval completes. MUST NOT be shared across cycles or agents.
     * @throws RagRetrievalAuditUnboundError if packBinding is empty.
     */
    recordRetrievalAttempt(orgId, agentId, cycleId, attempt, packBinding) {
        if (!packBinding || packBinding.length === 0) {
            throw new RagRetrievalAuditUnboundError(`recordRetrievalAttempt orgId=${orgId} agentId=${agentId} cycleId=${cycleId}`);
        }
        const retrievalId = crypto.randomUUID();
        const timestampNs = this.nowNs();
        const nowMs = Date.now();
        const event = {
            type: 'RAG_RETRIEVAL_ATTEMPTED',
            severity: attempt.allowedByAllowlist ? 'NOTICE' : 'WARNING',
            orgId,
            agentId,
            cycleId,
            retrievalId,
            // SHA-256 hash -- raw source URL NEVER logged (PT-RAG-AUDIT-1: SHA-256)
            sourceHash: this.sha256(attempt.source),
            sourceKind: attempt.sourceKind,
            // SHA-256 hash -- raw query text NEVER logged
            queryHash: this.sha256(attempt.queryText),
            allowedByAllowlist: attempt.allowedByAllowlist,
            rejectReason: attempt.rejectReason,
            packBinding,
            timestampNs,
            prevSha: this.prevSha,
        };
        this.emitAndChain(event);
        // Only register pending state when retrieval is allowed. A blocked
        // retrieval never proceeds, so no result is expected.
        if (attempt.allowedByAllowlist) {
            const pending = {
                orgId,
                agentId,
                cycleId,
                retrievalId,
                openedAtNs: BigInt(timestampNs),
                openedAtMs: nowMs,
                packBinding,
                resultReceived: false,
            };
            this.pendingAttempts.set(retrievalId, pending);
            // Start pairing-timeout watchdog
            const timer = setTimeout(() => {
                this.onPairingTimeout(retrievalId);
            }, this.config.retrievalAuditPairingTimeoutMs);
            // Do not prevent Node.js process exit
            if (timer.unref)
                timer.unref();
            this.pairingTimers.set(retrievalId, timer);
        }
        return retrievalId;
    }
    // ---------------------------------------------------------------------------
    // recordRetrievalResult()
    // ---------------------------------------------------------------------------
    /**
     * Record the outcome of a retrieval call.
     *
     * Call immediately after the retrieval returns (whether ok, error, or timeout).
     * Must be paired with the retrievalId returned by recordRetrievalAttempt().
     *
     * Cross-cycle and cross-agent pairing is rejected: if the (cycleId, agentId,
     * orgId) do not match the scope under which the retrievalId was opened,
     * throws RetrievalIdNotInCycleError (PT-RAG-AUDIT-2).
     *
     * @throws RagRetrievalAuditUnboundError if packBinding is empty.
     * @throws RetrievalIdNotInCycleError if retrievalId is unknown or mismatched.
     */
    recordRetrievalResult(orgId, agentId, cycleId, retrievalId, result, packBinding) {
        if (!packBinding || packBinding.length === 0) {
            throw new RagRetrievalAuditUnboundError(`recordRetrievalResult orgId=${orgId} agentId=${agentId} cycleId=${cycleId} retrievalId=${retrievalId}`);
        }
        // PT-RAG-AUDIT-2: scope validation -- reject cross-cycle / cross-agent pairing
        const pending = this.pendingAttempts.get(retrievalId);
        if (!pending) {
            throw new RetrievalIdNotInCycleError({
                retrievalId,
                orgId,
                agentId,
                cycleId,
                reason: 'retrievalId not found in pending attempts (unknown, already closed, or never opened)',
            });
        }
        if (pending.orgId !== orgId ||
            pending.agentId !== agentId ||
            pending.cycleId !== cycleId) {
            throw new RetrievalIdNotInCycleError({
                retrievalId,
                orgId,
                agentId,
                cycleId,
                reason: `retrievalId was opened in scope (org='${pending.orgId}', ` +
                    `agent='${pending.agentId}', cycle='${pending.cycleId}') ` +
                    `but result submitted for different scope`,
            });
        }
        // Cancel the pairing-timeout watchdog
        const timer = this.pairingTimers.get(retrievalId);
        if (timer) {
            clearTimeout(timer);
            this.pairingTimers.delete(retrievalId);
        }
        pending.resultReceived = true;
        this.pendingAttempts.delete(retrievalId);
        const timestampNs = this.nowNs();
        const event = {
            type: 'RAG_RETRIEVAL_COMPLETED',
            severity: result.providerStatus === 'ok' ? 'NOTICE' : 'WARNING',
            orgId,
            agentId,
            cycleId,
            retrievalId,
            chunksReturned: result.chunksReturned,
            // SHA-256 of each chunkRef -- raw strings NEVER logged
            chunkRefHashes: result.chunkRefs.map((ref) => this.sha256(ref)),
            tookMs: result.tookMs,
            providerStatus: result.providerStatus,
            errorReason: result.errorReason,
            packBinding,
            timestampNs,
            prevSha: this.prevSha,
        };
        this.emitAndChain(event);
    }
    // ---------------------------------------------------------------------------
    // cycleEnd()
    // ---------------------------------------------------------------------------
    /**
     * Reconcile all open retrievalIds for the given cycle.
     *
     * Any retrievalId opened in this cycle that has NOT received a result emits
     * RAG_RETRIEVAL_AUDIT_INCOMPLETE severity ERROR. This catches retrieval calls
     * that completed but whose result logging was bypassed.
     *
     * Strict-pack tenants (HIPAA, GDPR, PCI DSS, HMDA, DORA) MUST call this at
     * every cycle teardown. Omitting it is itself a governance gap (no automatic
     * enforcement here -- the orchestrator is responsible for wiring this call).
     *
     * @returns Number of INCOMPLETE events emitted.
     */
    cycleEnd(orgId, agentId, cycleId) {
        const incomplete = [];
        for (const [id, pending] of this.pendingAttempts.entries()) {
            if (pending.orgId === orgId &&
                pending.agentId === agentId &&
                pending.cycleId === cycleId &&
                !pending.resultReceived) {
                incomplete.push(pending);
                // Cancel any still-running pairing timer
                const timer = this.pairingTimers.get(id);
                if (timer) {
                    clearTimeout(timer);
                    this.pairingTimers.delete(id);
                }
                this.pendingAttempts.delete(id);
            }
        }
        const detectedNs = this.nowNs();
        for (const pending of incomplete) {
            const event = {
                type: 'RAG_RETRIEVAL_AUDIT_INCOMPLETE',
                severity: 'ERROR',
                orgId: pending.orgId,
                agentId: pending.agentId,
                cycleId: pending.cycleId,
                retrievalId: pending.retrievalId,
                openedAtNs: String(pending.openedAtNs),
                packBinding: pending.packBinding,
                timestampNs: detectedNs,
                prevSha: this.prevSha,
            };
            this.emitAndChain(event);
        }
        return incomplete.length;
    }
    // ---------------------------------------------------------------------------
    // Accessors (for testing)
    // ---------------------------------------------------------------------------
    /**
     * Current SHA chain head (SHA-256 of the last emitted event's JSON).
     * Exposed for test introspection of chain integrity.
     */
    get currentChainHead() {
        return this.prevSha;
    }
    /**
     * Number of pending (unmatched) attempts currently tracked.
     */
    get pendingAttemptCount() {
        return this.pendingAttempts.size;
    }
    // ---------------------------------------------------------------------------
    // Private helpers
    // ---------------------------------------------------------------------------
    /**
     * Compute SHA-256 of a string.
     *
     * PT-RAG-AUDIT-1: explicitly SHA-256 (not MD5, not SHA-1).
     * Collision resistance is computationally infeasible for SHA-256.
     */
    sha256(input) {
        return crypto.createHash('sha256').update(input, 'utf8').digest('hex');
    }
    /**
     * Emit an audit event and advance the SHA chain.
     * The chain ensures any replay or modification is detectable (PT-RAG-AUDIT-3).
     */
    emitAndChain(event) {
        this.auditSink(event);
        this.prevSha = crypto
            .createHash('sha256')
            .update(JSON.stringify(event), 'utf8')
            .digest('hex');
    }
    /**
     * Return current time as nanoseconds (string) using process.hrtime.bigint().
     * Uses Date.now() * 1e6 as a fallback in environments without hrtime.bigint.
     */
    nowNs() {
        try {
            return String(process.hrtime.bigint());
        }
        catch {
            return String(BigInt(Date.now()) * 1000000n);
        }
    }
    /**
     * Pairing-timeout callback. Fires when recordRetrievalResult() was not
     * called within retrievalAuditPairingTimeoutMs of recordRetrievalAttempt().
     */
    onPairingTimeout(retrievalId) {
        const pending = this.pendingAttempts.get(retrievalId);
        if (!pending || pending.resultReceived) {
            // Already closed by recordRetrievalResult() or cycleEnd() -- race.
            return;
        }
        this.pairingTimers.delete(retrievalId);
        this.pendingAttempts.delete(retrievalId);
        const detectedNs = this.nowNs();
        const event = {
            type: 'RAG_RETRIEVAL_AUDIT_PAIRING_VIOLATION',
            severity: 'ERROR',
            orgId: pending.orgId,
            agentId: pending.agentId,
            cycleId: pending.cycleId,
            retrievalId: pending.retrievalId,
            openedAtNs: String(pending.openedAtNs),
            detectedAtNs: detectedNs,
            pairingTimeoutMs: this.config.retrievalAuditPairingTimeoutMs,
            packBinding: pending.packBinding,
            timestampNs: detectedNs,
            prevSha: this.prevSha,
        };
        this.emitAndChain(event);
    }
}
exports.RagRetrievalAuditLogger = RagRetrievalAuditLogger;
//# sourceMappingURL=rag-retrieval-audit.js.map