"use strict";
/**
 * JWT License Validator -- Client-side license validation for @connexum/ai-governance.
 *
 * Supports two validation modes:
 * 1. **Offline**: Verify JWT locally using embedded Connexum RSA public key.
 *    Works in air-gapped environments. Checks signature, expiry, and claims.
 * 2. **Online**: POST JWT to license server for full validation including
 *    revocation checking.
 *
 * License JWTs use RS256 (RSA PKCS#1 v1.5 with SHA-256).
 *
 * Phase 3 (Key Vault):
 *   - EMBEDDED_PUBLIC_KEYS is populated from src/license/embedded-public-keys.json,
 *     which is written at SDK publish time by scripts/snapshot-vault-public-keys.mjs.
 *   - At validator boot, a background refresh is attempted against the vault public
 *     endpoint (GET /api/v1/key-vault/public/<alias>). On success the in-memory key
 *     map is updated. On failure the embedded JSON is used as fallback (Invariant 4:
 *     offline-verifiable).
 *   - Cache TTL is 24 hours. Stale cache triggers a refresh on next validation call.
 *   - The old cxgov-key-1 placeholder is DELETED. Any token signed with that dead key
 *     is rejected by the unknown-kid path.
 *
 * @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.JwtValidator = exports.TIER_LIMITS = void 0;
exports._resetPublicKeyCacheForTest = _resetPublicKeyCacheForTest;
const crypto = __importStar(require("crypto"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
/* ------------------------------------------------------------------ */
/*  Embedded public keys (Phase 3 — snapshotted at publish time)      */
/* ------------------------------------------------------------------ */
/**
 * Load embedded public keys from the publish-time snapshot JSON.
 * Falls back to an empty map if the file is missing (will force live fetch).
 * The snapshot is written by scripts/snapshot-vault-public-keys.mjs and is
 * version-controlled so offline installs work without contacting the vault
 * (Locked Invariant 4: offline-verifiable passports).
 *
 * cxgov-key-1 (in-git compromised key) is intentionally absent.
 * Only cxgov-key-2 and later aliases are accepted.
 */
function loadEmbeddedPublicKeys() {
    try {
        // Support both ESM and CJS module resolution.
        // __dirname is available in CJS. In ESM it is not defined — we catch ReferenceError.
        let snapshotPath;
        try {
            // CJS path: __dirname is defined
            // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-explicit-any
            snapshotPath = path.join(__dirname, 'embedded-public-keys.json');
        }
        catch {
            // ESM fallback: try to resolve relative to this file using Node path tricks
            // This branch is hit in ESM contexts where __dirname is unavailable.
            // In that case we use a relative path from the module source root.
            snapshotPath = path.join(path.dirname(require?.main?.filename ?? process.cwd()), 'src', 'license', 'embedded-public-keys.json');
        }
        if (!fs.existsSync(snapshotPath)) {
            // File absent: return empty so live-fetch is used at boot
            return {};
        }
        const raw = fs.readFileSync(snapshotPath, 'utf-8');
        const parsed = JSON.parse(raw);
        if (!parsed.keys || typeof parsed.keys !== 'object')
            return {};
        // Sanity-check each key: must be a real PEM, never a placeholder
        const out = {};
        for (const [kid, pem] of Object.entries(parsed.keys)) {
            if (typeof pem !== 'string')
                continue;
            if (pem.toLowerCase().includes('placeholder'))
                continue;
            if (!pem.includes('-----BEGIN PUBLIC KEY-----'))
                continue;
            out[kid] = pem;
        }
        // Fix 5 (Shield S-P3-002): warn when the embedded key set is empty so operators
        // know offline validation requires live vault connectivity. Silent empty-key sets
        // cause opaque "Unknown key ID" errors with no root-cause hint for git installs.
        if (Object.keys(out).length === 0) {
            process.stderr.write('[@connexum/ai-governance] WARNING: embedded-public-keys.json is empty. ' +
                'License JWT validation will require live vault connectivity. ' +
                'If this is a git install, run `npm run snapshot-vault-keys` or install from a published package.\n');
        }
        return out;
    }
    catch {
        // Any error reading/parsing: degrade to empty (live-fetch path)
        return {};
    }
}
/**
 * The embedded public key map, populated from the publish-time snapshot.
 * Mutated by refreshPublicKeysFromVault() on live-fetch success.
 */
const EMBEDDED_PUBLIC_KEYS = loadEmbeddedPublicKeys();
/* ------------------------------------------------------------------ */
/*  Live vault public-key refresh (Phase 3)                           */
/* ------------------------------------------------------------------ */
/**
 * Default vault endpoint used for background public-key refresh.
 * Can be overridden via VAULT_PUBLIC_URL environment variable.
 */
