diff --git a/lib/net.mjs b/lib/net.mjs new file mode 100644 index 0000000..6ec7a85 --- /dev/null +++ b/lib/net.mjs @@ -0,0 +1,9 @@ +// OCP network helpers — shared so server.mjs and tests use one definition. (issue #125) + +// A bind address is "loopback" only if it cannot be reached from another host. +// Any other address (0.0.0.0, ::, a concrete LAN/Tailscale IP, etc.) is +// network-exposed and must trigger the TUI LAN gate. +export function isLoopbackBind(addr) { + return addr === "127.0.0.1" || addr === "::1" || addr === "localhost" || + addr === "::ffff:127.0.0.1" || /^127\./.test(addr); +} diff --git a/server.mjs b/server.mjs index 559f853..934978b 100644 --- a/server.mjs +++ b/server.mjs @@ -36,6 +36,7 @@ import { dirname, join } from "node:path"; import { homedir } from "node:os"; import { validateKey, recordUsage, getUsageByKey, getUsageTimeline, getRecentUsage, createKey, listKeys, revokeKey, closeDb, checkQuota, updateKeyQuota, getKeyQuota, findKey, cacheHash, getCachedResponse, setCachedResponse, clearCache, getCacheStats, hasCacheControl, singleflight, getInflightStats } from "./keys.mjs"; import { DEFAULT_PORT } from "./lib/constants.mjs"; +import { isLoopbackBind } from "./lib/net.mjs"; import { runTuiTurn, reapStaleTuiSessions } from "./lib/tui/session.mjs"; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -299,14 +300,6 @@ const TUI_CWD = process.env.OCP_TUI_CWD || `${process.env.HOME}/.ocp-tui/work` const TUI_HOME = process.env.OCP_TUI_HOME || process.env.HOME; const TUI_ENTRYPOINT = process.env.OCP_TUI_ENTRYPOINT || "cli"; // cli|auto|off — see ADR 0007 -// A bind address is "loopback" only if it cannot be reached from another host. -// Any other address (0.0.0.0, ::, a concrete LAN/Tailscale IP, etc.) is -// network-exposed and must trigger the TUI LAN gate. (issue #115) -function isLoopbackBind(addr) { - return addr === "127.0.0.1" || addr === "::1" || addr === "localhost" || - addr === "::ffff:127.0.0.1" || /^127\./.test(addr); -} - // SECURITY fail-loud: TUI-mode is incompatible with any configuration that allows // non-operator prompts to reach the interactive claude session. Three cases: // 1. AUTH_MODE=multi — guest/anonymous keys can submit prompts. diff --git a/test-features.mjs b/test-features.mjs index f4a465f..7e31500 100644 --- a/test-features.mjs +++ b/test-features.mjs @@ -4,6 +4,7 @@ * Tests database layer functions directly — no server needed. */ import { getDb, createKey, listKeys, validateKey, recordUsage, checkQuota, updateKeyQuota, getKeyQuota, findKey, cacheHash, getCachedResponse, setCachedResponse, clearCache, getCacheStats, closeDb, hasCacheControl, singleflight, getInflightStats } from "./keys.mjs"; +import { isLoopbackBind } from "./lib/net.mjs"; import { createHash } from "node:crypto"; import { strict as assert } from "node:assert"; import { unlinkSync } from "node:fs"; @@ -1798,17 +1799,10 @@ test("KEY_NAME_RE: 65-char string → invalid", () => { assert.ok(!KEY_NAME_RE.test("x".repeat(65))); }); -// ── isLoopbackBind helper (issue #115) ────────────────────────────────────── -// MIRRORS server.mjs isLoopbackBind — copied verbatim to avoid importing server.mjs -// (top-level server.listen() would start a live HTTP server). -// Keep in sync with the definition in server.mjs above the TUI gate block. +// ── isLoopbackBind helper (issue #115, extracted to lib/net.mjs via #125) ────── +// Tests the imported lib/net.mjs helper — the real shared definition used by server.mjs. console.log("\nisLoopbackBind helper (issue #115):"); -function isLoopbackBind(addr) { - return addr === "127.0.0.1" || addr === "::1" || addr === "localhost" || - addr === "::ffff:127.0.0.1" || /^127\./.test(addr); -} - test("isLoopbackBind: '127.0.0.1' → true", () => { assert.equal(isLoopbackBind("127.0.0.1"), true); });