Fase 1: require session auth on the db-worker-node HTTP/SSE server

Prepares the daemon to be reachable off loopback as part of the
server-client migration (see the approved implementation plan). Today it
trusts any caller that can reach the port; this makes every route except
health checks and login require a bearer session token.

- New frontend.worker.db-worker-node-auth: scrypt-hashed username/password
  credential persisted at <root-dir>/db-worker-auth.json (mode 0600),
  bootstrapped from LOGSEQ_DAEMON_USERNAME/PASSWORD on first launch since
  there's no interactive setup yet. In-memory bearer sessions with
  constant-time comparison, a failed-login lockout, and list/revoke by
  device. The daemon now refuses to start with no credential configured at
  all -- not just when binding beyond loopback -- since without one no
  token could ever be issued.
- db_worker_node.cljs: POST /v1/auth/login and GET/DELETE
  /v1/auth/sessions; every other route requires Authorization: Bearer
  except /healthz, which stays open (readiness polling happens before any
  session exists) but now omits :pid/:root-dir from its payload. Adds a
  Host-header allowlist ahead of auth to guard against DNS rebinding, and
  configurable --host/--port/--allowed-origin/--allowed-host in place of
  the hardcoded 127.0.0.1 bind and single CORS origin.
- EventSource can't send custom headers, so /v1/events accepts the bearer
  token as a query param instead; persist_db/remote.cljs's events-url
  carries it there now (interim until the Fase 4 WebSocket migration).
- Electron always sent :auth-token nil (verified by reading db_worker.cljs
  -- one of the plan's open questions). daemon/spawn-server! now seeds a
  per-root-dir local device credential
  (<root-dir>/db-worker-local-credential.json, reused across restarts) via
  LOGSEQ_DAEMON_USERNAME/PASSWORD, and cli/server.cljs logs in once the
  spawned or reused daemon is ready, threading the resulting token through
  ensure-server! to Electron's runtime config.

Not done: the OCaml CLI (cli/lib/transport.ml) also calls /v1/invoke
directly and will start receiving 401s once this ships -- confirmed with
the user to leave that gap for now rather than touch the OCaml toolchain
in this pass.

Verified with clojure -M:cljs compile app db-worker-node electron (0
warnings) and the full logseq.db-worker.daemon-test,
logseq.cli.server-test, frontend.worker.db-worker-node-test,
electron.db-worker-manager-test suites (113 tests, 480 assertions, 0
failures) plus frontend.state-test and frontend.persist-db.remote-test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 18:59:08 +02:00
parent 52971f8ec9
commit 49d9f7da84
9 changed files with 507 additions and 74 deletions

View File

@@ -23,8 +23,14 @@
(js/encodeURIComponent repo)))
(defn- events-url
[base-url]
(str (normalize-base-url base-url) "/v1/events"))
"EventSource has no header API, so the bearer token travels as a query
param here instead of the `Authorization` header every other request
uses -- the server accepts it for this route only (see
request-authorized? in db_worker_node.cljs). Interim until the Fase 4
WebSocket migration, which carries it via Sec-WebSocket-Protocol instead."
[base-url auth-token]
(cond-> (str (normalize-base-url base-url) "/v1/events")
(seq auth-token) (str "?token=" (js/encodeURIComponent auth-token))))
(defn- base-headers
[auth-token]
@@ -175,7 +181,7 @@
(when @connected?
(reset! subscription
(open-sse-fn
{:url (events-url base-url)
{:url (events-url base-url auth-token)
:headers (base-headers auth-token)
:on-message (fn [chunk]
(when @connected?