/**
 * ApiTokenAuditor — minimum scope and rotation cadence enforcement.
 *
 * Security model (two audit dimensions):
 *   1. Scope audit  — verifies each registered token only holds the scopes
 *                     its declared policy permits (minimum scope principle)
 *   2. Rotation audit — checks token age against the rotation cadence policy
 *                       and emits warnings at the warning threshold, errors
 *                       when rotation is overdue
 *
 * In managed mode, out-of-policy tokens are blocked from use.
 * In advisory mode, violations are reported but not blocked.
 */
import { logger } from '../utils/logger.js';
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/** Default rotation cadence: 90 days in ms. */
export const DEFAULT_ROTATION_CADENCE_MS = 90 * 24 * 60 * 60 * 1000;
/** Default warning threshold: 14 days before rotation deadline. */
export const DEFAULT_ROTATION_WARNING_THRESHOLD_MS = 14 * 24 * 60 * 60 * 1000;
// ---------------------------------------------------------------------------
// ApiTokenAuditor
// ---------------------------------------------------------------------------
/**
 * Audits registered API tokens for scope minimization and rotation cadence.
 *
 * Usage:
 * ```ts
 * const auditor = new ApiTokenAuditor({ managed: true });
 * auditor.registerPolicy({
 *   id: 'openai',
 *   name: 'OpenAI API',
 *   allowedScopes: ['completions:write', 'models:read'],
 *   rotationCadenceMs: 90 * 24 * 60 * 60 * 1000,
 * });
 * auditor.registerToken({
 *   id: 'tok_openai_main',
 *   label: 'OPENAI_API_KEY',
 *   issuedAt: Date.now() - 30 * 24 * 60 * 60 * 1000,
 *   grantedScopes: ['completions:write', 'models:read'],
 *   policyId: 'openai',
 * });
 * const report = auditor.auditAll();
 * ```
 */
