From 1b02f181fa6ff6aa6c664eba841194bbc933acca Mon Sep 17 00:00:00 2001 From: dtzp555-max Date: Mon, 1 Jun 2026 07:03:55 +1000 Subject: [PATCH] refactor: extract isLoopbackBind to lib/net.mjs (#125) (#127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up cleanup from #115's review. isLoopbackBind was defined in server.mjs and copy-pasted into test-features.mjs (with a "keep in sync" comment, because importing server.mjs would run server.listen()). Extracted to a new importable lib/net.mjs; server.mjs and the test now import the one definition, removing the drift surface. Pure refactor — the function body is byte-identical to the prior server.mjs version, the TUI LAN-gate call site is unchanged, and the 8 isLoopbackBind truth-table tests now exercise the real shared function. 181 tests pass. ALIGNMENT.md: touches server.mjs but adds/removes no operation — it relocates an existing (#115) helper into a module. cli.js citation N/A under Rule 2. Independent fresh-context reviewer (opus): APPROVE (Iron Rule 10) — byte-identical body, exactly one definition, wiring + scope correct, no test dropped, scope clean. Closes #125. Co-authored-by: dtzp555 Co-authored-by: Claude Opus 4.8 --- lib/net.mjs | 9 +++++++++ server.mjs | 9 +-------- test-features.mjs | 12 +++--------- 3 files changed, 13 insertions(+), 17 deletions(-) create mode 100644 lib/net.mjs 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); });