const DEFAULT_VAULT_BASE_URL = 'http://localhost:4201';
/** Cache TTL: 24 hours in milliseconds. */
const PUBLIC_KEY_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
let _lastVaultRefreshAt = 0;
/**
 * Reset the module-level public-key cache timestamp.
 * FOR TESTING ONLY — forces the next _maybeRefreshLiveKeys() call to re-fetch.
 * @internal
 */
function _resetPublicKeyCacheForTest() {
    _lastVaultRefreshAt = 0;
}
/**
 * Refresh public keys from the vault's unauthenticated public endpoint.
 * Returns the map of kid→PEM fetched. On any failure, returns null and
 * the caller falls back to EMBEDDED_PUBLIC_KEYS (Invariant 4: offline-verifiable).
 *
 * @internal
 */
async function refreshPublicKeysFromVault(vaultBaseUrl, alias) {
    const url = `${vaultBaseUrl}/api/v1/key-vault/public/${encodeURIComponent(alias)}`;
    try {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), 5_000);
        let response;
        try {
            response = await fetch(url, { signal: controller.signal });
        }
        finally {
            clearTimeout(timeoutId);
        }
        if (!response.ok)
            return null;
        const data = await response.json();
        const pem = data?.public_key_pem;
        const kid = data?.kid;
        if (!pem || !kid)
            return null;
        if (pem.toLowerCase().includes('placeholder'))
            return null;
        if (!pem.includes('-----BEGIN PUBLIC KEY-----'))
            return null;
        return { [kid]: pem };
    }
    catch {
        // Network error, timeout, parse error — all treated as "vault unreachable"
        return null;
    }
}
/* ------------------------------------------------------------------ */
/*  Free tier (no JWT required)                                        */
/* ------------------------------------------------------------------ */
const FREE_LICENSE = {
    tier: 'free',
    org: 'unlicensed',
    maxAgents: 0,
    maxPacks: 0,
    features: ['scanner'],
    iss: 'my-cc.io',
    aud: '@connexum/ai-governance',
    sub: 'free-tier',
    iat: 0,
    exp: 0,
    jti: 'free',
    kid: 'none',
};
/* ------------------------------------------------------------------ */
/*  Tier definitions                                                   */
/* ------------------------------------------------------------------ */
exports.TIER_LIMITS = {
    free: {
        maxAgents: 0,
        maxPacks: 0,
        features: ['scanner'],
    },
    starter: {
        // Legacy tier -- not in current pricing. Mapped to professional limits.
        maxAgents: 25,
        maxPacks: 1,
        features: ['basic-hooks', 'single-pack', 'dashboard', 'events', 'sso', 'api-access', 'webhooks'],
    },
    professional: {
        maxAgents: 25,
        maxPacks: 1,
        features: ['basic-hooks', 'single-pack', 'dashboard', 'events', 'audit-reports', 'sso', 'api-access', 'webhooks'],
    },
    enterprise: {
        maxAgents: 100,
        maxPacks: 99,
        features: [
            'basic-hooks', 'multi-pack', 'dashboard', 'events', 'audit-reports',
            'sso', 'api-access', 'webhooks', 'white-label', 'sla', 'dedicated-support',
            'siem-integration', 'encryption', 'custom-packs',
        ],
    },
};
/* ------------------------------------------------------------------ */
/*  JwtValidator                                                       */
/* ------------------------------------------------------------------ */
class JwtValidator {
    config;
    lastOnlineValidation = 0;
    /** Live public-key map, seeded from EMBEDDED_PUBLIC_KEYS and refreshed on vault fetch. */
    livePublicKeys;
    constructor(config = {}) {
        const vaultBaseUrl = config.vaultBaseUrl ??
            (process.env['VAULT_PUBLIC_URL'] ?? DEFAULT_VAULT_BASE_URL);
        this.config = {
            serverUrl: config.serverUrl ?? '',
            publicKeys: { ...EMBEDDED_PUBLIC_KEYS, ...(config.publicKeys ?? {}) },
            forceOffline: config.forceOffline ?? false,
            timeoutMs: config.timeoutMs ?? 5000,
            vaultBaseUrl,
            vaultKeyAlias: config.vaultKeyAlias ?? 'cxgov-key-2',
        };
        // Seed live key map from embedded snapshot + any caller-supplied keys
        this.livePublicKeys = { ...this.config.publicKeys };
        // Trigger a background refresh at construction time (best-effort; does not block)
        // Invariant 3: SDK never throws on network failure
        if (vaultBaseUrl && !this.config.forceOffline) {
            void this._maybeRefreshLiveKeys();
        }
    }
    /**
     * Refresh public keys from the vault if the cache is stale (>24h old).
     * Updates the in-memory livePublicKeys map on success.
     * Never throws — any network failure leaves the current map in place.
     * @internal
     */
    async _maybeRefreshLiveKeys() {
        const now = Date.now();
        if (now - _lastVaultRefreshAt < PUBLIC_KEY_CACHE_TTL_MS)
            return; // still fresh
        const fetched = await refreshPublicKeysFromVault(this.config.vaultBaseUrl, this.config.vaultKeyAlias);
        if (fetched && Object.keys(fetched).length > 0) {
            // Fix 4 (Shield S-P3-001): embedded snapshot keys are immutable by live-refresh.
            // Live-refresh can only ADD new kids (key rotation), never replace existing ones.
            // BEFORE (unsafe): Object.assign(this.livePublicKeys, fetched)
            //   → live-refresh overwrites embedded keys on kid collision; an attacker with
            //     VAULT_PUBLIC_URL env injection can serve a forged key for 'cxgov-key-2'.
            // AFTER (safe): only add fetched kid if NOT already present in livePublicKeys.
            //   → publish-time snapshot is the authoritative trust anchor; rotation works
            //     by issuing a new kid (e.g. 'cxgov-key-3') which IS added.
            for (const [kid, pem] of Object.entries(fetched)) {
                if (!(kid in this.livePublicKeys)) {
                    this.livePublicKeys[kid] = pem;
                }
            }
            _lastVaultRefreshAt = now;
        }
        // Failure: leave livePublicKeys unchanged (fall back to embedded snapshot)
    }
    /**
     * Validate a license key or JWT token.
     *
     * - If empty or 'free', returns free tier.
     * - If a JWT, validates signature + claims offline.
     * - If online server is configured and reachable, also checks revocation.
     * - Public-key cache is refreshed from the vault if >24h stale.
     */
    async validate(token) {
        const now = new Date();
        // Free tier
        if (!token || token === 'free') {
            return {
                valid: true,
                claims: { ...FREE_LICENSE, iat: Math.floor(now.getTime() / 1000) },
                offline: true,
                validatedAt: now.toISOString(),
            };
        }
        // Refresh public keys from vault if stale (best-effort, never throws)
        if (this.config.vaultBaseUrl && !this.config.forceOffline) {
            await this._maybeRefreshLiveKeys();
        }
        // Try offline validation first (always available)
        const offlineResult = this.validateOffline(token);
        // If offline fails, no point trying online
        if (!offlineResult.valid) {
            return offlineResult;
        }
        // If forced offline or no server configured, return offline result
        if (this.config.forceOffline || !this.config.serverUrl) {
            return offlineResult;
        }
        // Try online validation for revocation check
        try {
            const onlineResult = await this.validateOnline(token);
            this.lastOnlineValidation = Date.now();
            return onlineResult;
        }
        catch {
            // Online failed, fall back to offline result with grace period check
            const claims = offlineResult.claims;
            const graceDays = claims.offlineGracePeriodDays ?? 7;
            const gracePeriodMs = graceDays * 24 * 60 * 60 * 1000;
            if (this.lastOnlineValidation > 0 && (Date.now() - this.lastOnlineValidation) > gracePeriodMs) {
                return {
                    valid: false,
                    claims,
                    error: `Offline grace period expired (${graceDays} days). License server unreachable.`,
                    offline: true,
                    validatedAt: now.toISOString(),
                    daysUntilExpiry: offlineResult.daysUntilExpiry,
                };
            }
            return { ...offlineResult, offline: true };
        }
    }
    /**
     * Validate a JWT offline using the embedded public key.
     */
    validateOffline(token) {
        const now = new Date();
        try {
            // Decode JWT (header.payload.signature)
            const parts = token.split('.');
            if (parts.length !== 3) {
                return { valid: false, claims: null, error: 'Invalid JWT format', offline: true, validatedAt: now.toISOString() };
            }
            const header = JSON.parse(base64UrlDecode(parts[0]));
            const payload = JSON.parse(base64UrlDecode(parts[1]));
            // Verify algorithm
            if (header.alg !== 'RS256') {
                return { valid: false, claims: null, error: `Unsupported algorithm: ${header.alg}`, offline: true, validatedAt: now.toISOString() };
            }
            // Get public key by kid.
            // NOTE: cxgov-key-1 (in-git compromised key) is intentionally absent from
            // livePublicKeys. Any token claiming kid='cxgov-key-1' will hit the
            // "Unknown key ID" rejection path. This is correct and intentional (VT3-12).
            const kid = header.kid;
            if (!kid) {
                return { valid: false, claims: null, error: 'JWT header missing kid claim', offline: true, validatedAt: now.toISOString() };
            }
            const publicKey = this.livePublicKeys[kid];
            if (!publicKey) {
                return { valid: false, claims: null, error: `Unknown key ID: ${kid}`, offline: true, validatedAt: now.toISOString() };
            }
            // Verify signature
            const signatureInput = `${parts[0]}.${parts[1]}`;
            const signature = base64UrlToBuffer(parts[2]);
            const verifier = crypto.createVerify('RSA-SHA256');
            verifier.update(signatureInput);
            if (!verifier.verify(publicKey, signature)) {
                return { valid: false, claims: null, error: 'Invalid JWT signature', offline: true, validatedAt: now.toISOString() };
            }
            // Validate claims
            const claims = payload;
            if (claims.iss !== 'my-cc.io') {
                return { valid: false, claims, error: `Invalid issuer: ${claims.iss}`, offline: true, validatedAt: now.toISOString() };
            }
            if (claims.aud !== '@connexum/ai-governance') {
                return { valid: false, claims, error: `Invalid audience: ${claims.aud}`, offline: true, validatedAt: now.toISOString() };
            }
            // Check expiry
            const nowUnix = Math.floor(now.getTime() / 1000);
            const daysUntilExpiry = Math.floor((claims.exp - nowUnix) / 86400);
            if (claims.exp && claims.exp < nowUnix) {
                return {
                    valid: false,
                    claims,
                    error: `License expired ${Math.abs(daysUntilExpiry)} days ago`,
                    offline: true,
                    validatedAt: now.toISOString(),
                    daysUntilExpiry,
                };
            }
            // Validate tier
            if (!['free', 'starter', 'professional', 'enterprise'].includes(claims.tier)) {
                return { valid: false, claims, error: `Invalid tier: ${claims.tier}`, offline: true, validatedAt: now.toISOString() };
            }
            return {
                valid: true,
                claims,
                offline: true,
                validatedAt: now.toISOString(),
                daysUntilExpiry,
            };
        }
        catch (err) {
            return {
                valid: false,
                claims: null,
                error: `JWT parse error: ${err.message}`,
                offline: true,
                validatedAt: now.toISOString(),
            };
        }
    }
    /**
     * Validate a JWT against the license server (online).
     * Checks revocation list in addition to signature/claims.
     */
    async validateOnline(token) {
        const url = `${this.config.serverUrl}/api/v1/license/validate`;
        const controller = new AbortController();
        const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs);
        try {
            const response = await fetch(url, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ token }),
                signal: controller.signal,
            });
            if (!response.ok) {
                const body = await response.text();
                return {
                    valid: false,
                    claims: null,
                    error: `License server error: ${response.status} ${body}`,
                    offline: false,
                    validatedAt: new Date().toISOString(),
                };
            }
            const result = await response.json();
            return { ...result, offline: false };
        }
        finally {
            clearTimeout(timeout);
        }
    }
    /**
     * Check if a specific feature is enabled for the given claims.
     */
    static hasFeature(claims, feature) {
        return claims.features.includes(feature);
    }
    /**
     * Check if the license allows a specific number of agents.
     */
    static canAddAgent(claims, currentCount) {
        return currentCount < claims.maxAgents;
    }
    /**
     * Check if the license allows a specific number of packs.
     */
    static canAddPack(claims, currentCount) {
        return currentCount < claims.maxPacks;
    }
    /**
     * Get a free-tier claims object (for when no license is provided).
     */
    static getFreeTierClaims() {
        return { ...FREE_LICENSE, iat: Math.floor(Date.now() / 1000) };
    }
}
exports.JwtValidator = JwtValidator;
/* ------------------------------------------------------------------ */
/*  Base64URL helpers                                                  */
/* ------------------------------------------------------------------ */
function base64UrlDecode(str) {
    const padded = str + '='.repeat((4 - str.length % 4) % 4);
    return Buffer.from(padded, 'base64').toString('utf-8');
}
function base64UrlToBuffer(str) {
    const padded = str + '='.repeat((4 - str.length % 4) % 4);
    return Buffer.from(padded, 'base64');
}
//# sourceMappingURL=jwt-validator.js.map