"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.startAgentServer = startAgentServer;
const crypto_1 = require("crypto");
const express_1 = __importDefault(require("express"));
const health_1 = require("./health");
/**
 * Start the agent HTTP API server on 127.0.0.1.
 *
 * Routes:
 *   GET /v1/secrets       → all secrets (authenticated)
 *   GET /v1/keys          → key names (authenticated)
 *   GET /v1/health        → health check (unauthenticated)
 *   GET /v1/ready         → readiness probe (unauthenticated)
 */
function startAgentServer(options) {
    const { port, token, cache, cacheTtl, refresh, decryptor, encryptedStore } = options;
    const jitMode = !!decryptor && !!encryptedStore;
    const app = (0, express_1.default)();
    // Coalesce concurrent refresh calls. Multiple requests hitting an expired
    // cache at once should trigger a single refresh, not N parallel fetches.
    let inflightRefresh = null;
    const refreshOnce = () => {
        if (!refresh)
            return Promise.resolve();
        if (!inflightRefresh) {
            inflightRefresh = refresh().finally(() => {
                inflightRefresh = null;
            });
        }
        return inflightRefresh;
    };
    // Host header validation — block DNS rebinding attacks.
    // Allowed hosts are static after startup; compute once.
    const allowedHosts = new Set([`127.0.0.1:${port}`, "127.0.0.1"]);
    app.use("/v1", (req, res, next) => {
        const host = req.headers.host ?? "";
        if (!allowedHosts.has(host)) {
            res.status(403).json({ error: "Forbidden: invalid Host header" });
            return;
        }
        next();
    });
    // Prevent intermediary caches from storing decrypted secret values
    app.use("/v1/secrets", (_req, res, next) => {
        res.setHeader("Cache-Control", "no-store");
        next();
    });
    // Unauthenticated endpoints — must be mounted before the auth middleware
    app.get("/v1/health", (0, health_1.healthHandler)(cache, cacheTtl, encryptedStore));
    app.get("/v1/ready", (0, health_1.readyHandler)(cache, cacheTtl, encryptedStore));
    // Bearer token authentication for secrets endpoints
    app.use("/v1/secrets", authMiddleware(token));
    app.use("/v1/keys", authMiddleware(token));
    // TTL guard — in cached mode, try to refresh the cache before failing.
    // The cache only 503s if the refresh itself fails (IAM denied, envelope
    // gone, decrypt error) — elapsed time alone is never grounds for rejection.
    // In JIT mode, freshness is proved by KMS success on each request.
    const ttlGuard = async (_req, res, next) => {
        if (jitMode) {
            if (!encryptedStore.isReady()) {
                res.status(503).json({ error: "Secrets not yet loaded" });
                return;
            }
            next();
            return;
        }
        if (cacheTtl === undefined || !cache.isExpired(cacheTtl)) {
            next();
            return;
        }
        if (!refresh) {
            res.status(503).json({ error: "Secrets expired" });
            return;
        }
        try {
            await refreshOnce();
        }
        catch (err) {
            const message = err instanceof Error ? err.message : String(err);
            res.status(503).json({ error: "Refresh failed", detail: message });
            return;
        }
        if (cache.isExpired(cacheTtl)) {
            res.status(503).json({ error: "Secrets expired" });
            return;
        }
        next();
    };
    app.use("/v1/secrets", ttlGuard);
    app.use("/v1/keys", ttlGuard);
    // GET /v1/secrets — all secrets
    app.get("/v1/secrets", async (_req, res) => {
        if (jitMode) {
            // JIT mode: decrypt on every request — KMS is the live authorization gate
            const artifact = encryptedStore.get();
            if (!artifact) {
                res.status(503).json({ error: "Secrets not yet loaded" });
                return;
            }
            try {
                const { values } = await decryptor.decrypt(artifact);
                res.json(values);
            }
            catch (err) {
                const message = err instanceof Error ? err.message : String(err);
                res.status(503).json({ error: "Decryption failed", detail: message });
            }
        }
        else {
            // Cached mode: serve from in-memory cache
            const all = cache.getAll();
            if (!all) {
                res.status(503).json({ error: "Secrets not yet loaded" });
                return;
            }
            res.json(all);
        }
    });
    // GET /v1/keys — list key names in flat `<namespace>__<key>` form
    app.get("/v1/keys", async (_req, res) => {
        if (jitMode) {
            const artifact = encryptedStore.get();
            if (!artifact) {
                res.status(503).json({ error: "Secrets not yet loaded" });
                return;
            }
            try {
                const { keys } = await decryptor.decrypt(artifact);
                res.json(keys);
            }
            catch (err) {
                const message = err instanceof Error ? err.message : String(err);
                res.status(503).json({ error: "Decryption failed", detail: message });
            }
        }
        else {
            res.json(cache.getKeys());
        }
    });
    const url = `http://127.0.0.1:${port}`;
    return new Promise((resolve, reject) => {
        let server;
        try {
            server = app.listen(port, "127.0.0.1", () => {
                resolve({
                    url,
                    stop: () => new Promise((resolveStop, rejectStop) => {
                        // Close idle connections immediately, then force-close active ones after 3s
                        server.close((err) => (err ? rejectStop(err) : resolveStop()));
                        const drainTimer = setTimeout(() => {
                            server.closeAllConnections();
                        }, 3000);
                        drainTimer.unref();
                    }),
                    address: () => server.address(),
                });
            });
            server.on("error", (err) => reject(err));
        }
        catch (err) {
            reject(err);
        }
    });
}
function authMiddleware(token) {
    const expectedBuf = Buffer.from(token);
    return (req, res, next) => {
        const authHeader = req.headers.authorization ?? "";
        const provided = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
        const providedBuf = Buffer.from(provided);
        if (!provided ||
            providedBuf.length !== expectedBuf.length ||
            !(0, crypto_1.timingSafeEqual)(providedBuf, expectedBuf)) {
            res.status(401).json({ error: "Unauthorized" });
            return;
        }
        next();
    };
}
//# sourceMappingURL=server.js.map