export class ApiTokenAuditor {
    _policies = new Map();
    _tokens = new Map();
    _config;
    _emitter;
    constructor(config = { managed: false }, options) {
        this._config = config;
        this._emitter = options?.emitter ?? ((_event) => { });
    }
    // -------------------------------------------------------------------------
    // Registry
    // -------------------------------------------------------------------------
    /**
     * Register a scope policy. Policies must be registered before tokens that
     * reference them.
     */
    registerPolicy(policy) {
        this._policies.set(policy.id, policy);
        logger.debug('ApiTokenAuditor: registered policy', { policyId: policy.id });
    }
    /**
     * Register an API token for auditing.
     * Throws if the referenced policyId is not registered.
     */
    registerToken(metadata) {
        if (!this._policies.has(metadata.policyId)) {
            throw new Error(`ApiTokenAuditor: policyId '${metadata.policyId}' not registered — register the policy before the token`);
        }
        this._tokens.set(metadata.id, metadata);
        logger.debug('ApiTokenAuditor: registered token', { tokenId: metadata.id, label: metadata.label });
    }
    /**
     * Deregister a token (e.g. on rotation — remove the old registration,
     * then registerToken with the new metadata).
     */
    deregisterToken(tokenId) {
        return this._tokens.delete(tokenId);
    }
    // -------------------------------------------------------------------------
    // Individual audits
    // -------------------------------------------------------------------------
    /**
     * Audit the scope of a single token against its policy.
     * Returns a TokenScopeAuditResult or null if the token is not registered.
     */
    auditScope(tokenId) {
        const token = this._tokens.get(tokenId);
        if (!token)
            return null;
        const policy = this._policies.get(token.policyId);
        if (!policy)
            return null;
        return this._auditScopeFor(token, policy);
    }
    /**
     * Audit the rotation cadence of a single token.
     * Returns a TokenRotationAuditResult or null if the token is not registered.
     */
    auditRotation(tokenId, now = Date.now()) {
        const token = this._tokens.get(tokenId);
        if (!token)
            return null;
        const policy = this._policies.get(token.policyId);
        if (!policy)
            return null;
        return this._auditRotationFor(token, policy, now);
    }
    // -------------------------------------------------------------------------
    // Internal audit helpers (avoid map lookups + non-null assertions in auditAll)
    // -------------------------------------------------------------------------
    _auditScopeFor(token, policy) {
        const allowedSet = new Set(policy.allowedScopes);
        const excessScopes = token.grantedScopes.filter((s) => !allowedSet.has(s));
        const outcome = excessScopes.length > 0 ? 'violation' : 'ok';
        if (outcome === 'violation') {
            logger.warn('ApiTokenAuditor: scope violation', {
                tokenId: token.id,
                label: token.label,
                excessScopes,
                policyId: policy.id,
            });
        }
        return { tokenId: token.id, outcome, excessScopes, policyId: policy.id };
    }
    _auditRotationFor(token, policy, now) {
        const cadenceMs = policy.rotationCadenceMs ?? DEFAULT_ROTATION_CADENCE_MS;
        const warningThresholdMs = policy.rotationWarningThresholdMs ?? DEFAULT_ROTATION_WARNING_THRESHOLD_MS;
        const ageMs = now - token.issuedAt;
        const dueAt = token.issuedAt + cadenceMs;
        const msUntilDue = dueAt - now;
        let outcome;
        if (msUntilDue < 0) {
            outcome = 'overdue';
            logger.warn('ApiTokenAuditor: token rotation overdue', {
                tokenId: token.id,
                label: token.label,
                ageMs,
                cadenceMs,
                overdueMsByMs: Math.abs(msUntilDue),
            });
        }
        else if (msUntilDue <= warningThresholdMs) {
            outcome = 'warning';
            logger.info('ApiTokenAuditor: token rotation approaching', {
                tokenId: token.id,
                label: token.label,
                msUntilDue,
                warningThresholdMs,
            });
        }
        else {
            outcome = 'ok';
        }
        return { tokenId: token.id, outcome, ageMs, cadenceMs, msUntilDue, dueAt };
    }
    // -------------------------------------------------------------------------
    // Full audit
    // -------------------------------------------------------------------------
    /**
     * Run scope and rotation audits for all registered tokens.
     *
     * In managed mode, tokens with scope violations or overdue rotation are
     * flagged as blocked. Callers must check `result.blocked` or
     * `report.blocked` before using a token.
     */
    auditAll(now = Date.now()) {
        const results = [];
        const blocked = [];
        const scopeViolations = [];
        const rotationWarnings = [];
        const rotationOverdue = [];
        for (const [tokenId, metadata] of this._tokens) {
            const policy = this._policies.get(metadata.policyId);
            if (!policy)
                continue;
            const scope = this._auditScopeFor(metadata, policy);
            const rotation = this._auditRotationFor(metadata, policy, now);
            const hasViolation = scope.outcome === 'violation';
            const isOverdue = rotation.outcome === 'overdue';
            const isWarning = rotation.outcome === 'warning';
            // In managed mode: block tokens with scope violations or overdue rotation
            const isBlocked = this._config.managed && (hasViolation || isOverdue);
            const result = {
                tokenId,
                label: metadata.label,
                scope,
                rotation,
                blocked: isBlocked,
            };
            results.push(result);
            if (hasViolation) {
                scopeViolations.push(tokenId);
                this._emitter({
                    type: 'TOKEN_SCOPE_VIOLATION',
                    tokenId,
                    label: metadata.label,
                    policyId: scope.policyId,
                    excessScopes: scope.excessScopes,
                });
            }
            if (isOverdue) {
                rotationOverdue.push(tokenId);
                this._emitter({
                    type: 'TOKEN_ROTATION_EXPIRED',
                    tokenId,
                    label: metadata.label,
                    ageMs: rotation.ageMs,
                    cadenceMs: rotation.cadenceMs,
                    dueAt: rotation.dueAt,
                });
            }
            if (isWarning) {
                rotationWarnings.push(tokenId);
                this._emitter({
                    type: 'TOKEN_ROTATION_WARNING',
                    tokenId,
                    label: metadata.label,
                    msUntilDue: rotation.msUntilDue,
                    dueAt: rotation.dueAt,
                    ageMs: rotation.ageMs,
                });
            }
            if (isBlocked) {
                blocked.push(tokenId);
                const reason = hasViolation && isOverdue
                    ? 'scope_violation_and_rotation_overdue'
                    : hasViolation
                        ? 'scope_violation'
                        : 'rotation_overdue';
                this._emitter({ type: 'TOKEN_BLOCKED', tokenId, label: metadata.label, reason });
                logger.error('ApiTokenAuditor: token blocked (out-of-policy, managed mode)', {
                    tokenId,
                    label: metadata.label,
                    scopeViolation: hasViolation,
                    rotationOverdue: isOverdue,
                });
            }
        }
        return {
            results,
            blocked,
            scopeViolations,
            rotationWarnings,
            rotationOverdue,
            capturedAt: now,
        };
    }
    /**
     * Check whether a specific token is currently blocked.
     * Returns false when not in managed mode or when the token is not registered.
     */
    isBlocked(tokenId, now = Date.now()) {
        if (!this._config.managed)
            return false;
        const scope = this.auditScope(tokenId);
        if (!scope)
            return false;
        const rotation = this.auditRotation(tokenId, now);
        if (!rotation)
            return false;
        return scope.outcome === 'violation' || rotation.outcome === 'overdue';
    }
    // -------------------------------------------------------------------------
    // Accessors
    // -------------------------------------------------------------------------
    /** Whether managed mode is active. */
    get isManaged() {
        return this._config.managed;
    }
    /** Number of registered tokens. */
    get tokenCount() {
        return this._tokens.size;
    }
    /** Number of registered policies. */
    get policyCount() {
        return this._policies.size;
    }
    /** Get a registered policy by id, or undefined if not found. */
    getPolicy(id) {
        return this._policies.get(id);
    }
    /** Get registered token metadata by id (without the secret value). */
    getTokenMetadata(id) {
        return this._tokens.get(id);
    }
}
