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:
@@ -0,0 +1,42 @@
|
||||
import { sidecarPort } from '@@/sidecar-registry';
|
||||
import { snapshotPlugins } from '@@/plugins/mount';
|
||||
|
||||
/*
|
||||
Where a plugin's socket actually goes.
|
||||
|
||||
A target is a TCP address and never a URL, and that is what keeps the relay ignorant of the protocol on
|
||||
the other side. The ignorance is the point: it is the only reason one relay can serve an RFB display and a
|
||||
line-oriented sidecar without knowing which is which. The moment this returned a scheme or a path, the
|
||||
relay would have to care what it was carrying.
|
||||
|
||||
Two forms, because two things genuinely differ:
|
||||
|
||||
{ port: 5900 } a fixed port a plugin's own service listens on — `x11vnc` for a desktop, or
|
||||
anything the plugin starts itself and knows the number of in advance.
|
||||
{ sidecar: 'name' } resolved at connect time, because a sidecar binds an EPHEMERAL port and announces
|
||||
it. A manifest cannot know that number, which is why the indirection exists rather
|
||||
than being a convenience.
|
||||
|
||||
**Host is always loopback and is not configurable.** A plugin socket that could name an arbitrary host
|
||||
would be a request-forgery primitive handed to every plugin author and reachable by anyone the plugin is
|
||||
granted to. There is no case for it that a sidecar does not already serve.
|
||||
*/
|
||||
|
||||
export type PluginWsTarget = { host: string; port: number };
|
||||
|
||||
export async function pluginWsTargetFor(appName: string): Promise<PluginWsTarget | null> {
|
||||
const { states } = await snapshotPlugins();
|
||||
// Installed, not merely present on disk. A plugin directory that exists but is not installed has no
|
||||
// routes and no permission, so it must have no socket either.
|
||||
const state = states.find((st) => st.plugin.appName === appName && st.install);
|
||||
const socket = state?.plugin.manifest.websocket;
|
||||
if (!socket) return null;
|
||||
|
||||
if ('port' in socket) return { host: '127.0.0.1', port: socket.port };
|
||||
|
||||
// A sidecar that has not announced its port yet is indistinguishable from one that never will, and both
|
||||
// are correctly a refusal — there is nothing to relay onto. The caller says "not accepting connections",
|
||||
// which is true either way.
|
||||
const port = sidecarPort(socket.sidecar);
|
||||
return port ? { host: '127.0.0.1', port } : null;
|
||||
}
|
||||
Reference in New Issue
Block a user