import { randomBytes } from "node:crypto"; import { existsSync } from "node:fs"; import { readFile, stat, writeFile } from "node:fs/promises"; import path from "node:path"; import type { DesktopReachability, DesktopSetup } from "@rakazo/contracts"; import { LOCAL_SETTINGS_PAGE } from "@rakazo/contracts/local-settings"; import { app, BrowserWindow, dialog, ipcMain, Menu, net, type Session, session, shell, } from "electron"; import { DesktopUpdateController, type ElectronAutoUpdater, LAUNCH_CHECK_DELAY_MS, } from "./auto-update.js"; import { openBrowserAuth } from "./browser-auth.js"; import { DOCKER_INSTALL_LINKS, isDesktopSetupLink, runDocker } from "./docker-cli.js"; import { requestLocalSettings } from "./local-settings.js"; import { LocalStackController, readStackToken, readStackWebUrl, resolveImageTag, stackDir, stackResourceDir, } from "./local-stack.js"; import { oauthCallbackFrom } from "./oauth-callback.js"; import { bundledRendererCandidates, contentType, forwardedRendererRequestInit, immutableRendererAsset, isRendererAssetMiss, } from "./renderer-assets.js"; import { installSessionPermissions } from "./session-permissions.js"; import { DEFAULT_LOCAL_WEB_URL, desktopStackImageTag, isRakazoHealth, managedLocalOpenUrl, maySendDesktopStackToken, normalizeServerUrl, parseSetupInput, probeFailureMessage, readProbeJson, resolveStartupTarget, safeExternalUrl, servesBundledRenderer, sessionPartitionForServerUrl, } from "./setup-config.js"; import { clearSetup, readSetup, writeSetup } from "./setup-store.js"; import { shouldOpenInAppPopup } from "./window-open.js"; import { browserWindowOptions, developmentIconFile, setupWindowOptions, warmWindowTtlMs, } from "./window-options.js"; const PERFORMANCE_USER_DATA = process.env.RAKAZO_PERFORMANCE_USER_DATA; /** Test hook: where the app-managed stack answers. Mode `new` still requires loopback. */ const LOCAL_WEB_URL = process.env.RAKAZO_LOCAL_WEB_URL?.trim() || DEFAULT_LOCAL_WEB_URL; const PROBE_TIMEOUT_MS = 8_000; const DESKTOP_STACK_PROBE_PATH = "/.well-known/rakazo-desktop-stack"; const DESKTOP_STACK_TOKEN_HEADER = "x-rakazo-desktop-stack-token"; let mainWindow: BrowserWindow | null = null; const appWindowTargets = new WeakMap(); let setupWindow: BrowserWindow | null = null; let settingsWindow: BrowserWindow | null = null; let openingSettings = false; let settingsCleanup: Promise = Promise.resolve(); let settingsTarget: { origin: string; token: string } | null = null; const bundledRendererInstallations = new Set(); let currentSetup: DesktopSetup | null = null; let currentTargetUrl: string | null = null; let setupError: string | null = null; let setupSaveInProgress = false; let openAppPromise: Promise | null = null; /** Prior app window kept until setup is persisted (or the switch is abandoned). */ let pendingPreviousWindow: BrowserWindow | null = null; let quitting = false; let warmWindowTimer: NodeJS.Timeout | undefined; // Number of short-lived hidden probe windows currently alive. On Windows/Linux, // destroying the last window fires "window-all-closed" -> app.quit(); a probe // that runs before the first real window exists must not count as "all closed". let liveProbeWindows = 0; const WARM_WINDOW_TTL_MS = warmWindowTtlMs(process.env.RAKAZO_WARM_WINDOW_TTL_MS); const updaterEnvironment = { packaged: app.isPackaged, version: app.getVersion(), disabled: process.env.RAKAZO_DISABLE_AUTO_UPDATE === "1", }; const desktopUpdater = new DesktopUpdateController( updaterEnvironment, async () => { const module = await import("electron-updater"); return (module.default ?? module).autoUpdater as unknown as ElectronAutoUpdater; }, undefined, () => { quitting = false; }, ); let launchUpdateCheckScheduled = false; let localStack: LocalStackController; markOnce("rk:main:module-evaluated"); if (PERFORMANCE_USER_DATA) { app.setPath("userData", PERFORMANCE_USER_DATA); app.setPath("sessionData", path.join(PERFORMANCE_USER_DATA, "session")); } app.once("will-finish-launching", () => markOnce("rk:main:will-finish-launching")); app.once("ready", () => markOnce("rk:main:ready")); // Includes fresh partitions and popup-created sessions, before they load remote content. app.on("session-created", (value) => installSessionPermissions(value, permissionTarget)); function permissionTarget() { if (mainWindow === null || mainWindow.isDestroyed()) return null; const url = appWindowTargets.get(mainWindow); return url === undefined ? null : { webContents: mainWindow.webContents, url }; } function markOnce(name: string) { if (performance.getEntriesByName(name).length === 0) performance.mark(name); } function windowFrom(event: Electron.IpcMainInvokeEvent) { return BrowserWindow.fromWebContents(event.sender); } function fromMainWindow(event: Electron.IpcMainInvokeEvent) { return mainWindow !== null && windowFrom(event) === mainWindow; } /** Dock/taskbar icon for unpackaged launches; packaged builds get theirs from electron-builder. */ function developmentIcon() { if (app.isPackaged) return undefined; const icon = path.join(app.getAppPath(), "assets", developmentIconFile(process.platform)); return existsSync(icon) ? icon : undefined; } function sessionPartitionKey(targetUrl: string) { return sessionPartitionForServerUrl(targetUrl); } function legacyDefaultSessionFlag(partition: string) { return path.join( app.getPath("userData"), `legacy-default-${partition.replace(/[^a-zA-Z0-9_-]/g, "_")}.flag`, ); } /** * Prefer the default session when that origin already has cookies or site * storage there, so upgrades keep localStorage/IndexedDB. Fresh origins get * an isolated partition. */ async function resolveSessionForTarget(targetUrl: string) { const partition = sessionPartitionKey(targetUrl); if (partition === null) { return { partition: null, value: session.defaultSession }; } if (existsSync(legacyDefaultSessionFlag(partition))) { return { partition: null, value: session.defaultSession }; } const origin = safeOrigin(targetUrl); if (origin !== null) { try { const defaultCookies = await session.defaultSession.cookies.get({ url: origin }); if (defaultCookies.length > 0) { await writeFile(legacyDefaultSessionFlag(partition), new Date().toISOString(), "utf8"); return { partition: null, value: session.defaultSession }; } } catch { // Continue with a storage probe when the profile looks pre-partition. } if (defaultSessionProfileExists() && (await defaultSessionHasOriginData(origin))) { try { await writeFile(legacyDefaultSessionFlag(partition), new Date().toISOString(), "utf8"); } catch { // Flag is best-effort; still stay on the default session this launch. } return { partition: null, value: session.defaultSession }; } } return { partition, value: session.fromPartition(partition) }; } function defaultSessionProfileExists() { const root = app.getPath("userData"); return ( existsSync(path.join(root, "Local Storage")) || existsSync(path.join(root, "IndexedDB")) || existsSync(path.join(root, "Cookies")) || existsSync(path.join(root, "Network", "Cookies")) || existsSync(path.join(root, "Cache Storage")) ); } /** Page storage in the default session means a pre-partition install for this origin. */ async function defaultSessionHasOriginData(origin: string): Promise { const probe = new BrowserWindow({ show: false, width: 1, height: 1, webPreferences: { nodeIntegration: false, contextIsolation: true, sandbox: true, }, }); // Increment only after construction succeeds so a throw cannot leave the // counter stuck > 0 and permanently block quit on Windows/Linux. liveProbeWindows++; try { await probe.loadURL(origin); return (await probe.webContents.executeJavaScript(`(async () => { if (localStorage.length > 0 || sessionStorage.length > 0) return true; if (typeof indexedDB !== "undefined" && indexedDB.databases) { try { const databases = await indexedDB.databases(); if (databases.length > 0) return true; } catch {} } if (typeof caches !== "undefined") { try { const keys = await caches.keys(); if (keys.length > 0) return true; } catch {} } return false; })()`)) as boolean; } catch { return false; } finally { if (!probe.isDestroyed()) probe.destroy(); liveProbeWindows--; } } function createWindow(url: string, partition: string | null) { markOnce("rk:main:window-create-start"); const icon = developmentIcon(); const win = new BrowserWindow({ ...browserWindowOptions(process.platform), ...(icon ? { icon } : {}), webPreferences: { preload: path.join(import.meta.dirname, "preload.cjs"), nodeIntegration: false, contextIsolation: true, sandbox: true, ...(partition === null ? {} : { partition }), }, }); mainWindow = win; appWindowTargets.set(win, url); const targetOrigin = safeOrigin(url); // Intentional OAuth flows open the provider's authorize page via a named // window; give those and same-origin popups a normal frame. Everything else // opens in the system browser so a connected server cannot navigate us away. // Hoisted for loopback OAuth capture so MCP/in-app localhost callbacks are skipped. const appOrigin = targetOrigin ?? safeOrigin(url); win.webContents.setWindowOpenHandler(({ url: childUrl, frameName }) => { if (shouldOpenInAppPopup(appOrigin, childUrl, frameName)) { return { action: "allow", overrideBrowserWindowOptions: oauthPopupWindowOptions(), }; } const external = safeExternalUrl(childUrl); if (external !== null) void shell.openExternal(external); return { action: "deny" }; }); win.webContents.on("will-navigate", (event, navigationUrl) => { if (targetOrigin !== null && safeOrigin(navigationUrl) === targetOrigin) return; event.preventDefault(); const external = safeExternalUrl(navigationUrl); if (external !== null) void shell.openExternal(external); }); // The popup has no address bar, so a loopback redirect would otherwise strand // the user on a blank window holding the authorization code in a URL they // cannot read. Capture it here and hand it to the app instead. win.webContents.on("did-create-window", (popup) => { const capture = (details: { preventDefault: () => void; url: string; isMainFrame?: boolean; }) => { // will-redirect can fire for iframes; only the top-level callback counts. if (details.isMainFrame === false) return; const callback = oauthCallbackFrom(details.url, { excludeOrigins: appOrigin !== null ? [appOrigin] : [], }); if (!callback) return; details.preventDefault(); if (!win.isDestroyed()) win.webContents.send("desktop.oauth.callback", callback); if (!popup.isDestroyed()) popup.close(); }; popup.webContents.on("will-redirect", (details) => capture(details)); popup.webContents.on("will-navigate", (details) => capture(details)); }); win.on("close", (event) => { if ( process.platform === "darwin" && !quitting && process.env.RAKAZO_DISABLE_WARM_WINDOW !== "1" ) { event.preventDefault(); win.hide(); clearTimeout(warmWindowTimer); warmWindowTimer = setTimeout(() => { if (mainWindow === win && !win.isDestroyed() && !win.isVisible()) win.destroy(); }, WARM_WINDOW_TTL_MS); } }); win.once("closed", () => { clearTimeout(warmWindowTimer); if (mainWindow === win) mainWindow = null; }); markOnce("rk:main:window-created"); if (win.isVisible()) markOnce("rk:main:window-shown"); win.once("show", () => markOnce("rk:main:window-shown")); win.once("ready-to-show", () => markOnce("rk:main:ready-to-show")); win.webContents.once("dom-ready", () => markOnce("rk:main:dom-ready")); win.webContents.once("did-finish-load", () => markOnce("rk:main:did-finish-load")); win.webContents.once("did-stop-loading", () => markOnce("rk:main:did-stop-loading")); markOnce("rk:main:load-url-start"); const loaded = loadAppUrl(win, url).then( () => markOnce("rk:main:load-url-resolved"), (error: unknown) => { markOnce("rk:main:load-url-rejected"); throw error; }, ); if (!launchUpdateCheckScheduled) { launchUpdateCheckScheduled = true; setTimeout(() => void desktopUpdater.check(false), LAUNCH_CHECK_DELAY_MS).unref(); setInterval(() => void desktopUpdater.check(false), 60 * 60 * 1_000).unref(); } return { loaded, win }; } async function probeDocument(url: string): Promise { // Test/dev harnesses may load data: or file: documents; only probe real servers. if (!url.startsWith("http://") && !url.startsWith("https://")) return null; try { const response = await net.fetch(url, { method: "GET", redirect: "manual", cache: "no-store", credentials: "omit", signal: AbortSignal.timeout(PROBE_TIMEOUT_MS), }); // Allow 3xx (e.g. / → /login); reject hard HTTP errors before opening a window. if (response.status >= 400) { return `The server answered with HTTP ${response.status}.`; } return null; } catch (error) { return probeFailureMessage(error); } } /** * loadURL alone treats many HTTP error documents as success. Reject main-frame * failures, renderer crashes during load, HTTP 4xx/5xx main-frame responses, and * shells that never mount application content. */ function loadAppUrl(win: BrowserWindow, url: string): Promise { const contents = win.webContents; const targetSession = contents.session; let mainStatus: number | undefined; targetSession.webRequest.onCompleted({ urls: ["*://*/*"] }, (details) => { if (details.webContentsId === contents.id && details.resourceType === "mainFrame") { mainStatus = details.statusCode; } }); return new Promise((resolve, reject) => { let settled = false; const settle = (error?: Error) => { if (settled) return; settled = true; contents.removeListener("did-fail-load", onFail); // Keep render-process-gone until document readiness finishes so a crash // during mount still fails the switch. targetSession.webRequest.onCompleted(null); if (error) { contents.removeListener("render-process-gone", onGone); reject(error); return; } if (mainStatus !== undefined && mainStatus >= 400) { contents.removeListener("render-process-gone", onGone); reject(new Error(`The server answered with HTTP ${mainStatus}.`)); return; } void waitForMountedAppDocument(contents) .then(() => { contents.removeListener("render-process-gone", onGone); if (contents.isCrashed()) { reject(new Error("Renderer stopped after load.")); return; } resolve(); }) .catch((inspectError: unknown) => { contents.removeListener("render-process-gone", onGone); reject(inspectError instanceof Error ? inspectError : new Error(String(inspectError))); }); }; const onFail = ( _event: Electron.Event, _errorCode: number, errorDescription: string, _validatedURL: string, isMainFrame: boolean, ) => { if (isMainFrame) settle(new Error(errorDescription || "Page failed to load.")); }; const onGone = (_event: Electron.Event, details: Electron.RenderProcessGoneDetails) => { settle(new Error(`Renderer stopped (${details.reason}).`)); }; contents.on("did-fail-load", onFail); contents.on("render-process-gone", onGone); void contents.loadURL(url).then( async () => { // onCompleted can lag loadURL; wait briefly for the main-frame status. const deadline = Date.now() + 500; while (mainStatus === undefined && Date.now() < deadline) { await new Promise((r) => setTimeout(r, 10)); } settle(); }, (error: unknown) => { settle(error instanceof Error ? error : new Error(String(error))); }, ); }); } /** * Empty `#root` shells and session-pending skeletons count as loaded HTML but are * not a usable app. After session resolves, wait for a bootstrapped shell * (`data-ready` / shell-ready mark) or an auth/welcome/onboarding surface so a * bare Suspense fallback or pre-bootstrap ShellPage cannot pass. Plain e2e * fixtures omit the Rakazo app-state marker. */ async function waitForMountedAppDocument(contents: Electron.WebContents) { const deadline = Date.now() + 8_000; while (Date.now() < deadline) { if (contents.isCrashed()) throw new Error("Renderer stopped after load."); const ready = (await contents.executeJavaScript(`(() => { const appState = document.querySelector("[data-rakazo-app-state]")?.getAttribute("data-rakazo-app-state") ?? null; if (appState === "session-pending") return false; const shell = document.querySelector('[data-testid="shell-root"]'); const shellBootstrapped = Boolean( (shell && shell.getAttribute("data-ready") === "true") || performance.getEntriesByName("rk:renderer:shell-ready").length > 0, ); const authOrWelcomeSurface = Boolean( document.querySelector('[data-rakazo-surface="welcome"]') || document.querySelector( 'form input[type="email"], form input[name="email"], form input#email', ) || Array.from(document.querySelectorAll("button")).some((button) => /sign\\s*in/i.test((button.textContent || "").trim()), ) || document.querySelector( '[aria-label="Model"], [aria-label="Model id"], [aria-label="Models from server"]', ), ); const surfaceReady = shellBootstrapped || authOrWelcomeSurface; const sessionReady = appState === "ready" || performance.getEntriesByName("rk:renderer:session-committed").length > 0; if (sessionReady && surfaceReady) return true; // Desktop e2e fixtures mount a plain page without Rakazo app-state markers. if (appState === null) { const bodyText = (document.body?.innerText || "").trim(); if (bodyText.includes("Opening your Space")) return false; if (bodyText === "Loading…" || bodyText === "Loading...") return false; const mainText = (document.querySelector("main")?.textContent || "").trim(); const rootChildren = document.getElementById("root")?.childElementCount ?? 0; return mainText.length > 0 || rootChildren > 0; } return false; })()`)) as boolean; if (ready) return; await new Promise((r) => setTimeout(r, 50)); } throw new Error("The server page did not become ready."); } async function installBundledRenderer( targetUrl: string, targetSession: Session, partition: string | null, ) { if (!app.isPackaged || process.env.RAKAZO_DISABLE_BUNDLED_RENDERER === "1") return; if (!servesBundledRenderer(targetUrl)) return; const webUrl = new URL(targetUrl); const installationKey = `${partition ?? "default"}:${webUrl.protocol}`; if (bundledRendererInstallations.has(installationKey)) return; const root = path.join(process.resourcesPath, "web"); await targetSession.protocol.handle(webUrl.protocol.slice(0, -1), async (request) => { const forward = () => { return targetSession.fetch(request, forwardedRendererRequestInit(request, webUrl.origin)); }; if (request.method !== "GET" && request.method !== "HEAD") { return forward(); } const acceptsHtml = request.headers.get("accept")?.includes("text/html") ?? false; const candidates = bundledRendererCandidates(root, request.url, webUrl.origin, acceptsHtml); if (!candidates) return forward(); for (const file of candidates) { let body: Buffer | null = null; try { if (request.method === "HEAD") { if (!(await stat(file)).isFile()) continue; } else { body = await readFile(file); } } catch (error) { if (isRendererAssetMiss(error)) continue; throw error; } const headers = new Headers({ "cache-control": immutableRendererAsset(file) ? "public, max-age=31536000, immutable" : "no-cache", "content-type": contentType(file), "x-content-type-options": "nosniff", }); return new Response(body, { headers }); } return forward(); }); bundledRendererInstallations.add(installationKey); markOnce("rk:main:bundled-renderer-ready"); } function oauthPopupWindowOptions() { return { width: 560, height: 720, frame: true, titleBarStyle: "default" as const, autoHideMenuBar: true, backgroundColor: "#07080A", webPreferences: { preload: "", nodeIntegration: false, contextIsolation: true, sandbox: true, }, }; } function createSetupWindow() { const icon = developmentIcon(); const win = new BrowserWindow({ ...setupWindowOptions(process.platform), ...(icon ? { icon } : {}), webPreferences: { preload: path.join(import.meta.dirname, "setup-preload.cjs"), nodeIntegration: false, contextIsolation: true, sandbox: true, // A long pull runs while this window sits behind others; throttled timers would freeze it. backgroundThrottling: false, }, }); setupWindow = win; win.webContents.setWindowOpenHandler(() => ({ action: "deny" })); win.once("closed", () => { if (setupWindow === win) setupWindow = null; // Closing setup without saving restores a connected session (Change Server cancel). restoreAppWindowAfterSetup(); }); void win.loadFile(path.join(import.meta.dirname, "setup.html")); markOnce("rk:main:setup-window-created"); return win; } /** Hide the app while setup is open; do not clear the saved target until a new one opens. */ function showSetupWindow(error: string | null = null) { setupError = error; let win: BrowserWindow; if (setupWindow !== null && !setupWindow.isDestroyed()) { if (error !== null) setupWindow.reload(); win = setupWindow; } else { win = createSetupWindow(); } if (mainWindow !== null && !mainWindow.isDestroyed()) mainWindow.hide(); win.show(); win.focus(); return win; } function restoreAppWindowAfterSetup() { if (quitting) return; if (setupWindow !== null && !setupWindow.isDestroyed()) return; if (mainWindow === null || mainWindow.isDestroyed() || currentTargetUrl === null) return; clearTimeout(warmWindowTimer); mainWindow.show(); mainWindow.focus(); } async function showLocalSettings() { if (settingsWindow && !settingsWindow.isDestroyed()) { settingsWindow.show(); settingsWindow.focus(); return; } if (openingSettings) return; openingSettings = true; try { const url = localStack.webUrl(); const token = await readStackToken(stackDir(app.getPath("userData"))); if (!token || !(await localStack.matchesDesiredStack(url))) { await dialog.showMessageBox({ message: "Start the local server before opening its settings.", type: "info", }); return; } await settingsCleanup; const partition = "local-server-settings"; const targetSession = session.fromPartition(partition); installSessionPermissions(targetSession, () => null); await installBundledRenderer(url, targetSession, partition); const win = new BrowserWindow({ ...browserWindowOptions(process.platform), title: "Local Server Settings", frame: true, titleBarStyle: "default", trafficLightPosition: undefined, webPreferences: { preload: path.join(import.meta.dirname, "preload.cjs"), nodeIntegration: false, contextIsolation: true, sandbox: true, partition, }, }); settingsWindow = win; const origin = new URL(url).origin; settingsTarget = { origin, token }; win.webContents.setWindowOpenHandler(({ url: externalUrl }) => { const external = safeExternalUrl(externalUrl); if (external) void shell.openExternal(external); return { action: "deny" }; }); const preventNavigation = (event: Electron.Event, target: string) => { if (target === `${origin}${LOCAL_SETTINGS_PAGE}`) return; event.preventDefault(); }; win.webContents.on("will-navigate", preventNavigation); win.webContents.on("will-redirect", preventNavigation); win.once("closed", () => { if (settingsWindow === win) { settingsWindow = null; settingsTarget = null; } const protocol = new URL(url).protocol; if (bundledRendererInstallations.delete(`${partition}:${protocol}`)) { targetSession.protocol.unhandle(protocol.slice(0, -1)); } settingsCleanup = targetSession.clearStorageData().catch(() => undefined); }); await win.loadURL(`${origin}${LOCAL_SETTINGS_PAGE}`); } catch { settingsWindow?.close(); await dialog.showMessageBox({ message: "Could not open local server settings. Try again.", type: "error", }); } finally { openingSettings = false; } } function installApplicationMenu() { const localSettings: Electron.MenuItemConstructorOptions = { id: "local-server-settings", label: "Local Server Settings…", accelerator: "CmdOrCtrl+,", click: () => { void showLocalSettings(); }, }; const changeServer: Electron.MenuItemConstructorOptions = { id: "change-rakazo-server", label: "Change Rakazo Server…", accelerator: "CmdOrCtrl+Shift+K", click: () => showSetupWindow(), }; const stopStack: Electron.MenuItemConstructorOptions = { id: "stop-local-stack", label: "Stop Local Stack", // The stack keeps running after quit (bots are always on); this is the explicit off switch. click: () => { if (currentSetup?.mode === "new") void localStack.stop(); }, }; const template: Electron.MenuItemConstructorOptions[] = process.platform === "darwin" ? [ { label: app.name, submenu: [ { role: "about" }, { type: "separator" }, localSettings, changeServer, stopStack, { type: "separator" }, { role: "hide" }, { role: "hideOthers" }, { role: "unhide" }, { type: "separator" }, { role: "quit" }, ], }, { role: "editMenu" }, { role: "windowMenu" }, ] : [ { label: "File", submenu: [ localSettings, changeServer, stopStack, { type: "separator" }, { role: "quit" }, ], }, { role: "editMenu" }, { role: "windowMenu" }, ]; Menu.setApplicationMenu(Menu.buildFromTemplate(template)); } /** Setup IPC must only answer the setup window, never a connected Rakazo server. */ function fromSetupWindow(event: Electron.IpcMainInvokeEvent) { return ( setupWindow !== null && !setupWindow.isDestroyed() && event.sender === setupWindow.webContents ); } async function probeServer(rawUrl: string, signal?: AbortSignal): Promise { const url = normalizeServerUrl(rawUrl); if (url === null) return { ok: false, error: "Enter a valid http:// or https:// address." }; const timeout = AbortSignal.timeout(PROBE_TIMEOUT_MS); try { const response = await net.fetch(`${url}/rpc/health`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ json: {} }), cache: "no-store", credentials: "omit", redirect: "manual", signal: signal ? AbortSignal.any([timeout, signal]) : timeout, }); if (response.status >= 300 && response.status < 400) { return { ok: false, status: response.status, url, error: "That address redirects elsewhere. Enter the final Rakazo server address.", }; } if (!response.ok) { return { ok: false, status: response.status, url, error: `The server answered with HTTP ${response.status}.`, }; } const health = await readProbeJson(response); if (!isRakazoHealth(health)) { return { ok: false, status: response.status, url, error: "That address did not respond like a Rakazo server.", }; } return { ok: true, status: response.status, url, }; } catch (error) { return { ok: false, url, error: probeFailureMessage(error) }; } } /** A public health response is not enough: another checkout may own the same fixed port. */ async function probeManagedStack( rawUrl: string, token: string, signal?: AbortSignal, ): Promise { const url = normalizeServerUrl(rawUrl); // Never put the private stack token on a cleartext LAN or .local hop. if (url === null || !maySendDesktopStackToken(url)) return null; const timeout = AbortSignal.timeout(PROBE_TIMEOUT_MS); try { const response = await net.fetch(`${url}${DESKTOP_STACK_PROBE_PATH}`, { method: "GET", headers: { [DESKTOP_STACK_TOKEN_HEADER]: token }, cache: "no-store", credentials: "omit", redirect: "manual", signal: signal ? AbortSignal.any([timeout, signal]) : timeout, }); if (!response.ok) return null; return desktopStackImageTag(await readProbeJson(response)); } catch { return null; } } function openApp(targetUrl: string) { if (openAppPromise !== null) return openAppPromise; openAppPromise = openAppOnce(targetUrl).finally(() => { openAppPromise = null; }); return openAppPromise; } function openFailureDetail(error: unknown): string { if (error instanceof Error) { const { message } = error; if ( message.startsWith("The server answered with HTTP") || message.startsWith("The server page loaded empty") || message.startsWith("The server page did not become ready") || message.startsWith("Renderer stopped") || message.startsWith("Page failed to load") ) { return message; } } return probeFailureMessage(error); } async function openAppOnce(targetUrl: string) { const target = await resolveSessionForTarget(targetUrl); const previous = mainWindow; let win: BrowserWindow | null = null; try { const documentError = await probeDocument(targetUrl); if (documentError !== null) { throw new Error(documentError); } await installBundledRenderer(targetUrl, target.value, target.partition); const created = createWindow(targetUrl, target.partition); win = created.win; await created.loaded; currentTargetUrl = targetUrl; setupError = null; // Keep the previous window until the caller commits (after setup.json is written). pendingPreviousWindow = previous !== null && !previous.isDestroyed() && previous !== win ? previous : null; if (pendingPreviousWindow !== null) pendingPreviousWindow.hide(); return true; } catch (error) { pendingPreviousWindow = null; // Keep the previous app window so Cancel / close can restore it. if (previous !== null && !previous.isDestroyed()) mainWindow = previous; // Show the setup window BEFORE destroying the failed one: on Windows/Linux, // destroying the last window fires "window-all-closed" -> app.quit() before // showSetupWindow() runs, so the app silently exits instead of showing this error. showSetupWindow(`Could not open that server. ${openFailureDetail(error)}`); if (win !== null && !win.isDestroyed()) win.destroy(); return false; } } /** Drop the previous app window after the new server is opened and persisted. */ function commitPendingAppSwitch() { const previous = pendingPreviousWindow; pendingPreviousWindow = null; if (previous !== null && !previous.isDestroyed() && previous !== mainWindow) previous.destroy(); } /** * Undo an open that could not be persisted. When a prior session exists, restore * it. On first run keep the connected window so the user can retry save. */ function abandonPendingAppSwitch( previousSetup: DesktopSetup | null, previousUrl: string | null, ): "restored" | "kept" { const previous = pendingPreviousWindow; pendingPreviousWindow = null; if (previous !== null && !previous.isDestroyed()) { const failed = mainWindow; if (failed !== null && !failed.isDestroyed() && failed !== previous) failed.destroy(); mainWindow = previous; currentSetup = previousSetup; currentTargetUrl = previousUrl; // If setup was already closed (e.g. during a slow write), make the restored // session visible — otherwise macOS can be left with no shown window. if (setupWindow === null || setupWindow.isDestroyed()) { clearTimeout(warmWindowTimer); previous.show(); previous.focus(); } return "restored"; } return "kept"; } /** Watch for a renderer crash until setup is persisted (or the switch is abandoned). */ function watchRendererUntilCommitted(win: BrowserWindow) { let crashed = false; const onGone = () => { crashed = true; }; win.webContents.once("render-process-gone", onGone); return { crashed: () => crashed || (!win.isDestroyed() && win.webContents.isCrashed()), dispose: () => { if (!win.isDestroyed()) win.webContents.removeListener("render-process-gone", onGone); }, }; } function destroySetupWindow() { const setup = setupWindow; setupWindow = null; if (setup !== null && !setup.isDestroyed()) setup.destroy(); } /** Best-effort restore of setup.json after a failed save that already wrote disk. */ async function rollbackSetupFile(userDataDir: string, previousSetup: DesktopSetup | null) { try { if (previousSetup !== null) await writeSetup(userDataDir, previousSetup); else await clearSetup(userDataDir); } catch { // Disk rollback is best-effort; callers already restored in-memory state when possible. } } /** * After a renderer crash around save: restore prior setup when possible, otherwise * drop the crashed first-run window so setup remains the only recovery surface. */ async function recoverFromCrashedSave( userDataDir: string, previousSetup: DesktopSetup | null, previousUrl: string | null, ): Promise { const outcome = abandonPendingAppSwitch(previousSetup, previousUrl); await rollbackSetupFile(userDataDir, previousSetup); if (outcome === "kept") { if (mainWindow !== null && !mainWindow.isDestroyed()) mainWindow.destroy(); mainWindow = null; currentSetup = previousSetup; currentTargetUrl = previousUrl; } const message = previousSetup !== null ? "Could not open that server. Renderer stopped. The previous instance was restored for the next launch." : "Could not open that server. Renderer stopped."; showSetupWindow(message); return message; } function safeOrigin(targetUrl: string) { try { return new URL(targetUrl).origin; } catch { return null; } } app.whenReady().then(async () => { installSessionPermissions(session.defaultSession, permissionTarget); const userDataDir = app.getPath("userData"); localStack = new LocalStackController({ platform: process.platform, env: process.env, exists: existsSync, run: runDocker, stackDir: stackDir(userDataDir), resourceDir: stackResourceDir({ packaged: app.isPackaged, resourcesPath: process.resourcesPath, appPath: app.getAppPath(), }), localWebUrl: process.env.RAKAZO_LOCAL_WEB_URL?.trim() || (await readStackWebUrl(stackDir(userDataDir), LOCAL_WEB_URL)), imageTag: resolveImageTag({ version: app.getVersion(), packaged: app.isPackaged, override: process.env.RAKAZO_IMAGE_TAG, }), probe: (url, signal, token) => probeManagedStack(url, token, signal), randomHex: (bytes) => randomBytes(bytes).toString("hex"), onState: (state) => { if (setupWindow !== null && !setupWindow.isDestroyed()) { setupWindow.webContents.send("desktop.setup.stack.changed", state); } }, }); currentSetup = await readSetup(userDataDir); const target = resolveStartupTarget({ envUrl: process.env.RAKAZO_WEB_URL, saved: currentSetup, forceSetup: process.env.RAKAZO_FORCE_SETUP === "1", }); if (process.env.RAKAZO_PERFORMANCE_CLEAR_CACHE === "1") { const cacheSessions = new Set([session.defaultSession]); if (target.kind === "app") { cacheSessions.add((await resolveSessionForTarget(target.url)).value); } await Promise.all( [...cacheSessions].flatMap((value) => [value.clearCache(), value.clearCodeCaches({})]), ); markOnce("rk:main:caches-cleared"); } const icon = developmentIcon(); if (process.platform === "darwin" && icon) app.dock?.setIcon(icon); installApplicationMenu(); const browserAuthAttempts = new Map(); const cancelBrowserAuth = () => { for (const attempt of browserAuthAttempts.values()) attempt.abort(); browserAuthAttempts.clear(); }; app.on("before-quit", cancelBrowserAuth); ipcMain.handle("desktop.oauth.open", async (event, url: unknown) => { if ( (!fromMainWindow(event) && !(settingsWindow !== null && windowFrom(event) === settingsWindow)) || event.senderFrame !== event.sender.mainFrame || typeof url !== "string" || url.length > 16_384 ) throw new Error("Invalid sign-in request."); if (browserAuthAttempts.has(url) || browserAuthAttempts.size >= 8) { throw new Error("A sign-in attempt is already active. Cancel it and retry."); } const controller = new AbortController(); browserAuthAttempts.set(url, controller); const stop = () => controller.abort(); const expiry = setTimeout(stop, 10 * 60_000); expiry.unref(); event.sender.once("destroyed", stop); event.sender.once("did-navigate", stop); controller.signal.addEventListener( "abort", () => { clearTimeout(expiry); event.sender.removeListener("destroyed", stop); event.sender.removeListener("did-navigate", stop); if (browserAuthAttempts.get(url) === controller) browserAuthAttempts.delete(url); }, { once: true }, ); try { await openBrowserAuth(url, { signal: controller.signal, onClose: stop, openExternal: (target) => shell.openExternal(target), onCallback: (callback) => { if (!event.sender.isDestroyed()) event.sender.send("desktop.oauth.callback", callback); }, }); } catch { controller.abort(); throw new Error("Could not open browser sign-in. Close other sign-in attempts and retry."); } }); ipcMain.handle("desktop.oauth.cancel", (event, url: unknown) => { if ( (!fromMainWindow(event) && !(settingsWindow !== null && windowFrom(event) === settingsWindow)) || event.senderFrame !== event.sender.mainFrame || typeof url !== "string" ) return; browserAuthAttempts.get(url)?.abort(); }); ipcMain.handle( "desktop.localSettings.request", async (event, pathname: unknown, body: unknown) => { if ( !settingsWindow || windowFrom(event) !== settingsWindow || event.senderFrame !== event.sender.mainFrame || !settingsTarget || event.senderFrame.url !== `${settingsTarget.origin}${LOCAL_SETTINGS_PAGE}` ) { throw new Error("Local settings are not active"); } return requestLocalSettings(settingsTarget, pathname, body, (input, init) => net.fetch(input instanceof URL ? input.href : input, { ...init, bypassCustomProtocolHandlers: true, }), ); }, ); ipcMain.handle("desktop.platform", () => process.platform); ipcMain.handle("desktop.files.pickDirectory", async (event) => { if (!fromMainWindow(event)) return null; const result = await dialog.showOpenDialog(windowFrom(event)!, { properties: ["openDirectory", "createDirectory"], }); if (result.canceled || result.filePaths.length === 0) return null; return result.filePaths[0] ?? null; }); ipcMain.handle("desktop.window.close", (event) => { windowFrom(event)?.close(); }); ipcMain.handle("desktop.window.minimize", (event) => { windowFrom(event)?.minimize(); }); ipcMain.handle("desktop.window.toggleMaximize", (event) => { const win = windowFrom(event); if (!win) return; if (win.isMaximized() || win.isFullScreen()) { win.setFullScreen(false); if (win.isMaximized()) win.unmaximize(); } else { win.maximize(); } }); ipcMain.handle("desktop.window.state", (event) => { const win = windowFrom(event); return { minimized: win?.isMinimized() ?? false, maximized: win?.isMaximized() ?? false, fullScreen: win?.isFullScreen() ?? false, }; }); ipcMain.handle("desktop.update.state", () => desktopUpdater.state()); ipcMain.handle("desktop.update.check", (event) => fromMainWindow(event) ? desktopUpdater.check(true) : desktopUpdater.state(), ); ipcMain.handle("desktop.update.download", (event) => fromMainWindow(event) ? desktopUpdater.download() : desktopUpdater.state(), ); ipcMain.handle("desktop.update.install", async (event) => { if (!fromMainWindow(event) || desktopUpdater.state().phase !== "ready") { return desktopUpdater.state(); } quitting = true; const state = await desktopUpdater.install(); // A failed install stays ready for retry but reports a message. Restore normal // window behavior while the user keeps working after that failure. if (state.phase !== "ready" || state.message !== null) quitting = false; return state; }); ipcMain.handle("desktop.setup.state", (event) => { if (!fromSetupWindow(event)) return null; return { defaultLocalUrl: localStack.webUrl(), saved: currentSetup, error: setupError ?? undefined, }; }); ipcMain.handle("desktop.setup.test", async (event, url: unknown) => { if (!fromSetupWindow(event)) return { ok: false, error: "Setup is not active." }; if (typeof url !== "string") return { ok: false, error: "Enter a server address." }; return probeServer(url); }); ipcMain.handle("desktop.setup.save", async (event, payload: unknown) => { if (!fromSetupWindow(event)) return { ok: false, error: "Setup is not active." }; if (setupSaveInProgress) return { ok: false, error: "A connection attempt is already running." }; setupSaveInProgress = true; const previousSetup = currentSetup; const previousUrl = currentTargetUrl; try { const setup = parseSetupInput(payload); if (setup === null) { return { ok: false, error: "Enter a valid server address. Public servers require HTTPS; a new local instance must use localhost.", }; } // Only open the exact origin selected and authenticated by the managed stack. let openSetup = setup; if (setup.mode === "new") { const managedUrl = managedLocalOpenUrl(setup.serverUrl, localStack.webUrl()); if (managedUrl === null || !(await localStack.matchesDesiredStack())) { return { ok: false, error: "The app-managed Rakazo services are not ready. Retry setup.", }; } openSetup = { mode: "new", serverUrl: managedUrl }; } const reachability = await probeServer(openSetup.serverUrl); if (!reachability.ok) return { ok: false, error: reachability.error }; // Open before persisting so a failed renderer load keeps the last working setup. currentSetup = openSetup; const opened = await openApp(openSetup.serverUrl); if (!opened) { currentSetup = previousSetup; return { ok: false, error: "Could not open that server. The previous instance was left unchanged.", }; } const appWindow = mainWindow; const rendererWatch = appWindow !== null && !appWindow.isDestroyed() ? watchRendererUntilCommitted(appWindow) : null; try { await writeSetup(userDataDir, openSetup); if (rendererWatch?.crashed()) { const message = await recoverFromCrashedSave(userDataDir, previousSetup, previousUrl); return { ok: false, error: message }; } // Commit while the crash listener is still armed. commitPendingAppSwitch(); if (rendererWatch?.crashed()) { const message = await recoverFromCrashedSave(userDataDir, previousSetup, previousUrl); return { ok: false, error: message }; } destroySetupWindow(); // Final check after setup closes — a crash in this gap still rolls back. if (rendererWatch?.crashed()) { const message = await recoverFromCrashedSave(userDataDir, previousSetup, previousUrl); return { ok: false, error: message }; } return { ok: true }; } catch { const outcome = abandonPendingAppSwitch(previousSetup, previousUrl); return { ok: false, error: outcome === "restored" ? "Could not save setup. The previous instance was restored." : "Connected, but could not save setup for the next launch. Try Continue again.", }; } finally { rendererWatch?.dispose(); } } finally { setupSaveInProgress = false; } }); ipcMain.handle("desktop.setup.quit", (event) => { if (fromSetupWindow(event)) app.quit(); }); ipcMain.handle("desktop.setup.openLink", async (event, link: unknown) => { if (!fromSetupWindow(event) || !isDesktopSetupLink(link)) return; await shell.openExternal(DOCKER_INSTALL_LINKS[link]); }); ipcMain.handle("desktop.setup.stack.state", (event) => fromSetupWindow(event) ? localStack.state() : null, ); ipcMain.handle("desktop.setup.stack.start", (event) => { if (!fromSetupWindow(event)) return null; // Respond right away; the setup window polls `stack.state` until a terminal phase. void localStack.start(); return localStack.state(); }); // Register before startup awaits so macOS dock clicks during probe/open are handled. app.on("activate", () => { if (setupWindow !== null && !setupWindow.isDestroyed()) { setupWindow.show(); setupWindow.focus(); return; } if (mainWindow !== null && !mainWindow.isDestroyed()) { clearTimeout(warmWindowTimer); mainWindow.show(); mainWindow.focus(); return; } if (openAppPromise !== null) return; if (currentTargetUrl === null) showSetupWindow(setupError); else void openApp(currentTargetUrl).then((opened) => { if (opened) commitPendingAppSwitch(); }); }); if (target.kind === "setup") { showSetupWindow(); } else if (target.source === "saved") { if (currentSetup?.mode === "new") { const managedUrl = managedLocalOpenUrl(target.url, localStack.webUrl()); const managedStackReady = managedUrl !== null ? await localStack.matchesDesiredStack() : false; if (managedStackReady && managedUrl !== null) { if (await openApp(managedUrl)) { commitPendingAppSwitch(); destroySetupWindow(); } } else { // Missing, stale, foreign, or owned by another process: reconcile the // app-managed stack before any API or computer traffic is allowed. void localStack.start(); showSetupWindow(); } } else { const reachability = await probeServer(target.url); if (reachability.ok) { if (await openApp(target.url)) { commitPendingAppSwitch(); destroySetupWindow(); } } else { showSetupWindow(`Could not reconnect to the saved server. ${reachability.error}`); } } } else { if (await openApp(target.url)) { commitPendingAppSwitch(); destroySetupWindow(); } } }); app.on("window-all-closed", () => { // A hidden session probe (defaultSessionHasOriginData) can be the only window // during startup; its teardown must not quit the app. if (liveProbeWindows > 0) return; if (process.platform !== "darwin") app.quit(); }); app.on("before-quit", () => { quitting = true; clearTimeout(warmWindowTimer); // Containers keep running; only an in-flight pull/up is cut short. localStack?.abort(); });