Minesweeper is now plugins/minesweeper (gitea.officer.dev/plugins/minesweeper). Removes /api/minesweeper, the Dashboard screen, the officerdev panel, the permission registry entry, the App.tsx route and the hand-written websocket provider in server.tsx. Registry entries: 22 -> 21, execution 4 -> 3. The relay changed transport, and that is the part worth reading. A sidecar announces exactly one port. That single announcement is the whole provenance argument in target.ts — the platform watched the port arrive at registration, which is the only thing separating it from a number a manifest asserted. But a raw-TCP relay requires that port to BE a raw TCP listener, so the sidecar cannot also serve HTTP on it, and the plugin's ordinary routes have to fall back to the typed command vocabulary over the registration socket. That is the mechanism the fleet has spent months retiring, and Minesweeper would have been the first thing to go back to it. Relaying websocket-to-websocket dissolves the conflict. The sidecar serves HTTP and /ws on the one port it announced, exactly as the pty sidecar already does; createSidecarProxy handles the HTTP half; and the conversion from frames to whatever the plugin actually speaks happens inside the plugin, which is the only process that knows. The relay is still ignorant of the payload — the ignorance sits one layer up. Also: the caller's JWT is no longer forwarded upstream. A sidecar has no use for it and forwarding one puts a bearer token into whatever that process logs. X-Officer-User carries the only part it needs. (The terminal relay still forwards its token to the pty sidecar; not touched here.) Verified: bunx tsgo clean, bun test 1403 pass / 0 fail, bun run docs:check green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
43 lines
2.4 KiB
TypeScript
43 lines
2.4 KiB
TypeScript
import { sidecarPort } from '@@/sidecar-registry';
|
|
import { snapshotPlugins } from '@@/plugins/mount';
|
|
|
|
/*
|
|
Where a plugin's socket actually goes.
|
|
|
|
A target is an ADDRESS — a host and a port — never a URL a manifest wrote. The relay turns it into
|
|
`ws://<host>:<port>/ws`, and `/ws` is a convention rather than a setting for the same reason the host is
|
|
not configurable: every value here has to be one the platform derived, not one a plugin asserted.
|
|
|
|
The target is always a SIDECAR. A fixed-port form was written and removed the same day: nothing stops a
|
|
manifest naming a port it does not own, so `{ port: 5432 }` plus a grant would be a raw TCP relay onto
|
|
Postgres for anyone that plugin reaches. Verifying the host is loopback is not verifying the port belongs
|
|
to the plugin, and only the second question matters.
|
|
|
|
A sidecar's port is different in kind, not degree: the platform watched it arrive at registration, so the
|
|
association is observed rather than asserted. It is also the SAME port the sidecar serves its HTTP on —
|
|
one announcement, one listener, `/ws` alongside the routes. That is what the relay's move to
|
|
websocket-to-websocket bought, and why a plugin wanting a socket no longer has to choose between having
|
|
one and having ordinary routes.
|
|
|
|
**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;
|
|
|
|
// 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;
|
|
}
|