SPB Git

Repository Public

README.md 6.1 KB

# @cr/server

Fastify server of Claude Remote: authentication, REST + WebSocket API under /api/v1, static web app, Postgres. It never spawns processes and never touches project files: everything goes to apps/runtime over its unix socket. See docs/CONTRACT.md (binding) for the API table.

# Environment

Variable Default Notes
NODE_ENV development production enforces the four requirements marked *
CR_PORT / CR_HOST 8620 / 127.0.0.1
CR_PUBLIC_ORIGIN * http://localhost:8620 exact origin; https in production. Cookie scope, Origin check, WebAuthn RP ID (= hostname)
DATABASE_URL * postgres://localhost/claude_remote_dev
CR_SECRET_KEY * dev: generated into $CR_DATA_DIR/dev-secret-key 64 hex chars; AES-256-GCM key for TOTP secrets
CR_RUNTIME_TOKEN * – ≥ 32 chars, same value as the runtime
CR_RUNTIME_SOCKET $CR_DATA_DIR/run/runtime.sock
CR_DATA_DIR ~/claude-remote-data holds setup-token (0600) until the administrator exists
CR_WORKSPACE_ROOTS ~/workspace label=/abs or /abs, comma separated; must match the runtime
CR_TRUSTED_PROXIES 127.0.0.1,::1,10.67.0.1 IPs/CIDRs whose X-Forwarded-For is honoured (tunnel gateway)
CR_EXEC_HOSTS empty node aliases allowed as task host (besides local)
CR_TERM_BUFFER_BYTES 4 MiB WebSocket backpressure high-water mark
CR_AGENTD_URL / CR_AGENTD_TOKEN – cluster telemetry; unset ⇒ host-only overview, health UNKNOWN
CR_NODE_ALIAS runtime hostname (short) alias of this host in agentd (isHost, edges)
CR_LOCAL_MODELS – JSON array {id,name,host,engine,endpoint}; unset ⇒ empty list
CR_SKILLS_REGISTRY_URL – e.g. https://skills.spboucher.ai; unset ⇒ {connected:false}
CR_WEB_DIR ../web/out static export; missing ⇒ API only
CR_LOG_LEVEL info pino JSON

# Runtime integration notes

  • Runtime errors with status 400/403/404/409/413/415/422/429/507 are forwarded to the browser with their code, message and details (e.g. PUT /projects/:id/file → 409 conflict with details: {sha256, mtimeMs} of the file on disk; upload → 409 exists).
  • PUT /projects/:id/file answers a full FileContent (the server adds content).
  • POST /projects/:id/upload?path=&overwrite=1 — without overwrite=1 an existing destination is a 409 exists; parent directories are created.
  • GET /projects/:id/git/diff?path=&staged=1 → {diff, truncated}; GET /projects/:id/search accepts path=.
  • GET /mcp lists the configuration only (status unknown, fast). ?live=1 adds live statuses (claude mcp list, up to 45 s cold, cached 60 s by the runtime); &refresh=1 bypasses the cache.
  • POST /skills/op: install = {projectId, path, replace?} (copied into the user's skills); enable|disable|remove = {name, kind, scope, projectId?} (projectId required for project scope).
  • session.exit without an exit code (tmux session vanished) is resolved against the runtime: gone ⇒ RECOVERABLE (STOPPING ⇒ STOPPED), exactly like the reconciler.

# Run

bash
pnpm --filter @cr/shared build && pnpm --filter @cr/server build
DATABASE_URL=… node dist/migrate.js     # applies migrations/*.sql (idempotent, advisory-locked)
node dist/main.js                       # also migrates on start

First run: read the one-time token on the host (cat $CR_DATA_DIR/setup-token; it is never logged), open the site, create the administrator, then enrol TOTP or a passkey. A second factor is mandatory: until one exists the session can only reach the enrolment endpoints.

# Security model in one page

  • Route auth levels (config.auth, default full): public (GET /health only), preauth (auth/status, setup, login, passkey login), restricted (valid session, second factor pending: status, logout, TOTP step, enrolment), full. One global onRequest guard (src/security/guard.ts) enforces them, also for WebSocket upgrades. http.test.ts iterates the live route table and fails if a route is reachable unauthenticated without being listed.
  • Mutations: X-CSRF-Token (from GET /auth/status) + Origin = CR_PUBLIC_ORIGIN. Pre-auth POSTs: Origin + application/json.
  • Sessions: random 32-byte cookie token (__Host-cr_sid; cr_sid only outside production on http://localhost), sha256 stored, idle 12 h, absolute 30 d, 15 min before the second factor, rotated on upgrade.
  • Request rate limits (per minute, config.rateLimit, route option config.rate): credential checks (/auth/login, /auth/setup, /auth/totp, passkey login options/verify) share a strict per-IP bucket (20); anonymous GET /auth/status has its own per-IP bucket (240) and counts in the caller's session bucket when the cookie is a valid session; GET /health has its own per-IP bucket (600); everything else is per session cookie (600). rate-limit.test.ts.
  • Throttling in login_attempts: user 5, IP 10, global 100 failures / 15 min; lock 60 s doubling per extra failure (cap 15 min). A correct password alone never clears the counter.
  • WebAuthn challenges are server-side and single-use (5 min): in auth_sessions.pending with a session, otherwise in preauth_challenges keyed by the hash of the __Host-cr_pre cookie.
  • Lockout responses: HTTP 429, Retry-After, body {error:{code:'locked',details:{retryAfterSec}}, ok:false, next:'done', retryAfterSec}.
  • Not implemented in this milestone: Web Push (setting stored only), installing from the skills registry (501).

# Tests

bash
npx vitest run        # needs local Postgres with createdb/dropdb on PATH

Each test file creates its own database cr_test_<random>, migrates it and drops it afterwards; src/integration/real-runtime.test.ts starts the REAL runtime (child process, own tmux socket, needs tmux and claude on PATH; never sends a prompt, never touches ~/.claude); for the other files the runtime is src/testkit/fake-runtime.ts (Fastify on a temp unix socket: echo PTY attach, /events, in-memory files). src/testkit/soft-authenticator.ts is a software passkey (ES256, real signatures) used to exercise WebAuthn verification.

Files main
Menu

Theme