| 296 | }; |
| 297 | |
| 298 | export async function startServer(opts: StartServerOptions = {}): Promise<ServerInstance> { |
| 299 | const port = opts.port ?? parseInt(process.env.PORT ?? "4788", 10); |
| 300 | const hostname = opts.hostname ?? "127.0.0.1"; |
| 301 | // ONE credential, always present: an explicit override or the stable token |
| 302 | // from auth.json (minted on first run). Auth is unconditionally on — loopback |
| 303 | // is no longer a free pass, since Executor runs arbitrary code that any local |
| 304 | // process could otherwise drive. |
| 305 | const authToken = normalizeCredential(opts.authToken) ?? loadOrMintLocalAuthToken(); |
| 306 | const isAuthorized = makeIsAuthorized(authToken); |
| 307 | // CORS-only origin allowlist (no Host gate — the bearer is the boundary). |
| 308 | const corsAllowedHosts = new Set<string>([ |
| 309 | ...DEFAULT_ALLOWED_HOSTS, |
| 310 | ...(opts.allowedHosts ?? []), |
| 311 | ]); |
| 312 | const clientDir = opts.clientDir ?? resolve(import.meta.dirname, "../dist"); |
| 313 | |
| 314 | startIntegrationsRefresh(); |
| 315 | |
| 316 | const ownsHandlers = opts.handlers === undefined; |
| 317 | const handlers = opts.handlers ?? (await getServerHandlers(authToken)); |
| 318 | let viteChild: ViteChild | null = null; |
| 319 | |
| 320 | const disposeOwnedResources = async (): Promise<void> => { |
| 321 | setOAuthCompletionListener(null); |
| 322 | if (ownsHandlers) { |
| 323 | await disposeServerHandlers(); |
| 324 | } else { |
| 325 | await closeProvidedHandlers(handlers); |
| 326 | } |
| 327 | if (viteChild) await viteChild.stop(); |
| 328 | }; |
| 329 | |
| 330 | // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: after handlers boot, failed static/dev/Bun startup must release DB ownership before surfacing the startup error |
| 331 | try { |
| 332 | // Mirror every OAuth callback completion into the local in-memory result |
| 333 | // store. The Electron desktop renderer polls /api/oauth/await/:sessionId |
| 334 | // for these when the user runs the flow in their system browser (no |
| 335 | // shared origin → no postMessage). Cloud doesn't register a listener; |
| 336 | // its same-origin web SPA receives results via postMessage directly. |
| 337 | setOAuthCompletionListener((result) => publishOAuthResult(result)); |
| 338 | |
| 339 | // Build static routes from either embedded assets, disk, or a spawned |
| 340 | // vite dev child (EXECUTOR_DEV=1). Vite mode takes precedence and |
| 341 | // disables the file-extension 404 short-circuit since vite serves |
| 342 | // hashed asset paths directly. |
| 343 | let staticRoutes: Record<string, StaticHandler> = {}; |
| 344 | let serveIndex: StaticHandler; |
| 345 | |
| 346 | const devMode = process.env.EXECUTOR_DEV === "1" && !opts.embeddedWebUI; |
| 347 | if (devMode) { |
| 348 | console.log("[executor] EXECUTOR_DEV=1 — spawning vite dev child for live UI"); |
| 349 | viteChild = await startViteChild(); |
| 350 | // Diagnostic only — this is the internal vite port the daemon proxies to. |
| 351 | // It must NOT read as a destination: the URL to open is the `Open:` line the |
| 352 | // CLI prints (the daemon port, with ?_token). Hitting the vite port directly |
| 353 | // skips that bootstrap and lands on the auth gate. |
| 354 | console.log(`[executor] (internal) vite dev child at ${viteChild.url} — don't open this`); |
| 355 | serveIndex = () => |