cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

1363 lines
48 KiB
TypeScript

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<BrowserWindow, string>();
let setupWindow: BrowserWindow | null = null;
let settingsWindow: BrowserWindow | null = null;
let openingSettings = false;
let settingsCleanup: Promise<void> = Promise.resolve();
let settingsTarget: { origin: string; token: string } | null = null;
const bundledRendererInstallations = new Set<string>();
let currentSetup: DesktopSetup | null = null;
let currentTargetUrl: string | null = null;
let setupError: string | null = null;
let setupSaveInProgress = false;
let openAppPromise: Promise<boolean> | 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<boolean> {
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<string | null> {
// 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<void> {
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<void>((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: "#0B0C0E",
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<DesktopReachability> {
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<string | null> {
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<string> {
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>([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<string, AbortController>();
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.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();
});