Refresh docs for current gateway auth flow

This commit is contained in:
daggerhashimoto 2026-03-13 14:42:59 +01:00
parent 9f79217e0d
commit c8cf655659
9 changed files with 77 additions and 59 deletions

View file

@ -180,30 +180,40 @@ Returns the application name and version from `package.json`.
### `GET /api/connect-defaults`
Provides gateway WebSocket URL and auth token for the frontend's auto-connect feature. **The gateway token is only returned to loopback clients** — remote clients receive `null`.
Provides the official gateway WebSocket URL and trust metadata for the frontend's auto-connect flow. The `token` field is always `null`; when `serverSideAuth` is `true`, Nerve expects the browser to connect with an empty token and injects `GATEWAY_TOKEN` server-side during the WebSocket handshake.
**Rate Limit:** None
**Rate Limit:** General (`60 requests / minute`)
**Response (loopback):**
```json
{
"wsUrl": "ws://127.0.0.1:18789/ws",
"token": "your-gateway-token",
"agentName": "Agent"
}
```
**Response (remote):**
**Response (trusted / auto-connect path):**
```json
{
"wsUrl": "ws://127.0.0.1:18789/ws",
"token": null,
"agentName": "Agent"
"agentName": "Agent",
"authEnabled": false,
"serverSideAuth": true
}
```
**Response (manual token still required):**
```json
{
"wsUrl": "ws://127.0.0.1:18789/ws",
"token": null,
"agentName": "Agent",
"authEnabled": true,
"serverSideAuth": false
}
```
`serverSideAuth` becomes `true` when Nerve can safely inject the configured gateway token for this request, such as:
- loopback / tunneled local access to the official gateway URL
- authenticated sessions on a network-exposed Nerve instance
If the browser is pointed at a custom gateway URL, or the request is not trusted for server-side injection, the connect dialog keeps the token field visible and the user must supply it manually.
---
## Events (SSE)

View file

@ -216,7 +216,7 @@ Cmd+K command palette.
#### `features/connect/`
| File | Purpose |
|------|---------|
| `ConnectDialog.tsx` | Initial gateway connection dialog with auto-connect from `/api/connect-defaults` |
| `ConnectDialog.tsx` | Initial gateway connection dialog. Uses official gateway URL + `serverSideAuth` metadata from `/api/connect-defaults` to decide whether the token field is needed |
#### `features/activity/`
| File | Purpose |
@ -260,7 +260,7 @@ Cmd+K command palette.
| Hook | File | Purpose |
|------|------|---------|
| `useWebSocket` | `hooks/useWebSocket.ts` | Core WebSocket management — connect, RPC, auto-reconnect with exponential backoff |
| `useConnectionManager` | `hooks/useConnectionManager.ts` | Auto-connect logic, credential persistence in `sessionStorage` |
| `useConnectionManager` | `hooks/useConnectionManager.ts` | Auto-connect logic, official gateway resolution, `serverSideAuth` gating, and credential persistence in `localStorage` |
| `useDashboardData` | `hooks/useDashboardData.ts` | Fetches memories and token data via REST + SSE |
| `useServerEvents` | `hooks/useServerEvents.ts` | SSE client for `/api/events` |
| `useInputHistory` | `hooks/useInputHistory.ts` | Up/down arrow input history |
@ -319,7 +319,7 @@ Applied in order in `app.ts`:
| `/api/auth/status` | `routes/auth.ts` | GET | Check whether auth is enabled and current session validity |
| `/api/auth/login` | `routes/auth.ts` | POST | Authenticate with password, set signed session cookie |
| `/api/auth/logout` | `routes/auth.ts` | POST | Clear session cookie |
| `/api/connect-defaults` | `routes/connect-defaults.ts` | GET | Pre-fill gateway URL/token for browser. Token only returned for loopback clients |
| `/api/connect-defaults` | `routes/connect-defaults.ts` | GET | Returns the official gateway WS URL plus `authEnabled` / `serverSideAuth` metadata. `token` is always `null`; trusted flows use server-side token injection |
| `/api/events` | `routes/events.ts` | GET, POST | SSE stream for real-time push (memory.changed, tokens.updated, status.changed, ping). POST for test events |
| `/api/tts` | `routes/tts.ts` | POST | Text-to-speech with provider auto-selection (OpenAI → Replicate → Edge). LRU cache with TTL |
| `/api/tts/config` | `routes/tts.ts` | GET, PUT | TTS voice configuration per provider (read / partial update) |

