"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.formatEphemeralToken = formatEphemeralToken;
exports.isEphemeralToken = isEphemeralToken;
exports.parseEphemeralToken = parseEphemeralToken;
exports.exchangeEphemeralToken = exchangeEphemeralToken;
exports.exchangeTokensInArgs = exchangeTokensInArgs;
exports.storeEphemeral = storeEphemeral;
exports.exchangeEphemeral = exchangeEphemeral;
exports.revokeEphemeral = revokeEphemeral;
exports.cleanupExpired = cleanupExpired;
const db_1 = require("../../lib/db");
const crypto_1 = require("../oauth/crypto");
const logger_1 = require("../../lib/logger");
const sql_1 = require("./sql");
// ── Token format ────────────────────────────────────────────────────────────
// Opaque string: eph:v1:<label>:<uuid>
// The LLM passes these through as-is. Exchange happens inside activity bodies.
const EPH_PREFIX = 'eph:v1:';
const EPH_REGEX = /^eph:v1:([a-z_][a-z0-9_]*):([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/;
function formatEphemeralToken(uuid, label) {
    return `${EPH_PREFIX}${label}:${uuid}`;
}
function isEphemeralToken(value) {
    return typeof value === 'string' && value.startsWith(EPH_PREFIX);
}
function parseEphemeralToken(value) {
    const match = value.match(EPH_REGEX);
    if (!match)
        return null;
    return { label: match[1], uuid: match[2] };
}
/**
 * Parse a token string and exchange the UUID for the decrypted value.
 * Returns null if the token format is invalid, expired, or uses exhausted.
 */
async function exchangeEphemeralToken(tokenString) {
    const parsed = parseEphemeralToken(tokenString);
    if (!parsed)
        return null;
    return exchangeEphemeral(parsed.uuid);
}
/**
 * Deep-traverse a JSON value, replacing any string matching the eph:v1: format
 * with the exchanged plaintext. Non-matching values pass through unchanged.
 */
async function exchangeTokensInArgs(obj) {
    if (typeof obj === 'string') {
        if (!isEphemeralToken(obj))
            return obj;
        const plaintext = await exchangeEphemeralToken(obj);
        if (plaintext !== null)
            return plaintext;
        logger_1.loggerRegistry.warn(`[ephemeral] token exchange failed: ${obj.slice(0, 40)}...`);
        return obj;
    }
    if (Array.isArray(obj)) {
        return Promise.all(obj.map(exchangeTokensInArgs));
    }
    if (obj && typeof obj === 'object') {
        const result = {};
        for (const [k, v] of Object.entries(obj)) {
            result[k] = await exchangeTokensInArgs(v);
        }
        return result;
    }
    return obj;
}
/**
 * Store a sensitive value. Returns the raw token UUID.
 * The value is encrypted at rest using AES-256-GCM.
 *
 * - maxUses: 0 (default) = unlimited exchanges until expired/revoked
 * - ttlSeconds: null (default) = no time-based expiry
 * - Both can be combined: e.g., maxUses=10, ttlSeconds=900
 */
async function storeEphemeral(value, opts = {}) {
    const pool = (0, db_1.getPool)();
    const encrypted = (0, crypto_1.encrypt)(value);
    const maxUses = opts.maxUses ?? 0;
    const expiresAt = opts.ttlSeconds
        ? `NOW() + ${opts.ttlSeconds} * INTERVAL '1 second'`
        : 'NULL';
    const { rows } = await pool.query((0, sql_1.INSERT_EPHEMERAL)(expiresAt), [Buffer.from(encrypted, 'base64'), opts.label || null, maxUses]);
    return rows[0].token;
}
/**
 * Exchange a raw UUID token for the decrypted value.
 *
 * - If max_uses > 0: increments use_count, returns null when exhausted
 * - If max_uses = 0: unlimited (never blocked by count)
 * - If expires_at is set: returns null after expiry
 * - Row is deleted when use_count reaches max_uses (if max_uses > 0)
 */
async function exchangeEphemeral(token) {
    const pool = (0, db_1.getPool)();
    // Atomic increment + check in one query.
    // Returns the row if the exchange is valid, nothing if expired/exhausted.
    const { rows } = await pool.query(sql_1.EXCHANGE_EPHEMERAL, [token]);
    if (rows.length === 0)
        return null;
    const { value: buf, use_count, max_uses } = rows[0];
    // Auto-delete when max_uses is reached (if bounded)
    if (max_uses > 0 && use_count >= max_uses) {
        await pool.query(sql_1.DELETE_EPHEMERAL, [token]);
    }
    return (0, crypto_1.decrypt)(buf.toString('base64'));
}
/**
 * Explicitly revoke a token before expiry/exhaustion.
 */
async function revokeEphemeral(token) {
    const pool = (0, db_1.getPool)();
    const { rowCount } = await pool.query(sql_1.DELETE_EPHEMERAL, [token]);
    return (rowCount ?? 0) > 0;
}
/**
 * Clean up expired tokens. Returns the number of rows deleted.
 */
async function cleanupExpired() {
    const pool = (0, db_1.getPool)();
    const { rowCount } = await pool.query(sql_1.CLEANUP_EXPIRED_EPHEMERAL);
    return rowCount ?? 0;
}
