Skip to content

@devframes/vite: devframeViteBridge has no way to set allowedOrigins → WS 403 (stuck "connecting") on non-loopback origins #348

Description

@antfubot

Summary

@devframes/vite's devframeViteBridge (and devframeVite) forward no allowedOrigins to initDevframe, so the bridged WebSocket inherits devframe's loopback-only origin check. Whenever the host Vite dev server is reached at a non-loopback originvite --host, Docker/WSL/dev-containers, Codespaces, or any tunnel — the RPC WebSocket upgrade is rejected with HTTP 403, and there is no option on the bridge to opt out or widen it.

For a client using connectDevframe() this manifests as the connection sitting on status: 'connecting' forever (devframe only leaves connecting once trust succeeds/fails over the socket), with no error surfaced to the UI — a confusing "stuck connecting" with a clean console.

Environment

  • @devframes/vite: 0.9.6
  • devframe: 0.9.6
  • vite: 8.2.x

Reproduction

// vite.config.ts
import { devframeViteBridge, devframeVitePlugin } from '@devframes/vite/single'
import { defineConfig } from 'vite'
import def from './my-devframe' // any DevframeDefinition with clientAssets

export default defineConfig({
  plugins: [
    devframeVitePlugin(def, { base: '/__tool/' }),
    devframeViteBridge(def, { base: '/__tool/' }),
  ],
})

Run vite --host and open the app via a non-loopback URL (LAN IP, container-forwarded host, or a tunnel). The SPA loads but never connects.

Minimal check against the WS endpoint:

# loopback origin — allowed
wscat -H 'Origin: http://127.0.0.1:5173' -c ws://127.0.0.1:5173/__tool/__ws   # 101

# any non-loopback origin — refused
wscat -H 'Origin: https://my-tunnel.example.dev' -c ws://127.0.0.1:5173/__tool/__ws   # HTTP 403

Expected

A bridge serving a same-origin dev tool should accept the dev server's own origin. At minimum, devframeViteBridge should expose a way to widen/disable the origin check, the way initDevframe and createDevServer already do via allowedOrigins.

Actual

DevframeViteBridgeOptions = { base, port, host, flags, auth, mcp } — no allowedOrigins. Internally (@devframes/vite/dist/single.mjs) the bridge calls:

const created = initDevframe(d, {
  base,
  distDir: false,
  ...(options.port != null ? { ws: { port: options.port } } : server.httpServer ? { server: server.httpServer } : { ws: { sidecar: true } }),
  auth: options.auth,
  mcp: options.mcp,
  // allowedOrigins is never passed -> initDevframe default (loopback-only)
})

initDevframe accepts allowedOrigins?: readonly string[] | WsOriginRegistry | false, but there's no way to reach it through the bridge.

Proposed fix

Add allowedOrigins?: readonly string[] | WsOriginRegistry | false to DevframeViteBridgeOptions (and DevframeViteOptions) and forward it to initDevframe. A dev-server bridge might also reasonably default to allowing the Vite server's own resolved origins (server.resolvedUrls) rather than loopback-only, since the tool is served same-origin.

The same gap exists on @devframes/next's createDevframeNextHandler (CreateDevframeNextHandlerOptions has no allowedOrigins), so its side-car WS is unreachable from a remotely-accessed Next dev server too.

Workaround

Bypass the bridge and mount the RPC backend via initDevframe directly with allowedOrigins: false:

import { initDevframe } from 'devframe/initiate'

function bridge(def): Plugin {
  let instance
  return {
    name: 'my-tool:rpc',
    apply: 'serve',
    async configureServer(server) {
      instance = initDevframe(def, {
        base: '/__tool/',
        distDir: false,
        ...(server.httpServer ? { server: server.httpServer as any } : { ws: { sidecar: true } }),
        allowedOrigins: false, // gated by OTP anyway
      })
      server.middlewares.use(instance.nodeMiddleware)
      await instance.ready
    },
    async closeBundle() { await instance?.close() },
  }
}

(allowedOrigins: false is acceptable here because the endpoint is served same-origin by the dev server and the OTP auth gate is the real trust boundary — but having a first-class option on the bridge would avoid re-implementing it.)


Found while migrating the UnoCSS Inspector to devframe. Thanks for devframe — the migration was otherwise smooth. This report was filed with the help of an agent.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions