feat(dashboard): bearer-token auth with browser persistence + MIT license

Pre-release polish. Two related changes bundled because they both land the
project on public-release footing:

Dashboard auth
- fn dashboard now gates the HTTP API + terminal/badge WebSockets behind a
  bearer token by default. Token resolution order: --token flag,
  FUSION_DASHBOARD_TOKEN env, FUSION_DAEMON_TOKEN env (back-compat), or an
  auto-generated fn_<32 hex>. --no-auth disables. The startup banner prints
  a click-to-open URL with ?token=<token> embedded.
- Auth middleware now also accepts fn_token=<token> as a query-string
  fallback so EventSource and WebSocket clients (which can't set custom
  headers) still authenticate.
- setupTerminalWebSocket / setupBadgeWebSocket now refuse unauthenticated
  upgrades with a proper 401 + socket close.
- Frontend: new auth.ts module captures ?token= off the URL into
  localStorage (key fn.authToken), strips it from the visible URL via
  replaceState, and installs a window.fetch wrapper that injects
  Authorization: Bearer <token> on every same-origin /api/* request.
  EventSource/WebSocket URL builders (api.ts, sse-bus.ts, useTerminal,
  useBadgeWebSocket) route through appendTokenQuery().

MIT license
- LICENSE file at repo root.
- license: "MIT" on root package.json and every packages/*/package.json,
  plus description/bugs metadata on the CLI package.

Docs
- docs/cli-reference.md documents --token / --no-auth / FUSION_DASHBOARD_TOKEN
  and the click-to-open auth flow.
- docs/getting-started.md, docs/docker.md, README.md point at the new flow
  and the CLI reference section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-21 18:01:20 -07:00
parent 2c16620eff
commit c0f9260998
24 changed files with 496 additions and 68 deletions

View File

@@ -40,16 +40,68 @@ fn init --name my-project --path /absolute/path/to/project
## `fn dashboard`
Start the web dashboard (default port `4040`).
Start the web dashboard (default port `4040`, bound to `127.0.0.1`).
```bash
fn dashboard
fn dashboard --port 5050
fn dashboard --host 0.0.0.0 # expose on LAN (use with care)
fn dashboard --token fn_yourStaticToken # reuse a fixed token
fn dashboard --no-auth # disable bearer auth (local only)
fn dashboard --interactive
fn dashboard --paused
fn dashboard --dev
```
| Option | Description |
|---|---|
| `--port`, `-p` | Dashboard HTTP port (default `4040`). |
| `--host` | Host to bind (default `127.0.0.1`, localhost only). Pass `0.0.0.0` to expose on all interfaces. |
| `--token <token>` | Bearer token to use. Default: `$FUSION_DASHBOARD_TOKEN``$FUSION_DAEMON_TOKEN` → auto-generated. |
| `--no-auth` | Disable bearer-token auth. Not recommended when binding to `0.0.0.0`. |
| `--paused` | Start with the engine paused (automation disabled). |
| `--interactive` | Interactive port selection. |
| `--dev` | Start dashboard only (no AI engine, no triage/scheduler). |
### Authentication
Unless `--no-auth` is passed, the dashboard API (including the terminal
WebSocket) is protected by a bearer token. On startup, Fusion prints both the
raw token and a click-to-open URL that embeds `?token=<token>`:
```
fn board
────────────────────────
→ http://localhost:4040
Auth: bearer token required
Token: fn_8f3a...
Open: http://localhost:4040/?token=fn_8f3a...
(the browser stores the token so you only need to click once)
```
On first visit the dashboard captures the token from the URL into
`localStorage` (key `fn.authToken`) and strips it from the visible URL so the
secret does not end up in browser history. Subsequent loads (including
closing and reopening the tab) reuse the stored token.
Precedence when resolving the token:
1. `--token <token>` flag
2. `FUSION_DASHBOARD_TOKEN` environment variable
3. `FUSION_DAEMON_TOKEN` environment variable (back-compat with `fn daemon`)
4. Random `fn_<32 hex>` generated per run
To reuse a stable token across runs, export one of the env vars:
```bash
export FUSION_DASHBOARD_TOKEN=fn_my_stable_token
fn dashboard
```
If you ever need to revoke access, either restart `fn dashboard` (which
rotates the auto-generated token) or clear the `fn.authToken` entry from
each client's `localStorage`.
---
## `fn serve`