/**
 * MemoryAuditChain -- Cryptographic continuity chain for cross-session memory.
 *
 * Every mutation to cross-session memory for an (orgId, agentId) namespace
 * produces a SHA-256 chain link bound to the prior state. Tampering with the
 * underlying store between sessions breaks the chain and fires a
 * MEMORY_CHAIN_TAMPERED audit event at severity ERROR.
 *
 * Design -- Merkle-like state commitment per (orgId, agentId):
 *
 *   nextStateHash = sha256(prevStateHash || operationHash || currentStateHash)
 *
 * Where:
 *   operationHash    = sha256(canonicalize({ kind, key, valueHash, scope,
 *                                           packBinding (sorted), seq, timestampNs }))
 *   currentStateHash = sha256(canonicalize(sortedStateEntries))
 *   sortedStateEntries = sorted-by-key list of { key, valueHash, scope, packBinding }
 *   valueHash        = sha256(JSON.stringify(value)) -- raw values NEVER emitted.
 *
 * Canonicalization rules (see canonicalizeJson):
 *   Object keys sorted lexicographically; arrays preserve element order;
 *   applied recursively. A third-party auditor can reproduce by applying the
 *   same sort-keys-and-stringify over the structured data.
 *
 * Attack model:
 *   External tampering: a DB write outside the API inserts a rogue entry.
 *     Detection: the next recordOperation recomputes currentState and finds
 *     the rogue entry; stateHash diverges from the chain.
 *   Replay attack: attacker replays a stale link (seq N) after the chain
 *     is at seq M > N. isReplay() check rejects seq !== M+1.
 *   Forgery: fabricated chain link without correct prevStateHash causes
 *     hash divergence at the next legitimate operation.
 *   Genesis spoofing: attacker fabricates a genesis for a different agentId.
 *     genesis = sha256('memory-chain-genesis-' + orgId + ':' + agentId),
 *     so a wrong agentId produces a wrong genesis and verification fails
 *     at the first real operation.
 *
 * Compliance bindings:
 *   HIPAA 164.312(c)(1): integrity controls for PHI in cross-session memory
 *   SOC 2 CC7.2: monitoring system components for tampering
 *   EU AI Act Art. 12: record-keeping requirements
 *   ISO 42001 A.9.1: AI system monitoring
 *
 * Storage:
 *   Chain links are stored as MEMORY_CHAIN_LINK audit events through the
 *   provided AuditSink. No new storage tier required -- the same Splunk HEC /
 *   Syslog RFC 5424 destinations used for governance events accept these.
 *
 * @connexum/ai-governance P3-T1
 */
import * as crypto from 'crypto';
// ---------------------------------------------------------------------------
// Canonicalization helpers
// ---------------------------------------------------------------------------
/**
 * Sort all keys of an object recursively, then serialize to JSON.
 * Ensures determinism across key insertion order.
 *
 * Rules:
 *   1. Object keys are sorted lexicographically (Unicode code point order).
 *   2. Arrays preserve element order (element order is semantically meaningful).
 *   3. null / primitives serialise as standard JSON.
 *   4. Canonicalization applied recursively to nested objects.
 *
 * A third-party auditor can reproduce:
 *   (1) Parse the stored canonical string back to JSON.
 *   (2) Apply this function to reserialise with sorted keys.
 *   (3) SHA-256 hash the result.
 */
