/**
 * SuspendStateCache — local per-agent suspend state cache with best-effort
 * background refresher for SDK-embedded deployments (ASI-10 SDK kill-switch).
 *
 * Design rationale
 * ────────────────
 * The operator kill-switch (POST /api/v1/agents/:id/suspend) writes
 * `operatorSuspended=true` to the governance-server. An SDK-embedded agent
 * running in the customer's process with no proxy must learn about the
 * suspension through a different channel — it cannot rely on the proxy's
 * forceOpen() shortcut because there is no proxy in its traffic path.
 *
 * This cache solves the problem without violating any Locked Invariant:
 *
 * Invariant 2 — advisory, never authoritative:
 *   The refresh network call is NEVER on the PreToolUse hot path. The cache
 *   is queried synchronously; the refresh runs asynchronously in the
 *   background. If the governance server is unreachable the agent continues
 *   operating on last-known state (provision-allow / provision-deny depending
 *   on the last-known value).
 *
 * Invariant 3 — graceful degradation:
 *   On network error / timeout, the cache keeps its last-known entry and does
 *   NOT throw. Other agents are completely unaffected — each agentId has an
 *   independent cache entry.
 *
 * Invariant 10 — push-receive only:
 *   The refresher POLLS an existing GET endpoint (GET /api/v1/agents/:id) on
 *   the governance server — we are pulling status for an agent we ourselves
 *   registered. This is not scanning customer infrastructure; it is checking
 *   our own platform's published registry data for an agent that opted in.
 *
 * Enforcement boundary
 * ────────────────────
 * Enforcement requires the @connexum/ai-governance runtime/hook to be
 * installed (the standard SDK install path). If the customer removes the
 * runtime hook entirely there is nothing to enforce on the client side — this
 * is inherent to any client-side control and is an accepted, documented gap
 * (see SECURITY.md §ASI-10-SDK-boundary).
 *
 * Usage
 * ─────
 *   const cache = new SuspendStateCache({
 *     agentId: 'agent-xyz',
 *     governanceServerUrl: 'https://api.my-cc.io',
 *     authToken: '<ORG_KEY>',
 *     circuitBreaker,          // root SDK CircuitBreaker instance
 *     ttlMs: 30_000,           // optional, default 30 s
 *     refreshTimeoutMs: 5_000, // optional, default 5 s
 *   });
 *   await cache.start();       // boots with an immediate fetch, then polls
 *   // ... agent runs; cache refreshes in background ...
 *   cache.stop();              // clears the poll interval
 *
 * @connexum/ai-governance  ASI-10 SDK kill-switch
 */
export class SuspendStateCache {
    agentId;
    governanceServerUrl;
    authToken;
    cb;
    ttlMs;
    refreshTimeoutMs;
    /** Per-agent cache entry. null = never successfully fetched yet. */
    entry = null;
    /** NodeJS timer reference; null when not running. */
    timer = null;
    constructor(config) {
        this.agentId = config.agentId;
        this.governanceServerUrl = config.governanceServerUrl;
        this.authToken = config.authToken;
        this.cb = config.circuitBreaker;
        this.ttlMs = config.ttlMs ?? 30_000;
        this.refreshTimeoutMs = config.refreshTimeoutMs ?? 5_000;
    }
    /**
     * Start the cache: perform an immediate fetch then schedule periodic
     * refreshes. The initial fetch failure is silently swallowed so boot
     * continues even if the governance server is temporarily unreachable.
     *
     * Returns `this` to allow chaining.
     */
    async start() {
        // Boot-time fetch (best-effort).
        await this._refresh();
        // Schedule periodic refreshes.
        this.timer = setInterval(() => {
            // Fire-and-forget; errors are swallowed inside _refresh().
            void this._refresh();
        }, this.ttlMs);
        // Allow the process to exit even while the interval is active
        // (unref() makes the timer non-blocking for process exit).
        if (this.timer.unref)
            this.timer.unref();
        return this;
    }
    /**
     * Stop the background refresh interval.
     * Safe to call multiple times.
     */
    stop() {
        if (this.timer !== null) {
            clearInterval(this.timer);
            this.timer = null;
        }
    }
    /**
     * Return the last-known suspend entry, or null if never fetched.
     * Does NOT trigger a network call. Purely synchronous.
     */
    getEntry() {
        return this.entry;
    }
    /**
     * Trigger an immediate out-of-band refresh (beyond the normal TTL).
     * Useful after the customer's code observes a suspend signal through
     * another channel and wants to synchronise immediately.
     *
     * Returns the updated entry, or null if the fetch failed.
     * Never throws.
     */
    async refresh() {
        await this._refresh();
        return this.entry;
    }
    // ---------------------------------------------------------------------------
    // Internal
    // ---------------------------------------------------------------------------
    /**
     * Fetch GET /api/v1/agents/:id from the governance server and update the
     * local cache and circuit breaker.
     *
     * Invariant 3 compliance: ALL error paths are swallowed. The method NEVER
     * throws. On any failure the last-known cache entry is preserved unchanged.
     */
    async _refresh() {
        const url = `${this.governanceServerUrl}/api/v1/agents/${encodeURIComponent(this.agentId)}`;
        try {
            const controller = new AbortController();
            const tid = setTimeout(() => controller.abort(), this.refreshTimeoutMs);
            let resp;
            try {
                resp = await fetch(url, {
                    method: 'GET',
                    headers: {
                        Authorization: `Bearer ${this.authToken}`,
                        Accept: 'application/json',
                    },
                    signal: controller.signal,
                });
            }
            finally {
                clearTimeout(tid);
            }
            if (!resp.ok) {
                // Non-2xx: governance server returned an error (e.g. 401, 404, 500).
                // Keep last-known state; do not update circuit breaker.
                return;
            }
            const body = (await resp.json());
            // AMBER-1 (Shield review 2026-05-29): only act on an EXPLICIT boolean.
            // A clean 200 whose body OMITS operatorSuspended (partial/cached response,
            // future API shape) must KEEP last-known state — never silently un-suspend
            // an active kill-switch. Anything that is not a real boolean is a no-op.
            if (typeof body.operatorSuspended !== 'boolean') {
                return;
            }
            const suspended = body.operatorSuspended;
            const fetchedAt = new Date().toISOString();
            // Update local cache entry.
            this.entry = { suspended, fetchedAt };
            // Propagate to circuit breaker (in-memory, no I/O, never throws).
            if (suspended) {
                this.cb.forceOpen(this.agentId, fetchedAt);
            }
            else {
                this.cb.resetForced(this.agentId);
            }
        }
        catch {
            // Network error, timeout, JSON parse error, or any other failure.
            // Invariant 3: keep last-known state, do NOT throw.
            // Other agents are unaffected — each agent has its own cache instance.
        }
    }
}
//# sourceMappingURL=suspend-state-cache.js.map