Skip to content

Commit 5ead924

Browse files
committed
docs(auth): security guide + preserve URL-query token auth
Persist a token supplied to `connectDevframe({ authToken })` so a host that bootstraps trust from its own page-URL query survives reconnects, matching the previous always-persist behaviour. The transport still forwards the token via the `?devframe_auth_token=` WS query param; add a test locking that in. Add a "Security" guide page and a SKILL section covering the trust model and secure-by-default practices for tools built on devframe: keep `auth` on, restrict `auth: false` to single-user localhost, treat tokens as secrets, serve over wss beyond loopback, authorize handlers, and origin-lock remote docks.
1 parent b47a934 commit 5ead924

5 files changed

Lines changed: 91 additions & 0 deletions

File tree

docs/.vitepress/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ function guideItems(prefix: string): DefaultTheme.NavItemWithLink[] {
2727
{ text: 'When Clauses', link: `${prefix}/guide/when-clauses` },
2828
{ text: 'Structured Diagnostics', link: `${prefix}/guide/diagnostics` },
2929
{ text: 'Client', link: `${prefix}/guide/client` },
30+
{ text: 'Security', link: `${prefix}/guide/security` },
3031
{ text: 'Standalone CLI', link: `${prefix}/guide/standalone-cli` },
3132
{ text: 'Hub (multi-tool)', link: `${prefix}/guide/hub` },
3233
{ text: 'Agent-Native (experimental)', link: `${prefix}/guide/agent-native` },

docs/guide/security.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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 pairs 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 pairing 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+
## Pairing and tokens
22+
23+
Pairing exchanges a short code for a long token:
24+
25+
1. The dev server shows a 6-digit one-time code in the developer's terminal.
26+
2. The developer types it into the browser, which calls `requestTrustWithCode(code)`.
27+
3. The server verifies the code, mints a high-entropy bearer token, records it as trusted, and returns it.
28+
4. The browser persists the token and presents it on reconnect; sibling tabs receive it over the `devframe-auth` channel.
29+
30+
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.
31+
32+
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.
33+
34+
## Practices for tools built on devframe
35+
36+
- **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.
37+
- **Keep `auth: false` local.** Reach for it only for single-user localhost tools; leave the default in place anywhere a connection could originate elsewhere.
38+
- **Treat tokens as secrets.** Never log the bearer token or the pairing code, and never bake either into build output.
39+
- **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.
40+
- **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.
41+
- **Serve encrypted off-machine.** Use `https://`/`wss://` for any surface reachable beyond `localhost`.

packages/devframe/src/client/rpc.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,12 @@ export async function getDevframeRpcClient(
238238
rpc: undefined!,
239239
}
240240
const authToken = getStoredAuthToken(options.authToken)
241+
// Persist a resolved token so one supplied out-of-band — e.g. a host that
242+
// bootstraps trust by passing `authToken` (read from its own page URL query)
243+
// — survives reconnects. The token is still sent to the server via the WS
244+
// URL query param (`?devframe_auth_token=`) by the transport.
245+
if (authToken)
246+
persistAuthToken(authToken)
241247
const clientRpc: DevframeClientRpcHost = new RpcFunctionsCollectorBase<DevframeRpcClientFunctions, DevframeRpcContext>(context)
242248

243249
async function fetchJsonFromBases(path: string): Promise<any> {

packages/devframe/src/rpc/transports/ws.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,35 @@ import { attachWsRpcTransport } from './ws-server'
88

99
vi.stubGlobal('WebSocket', WebSocket)
1010

11+
describe('ws auth token in URL', () => {
12+
it('appends the auth token as a URL query param, url-encoded, and omits it when absent', () => {
13+
const urls: string[] = []
14+
class CapturingWS {
15+
constructor(public url: string) {
16+
urls.push(url)
17+
}
18+
19+
addEventListener() {}
20+
removeEventListener() {}
21+
send() {}
22+
readyState = 0
23+
}
24+
25+
try {
26+
vi.stubGlobal('WebSocket', CapturingWS)
27+
createWsRpcChannel({ url: 'ws://127.0.0.1:1234' })
28+
createWsRpcChannel({ url: 'ws://127.0.0.1:1234', authToken: 'a b/c+d' })
29+
30+
expect(urls[0]).toBe('ws://127.0.0.1:1234')
31+
expect(urls[1]).toBe('ws://127.0.0.1:1234?devframe_auth_token=a%20b%2Fc%2Bd')
32+
}
33+
finally {
34+
// Restore the real ws implementation for the connection tests below.
35+
vi.stubGlobal('WebSocket', WebSocket)
36+
}
37+
})
38+
})
39+
1140
describe('devframe rpc', () => {
1241
it('should work w/ ws transport', async () => {
1342
const PORT = 3333

skills/devframe/SKILL.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,19 @@ Devframe re-exports a curated set of helpers under `devframe/utils/*`. They are
478478

479479
For "open file in editor" + "reveal in finder", prefer the prebuilt `openHelpers` RPC recipe — it wires the two utilities into named RPC functions ready to register.
480480

481+
## Security (secure by default)
482+
483+
RPC handlers run with the full privileges of the host process, so the boundary that matters is who may connect. Keep that boundary tight:
484+
485+
- **`auth` defaults to `true`** — dev-mode connections must pair before calls are accepted. Devframe ships the node primitives (`exchangeTempAuthCode`, `verifyAuthToken` in `devframe/node/auth`); the host adapter (e.g. Vite DevTools) provides the interactive `devframe:anonymous:auth` + `devframe:auth:exchange` handlers and pairing UI.
486+
- **`auth: false` trusts every reachable connection.** Use it only for single-user `localhost` tools. Never pair it with a non-loopback bind host, a tunnel, or a shared/CI environment. The default bind host is already `localhost`.
487+
- **Pairing** exchanges a 6-digit one-time code (shown in the developer's terminal) for a node-issued bearer token via `requestTrustWithCode(code)`. The code is single-use, expires in 5 min, compared in constant time, and rotates after repeated failures — show it only in the terminal, never over the network.
488+
- **Tokens are secrets.** The bearer token rides the WS URL (`?devframe_auth_token=…`) — serve over `wss://`/`https://` beyond loopback. Never log the token or code, never bake them into build output. Revoke via `revokeAuthToken(...)`; clients drop to untrusted on `devframe:auth:revoked`.
489+
- **Authorize handlers.** Any trusted client can call any registered function — validate inputs, and mark state-changing functions `type: 'destructive'` so MCP/agent clients prompt first.
490+
- **Origin-lock remote docks** (`originLock`) so a dock token is honored only from its expected origin.
491+
492+
See [Security](https://devfra.me/security) for the full reference.
493+
481494
## Testing
482495

483496
- Unit-test host classes with fake contexts.
@@ -497,6 +510,7 @@ Devframe-level pages (one-tool, portable surface):
497510
- [Structured Diagnostics](https://devfra.me/diagnostics) — coded errors via `ctx.diagnostics`, register custom codes
498511
- [Utilities](https://devfra.me/utilities) — bundled `devframe/utils/*` helpers (colors, hash, launchEditor, structured-clone, …)
499512
- [Client](https://devfra.me/client) — auth handshake, modes, discovery
513+
- [Security](https://devfra.me/security) — trust model, pairing, secure-by-default practices
500514
- [Agent-Native](https://devfra.me/agent-native) — agent field, tools/resources, MCP + Claude Desktop
501515

502516
Host-specific extras (when mounting into Vite DevTools — other hosts have their own equivalents):

0 commit comments

Comments
 (0)