| 65 | * of idle TUI CPU (#200). Pi never imports this module, so `Bun.serve` is safe. |
| 66 | */ |
| 67 | export class MagicContextRpcServer { |
| 68 | private server: Server<WsData> | null = null; |
| 69 | private port = 0; |
| 70 | private handlers = new Map<string, RpcHandler>(); |
| 71 | private portFilePath: string; |
| 72 | private portDir: string; |
| 73 | private startedAt = Date.now(); |
| 74 | /** Every authenticated WS socket, so dispose can close them all. */ |
| 75 | private sockets = new Set<ServerWebSocket<WsData>>(); |
| 76 | // Unguessable per-process bearer token, published in the (user-private) port |
| 77 | // file and required on every non-health RPC call AND in the WS hello. Defends |
| 78 | // side-effecting endpoints (recomp/upgrade/dismiss) and the push channel |
| 79 | // against any local process or browser-origin script that merely |
| 80 | // discovers/guesses the port. |
| 81 | private readonly token = randomBytes(32).toString("hex"); |
| 82 | |
| 83 | constructor(storageDir: string, directory: string) { |
| 84 | this.portFilePath = rpcPortFilePath(storageDir, directory); |
| 85 | this.portDir = rpcPortDir(storageDir, directory); |
| 86 | } |
| 87 | |
| 88 | /** Register an RPC method handler. */ |
| 89 | handle(method: string, handler: RpcHandler): void { |
| 90 | this.handlers.set(method, handler); |
| 91 | } |
| 92 | |
| 93 | /** Start the server on a random port, write port to disk. */ |
| 94 | async start(): Promise<number> { |
| 95 | const self = this; |
| 96 | const server = Bun.serve<WsData>({ |
| 97 | port: 0, |
| 98 | hostname: "127.0.0.1", |
| 99 | fetch(req, srv) { |
| 100 | return self.handleFetch(req, srv); |
| 101 | }, |
| 102 | websocket: { |
| 103 | open(ws) { |
| 104 | // Close the socket if it doesn't authenticate promptly. A |
| 105 | // never-authenticated socket holds no sink and is harmless, |
| 106 | // but we don't want to keep raw connections open forever. |
| 107 | ws.data.authTimer = setTimeout(() => { |
| 108 | if (!ws.data.authed) ws.close(WS_CLOSE_UNAUTHORIZED, "auth timeout"); |
| 109 | }, WS_AUTH_TIMEOUT_MS); |
| 110 | }, |
| 111 | message(ws, raw) { |
| 112 | self.handleWsMessage(ws, raw); |
| 113 | }, |
| 114 | close(ws) { |
| 115 | if (ws.data.authTimer) clearTimeout(ws.data.authTimer); |
| 116 | ws.data.unregister?.(); |
| 117 | self.sockets.delete(ws); |
| 118 | }, |
| 119 | }, |
| 120 | }); |
| 121 | |
| 122 | this.server = server; |
| 123 | this.port = server.port ?? 0; |
| 124 |
nothing calls this directly
no outgoing calls
no test coverage detected