/**
 * Local-server discovery for the stdio MCP proxy (Increment 1 of the unified
 * multi-peer editing model — see ~/.claude/plans/indexed-stirring-turtle.md).
 *
 * When `studiograph serve` is running it writes `.studiograph/server.json`
 * describing how to reach it ({ url, pid, workspacePath, serverId }). The stdio
 * proxy (`stdio-proxy.ts`) reads that file and decides, per tool call, whether
 * to forward to the live HTTP server (so writes go through BodyWriteCoordinator
 * and merge into any open editor) or to fall back to an in-process server (safe:
 * no server ⇒ no live editor to clobber).
 *
 * SECURITY (Codex-hardened):
 *  - The Bearer token is only ever sent to a LOOPBACK url. A non-loopback url in
 *    server.json is treated as "no server" — never trusted, never sent a token.
 *  - A 401/403 from the probe means "server present, auth rejected" → FAIL LOUD.
 *    We must NOT fall back to a local in-process write in that case, because a
 *    live editor may be open; a silent local write is the exact data-loss path
 *    this increment removes.
 *  - Identity is the serverId echo + workspacePath match, NOT team_name.
 */
import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'fs';
import { join } from 'path';
/** Path to the discovery file for a workspace. */
export function serverInfoPath(workspacePath) {
    return join(workspacePath, '.studiograph', 'server.json');
}
/**
 * Atomically write the discovery file (tmp + rename, like SessionManager's
 * pending-commits persistence) and make sure `server.json` is gitignored in
 * `.studiograph/.gitignore` (covers workspaces created before this template
 * change). Best-effort on the gitignore — a failure there never blocks serve.
 */
export function writeServerInfo(workspacePath, info) {
    const path = serverInfoPath(workspacePath);
    const tmp = `${path}.tmp`;
    writeFileSync(tmp, JSON.stringify(info, null, 2), 'utf-8');
    renameSync(tmp, path);
    try {
        ensureGitignoreEntry(join(workspacePath, '.studiograph', '.gitignore'), 'server.json');
    }
    catch {
        /* non-fatal — server.json is ephemeral; worst case it shows as untracked */
    }
}
/** Best-effort removal of the discovery file (called after server.close()). */
export function deleteServerInfo(workspacePath) {
    try {
        unlinkSync(serverInfoPath(workspacePath));
    }
    catch {
        /* already gone / never written */
    }
}
/** Read + parse the discovery file. Returns null on missing or malformed JSON. */
export function readServerInfo(workspacePath) {
    const path = serverInfoPath(workspacePath);
    if (!existsSync(path))
        return null;
    try {
        const raw = JSON.parse(readFileSync(path, 'utf-8'));
        if (raw &&
            typeof raw.url === 'string' &&
            typeof raw.pid === 'number' &&
            typeof raw.workspacePath === 'string' &&
            typeof raw.serverId === 'string') {
            return raw;
        }
        return null;
    }
    catch {
        return null;
    }
}
/** Append `entry` to a `.gitignore` file if not already present (one per line). */
export function ensureGitignoreEntry(gitignorePath, entry) {
    if (existsSync(gitignorePath)) {
        const content = readFileSync(gitignorePath, 'utf-8');
        if (content.split('\n').some(line => line.trim() === entry))
            return;
        const sep = content.endsWith('\n') ? '' : '\n';
        writeFileSync(gitignorePath, `${content}${sep}${entry}\n`);
    }
    else {
        writeFileSync(gitignorePath, `${entry}\n`);
    }
}
/** True only for loopback hosts. The Bearer token is never sent elsewhere. */
export function isLoopbackUrl(url) {
    let host;
    try {
        host = new URL(url).hostname;
    }
    catch {
        return false;
    }
    // Node keeps the brackets on IPv6 hostnames ("[::1]") — strip them.
    if (host.startsWith('[') && host.endsWith(']'))
        host = host.slice(1, -1);
    return host === '127.0.0.1' || host === 'localhost' || host === '::1';
}
/** True if a process with `pid` exists (signal 0 probe). */
export function isPidAlive(pid) {
    if (!Number.isInteger(pid) || pid <= 0)
        return false;
    try {
        process.kill(pid, 0);
        return true;
    }
    catch (err) {
        // EPERM = process exists but we can't signal it → still alive.
        return err?.code === 'EPERM';
    }
}
/**
 * Decide whether the stdio proxy should forward to the live local server.
 *
 * Returns `remote` only when every identity check passes; `fail` only when the
 * server is reachable but rejects our token (401/403); `local` for every other
 * case (no file, dead pid, non-loopback, wrong workspace, connection refused,
 * or a different server answering on the port).
 */
export async function probeServer(workspacePath, token, deps = {}) {
    const fetchFn = deps.fetchFn ?? fetch;
    const pidAlive = deps.isPidAliveFn ?? isPidAlive;
    const readInfo = deps.readInfoFn ?? readServerInfo;
    const info = readInfo(workspacePath);
    if (!info)
        return { mode: 'local' };
    if (!pidAlive(info.pid))
        return { mode: 'local' };
    if (!isLoopbackUrl(info.url))
        return { mode: 'local' };
    if (info.workspacePath !== workspacePath)
        return { mode: 'local' };
    let res;
    try {
        res = await fetchFn(`${info.url}/api/server-info`, {
            headers: { Authorization: `Bearer ${token}` },
        });
    }
    catch {
        // ECONNREFUSED / DNS / abort → the pid is alive but isn't this server
        // (pid reuse, or a server mid-shutdown). Treat as no server.
        return { mode: 'local' };
    }
    if (res.status === 401 || res.status === 403) {
        return { mode: 'fail', reason: `server rejected token (HTTP ${res.status})` };
    }
    if (!res.ok)
        return { mode: 'local' };
    let body;
    try {
        body = await res.json();
    }
    catch {
        return { mode: 'local' };
    }
    if (body?.serverId !== info.serverId || body?.workspacePath !== workspacePath) {
        // A different studiograph instance is answering on that port.
        return { mode: 'local' };
    }
    return { mode: 'remote', url: info.url, serverId: info.serverId };
}
//# sourceMappingURL=server-discovery.js.map