/**
 * EntitlementClient -- S-02
 *
 * Client-side SDK component that exchanges a VendorCode for a short-lived
 * Entitlement Token (24h JWT), caches it, and refreshes it before expiry.
 *
 * The token carries:
 *   - tier
 *   - packEntitlements
 *   - subscriptionActiveUntil
 *   - graceUntil (if in grace mode)
 *   - status ("active" | "grace" | "revoked")
 *   - iat, exp
 *
 * Signed with ENTITLEMENT_SIGNING_KEY (HMAC-SHA256, distinct from all other
 * secrets per boot-gate distinctness check).
 *
 * Usage:
 *   const client = new EntitlementClient({ vendorCode, licenseServerUrl, cache });
 *   const token = await client.getToken();   // memoized, refreshes 1h before expiry
 *   await client.refresh();                  // force round-trip
 *   const status = await client.revalidate(); // returns 'active' | 'grace' | 'revoked'
 *
 * @connexum/ai-governance S-02
 */
import * as crypto from 'node:crypto';
import * as https from 'node:https';
import * as http from 'node:http';
// ---------------------------------------------------------------------------
// In-memory default cache (used when caller does not supply one)
// ---------------------------------------------------------------------------
export class InMemoryEntitlementCache {
    store = new Map();
    get(key) {
        return this.store.get(key);
    }
    set(key, token) {
        this.store.set(key, token);
    }
    delete(key) {
        this.store.delete(key);
    }
}
// ---------------------------------------------------------------------------
// EntitlementClient
// ---------------------------------------------------------------------------
export class EntitlementClient {
    cfg;
    constructor(config) {
        this.cfg = {
            tokenTtlMs: 24 * 60 * 60 * 1000,
            refreshLeadMs: 60 * 60 * 1000,
            machineId: generateMachineId(),
            sdkVersion: '1.0.0-beta.1',
            ...config,
        };
    }
    /**
     * Return a cached token, refreshing it if it will expire within
     * refreshLeadMs. On first call, fetches from the license server.
     */
    async getToken() {
        const cacheKey = this.cfg.vendorCode;
        const cached = this.cfg.cache.get(cacheKey);
        if (cached && !this._isExpiringSoon(cached)) {
            return cached;
        }
        return this.refresh();
    }
    /**
     * Force a round-trip to the license server, bypassing the cache.
     * Updates the cache with the fresh token.
     */
    async refresh() {
        const response = await this._exchange();
        const token = {
            jwt: response.token,
            claims: decodeJwtPayload(response.token),
        };
        this.cfg.cache.set(this.cfg.vendorCode, token);
        return token;
    }
    /**
     * Revalidate the vendor code against the license server.
     * Returns the current status without fully refreshing the cached token.
     */
    async revalidate() {
        try {
            const token = await this.refresh();
            return token.claims.status;
        }
        catch (err) {
            if (err instanceof EntitlementError && err.code === 'subscription_inactive') {
                return 'revoked';
            }
            throw err;
        }
    }
    // ---------------------------------------------------------------------------
    // Internal
    // ---------------------------------------------------------------------------
    _isExpiringSoon(token) {
        const expiryMs = token.claims.exp * 1000;
        const leadMs = this.cfg.refreshLeadMs;
        return Date.now() + leadMs >= expiryMs;
    }
    async _exchange() {
        const body = JSON.stringify({
            vendorCode: this.cfg.vendorCode,
            machineId: this.cfg.machineId,
            sdkVersion: this.cfg.sdkVersion,
        });
        const url = new URL('/api/v1/license/exchange', this.cfg.licenseServerUrl);
        const response = await httpPost(url, body);
        if (response.status === 404) {
            throw new EntitlementError('vendor_code_not_found', 'Vendor code not found');
        }
        if (response.status === 403) {
            const data = safeJsonParse(response.body);
            throw new EntitlementError('subscription_inactive', data?.error ?? 'Subscription inactive');
        }
        if (response.status !== 200) {
            throw new EntitlementError('exchange_failed', `License server returned HTTP ${response.status}`);
        }
        const data = safeJsonParse(response.body);
        if (!data?.token) {
            throw new EntitlementError('exchange_failed', 'License server returned no token');
        }
        return { token: data.token };
    }
}
// ---------------------------------------------------------------------------
// Errors
// ---------------------------------------------------------------------------
export class EntitlementError extends Error {
    code;
    constructor(code, message) {
        super(message);
        this.name = 'EntitlementError';
        this.code = code;
    }
}
// ---------------------------------------------------------------------------
// JWT helpers (minimal, no external deps)
// ---------------------------------------------------------------------------
/**
 * Decode the payload of a JWT without verifying the signature.
 * Signature verification is done server-side on the exchange endpoint.
 * Client-side, we only need the claims for expiry/status checks.
 */
