From fe925d4aecd42c295bdfb849a26c6d0cb1768c6b Mon Sep 17 00:00:00 2001 From: Kaysser Kayyali Date: Tue, 23 Jun 2026 01:08:46 +0000 Subject: [PATCH] feat(E7.2): localhost-default bind + refuse-to-start + first-run auth prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Safe-by-default dashboard exposure: localhost-only unless you explicitly opt in to a public bind with a token. - src/cli.ts: host default 0.0.0.0 → 127.0.0.1 (localhost-only; --host 0.0.0.0 to expose, which then requires a token when the auth flag is on). - src/server.ts startServer guards (before listen): SELF-LOCKOUT (flag on + no token → throw — you'd otherwise brick the dashboard with no recovery short of editing .env) + PUBLIC-EXPOSURE gate (flag on + 0.0.0.0 + no token → throw). Both use __authState (mutable seam) so they're testable. __authState.bound set from cfg.host. When the flag is off, the guards are skipped (back-compat escape hatch — the bind still defaults to 127.0.0.1). - src/server.ts authenticate: E7.2 bind-gating — enforce only on a PUBLIC bind (0.0.0.0); 127.0.0.1 is localhost-trusted (requireAuth routes stay open even with the flag on). The refuse-to-start guard ensures a 0.0.0.0 bind has a token. - src/server.ts routes: GET /api/auth/status (open; {authRequired, bound, relayConfigured, foundryConfigured} — booleans only, no secret values), POST /api/auth/login (open; validates token constant-time, sets an HttpOnly SameSite=Strict cookie; 401 invalid credentials on mismatch, no leak; empty token = unset; no token configured → 401), POST /api/auth/logout (clears cookie, Max-Age=0). - src/dashboard.html: first-run login card (token input, shown when authRequired && no stored token), a shared apiFetch wrapper (attaches the stored token as a Bearer header, on 401 → show login), checkAuth gating init; bare fetch('/api/') calls migrated to apiFetch (auth endpoints stay plain fetch). - .env.example: documents DASHBOARD_AUTH_TOKEN + ENABLE_AUTH_MIDDLEWARE. - tests: e7-1-auth/dispatch updated for bind-gating (enforcement tests set bound=0.0.0.0; + a 127.0.0.1 no-enforcement test). e7-2-auth.test.ts (13 tests): auth-status (off/localhost/public/no-secret-leak), login (valid-cookie/invalid-401/empty/unset), logout (clears), refuse-to-start (self-lockout / 0.0.0.0-no-token / token-set-passes / flag-off-no-guard). tsc clean; 194 passing project-wide (18 pre-existing fixture-missing unchanged). Co-Authored-By: Claude --- .env.example | 10 +++ src/cli.ts | 2 +- src/dashboard.html | 81 +++++++++++++++---- src/server.ts | 58 +++++++++++++- tests/e7-1-auth.test.ts | 14 +++- tests/e7-1-dispatch.test.ts | 10 ++- tests/e7-2-auth.test.ts | 152 ++++++++++++++++++++++++++++++++++++ 7 files changed, 301 insertions(+), 26 deletions(-) create mode 100644 tests/e7-2-auth.test.ts diff --git a/.env.example b/.env.example index 87d81c2..ac69492 100644 --- a/.env.example +++ b/.env.example @@ -33,6 +33,16 @@ FOUNDRY_WORLD= # Seconds before an idle headless session is reaped (relay env). HEADLESS_SESSION_TIMEOUT=3600 +# === Dashboard auth (E7) === +# The dashboard binds 127.0.0.1 by default (localhost-only, no auth needed). +# To expose it on your tailnet, pass --host 0.0.0.0 AND set a token below, then +# flip ENABLE_AUTH_MIDDLEWARE=on. With the flag on, a 0.0.0.0 bind WITHOUT a +# token is refused at boot (safe-by-default); the flag on without a token is +# also refused (self-lockout guard — you'd otherwise brick the dashboard). +# (Optional; off-by-default — the dashboard is localhost-only without these.) +DASHBOARD_AUTH_TOKEN= +ENABLE_AUTH_MIDDLEWARE=false + # IMPORTANT — networking: Foundry's rest-api module connects OUT to the relay over # WebSocket. So the relay must be REACHABLE FROM your Foundry host. If Foundry runs # elsewhere, expose RELAY_PORT (port-forward / tailnet / public domain) and point the diff --git a/src/cli.ts b/src/cli.ts index 7dbb39b..3527529 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -193,7 +193,7 @@ export async function cmdUi(opts: CliOptions): Promise { outDir: out, mode: opts.mode, port: opts.port ?? 7788, - host: opts.host ?? "0.0.0.0", + host: opts.host ?? "127.0.0.1", // E7.2: safe-by-default (localhost). --host 0.0.0.0 exposes on the tailnet (needs DASHBOARD_AUTH_TOKEN when ENABLE_AUTH_MIDDLEWARE=on). relayCfg: relayCfg.apiKey ? relayCfg : undefined, foundryCfg: foundryCfg.dataDir ? foundryCfg : undefined, }; diff --git a/src/dashboard.html b/src/dashboard.html index aba0653..8319850 100644 --- a/src/dashboard.html +++ b/src/dashboard.html @@ -76,6 +76,17 @@ +

Foundry ⇄ Obsidian merge

loading…
@@ -126,9 +137,43 @@
Select a row to inspect.