"use strict";
/**
 * TenantProviderAgreementRegistry -- Per-tenant, per-provider BAA/DPA/SCC/BCR tracking.
 *
 * Provider-level compliance (src/security/provider-compliance.ts) answers:
 *   "Does Anthropic offer a BAA product?"
 * This module answers:
 *   "Does tenant A have a signed, active BAA with Anthropic?"
 *
 * The two questions are distinct. HIPAA 45 CFR 164.308(b) requires a Business
 * Associate Agreement between a Covered Entity and each Business Associate. An
 * Anthropic BAA held by Org A does not transfer to Org B. Without per-tenant
 * tracking, the platform cannot assert "PHI flows only to providers with a
 * tenant-active BAA" for any individual customer -- every audit, every CCO
 * Q&A, every HIPAA assessment will identify this as a control gap.
 *
 * GDPR Art. 28 parallels this: each controller-processor relationship requires
 * its own Data Processing Agreement. Tenant-scoped DPA tracking is the only
 * compliant posture.
 *
 * Gate enforcement:
 *   1. Look up bound packs for the action.
 *   2. Union all `requiredAgreementKinds` across bound packs.
 *   3. Call requireActive(tenantId, providerId, unionKinds).
 *   4. Any missing kind: throw MissingTenantProviderAgreementError + emit
 *      TENANT_PROVIDER_AGREEMENT_MISSING severity ERROR; deny the action.
 *
 * v0 storage: in-memory map. State is rebuilt on process restart.
 * v1 plan: persist to the @connexum/database Prisma store (TenantProviderAgreement
 * model) so freeze state and agreement records survive pod restarts. The v0
 * in-memory store is correct for single-process deployments; production
 * multi-pod deployments require v1 persistence.
 *
 * Document hash semantics (v0):
 *   The operator supplies a SHA-256 hash of their agreement document at
 *   registration. We store it. On every gate evaluation we compare the stored
 *   hash against itself (immutability promise at rest). An operator who later
 *   registers a *different* hash for an existing (tenantId, providerId, kind,
 *   effectiveDate) combo triggers AGREEMENT_DOCUMENT_TAMPERED (PT-BAA-2).
 *
 *   v1 plan: operator provides a signed-URL (with X-Document-Sha256 header)
 *   pointing to their document store (S3, Azure Blob, KMS-vaulted). The
 *   registry fetches the header on each gate evaluation and compares the
 *   live hash to the stored hash. Acceptable operator-side primitives:
 *   (a) S3 pre-signed URL with ETag/Content-SHA256 header, or
 *   (b) Azure Blob with x-ms-content-md5 + SHA-256 Content-MD5 equivalent, or
 *   (c) KMS attestation document (AWS KMS GenerateDataKeyPair with signed hash),
 *   or (d) Hashicorp Vault transit engine signature. Option (c) KMS attestation
 *   is recommended for HIPAA/GDPR regulated environments as it provides a
 *   hardware-root-of-trust chain. See docs/runtime/tenant-provider-agreements.md.
 *
 * Pack-binding primacy invariant: every operation that writes a record requires
 * a non-empty packBinding[]. Ref: feedback_pack_binding_primacy.md.
 *
 * Audit chain: every gate decision (allow/deny) and every lifecycle event
 * (register/revoke) emits an audit event with SHA-256 chained prev-SHA.
 *
 * Required permission: 'governance.compliance.register' (ADMIN role).
 * Enforcement: OperatorRoles.checkPermission is injected at construction time
 * to avoid circular dependency.
 *
 * @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.TenantProviderAgreementRegistry = exports.AgreementPermissionError = exports.EmptyPackBindingError = exports.AgreementRevocationError = exports.AgreementRegistrationError = exports.MissingTenantProviderAgreementError = void 0;
const crypto = __importStar(require("node:crypto"));
// ---------------------------------------------------------------------------
// Errors
// ---------------------------------------------------------------------------
class MissingTenantProviderAgreementError extends Error {
    tenantId;
    providerId;
    missingKinds;
    packBinding;
    constructor(opts) {
        super(`Tenant '${opts.tenantId}' lacks active agreement(s) [${opts.missingKinds.join(', ')}] ` +
            `with provider '${opts.providerId}'. Action denied. ` +
            `An OPERATOR_ADMIN must register the required agreement(s) before PHI/PII can be routed ` +
            `through this provider for this tenant. Packs requiring these kinds: [${opts.packBinding.join(', ')}].`);
        this.name = 'MissingTenantProviderAgreementError';
        this.tenantId = opts.tenantId;
        this.providerId = opts.providerId;
        this.missingKinds = opts.missingKinds;
        this.packBinding = opts.packBinding;
    }
}
exports.MissingTenantProviderAgreementError = MissingTenantProviderAgreementError;
class AgreementRegistrationError extends Error {
    constructor(message) {
        super(message);
        this.name = 'AgreementRegistrationError';
    }
}
exports.AgreementRegistrationError = AgreementRegistrationError;
class AgreementRevocationError extends Error {
    constructor(message) {
        super(message);
        this.name = 'AgreementRevocationError';
    }
}
exports.AgreementRevocationError = AgreementRevocationError;
class EmptyPackBindingError extends Error {
    operation;
    constructor(operation) {
        super(`Operation '${operation}' requires a non-empty packBinding. ` +
            `Per the Connexum AI Governance pack-binding-primacy invariant, ` +
            `every record must carry at least one compliance pack binding.`);
        this.name = 'EmptyPackBindingError';
        this.operation = operation;
    }
}
exports.EmptyPackBindingError = EmptyPackBindingError;
class AgreementPermissionError extends Error {
    operatorId;
    constructor(operatorId, action, reason) {
        super(`Permission denied for operator '${operatorId}' attempting '${action}': ${reason}. ` +
            `Only ADMIN-role operators may register or revoke tenant provider agreements.`);
        this.name = 'AgreementPermissionError';
        this.operatorId = operatorId;
    }
}
exports.AgreementPermissionError = AgreementPermissionError;
// ---------------------------------------------------------------------------
// TenantProviderAgreementRegistry
// ---------------------------------------------------------------------------
class TenantProviderAgreementRegistry {
    records = new Map();
    onAudit;
    permissionChecker;
    tenantContext;
    // SHA chain state
    lastSha = '';
    constructor(config = {}) {
        this.onAudit = config.onAudit ?? (() => undefined);
        this.permissionChecker = config.permissionChecker ?? {
            checkPermission: () => ({ allowed: true, reason: 'No checker injected (dev mode)' }),
        };
        this.tenantContext = config.tenantContext;
    }
    // ---------------------------------------------------------------------------
    // register()
    // ---------------------------------------------------------------------------
    /**
     * Register a tenant-provider agreement.
     *
     * Operator-only (governance.compliance.register). Pack-binding mandatory.
     * Throws on effectiveDate > expirationDate.
     * Audit-chained as TENANT_PROVIDER_AGREEMENT_REGISTERED severity ERROR.
     *
     * PT-BAA-2: If an agreement with the same (tenantId, providerId, kind,
     * effectiveDate) exists and the caller supplies a different documentSha256,
     * emits AGREEMENT_DOCUMENT_TAMPERED severity ERROR before overwriting.
     */
    register(tenantId, providerId, payload, approverIdentity, packBinding) {
        this.assertTenantContext(tenantId, 'register');
        this.assertNonEmptyPackBinding(packBinding, 'register');
        // Permission check
        const perm = this.permissionChecker.checkPermission(approverIdentity, 'governance.compliance.register');
        if (!perm.allowed) {
            throw new AgreementPermissionError(approverIdentity, 'governance.compliance.register', perm.reason);
        }
        // Date validation
        if (payload.expirationDate !== undefined &&
            payload.effectiveDate > payload.expirationDate) {
            throw new AgreementRegistrationError(`effectiveDate (${payload.effectiveDate.toISOString()}) must not be after ` +
                `expirationDate (${payload.expirationDate.toISOString()}).`);
        }
        const key = this.tenantKey(tenantId);
        if (!this.records.has(key)) {
            this.records.set(key, []);
        }
        const tenantRecords = this.records.get(key);
        // PT-BAA-2: check for document hash tampering on re-registration
        const existing = tenantRecords.find(r => r.providerId === providerId &&
            r.agreementKind === payload.kind &&
            r.effectiveDate.getTime() === payload.effectiveDate.getTime() &&
            r.status !== 'REVOKED');
        if (existing && payload.documentSha256 !== undefined) {
            if (existing.documentSha256 !== undefined &&
                existing.documentSha256 !== payload.documentSha256) {
                this.emitEvent({
                    type: 'AGREEMENT_DOCUMENT_TAMPERED',
                    severity: 'ERROR',
                    tenantId,
                    providerId,
                    packBinding,
                    existingId: existing.id,
                    detail: `Operator submitted a different documentSha256 for an existing ` +
                        `(tenantId=${tenantId}, providerId=${providerId}, kind=${payload.kind}, ` +
                        `effectiveDate=${payload.effectiveDate.toISOString()}) agreement. ` +
                        `Stored: ${existing.documentSha256}, submitted: ${payload.documentSha256}. ` +
                        `This may indicate the operator replaced the agreement document without ` +
                        `a formal amendment process. Flag for compliance review.`,
                });
            }
        }
        const record = {
            id: `agr_${crypto.randomBytes(8).toString('hex')}`,
            tenantId,
            providerId,
            agreementKind: payload.kind,
            agreementId: payload.agreementId,
            effectiveDate: payload.effectiveDate,
            expirationDate: payload.expirationDate,
            documentRef: payload.documentRef,
            documentSha256: payload.documentSha256,
            signedBy: payload.signedBy,
            status: 'ACTIVE',
            packBinding: [...packBinding],
            createdAt: new Date(),
            registeredBy: approverIdentity,
        };
        tenantRecords.push(record);
        this.emitEvent({
            type: 'TENANT_PROVIDER_AGREEMENT_REGISTERED',
            severity: 'ERROR', // Compliance posture change = audit-grade
            tenantId,
            providerId,
            packBinding,
            agreementId: record.id,
            kind: payload.kind,
            effectiveDate: payload.effectiveDate.toISOString(),
            expirationDate: payload.expirationDate?.toISOString(),
            registeredBy: approverIdentity,
        });
        return record;
    }
    // ---------------------------------------------------------------------------
    // revoke()
    // ---------------------------------------------------------------------------
    /**
     * Revoke a tenant-provider agreement by kind.
     *
     * Operator-only (governance.compliance.register). Mandatory reason.
     * Sets status REVOKED on all matching ACTIVE records for this (tenant, provider, kind).
     * Audit-chained as TENANT_PROVIDER_AGREEMENT_REVOKED severity ERROR.
     */
    revoke(tenantId, providerId, agreementKind, approverIdentity, reason, packBinding) {
        this.assertTenantContext(tenantId, 'revoke');
        this.assertNonEmptyPackBinding(packBinding, 'revoke');
        if (!reason || reason.trim().length === 0) {
            throw new AgreementRevocationError(`Revocation requires a non-empty reason string. ` +
                `Document the reason for revoking the ${agreementKind} agreement with ` +
                `provider '${providerId}' for tenant '${tenantId}'.`);
        }
        const perm = this.permissionChecker.checkPermission(approverIdentity, 'governance.compliance.register');
        if (!perm.allowed) {
            throw new AgreementPermissionError(approverIdentity, 'governance.compliance.register', perm.reason);
        }
        const key = this.tenantKey(tenantId);
        const tenantRecords = this.records.get(key) ?? [];
        const now = new Date();
        let revokedCount = 0;
        for (const record of tenantRecords) {
            if (record.providerId === providerId &&
                record.agreementKind === agreementKind &&
                record.status === 'ACTIVE') {
                record.status = 'REVOKED';
                record.revokedAt = now;
                record.revokedReason = reason;
                revokedCount++;
            }
        }
        this.emitEvent({
            type: 'TENANT_PROVIDER_AGREEMENT_REVOKED',
            severity: 'ERROR',
            tenantId,
            providerId,
            packBinding,
            agreementKind,
            reason,
            revokedCount,
            revokedBy: approverIdentity,
            revokedAt: now.toISOString(),
        });
    }
    // ---------------------------------------------------------------------------
    // getActive()
    // ---------------------------------------------------------------------------
    /**
     * Return the first active, non-expired agreement of the given kind for this
     * (tenantId, providerId) pair, or null if none exists.
     */
    getActive(tenantId, providerId, agreementKind) {
        this.assertTenantContext(tenantId, 'getActive');
        const key = this.tenantKey(tenantId);
        const tenantRecords = this.records.get(key) ?? [];
        const now = new Date();
        return (tenantRecords.find(r => r.providerId === providerId &&
            r.agreementKind === agreementKind &&
            r.status === 'ACTIVE' &&
            (r.expirationDate === undefined || r.expirationDate > now)) ?? null);
    }
    // ---------------------------------------------------------------------------
    // requireActive()
    // ---------------------------------------------------------------------------
    /**
     * Enforcement gate. Throws MissingTenantProviderAgreementError if any
     * required kind has no active, non-expired agreement.
     *
     * Called by upstream pack evaluation before routing PHI/PII through a provider.
     * Emits TENANT_PROVIDER_AGREEMENT_GATE_ALLOW or _GATE_DENY (both chained).
     */
    requireActive(tenantId, providerId, requiredKinds, packBinding = []) {
        this.assertTenantContext(tenantId, 'requireActive');
        const missing = [];
        for (const kind of requiredKinds) {
            const active = this.getActive(tenantId, providerId, kind);
            if (!active) {
                missing.push(kind);
            }
        }
        if (missing.length > 0) {
            this.emitEvent({
                type: 'TENANT_PROVIDER_AGREEMENT_MISSING',
                severity: 'ERROR',
                tenantId,
                providerId,
                packBinding,
                missingKinds: missing,
                requiredKinds,
            });
            this.emitEvent({
                type: 'TENANT_PROVIDER_AGREEMENT_GATE_DENY',
                severity: 'ERROR',
                tenantId,
                providerId,
                packBinding,
                missingKinds: missing,
                requiredKinds,
            });
            throw new MissingTenantProviderAgreementError({
                tenantId,
                providerId,
                missingKinds: missing,
                packBinding,
            });
        }
        this.emitEvent({
            type: 'TENANT_PROVIDER_AGREEMENT_GATE_ALLOW',
            severity: 'INFO',
            tenantId,
            providerId,
            packBinding,
            requiredKinds,
        });
    }
    // ---------------------------------------------------------------------------
    // listExpiringWithin()
    // ---------------------------------------------------------------------------
    /**
     * Return all ACTIVE agreements expiring within the given number of days.
     * Used by operator dashboards and automated renewal reminders.
     */
    listExpiringWithin(daysAhead) {
        const cutoff = new Date(Date.now() + daysAhead * 24 * 60 * 60 * 1000);
        const results = [];
        for (const [, tenantRecords] of this.records) {
            for (const record of tenantRecords) {
                if (record.status === 'ACTIVE' &&
                    record.expirationDate !== undefined &&
                    record.expirationDate <= cutoff) {
                    results.push(record);
                }
            }
        }
        return results;
    }
    // ---------------------------------------------------------------------------
    // Private helpers
    // ---------------------------------------------------------------------------
    tenantKey(tenantId) {
        return `tenant::${tenantId}`;
    }
    /**
     * Cross-tenant isolation guard.
     * When the registry is constructed with a tenantContext, only that tenant's
     * records may be accessed. A different tenantId is a cross-tenant traversal
     * attempt (PT-BAA-1) and is blocked.
     */
    assertTenantContext(tenantId, operation) {
        if (this.tenantContext !== undefined && this.tenantContext !== tenantId) {
            throw new Error(`[PT-BAA-1] Cross-tenant access denied. ` +
                `Registry is scoped to tenant '${this.tenantContext}'; ` +
                `attempted to '${operation}' for tenant '${tenantId}'. ` +
                `Cross-tenant agreement lookups are a security boundary violation.`);
        }
    }
    assertNonEmptyPackBinding(packBinding, operation) {
        if (!packBinding || packBinding.length === 0) {
            throw new EmptyPackBindingError(operation);
        }
    }
    emitEvent(payload) {
        const now = new Date().toISOString();
        const eventContent = JSON.stringify({ ...payload, timestamp: now });
        const currentSha = crypto.createHash('sha256').update(this.lastSha + eventContent).digest('hex');
        const event = Object.assign({}, payload, {
            timestamp: now,
            prevSha: this.lastSha || undefined,
        });
        this.lastSha = currentSha;
        this.onAudit(event);
    }
}
exports.TenantProviderAgreementRegistry = TenantProviderAgreementRegistry;
//# sourceMappingURL=tenant-provider-agreements.js.map