"use strict";
/**
 * PackDrivenClassifier -- category enforcement loaded from pack declarations.
 *
 * ARCHITECTURAL DIRECTIVE (Thomas, 2026-04-24):
 * "After W1 lands, NO runtime code should reference 'PHI', 'EDUCATION_RECORD',
 * or any other category by literal string. Categories are loaded from pack
 * declarations at boot time and looked up by id."
 *
 * This class replaces all hardcoded category string comparisons throughout
 * the runtime. Packs declare their categories, classifiers, and actions.
 * The runtime evaluates them at classification time.
 *
 * Strictest-wins resolution: BLOCK > REQUIRE_APPROVAL > WARN > LOG > ALLOW
 *
 * Audit events are emitted for EVERY matched category automatically --
 * no pack-specific logging code required. New packs added after deployment
 * are enforced immediately after loadFromPacks() is called.
 *
 * @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.PackDrivenClassifier = exports.IdentityAttributionError = exports.PLAINTEXT_TRANSPORT_MECHANISMS = exports.TRANSPORT_DEFAULT_INTEGRITY = void 0;
exports.getGlobalClassifier = getGlobalClassifier;
exports.setGlobalClassifier = setGlobalClassifier;
const crypto = __importStar(require("crypto"));
/** Default integrity per transport mechanism. */
exports.TRANSPORT_DEFAULT_INTEGRITY = {
    'email': 'tls', // SMTPS; fallback 'plaintext' if plain SMTP
    'api-response': 'tls', // HTTPS assumed
    'webhook': 'tls', // falls back to 'signed' if HMAC present
    'chat-response': 'tls',
    'slack': 'tls',
    'sms': 'plaintext', // SMS is unencrypted by protocol
    'file-export': 'plaintext', // 'signed' if signed, else 'plaintext'
    'database-write': 'encrypted', // 'encrypted' if at-rest encryption verified
    'voice-call': 'tls', // SIP-TLS; else 'plaintext'
    'print': 'plaintext',
    'fax': 'plaintext',
    'in-memory-return': 'encrypted', // process memory (encrypted by OS memory protection)
    'custom': 'plaintext', // flagged for review
};
/**
 * Plaintext transport mechanisms that warrant a compliance warning when used
 * with subject-keyed categories (disclosure.plaintext-transport event).
 *
 * GAP-3: 'custom' is included because custom mechanism is intended for manual
 * review and cannot be assumed to provide any transport security guarantee.
 * When mechanism === 'custom', the server-side API boundary forces
 * integrity = 'plaintext' regardless of caller-asserted value, and records
 * the original caller-supplied value as callerAssertedIntegrity on the event.
 */
exports.PLAINTEXT_TRANSPORT_MECHANISMS = new Set([
    'sms', 'fax', 'print', 'custom',
]);
// ---------------------------------------------------------------------------
// IdentityAttributionError (P3.5 fail-closed enforcement)
// ---------------------------------------------------------------------------
/**
 * Thrown when a subject-keyed category fires but no dataSubjectId was supplied
 * and ENFORCE_FIVE_DIM_IDENTITY runtime flag is true.
 *
 * In Phase 1 (this cycle), the runtime is in grace-period mode (flag=false):
 * it emits a 'disclosure.missing-subject-id' warning event instead of throwing.
 *
 * Phase 2 (after education packs + 2-week migration window): flag flips to true
 * and all callers MUST supply dataSubjectId for subject-keyed categories.
 */