View file

@ -228,12 +228,12 @@ const fetchLimits = createCachedFetch(
- **Authentication:** Session-cookie auth via `middleware/auth.ts`. When enabled, all `/api/*` routes (except auth/health) require a valid HMAC-SHA256 signed cookie. WebSocket upgrades checked in `ws-proxy.ts`
- **Session tokens:** Stateless signed cookies (`HttpOnly`, `SameSite=Strict`). Password hashing via scrypt. Gateway token accepted as fallback password
- **CORS:** Strict origin allowlist — only localhost variants and explicitly configured origins
- **Token exposure:** Gateway token only returned to loopback clients (`/api/connect-defaults`)
- **Token exposure:** Managed gateway auth uses server-side token injection. `/api/connect-defaults` returns `token: null` and trust metadata instead of the raw gateway token
- **Device identity:** Ed25519 keypair for gateway WS auth (`~/.nerve/device-identity.json`). Required for operator scopes on OpenClaw 2026.2.19+
- **File serving:** MIME-type allowlist + directory traversal prevention + allowed prefix check
- **Body limits:** Configurable per-route (general API vs transcribe uploads)
- **Rate limiting:** Per-IP sliding window with separate limits for expensive operations
- **Credentials:** `sessionStorage` (not `localStorage`) for gateway auth — cleared on tab close
- **Credentials:** Browser connection config persists in `localStorage` as `oc-config`. Official managed gateway flows can keep the token empty; custom manual tokens may persist until cleared
- **Input validation:** Zod schemas on all POST/PUT request bodies
### Graceful Shutdown

View file

@ -85,7 +85,7 @@ The wizard backs up existing `.env` files (e.g. `.env.bak.1708100000000`) before
> **⚠️ Network exposure:** Setting `HOST=0.0.0.0` exposes all endpoints to the network. Enable authentication (`NERVE_AUTH=true`) and set a password via the setup wizard before binding to a non-loopback address. Without auth, anyone with network access can read/write agent memory, modify config files, and control sessions. See [Security](SECURITY.md) for the full threat model.
```env
```bash
PORT=3080
SSL_PORT=3443
HOST=127.0.0.1
@ -98,7 +98,7 @@ HOST=127.0.0.1
| `GATEWAY_TOKEN` | — | **Yes** | Authentication token for the OpenClaw gateway. The setup wizard auto-detects this. See note below |
| `GATEWAY_URL` | `http://127.0.0.1:18789` | No | Gateway HTTP endpoint URL |
```env
```bash
GATEWAY_TOKEN=your-token-here
GATEWAY_URL=http://127.0.0.1:18789
```
@ -123,7 +123,7 @@ This allows the browser UI to connect without having to manually enter or store
|----------|---------|-------------|
| `AGENT_NAME` | `Agent` | Display name shown in the UI header and server info |
```env
```bash
AGENT_NAME=Friday
```
@ -134,7 +134,7 @@ AGENT_NAME=Friday
| `OPENAI_API_KEY` | Enables OpenAI TTS (multiple voices) and Whisper audio transcription |
| `REPLICATE_API_TOKEN` | Enables Replicate-hosted TTS models (e.g. Qwen TTS). Requires `ffmpeg` for WAV→MP3 |
```env
```bash
OPENAI_API_KEY=sk-...
REPLICATE_API_TOKEN=r8_...
```
@ -154,7 +154,7 @@ TTS provider fallback chain (when no explicit provider is requested):
| `NERVE_LANGUAGE` | `en` | Preferred voice language (ISO 639-1). Legacy `LANGUAGE` is still accepted but deprecated |
| `EDGE_VOICE_GENDER` | `female` | Edge TTS voice gender: `female` or `male` |
```env
```bash
# Use local speech-to-text (no API key needed)
STT_PROVIDER=local
WHISPER_MODEL=tiny
@ -178,7 +178,7 @@ Voice phrase overrides (stop/cancel/wake words) are stored at `~/.nerve/voice-ph
| `WS_ALLOWED_HOSTS` | `localhost,127.0.0.1,::1` | Additional WebSocket proxy allowed hostnames, comma-separated |
| `TRUSTED_PROXIES` | `127.0.0.1,::1,::ffff:127.0.0.1` | IP addresses trusted to set `X-Forwarded-For` / `X-Real-IP` headers, comma-separated |
```env
```bash
# Tailscale example
ALLOWED_ORIGINS=http://100.64.0.5:3080
CSP_CONNECT_EXTRA=http://100.64.0.5:3080 ws://100.64.0.5:3080
@ -199,7 +199,7 @@ Nerve includes a built-in authentication layer that protects all API endpoints,
| `NERVE_SESSION_SECRET` | *(auto-generated)* | 32-byte hex string for HMAC-SHA256 cookie signing. Auto-generated during setup. If not set, an ephemeral secret is generated at startup (sessions won't survive restarts) |
| `NERVE_SESSION_TTL` | `2592000000` (30 days) | Session lifetime in milliseconds |
```env
```bash
NERVE_AUTH=true
NERVE_PASSWORD_HASH=<generated-by-setup>
NERVE_SESSION_SECRET=<generated-by-setup>
@ -209,13 +209,13 @@ NERVE_SESSION_SECRET=<generated-by-setup>
When `HOST=0.0.0.0` and `NERVE_AUTH=false`, the server **refuses to start** to prevent accidentally exposing all endpoints without authentication. Set `NERVE_ALLOW_INSECURE=true` to override this safety check. **Not recommended for production.**
```env
```bash
NERVE_ALLOW_INSECURE=true
```
**Quick enable (with gateway token as password):**
```env
```bash
NERVE_AUTH=true
NERVE_SESSION_SECRET=$(openssl rand -hex 32)
# No NERVE_PASSWORD_HASH needed — your GATEWAY_TOKEN works as the password
@ -239,7 +239,7 @@ Override these for proxies, self-hosted endpoints, or API-compatible alternative
| `OPENAI_BASE_URL` | `https://api.openai.com/v1` | OpenAI-compatible API base URL |
| `REPLICATE_BASE_URL` | `https://api.replicate.com/v1` | Replicate API base URL |
```env
```bash
OPENAI_BASE_URL=https://api.openai.com/v1
REPLICATE_BASE_URL=https://api.replicate.com/v1
```
@ -263,7 +263,7 @@ REPLICATE_BASE_URL=https://api.replicate.com/v1
| `NERVE_WATCH_WORKSPACE_RECURSIVE` | `false` | Enables recursive `fs.watch` for the entire workspace (legacy behavior). Disabled by default to prevent Linux inotify `ENOSPC` watcher exhaustion. |
| `WORKSPACE_ROOT` | *(auto-detected)* | Allowed base directory for git workdir registration. Auto-derived from `git worktree list` or parent of `process.cwd()` |
```env
```bash
FILE_BROWSER_ROOT=/home/user
MEMORY_PATH=/custom/path/MEMORY.md
MEMORY_DIR=/custom/path/memory/
@ -279,7 +279,7 @@ NERVE_WATCH_WORKSPACE_RECURSIVE=false
| `TTS_CACHE_TTL_MS` | `3600000` (1 hour) | Time-to-live for cached TTS audio in milliseconds |
| `TTS_CACHE_MAX` | `200` | Maximum number of cached TTS entries (in-memory LRU) |
```env
```bash
TTS_CACHE_TTL_MS=7200000
TTS_CACHE_MAX=500
```
@ -382,7 +382,7 @@ Or use the setup wizard's Custom access mode, which generates them automatically
## Minimal `.env` Example
```env
```bash
GATEWAY_TOKEN=abc123def456
```
@ -390,7 +390,7 @@ Everything else uses defaults. This is sufficient for local-only usage.
## Full `.env` Example
```env
```bash
# Gateway (required)
GATEWAY_TOKEN=abc123def456
GATEWAY_URL=http://127.0.0.1:18789

View file

@ -76,7 +76,7 @@ openclaw devices approve <requestId>
### Browser keeps old credentials
**Fix:** Open a new tab or private window. Nerve stores the gateway token in `sessionStorage`, which clears when the tab closes.
**Fix:** Clear site data or remove `localStorage.oc-config`. Nerve stores the gateway URL and any manually-entered token there for reconnects, so a stale manual token can override the official managed connection path.
## Security notes

View file

@ -46,7 +46,7 @@ When prompted:
If your gateway hostname isn't localhost, add it to `.env`:
```env
```bash
WS_ALLOWED_HOSTS=<gateway-hostname-or-ip>
```

View file

@ -76,7 +76,7 @@ Follow the same-host steps for Nerve, then add:
In `.env`:
```env
```bash
GATEWAY_URL=<remote-gateway-url>
WS_ALLOWED_HOSTS=<remote-gateway-hostname-or-ip>
```
@ -115,9 +115,11 @@ In the browser: login screen appears, connect succeeds, sessions load, messages
## Common issues
### Remote clients don't get auto token prefill
### Remote clients may still need manual credentials
`/api/connect-defaults` only returns the token to loopback clients. Remote users must enter the gateway token manually in the connect dialog.
Remote clients can still auto-connect when Nerve trusts the request and the browser is using the official gateway URL. In that case `/api/connect-defaults` reports `serverSideAuth=true`, the browser sends an empty token, and Nerve injects `GATEWAY_TOKEN` server-side during the WebSocket handshake.
Manual token entry is only required for custom gateway URLs or untrusted access paths.
### Reverse proxy and trusted proxy settings

View file

@ -62,7 +62,7 @@ Security is enforced through network-level controls:
1. **Localhost binding** — The server binds to `127.0.0.1` by default. Only local processes can connect.
2. **CORS allowlist** — Browsers enforce the Origin check. Only configured origins receive CORS headers.
3. **Gateway token isolation** — The sensitive `GATEWAY_TOKEN` is never sent to the browser. Instead, Nerve injects it server-side into the WebSocket connection upgrade for trusted clients.
4. **Session storage** — The frontend stores the gateway token in `sessionStorage` (cleared when the tab closes), not `localStorage`.
4. **Client config persistence** — The frontend stores the gateway URL and optional manual token in `localStorage` as `oc-config`. Trusted official-gateway flows usually keep the token empty because server-side injection handles auth.
### When Auth is Enabled
@ -118,7 +118,7 @@ CORS is enforced on all requests via Hono's CORS middleware.
**Additional origins** via `ALLOWED_ORIGINS` env var (comma-separated). Each entry is normalised through the `URL` constructor:
```env
```bash
ALLOWED_ORIGINS=http://100.64.0.5:3080,https://my-server.tailnet.ts.net:3443
```
@ -287,7 +287,7 @@ The WebSocket proxy (connecting the frontend to the OpenClaw gateway) restricts
**Extend via** `WS_ALLOWED_HOSTS` env var (comma-separated):
```env
```bash
WS_ALLOWED_HOSTS=my-server.tailnet.ts.net,100.64.0.5
```
@ -298,13 +298,15 @@ This prevents the proxy from being used to connect to arbitrary external hosts.
Nerve performs **server-side token injection** to provide a zero-config connection experience for local and authenticated users without exposing the `GATEWAY_TOKEN` to the browser storage.
**Injection Logic:**
1. The WebSocket proxy identifies if a connection is **trusted**.
- **Local Trusted**: The client IP (resolved via `TRUSTED_PROXIES` if applicable) is a loopback address (`127.0.0.1` / `::1`).
- **Session Trusted**: The request carries a valid session cookie (`NERVE_AUTH=true`).
2. If trusted and a `GATEWAY_TOKEN` is configured, Nerve intercepts the client's `connect` request.
3. If the client did not provide a token, Nerve injects the server's `GATEWAY_TOKEN`.
1. `GET /api/connect-defaults` returns the official gateway WebSocket URL, `token: null`, and a `serverSideAuth` flag.
2. The WebSocket proxy only injects `GATEWAY_TOKEN` when all of these are true:
- a gateway token is configured on the server
- the request is trusted (`loopback` access or an authenticated session)
- the WebSocket upgrade `Origin` is allowed
3. For the official gateway URL, the browser connects with an empty token when `serverSideAuth=true`.
4. Custom gateway URLs or untrusted contexts still require manual token entry in the connect dialog.
This allows the UI to hide the "Auth Token" field and auto-connect for trusted users while keeping the token strictly on the server.
This keeps the managed gateway token on the server while still allowing explicit manual credentials for unsupported or custom connection paths.
### Device Identity & Gateway Scopes
@ -374,10 +376,10 @@ HSTS is always sent (`max-age=31536000; includeSubDomains`), even over HTTP. Bro
| Secret | Storage | Exposure |
|--------|---------|----------|
| `GATEWAY_TOKEN` | `.env` file (chmod 600) | Only returned to loopback clients via `/api/connect-defaults`. Never logged. |
| `GATEWAY_TOKEN` | `.env` file (chmod 600) | Used server-side for trusted official-gateway connections. `/api/connect-defaults` returns `token: null`. Never logged. |
| `OPENAI_API_KEY` | `.env` file | Used server-side only. Never sent to clients. |
| `REPLICATE_API_TOKEN` | `.env` file | Used server-side only. Never sent to clients. |
| Gateway token (client) | `sessionStorage` | Cleared when browser tab closes. Not persisted to disk. |
| Gateway URL + optional manual token | `localStorage` (`oc-config`) | Used for reconnects. Trusted official-gateway flows usually keep the token empty; manually entered custom-gateway tokens persist until cleared. |
The setup wizard applies `chmod 600` to `.env` and backup files, restricting read access to the file owner.
@ -388,7 +390,7 @@ The setup wizard applies `chmod 600` to `.env` and backup files, restricting rea
| Measure | Details |
|---------|---------|
| **DOMPurify** | All rendered HTML (agent messages, markdown) passes through DOMPurify with a strict tag/attribute allowlist |
| **Session storage** | Gateway token stored in `sessionStorage`, not `localStorage` — cleared on tab close |
| **Local storage** | Connection preferences are stored in `localStorage` as `oc-config`. The official managed gateway path can keep the token empty; manually entered custom tokens may persist until cleared |
| **CSP enforcement** | `script-src 'self' https://s3.tradingview.com` blocks inline scripts and limits external scripts to TradingView chart widgets only |
| **No eval** | No use of `eval()`, `Function()`, or `innerHTML` with unsanitised content |

View file

@ -122,8 +122,8 @@ The server detects `EADDRINUSE` and exits with a clear error (see `server/index.
**Fix:**
- Verify the gateway is running: `openclaw gateway status`
- Check token: the server reads `GATEWAY_TOKEN` or `OPENCLAW_GATEWAY_TOKEN` env var
- For local access, `/api/connect-defaults` auto-provides the token (loopback only)
- For remote access, the token is NOT auto-provided (security). Enter it manually in the connection dialog
- For trusted official-gateway access, `/api/connect-defaults` advertises `serverSideAuth=true` and Nerve connects with an empty browser-side token
- For custom gateway URLs or untrusted access, enter the token manually in the connection dialog
### Connection drops and "SIGNAL LOST" banner
@ -144,17 +144,21 @@ curl http://127.0.0.1:3080/health
**Fix:**
- If gateway is unreachable, restart it: `openclaw gateway restart`
- If persistent, check firewall rules or network configuration
- The client stores credentials in `sessionStorage` (cleared on tab close) — if credentials are lost, reconnect manually
- If a stale manual token is saved in `localStorage`, clear `oc-config` and reload before reconnecting
### Auto-connect doesn't work
**Symptom:** ConnectDialog appears even though the gateway is running.
**Cause:** The frontend fetches `/api/connect-defaults` on mount. This endpoint only returns the token for loopback clients (127.0.0.1, ::1).
**Cause:** The frontend fetches `/api/connect-defaults` on mount, but auto-connect only happens when:
- `serverSideAuth=true`
- the saved gateway URL is empty or matches the server's official gateway URL
- the initial connect attempt succeeds
**Fix:**
- If accessing Nerve remotely (SSH tunnel, reverse proxy), you must enter the gateway URL and token manually
- Alternatively, set the gateway URL in the connection dialog — the server's WebSocket proxy handles the actual connection
- If you want the managed path, clear stale browser config (`localStorage.oc-config`) and reload
- If you are using a custom gateway URL, manual token entry is expected
- If you are remote but authenticated and using the official gateway URL, the dialog should not be required after stale config is cleared
---
@ -193,11 +197,11 @@ WS_ALLOWED_HOSTS=mygateway.local npm start
**Symptom:** Server logs show `[ws-proxy] Gateway closed: code=1008, reason=unauthorized: device token mismatch`.
**Causes:**
1. **Stale browser token.** The browser caches the gateway token in `sessionStorage`. If the token changes (e.g., after re-running setup or restarting the gateway), the browser still sends the old one.
1. **Stale browser config.** The browser may still have an old manually-entered token saved in `localStorage` (`oc-config`), often from an older build or a custom gateway connection.
2. **Token mismatch across config files.** OpenClaw 2026.2.19 has a known bug where `openclaw onboard` writes different tokens to the systemd service file and `openclaw.json`. The gateway uses the systemd env var; Nerve reads from `.env`.
**Fix (stale browser):**
Close the tab completely and open a fresh one (or use incognito). `sessionStorage` is cleared on tab close.
**Fix (stale browser config):**
Clear site data or remove `localStorage.oc-config`, then reload so the official managed gateway path can reconnect with an empty token.
**Fix (token mismatch):**
Re-run the setup wizard — it reads the real token from the systemd service file and aligns everything: