* Start the HTTP + WebSocket server. * HTTP serves the plugin UI content; WebSocket handles plugin communication.
()
| 194 | * HTTP serves the plugin UI content; WebSocket handles plugin communication. |
| 195 | */ |
| 196 | async start(): Promise<void> { |
| 197 | if (this._isStarted) return; |
| 198 | |
| 199 | return new Promise((resolve, reject) => { |
| 200 | let rejected = false; |
| 201 | const rejectOnce = (error: any) => { |
| 202 | if (!rejected) { |
| 203 | rejected = true; |
| 204 | reject(error); |
| 205 | } |
| 206 | }; |
| 207 | |
| 208 | try { |
| 209 | // Create HTTP server first — handles plugin UI requests |
| 210 | this.httpServer = createHttpServer((req, res) => this.handleHttpRequest(req, res)); |
| 211 | |
| 212 | // Attach WebSocket server to the HTTP server (shares the same port) |
| 213 | this.wss = new WSServer({ |
| 214 | server: this.httpServer, |
| 215 | maxPayload: 100 * 1024 * 1024, // 100MB — screenshots and large component data can be big |
| 216 | verifyClient: (info, callback) => { |
| 217 | // Mitigate Cross-Site WebSocket Hijacking (CSWSH): |
| 218 | // Reject connections from unexpected browser origins. |
| 219 | const origin = info.origin; |
| 220 | // Exact match only — startsWith would let e.g. |
| 221 | // https://www.figma.com.attacker.example through. |
| 222 | const allowed = |
| 223 | !origin || // No origin — local process (e.g. Node.js client) |
| 224 | origin === 'null' || // Sandboxed iframe / Figma Desktop plugin UI |
| 225 | origin === 'https://www.figma.com' || |
| 226 | origin === 'https://figma.com'; |
| 227 | if (allowed) { |
| 228 | callback(true); |
| 229 | } else { |
| 230 | logger.warn({ origin }, 'Rejected WebSocket connection from unauthorized origin'); |
| 231 | callback(false, 403, 'Unauthorized Origin'); |
| 232 | } |
| 233 | }, |
| 234 | }); |
| 235 | |
| 236 | // Error handler for startup failures (EADDRINUSE, etc.) |
| 237 | // Must be on BOTH httpServer and wss — the WSS re-emits HTTP server errors |
| 238 | // and throws if no listener is attached. |
| 239 | const onStartupError = (error: any) => { |
| 240 | if (!this._isStarted) { |
| 241 | try { if (this.wss) { this.wss.close(); this.wss = null; } } catch { /* ignore */ } |
| 242 | try { if (this.httpServer) { this.httpServer.close(); this.httpServer = null; } } catch { /* ignore */ } |
| 243 | rejectOnce(error); |
| 244 | } else { |
| 245 | logger.error({ error }, 'HTTP/WebSocket server error'); |
| 246 | } |
| 247 | }; |
| 248 | |
| 249 | this.httpServer.on('error', onStartupError); |
| 250 | this.wss.on('error', onStartupError); |
| 251 | |
| 252 | // Start listening on the HTTP server (which also handles WS upgrades) |
| 253 | this.httpServer.listen(this.options.port, this.options.host || 'localhost', () => { |
nothing calls this directly
no test coverage detected