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 origin — vite --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.
Summary
@devframes/vite'sdevframeViteBridge(anddevframeVite) forward noallowedOriginstoinitDevframe, so the bridged WebSocket inherits devframe's loopback-only origin check. Whenever the host Vite dev server is reached at a non-loopback origin —vite --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 onstatus: 'connecting'forever (devframe only leavesconnectingonce 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.6devframe: 0.9.6vite: 8.2.xReproduction
Run
vite --hostand 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:
Expected
A bridge serving a same-origin dev tool should accept the dev server's own origin. At minimum,
devframeViteBridgeshould expose a way to widen/disable the origin check, the wayinitDevframeandcreateDevServeralready do viaallowedOrigins.Actual
DevframeViteBridgeOptions={ base, port, host, flags, auth, mcp }— noallowedOrigins. Internally (@devframes/vite/dist/single.mjs) the bridge calls:initDevframeacceptsallowedOrigins?: readonly string[] | WsOriginRegistry | false, but there's no way to reach it through the bridge.Proposed fix
Add
allowedOrigins?: readonly string[] | WsOriginRegistry | falsetoDevframeViteBridgeOptions(andDevframeViteOptions) and forward it toinitDevframe. 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'screateDevframeNextHandler(CreateDevframeNextHandlerOptionshas noallowedOrigins), 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
initDevframedirectly withallowedOrigins: false:(
allowedOrigins: falseis 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.