diff --git a/.gitignore b/.gitignore index ce4d407..6f3de28 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,9 @@ public/dist # storybook *storybook.log -storybook-static \ No newline at end of file +storybook-static + +# Electron +out/ +release/ +web-dist/ \ No newline at end of file diff --git a/README.md b/README.md index dfd4f80..6e417a1 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,17 @@ ![Node.js](https://img.shields.io/badge/Node.js-v24.11.1-green?style=flat\&logo=nodedotjs) ![Vite](https://img.shields.io/badge/Vite-Bundler-blue?style=flat\&logo=vite) ![React](https://img.shields.io/badge/React-Framework-cyan?style=flat\&logo=react) +![Electron](https://img.shields.io/badge/Electron-33.x-47848F?style=flat\&logo=electron) ![TypeScript](https://img.shields.io/badge/TypeScript-Language-blue?style=flat\&logo=typescript) ![Vitest](https://img.shields.io/badge/Vitest-Testing-green?style=flat\&logo=vitest) -A **scalable, enterprise-ready frontend monorepo template** built with **Turborepo**, **pnpm**, and **Vite**. +A **scalable, enterprise-ready Web & Desktop monorepo** built with **Turborepo**, **pnpm**, **Vite**, and **Electron**. This repository is designed for long-term maintainability, featuring: * Shared logic and UI libraries * Centralized tooling configuration * Turbo-powered task orchestration and caching +* Native desktop distribution with auto-updates * Dedicated documentation & component playground using Storybook --- @@ -26,6 +28,7 @@ The monorepo is organized into **Apps** (deployable applications) and **Packages . ├── apps/ │ ├── web/ # Main React Application (Vite + TypeScript) +│ ├── desktop/ # Electron Desktop Wrapper (electron-vite) │ └── docs-dev/ # Component Documentation & Playground (Storybook) │ ├── packages/ @@ -48,7 +51,7 @@ The monorepo is organized into **Apps** (deployable applications) and **Packages Ensure your local environment matches the following versions to avoid compatibility issues: -* **Node.js**: `v24.11.1` +* **Node.js**: `v20+` (tested with `v24.11.1`) — required for the `--import tsx` flag used by the desktop prebuild script * **pnpm**: `v8.15.6` (Enforced via the `packageManager` field in `package.json`) @@ -68,22 +71,51 @@ This repository uses **Turborepo** to orchestrate tasks efficiently. All command ### Development -| Command | Description | -| ------------------- | --------------------------------------------------------------------------- | -| `pnpm dev` | Start **all applications** (`web` and `docs-dev`) in parallel | -| `pnpm dev:web` | Start only the **Main Web App** (usually at `http://localhost:5173`) | -| `pnpm dev:docs-dev` | Start **Storybook** for UI development (usually at `http://localhost:6006`) | +| Command | Description | +| -------------------- | --------------------------------------------------------------------------- | +| `pnpm dev` | Start **all applications** (`web` and `docs-dev`) in parallel | +| `pnpm dev:web` | Start only the **Main Web App** (usually at `http://localhost:5173`) | +| `pnpm dev:docs-dev` | Start **Storybook** for UI development (usually at `http://localhost:6006`) | +| `pnpm dev:desktop` | Start the **Web App + Electron** in parallel for desktop development | ### Building & Quality -| Command | Description | -| --------------------- | --------------------------------------------- | -| `pnpm build` | Build all apps and packages using Turbo cache | -| `pnpm build:web` | Build only the web application | -| `pnpm build:docs-dev` | Build only the docs-dev application | -| `pnpm test` | Run unit tests (Vitest) across all packages | -| `pnpm lint` | Run ESLint across the workspace | -| `pnpm format` | Format code using Prettier | +| Command | Description | +| --------------------- | ------------------------------------------------------- | +| `pnpm build` | Build all apps and packages using Turbo cache | +| `pnpm build:web` | Build only the web application | +| `pnpm build:docs-dev` | Build only the docs-dev application | +| `pnpm build:desktop` | Build the web app, then compile the Electron app | +| `pnpm test` | Run unit tests (Vitest) across all packages | +| `pnpm lint` | Run ESLint across the workspace | +| `pnpm format` | Format code using Prettier | + +### 🚀 Desktop Packaging & Distribution + +To package the application into a production-ready installer, use the following commands from the **root directory**: + +| Command | Platform | Output Artifact | +| ---------------------- | ----------- | ------------------------------------------ | +| `pnpm package:desktop` | 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` | + +> [!IMPORTANT] +> **Build Sequence**: All `package:*` commands execute the following pipeline automatically: +> +> 1. **`turbo run build --filter=web`** — Compiles the React SPA into `apps/web/dist/`. +> 2. **`prebuild` hook** — Runs `node --import tsx scripts/copy-web-dist.ts`, which copies `apps/web/dist/` → `apps/desktop/web-dist/`. +> 3. **`electron-builder`** — Bundles `web-dist/` into the packaged app via the `files` and `extraResources` blocks in `electron-builder.yml`. +> +> You do not need to run these steps manually — they are chained via npm scripts. + +> [!WARNING] +> **macOS Code Signing**: To build a distributable macOS app with Auto-Update support, you **must** have an Apple Developer Certificate and provide `CSC_LINK` and `CSC_KEY_PASSWORD` in your environment. Without code signing, macOS Gatekeeper will block the app and auto-updates will fail. See [AUTO_UPDATER.md](apps/desktop/docs/AUTO_UPDATER.md) for details. + +> [!NOTE] +> **Cross-Compilation**: It is highly recommended to build for Windows on a Windows machine and for macOS on a Mac. Cross-compilation (e.g., building `.dmg` on Linux) may fail due to platform-specific toolchain dependencies. Use a CI matrix strategy (e.g., GitHub Actions with `runs-on: [macos-latest, windows-latest, ubuntu-latest]`) for multi-platform releases. + --- @@ -105,7 +137,42 @@ The main consumer-facing application. --- -### 2. `apps/docs-dev` (Storybook) +### 2. `apps/desktop` + +The **Electron desktop wrapper** that embeds `apps/web` for native desktop experiences. + +* In **development**: loads the Vite dev server with full hot reload +* In **production**: serves the static web build via a secure custom `app://` protocol +* Configurable target app via `.env` (can wrap `apps/web`, `apps/docs-dev`, or any future app) + +**Tech Stack**: + +* Electron 33.x +* electron-vite +* electron-builder +* electron-updater + +**Key Capabilities**: + +| Feature | Description | +|---|---| +| 🖨️ Native Printing | Silent and direct printing via secure IPC bridge | +| 🔄 Auto-Updates | Background downloads via GitHub Releases (switchable to S3) | +| 🔒 Secure IPC Bridge | `contextIsolation: true`, `nodeIntegration: false`, `sandbox: true` | +| 🌐 Custom Protocol | `app://` serves static files with SPA routing fallback to `index.html` | +| 🛡️ CORS Bypass | Transparent Origin header rewriting for cloud API calls | + +**Desktop Documentation**: + +| Document | Contents | +|---|---| +| [CONFIGURATION.md](apps/desktop/docs/CONFIGURATION.md) | Target app switching, `app://` protocol internals, HashRouter fallback | +| [AUTO_UPDATER.md](apps/desktop/docs/AUTO_UPDATER.md) | Release workflow, CI/CD variables, provider switching, code signing | +| [IPC_ARCHITECTURE.md](apps/desktop/docs/IPC_ARCHITECTURE.md) | Security model, Three-Step Bridge pattern, extending native features | + +--- + +### 3. `apps/docs-dev` (Storybook) An isolated environment for developing and documenting UI components. @@ -114,7 +181,7 @@ An isolated environment for developing and documenting UI components. --- -### 3. `packages/utils` +### 4. `packages/utils` Shared business logic and reusable utility modules that can be consumed across multiple applications. Fully tested using Vitest. @@ -122,7 +189,7 @@ This package is intended to hold non-UI, cross-cutting logic such as date/time h --- -### 4. `packages/ui` +### 5. `packages/ui` Shared UI component library (Buttons, Inputs, Cards, Layouts). @@ -131,7 +198,7 @@ Shared UI component library (Buttons, Inputs, Cards, Layouts). --- -### 5. `packages/configs` +### 6. `packages/configs` Single source of truth for tooling configuration. @@ -150,7 +217,7 @@ This repository uses **Turborepo caching** for builds, tests, and other artifact To fully clean the workspace (dependencies, build outputs, and Turbo cache): ```bash -rm -rf node_modules **/*/node_modules .turbo **/*/.turbo dist **/*/dist +rm -rf node_modules **/*/node_modules .turbo **/*/.turbo dist **/*/dist out **/*/out web-dist **/*/web-dist release **/*/release ``` --- diff --git a/apps/desktop/README.md b/apps/desktop/README.md new file mode 100644 index 0000000..e8653b7 --- /dev/null +++ b/apps/desktop/README.md @@ -0,0 +1,179 @@ +# Desktop + +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. + +--- + +## Quick Start + +```bash +# From the monorepo root + +# Install dependencies +pnpm install + +# Development (starts both the web dev server and Electron) +pnpm dev:desktop + +# Build for production +pnpm build:desktop + +# Package for distribution +pnpm package:desktop +``` + +--- + +## How It Works + +| Environment | Operational Logic | +|---|---| +| **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. | + +--- + +## Project Structure + +``` +apps/desktop/ +├── 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 bridge: syncs web build → web-dist/ +├── src/ +│ ├── main/ +│ │ └── index.ts # Main process: protocol, CORS, IPC, updater +│ ├── preload/ +│ │ └── index.ts # Secure contextBridge API surface +│ └── 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 +├── tsconfig.preload.json +└── tsconfig.renderer.json +``` + +--- + +## Scripts + +### Development & Build + +| Script | Description | +|---|---| +| `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//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= +> CSC_KEY_PASSWORD= +> APPLE_ID= +> APPLE_APP_SPECIFIC_PASSWORD= +> APPLE_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 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 comprehensive guidance on target app switching, protocol internals, and the HashRouter fallback procedure. + +--- + +## Core Capabilities + +### 🌐 Custom `app://` Protocol + +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. + +### 🖨️ Hardware Bridge + +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. + +--- + +## Hardened Security Perimeter + +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. + +| 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 `` 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 | Scope | +|---|---| +| [CONFIGURATION.md](docs/CONFIGURATION.md) | Target app switching, `app://` protocol internals, HashRouter fallback procedure | +| [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 | diff --git a/apps/desktop/docs/AUTO_UPDATER.md b/apps/desktop/docs/AUTO_UPDATER.md new file mode 100644 index 0000000..2f1ade4 --- /dev/null +++ b/apps/desktop/docs/AUTO_UPDATER.md @@ -0,0 +1,398 @@ +# Auto-Update System + +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 + +- [Reactive Update Flow](#reactive-update-flow) +- [Current Provider: GitHub Releases](#current-provider-github-releases) +- [Release Workflow: The Deterministic Pipeline](#release-workflow-the-deterministic-pipeline) +- [CI/CD Environment Variables](#cicd-environment-variables) +- [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) + +--- + +## Reactive Update Flow + +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() │ +│ │ │ +│ ├─→ '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 Sequence + +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 declared in `electron-builder.yml`: + +```yaml +publish: + provider: github + owner: YOUR_GITHUB_ORG + repo: YOUR_REPO_NAME +``` + +### Operational Mechanics + +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 the platform-specific manifest: `latest.yml` (Windows), `latest-mac.yml` (macOS), or `latest-linux.yml` (Linux). + +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: The Deterministic Pipeline + +To maintain release integrity, follow this deterministic pipeline to synchronize web assets and native binaries. + +### Manual Release + +```bash +# 1. Version bump — semver discipline +cd apps/desktop +npm version patch # or: minor, major + +# 2. Compile web assets +cd ../.. +pnpm build --filter=web + +# 3. Synchronize, compile, and publish +cd apps/desktop +pnpm run prebuild +GH_TOKEN=your_token electron-builder --publish always --config electron-builder.yml +``` + +> [!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. + +### Automated Release (GitHub Actions) + +```yaml +name: Release Desktop + +on: + push: + tags: + - 'desktop-v*' + +jobs: + release: + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - run: pnpm install + - run: pnpm build --filter=web + - run: cd apps/desktop && pnpm run prebuild + - run: cd apps/desktop && pnpm run build + + - name: Publish + run: cd apps/desktop && npx electron-builder --publish always --config electron-builder.yml + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + CSC_LINK: ${{ secrets.CSC_LINK }} + CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} +``` + +--- + +## CI/CD Environment Variables + +| Variable | Required | Platform | Description | +|---|---|---|---| +| `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 | 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 | Passphrase for the Windows certificate. | + +### Configuring Secrets + +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 }}`. + +--- + +## Deployment Strategies + +### 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: + provider: s3 + bucket: your-bucket-name + region: ap-southeast-1 + path: /desktop-releases + acl: private +``` + +**Additional environment variables:** + +| Variable | Description | +|---|---| +| `AWS_ACCESS_KEY_ID` | IAM access key with S3 `PutObject` and `GetObject` permissions | +| `AWS_SECRET_ACCESS_KEY` | IAM secret key | + +**Bucket structure:** +``` +your-bucket/desktop-releases/ + ├── 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. + +### Generic File Server (Self-Hosted) + +For self-hosted infrastructure (Nginx, Caddy, etc.): + +```yaml +publish: + provider: generic + url: https://updates.your-domain.com/desktop +``` + +Your server must host the same directory structure as the S3 layout above. + +> [!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 { + listen 443 ssl; + server_name updates.your-domain.com; + + location /desktop/ { + 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: The Trust Boundary + +> [!WARNING] +> **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 ($99/year Apple Developer Program). +- The `electron-builder.yml` is configured with: + ```yaml + mac: + hardenedRuntime: true + gatekeeperAssess: false + entitlements: build/entitlements.mac.plist + entitlementsInherit: build/entitlements.mac.plist + ``` +- You must create `apps/desktop/build/entitlements.mac.plist`: + ```xml + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + + + ``` +- **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 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 enforced** by the OS for AppImage distribution. +- Optional GPG signing is available for package managers that support it. + +--- + +## Testing Updates in Development + +> [!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. + +### What Happens in Dev Mode + +In `src/main/index.ts`, the `setupAutoUpdaterEvents()` function detects `IS_DEV` and returns early: + +```typescript +if (IS_DEV) { + autoUpdater.autoDownload = false; + return; // Skip event registration — no update server in dev +} +``` + +This means: +- No update check is performed on startup. +- No `electron-updater` events are emitted. +- The `useElectronUpdater()` hook will remain in `idle` status. + +### How to Test Updates + +Auto-update can **only** be fully validated using a **packaged, signed build** distributed through a real update channel: + +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. | diff --git a/apps/desktop/docs/CONFIGURATION.md b/apps/desktop/docs/CONFIGURATION.md new file mode 100644 index 0000000..b2f3533 --- /dev/null +++ b/apps/desktop/docs/CONFIGURATION.md @@ -0,0 +1,249 @@ +# Configuration Guide + +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 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 Orchestration + +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` Declaration + +```env +# 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 Vite development server endpoint for the target application. +DESKTOP_DEV_SERVER_URL=http://localhost:5173 +``` + +### Switching the Target Application + +To redirect the wrapper to a different application — for example, `apps/admin` — modify the configuration and re-execute the build pipeline: + +1. **Update the `.env` declaration:** + ```env + DESKTOP_TARGET_APP=admin + DESKTOP_DEV_SERVER_URL=http://localhost:3001 + ``` + +2. **Verify the target app exports a `build` script** that emits static assets to `dist/`. + +3. **Execute the deterministic build pipeline:** + ```bash + pnpm build --filter=admin && cd apps/desktop && pnpm run build + ``` + +### The Deployment Bridge + +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//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`. + +``` +┌─────────────────────────────┐ Deployment Bridge ┌──────────────────────────┐ +│ apps//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 Registry + +| 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 | +|---|---|---|---| +| **Prebuild** | `copy-web-dist.ts` reads `DESKTOP_TARGET_APP` | `monorepo-root/apps//dist/` → `apps/desktop/web-dist/` | Deployment Bridge: build-time synchronization | +| **Development** | `__dirname` relative traversal from `out/main/` | `apps/desktop/out/main/` → `../../` → `apps/` → `/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 Routing: Overcoming Protocol Constraints + +### The Constraint + +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. + +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 Solution: A Privileged Virtual File System + +The `app://` scheme is a **Privileged Virtual File System** that resolves SPA routing conflicts by implementing a **Heuristic Resource Loader**. It operates as follows: + +``` +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 +``` + +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. + +### 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 + }, + }, +]); +``` + +--- + +## Defense-in-Depth: Multi-Layered Protection + +The custom protocol handler enforces a **multi-layered defense perimeter** that goes beyond standard Electron security defaults. + +| 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 | + +--- + +## 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'; ++ import { HashRouter, Navigate, Route, Routes } from 'react-router-dom'; + + export default function App() { + return ( + +- ++ + Loading...}> + + {/* All route definitions remain unchanged */} + + +- ++ + + ); + } +``` + +All routes transition to hash-based addressing: `#/app/dashboard`, `#/auth/login`, `#/showcase`. + +### Step 2: Decommission the Custom Protocol — Main Process + +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([ ... ]); +``` + +**b)** Delete the entire `registerAppProtocol()` function. + +**c)** Remove the `registerAppProtocol()` invocation inside `app.whenReady()`. + +**d)** Redirect production content loading in `createWindow()`: +```diff + if (IS_DEV) { + mainWindow.loadURL(DEV_SERVER_URL); + mainWindow.webContents.openDevTools({ mode: 'detach' }); + } else { +- mainWindow.loadURL('app://-/index.html'); ++ const webDistPath = getWebDistPath(); ++ mainWindow.loadFile(join(webDistPath, 'index.html')); + } +``` + +**e)** Inject a CSP `` tag into the web app's `index.html`, since the In-Flight Policy Injection layer is no longer available: +```html + +``` + +### Trade-off Analysis + +| Dimension | Custom `app://` Protocol | `file://` + HashRouter | +|---|---|---| +| **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 `` 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 | diff --git a/apps/desktop/docs/IPC_ARCHITECTURE.md b/apps/desktop/docs/IPC_ARCHITECTURE.md new file mode 100644 index 0000000..6923f21 --- /dev/null +++ b/apps/desktop/docs/IPC_ARCHITECTURE.md @@ -0,0 +1,420 @@ +# IPC Architecture & Security Model + +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 + +- [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) + +--- + +## Privilege Separation Model + +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. + +### Trust Level Matrix + +| 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 | + +### Process Topology + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ MAIN PROCESS [Fully Trusted] │ +│ │ +│ 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', { ... }) │ +├───────────── Non-Bypassable Isolation Boundary ─────────────────┤ +│ RENDERER [Zero-Trust Environment] │ +│ │ +│ 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 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. | + +--- + +## Standard Operating Procedure: The Three-Step Bridge + +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. + +> [!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). + +### Step 1: Register the Handler — Main Process + +**File:** `apps/desktop/src/main/index.ts` + +```typescript +// 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; +}); + +// 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); +}); +``` + +**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 — Preload Gateway + +**File:** `apps/desktop/src/preload/index.ts` + +```typescript +const electronAPI = { + // Command pattern exposure + featureAction: (arg1: string, arg2: number): Promise => { + return ipcRenderer.invoke('feature:action', arg1, arg2); + }, + + // Event pattern exposure + featureFire: (data: SomeType): void => { + ipcRenderer.send('feature:fire', data); + }, + + // Main→Renderer push events (with automatic lifecycle cleanup) + onFeatureEvent: createEventSubscription('feature:event'), +}; + +contextBridge.exposeInMainWorld('electronAPI', electronAPI); +``` + +**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: Declare the Interface — React Application + +**File:** `apps/web/src/types/electron.d.ts` + +```typescript +interface ElectronAPI { + // ... existing methods ... + + featureAction: (arg1: string, arg2: number) => Promise; + featureFire: (data: SomeType) => void; + onFeatureEvent: (callback: (data: EventDataType) => void) => () => void; +} +``` + +--- + +## Verified Channel Manifest + +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. + +### Printer Subsystem + +| 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 | + +**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 consumption hook:** `useElectronPrinter()` in `apps/web/src/hooks/use-electron-printer.ts` + +--- + +### Auto-Updater Subsystem + +| 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 handlers:** `setupAutoUpdaterIPC()` + `setupAutoUpdaterEvents()` in `src/main/index.ts` + +**React consumption 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. + +--- + +## Extending the Bridge: Guided Walkthrough + +**Scenario:** Expose the application version to the React UI. + +### 1. Main Process — Register Handler + +```typescript +// In app.whenReady() callback, src/main/index.ts +ipcMain.handle('app:get-version', () => { + return app.getVersion(); +}); +``` + +### 2. Preload Gateway — Expose Method + +```typescript +// Add to the electronAPI object, src/preload/index.ts +const electronAPI = { + // ... existing methods ... + getAppVersion: (): Promise => { + return ipcRenderer.invoke('app:get-version'); + }, +}; +``` + +### 3. TypeScript Interface — Declare Type + +```typescript +// Add to ElectronAPI interface, apps/web/src/types/electron.d.ts +interface ElectronAPI { + // ... existing methods ... + getAppVersion: () => Promise; +} +``` + +### 4. React — Consume + +```tsx +function VersionBadge() { + const [version, setVersion] = useState(''); + + useEffect(() => { + if (window.electronAPI) { + window.electronAPI.getAppVersion().then(setVersion); + } + }, []); + + if (!version) return null; + return v{version}; +} +``` + +### 5. Update This Manifest + +After implementing a new channel, **add it to the Verified Channel Manifest** in this document. Undocumented channels are unauthorized channels. + +--- + +## Critical Audit Checklist: Anti-Patterns + +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 +// VIOLATION: Catastrophic Failure — Total Attack Surface Expansion +contextBridge.exposeInMainWorld('ipc', ipcRenderer); +``` + +**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 +// VIOLATION: Unauthenticated Code Execution +contextBridge.exposeInMainWorld('require', require); +``` + +**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 +// VIOLATION: Catastrophic Failure — Complete Boundary Collapse +new BrowserWindow({ + webPreferences: { nodeIntegration: true, contextIsolation: false } +}); +``` + +**Threat:** Every `