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:
@@ -282,6 +282,26 @@ export type PluginManifest = {
|
||||
* a missing one that cannot be installed here refuses the install rather than proceeding to a plugin
|
||||
* that runs and quietly produces nothing.
|
||||
*/
|
||||
/**
|
||||
* A websocket this plugin serves, relayed by the platform.
|
||||
*
|
||||
* Declaring this makes `/api/plugin-ws/<appName>` a byte relay onto the address below. It exists because
|
||||
* Bun's route table is fixed at `serve()`, so a plugin installed afterwards could never add a socket route
|
||||
* — which is why `/minesweeper` had to live in the platform and `plugins/desktop` could not stream a
|
||||
* display at all.
|
||||
*
|
||||
* Authorisation is the plugin's OWN capability, asked as if the socket were a GET against its HTTP prefix.
|
||||
* There is deliberately no separate socket permission: granting the plugin grants its socket, and nothing
|
||||
* can drift out of step because there is only one decision.
|
||||
*
|
||||
* `{ port }` for a fixed loopback port the plugin's own service listens on. `{ sidecar }` when the target
|
||||
* is a sidecar, whose port is ephemeral and announced at registration — a manifest cannot know that
|
||||
* number, which is the whole reason the indirection exists.
|
||||
*
|
||||
* The host is always loopback and cannot be set. A plugin naming an arbitrary host would be a
|
||||
* request-forgery primitive reachable by anyone the plugin is granted to.
|
||||
*/
|
||||
websocket?: { port: number } | { sidecar: string };
|
||||
osDependencies?: OsDependency[];
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user