"use strict";
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.configFor = configFor;
exports._setClockForTest = _setClockForTest;
exports.clearSessionRateState = clearSessionRateState;
exports._resetForTest = _resetForTest;
exports.tryAdmit = tryAdmit;
/**
 * Per-session, per-tool rate limiting for `tools/call`.
 *
 * Four configurable limits per profile (operations / application):
 *   - perToolPerSecond:   sustained per-tool rate (token bucket refill)
 *   - perToolBurst:       per-tool burst capacity (token bucket size)
 *   - sessionConcurrency: max in-flight tool calls per session
 *   - sessionPerSecond:   sustained per-session rate across all tools
 *
 * Limit hits surface as `result.isError = true` with `kind: 'rate_limited'`
 * (NOT a JSON-RPC error) per the MCP spec's tools-call convention. The LLM
 * sees and adapts; the protocol envelope stays clean.
 *
 * State is in-memory per worker process. Buckets are evicted lazily when
 * a session's record is removed (#619 cleanup) or after they've been idle
 * past the idle eviction threshold. Multi-process coordination isn't
 * attempted in v1 — the limits are per-worker.
 */
const env = __importStar(require("../../utility/environment/environmentManager.js"));
const hdbTerms_ts_1 = require("../../utility/hdbTerms.js");
const DEFAULTS = {
    operations: { perToolPerSecond: 10, perToolBurst: 20, sessionConcurrency: 25, sessionPerSecond: 100 },
    application: { perToolPerSecond: 25, perToolBurst: 50, sessionConcurrency: 50, sessionPerSecond: 200 },
};
const CONFIG_KEYS = {
    operations: {
        perToolPerSecond: hdbTerms_ts_1.CONFIG_PARAMS.MCP_OPERATIONS_RATELIMIT_PERTOOLPERSECOND,
        perToolBurst: hdbTerms_ts_1.CONFIG_PARAMS.MCP_OPERATIONS_RATELIMIT_PERTOOLBURST,
        sessionConcurrency: hdbTerms_ts_1.CONFIG_PARAMS.MCP_OPERATIONS_RATELIMIT_SESSIONCONCURRENCY,
        sessionPerSecond: hdbTerms_ts_1.CONFIG_PARAMS.MCP_OPERATIONS_RATELIMIT_SESSIONPERSECOND,
    },
    application: {
        perToolPerSecond: hdbTerms_ts_1.CONFIG_PARAMS.MCP_APPLICATION_RATELIMIT_PERTOOLPERSECOND,
        perToolBurst: hdbTerms_ts_1.CONFIG_PARAMS.MCP_APPLICATION_RATELIMIT_PERTOOLBURST,
        sessionConcurrency: hdbTerms_ts_1.CONFIG_PARAMS.MCP_APPLICATION_RATELIMIT_SESSIONCONCURRENCY,
        sessionPerSecond: hdbTerms_ts_1.CONFIG_PARAMS.MCP_APPLICATION_RATELIMIT_SESSIONPERSECOND,
    },
};
function configFor(profile) {
    const keys = CONFIG_KEYS[profile];
    const defaults = DEFAULTS[profile];
    const read = (key, fallback) => {
        const v = env.get(key);
        return typeof v === 'number' && v > 0 ? v : fallback;
    };
    return {
        perToolPerSecond: read(keys.perToolPerSecond, defaults.perToolPerSecond),
        perToolBurst: read(keys.perToolBurst, defaults.perToolBurst),
        sessionConcurrency: read(keys.sessionConcurrency, defaults.sessionConcurrency),
        sessionPerSecond: read(keys.sessionPerSecond, defaults.sessionPerSecond),
    };
}
/**
 * Token bucket: starts full at `burst`, refills at `rate` tokens per
 * second up to `burst`, drained by `tryConsume(1)`. Stateless aside from
 * `tokens` + `lastRefill`, both updated on every consume call.
 */
