Files
obsidian-foundry-sync/.env.example
Kaysser Kayyali fe925d4aec feat(E7.2): localhost-default bind + refuse-to-start + first-run auth prompt
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 <noreply@anthropic.com>
2026-06-23 01:08:46 +00:00

76 lines
4.2 KiB
Plaintext

# ─────────────────────────────────────────────────────────────────────────────
# foundry-obsidian-sync — environment template.
# Copy to .env and fill in: cp .env.example .env
# (Or just run the /setup skill, which fills most of this in for you.)
# .env is gitignored — never commit real keys or passwords.
# ─────────────────────────────────────────────────────────────────────────────
# === Relay (ThreeHats foundryvtt-rest-api-relay) ===
# Host port the relay publishes (its web UI + REST API + WS endpoint live here).
RELAY_PORT=3010
RELAY_CONTAINER=foundry-rest-api-relay
# URL the relay advertises to itself (email links, etc.). Usually http://localhost:<RELAY_PORT>.
RELAY_FRONTEND_URL=http://localhost:3010
# URL the sync TOOL calls the relay at. For a local compose this is
# http://localhost:<RELAY_PORT>. If Foundry is remote and you front the relay with
# a public/tailnet domain, set this to that URL instead.
RELAY_URL=http://localhost:3010
# x-api-key for /get, /update, /search. Created via the relay's web UI on first run
# (sign up at http://localhost:<RELAY_PORT>, copy the key from the dashboard).
RELAY_API_KEY=
# Optional: pin a specific connected Foundry client (leave blank to auto-resolve
# when exactly one client is connected to the key).
RELAY_CLIENT_ID=
# === Headless Foundry session (the relay drives a headless browser at Foundry) ===
# Your Foundry server URL. The relay logs a headless user into THIS Foundry.
FOUNDRY_URL=https://your-foundry.example.com
# Foundry player credentials the headless session uses to log in.
RELAY_USER=
RELAY_PASSWORD=
# Foundry world id/title to launch in the headless session.
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
# module's relay URL at ws(s)://<that-reachable-host>:<RELAY_PORT>.
# === Vault (the sync tool / dashboard reads & writes this directly on the host) ===
# Absolute path on the HOST to your Obsidian vault root (the folder containing
# .obsidian). You edit these notes in your own Obsidian desktop app, pointed at this
# vault; the host-run dashboard reads/writes the same files.
# QUOTE any path with spaces — sync.sh sources this file in bash, so an unquoted
# "VAULT=/home/me/My Vault" tries to run "Vault" as a command and VAULT stays unset.
VAULT="/home/me/My Vault"
# The refined-notes subdirectory the dashboard's --vault points at. Defaults to
# ${VAULT}/Refined if you leave this blank.
REFINED="${VAULT}/Refined"
# Optional Campaign Codex export dir (omit to let the dashboard auto-build stubs).
CC=
# === Sync tool ===
# Optional: a Foundry journal LevelDB snapshot, for offline dashboard indexing
# (to-foundry / ui). Leave blank if you only ever push live via the relay.
JOURNAL=
# Sandbox output dir for the tool (name-uuid.json, index.json, converted notes).
OUT=./out
# === Foundry host control (OPTIONAL — only for `refresh --full-index`) ===
# Only needed if you run `./sync.sh refresh --full-index` to stop/start a LOCAL
# Foundry Docker container and read its live journal LevelDB. Live `push` and plain
# `refresh` go through the relay and never touch these.
FOUNDRY_CONTAINER=
FOUNDRY_DATA_DIR=