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