class TokenBucket {
    rate;
    burst;
    tokens;
    lastRefill;
    constructor(rate, burst) {
        this.rate = rate;
        this.burst = burst;
        this.tokens = burst;
        this.lastRefill = now();
    }
    tryConsume() {
        this.refill();
        if (this.tokens >= 1) {
            this.tokens -= 1;
            return true;
        }
        return false;
    }
    /**
     * Refill-and-check, without consuming. Used by `tryAdmit` to peek each
     * bucket so denial doesn't burn a token in *another* bucket.
     */
    hasToken() {
        this.refill();
        return this.tokens >= 1;
    }
    refill() {
        const t = now();
        const elapsedSec = (t - this.lastRefill) / 1000;
        this.lastRefill = t;
        if (elapsedSec <= 0)
            return;
        this.tokens = Math.min(this.burst, this.tokens + elapsedSec * this.rate);
    }
}
/** Lazily monkey-patchable for tests. */
let now = () => Date.now();
function _setClockForTest(fn) {
    now = fn ?? (() => Date.now());
}
const sessions = new Map();
// Belt-and-braces against state leaks: sessions that get TTL-evicted from the
// system.mcp_session table never reach deleteSession() in this process, so
// `clearSessionRateState` is never called for them. Prune any session that
// hasn't admitted a call in this many ms on every getOrCreate. The threshold
// is generously above the default idle timeout (1800s) — well-behaved live
// sessions never get pruned by accident.
const IDLE_PRUNE_MS = 60 * 60 * 1000; // 1 hour
const PRUNE_INTERVAL_MS = 5 * 60 * 1000; // run at most every 5 minutes
let lastPruneAt = 0;
function pruneIdleSessions() {
    const t = now();
    if (t - lastPruneAt < PRUNE_INTERVAL_MS)
        return;
    lastPruneAt = t;
    const cutoff = t - IDLE_PRUNE_MS;
    for (const [id, s] of sessions) {
        if (s.inFlight === 0 && s.lastSeen < cutoff) {
            sessions.delete(id);
        }
    }
}
function getOrCreate(sessionId, profile) {
    pruneIdleSessions();
    let s = sessions.get(sessionId);
    if (!s) {
        const config = configFor(profile);
        s = {
            perTool: new Map(),
            sessionRate: new TokenBucket(config.sessionPerSecond, config.sessionPerSecond),
            inFlight: 0,
            config,
            profile,
            lastSeen: now(),
        };
        sessions.set(sessionId, s);
    }
    else {
        s.lastSeen = now();
    }
    return s;
}
/** Drop a session's rate-limit state (called on session deletion). */
function clearSessionRateState(sessionId) {
    sessions.delete(sessionId);
}
/** Test seam: drop all sessions. */
function _resetForTest() {
    sessions.clear();
}
/**
 * Attempt to admit a tools/call. If allowed, returns a `release()` that
 * decrements in-flight; the caller MUST invoke it (even on tool failure)
 * via `try { ... } finally { release(); }`.
 */
function tryAdmit(sessionId, toolName, profile) {
    const state = getOrCreate(sessionId, profile);
    if (state.inFlight >= state.config.sessionConcurrency) {
        return { allowed: false, reason: 'concurrency' };
    }
    let toolBucket = state.perTool.get(toolName);
    if (!toolBucket) {
        toolBucket = new TokenBucket(state.config.perToolPerSecond, state.config.perToolBurst);
        state.perTool.set(toolName, toolBucket);
    }
    // Peek both buckets first. Consuming session-rate before checking per-tool
    // (or vice versa) silently drains the unrelated bucket on the denied path.
    if (!toolBucket.hasToken()) {
        return { allowed: false, reason: 'per_tool' };
    }
    if (!state.sessionRate.hasToken()) {
        return { allowed: false, reason: 'session_rate' };
    }
    // Both have capacity — actually deduct. The peeks above ran refill(), so
    // this immediate-follow-up tryConsume sees the same fresh state and is
    // guaranteed to succeed (refill() is a no-op for elapsedSec ≤ 0).
    toolBucket.tryConsume();
    state.sessionRate.tryConsume();
    state.inFlight += 1;
    return {
        allowed: true,
        release: () => {
            if (state.inFlight > 0)
                state.inFlight -= 1;
        },
    };
}
//# sourceMappingURL=rateLimit.js.map