|
| 1 | +--- |
| 2 | +outline: deep |
| 3 | +--- |
| 4 | + |
| 5 | +# Security |
| 6 | + |
| 7 | +Devframe tools are secure by default: connections bind to `localhost`, and dev-mode RPC requires a trust handshake before a browser is accepted. This page covers the trust model and the practices that keep a tool safe as it moves beyond a single developer's machine. |
| 8 | + |
| 9 | +## Trust model |
| 10 | + |
| 11 | +An RPC handler runs with the full privileges of the process hosting it — filesystem, child processes, network. A trusted connection can call any registered function, so the boundary that matters is *who is allowed to connect*. |
| 12 | + |
| 13 | +Two postures cover that boundary: |
| 14 | + |
| 15 | +- **Authenticated (default).** `auth` defaults to `true`. The browser authenticates with the server before calls are accepted, and reconnects by presenting a node-issued bearer token. Devframe supplies the node-side primitives (`exchangeTempAuthCode`, `verifyAuthToken`); the host adapter — e.g. Vite DevTools — provides the interactive handler and authentication UI. |
| 16 | +- **Unauthenticated opt-out.** Setting `auth: false` starts the server with an auto-trust handshake. It exists for single-user tools talking to their own `localhost`, where a round-trip would only add friction. |
| 17 | + |
| 18 | +> [!WARNING] |
| 19 | +> `auth: false` trusts every connection that can reach the port. Only use it when the surface is reachable solely by the local developer. Never combine it with a non-loopback bind host, a tunnelled port, or a shared/CI environment. |
| 20 | +
|
| 21 | +## Authentication flow |
| 22 | + |
| 23 | +Authentication exchanges a short code for a long-lived token. A node mints and owns the token; the browser only ever sends the short code, and only over the open socket. |
| 24 | + |
| 25 | +1. A fresh client connects unauthenticated and calls `devframe:anonymous:auth` with its stored token (empty on first run). The server returns `{ isTrusted: false }`, so the trust gate stays open while the UI prompts for a code. |
| 26 | +2. The dev server shows a 6-digit one-time code in the developer's terminal. |
| 27 | +3. The developer enters it; the browser calls `requestTrustWithCode(code)` → `devframe:auth:exchange`. |
| 28 | +4. The server verifies the code, mints a high-entropy bearer token, records it as trusted, marks the session trusted, and returns the token. |
| 29 | +5. The browser persists the token and presents it on reconnect (`devframe:anonymous:auth` → `verifyAuthToken`); sibling tabs receive it over the `devframe-auth` channel and become trusted too. |
| 30 | + |
| 31 | +The 6-digit code is single-use, expires after five minutes, is compared in constant time, and rotates after repeated wrong attempts — which is what keeps a short code brute-force resistant. Show it only in a trusted channel (the terminal), never over the network. |
| 32 | + |
| 33 | +The bearer token is a secret. It travels to the server on the WebSocket URL (`?devframe_auth_token=…`), so serve over `wss://`/`https://` whenever the surface is reachable beyond loopback. Revoke a token with `revokeAuthToken(context, storage, token)`; affected clients drop to untrusted via the `devframe:auth:revoked` event. |
| 34 | + |
| 35 | +### Auth methods |
| 36 | + |
| 37 | +Devframe owns the wire contract; the host adapter registers the handlers on top of the `devframe/node/auth` primitives (the standalone server registers a noop auto-trust handler when `auth: false`). |
| 38 | + |
| 39 | +| RPC method | Direction | Shape | |
| 40 | +|------------|-----------|-------| |
| 41 | +| `devframe:anonymous:auth` | client → server | `{ authToken, ua, origin }` → `{ isTrusted }` — re-authenticate a stored token | |
| 42 | +| `devframe:auth:exchange` | client → server | `{ code, ua, origin }` → `{ authToken \| null }` — exchange a one-time code for a token | |
| 43 | +| `devframe:auth:revoked` | server → client | event — the connection's token was revoked | |
| 44 | + |
| 45 | +Node primitives (`devframe/node/auth`): |
| 46 | + |
| 47 | +| Function | Role | |
| 48 | +|----------|------| |
| 49 | +| `getTempAuthCode()` / `refreshTempAuthCode()` | read / rotate the current one-time code to display | |
| 50 | +| `exchangeTempAuthCode(code, session, { ua, origin }, storage)` | verify a code, mint + store the token, trust the session, return the token (or `null`) | |
| 51 | +| `verifyAuthToken(token, session, storage)` | trust a session presenting a known token (reconnect) | |
| 52 | +| `buildOtpAuthUrl(origin, code?)` | build a magic-link URL embedding the code | |
| 53 | +| `revokeAuthToken(context, storage, token)` | delete a token and disconnect any sessions using it | |
| 54 | + |
| 55 | +Client methods (`devframe/client`): `requestTrustWithCode(code)` (exchange a code), `requestTrustWithToken(token)` (re-authenticate a token), `ensureTrusted(timeout?)` / `isTrusted` (the trust gate). |
| 56 | + |
| 57 | +### Magic-link authentication |
| 58 | + |
| 59 | +To skip typing, a host can print a link that embeds the code and open the browser straight into an authenticated session. Build it from the current code with `buildOtpAuthUrl(origin)` (devframe stays headless, so the host prints its own banner): |
| 60 | + |
| 61 | +``` |
| 62 | +Devtools ready — authenticate this browser: http://localhost:3000/?devframe_otp=123456 |
| 63 | +``` |
| 64 | + |
| 65 | +`connectDevframe` reads the `devframe_otp` parameter, exchanges it, and removes it from the URL before anything else. Only the short-lived, single-use **code** ever rides the URL — the resulting bearer token is stored, never written back to it. Because the link grants trust to whoever opens it within the code's lifetime, print it only to a trusted channel (the terminal), exactly as you would the bare code. |
| 66 | + |
| 67 | +Higher-level integrations can drive their own authentication UI instead: disable the built-in handling with the `otpParam: false` client option, then call the exposed `authenticateWithUrlOtp(rpc)` (consume the code from the URL and exchange it) or `consumeOtpFromUrl()` (read and strip the code) from `devframe/client`. |
| 68 | + |
| 69 | +## Practices for tools built on devframe |
| 70 | + |
| 71 | +- **Stay on loopback.** The default bind host is `localhost`. Bind to a routable address only when you intend to, and require authentication when you do. |
| 72 | +- **Keep `auth: false` local.** Reach for it only for single-user localhost tools; leave the default in place anywhere a connection could originate elsewhere. |
| 73 | +- **Treat tokens as secrets.** Never log the bearer token or the one-time code, and never bake either into build output. |
| 74 | +- **Authorize every handler.** A registered function is callable by any trusted client. Validate inputs, and mark state-changing functions `type: 'destructive'` so MCP and agent clients prompt before invoking them. |
| 75 | +- **Origin-lock remote docks.** When a hub embeds a remote-UI dock, enable `originLock` so a dock token is only honored from its expected origin. |
| 76 | +- **Serve encrypted off-machine.** Use `https://`/`wss://` for any surface reachable beyond `localhost`. |
0 commit comments