refactor: Revise configuration and IPC architecture documentation for clarity and security enhancements

- Updated CONFIGURATION.md to reflect changes in target app orchestration, environment variables, and production routing.
- Enhanced IPC_ARCHITECTURE.md with a focus on privilege separation, standardized operating procedures, and critical audit checklists.
- Added detailed guidelines for extending the IPC bridge and maintaining security integrity.
- Introduced new package scripts for macOS, Windows, and Linux builds in package.json.
This commit is contained in:
Firman Ramdhani
2026-04-06 10:17:32 +07:00
parent 8cac18edf4
commit 75deeece9f
6 changed files with 773 additions and 367 deletions
+97 -45
View File
@@ -1,6 +1,8 @@
# Eigen Desktop
# Desktop
An Electron wrapper for the web applications in this monorepo. Powered by **electron-vite** for development and **electron-builder** for production packaging.
The native gateway for our monorepo applications.
> This package serves as a secure, high-performance Electron wrapper that transforms our web-based assets into first-class desktop experiences. Built on top of **electron-vite** for near-instant development cycles and **electron-builder** for seamless cross-platform distribution.
---
@@ -26,10 +28,10 @@ pnpm package:desktop
## How It Works
| Environment | Behavior |
| Environment | Operational Logic |
|---|---|
| **Development** | Electron loads the Vite dev server (`http://localhost:5173`). Hot reload works normally. |
| **Production** | Electron registers a custom `app://` protocol that serves the static build output of the target web app. SPA client-side routing is fully supported via an `index.html` fallback. |
| **Development** | Bridges the Electron shell with the Vite Dev Server, enabling Hot Module Replacement (HMR) and real-time UI synchronization at `http://localhost:5173`. |
| **Production** | Orchestrates a Secure Custom Protocol (`app://`) to serve optimized static assets, ensuring seamless SPA client-side routing via an intelligent `index.html` fallback mechanism. |
---
@@ -37,22 +39,22 @@ pnpm package:desktop
```
apps/desktop/
├── docs/ # Documentation
│ ├── CONFIGURATION.md # Target app, routing, HashRouter fallback
│ ├── AUTO_UPDATER.md # Release process, CI/CD, code signing
│ └── IPC_ARCHITECTURE.md # Security model, adding new features
├── docs/ # Architecture & Operations Documentation
│ ├── CONFIGURATION.md # Target app switching, routing fallback procedures
│ ├── AUTO_UPDATER.md # Release lifecycle, CI/CD, code signing
│ └── IPC_ARCHITECTURE.md # Security model, extensibility patterns
├── scripts/
│ └── copy-web-dist.ts # Prebuild: copies web app build → web-dist/
│ └── copy-web-dist.ts # Prebuild bridge: syncs web build → web-dist/
├── src/
│ ├── main/
│ │ └── index.ts # Main process: protocol, CORS, IPC, updater
│ │ └── index.ts # Main process: protocol, CORS, IPC, updater
│ ├── preload/
│ │ └── index.ts # Secure contextBridge API
│ │ └── index.ts # Secure contextBridge API surface
│ └── renderer/
│ └── index.html # Renderer shell (CSP reference)
├── .env # Target app configuration
├── electron-builder.yml # Packaging & auto-update config
├── electron-vite.config.ts # Build config (main, preload, renderer)
│ └── index.html # Renderer shell
├── .env # Runtime configuration
├── electron-builder.yml # Packaging & auto-update provider config
├── electron-vite.config.ts # Three-target build config (main, preload, renderer)
├── package.json
├── tsconfig.json
├── tsconfig.main.json
@@ -64,64 +66,114 @@ apps/desktop/
## Scripts
### Development & Build
| Script | Description |
|---|---|
| `pnpm dev` | Start electron-vite dev server |
| `pnpm build` | Compile TypeScript → `out/` |
| `pnpm prebuild` | Copy target web app's `dist/``web-dist/` |
| `pnpm package` | Build + package for current platform |
| `pnpm package:win` | Package for Windows (NSIS) |
| `pnpm package:mac` | Package for macOS (DMG + ZIP) |
| `pnpm package:linux` | Package for Linux (AppImage) |
| `pnpm dev` | Launch the electron-vite development server with live reload |
| `pnpm build` | Compile main, preload, and renderer TypeScript modules `out/` |
| `pnpm prebuild` | Synchronize the target web app's build output via `scripts/copy-web-dist.ts` — copies `apps/<DESKTOP_TARGET_APP>/dist/``web-dist/`. Invoked automatically before `pnpm build`. |
| `pnpm preview` | Preview the compiled Electron app locally without generating a distributable |
### 🚀 Packaging & Distribution
To generate a production-ready installer, execute from **within `apps/desktop/`** or use the root-level `pnpm package:*` commands, which orchestrate the full pipeline automatically:
| Command | Platform | Output Artifact |
|---|---|---|
| `pnpm package` | Current OS | Detects host OS and builds accordingly |
| `pnpm package:mac` | macOS | `.dmg` and `.zip` (supports x64 & arm64) |
| `pnpm package:win` | Windows | `.exe` (NSIS Installer) |
| `pnpm package:linux` | Linux | `.AppImage` |
All artifacts are emitted to the `release/` directory.
> [!IMPORTANT]
> **Deterministic Build Pipeline**: All `package:*` commands strictly enforce a deterministic build pipeline: compiling web assets via Turborepo, synchronizing the output via the `prebuild` bridge (`node --import tsx scripts/copy-web-dist.ts`), and finally generating the native binary through `electron-builder`.
>
> **Running locally within `apps/desktop/`**: These scripts assume the web app has already been compiled. Either run `pnpm build --filter=web` beforehand, or use the root-level `pnpm package:*` commands which handle the complete orchestration.
> [!WARNING]
> **macOS Code Signing**: Distributable macOS builds with Auto-Update capability **require** an Apple Developer Certificate. Provide the following environment variables:
> ```bash
> CSC_LINK=<base64-encoded .p12 certificate>
> CSC_KEY_PASSWORD=<certificate password>
> APPLE_ID=<your apple id>
> APPLE_APP_SPECIFIC_PASSWORD=<app-specific password>
> APPLE_TEAM_ID=<team id>
> ```
> Without valid code signing, macOS Gatekeeper will quarantine the application and `electron-updater` will reject update payloads. See [docs/AUTO_UPDATER.md](docs/AUTO_UPDATER.md) for the complete requirements.
> [!NOTE]
> **Cross-Compilation Advisory**: It is strongly recommended to build for each platform on its native OS. Cross-compilation (e.g., producing `.dmg` on Linux) may fail due to platform-specific toolchain dependencies. For CI, leverage a matrix strategy:
> ```yaml
> strategy:
> matrix:
> os: [macos-latest, windows-latest, ubuntu-latest]
> runs-on: ${{ matrix.os }}
> ```
---
## Configuration
The target web app is configured via `.env`:
The target web application is configured via `.env`:
```env
DESKTOP_TARGET_APP=web
DESKTOP_DEV_SERVER_URL=http://localhost:5173
```
See [docs/CONFIGURATION.md](docs/CONFIGURATION.md) for details on switching target apps and routing fallbacks.
See [docs/CONFIGURATION.md](docs/CONFIGURATION.md) for comprehensive guidance on target app switching, protocol internals, and the HashRouter fallback procedure.
---
## Features
## Core Capabilities
### Custom `app://` Protocol
Serves the web app's static build with SPA routing support. Includes path traversal protection and CSP header injection.
### 🌐 Custom `app://` Protocol
### Hardware Printing
The React app can list printers and trigger print jobs via `window.electronAPI.getPrinters()` and `window.electronAPI.print()`.
Provides a secure file-serving layer with built-in **Path Traversal Protection** and automated **CSP Header Injection**. All requests to unknown paths are intelligently rerouted to `index.html`, enabling React Router to resolve routes client-side without blank screens or 404 errors.
### Auto-Update
Background update checks via GitHub Releases with download progress forwarding to the React UI. See [docs/AUTO_UPDATER.md](docs/AUTO_UPDATER.md).
### 🖨️ Hardware Bridge
### CORS Bypass
API requests from the `app://` origin are transparently handled by stripping non-standard Origin headers and injecting CORS response headers.
Enables granular control over system peripherals — such as printers — through an asynchronous IPC communication layer. The React app can enumerate connected printers and dispatch print jobs via `window.electronAPI.getPrinters()` and `window.electronAPI.print()`, all without exposing native APIs to the renderer.
### 🔄 Auto-Update Engine
A fully managed update lifecycle powered by `electron-updater`. Background download progress is forwarded in real-time to the React UI via IPC event subscriptions, enabling rich notification experiences. See [docs/AUTO_UPDATER.md](docs/AUTO_UPDATER.md).
### 🛡️ CORS Bypass Proxy
A transparent proxy mechanism that handles cross-origin requests by sanitizing non-standard `app://` and `file://` Origin headers on outgoing requests and injecting permissive CORS response headers on incoming responses — allowing seamless integration with cloud APIs without server-side configuration changes.
---
## Security
## Hardened Security Perimeter
- `contextIsolation: true` — preload runs in an isolated context
- `nodeIntegration: false` — no Node.js APIs in the renderer
- `sandbox: true` — Chromium sandbox enabled
- `webSecurity: true` — same-origin policy enforced
- Path traversal protection in the custom protocol handler
- CSP headers injected on all HTML responses
The Desktop Wrapper enforces a **hardened security perimeter**, strictly isolating the Node.js Main Process from the Renderer Context. Our architecture is built upon the principle of **Least Privilege**, ensuring that the web application only interacts with system hardware through a verified, secure IPC bridge.
See [docs/IPC_ARCHITECTURE.md](docs/IPC_ARCHITECTURE.md) for the full security model and how to safely extend the app.
| Setting | Value | Purpose |
|---|---|---|
| `contextIsolation` | `true` | Preload executes in a hermetically sealed JavaScript context |
| `nodeIntegration` | `false` | Zero Node.js API surface exposed to the renderer |
| `sandbox` | `true` | Chromium OS-level sandbox enforced |
| `webSecurity` | `true` | Same-origin policy strictly upheld |
**Defense-in-depth protections in `src/main/index.ts`**:
- **Path Traversal Guard** — The `app://` protocol handler validates all resolved file paths remain within the `web-dist/` boundary using `normalize()` + `startsWith()`. Traversal attempts like `app://-/../../etc/passwd` are met with `403 Forbidden`.
- **CSP Header Injection** — Content-Security-Policy headers are injected as HTTP response headers on every HTML response served by the custom protocol — not via a `<meta>` tag — ensuring they cannot be stripped or bypassed by injected scripts.
- **Origin Sanitization** — `session.defaultSession.webRequest` intercepts all outgoing requests, stripping `app://` / `file://` Origin headers to prevent backend CORS rejections.
- **Navigation Guard** — The `will-navigate` event intercepts and blocks all navigation attempts to URLs outside the `app://` protocol and the authorized dev server origin.
See [docs/IPC_ARCHITECTURE.md](docs/IPC_ARCHITECTURE.md) for the full security model, the Three-Step Bridge pattern, and guidance on safely extending the app with new native features.
---
## Documentation
| Document | Contents |
| Document | Scope |
|---|---|
| [CONFIGURATION.md](docs/CONFIGURATION.md) | Target app switching, `app://` protocol internals, HashRouter fallback procedure |
| [AUTO_UPDATER.md](docs/AUTO_UPDATER.md) | Release workflow, CI/CD variables, S3/generic provider switching, code signing |
| [IPC_ARCHITECTURE.md](docs/IPC_ARCHITECTURE.md) | Security model, Three-Step Bridge pattern, existing IPC channels, extension guide |
| [AUTO_UPDATER.md](docs/AUTO_UPDATER.md) | Release lifecycle, CI/CD variables, provider switching, code signing |
| [IPC_ARCHITECTURE.md](docs/IPC_ARCHITECTURE.md) | Security model, Three-Step Bridge pattern, existing IPC channels, extensibility guide |
+196 -108
View File
@@ -1,63 +1,71 @@
# Auto-Update System
This document covers the Electron auto-update system powered by `electron-updater`, including the release workflow, provider configuration, CI/CD requirements, and code signing.
The Unified Update Lifecycle.
> This document defines the strategic implementation of our cross-platform auto-update system. Powered by **electron-updater**, this architecture ensures seamless, background delivery of application patches, maintaining version parity across our global user base.
---
## Table of Contents
- [Architecture Overview](#architecture-overview)
- [Reactive Update Flow](#reactive-update-flow)
- [Current Provider: GitHub Releases](#current-provider-github-releases)
- [Release Workflow](#release-workflow)
- [Release Workflow: The Deterministic Pipeline](#release-workflow-the-deterministic-pipeline)
- [CI/CD Environment Variables](#cicd-environment-variables)
- [Switching to AWS S3](#switching-to-aws-s3)
- [Switching to a Generic File Server](#switching-to-a-generic-file-server)
- [Code Signing Requirements](#code-signing-requirements)
- [Troubleshooting](#troubleshooting)
- [Deployment Strategies](#deployment-strategies)
- [Code Signing: The Trust Boundary](#code-signing-the-trust-boundary)
- [Testing Updates in Development](#testing-updates-in-development)
- [Diagnostic Runbook](#diagnostic-runbook)
---
## Architecture Overview
## Reactive Update Flow
The auto-update flow involves three layers:
The following diagram illustrates the **Reactive Update Flow**, bridging the Node.js Main Process with the React UI layer through a secure IPC event stream.
```
┌─────────────────────────────────────────────────────────┐
│ Main Process (src/main/index.ts) │
│ │
│ autoUpdater.checkForUpdatesAndNotify() │
Events: checking → available → progress → downloaded
sendToRenderer('updater:*', data)
├─────────────────────────────────────────────────────────┤
Preload (src/preload/index.ts)
│ │
contextBridge: onUpdateAvailable, onDownloadProgress,
│ onUpdateDownloaded, checkForUpdates, │
installUpdate
├─────────────────────────────────────────────────────────┤
Renderer / React (apps/web)
useElectronUpdater() hook
→ status, progress, updateInfo, errorMessage
→ checkForUpdates(), installUpdate()
─────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────
│ Main Process (src/main/index.ts)
│ autoUpdater.checkForUpdatesAndNotify()
├─→ 'checking-for-update'
├─→ 'update-available' → { version, releaseDate }
├─→ 'download-progress' → { percent, bytesPerSecond }
│ ├─→ 'update-downloaded' → { version, releaseNotes } │
└─→ 'error' → { message }
sendToRenderer('updater:*', payload)
├──────────────────────── IPC Bridge ──────────────────────────────┤
Preload (src/preload/index.ts)
│ │
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateAvailable, onDownloadProgress,
onUpdateDownloaded, onUpdateError,
checkForUpdates, installUpdate
})
──────────────────────── Renderer ────────────────────────────────
│ React App (apps/web) │
│ │
│ useElectronUpdater() hook │
│ → Reactive state: status, progress, updateInfo, errorMessage │
│ → Actions: checkForUpdates(), installUpdate() │
└──────────────────────────────────────────────────────────────────┘
```
### Lifecycle
### Lifecycle Sequence
1. **App starts** → After a 3-second delay, `autoUpdater.checkForUpdatesAndNotify()` is called.
2. **Update available** → If `autoDownload` is `true` (default), downloads automatically.
3. **Download progress**`download-progress` events are forwarded to the renderer.
4. **Update downloaded** → The renderer shows a "Restart to Update" prompt.
5. **User clicks install**`autoUpdater.quitAndInstall()` restarts the app with the new version.
1. **App launch** → After a 3-second initialization delay, `autoUpdater.checkForUpdatesAndNotify()` is invoked.
2. **Update detected** → If `autoDownload` is `true` (default), the binary payload downloads in the background.
3. **Progress streaming**`download-progress` events are forwarded to the renderer via IPC in real-time.
4. **Download complete** → The renderer surfaces a "Restart to Update" prompt to the user.
5. **User-initiated install**`autoUpdater.quitAndInstall()` terminates the current process and launches the updated binary.
---
## Current Provider: GitHub Releases
The update provider is configured in `electron-builder.yml`:
The update provider is declared in `electron-builder.yml`:
```yaml
publish:
@@ -66,46 +74,45 @@ publish:
repo: YOUR_REPO_NAME
```
### How it Works
### Operational Mechanics
1. When you run `electron-builder --publish always`, it:
- Builds the app for your target platform.
1. When `electron-builder --publish always` executes, it:
- Compiles the application for the target platform.
- Uploads the installer(s) to a **GitHub Release** tagged with the version from `package.json`.
- Generates and uploads `latest.yml` (Windows), `latest-mac.yml` (macOS), or `latest-linux.yml` (Linux).
- Generates and uploads the platform-specific manifest: `latest.yml` (Windows), `latest-mac.yml` (macOS), or `latest-linux.yml` (Linux).
2. When the packaged app calls `checkForUpdates()`, `electron-updater`:
- Reads `app-update.yml` from the app's `resources/` directory (auto-generated during build).
- Fetches the appropriate `latest*.yml` from the configured GitHub release.
- Compares versions and downloads the update if a newer version exists.
### `app-update.yml`
This file is **automatically generated** by `electron-builder` during the build process. It contains the provider configuration and is placed in the packaged app's `resources/` directory. You do NOT need to create or manage this file manually.
2. When the packaged application calls `checkForUpdates()`, `electron-updater`:
- Reads `app-update.yml` from the app's `resources/` directory (auto-generated during build — never manually created).
- Fetches the appropriate `latest*.yml` manifest from the configured release endpoint.
- Performs a semantic version comparison and initiates the download if a newer version exists.
---
## Release Workflow
## Release Workflow: The Deterministic Pipeline
To maintain release integrity, follow this deterministic pipeline to synchronize web assets and native binaries.
### Manual Release
```bash
# 1. Bump the version
# 1. Version bump — semver discipline
cd apps/desktop
npm version patch # or minor, major
npm version patch # or: minor, major
# 2. Build the web app
# 2. Compile web assets
cd ../..
pnpm build --filter=web
# 3. Build & publish the Electron app
# 3. Synchronize, compile, and publish
cd apps/desktop
pnpm run prebuild
GH_TOKEN=your_token electron-builder --publish always --config electron-builder.yml
```
### Automated Release (GitHub Actions)
> [!CAUTION]
> **Treat `GH_TOKEN` as a critical secret.** It grants write access to your repository's release assets. Never commit it to version control, never log it in CI output, and always inject it via encrypted secrets or a vault.
A typical CI workflow:
### Automated Release (GitHub Actions)
```yaml
name: Release Desktop
@@ -152,30 +159,30 @@ jobs:
| Variable | Required | Platform | Description |
|---|---|---|---|
| `GH_TOKEN` | Yes | All | GitHub personal access token with `repo` scope. Used by electron-builder to create/upload releases. |
| `GH_TOKEN` | Yes | All | GitHub personal access token with `repo` scope. Authorizes release artifact uploads. |
| `CSC_LINK` | macOS/Windows | macOS, Windows | Base64-encoded `.p12` code signing certificate. Generate with: `base64 -i cert.p12 \| pbcopy` |
| `CSC_KEY_PASSWORD` | macOS/Windows | macOS, Windows | Password for the `.p12` certificate. |
| `APPLE_ID` | macOS only | macOS | Apple ID email for notarization. |
| `CSC_KEY_PASSWORD` | macOS/Windows | macOS, Windows | Passphrase for the `.p12` certificate. |
| `APPLE_ID` | macOS only | macOS | Apple ID email for notarization submission. |
| `APPLE_APP_SPECIFIC_PASSWORD` | macOS only | macOS | App-specific password generated at [appleid.apple.com](https://appleid.apple.com). |
| `APPLE_TEAM_ID` | macOS only | macOS | Your Apple Developer Team ID. |
| `WIN_CSC_LINK` | Windows only | Windows | Separate Windows code signing certificate (if different from `CSC_LINK`). |
| `WIN_CSC_KEY_PASSWORD` | Windows only | Windows | Password for the Windows certificate. |
| `WIN_CSC_KEY_PASSWORD` | Windows only | Windows | Passphrase for the Windows certificate. |
### Setting Secrets in GitHub Actions
### Configuring Secrets
1. Go to **Settings → Secrets and variables → Actions** in your repository.
1. Navigate to **Settings → Secrets and variables → Actions** in your GitHub repository.
2. Add each variable as a **Repository secret**.
3. Reference them in workflow files as `${{ secrets.VARIABLE_NAME }}`.
### Setting Variables in Turborepo
In `turbo.json`, the build task already has `env` awareness via `"inputs": ["$TURBO_DEFAULT$", ".env*"]`. For CI-specific variables, pass them through the environment — Turborepo does NOT manage CI secrets.
---
## Switching to AWS S3
## Deployment Strategies
To use a private S3 bucket instead of GitHub Releases, update `electron-builder.yml`:
### AWS S3 (Private Infrastructure)
For enterprise environments requiring private infrastructure, the system can be reconfigured to target an **AWS S3 Bucket** or a **CloudFront Distribution**.
Update `electron-builder.yml`:
```yaml
publish:
@@ -186,34 +193,31 @@ publish:
acl: private
```
### Additional AWS Environment Variables
**Additional environment variables:**
| Variable | Description |
|---|---|
| `AWS_ACCESS_KEY_ID` | IAM access key with S3 write permissions |
| `AWS_ACCESS_KEY_ID` | IAM access key with S3 `PutObject` and `GetObject` permissions |
| `AWS_SECRET_ACCESS_KEY` | IAM secret key |
### S3 Bucket Policy
The bucket must allow public read access to the update files, or you must configure a CloudFront distribution in front of it. `electron-updater` needs to `GET` the `latest*.yml` files without authentication.
Recommended bucket structure:
**Bucket structure:**
```
your-bucket/desktop-releases/
├── latest.yml (Windows)
├── latest-mac.yml (macOS)
├── latest-linux.yml (Linux)
├── latest.yml (Windows manifest)
├── latest-mac.yml (macOS manifest)
├── latest-linux.yml (Linux manifest)
├── EigenDesktop-Setup-0.2.0.exe
├── EigenDesktop-0.2.0.dmg
├── EigenDesktop-0.2.0-mac.zip
└── EigenDesktop-0.2.0.AppImage
```
---
> [!NOTE]
> The bucket must allow public read access to the manifest files (`latest*.yml`), or you must configure a CloudFront distribution. `electron-updater` performs unauthenticated `GET` requests to resolve the latest version.
## Switching to a Generic File Server
### Generic File Server (Self-Hosted)
For a self-hosted server (Nginx, Caddy, etc.):
For self-hosted infrastructure (Nginx, Caddy, etc.):
```yaml
publish:
@@ -221,9 +225,12 @@ publish:
url: https://updates.your-domain.com/desktop
```
Your server must host the same file structure as S3 above. On each release, upload the installer files and `latest*.yml` to the server.
Your server must host the same directory structure as the S3 layout above.
### Nginx Example
> [!IMPORTANT]
> **MIME Type Configuration**: Ensure your file server correctly serves `.yml` files with `text/yaml` and installer binaries with `application/octet-stream`. Incorrect MIME types will cause download corruption or silent update failures.
**Nginx reference:**
```nginx
server {
@@ -234,21 +241,27 @@ server {
alias /var/www/desktop-releases/;
autoindex off;
add_header Cache-Control "no-cache";
# MIME types for update manifests
types {
text/yaml yml;
application/octet-stream exe dmg AppImage zip;
}
}
}
```
---
## Code Signing Requirements
## Code Signing: The Trust Boundary
> [!WARNING]
> **macOS auto-updates will FAIL without code signing.** Apple's Gatekeeper will block unsigned apps, and `electron-updater` will refuse to apply updates to unsigned builds. This is enforced by the OS, not by Electron.
> **Code signing is not merely a requirement — it is the Trust Boundary established by the operating system.** macOS Gatekeeper will explicitly terminate unsigned applications or refuse background updates to maintain system integrity. Windows SmartScreen will display alarming warnings to users. Without valid signatures, `electron-updater` will **reject update payloads entirely**.
### macOS
- Requires an **Apple Developer ID Application** certificate.
- The `electron-builder.yml` is already configured with:
- Requires an **Apple Developer ID Application** certificate ($99/year Apple Developer Program).
- The `electron-builder.yml` is configured with:
```yaml
mac:
hardenedRuntime: true
@@ -259,7 +272,8 @@ server {
- You must create `apps/desktop/build/entitlements.mac.plist`:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
@@ -271,40 +285,114 @@ server {
</dict>
</plist>
```
- **Notarization** is required for macOS 10.15+. Provide `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, and `APPLE_TEAM_ID`.
- **Notarization** is mandatory for macOS 10.15+. Provide `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, and `APPLE_TEAM_ID`.
### Windows
- Requires an **EV Code Signing Certificate** or a standard code signing certificate.
- Without signing, Windows SmartScreen will show a warning to users.
- EV certificates eliminate SmartScreen warnings immediately; standard certificates build reputation over time.
- Requires an **EV Code Signing Certificate** or a standard code signing certificate from a trusted CA.
- Without signing, Windows SmartScreen warns users with "Windows protected your PC" — severely impacting adoption.
- **EV certificates** eliminate SmartScreen warnings immediately; standard certificates build trust reputation over time through Microsoft's telemetry.
### Linux
- Code signing is **not required** for Linux.
- AppImage files work without signatures. However, you can optionally sign with GPG for package managers that support it.
- Code signing is **not enforced** by the OS for AppImage distribution.
- Optional GPG signing is available for package managers that support it.
---
## Troubleshooting
## Testing Updates in Development
### "Update check failed" on startup
> [!NOTE]
> The auto-updater is **intentionally disabled** in development mode to prevent runtime crashes. Setting `forceDevUpdateConfig` requires a `dev-app-update.yml` file, which introduces unnecessary complexity during local development.
- **Cause**: The app is offline, or the update server is unreachable.
- **Impact**: None — the error is caught and logged. The app continues to function normally.
- **Verification**: Check the main process console for `[AutoUpdater] Startup check failed (possibly offline)`.
### What Happens in Dev Mode
### `app-update.yml` not found in production build
In `src/main/index.ts`, the `setupAutoUpdaterEvents()` function detects `IS_DEV` and returns early:
- **Cause**: The `publish` block in `electron-builder.yml` is missing or misconfigured.
- **Fix**: Ensure the `publish` block exists. Run `electron-builder --publish never` first to verify the file is generated in `release/*/resources/app-update.yml`.
```typescript
if (IS_DEV) {
autoUpdater.autoDownload = false;
return; // Skip event registration — no update server in dev
}
```
### "Cannot update: code signature is invalid" (macOS)
This means:
- No update check is performed on startup.
- No `electron-updater` events are emitted.
- The `useElectronUpdater()` hook will remain in `idle` status.
- **Cause**: The app was not signed or the signature is broken.
- **Fix**: Ensure `CSC_LINK` and `CSC_KEY_PASSWORD` are set correctly in CI. Verify with: `codesign --verify --deep --strict release/mac*/EigenDesktop.app`.
### How to Test Updates
### Updates work on Windows/Linux but not macOS
Auto-update can **only** be fully validated using a **packaged, signed build** distributed through a real update channel:
- **Cause**: macOS requires **both** a signed app AND notarization.
- **Fix**: Provide all Apple credential environment variables and ensure the `mac.hardenedRuntime` and entitlements are configured.
1. **Publish v0.1.0** → Package and release a signed build.
2. **Bump to v0.2.0** → Increment the version in `package.json`.
3. **Publish v0.2.0** → Package and release the updated build.
4. **Launch v0.1.0** → The app should detect v0.2.0, download it, and prompt the user to restart.
For rapid iteration, use the `generic` provider pointing to a local Nginx or Python HTTP server:
```yaml
# electron-builder.yml (temporary, for testing)
publish:
provider: generic
url: http://localhost:8080/updates
```
```bash
# Serve the release directory locally
cd apps/desktop/release
python3 -m http.server 8080 --directory .
```
---
## Diagnostic Runbook
### Issue: "Update check failed" on startup
| | |
|---|---|
| **Symptom** | Console logs `[AutoUpdater] Startup check failed (possibly offline)` |
| **Root Cause** | The machine is offline, or the update server (GitHub/S3/generic) is unreachable. |
| **Resolution** | No action required. The error is caught in a `try/catch` block, logged to the console, and the application continues to function normally. The next check will occur on the next app launch. |
---
### Issue: `app-update.yml` not found in production build
| | |
|---|---|
| **Symptom** | `electron-updater` throws "Cannot find app-update.yml" immediately after launch. |
| **Root Cause** | The `publish` block in `electron-builder.yml` is missing or misconfigured. `electron-builder` generates `app-update.yml` only when a valid provider is declared. |
| **Resolution** | Verify the `publish` block exists in `electron-builder.yml`. Run `electron-builder --publish never` and inspect `release/*/resources/app-update.yml` to confirm generation. |
---
### Issue: "Cannot update: code signature is invalid" (macOS)
| | |
|---|---|
| **Symptom** | The updater downloads a new version but refuses to apply it, logging a signature validation error. |
| **Root Cause** | The application was not signed, or the signing certificate has expired / been revoked. |
| **Resolution** | Ensure `CSC_LINK` and `CSC_KEY_PASSWORD` are correctly set in CI. Verify the packaged app with: `codesign --verify --deep --strict release/mac*/Desktop.app`. Re-sign and re-publish if the certificate was rotated. |
---
### Issue: Updates work on Windows/Linux but not macOS
| | |
|---|---|
| **Symptom** | Windows and Linux users receive updates, but macOS users see no update prompt. |
| **Root Cause** | macOS requires **both** a valid code signature AND Apple notarization. Without notarization, Gatekeeper silently quarantines the update payload. |
| **Resolution** | Provide all Apple credential environment variables (`APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, `APPLE_TEAM_ID`) and ensure `hardenedRuntime: true` is set in `electron-builder.yml`. Re-package and re-publish. |
---
### Issue: S3/Generic provider returns corrupted downloads
| | |
|---|---|
| **Symptom** | Users report that the update downloads but fails to install, or the downloaded file is 0 bytes. |
| **Root Cause** | The file server is serving update manifests or binaries with incorrect MIME types, or a CDN is caching stale `latest*.yml` files. |
| **Resolution** | Verify MIME types: `.yml` → `text/yaml`, `.exe`/`.dmg`/`.AppImage`/`.zip` → `application/octet-stream`. Add `Cache-Control: no-cache` headers to `latest*.yml` responses. Invalidate CDN cache after publishing a new release. |
+145 -79
View File
@@ -1,113 +1,182 @@
# Configuration Guide
This document covers how to configure the Electron desktop wrapper, manage the target web application, and handle production SPA routing.
The Blueprint for Runtime Control.
> This guide defines the operational parameters of the Desktop Wrapper. It governs the orchestration of target applications, encapsulates the mechanics of our proprietary production routing, and provides a fail-safe **Break Glass Procedure** for emergency infrastructure transitions.
---
## Table of Contents
- [Target App Configuration](#target-app-configuration)
- [Environment Variables](#environment-variables)
- [Production SPA Routing (Custom `app://` Protocol)](#production-spa-routing-custom-app-protocol)
- [Break Glass: Reverting to `file://` + HashRouter](#break-glass-reverting-to-file--hashrouter)
- [Target App Orchestration](#target-app-orchestration)
- [Environment Variables Registry](#environment-variables-registry)
- [Deterministic Path Resolution](#deterministic-path-resolution)
- [Production Routing: Overcoming Protocol Constraints](#production-routing-overcoming-protocol-constraints)
- [Defense-in-Depth: Multi-Layered Protection](#defense-in-depth-multi-layered-protection)
- [Break Glass Procedure: Disaster Recovery Protocol](#break-glass-procedure-disaster-recovery-protocol)
---
## Target App Configuration
## Target App Orchestration
The desktop app wraps any web application in the monorepo. The target is configured via environment variables in `apps/desktop/.env`.
The Desktop Wrapper is architected to embed **any** web application within the monorepo ecosystem. The target application is resolved at build time through a declarative configuration surface in `apps/desktop/.env`.
### `.env` File
### `.env` Declaration
```env
# The workspace name of the target web app to wrap.
# Must match a directory under apps/ (e.g., "web", "docs-dev", "admin").
# The workspace identifier of the target web application.
# Must correspond to a directory under apps/ (e.g., "web", "docs-dev", "admin").
DESKTOP_TARGET_APP=web
# The dev server URL for the target web app.
# This is the URL that Vite serves during development.
# The Vite development server endpoint for the target application.
DESKTOP_DEV_SERVER_URL=http://localhost:5173
```
### Switching to a Different App
### Switching the Target Application
To wrap `apps/admin` instead of `apps/web`:
To redirect the wrapper to a different application — for example, `apps/admin` — modify the configuration and re-execute the build pipeline:
1. Update `.env`:
1. **Update the `.env` declaration:**
```env
DESKTOP_TARGET_APP=admin
DESKTOP_DEV_SERVER_URL=http://localhost:3001
```
2. Ensure the target app has a `build` script that outputs to `dist/`.
2. **Verify the target app exports a `build` script** that emits static assets to `dist/`.
3. Run the desktop build:
3. **Execute the deterministic build pipeline:**
```bash
pnpm build --filter=admin && cd apps/desktop && pnpm run build
```
The `scripts/copy-web-dist.ts` prebuild script reads `DESKTOP_TARGET_APP` and copies `apps/<target>/dist/` into `apps/desktop/web-dist/`, which is then bundled by electron-builder.
### The Deployment Bridge
### How Path Resolution Works
The `prebuild` hook invokes `scripts/copy-web-dist.ts`, which serves as the **Deployment Bridge** between the web workspace and the native container. It reads `DESKTOP_TARGET_APP`, resolves the corresponding `apps/<target>/dist/` directory, and synchronizes the contents into `apps/desktop/web-dist/`. This bridge directory is then ingested by `electron-builder` via both the `files` and `extraResources` declarations in `electron-builder.yml`.
```
Prebuild (copy-web-dist.ts):
monorepo-root/apps/<DESKTOP_TARGET_APP>/dist/ → apps/desktop/web-dist/
Development (main process):
__dirname (out/main/) → ../../ → apps/ → apps/<target>/dist/
Production (packaged app):
process.resourcesPath → Contents/Resources/web-dist/
┌─────────────────────────────┐ Deployment Bridge ┌──────────────────────────┐
apps/<target>/dist/ │ ─── copy-web-dist.ts ────────→ │ apps/desktop/web-dist/
│ (Vite build output) │ prebuild hook │ (Native container) │
└─────────────────────────────┘ └──────────────────────────┘
electron-builder
files + extraResources
┌──────────────────────┐
│ Packaged .app/.exe │
│ resources/web-dist/ │
└──────────────────────┘
```
---
## Environment Variables
## Environment Variables Registry
| Variable | Default | Used By | Description |
| Variable | Default | Security Scope | Consumer | Description |
|---|---|---|---|---|
| `DESKTOP_TARGET_APP` | `web` | Build-time | `copy-web-dist.ts` | Workspace identifier of the web app to embed |
| `DESKTOP_DEV_SERVER_URL` | `http://localhost:5173` | Runtime (dev) | `src/main/index.ts` | Dev server URL loaded in the Electron window during development |
| `GH_TOKEN` | — | CI/CD | `electron-builder` | GitHub personal access token for publishing releases |
| `CSC_LINK` | — | CI/CD | `electron-builder` | Base64-encoded `.p12` code signing certificate |
| `CSC_KEY_PASSWORD` | — | CI/CD | `electron-builder` | Passphrase for the `.p12` certificate |
| `APPLE_ID` | — | CI/CD (macOS) | `electron-builder` | Apple ID email for notarization submission |
| `APPLE_APP_SPECIFIC_PASSWORD` | — | CI/CD (macOS) | `electron-builder` | App-specific password for notarization |
| `APPLE_TEAM_ID` | — | CI/CD (macOS) | `electron-builder` | Apple Developer Team ID |
> [!IMPORTANT]
> **Build-time** variables are consumed during the `prebuild` phase and baked into the artifact. **Runtime** variables are read by the Electron main process at launch. **CI/CD** variables are secrets injected exclusively in the deployment environment — they must never appear in source control or local `.env` files.
---
## Deterministic Path Resolution
The following matrix defines how the target app's static assets are resolved across every phase of the application lifecycle. Each path is **deterministic** — there is no runtime ambiguity.
| Phase | Resolution Strategy | Resolved Path | Context |
|---|---|---|---|
| `DESKTOP_TARGET_APP` | `web` | `copy-web-dist.ts` | Workspace name of the web app to embed |
| `DESKTOP_DEV_SERVER_URL` | `http://localhost:5173` | `src/main/index.ts` | URL of the target app's Vite dev server |
| `GH_TOKEN` | — | `electron-builder` | GitHub token for publishing releases |
| `CSC_LINK` | — | `electron-builder` | Base64-encoded code signing certificate |
| `CSC_KEY_PASSWORD` | — | `electron-builder` | Password for the signing certificate |
| **Prebuild** | `copy-web-dist.ts` reads `DESKTOP_TARGET_APP` | `monorepo-root/apps/<target>/dist/` `apps/desktop/web-dist/` | Deployment Bridge: build-time synchronization |
| **Development** | `__dirname` relative traversal from `out/main/` | `apps/desktop/out/main/` → `../../` → `apps/` → `<target>/dist/` | Direct filesystem access to the web app's build output |
| **Production** | `process.resourcesPath` | `Contents/Resources/web-dist/` (macOS) / `resources/web-dist/` (Windows/Linux) | OS-specific resource directory within the packaged binary |
> [!NOTE]
> The development path relies on `__dirname` pointing to `apps/desktop/out/main/` at runtime. If electron-vite's output directory is ever reconfigured, this traversal must be updated in `getWebDistPath()` within `src/main/index.ts`.
---
## Production SPA Routing (Custom `app://` Protocol)
## Production Routing: Overcoming Protocol Constraints
### The Problem
### The Constraint
React apps using `BrowserRouter` rely on the server to always return `index.html` for any URL path (e.g., `/dashboard`, `/auth/login`). With Electron's `file://` protocol, requesting `file:///app/dashboard` looks for an actual file at that path — which doesn't exist — resulting in a blank screen or "file not found" error.
React applications using `BrowserRouter` rely on a fundamental server-side contract: **every URL path must return `index.html`**. Paths like `/dashboard`, `/auth/login`, and `/settings/profile` do not correspond to physical files — they are virtual routes resolved entirely by the client-side router.
### The Solution
Electron's default `file://` protocol breaks this contract. Requesting `file:///app/dashboard` triggers a literal filesystem lookup for a file named `dashboard`, which does not exist, resulting in a blank screen or an OS-level "file not found" error.
The main process registers a custom `app://` protocol with a handler that:
### The Solution: A Privileged Virtual File System
1. Receives a request like `app://-/dashboard`.
2. Strips the protocol and hostname to get the path: `dashboard`.
3. Checks if a real file exists at `web-dist/dashboard`.
4. **If yes** → serves the file with the correct MIME type.
5. **If no** → serves `web-dist/index.html` instead (SPA fallback).
The `app://` scheme is a **Privileged Virtual File System** that resolves SPA routing conflicts by implementing a **Heuristic Resource Loader**. It operates as follows:
This allows React Router to handle all client-side routing normally. Deep links, page refreshes, and direct URL entry all work because every unknown path falls back to `index.html`.
```
Request: app://-/settings/profile
├─ Decode URI → "settings/profile"
├─ Normalize + validate path (security boundary check)
├─ Does web-dist/settings/profile exist as a file?
│ ├─ YES → Serve with correct MIME type + CSP headers
│ └─ NO → Heuristic Fallback: serve web-dist/index.html
└─ React Router resolves /settings/profile client-side
```
### Security Measures
If a requested URI does not map to a physical asset, the handler intelligently intercepts the request to serve the `index.html` entry point, allowing React Router to maintain stateful client-side navigation. This ensures that deep links, page refreshes, and direct URL entry all function without modification to the React app's routing configuration.
- **Path traversal protection**: The resolved file path is validated to stay within `web-dist/` using `normalize()` + `startsWith()` check. Requests like `app://-/../../etc/passwd` return `403 Forbidden`.
- **CSP headers**: Content-Security-Policy headers are injected on every HTML response served by the protocol handler.
- **Scheme privileges**: The `app` scheme is registered with `standard: true`, `secure: true`, `supportFetchAPI: true`, and `corsEnabled: true` — making it behave like `https://` to the renderer process.
### Scheme Registration
The scheme **must** be registered synchronously at module load time, before `app.whenReady()`. This is a Chromium requirement — deferred registration will silently fail:
```typescript
protocol.registerSchemesAsPrivileged([
{
scheme: 'app',
privileges: {
standard: true, // Enables URL parsing (host, path, query)
secure: true, // Treated as a secure origin (HTTPS equivalent)
supportFetchAPI: true, // Allows fetch() from this scheme
corsEnabled: true, // Enables CORS for cross-origin requests
stream: true, // Supports streaming responses
},
},
]);
```
---
## Break Glass: Reverting to `file://` + HashRouter
## Defense-in-Depth: Multi-Layered Protection
If the custom `app://` protocol ever causes issues (e.g., a third-party library incompatibility), you can fall back to the standard `file://` protocol with `HashRouter`. This requires two changes.
The custom protocol handler enforces a **multi-layered defense perimeter** that goes beyond standard Electron security defaults.
### Step 1: Switch Router in the React App
| Layer | Technique | Implementation | Threat Mitigated |
|---|---|---|---|
| **I/O Sanitization** | Path traversal guard | `normalize()` + `startsWith()` validation against `web-dist/` boundary | Directory traversal attacks (`../../etc/passwd`) → `403 Forbidden` |
| **In-Flight Policy Injection** | CSP response headers | `Content-Security-Policy` injected as HTTP response headers on every HTML payload | XSS execution via script injection |
| **Cryptographic Isolation** | Privileged scheme registration | `app` scheme registered with `standard`, `secure`, `supportFetchAPI`, `corsEnabled` | Scheme downgrade attacks; the renderer treats `app://` identically to `https://` |
| **Resource Type Validation** | `statSync.isFile()` check | Only regular files are served; directories return the SPA fallback | Information disclosure via directory listing |
| **Origin Sanitization** | CORS bypass proxy | `webRequest.onBeforeSendHeaders` strips `app://` Origin headers on outgoing requests | Backend CORS rejection of non-standard origins |
| **Navigation Confinement** | `will-navigate` guard | Blocks navigation to URLs outside `app://` and the authorized dev server | Phishing via in-app redirect to malicious sites |
In the target web app (e.g., `apps/web/src/apps/index.tsx`):
---
## Break Glass Procedure: Disaster Recovery Protocol
> [!CAUTION]
> **This is a formal Disaster Recovery Protocol.** Execute only if the custom `app://` protocol causes an irrecoverable failure — for example, a critical third-party library that refuses to operate under a non-standard URI scheme. This procedure requires coordinated changes across both the React application and the Electron main process. Estimated recovery time: **15 minutes**.
### Step 1: Switch the Router — React Application
In the target web app's entry point (e.g., `apps/web/src/apps/index.tsx`):
```diff
- import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
@@ -130,29 +199,22 @@ In the target web app (e.g., `apps/web/src/apps/index.tsx`):
}
```
Routes will now use hash-based URLs: `#/app/dashboard`, `#/auth/login`, `#/showcase`.
All routes transition to hash-based addressing: `#/app/dashboard`, `#/auth/login`, `#/showcase`.
### Step 2: Switch to `file://` in the Main Process
### Step 2: Decommission the Custom Protocol — Main Process
In `apps/desktop/src/main/index.ts`:
**a)** Remove the scheme registration at the top of the file:
In `apps/desktop/src/main/index.ts`, execute the following surgical removals:
**a)** Remove the scheme registration block at the top of the file:
```diff
- protocol.registerSchemesAsPrivileged([
- {
- scheme: 'app',
- privileges: { standard: true, secure: true, ... },
- },
- ]);
- protocol.registerSchemesAsPrivileged([ ... ]);
```
**b)** Remove the `registerAppProtocol()` function entirely.
**b)** Delete the entire `registerAppProtocol()` function.
**c)** Remove the `registerAppProtocol()` call in `app.whenReady()`.
**d)** Change the production content loading in `createWindow()`:
**c)** Remove the `registerAppProtocol()` invocation inside `app.whenReady()`.
**d)** Redirect production content loading in `createWindow()`:
```diff
if (IS_DEV) {
mainWindow.loadURL(DEV_SERVER_URL);
@@ -164,20 +226,24 @@ In `apps/desktop/src/main/index.ts`:
}
```
**e)** Add a CSP `<meta>` tag to the web app's `index.html` since there's no protocol handler to inject headers:
**e)** Inject a CSP `<meta>` tag into the web app's `index.html`, since the In-Flight Policy Injection layer is no longer available:
```html
<meta http-equiv="Content-Security-Policy"
content="default-src 'self' file:; script-src 'self' file:; style-src 'self' 'unsafe-inline' file:; connect-src 'self' https:; img-src 'self' file: data: https:; font-src 'self' file: data:;" />
content="default-src 'self' file:; script-src 'self' file:;
style-src 'self' 'unsafe-inline' file:;
connect-src 'self' https:;
img-src 'self' file: data: https:;
font-src 'self' file: data:;" />
```
### Comparison
### Trade-off Analysis
| Aspect | Custom `app://` | `file://` + HashRouter |
| Dimension | Custom `app://` Protocol | `file://` + HashRouter |
|---|---|---|
| URL appearance | `/app/dashboard` | `#/app/dashboard` |
| React Router | `BrowserRouter` (no change) | Must use `HashRouter` |
| Deep linking | Full support | Hash-based |
| Implementation complexity | Higher | Lower |
| CSP delivery | Via response headers | Via `<meta>` tag |
| Third-party compatibility | Rare edge cases | Maximum compatibility |
| **Aesthetic Integrity** | Clean URLs: `/app/dashboard` | Hash prefix: `#/app/dashboard` |
| **Router Compatibility** | `BrowserRouter` — zero changes required | Must migrate to `HashRouter` |
| **Deep Linking** | Full, native-style support | Hash-based only |
| **Protocol-Native Compatibility** | Rare edge cases with non-standard scheme detection | Maximum third-party compatibility |
| **Security Delivery Vector** | CSP via response headers (strongest enforcement) | CSP via `<meta>` tag (bypassable by early script execution) |
| **Implementation Complexity** | Higher (custom protocol handler + security layers) | Lower (no custom protocol infrastructure) |
| **Recovery Time** | — | ~15 minutes, 2 files |
+245 -115
View File
@@ -1,218 +1,243 @@
# IPC Architecture & Security Model
This document explains the security model of the Electron desktop wrapper, documents the existing IPC channels, and provides a step-by-step guide for extending the app with new native features.
The Secure Communication Blueprint.
> This document defines the **Hardened Security Perimeter** and communication topology governing the Desktop Wrapper. Every native capability exposed to the renderer is mediated through a **Non-Bypassable IPC Bridge**, ensuring that the Node.js Main Process remains cryptographically and logically isolated from untrusted web content. Adherence to this document is **mandatory** — deviations constitute security violations subject to immediate remediation.
---
## Table of Contents
- [Security Model](#security-model)
- [The Three-Step Bridge Pattern](#the-three-step-bridge-pattern)
- [Existing IPC Channels](#existing-ipc-channels)
- [Adding a New Feature: Step-by-Step Example](#adding-a-new-feature-step-by-step-example)
- [Anti-Patterns to Avoid](#anti-patterns-to-avoid)
- [Privilege Separation Model](#privilege-separation-model)
- [Standard Operating Procedure: The Three-Step Bridge](#standard-operating-procedure-the-three-step-bridge)
- [Verified Channel Manifest](#verified-channel-manifest)
- [Extending the Bridge: Guided Walkthrough](#extending-the-bridge-guided-walkthrough)
- [Critical Audit Checklist: Anti-Patterns](#critical-audit-checklist-anti-patterns)
- [The Gold Standard for Native Integration](#the-gold-standard-for-native-integration)
---
## Security Model
## Privilege Separation Model
The Electron desktop wrapper enforces a strict security boundary between the main process (Node.js) and the renderer process (web app). This is critical because the renderer runs untrusted web content that could be compromised by XSS, malicious dependencies, or supply chain attacks.
The desktop wrapper enforces a **strict privilege separation** between three execution contexts, each operating under fundamentally different trust levels. This architecture ensures that a compromise in any single layer cannot escalate to full system access.
### Core Principles
### Trust Level Matrix
| Setting | Value | Why |
|---|---|---|
| `contextIsolation` | `true` | The preload script runs in an **isolated JavaScript context**. The renderer cannot access Node.js APIs, `require()`, or the preload's scope. |
| `nodeIntegration` | `false` | Node.js APIs (`fs`, `child_process`, `os`, etc.) are **completely unavailable** in the renderer. |
| `sandbox` | `true` | The renderer process runs in a Chromium sandbox with restricted OS-level access. |
| `webSecurity` | `true` | Same-origin policy is enforced. Cross-origin requests follow standard browser rules. |
| Context | Trust Level | Privilege Scope | Security Guarantee |
|---|---|---|---|
| **Main Process** | Fully Trusted | Unrestricted Node.js access: filesystem, network, printers, OS APIs, child processes | Only code authored by the engineering team executes here |
| **Preload Script** | Controlled | Restricted to `ipcRenderer.invoke()` and `ipcRenderer.send()` — no direct Node.js access | Executes in a **Hermetically Sealed Context** — isolated from both the Main Process globals and the Renderer's DOM |
| **Renderer** | Untrusted | Standard browser sandbox — zero Node.js API surface | Designated as a **Zero-Trust Environment** — may execute third-party code, npm packages, or XSS payloads |
### What This Means in Practice
### Process Topology
```
┌──────────────────────────────────────────────────────────────────┐
Main Process
│ Full Node.js access: filesystem, printers, native APIs, │
│ auto-updater, child processes, network (unrestricted) │
MAIN PROCESS [Fully Trusted]
│ │
ipcMain.handle('channel', handler)
├──────────────────────────────────────────────────────────────────┤
Preload Script
Isolated context. Can use ipcRenderer (send/invoke only).
Exposes a MINIMAL API surface via contextBridge.
UNRESTRICTED PRIVILEGES
│ Filesystem · Network · Printers · Native APIs · Child Processes │
Auto-Updater · OS Integration · System Notifications
ipcMain.handle('channel', handler) ← Command handlers
│ ipcMain.on('channel', handler) ← Event listeners │
│ webContents.send('channel', data) ← Downstream push │
├───────────── Non-Bypassable Isolation Boundary ─────────────────┤
│ PRELOAD SCRIPT [Secure Gateway] │
│ │
│ HERMETICALLY SEALED CONTEXT │
│ Performs Interface Narrowing: transforms broad IPC capabilities │
│ into a minimal, auditable API surface. Acts as the sole │
│ authorized mediator between trusted and untrusted contexts. │
│ │
│ contextBridge.exposeInMainWorld('electronAPI', { ... }) │
├──────────────────────────────────────────────────────────────────┤
Renderer (React App)
│ Standard browser environment. NO Node.js access. │
│ Can ONLY call methods on window.electronAPI. │
│ Cannot access ipcRenderer, require, fs, etc. │
├───────────── Non-Bypassable Isolation Boundary ─────────────────┤
RENDERER [Zero-Trust Environment]
│ │
window.electronAPI.someMethod()
UNTRUSTED WEB CONTENT
│ Standard browser sandbox. Zero access to: require, __dirname, │
│ process, fs, child_process, net, os, ipcRenderer. │
│ │
│ ONLY authorized interaction vector: │
│ window.electronAPI.methodName(args) │
└──────────────────────────────────────────────────────────────────┘
```
The renderer communicates with the main process **only** through the API surface defined in the preload script. This API surface is deliberately narrow — each exposed method does exactly one thing.
The Preload Script functions as a **Secure Gateway** that performs **Interface Narrowing** — it transforms the broad, unrestricted IPC capabilities of the Main Process into a deliberately narrow, type-safe API surface. The renderer communicates with native functionality **exclusively** through this gateway. There are no alternative paths, no escape hatches, and no backdoors.
### Enforcement Configuration
These settings are declared in `BrowserWindow.webPreferences` and are **non-negotiable**:
| Setting | Value | Enforcement |
|---|---|---|
| `contextIsolation` | `true` | The Preload executes in a hermetically sealed V8 context. The renderer **cannot** access `require()`, Node.js globals, or any variable from the preload's scope. |
| `nodeIntegration` | `false` | **Zero** Node.js API surface in the renderer. `fs`, `child_process`, `os`, `net`, and all built-in modules are completely unavailable. |
| `sandbox` | `true` | The renderer process runs inside a **Chromium OS-level sandbox**, restricting system calls and file access at the kernel level. |
| `webSecurity` | `true` | The same-origin policy is **strictly enforced**, preventing cross-origin data exfiltration from the renderer. |
---
## The Three-Step Bridge Pattern
## Standard Operating Procedure: The Three-Step Bridge
Every native feature follows the same three-step pattern:
Every native feature in this architecture **must** follow the Three-Step Bridge — a Standard Operating Procedure (SOP) that ensures traceability, type-safety, and auditability across the entire IPC surface.
### Step 1: Register the Handler in the Main Process
> [!IMPORTANT]
> **Deterministic Synchronization**: Maintaining parity between the Main Process handler, the Preload Gateway exposure, and the TypeScript interface declaration is **mandatory**. A mismatch between any two of the three layers will result in either a **Type-Safety Gap** (silent failures in development) or a **Runtime Regression** (crashes in production).
File: `apps/desktop/src/main/index.ts`
### Step 1: Register the Handler — Main Process
**File:** `apps/desktop/src/main/index.ts`
```typescript
// Use ipcMain.handle for request/response (returns a value)
ipcMain.handle('feature:action', async (_event, arg1, arg2) => {
// Perform the native operation
// COMMAND PATTERN: Use ipcMain.handle for request/response operations
// The handler returns a value to the renderer via a resolved Promise.
ipcMain.handle('feature:action', async (_event, arg1: string, arg2: number) => {
// Validate inputs. Never trust data from the renderer.
if (typeof arg1 !== 'string' || typeof arg2 !== 'number') {
throw new Error('Invalid arguments');
}
const result = await someNativeAPI(arg1, arg2);
return result;
});
// Use ipcMain.on for fire-and-forget (no return value)
ipcMain.on('feature:fire', (_event, data) => {
doSomething(data);
// EVENT PATTERN: Use ipcMain.on for fire-and-forget operations
// No return value — the renderer does not wait for a response.
ipcMain.on('feature:fire', (_event, data: SomeType) => {
performSideEffect(data);
});
```
**Naming convention**: Use `namespace:action` format. Examples: `printer:get-list`, `updater:check`, `fs:read-file`.
**Channel naming convention:** `namespace:action` — examples: `printer:get-list`, `updater:check`, `app:get-version`. Namespaces must be unique, descriptive, and never generic.
### Step 2: Expose via contextBridge in the Preload Script
### Step 2: Expose via contextBridge Preload Gateway
File: `apps/desktop/src/preload/index.ts`
**File:** `apps/desktop/src/preload/index.ts`
```typescript
const electronAPI = {
// For request/response channels
// Command pattern exposure
featureAction: (arg1: string, arg2: number): Promise<ResultType> => {
return ipcRenderer.invoke('feature:action', arg1, arg2);
},
// For fire-and-forget channels
// Event pattern exposure
featureFire: (data: SomeType): void => {
ipcRenderer.send('feature:fire', data);
},
// For main→renderer events (push notifications)
// Main→Renderer push events (with automatic lifecycle cleanup)
onFeatureEvent: createEventSubscription<EventDataType>('feature:event'),
};
contextBridge.exposeInMainWorld('electronAPI', electronAPI);
```
**Rules**:
- Never expose `ipcRenderer` directly.
- Never expose `ipcRenderer.on` — use the `createEventSubscription()` helper that returns an unsubscribe function.
- Always specify TypeScript types for function signatures.
**Non-negotiable rules:**
- **Never** expose `ipcRenderer` directly — this is a **Catastrophic Failure** pattern.
- **Never** expose `ipcRenderer.on` without cleanup — use `createEventSubscription()`, which returns an unsubscribe function for React `useEffect` lifecycle management.
- **Always** declare explicit TypeScript types for all function signatures.
### Step 3: Update TypeScript Declarations in the React App
### Step 3: Declare the Interface — React Application
File: `apps/web/src/types/electron.d.ts`
**File:** `apps/web/src/types/electron.d.ts`
```typescript
interface ElectronAPI {
// ... existing methods ...
// New feature
featureAction: (arg1: string, arg2: number) => Promise<ResultType>;
featureFire: (data: SomeType) => void;
onFeatureEvent: (callback: (data: EventDataType) => void) => () => void;
}
```
All three files must stay in sync. If you add a channel to the main process, you must expose it in the preload and declare it in the type file.
---
## Existing IPC Channels
## Verified Channel Manifest
### Printer Channels
The following is the **complete, authoritative registry** of all authorized IPC channels. These channels are the **only** permitted vectors for native interaction. Any IPC channel not listed here is unauthorized and must be treated as a security anomaly.
| Channel | Direction | Type | Description |
|---|---|---|---|
| `printer:get-list` | Renderer → Main → Renderer | `invoke` / `handle` | Returns `ElectronPrinterInfo[]` — list of all connected printers. |
| `printer:print` | Renderer → Main → Renderer | `invoke` / `handle` | Triggers a print job with given options. Returns `{ success, failureReason? }`. |
### Printer Subsystem
**Main process implementation**: `setupPrinterIPC()` in `src/main/index.ts`
| Channel | Direction | Pattern | Payload | Access Control |
|---|---|---|---|---|
| `printer:get-list` | Renderer → Main → Renderer | `invoke` / `handle` | Returns `ElectronPrinterInfo[]` | Read-only hardware enumeration |
| `printer:print` | Renderer → Main → Renderer | `invoke` / `handle` | Accepts `ElectronPrintOptions`, returns `{ success, failureReason? }` | Controlled hardware invocation |
**Preload exposure**:
**Main process handler:** `setupPrinterIPC()` in `src/main/index.ts`
**Preload Gateway surface:**
```typescript
getPrinters: () => ipcRenderer.invoke('printer:get-list')
print: (options?) => ipcRenderer.invoke('printer:print', options)
```
**React hook**: `useElectronPrinter()` in `apps/web/src/hooks/use-electron-printer.ts`
**React consumption hook:** `useElectronPrinter()` in `apps/web/src/hooks/use-electron-printer.ts`
---
### Auto-Updater Channels
### Auto-Updater Subsystem
| Channel | Direction | Type | Description |
|---|---|---|---|
| `updater:check` | Renderer → Main | `invoke` / `handle` | Triggers a manual update check. Returns the check result. |
| `updater:install` | Renderer → Main | `send` / `on` | Quits the app and installs the downloaded update. |
| `updater:checking` | Main → Renderer | `send` | Emitted when the updater starts checking. |
| `updater:available` | Main → Renderer | `send` | Emitted when an update is found. Payload: `UpdateInfo`. |
| `updater:not-available` | Main → Renderer | `send` | Emitted when the app is up to date. Payload: `UpdateInfo`. |
| `updater:progress` | Main → Renderer | `send` | Emitted during download. Payload: `ProgressInfo`. |
| `updater:downloaded` | Main → Renderer | `send` | Emitted when download completes. Payload: `UpdateInfo`. |
| `updater:error` | Main → Renderer | `send` | Emitted on error. Payload: error message string. |
| Channel | Direction | Pattern | Payload | Access Control |
|---|---|---|---|---|
| `updater:check` | Renderer → Main | `invoke` / `handle` | Returns update check result | Read-only version query |
| `updater:install` | Renderer → Main | `send` / `on` | No payload | Privileged: quits app and installs |
| `updater:checking` | Main → Renderer | `send` | No payload | Status notification |
| `updater:available` | Main → Renderer | `send` | `UpdateInfo { version, releaseDate, releaseNotes }` | Status notification |
| `updater:not-available` | Main → Renderer | `send` | `UpdateInfo` | Status notification |
| `updater:progress` | Main → Renderer | `send` | `ProgressInfo { percent, bytesPerSecond, transferred, total }` | Progress telemetry |
| `updater:downloaded` | Main → Renderer | `send` | `UpdateInfo` | Status notification |
| `updater:error` | Main → Renderer | `send` | Error message string | Error telemetry |
**Main process implementation**: `setupAutoUpdaterIPC()` and `setupAutoUpdaterEvents()` in `src/main/index.ts`
**Main process handlers:** `setupAutoUpdaterIPC()` + `setupAutoUpdaterEvents()` in `src/main/index.ts`
**Preload exposure**: `checkForUpdates()`, `installUpdate()`, `onUpdateAvailable()`, `onDownloadProgress()`, `onUpdateDownloaded()`, `onUpdateError()`, `onUpdateChecking()`, `onUpdateNotAvailable()`
**React consumption hook:** `useElectronUpdater()` in `apps/web/src/hooks/use-electron-updater.ts`
**React hook**: `useElectronUpdater()` in `apps/web/src/hooks/use-electron-updater.ts`
> [!NOTE]
> The `updater:install` channel is the **highest-privilege IPC operation** in the system — it terminates the running process and launches a new binary. It should only be triggered by an explicit user action, never automatically.
---
## Adding a New Feature: Step-by-Step Example
## Extending the Bridge: Guided Walkthrough
**Scenario**: Add a method to read the app's version from the main process.
**Scenario:** Expose the application version to the React UI.
### 1. Main Process
In `src/main/index.ts`, add inside `app.whenReady()`:
### 1. Main Process — Register Handler
```typescript
// In app.whenReady() callback, src/main/index.ts
ipcMain.handle('app:get-version', () => {
return app.getVersion();
});
```
### 2. Preload Script
In `src/preload/index.ts`, add to the `electronAPI` object:
### 2. Preload Gateway — Expose Method
```typescript
// Add to the electronAPI object, src/preload/index.ts
const electronAPI = {
// ... existing methods ...
getAppVersion: (): Promise<string> => {
return ipcRenderer.invoke('app:get-version');
},
};
```
### 3. TypeScript Declarations
In `apps/web/src/types/electron.d.ts`, add to the `ElectronAPI` interface:
### 3. TypeScript Interface — Declare Type
```typescript
// Add to ElectronAPI interface, apps/web/src/types/electron.d.ts
interface ElectronAPI {
// ... existing methods ...
getAppVersion: () => Promise<string>;
}
```
### 4. React Usage
### 4. React — Consume
```tsx
function VersionDisplay() {
function VersionBadge() {
const [version, setVersion] = useState('');
useEffect(() => {
@@ -222,69 +247,174 @@ function VersionDisplay() {
}, []);
if (!version) return null;
return <span>v{version}</span>;
return <span className="version-badge">v{version}</span>;
}
```
### 5. Update This Manifest
After implementing a new channel, **add it to the Verified Channel Manifest** in this document. Undocumented channels are unauthorized channels.
---
## Anti-Patterns to Avoid
## Critical Audit Checklist: Anti-Patterns
### ❌ Never expose `ipcRenderer` directly
The following patterns constitute **critical security violations**. Each one expands the attack surface from "browser-level sandboxed web content" to "unrestricted OS-level code execution." Their presence in production code warrants **immediate incident response**.
---
### ❌ Exposing raw `ipcRenderer`
```typescript
// BAD — gives the renderer unrestricted IPC access
// VIOLATION: Catastrophic Failure — Total Attack Surface Expansion
contextBridge.exposeInMainWorld('ipc', ipcRenderer);
```
### ❌ Never expose `require` or Node.js APIs
**Threat:** The renderer gains **unrestricted IPC access** — it can invoke any channel, including channels that were never intended to be callable from the renderer. A single XSS vulnerability escalates to arbitrary native code execution.
**Classification:** **Total System Compromise**
---
### ❌ Exposing `require` or Node.js APIs
```typescript
// BAD — allows arbitrary code execution from the renderer
// VIOLATION: Unauthenticated Code Execution
contextBridge.exposeInMainWorld('require', require);
```
### ❌ Never use `nodeIntegration: true`
**Threat:** The renderer can `require('child_process').exec('rm -rf /')`. A single XSS vulnerability in *any* dependency — including transitive ones — escalates to **full filesystem access, credential theft, reverse shells, and data exfiltration**.
**Classification:** **Total System Compromise**
---
### ❌ Enabling `nodeIntegration`
```typescript
// BAD — completely disables the security boundary
// VIOLATION: Catastrophic Failure — Complete Boundary Collapse
new BrowserWindow({
webPreferences: { nodeIntegration: true, contextIsolation: false }
});
```
### ❌ Never pass unsanitized IPC data to shell commands
**Threat:** Every `<script>` tag in the renderer — including XSS payloads, compromised npm packages, and injected analytics scripts — gains full Node.js capabilities. The isolation boundary **ceases to exist**.
**Classification:** **Total System Compromise**
---
### ❌ Passing unsanitized IPC data to shell commands
```typescript
// BAD — command injection vulnerability
// VIOLATION: Command Injection — Unauthenticated Code Execution
ipcMain.handle('run-cmd', (_event, cmd: string) => {
exec(cmd); // Attacker can run ANY command
exec(cmd); // The renderer controls the command string
});
```
### ✅ Always validate IPC arguments in the main process
**Threat:** The renderer can execute **arbitrary system commands** with the privileges of the Electron main process (typically the current user). This is the most direct path from XSS to OS-level compromise.
**Classification:** **Unauthenticated Code Execution**
---
### ❌ Registering overly broad IPC channels
```typescript
// GOOD — validate and constrain inputs
// VIOLATION: Attack Surface Expansion — Unrestricted File Read
ipcMain.handle('file:read', (_event, path: string) => {
return readFileSync(path, 'utf-8'); // No validation
});
```
**Threat:** The renderer can read **any file** on the filesystem — SSH keys, environment files, database credentials, browser cookies. Input validation is not optional.
**Classification:** **Sensitive Data Exfiltration**
---
## The Gold Standard for Native Integration
The following patterns represent the **mandatory standard** for all IPC implementations. Adherence is non-negotiable.
### ✅ Validate and constrain all IPC arguments
```typescript
// GOLD STANDARD: Input validation, path confinement, scope restriction
ipcMain.handle('file:read', async (_event, filename: string) => {
// Validate: only allow specific filenames, no path separators
if (filename.includes('/') || filename.includes('\\')) {
// Reject path separators — confine to a single directory
if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
throw new Error('Invalid filename');
}
// Resolve within a controlled directory only
const safePath = join(app.getPath('userData'), 'data', filename);
// Verify the resolved path stays within bounds
if (!safePath.startsWith(join(app.getPath('userData'), 'data'))) {
throw new Error('Path traversal detected');
}
return readFileSync(safePath, 'utf-8');
});
```
### ✅ Always return unsubscribe functions for event listeners
**Principle:** Never trust data originating from the renderer. Validate types, constrain scope, and verify resolved paths.
---
### ✅ Return unsubscribe functions — Memory Leak Mitigation
```typescript
// GOOD — prevents memory leaks in React's useEffect
onSomeEvent: createEventSubscription<DataType>('channel:event')
// GOLD STANDARD: The createEventSubscription helper ensures automatic cleanup
function createEventSubscription<T>(channel: string) {
return (callback: (data: T) => void): (() => void) => {
const handler = (_event: Electron.IpcRendererEvent, data: T) => callback(data);
ipcRenderer.on(channel, handler);
// In React:
// Return an unsubscribe function — critical for React lifecycle
return () => {
ipcRenderer.removeListener(channel, handler);
};
};
}
```
**In React's `useEffect`:**
```tsx
useEffect(() => {
const unsub = window.electronAPI.onSomeEvent((data) => { /* ... */ });
return () => unsub(); // Cleanup on unmount
if (!window.electronAPI) return;
// Subscribe — handler is registered in the Preload's IPC layer
const unsub = window.electronAPI.onSomeEvent((data) => {
setState(data);
});
// Cleanup on unmount — prevents listener accumulation
return () => unsub();
}, []);
```
**Principle:** Without the unsubscribe pattern, every component mount adds a **new IPC listener** that persists after unmount. Over time — especially with React's StrictMode double-mounting in development — this causes **memory leaks**, **duplicate event handling**, and **performance degradation**. The `createEventSubscription` helper enforces automatic, deterministic cleanup tied to React's component lifecycle.
---
### ✅ Gate all Electron calls behind runtime detection
```typescript
// GOLD STANDARD: Environment-safe consumption
function useElectronFeature() {
const isElectron = typeof window !== 'undefined' && !!window.electronAPI;
const doSomething = useCallback(() => {
if (!window.electronAPI) return; // No-op in browser
window.electronAPI.someMethod();
}, []);
return { isElectron, doSomething };
}
```
**Principle:** The React app must run identically in both Electron and standard browser environments. All `window.electronAPI` access must be gated behind a runtime check. Never assume the IPC bridge exists.