a plugin can serve a websocket

Bun's route table is fixed at serve() and WS_ROUTES is read once before it, so a
plugin installed afterwards could never add a socket route. That was a hard
ceiling, not a preference: /minesweeper had to live in the platform and
plugins/desktop could not stream a display at all.

The way out was already in the tree and had one consumer. dev-proxy serves a socket
with no WS_ROUTES entry, through a WILDCARD route whose handler calls
server.upgrade itself. Nothing has to become dynamic, because a plugin name is a
path segment rather than a route — one wildcard declared at boot serves every
plugin that will ever be installed.

Authorisation is the PLUGIN's own capability, asked as if the socket were a GET
against its HTTP prefix. That is the safety argument: there is no second permission
to grant, forget, or drift, so granting a plugin grants its socket and revoking it
closes it. It is also why plugin-ws is exempt from the boot totality check rather
than carrying a registry entry — one entry covering every plugin's socket would
mean granting one plugin granted them all.

A target is a TCP address and never a URL, which keeps the relay ignorant of what
it carries. That ignorance is the only reason one relay can serve an RFB display
and a line-oriented sidecar. Loopback only and not configurable: a plugin naming an
arbitrary host would be a request-forgery primitive handed to every plugin author.

sidecarPort() is new because createSidecarProxy learned announced ports per-proxy
and that state was unreachable from anywhere else — a second subscription would
have been a second thing to keep in step.

Verified: boots past the totality gate, and both an existing and a non-existent
plugin refuse 401 before revealing which. NOT yet verified: that it relays a byte.
Minesweeper is about to be the first caller, which is what proves it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-23 15:21:10 +00:00
co-authored by Claude Opus 5
parent 1c435a88ae
commit eba0e86758
6 changed files with 291 additions and 1 deletions
+6
View File
@@ -54,6 +54,12 @@ const EXEMPT_WS_PROVIDERS: Record<string, string> = {
// same cookie the HTTP side issues after verifying the OWNER's token — so it carries proof of the owner
// without carrying an account through `upgradeWs`. Refused outright without that cookie.
'dev-proxy': 'dev server HMR relay; gated by the owner-only cookie the dev proxy issues',
// One relay serving every plugin's socket, so a single registry entry here would be a HOLE: granting one
// plugin would grant them all. `upgradePluginWs` therefore asks the PLUGIN's own capability, as if the
// socket were a GET against its HTTP prefix — which means the decision is already in the registry, just
// not under this provider's name. Exempt because the check is per-request and per-plugin, never because
// it is absent.
'plugin-ws': "relayed per plugin; authorised by that plugin's own capability in upgradePluginWs",
};
/**