class IdentityAttributionError extends Error {
    packId;
    categoryId;
    missingDimension;
    constructor(packId, categoryId, missingDimension) {
        super(`Five-dimensional identity enforcement: missing ${missingDimension} ` +
            `for subject-keyed category ${packId}::${categoryId}. ` +
            `Supply ${missingDimension} on check_action() to satisfy HIPAA §164.528 / FERPA §99.32 disclosure accounting.`);
        this.name = 'IdentityAttributionError';
        this.packId = packId;
        this.categoryId = categoryId;
        this.missingDimension = missingDimension;
    }
}
exports.IdentityAttributionError = IdentityAttributionError;
// ---------------------------------------------------------------------------
// Action severity for strictest-wins resolution
// ---------------------------------------------------------------------------
const ACTION_SEVERITY = {
    ALLOW: 0,
    LOG: 1,
    WARN: 2,
    REQUIRE_APPROVAL: 3,
    BLOCK: 4,
};
function stricterAction(a, b) {
    return ACTION_SEVERITY[a] >= ACTION_SEVERITY[b] ? a : b;
}
function actionToEventType(action) {
    switch (action) {
        case 'BLOCK': return 'pack.enforcement.block';
        case 'REQUIRE_APPROVAL': return 'pack.enforcement.require_approval';
        case 'WARN': return 'pack.enforcement.warn';
        case 'LOG': return 'pack.enforcement.log';
        default: return 'pack.enforcement.allow';
    }
}
// ---------------------------------------------------------------------------
// PackDrivenClassifier
// ---------------------------------------------------------------------------
class PackDrivenClassifier {
    registry = new Map();
    auditLog = [];
    /**
     * W1 residual (2026-04-24): stores industry tags per pack, populated at loadFromPacks
     * time. Enables incident-notifier to derive severity labels from pack.industries without
     * hardcoding any category or industry strings outside of the packs themselves.
     */
    packIndustries = new Map();
    /**
     * Load category classifiers from an array of packs.
     * Safe to call multiple times -- later calls ADD to the registry.
     * A new pack added after deployment takes effect immediately.
     */
    loadFromPacks(packs) {
        for (const pack of packs) {
            // Store industries for all packs, even those with no scanCategories,
            // so deriveSeverityLabels can resolve labels for any matched packId.
            this.packIndustries.set(pack.id, pack.industries ?? []);
            if (!pack.dataPolicy?.scanCategories)
                continue;
            const subjectKeyed = new Set(pack.subjectKeyedCategories ?? []);
            for (const categoryId of pack.dataPolicy.scanCategories) {
                const rawAction = pack.dataPolicy.categoryActions?.[categoryId] ?? 'LOG';
                const action = this.normalizeAction(rawAction);
                const classifierDef = pack.categoryClassifiers?.[categoryId] ?? {};
                const key = `${pack.id}::${categoryId}`;
                this.registry.set(key, {
                    packId: pack.id,
                    packVersion: pack.version,
                    categoryId,
                    action,
                    classifierDef,
                    subjectKeyed: subjectKeyed.has(categoryId),
                });
            }
        }
    }
    /**
     * Classify an input string against all registered categories.
     * Only evaluates packs whose IDs are in context.activePackIds (if set).
     */
    classify(input, context = {}) {
        const matched = [];
        for (const [, entry] of this.registry) {
            // Project-level pack selection gate
            if (context.activePackIds !== undefined) {
                if (!context.activePackIds.includes(entry.packId))
                    continue;
            }
            if (this.runClassifier(input, context, entry)) {
                matched.push({
                    packId: entry.packId,
                    categoryId: entry.categoryId,
                    action: entry.action,
                    matchConfidence: 1.0,
                    packVersion: entry.packVersion,
                });
            }
        }
        return { matched, hasMatch: matched.length > 0 };
    }
    /**
     * Resolve matched categories into a single GovernanceDecision (strictest-wins).
     * Emits a canonical audit event per matched category with SHA-256 chain signature.
     *
     * P3.5: Accepts optional FiveDimIdentityContext via context.identity.
     * When a subject-keyed category is matched and dataSubject is absent:
     *   - ENFORCE_FIVE_DIM_IDENTITY=false (default, grace period):
     *     emits 'disclosure.missing-subject-id' warning; does NOT fail-closed.
     *   - ENFORCE_FIVE_DIM_IDENTITY=true (Phase 2, flip after education packs):
     *     throws IdentityAttributionError immediately.
     *
     * Plaintext transport on a subject-keyed category emits
     * 'disclosure.plaintext-transport' compliance warning.
     */
    applyActions(matched, context, input) {
        const decisionId = crypto.randomUUID();
        const subjectDigest = crypto.createHash('sha256').update(input).digest('hex');
        const timestamp = new Date().toISOString();
        const enforceFiveDim = this.getEnforceFiveDimFlag();
        // GAP-3: Force plaintext integrity on custom transport mechanism.
        // The caller may attempt to bypass plaintext-transport checks by supplying
        // mechanism='custom' with integrity='tls'. Override integrity to 'plaintext'
        // at the API boundary and record the caller-supplied value for auditors.
        let callerAssertedIntegrity;
        let identity = context.identity;
        if (identity?.transport?.mechanism === 'custom' && identity.transport.integrity !== 'plaintext') {
            callerAssertedIntegrity = identity.transport.integrity;
            identity = {
                ...identity,
                transport: { ...identity.transport, integrity: 'plaintext' },
            };
        }
        let finalAction = 'ALLOW';
        for (const m of matched) {
            finalAction = stricterAction(finalAction, m.action);
        }
        // Compute pseudonymized dataSubjectId once (used across events).
        // GAP-1: pepper is per-tenant -- different orgIds produce different peppers,
        // preventing cross-tenant subject pseudonym collision.
        const orgId = context.orgId;
        let dataSubjectId;
        let dataSubjectType;
        if (identity?.dataSubject) {
            const ds = identity.dataSubject;
            dataSubjectType = ds.subjectType;
            if (ds.pseudonymized === false) {
                // Raw value -- hash it now before storing
                dataSubjectId = crypto.createHash('sha256')
                    .update(ds.subjectId + this.perTenantPepper(orgId))
                    .digest('hex');
            }
            else {
                dataSubjectId = ds.subjectId; // already pseudonymized by caller
            }
        }
        // P1C-04: resolve transaction context and pciEventType once (used across events).
        // Only attached to events from the pci-dss pack for subject-keyed categories.
        const txCtx = context.transactionContext;
        // Emit one canonical audit event per matched category
        for (const m of matched) {
            const entry = this.registry.get(`${m.packId}::${m.categoryId}`);
            const isSubjectKeyed = entry?.subjectKeyed ?? false;
            // Five-dimensional identity check for subject-keyed categories (enforce-mode)
            if (isSubjectKeyed && !dataSubjectId && enforceFiveDim) {
                throw new IdentityAttributionError(m.packId, m.categoryId, 'dataSubjectId');
            }
            // P1C-04: attach PCI Req-10 fields when this is a PCI DSS subject-keyed category
            // and a transaction context was supplied by the caller.
            let transactionContext;
            let pciEventType;
            if (m.packId === 'pci-dss' && isSubjectKeyed && txCtx) {
                transactionContext = txCtx;
                pciEventType = this.derivePciEventType(txCtx.transactionType, finalAction);
            }
            // Main enforcement event with all five dimensions embedded (always first)
            const event = {
                type: actionToEventType(m.action),
                packId: m.packId,
                categoryId: m.categoryId,
                matchConfidence: m.matchConfidence,
                subjectDigest,
                action: m.action,
                decisionId,
                timestamp,
                packVersion: m.packVersion,
                // GAP-1: include orgId so events are attributable to the correct tenant
                orgId,
                // Five-dimensional identity (P3.5)
                operatorId: identity?.operatorId,
                dataSubjectId,
                dataSubjectType,
                recipientId: identity?.recipient?.recipientId,
                recipientType: identity?.recipient?.recipientType,
                transportMechanism: identity?.transport?.mechanism,
                transportIntegrity: identity?.transport?.integrity,
                attributionMethod: this.buildAttributionMethodRecord(identity),
                // P1C-04: PCI Req-10 transaction fields (pci-dss pack only)
                transactionContext,
                pciEventType,
            };
            this.auditLog.push(event);
            // Grace-period warning: subject-keyed category missing dataSubjectId
            if (isSubjectKeyed && !dataSubjectId) {
                const warningEvent = {
                    type: 'disclosure.missing-subject-id',
                    packId: m.packId,
                    categoryId: m.categoryId,
                    matchConfidence: m.matchConfidence,
                    subjectDigest,
                    action: m.action,
                    decisionId,
                    timestamp,
                    packVersion: m.packVersion,
                    operatorId: identity?.operatorId,
                    missingSubjectId: true,
                };
                this.auditLog.push(warningEvent);
            }
            // GAP-2: disclosure.unattributed -- subject-keyed category missing recipient.
            // When a subject-keyed category fires and no recipientId is supplied,
            // emit a disclosure.unattributed event immediately after the primary event
            // so the audit chain records that output was produced without identifying
            // the downstream recipient. Required for HIPAA §164.528 disclosure accounting.
            if (isSubjectKeyed && !identity?.recipient?.recipientId) {
                const unattributed = {
                    type: 'disclosure.unattributed',
                    packId: m.packId,
                    categoryId: m.categoryId,
                    matchConfidence: m.matchConfidence,
                    subjectDigest,
                    action: m.action,
                    decisionId,
                    timestamp,
                    packVersion: m.packVersion,
                    orgId,
                    operatorId: identity?.operatorId,
                    dataSubjectId,
                    dataSubjectType,
                };
                this.auditLog.push(unattributed);
            }
            // Plaintext transport warning on subject-keyed categories.
            // GAP-3: 'custom' mechanism is always plaintext after the override above.
            if (isSubjectKeyed && identity?.transport) {
                const mechanism = identity.transport.mechanism;
                if (exports.PLAINTEXT_TRANSPORT_MECHANISMS.has(mechanism)) {
                    const plainWarn = {
                        type: 'disclosure.plaintext-transport',
                        packId: m.packId,
                        categoryId: m.categoryId,
                        matchConfidence: m.matchConfidence,
                        subjectDigest,
                        action: 'WARN',
                        decisionId,
                        timestamp,
                        packVersion: m.packVersion,
                        orgId,
                        operatorId: identity?.operatorId,
                        dataSubjectId,
                        dataSubjectType,
                        transportMechanism: mechanism,
                        transportIntegrity: identity.transport.integrity, // 'plaintext' after GAP-3 override
                        callerAssertedIntegrity, // original caller-supplied value (GAP-3)
                    };
                    this.auditLog.push(plainWarn);
                }
            }
        }
        return { action: finalAction, matched, decisionId };
    }
    /**
     * Convenience: classify + applyActions in a single call.
     */
    evaluate(input, context = {}) {
        const result = this.classify(input, context);
        return this.applyActions(result.matched, context, input);
    }
    /**
     * Return audit events emitted since last drain (or all if never drained).
     */
    drainAuditEvents() {
        const events = this.auditLog.slice();
        this.auditLog = [];
        return events;
    }
    /**
     * Peek at audit events without draining.
     */
    peekAuditEvents() {
        return this.auditLog;
    }
    /**
     * Number of registered (packId, categoryId) entries.
     */
    get registrySize() {
        return this.registry.size;
    }
    /**
     * List all registered category entries. Used by behavioral validators.
     */
    listRegisteredEntries() {
        return Array.from(this.registry.values()).map(e => ({
            packId: e.packId,
            categoryId: e.categoryId,
            action: e.action,
        }));
    }
    /**
     * W1 residual (2026-04-24): return the industries array for a pack ID as declared
     * in CompliancePack.industries. Returns [] if the pack was never loaded.
     *
     * Used by incident-notifier.ts to derive human-readable severity labels from
     * pack metadata instead of hardcoded category strings.
     */
    getPackIndustries(packId) {
        return this.packIndustries.get(packId) ?? [];
    }
    /**
     * W1 residual (2026-04-24): list all registered category IDs that have at least
     * the given minimum action severity. Used to detect non-BLOCK regulated-data
     * categories (e.g. PII at WARN level) for incident classification.
     *
     * Ordering: ALLOW < LOG < WARN < REQUIRE_APPROVAL < BLOCK
     */
    listCategoriesByMinAction(minAction) {
        const minSeverity = ACTION_SEVERITY[minAction];
        return Array.from(this.registry.values())
            .filter(e => ACTION_SEVERITY[e.action] >= minSeverity)
            .map(e => ({ packId: e.packId, categoryId: e.categoryId, action: e.action }));
    }
    // ---------------------------------------------------------------------------
    // Private helpers
    // ---------------------------------------------------------------------------
    runClassifier(input, context, entry) {
        const def = entry.classifierDef;
        // Custom classifier takes precedence
        if (def.classifier) {
            return def.classifier(input, context);
        }
        // Regex pattern fallback
        if (def.pattern) {
            def.pattern.lastIndex = 0; // Reset global flags
            return def.pattern.test(input);
        }
        // No classifier defined for this category -- category is declared but
        // not yet instrumented. Default to false (no match) to avoid false
        // positives. Behavioral validators will catch missing classifiers.
        return false;
    }
    normalizeAction(raw) {
        const upper = raw.toUpperCase();
        // Pack-author vocabulary aliases for the human-review gate.
        // Medical/aerospace packs (FDA 820 CAPA, IEC 62304 Class B/C, ISO 26262
        // ASIL B/C, IEC 62443 SL2/SL3, ISO 15189, DO-178C Level B/C, AS-9100,
        // FDA SaMD Pre-Cert, NIST SP 800-82 Purdue) commonly write 'REVIEW' or
        // 'REVIEWED' meaning "route to human approval queue". Canonical
        // GovernanceAction = REQUIRE_APPROVAL.
        if (upper === 'REVIEW' || upper === 'REVIEWED')
            return 'REQUIRE_APPROVAL';
        const action = upper;
        if (ACTION_SEVERITY[action] !== undefined)
            return action;
        return 'LOG';
    }
    /**
     * P1C-04: Derive the PCI Req-10 event type from transaction type + action.
     *
     * BLOCK always maps to DENIED (Req 10.2.1.7 -- rejected access attempts).
     * All other actions map the transaction type to the canonical Req-10 code.
     * Inlined here to avoid a circular runtime dependency on pcidss.ts.
     */
    derivePciEventType(transactionType, action) {
        if (action === 'BLOCK')
            return 'DENIED';
        switch (transactionType) {
            case 'authorization': return 'AUTH';
            case 'capture': return 'CAPTURE';
            case 'refund': return 'REFUND';
            case 'void': return 'VOID';
            case 'query': return 'QUERY';
            default: return 'AUTH';
        }
    }
    /**
     * P3.5: Returns whether five-dimensional identity is in hard-enforce mode.
     * Controlled by ENFORCE_FIVE_DIM_IDENTITY environment variable.
     * Default: false (grace period for existing callers).
     * Phase 2: flip to true after education packs + 2-week migration window.
     */
    getEnforceFiveDimFlag() {
        const env = (typeof process !== 'undefined') ? process.env['ENFORCE_FIVE_DIM_IDENTITY'] : undefined;
        return env === 'true' || env === '1';
    }
    /**
     * GAP-1: Per-tenant pepper for subject_id pseudonymization.
     *
     * Derives a pepper scoped to `orgId` using HKDF-style construction:
     *   HMAC-SHA-256(globalPepper, orgId || "subject-pseudonym-v1")
     *
     * Same orgId always produces the same pepper (intra-tenant pseudonyms
     * are stable across sessions). Different orgIds produce different peppers,
     * preventing cross-tenant subject pseudonym collision even when two
     * organisations share the same raw dataSubjectId.
     *
     * When orgId is absent (anonymous context or tests without tenant scope),
     * falls back to the global pepper so existing callers are unaffected.
     *
     * BREAKING CHANGE NOTE: existing pseudonyms produced by the old
     * single-global-pepper implementation will change shape once a caller
     * supplies an orgId for the first time. No existing production customers
     * to migrate; see commit message for full impact statement.
     */
    perTenantPepper(orgId) {
        const globalPepper = this.getGlobalPepper();
        if (!orgId)
            return globalPepper;
        // HMAC-SHA-256(globalPepper, orgId + "subject-pseudonym-v1")
        return crypto.createHmac('sha256', globalPepper)
            .update(orgId + 'subject-pseudonym-v1')
            .digest('hex');
    }
    /**
     * Returns the global base pepper from SUBJECT_ID_PEPPER env var.
     * Tests default to a stable test value.
     */
    getGlobalPepper() {
        const env = (typeof process !== 'undefined') ? process.env['SUBJECT_ID_PEPPER'] : undefined;
        return env ?? 'connexum-governance-subject-pepper-v1';
    }
    /**
     * P3.5: Build the attributionMethod composite record.
     * Captures how each dimension was resolved for audit reviewers.
     */
    buildAttributionMethodRecord(identity) {
        if (!identity)
            return undefined;
        const record = {};
        if (identity.dataSubject?.attributionMethod) {
            record.dataSubject = identity.dataSubject.attributionMethod;
        }
        if (identity.recipient?.attributionMethod) {
            record.recipient = identity.recipient.attributionMethod;
        }
        // agent / operator attribution are surfaced via existing agentId / operatorId fields
        return Object.keys(record).length > 0 ? record : undefined;
    }
}
exports.PackDrivenClassifier = PackDrivenClassifier;
// ---------------------------------------------------------------------------
// Singleton for runtime use (lazy-initialized from active packs)
// ---------------------------------------------------------------------------
let _globalClassifier = null;
function getGlobalClassifier() {
    if (!_globalClassifier) {
        _globalClassifier = new PackDrivenClassifier();
    }
    return _globalClassifier;
}
function setGlobalClassifier(c) {
    _globalClassifier = c;
}
//# sourceMappingURL=pack-driven-classifier.js.map