export function canonicalizeJson(value) {
    if (value === null || value === undefined)
        return JSON.stringify(value);
    if (typeof value !== 'object')
        return JSON.stringify(value);
    if (Array.isArray(value)) {
        return '[' + value.map((v) => canonicalizeJson(v)).join(',') + ']';
    }
    const obj = value;
    const sortedKeys = Object.keys(obj).sort();
    const pairs = sortedKeys.map((k) => JSON.stringify(k) + ':' + canonicalizeJson(obj[k]));
    return '{' + pairs.join(',') + '}';
}
function sha256hex(data) {
    return crypto.createHash('sha256').update(data, 'utf8').digest('hex');
}
// ---------------------------------------------------------------------------
// Replay state for verifyChain
// ---------------------------------------------------------------------------
class ReplayState {
    entries = new Map();
    snapshot() {
        return [...this.entries.values()].sort((a, b) => a.key.localeCompare(b.key));
    }
    apply(link) {
        if (link.kind === 'set') {
            this.entries.set(link.key, {
                key: link.key,
                valueHash: link.valueHash,
                scope: link.scope,
                packBinding: [...link.packBinding],
            });
        }
        else {
            this.entries.delete(link.key);
        }
    }
}
// ---------------------------------------------------------------------------
// MemoryAuditChain
// ---------------------------------------------------------------------------
export class MemoryAuditChain {
    cfg;
    sink;
    chains = new Map();
    constructor(config = {}, sink) {
        this.cfg = {
            verifyOnRead: config.verifyOnRead ?? 'sample',
            sampleRate: config.sampleRate ?? 0.1,
            verifyOnStartup: config.verifyOnStartup ?? true,
            rng: config.rng ?? Math.random.bind(Math),
        };
        this.sink = sink ?? (() => undefined);
    }
    // -------------------------------------------------------------------------
    // Genesis hash
    // -------------------------------------------------------------------------
    /**
     * Genesis hash is a pure function of (orgId, agentId).
     * An attacker who fabricates a genesis using a different agentId will
     * produce a different genesis, causing verification failure at the first
     * real operation.
     */
    getGenesisHash(orgId, agentId) {
        return sha256hex('memory-chain-genesis-' + orgId + ':' + agentId);
    }
    // -------------------------------------------------------------------------
    // Current state hash
    // -------------------------------------------------------------------------
    getCurrentStateHash(orgId, agentId) {
        const chain = this.chains.get(this.chainKey(orgId, agentId));
        if (!chain || chain.links.length === 0) {
            return this.getGenesisHash(orgId, agentId);
        }
        return chain.links[chain.links.length - 1].stateHash;
    }
    // -------------------------------------------------------------------------
    // recordOperation
    // -------------------------------------------------------------------------
    /**
     * Record a mutation to cross-session memory and emit a chain link.
     *
     * Must be called by CrossSessionMemoryStore on every write and delete.
     *
     * MANDATORY: packBinding must be non-empty.
     * Raw values NEVER appear in any audit payload; only valueHash is stored.
     *
     * Returns the new state hash.
     */
    recordOperation(orgId, agentId, op) {
        if (!op.packBinding || op.packBinding.length === 0) {
            throw new EmptyPackBindingError(op.key, orgId, agentId);
        }
        const k = this.chainKey(orgId, agentId);
        if (!this.chains.has(k)) {
            this.chains.set(k, { links: [], state: new Map() });
        }
        const chain = this.chains.get(k);
        const prevStateHash = chain.links.length > 0
            ? chain.links[chain.links.length - 1].stateHash
            : this.getGenesisHash(orgId, agentId);
        const seq = chain.links.length + 1;
        // valueHash: sha256 of raw value. Raw value NEVER stored or emitted.
        const valueHash = op.kind === 'set' && op.value !== undefined
            ? sha256hex(JSON.stringify(op.value))
            : '';
        // Apply mutation to live state snapshot (no raw values stored here).
        if (op.kind === 'set') {
            chain.state.set(op.key, {
                key: op.key,
                valueHash,
                scope: op.scope,
                packBinding: [...op.packBinding],
            });
        }
        else {
            chain.state.delete(op.key);
        }
        // canonicalize(operation)
        const opPayload = {
            key: op.key,
            kind: op.kind,
            packBinding: [...op.packBinding].sort(),
            scope: op.scope,
            seq,
            timestampNs: op.timestampNs,
            valueHash,
        };
        const operationHash = sha256hex(canonicalizeJson(opPayload));
        // canonicalize(currentState) -- sorted by key, no raw values
        const stateSnapshot = [...chain.state.values()].sort((a, b) => a.key.localeCompare(b.key));
        const currentStateHash = sha256hex(canonicalizeJson(stateSnapshot));
        // nextStateHash = sha256(prevStateHash || operationHash || currentStateHash)
        const nextStateHash = sha256hex(prevStateHash + operationHash + currentStateHash);
        const link = {
            type: 'MEMORY_CHAIN_LINK',
            orgId,
            agentId,
            seq,
            prevStateHash,
            operationHash,
            stateHash: nextStateHash,
            timestamp: new Date().toISOString(),
            packBinding: [...op.packBinding],
            kind: op.kind,
            key: op.key,
            valueHash,
            scope: op.scope,
            timestampNs: op.timestampNs,
        };
        chain.links.push(link);
        this.sink(link);
        return nextStateHash;
    }
    // -------------------------------------------------------------------------
    // verifyChain
    // -------------------------------------------------------------------------
    /**
     * Replay all recorded links from genesis and recompute each stateHash.
     * Any divergence indicates tampering.
     *
     * On tamper detection:
     *   Emits MEMORY_CHAIN_TAMPERED event at severity ERROR.
     *   Returns { valid: false, brokenAt: seq, expected, actual }.
     *
     * On success:
     *   Returns { valid: true }.
     */
    verifyChain(orgId, agentId) {
        const k = this.chainKey(orgId, agentId);
        const chain = this.chains.get(k);
        if (!chain || chain.links.length === 0) {
            return { valid: true };
        }
        const replay = new ReplayState();
        let prevStateHash = this.getGenesisHash(orgId, agentId);
        for (const link of chain.links) {
            // Recompute operationHash using all fields stored on the link.
            const opPayload = {
                key: link.key,
                kind: link.kind,
                packBinding: [...link.packBinding].sort(),
                scope: link.scope,
                seq: link.seq,
                timestampNs: link.timestampNs,
                valueHash: link.valueHash,
            };
            const recomputedOperationHash = sha256hex(canonicalizeJson(opPayload));
            // Apply operation to replay state.
            replay.apply(link);
            // Recompute currentStateHash from the replay state after this operation.
            const stateSnapshot = replay.snapshot();
            const recomputedCurrentStateHash = sha256hex(canonicalizeJson(stateSnapshot));
            // Recompute the full nextStateHash.
            const recomputedNextStateHash = sha256hex(prevStateHash + recomputedOperationHash + recomputedCurrentStateHash);
            if (recomputedNextStateHash !== link.stateHash) {
                const tamperEvent = {
                    type: 'MEMORY_CHAIN_TAMPERED',
                    severity: 'ERROR',
                    orgId,
                    agentId,
                    brokenAt: link.seq,
                    expectedStateHash: recomputedNextStateHash,
                    actualStateHash: link.stateHash,
                    timestamp: new Date().toISOString(),
                };
                this.sink(tamperEvent);
                return {
                    valid: false,
                    brokenAt: link.seq,
                    expected: recomputedNextStateHash,
                    actual: link.stateHash,
                };
            }
            prevStateHash = link.stateHash;
        }
        return { valid: true };
    }
    // -------------------------------------------------------------------------
    // Sampling
    // -------------------------------------------------------------------------
    /**
     * Returns true if this read should trigger verifyChain().
     * Caller is responsible for calling verifyChain when this returns true.
     *
     * Default policy 'sample' at 10%: bounds overhead on high-throughput read
     * paths while still surfacing tampering within O(10) reads on average.
     * For HIPAA deployments, set verifyOnRead: 'always'.
     */
    shouldVerifyOnRead() {
        switch (this.cfg.verifyOnRead) {
            case 'always': return true;
            case 'never': return false;
            case 'sample': return this.cfg.rng() < this.cfg.sampleRate;
        }
    }
    // -------------------------------------------------------------------------
    // Replay attack detection
    // -------------------------------------------------------------------------
    /**
     * Returns true if the given seq is a replay (already processed or
     * out-of-expected-order) for this (orgId, agentId) chain.
     *
     * Expected next seq = current link count + 1.
     * Anything else is a replay or injection attempt.
     */
    isReplay(orgId, agentId, seq) {
        const k = this.chainKey(orgId, agentId);
        const chain = this.chains.get(k);
        const nextExpected = (chain?.links.length ?? 0) + 1;
        return seq !== nextExpected;
    }
    // -------------------------------------------------------------------------
    // Test / introspection helpers (prefixed _ -- not part of public API)
    // -------------------------------------------------------------------------
    /**
     * Tamper a stored link's stateHash directly.
     * Simulates an out-of-band write that corrupts the chain.
     * Only for test use -- DO NOT call in production code.
     */
    _tamperLink(orgId, agentId, seq, newStateHash) {
        const k = this.chainKey(orgId, agentId);
        const chain = this.chains.get(k);
        if (!chain)
            return;
        const link = chain.links.find((l) => l.seq === seq);
        if (link)
            link.stateHash = newStateHash;
    }
    /** Returns a copy of the chain links for a given (orgId, agentId). */
    _getLinks(orgId, agentId) {
        const k = this.chainKey(orgId, agentId);
        return [...(this.chains.get(k)?.links ?? [])];
    }
    // -------------------------------------------------------------------------
    // Private helpers
    // -------------------------------------------------------------------------
    chainKey(orgId, agentId) {
        return orgId + ':' + agentId;
    }
}
// ---------------------------------------------------------------------------
// Errors
// ---------------------------------------------------------------------------
export class EmptyPackBindingError extends Error {
    constructor(key, orgId, agentId) {
        super(`MemoryAuditChain.recordOperation: packBinding must be non-empty for key '${key}' ` +
            `(orgId=${orgId}, agentId=${agentId}). Pack-binding is mandatory per governance policy.`);
        this.name = 'EmptyPackBindingError';
    }
}
//# sourceMappingURL=memory-audit-chain.js.map