You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
refactor(auth)!: use "authenticate" wording and document the auth flow
Replace the "pair/pairing" vocabulary with "authenticate/authentication"
across the auth surface, and rename the API for consistency:
- `getTempAuthToken` → `getTempAuthCode` and `refreshTempAuthToken` →
`refreshTempAuthCode` (they handle the one-time code, not a token)
- `buildOtpPairingUrl` → `buildOtpAuthUrl`
- `pairWithUrlOtp` → `authenticateWithUrlOtp`
Document the auth methods (RPC wire contract + `devframe/node/auth` and
client primitives) and the end-to-end auth flow in the security guide.
BREAKING CHANGE: `getTempAuthToken` → `getTempAuthCode`,
`refreshTempAuthToken` → `refreshTempAuthCode`, `buildOtpPairingUrl` →
`buildOtpAuthUrl`, `pairWithUrlOtp` → `authenticateWithUrlOtp`. All unreleased.
Copy file name to clipboardExpand all lines: docs/guide/client.md
+5-5Lines changed: 5 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -66,7 +66,7 @@ The client picks a mode automatically from the backend field. Mode-specific code
66
66
67
67
## Trust & auth (WebSocket mode)
68
68
69
-
Dev-mode connections become trusted by pairing. A client that has paired before presents its stored token automatically on reconnect, and `ensureTrusted()` resolves once the server accepts it:
69
+
Dev-mode connections become trusted by authenticating. A client that authenticated before presents its stored token automatically on reconnect, and `ensureTrusted()` resolves once the server accepts it:
A fresh client holds no token. The dev server prints a 6-digit one-time code; pass it to `requestTrustWithCode` to exchange it for a node-issued token. The token is persisted for future reconnections and shared with sibling tabs, which become trusted without re-entering the code:
85
85
@@ -89,7 +89,7 @@ const ok = await rpc.requestTrustWithCode('047204')
89
89
90
90
The code is single-use, expires after five minutes, and is rotated after repeated wrong attempts, so re-display the current code if an exchange fails.
91
91
92
-
To pair without typing, a host can print a link embedding the code (`buildOtpPairingUrl(origin)`); `connectDevframe` reads the `devframe_otp` query parameter, exchanges it, and strips it from the URL. Rename it with the `otpParam` option, or set `otpParam: false` and drive pairing yourself with the exposed `pairWithUrlOtp(rpc)` / `consumeOtpFromUrl()` utilities.
92
+
To authenticate without typing, a host can print a link embedding the code (`buildOtpAuthUrl(origin)`); `connectDevframe` reads the `devframe_otp` query parameter, exchanges it, and strips it from the URL. Rename it with the `otpParam` option, or set `otpParam: false` and drive authentication yourself with the exposed `authenticateWithUrlOtp(rpc)` / `consumeOtpFromUrl()` utilities.
93
93
94
94
### Re-using an existing token
95
95
@@ -101,7 +101,7 @@ const ok = await rpc.requestTrustWithToken('a1b2c3…')
101
101
102
102
### Broadcast-channel sync
103
103
104
-
`connectDevframe` listens on a shared `BroadcastChannel` (named `devframe-auth` for cross-tab handshake interop with Vite DevTools' auth page) for `auth-update` messages. When another tab completes a pairing — or an auth page announces a token — every open client trusts it automatically, no reload required.
104
+
`connectDevframe` listens on a shared `BroadcastChannel` (named `devframe-auth` for cross-tab handshake interop with Vite DevTools' auth page) for `auth-update` messages. When another tab authenticates — or an auth page announces a token — every open client trusts it automatically, no reload required.
Copy file name to clipboardExpand all lines: docs/guide/security.md
+35-12Lines changed: 35 additions & 12 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -12,42 +12,65 @@ An RPC handler runs with the full privileges of the process hosting it — files
12
12
13
13
Two postures cover that boundary:
14
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.
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
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
17
18
18
> [!WARNING]
19
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
20
21
-
## Pairing and tokens
21
+
## Authentication flow
22
22
23
-
Pairing exchanges a short code for a long token:
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
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.
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.
29
30
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.
31
32
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.
33
34
34
-
### Magic-link pairing
35
+
### Auth methods
35
36
36
-
To skip typing, a host can print a link that embeds the code and open the browser straight into a paired session. Build it from the current code with `buildOtpPairingUrl(origin)` (devframe stays headless, so the host prints its own banner):
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):
37
60
38
61
```
39
-
Devtools ready — pair this browser: http://localhost:3000/?devframe_otp=123456
62
+
Devtools ready — authenticate this browser: http://localhost:3000/?devframe_otp=123456
40
63
```
41
64
42
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.
43
66
44
-
Higher-level integrations can drive their own pairing UI instead: disable the built-in handling with the `otpParam: false` client option, then call the exposed `pairWithUrlOtp(rpc)` (consume the code from the URL and exchange it) or `consumeOtpFromUrl()` (read and strip the code) from `devframe/client`.
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`.
45
68
46
69
## Practices for tools built on devframe
47
70
48
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.
49
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.
50
-
-**Treat tokens as secrets.** Never log the bearer token or the pairing code, and never bake either into build output.
73
+
-**Treat tokens as secrets.** Never log the bearer token or the one-time code, and never bake either into build output.
51
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.
52
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.
53
76
-**Serve encrypted off-machine.** Use `https://`/`wss://` for any surface reachable beyond `localhost`.
0 commit comments