export function decodeJwtPayload(jwt) {
    const parts = jwt.split('.');
    if (parts.length !== 3) {
        throw new EntitlementError('invalid_token', 'Malformed JWT: expected 3 segments');
    }
    try {
        const payload = Buffer.from(parts[1], 'base64url').toString('utf-8');
        return JSON.parse(payload);
    }
    catch {
        throw new EntitlementError('invalid_token', 'Failed to decode JWT payload');
    }
}
/**
 * Sign an Entitlement Token payload with HMAC-SHA256.
 * Returns a compact JWT string.
 */
export function signEntitlementToken(claims, secret, ttlMs = 24 * 60 * 60 * 1000) {
    const now = Math.floor(Date.now() / 1000);
    const fullClaims = {
        ...claims,
        iat: claims.iat ?? now,
        exp: claims.exp ?? now + Math.floor(ttlMs / 1000),
    };
    const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
    const payload = Buffer.from(JSON.stringify(fullClaims)).toString('base64url');
    const sigInput = `${header}.${payload}`;
    const sig = crypto
        .createHmac('sha256', secret)
        .update(sigInput)
        .digest('base64url');
    return `${sigInput}.${sig}`;
}
/**
 * Verify an Entitlement Token signature and return decoded claims.
 * Throws EntitlementError on any verification failure.
 */
export function verifyEntitlementToken(jwt, secret) {
    const parts = jwt.split('.');
    if (parts.length !== 3) {
        throw new EntitlementError('invalid_token', 'Malformed JWT: expected 3 segments');
    }
    const sigInput = `${parts[0]}.${parts[1]}`;
    const expectedSig = crypto
        .createHmac('sha256', secret)
        .update(sigInput)
        .digest('base64url');
    // Timing-safe comparison
    const actualSig = parts[2];
    if (actualSig.length !== expectedSig.length) {
        throw new EntitlementError('invalid_signature', 'Entitlement token signature mismatch');
    }
    const a = Buffer.from(actualSig);
    const b = Buffer.from(expectedSig);
    if (!crypto.timingSafeEqual(a, b)) {
        throw new EntitlementError('invalid_signature', 'Entitlement token signature mismatch');
    }
    const claims = decodeJwtPayload(jwt);
    // Check expiry
    const now = Math.floor(Date.now() / 1000);
    if (claims.exp < now) {
        throw new EntitlementError('token_expired', 'Entitlement token has expired');
    }
    return claims;
}
// ---------------------------------------------------------------------------
// HTTP helper (pure Node.js, no external deps)
// ---------------------------------------------------------------------------
async function httpPost(url, body) {
    return new Promise((resolve, reject) => {
        const lib = url.protocol === 'https:' ? https : http;
        const options = {
            hostname: url.hostname,
            port: url.port || (url.protocol === 'https:' ? 443 : 80),
            path: url.pathname + url.search,
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Content-Length': Buffer.byteLength(body),
            },
        };
        const req = lib.request(options, (res) => {
            const chunks = [];
            res.on('data', (chunk) => chunks.push(chunk));
            res.on('end', () => {
                resolve({
                    status: res.statusCode ?? 0,
                    body: Buffer.concat(chunks).toString('utf-8'),
                });
            });
        });
        req.on('error', reject);
        req.setTimeout(10_000, () => {
            req.destroy();
            reject(new EntitlementError('timeout', 'License server request timed out'));
        });
        req.write(body);
        req.end();
    });
}
function safeJsonParse(str) {
    try {
        return JSON.parse(str);
    }
    catch {
        return null;
    }
}
function generateMachineId() {
    // Deterministic per-process ID: hash of hostname + PID
    const input = `${process.env.HOSTNAME ?? 'unknown'}:${process.pid}`;
    return crypto.createHash('sha256').update(input).digest('hex').slice(0, 16);
}
//# sourceMappingURL=entitlement-client.js.map