import { test } from "node:test"; import assert from "node:assert/strict"; import { isLocalRequestAllowed } from "../../../src/lib/security/localEndpoints.ts"; /** * Tests for the /api/local/* security guard. The guard reads from * `globalThis.__omniRequestHeaders` (set by the Next.js middleware shim) and * from `process.env` (production opt-in and bearer token). Each test cleans * up both before and after running so the order doesn't matter. */ type OmniGlobals = { __omniRequestHeaders?: Headers }; const G = globalThis as OmniGlobals; function reset() { delete G.__omniRequestHeaders; } function setHeaders(headers: Record) { G.__omniRequestHeaders = new Headers(headers); } test("isLocalRequestAllowed: allows when no headers injected and not production", () => { const prevNodeEnv = process.env.NODE_ENV; const prevEnabled = process.env.OMNIROUTE_LOCAL_ENDPOINTS_ENABLED; delete process.env.NODE_ENV; delete process.env.OMNIROUTE_LOCAL_ENDPOINTS_ENABLED; reset(); try { const out = isLocalRequestAllowed(); assert.equal(out.allowed, true, `expected allowed, got: ${JSON.stringify(out)}`); } finally { reset(); if (prevNodeEnv !== undefined) process.env.NODE_ENV = prevNodeEnv; if (prevEnabled !== undefined) process.env.OMNIROUTE_LOCAL_ENDPOINTS_ENABLED = prevEnabled; } }); test("isLocalRequestAllowed: allows loopback host + empty xff (browser dev path)", () => { const prevNodeEnv = process.env.NODE_ENV; const prevToken = process.env.OMNIROUTE_LOCAL_ENDPOINTS_TOKEN; delete process.env.NODE_ENV; delete process.env.OMNIROUTE_LOCAL_ENDPOINTS_TOKEN; setHeaders({ host: "localhost:20128" }); try { const out = isLocalRequestAllowed(); assert.equal(out.allowed, true, `expected allowed, got: ${JSON.stringify(out)}`); } finally { reset(); if (prevNodeEnv !== undefined) process.env.NODE_ENV = prevNodeEnv; if (prevToken !== undefined) process.env.OMNIROUTE_LOCAL_ENDPOINTS_TOKEN = prevToken; } }); test("isLocalRequestAllowed: allows IPv4 loopback host 127.0.0.1", () => { const prevNodeEnv = process.env.NODE_ENV; delete process.env.NODE_ENV; setHeaders({ host: "127.0.0.1:20128" }); try { const out = isLocalRequestAllowed(); assert.equal(out.allowed, true); } finally { reset(); if (prevNodeEnv !== undefined) process.env.NODE_ENV = prevNodeEnv; } }); test("isLocalRequestAllowed: allows IPv6 loopback host [::1]", () => { const prevNodeEnv = process.env.NODE_ENV; delete process.env.NODE_ENV; setHeaders({ host: "[::1]:20128" }); try { const out = isLocalRequestAllowed(); assert.equal(out.allowed, true); } finally { reset(); if (prevNodeEnv !== undefined) process.env.NODE_ENV = prevNodeEnv; } }); test("isLocalRequestAllowed: rejects public host even with loopback x-forwarded-for", () => { const prevNodeEnv = process.env.NODE_ENV; const prevToken = process.env.OMNIROUTE_LOCAL_ENDPOINTS_TOKEN; delete process.env.NODE_ENV; delete process.env.OMNIROUTE_LOCAL_ENDPOINTS_TOKEN; // Public Host with loopback XFF is rejected — Host header is the // authoritative loopback check (defence against Host header injection // from a tunneled dev URL). setHeaders({ host: "example.com", "x-forwarded-for": "127.0.0.1" }); try { const out = isLocalRequestAllowed(); assert.equal(out.allowed, false); assert.equal(out.reason, "non-local origin"); } finally { reset(); if (prevNodeEnv !== undefined) process.env.NODE_ENV = prevNodeEnv; if (prevToken !== undefined) process.env.OMNIROUTE_LOCAL_ENDPOINTS_TOKEN = prevToken; } }); test("isLocalRequestAllowed: rejects non-loopback origin (no bearer token)", () => { const prevNodeEnv = process.env.NODE_ENV; const prevToken = process.env.OMNIROUTE_LOCAL_ENDPOINTS_TOKEN; delete process.env.NODE_ENV; delete process.env.OMNIROUTE_LOCAL_ENDPOINTS_TOKEN; setHeaders({ host: "example.com", "x-forwarded-for": "203.0.113.5" }); try { const out = isLocalRequestAllowed(); assert.equal(out.allowed, false); assert.equal(out.reason, "non-local origin"); } finally { reset(); if (prevNodeEnv !== undefined) process.env.NODE_ENV = prevNodeEnv; if (prevToken !== undefined) process.env.OMNIROUTE_LOCAL_ENDPOINTS_TOKEN = prevToken; } }); test("isLocalRequestAllowed: bearer token takes precedence over host check (desktop app)", () => { const prevNodeEnv = process.env.NODE_ENV; const prevToken = process.env.OMNIROUTE_LOCAL_ENDPOINTS_TOKEN; process.env.OMNIROUTE_LOCAL_ENDPOINTS_TOKEN = "s3cret-token-abc"; delete process.env.NODE_ENV; // Non-loopback origin BUT valid bearer token → allowed (desktop app) setHeaders({ host: "example.com", "x-forwarded-for": "203.0.113.5", authorization: "Bearer s3cret-token-abc", }); try { const out = isLocalRequestAllowed(); assert.equal(out.allowed, true, `expected allowed, got: ${JSON.stringify(out)}`); } finally { reset(); if (prevNodeEnv !== undefined) process.env.NODE_ENV = prevNodeEnv; if (prevToken !== undefined) process.env.OMNIROUTE_LOCAL_ENDPOINTS_TOKEN = prevToken; } }); test("isLocalRequestAllowed: rejects wrong bearer token even with token configured", () => { const prevNodeEnv = process.env.NODE_ENV; const prevToken = process.env.OMNIROUTE_LOCAL_ENDPOINTS_TOKEN; process.env.OMNIROUTE_LOCAL_ENDPOINTS_TOKEN = "s3cret-token-abc"; delete process.env.NODE_ENV; setHeaders({ host: "example.com", "x-forwarded-for": "203.0.113.5", authorization: "Bearer wrong-token", }); try { const out = isLocalRequestAllowed(); assert.equal(out.allowed, false); // Falls through to the host check, which rejects. assert.equal(out.reason, "non-local origin"); } finally { reset(); if (prevNodeEnv !== undefined) process.env.NODE_ENV = prevNodeEnv; if (prevToken !== undefined) process.env.OMNIROUTE_LOCAL_ENDPOINTS_TOKEN = prevToken; } }); test("isLocalRequestAllowed: production without opt-in rejects (no headers path)", () => { const prevNodeEnv = process.env.NODE_ENV; const prevEnabled = process.env.OMNIROUTE_LOCAL_ENDPOINTS_ENABLED; process.env.NODE_ENV = "production"; delete process.env.OMNIROUTE_LOCAL_ENDPOINTS_ENABLED; reset(); try { const out = isLocalRequestAllowed(); assert.equal(out.allowed, false); assert.equal(out.reason, "disabled in production"); } finally { reset(); if (prevNodeEnv !== undefined) process.env.NODE_ENV = prevNodeEnv; if (prevEnabled !== undefined) process.env.OMNIROUTE_LOCAL_ENDPOINTS_ENABLED = prevEnabled; } }); test("isLocalRequestAllowed: production WITH opt-in allows (no headers path)", () => { // The "no headers" path bypasses the host check entirely; it's the catch-all // for non-Next contexts (cron jobs, scripts). In production with opt-in, // the function returns { allowed: true } for that path. This is by design: // the production gate is at the route handler, not the guard. const prevNodeEnv = process.env.NODE_ENV; const prevEnabled = process.env.OMNIROUTE_LOCAL_ENDPOINTS_ENABLED; process.env.NODE_ENV = "production"; process.env.OMNIROUTE_LOCAL_ENDPOINTS_ENABLED = "1"; reset(); try { const out = isLocalRequestAllowed(); assert.equal(out.allowed, true); } finally { reset(); if (prevNodeEnv !== undefined) process.env.NODE_ENV = prevNodeEnv; if (prevEnabled !== undefined) process.env.OMNIROUTE_LOCAL_ENDPOINTS_ENABLED = prevEnabled; } });