// http-transport.ts — HTTP transport using SDK's StreamableHTTPServerTransport.
import { createServer } from 'node:http';
import { randomUUID } from 'node:crypto';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { PLANU_VERSION } from '../config/version.js';
import { runWithSessionContext } from './session-context.js';
import { checkHttpRateLimit } from './http-rate-limiter.js';
import { loadOAuthConfig, extractBearerToken, validateToken } from './oauth-validator.js';
const MAX_BODY_BYTES = 5 * 1024 * 1024; // 5MB
const SHUTDOWN_GRACE_MS = 5_000;
const sessions = new Map();
function setSecurityHeaders(res) {
    res.setHeader('X-Content-Type-Options', 'nosniff');
}
function setCorsHeaders(res, corsOrigin) {
    res.setHeader('Access-Control-Allow-Origin', corsOrigin);
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Authorization, x-api-key, mcp-session-id, Mcp-Session-Id, Last-Event-ID, x-planu-license-key');
    res.setHeader('Access-Control-Expose-Headers', 'Content-Type, mcp-session-id, Mcp-Session-Id');
}
function handleHealth(res) {
    setSecurityHeaders(res);
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ status: 'ok', version: PLANU_VERSION }));
}
const SERVER_CARD = JSON.stringify({
    serverInfo: {
        name: 'planu',
        version: PLANU_VERSION,
        description: "Spec Driven Development MCP server — gives AI coding agents structured memory of what to build, how to build it, and how to verify it's done. 105 tools, any language, no API key required.",
        homepage: 'https://planu.dev',
        repository: 'https://github.com/planu-dev/planu',
    },
    authentication: { required: false },
    capabilities: { tools: true, resources: true, prompts: false },
    installation: {
        npm: 'npx -y @planu/cli@latest',
        docker: 'docker run -i --rm ghcr.io/planu-dev/planu',
    },
});
function handleServerCard(res) {
    setSecurityHeaders(res);
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(SERVER_CARD);
}
async function readBody(req) {
    return new Promise((resolve, reject) => {
        const chunks = [];
        let size = 0;
        req.on('data', (chunk) => {
            size += chunk.length;
            if (size > MAX_BODY_BYTES) {
                req.destroy();
                reject(new Error('Body too large'));
                return;
            }
            chunks.push(chunk);
        });
        req.on('end', () => {
            resolve(Buffer.concat(chunks));
        });
        req.on('error', reject);
    });
}
async function handlePost(req, res, sessionId, serverFactory) {
    let body;
    try {
        body = await readBody(req);
    }
    catch {
        res.writeHead(413, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: 'Request body too large (max 5MB)' }));
        return;
    }
    let parsedBody;
    try {
        parsedBody = JSON.parse(body.toString('utf8'));
    }
    catch {
        res.writeHead(400, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: 'Invalid JSON in request body' }));
        return;
    }
    // Existing session — wrap in session context for per-session license
    if (sessionId) {
        const entry = sessions.get(sessionId);
        if (entry) {
            await runWithSessionContext({ sessionId, licenseKey: entry.licenseKey }, () => entry.transport.handleRequest(req, res, parsedBody));
            return;
        }
    }
    // Extract per-session license key from HTTP header (hosted multi-tenant mode)
    const licenseKey = req.headers['x-planu-license-key']?.trim() ?? undefined;
    // New session — create transport and connect a new server instance
    const transport = new StreamableHTTPServerTransport({
        sessionIdGenerator: () => randomUUID(),
    });
    transport.onclose = () => {
        const sid = transport.sessionId;
        if (sid) {
            sessions.delete(sid);
        }
    };
    const sessionServer = serverFactory();
    await sessionServer.connect(transport);
    await runWithSessionContext({ sessionId: transport.sessionId ?? sessionId ?? 'unknown', licenseKey }, () => transport.handleRequest(req, res, parsedBody));
    const newSessionId = transport.sessionId;
    if (newSessionId) {
        sessions.set(newSessionId, { transport, server: sessionServer, licenseKey });
    }
}
async function handleMcpRequest(req, res, serverFactory, corsOrigin) {
    setSecurityHeaders(res);
    if (corsOrigin) {
        setCorsHeaders(res, corsOrigin);
    }
    // OAuth 2.1 Bearer token validation (SPEC-161)
    const oauthConfig = loadOAuthConfig();
    if (oauthConfig.enabled) {
        const token = extractBearerToken(req.headers.authorization);
        if (!token) {
            res.writeHead(401, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify({ error: 'Unauthorized', message: 'Bearer token required' }));
            return;
        }
        const authResult = await validateToken(token, oauthConfig);
        if (!authResult.valid) {
            res.writeHead(401, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify({ error: 'Unauthorized', message: authResult.reason }));
            return;
        }
    }
    const sessionId = req.headers['mcp-session-id'];
    // Rate limit per session (or IP for new sessions)
    const rateLimitKey = sessionId ?? req.socket.remoteAddress ?? 'unknown';
    const rateResult = checkHttpRateLimit(rateLimitKey);
    if (!rateResult.allowed) {
        res.writeHead(429, {
            'Content-Type': 'application/json',
            'Retry-After': String(Math.ceil(rateResult.retryAfterMs / 1000)),
        });
        res.end(JSON.stringify({ error: 'Too many requests. Try again later.' }));
        return;
    }
    if (req.method === 'GET') {
        res.setHeader('Cache-Control', 'no-store');
        if (sessionId) {
            const entry = sessions.get(sessionId);
            if (entry) {
                await runWithSessionContext({ sessionId, licenseKey: entry.licenseKey }, () => entry.transport.handleRequest(req, res));
                return;
            }
        }
        res.writeHead(400, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: 'Missing or invalid session ID' }));
        return;
    }
    if (req.method === 'DELETE') {
        if (sessionId) {
            const entry = sessions.get(sessionId);
            if (entry) {
                await entry.transport.close();
                sessions.delete(sessionId);
                res.writeHead(200, { 'Content-Type': 'application/json' });
                res.end(JSON.stringify({ status: 'session closed' }));
                return;
            }
        }
        res.writeHead(404, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: 'Session not found' }));
        return;
    }
    if (req.method === 'POST') {
        await handlePost(req, res, sessionId, serverFactory);
        return;
    }
    res.writeHead(405, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ error: 'Method not allowed' }));
}
export async function createHttpTransport(serverFactory, config) {
    const httpServer = createServer((req, res) => {
        void (async () => {
            const url = req.url ?? '/';
            if (req.method === 'OPTIONS' && config.corsOrigin) {
                setSecurityHeaders(res);
                setCorsHeaders(res, config.corsOrigin);
                res.writeHead(204);
                res.end();
                return;
            }
            if (url === '/health' && req.method === 'GET') {
                handleHealth(res);
                return;
            }
            if (url === '/.well-known/mcp/server-card.json' && req.method === 'GET') {
                handleServerCard(res);
                return;
            }
            if (url === '/mcp') {
                await handleMcpRequest(req, res, serverFactory, config.corsOrigin);
                return;
            }
            setSecurityHeaders(res);
            res.writeHead(404, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify({ error: 'Not found' }));
        })();
    });
    const shutdownGracefully = async () => {
        console.error('[Planu] Shutting down...');
        const closePromises = [];
        for (const [id, entry] of sessions) {
            closePromises.push(entry.transport.close());
            sessions.delete(id);
        }
        await Promise.allSettled(closePromises);
        await new Promise((resolve) => {
            const timer = setTimeout(() => {
                resolve();
            }, SHUTDOWN_GRACE_MS);
            httpServer.close(() => {
                clearTimeout(timer);
                resolve();
            });
        });
        console.error('[Planu] Shutdown complete');
        process.exit(0);
    };
    process.on('SIGINT', () => void shutdownGracefully());
    process.on('SIGTERM', () => void shutdownGracefully());
    return new Promise((resolve) => {
        httpServer.listen(config.port, config.host, () => {
            console.error(`Planu MCP server listening on http://${config.host}:${config.port}`);
            resolve(httpServer);
        });
    });
}
export { sessions };
//# sourceMappingURL=http-transport.js.map