| 325 | }; |
| 326 | |
| 327 | export async function startServer(opts: StartServerOptions = {}): Promise<ServerInstance> { |
| 328 | const port = opts.port ?? parseInt(process.env.PORT ?? "4788", 10); |
| 329 | const hostname = opts.hostname ?? "127.0.0.1"; |
| 330 | // ONE credential, always present: an explicit override or the stable token |
| 331 | // from auth.json (minted on first run). Auth is unconditionally on — loopback |
| 332 | // is no longer a free pass, since Executor runs arbitrary code that any local |
| 333 | // process could otherwise drive. |
| 334 | const authToken = normalizeCredential(opts.authToken) ?? loadOrMintLocalAuthToken(); |
| 335 | const isAuthorized = makeIsAuthorized(authToken); |
| 336 | // CORS-only origin allowlist (no Host gate — the bearer is the boundary). |
| 337 | const corsAllowedHosts = new Set<string>([ |
| 338 | ...DEFAULT_ALLOWED_HOSTS, |
| 339 | ...(opts.allowedHosts ?? []), |
| 340 | ]); |
| 341 | const clientDir = opts.clientDir ?? resolve(import.meta.dirname, "../dist"); |
| 342 | |
| 343 | startIntegrationsRefresh(); |
| 344 | |
| 345 | const ownsHandlers = opts.handlers === undefined; |
| 346 | const handlers = opts.handlers ?? (await getServerHandlers(authToken)); |
| 347 | let viteChild: ViteChild | null = null; |
| 348 | |
| 349 | const disposeOwnedResources = async (): Promise<void> => { |
| 350 | setOAuthCompletionListener(null); |
| 351 | if (ownsHandlers) { |
| 352 | await disposeServerHandlers(); |
| 353 | } else { |
| 354 | await closeProvidedHandlers(handlers); |
| 355 | } |
| 356 | // Final analytics flush; the layer finalizer drains the buffer. |
| 357 | await disposeAnalytics(); |
| 358 | if (viteChild) await viteChild.stop(); |
| 359 | }; |
| 360 | |
| 361 | // 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 |
| 362 | try { |
| 363 | // Mirror every OAuth callback completion into the local in-memory result |
| 364 | // store. The Electron desktop renderer polls /api/oauth/await/:sessionId |
| 365 | // for these when the user runs the flow in their system browser (no |
| 366 | // shared origin → no postMessage). Cloud doesn't register a listener; |
| 367 | // its same-origin web SPA receives results via postMessage directly. |
| 368 | setOAuthCompletionListener((result) => publishOAuthResult(result)); |
| 369 | |
| 370 | // Build static routes from either embedded assets, disk, or a spawned |
| 371 | // vite dev child (EXECUTOR_DEV=1). Vite mode takes precedence and |
| 372 | // disables the file-extension 404 short-circuit since vite serves |
| 373 | // hashed asset paths directly. |
| 374 | let staticRoutes: Record<string, StaticHandler> = {}; |
| 375 | let serveIndex: StaticHandler; |
| 376 | |
| 377 | const devMode = process.env.EXECUTOR_DEV === "1" && !opts.embeddedWebUI; |
| 378 | if (devMode) { |
| 379 | console.log("[executor] EXECUTOR_DEV=1 — spawning vite dev child for live UI"); |
| 380 | viteChild = await startViteChild(); |
| 381 | // Diagnostic only — this is the internal vite port the daemon proxies to. |
| 382 | // It must NOT read as a destination: the URL to open is the `Open:` line the |
| 383 | // CLI prints (the daemon port, with ?_token). Hitting the vite port directly |
| 384 | // skips that bootstrap and lands on the auth gate. |