Skip to content

Security

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.

Trust model

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.

Two postures cover that boundary:

  • 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.
  • 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.

WARNING

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.

Authentication flow

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.

  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.
  2. The dev server shows a 6-digit one-time code in the developer's terminal.
  3. The developer enters it; the browser calls requestTrustWithCode(code)devframe:auth:exchange.
  4. The server verifies the code, mints a high-entropy bearer token, records it as trusted, marks the session trusted, and returns the token.
  5. The browser persists the token and presents it on reconnect (devframe:anonymous:authverifyAuthToken); sibling tabs receive it over the devframe-auth channel and become trusted too.

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.

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.

Auth methods

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).

RPC methodDirectionShape
devframe:anonymous:authclient → server{ authToken, ua, origin }{ isTrusted } — re-authenticate a stored token
devframe:auth:exchangeclient → server{ code, ua, origin }{ authToken | null } — exchange a one-time code for a token
devframe:auth:revokedserver → clientevent — the connection's token was revoked

Node primitives (devframe/node/auth):

FunctionRole
getTempAuthCode() / refreshTempAuthCode()read / rotate the current one-time code to display
exchangeTempAuthCode(code, session, { ua, origin }, storage)verify a code, mint + store the token, trust the session, return the token (or null)
verifyAuthToken(token, session, storage)trust a session presenting a known token (reconnect)
buildOtpAuthUrl(origin, code?)build a magic-link URL embedding the code
revokeAuthToken(context, storage, token)delete a token and disconnect any sessions using it

Client methods (devframe/client): requestTrustWithCode(code) (exchange a code), requestTrustWithToken(token) (re-authenticate a token), ensureTrusted(timeout?) / isTrusted (the trust gate).

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):

Devtools ready — authenticate this browser: http://localhost:3000/?devframe_otp=123456

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.

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.

Practices for tools built on devframe

  • 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.
  • Keep auth: false local. Reach for it only for single-user localhost tools; leave the default in place anywhere a connection could originate elsewhere.
  • Treat tokens as secrets. Never log the bearer token or the one-time code, and never bake either into build output.
  • 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.
  • 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.
  • Serve encrypted off-machine. Use https:///wss:// for any surface reachable beyond localhost.

Released under the MIT License.