From c590674242606ec2b587d3e6640186007c874d4c Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Wed, 5 Aug 2026 23:56:42 +0000 Subject: [PATCH 1/4] docs: plan for /__devframes/ standard middleware handlers --- plans/devframes-standard-middleware.md | 183 +++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 plans/devframes-standard-middleware.md diff --git a/plans/devframes-standard-middleware.md b/plans/devframes-standard-middleware.md new file mode 100644 index 00000000..58cb402f --- /dev/null +++ b/plans/devframes-standard-middleware.md @@ -0,0 +1,183 @@ +# Plan: `/__devframes/` framework-agnostic standard middleware + +> Plan of record settled in a design interview on 2026-08-05. Implementation lands as a +> 5-PR GitHub stack (bottom → top), each layer passing the full gauntlet +> (`pnpm lint && pnpm knip && pnpm test && pnpm typecheck && pnpm build`). + +## Goal + +One web-standard handler (`(Request) => Response`, Comark-style — see +) that carries the +entire devtools surface — mounted devframes, WS RPC, auth, MCP, embedded floating mode — +mountable on any framework with a single catch-all route (Vite, Nitro, Hono, Next.js, Nuxt, +SvelteKit), running on Node ≥ 20 and Bun. The hub stays headless; UI is a composable slot. + +## Architecture + +### Two `createHandler` factories, one UI slot + +| Import | Serves | Default base | +|---|---|---| +| `devframe/handler` | **One devframe**: its SPA (`distDir`; omitted → bridge mode serving only meta + WS), `__connection.json`, `__mcp`, WS RPC, auth. Own isolated context, one `def.setup(ctx)`. | `/__/` (hosted rule) | +| `@devframes/hub/handler` | **Multi-frame, headless**: shared hub context (docks/terminals/messages/commands + hub built-in RPCs + shared-state slots); every frame's `setup(ctx)` runs against it → one merged RPC registry, one WS endpoint, **one hub Auth**, one aggregate MCP. Frames auto-registered as iframe docks. | `/__devframes/` | + +**`DevframeHubUi` slot** (type lives in `@devframes/hub`; data-first, zero policy): + +```ts +interface DevframeHubUi { + viewer?: { distDir: string } // standalone viewer SPA served at the namespace root + embedded?: { entry: string } // prebuilt bootstrap served at embedded.js +} +``` + +`@devframes/hub-ui` (new package) exports `createUi(options?)` — the *reference* implementation +(a port of Vite DevTools' web components). Vite DevTools / community supply their own `ui` +object to the same slot, reusing all infra. There is **no** `createHandler` in hub-ui. + +### Handler API (both factories) + +```ts +const h = createHandler(defOrOptions, { + base?, // mount base; hosted default /__/ (core) or /__devframes/ (hub) + server?, // sugar: node http server → shared WS upgrade at __ws + ws?: DevframeWsOptions, // explicit control — url > port > route (default '__ws') + auth?, // default TRUE (existing OTP/token machinery); explicit false to opt out + mcp?, // per-frame MCP (core) / aggregate MCP (hub) + key?, // globalThis memoization — HMR re-evaluation returns the live instance + origin?, // banner origin override; else derived lazily from first request + // hub only: + devframes?, context?, // declarative list OR pre-built hub context + configure?, // async (ctx) => {} for docks/commands/terminals/messages registration + ui?, // DevframeHubUi +}) +// → { fetch(request, runtimeCtx?), nodeMiddleware, websocket, ready, context, +// connectionMeta(), close() } +``` + +- Sync factory, **eager** async init; `fetch` awaits `ready` internally. +- `fetch` 404s inside its base; `nodeMiddleware` (connect-style) calls `next()` outside it. +- Bun: `fetch(req, server)` second arg + exposed `websocket` hooks (crossws Bun adapter). +- `key` memoization: a re-evaluation returns the live instance (closes/replaces it if the + options changed) — prevents eager side-car leaks under Next/Nitro/SvelteKit dev HMR. + +### WebSocket resolution (precedence) + +1. `ws.url` — advertise an external endpoint verbatim; the handler owns **no** transport. + Hosts that want the handler's RPC on their *own* WS server use the documented recipe: + `attachWsRpcTransport(handler.context RPC group, { server, path })` + a matching `ws`. +2. `ws.port` — explicit side-car port. +3. `server` — shared upgrade on the host's node http server at ``. +4. *(default)* — **eager** auto side-car on a free port, started at handler creation so + `__connection.json` is stable from the first request. + +All four advertised consistently in `__connection.json`. The WS route unifies on **`__ws`** +everywhere (breaking: was `__devframe_ws`), matching upstream Vite DevTools' `/__devtools/__ws`. + +### Path layout (hub, under base `/__devframes/`) + +| Path | Serves | Condition | +|---|---|---| +| `/` | `ui.viewer` dist, else the index document | — | +| `__index.json` | JSON index: frame ids/bases, endpoint paths | always | +| `embedded.js` | `ui.embedded.entry` | 404 without `ui.embedded` | +| `__connection.json` | hub connection meta | always | +| `__ws` | WS upgrade route (shared-server tier) | always | +| `__client-imports.js` | dock client-script import map | always | +| `__mcp` | **aggregate** MCP over the shared context registry | when `mcp` enabled | +| `/` | each frame's SPA + its per-frame `__connection.json` | reserved-name-validated ids | + +Per-frame `__mcp` exists only on the singular handler (the hub's shared context makes the +aggregate the meaningful endpoint; tool ids are already namespaced `devframes:plugin::*`). + +### Auth + +- Gated **by default** on both factories (existing `createInteractiveAuth` OTP + token + machinery; `anonymous:` pre-trust prefix; WS origin gate). +- **Hub: a single Auth.** One `DevframeAuthHandler` owned by the hub handler, one OTP + handshake, one trusted-token store, enforced at the one shared transport. Mounted frames + have no auth of their own — trust established once covers every frame, the aggregate MCP + origin gate, and the hub built-ins. Iframes may arrive pre-authorized via hub-served + `authToken` meta or reuse the parent page's connection (`__DEVFRAME_CONNECTION__`). +- Banner origin derived lazily from the first request (`origin` option overrides). + +### Embedded mode + +- `embedded.js` = prebuilt bundle: headless `createDevframeClientHost` + hub-ui's + `DockEmbedded`. **Always visible on load** — no view-mode model in hub-ui. Visibility + policy belongs to whoever authors the entry (Vite DevTools keeps its normal/passive/hidden + model in *its own* entry via its own `embedded: { entry }`). Dock-local state + (position/collapse) stays — component behavior, not visibility policy. +- Base discovery from `import.meta.url`; OTP/auth UI included. +- Injection = documented one-line `