---
name: fantasia-electron-main
description: >-
  Works on Fantasia Archive Electron main process: app lifecycle, window
  management, platform tweaks, native integrations, and ipcMain registration
  (register*Ipc + electron-ipc-bridge channel names). Use when editing
  electron-main.ts, src-electron/mainScripts/, or main-side tests.
---

# Fantasia Archive — Electron main process

## Entry and flow

- **Entry**: **`electron-main.ts`** — Chromium stderr filter, app name/`userData`, Windows DevTools workaround, **`startApp`** (all IPC registrars), native shell menu, **`openAppWindowManager`**. **`startApp()`** before any **`BrowserWindow`** so preload channels exist before renderer load.
- **IPC style**: prefer **`ipcMain.handle`** + **`ipcRenderer.invoke`**. **`sendSync`** last resort only — document exceptions.
- **`userData`**: **`fixAppName()`** in **`appIdentity_manager.ts`**. **`TEST_ENV`** **`components`**/**`e2e`** → **`playwright-user-data`** via **`playwrightIsolatedUserDataDirName.ts`**
- **Modules**: **`src-electron/mainScripts/`** feature folders over growing **`electron-main.ts`**
- **IPC registration**: channel strings **`electron-ipc-bridge.ts`**; handlers **`ipcManagement/register*Ipc.ts`**; **`registerAllFaIpc()`** from **`ipcManagement_manager.ts`**

## IPC payload validation (Zod)

**`ipcMain.handle` args untrusted at runtime** — validate in main before stores/privileged APIs.

- Objects/patches: Zod schema in **`src-electron/shared/`** — reference **`faUserSettingsPatchSchema.ts`**
- Throw on invalid → **`invoke`** rejects; renderer handles
- Single primitives: **`typeof`** + predicate OK (e.g. **`checkIfExternalUrl`**)
- **`registerFaExtraEnvIpc`**: env/harness trust, not preload IPC
- **`zod`** in **`package.json`** **`dependencies`**
- Vitest: **`shared/_tests/`** + registrar tests for invalid payloads

### When Zod not replacing existing code

- External links: one string + URL check
- Window control, app details, devtools: no structured renderer payload
- Future bulk DB APIs: Zod from start

## Testing

- Vitest: **`mainScripts/_tests/`**, per-area **`_tests/`**, **`contentBridgeAPIs/_tests/`**
- After main changes: dev scoped gate + connected **`src-electron/**/_tests`** during edits; **full** **`yarn testbatch:verify`** before commit ([fantasia-dev-scoped-verify](../fantasia-dev-scoped-verify/SKILL.md))

## Renderer sandbox

- **`webPreferences`**: **`sandbox: true`**, **`contextIsolation: true`**, **`nodeIntegration: false`**. Preload → main IPC for privileged work. [Electron Process Sandboxing](https://www.electronjs.org/docs/latest/tutorial/sandbox)

## Window chrome IPC

- **`registerFaWindowControlIpc`**, **`registerFaAppDetailsIpc`** — **`BrowserWindow.fromWebContents(event.sender)`**
- **Preload path**: **`path.resolve(currentDir, …)`** from bundled main chunk — no extra **`..`** assuming subfolder of **`mainScripts/`**

## Packaged DevTools (intentional — do not regress)

**Product law:** installed / **`app.isPackaged`** builds **must** open Chrome DevTools via **Help → Toggle developer tools**, default keybind (**F12** / primary chord), and **`faDevToolsControl`** IPC — same as unpackaged.

- **Registrar**: **`registerFaDevToolsIpc`** (**`FA_DEVTOOLS_IPC`**) → **`openDevTools` / `closeDevTools` / status**. Bridge: **`faDevToolsControlAPI`** → renderer **`toggleDevTools`**.
- **Forbidden**: gating DevTools on **`app.isPackaged`**, **`webPreferences.devTools: false`**, or “security hardening” that no-ops packaged toggle/open/close/status. That regression has shipped more than once; treat as bug, not hardening.
- **Allowed**: keep sandbox / contextIsolation / sender checks elsewhere; DevTools itself stays available after install.
- **Vitest**: **`registerFaDevToolsIpc.vitest.test.ts`** must assert packaged path can open — never re-add “packaged no-op” specs as desired behavior.
- **Smoke**: after Electron packaging, verify menu/keybind still opens DevTools ([fantasia-release-build](../fantasia-release-build/SKILL.md)).

## Security hardening (main)

- **`app://`** — **`registerFaAppProtocolWiring`**: host allowlist + **`path.relative`** guard against traversal outside app root.
- **IPC sender** — privileged mutate / project DB handlers validate **`event.sender`** via **`assertMainWindowSender`** (main window **`webContents.id`**). Failsafe path reply + OS-open keep dedicated checks. **Do not** pair sender checks with packaged DevTools disable — see **Packaged DevTools** above.
- **Navigation** — **`will-navigate`** allowlist: **`app:`** + DEV **`APP_URL`** origin only; foreign http(s) **`preventDefault`** then **`shell.openExternal`** when **`checkIfExternalUrl`**. **`setWindowOpenHandler`** deny. (**`mainWindowCreationWiring.ts`**).
- **`openExternal`** — **`faExternalUrlPredicate`**: block RFC1918 + link-local targets.
- **Project paths** — **`createResolveHardenedFaProjectFilePath`** (**`functions/`**) + **`faProjectFilePathHardeningWiring.ts`** before open/reconnect; failsafe reconnect prefers last-known mirrored path and only accepts a renderer reply that hardens to the same path; packaged builds omit dev **`ELECTRON_MAIN_FILEPATH`** leak.

## Keybind persistence

**`mainScripts/keybinds/`** — overrides, Zod patch, **`registerFaKeybindsIpc`**. See [fantasia-keybinds](../fantasia-keybinds/SKILL.md).

## SQLite and files

- **`.faproject`**: [docs/database/projectDB.md](../../../docs/database/projectDB.md)
- App JSON: **`electron-store`** — [appUserDataKv.md](../../../docs/database/appUserDataKv.md)
- [fantasia-sqlite-main](../fantasia-sqlite-main/SKILL.md)

## Types

Shared types → **`types/`**. See [types-folder.mdc](../../rules/types-folder.mdc).
