Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/content/1.guide/14.security.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ For your own auth UI, disable built-in handling with `otpParam: false`, then cal

- **Stay on loopback.** Bind to a routable address only intentionally, and require authentication when you do.
- **Keep `auth: false` local.** The hosted bridges (`devframeViteBridge`, `@devframes/next`'s handler) gate their side-car by default; opt out with an explicit `auth: false` only when the host framework owns the trust boundary another way.
- **Gate the socket in a listener you own.** The `server` option binds its `upgrade` listener after async setup, so a guard that wraps the listeners present at startup misses it. To run your own check first, such as the peer address from `req.socket.remoteAddress` or a session cookie, leave `server` unset and own the listener. It sees every upgrade on the server, so act only on requests to `<base>__ws` and leave the rest to the host framework. For those, attach an `error` handler to the socket, then call `devtools.handleUpgrade(req, socket, head)` when the check passes, or write a `403` response and destroy the socket when it fails. The built-in socket gate checks `Origin` and lets `Origin`-less clients through, so with `auth: false` on a non-loopback bind any client that reaches the port can open the socket.
- **The MCP route trusts same-machine callers, harden it when that's not your boundary.** Two gates enforce that default: an origin gate (loopback-only, `Origin`-less rejected) is browser DNS-rebinding hardening, and a peer-address gate rejects a non-loopback caller even with a forged loopback `Origin` (the socket address can't be forged the way a header can). So the `'auto'` default - which mounts the route once agent tools exist - and `mcp: true` are enough for a local dev tool. Neither gate proves *which* caller it is, though, so to intentionally reach the route beyond loopback (a widened `allowedOrigins`, a hosted app) or to expose destructive tools, add an identity check with `mcp: { authorization }` (a bearer from an env var, or a callback), which also lifts the loopback-peer restriction - or turn the route off with `mcp: false`. See [MCP](/adapters/mcp).
- **Treat tokens as secrets.** Never log the bearer token or the one-time code, or bake either into build output.
- **Authorize every handler.** Validate inputs, and mark state-changing functions `type: 'destructive'` so MCP and agent clients prompt before invoking them.
Expand Down
2 changes: 1 addition & 1 deletion docs/content/2.adapters/1.initiate.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ Fetch handlers only hand over `Request`s, so the host framework binds the RPC so
1. **`ws.port`**: a side-car server on that exact port.
2. **`server`**: share the host framework's `node:http` server; the upgrade binds at `<base>__ws`. No extra ports.
3. **`ws: { sidecar: true }`**: a side-car server on a free port, for host frameworks whose handlers never see upgrades (Next.js route handlers, Nitro, Rsbuild).
4. **The host framework's own upgrades.** With none set, the socket waits: `devtools.attach(server)` routes a server's `upgrade` events (returning a detach fn); `devtools.handleUpgrade(req, socket, head)` completes a single one from a listener you own.
4. **The host framework's own upgrades.** With none set, the socket waits: `devtools.attach(server)` routes a server's `upgrade` events (returning a detach fn); `devtools.handleUpgrade(req, socket, head)` completes a single one from a listener you own. To run your own check before the socket opens, own the listener, match the `<base>__ws` path first (the listener sees every upgrade on the server, including the host framework's own), and call `handleUpgrade` once the check passes; the `server` option attaches its listener asynchronously, after any guard that wraps the listeners present at startup.

`ws.url` controls the *advertisement* instead, so the browser dials it verbatim. Alone, an external WebSocket server owns the transport and its auth (wire the running devframe's `context` via `createContextRpcServer` + a WS transport); alongside a local binding it overrides only the advertisement (the tunnel pattern).

Expand Down
Loading