refactor: migrate docs-dev from storybook to vitepress config and update devcontainer configuration
This commit is contained in:
@@ -1,186 +0,0 @@
|
||||
# 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/<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 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.
|
||||
|
||||
### 🔒 Single Instance Lock & Data Integrity
|
||||
|
||||
The application enforces a **single running instance** via `app.requestSingleInstanceLock()`. If a user attempts to launch a second instance, the duplicate process is terminated immediately and the existing window is restored and focused. This mechanism serves two critical purposes:
|
||||
|
||||
- **Data Integrity**: Prevents race conditions and write conflicts in local databases (IndexedDB/PouchDB) that could arise from concurrent access by multiple Electron processes.
|
||||
- **Resource Efficiency**: Avoids duplicate memory allocation, IPC handler registration, and protocol handler conflicts.
|
||||
|
||||
---
|
||||
|
||||
## 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 `<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 | 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 |
|
||||
@@ -1,380 +0,0 @@
|
||||
[← Back to Root](../../../README.md)
|
||||
|
||||
# Desktop Auto-Update System
|
||||
|
||||
`apps/desktop` utilizes a unified update lifecycle powered by **electron-updater**. This architecture ensures seamless, background delivery of application patches, maintaining version parity across our global user base.
|
||||
|
||||
---
|
||||
|
||||
## 🏗 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.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
%% ─── Styling Definitions (Dark-Mode Friendly Enterprise Palette) ───
|
||||
classDef appEntity fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e3a8a
|
||||
classDef coreEntity fill:#e2e8f0,stroke:#64748b,stroke-width:2px,color:#0f172a
|
||||
classDef ipcBridge fill:#10b981,stroke:#047857,stroke-width:2px,color:#ffffff
|
||||
|
||||
%% ─── Subgraphs ───
|
||||
subgraph MainProcess ["Main Process (src/main/index.ts)"]
|
||||
AUTO[autoUpdater.checkForUpdatesAndNotify]
|
||||
EVENTS{{Update Events: progress, downloaded, error}}
|
||||
SENDER[sendToRenderer]
|
||||
end
|
||||
|
||||
subgraph Preload ["IPC Bridge (src/preload/index.ts)"]
|
||||
EXPOSE{contextBridge.exposeInMainWorld}
|
||||
end
|
||||
|
||||
subgraph Renderer ["Renderer (React App - apps/web)"]
|
||||
HOOK([useElectronUpdater Hook])
|
||||
ACTIONS[UI Actions: Install, Check]
|
||||
end
|
||||
|
||||
%% ─── Flow & Relationships ───
|
||||
AUTO ---> EVENTS
|
||||
EVENTS ---> SENDER
|
||||
SENDER ===>|'updater:*' Event Stream| EXPOSE
|
||||
EXPOSE ===>|electronAPI window object| HOOK
|
||||
HOOK -.->|Reactive State Status and Progress| ACTIONS
|
||||
ACTIONS -.->|ipcRenderer.invoke| EXPOSE
|
||||
EXPOSE -.->|Trigger Update or Install| AUTO
|
||||
|
||||
%% ─── Apply Styles ───
|
||||
class AUTO,EVENTS,SENDER coreEntity;
|
||||
class EXPOSE ipcBridge;
|
||||
class HOOK,ACTIONS appEntity;
|
||||
|
||||
%% ─── Subgraph Backgrounds (Transparent for Native GitHub Support) ───
|
||||
style MainProcess fill:transparent,stroke:#94a3b8,stroke-width:2px,stroke-dasharray: 5 5
|
||||
style Preload fill:transparent,stroke:#10b981,stroke-width:2px,stroke-dasharray: 5 5
|
||||
style Renderer fill:transparent,stroke:#3b82f6,stroke-width:2px,stroke-dasharray: 5 5
|
||||
```
|
||||
|
||||
### 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:**
|
||||
|
||||
```text
|
||||
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](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
|
||||
<?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](http://www.apple.com/DTDs/PropertyList-1.0.dtd)">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
- **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 | Root Cause | Resolution |
|
||||
| -------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Console logs `[AutoUpdater] Startup check failed (possibly offline)` | The machine is offline, or the update server (GitHub/S3/generic) is unreachable. | 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 | Root Cause | Resolution |
|
||||
| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `electron-updater` throws "Cannot find app-update.yml" immediately after launch. | The `publish` block in `electron-builder.yml` is missing or misconfigured. `electron-builder` generates `app-update.yml` only when a valid provider is declared. | 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 | Root Cause | Resolution |
|
||||
| -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| The updater downloads a new version but refuses to apply it, logging a signature validation error. | The application was not signed, or the signing certificate has expired / been revoked. | 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 | Root Cause | Resolution |
|
||||
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Windows and Linux users receive updates, but macOS users see no update prompt. | macOS requires **both** a valid code signature AND Apple notarization. Without notarization, Gatekeeper silently quarantines the update payload. | 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 | Root Cause | Resolution |
|
||||
| ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Users report that the update downloads but fails to install, or the downloaded file is 0 bytes. | The file server is serving update manifests or binaries with incorrect MIME types, or a CDN is caching stale `latest*.yml` files. | 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. |
|
||||
@@ -1,282 +0,0 @@
|
||||
[← Back to Root](../../../README.md)
|
||||
|
||||
# Desktop 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/<target>/dist/` directory, and synchronizes the contents into `apps/desktop/web-dist/`. This bridge directory is then ingested by `electron-builder`.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
%% ─── Styling Definitions (Dark-Mode Friendly Enterprise Palette) ───
|
||||
classDef webApp fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e3a8a
|
||||
classDef bridge fill:#10b981,stroke:#047857,stroke-width:2px,color:#ffffff
|
||||
classDef electronApp fill:#e2e8f0,stroke:#64748b,stroke-width:2px,color:#0f172a
|
||||
classDef finalArtifact fill:#6366f1,stroke:#4338ca,stroke-width:2px,color:#ffffff
|
||||
|
||||
%% ─── Nodes ───
|
||||
SOURCE[(apps/TARGET/dist/)]
|
||||
SCRIPT{copy-web-dist.ts}
|
||||
DEST[(apps/desktop/web-dist/)]
|
||||
BUILDER(electron-builder)
|
||||
OUTPUT([Packaged .app / .exe])
|
||||
|
||||
%% ─── Flow ───
|
||||
SOURCE ===>|Vite Build Output| SCRIPT
|
||||
SCRIPT ===>|Deployment Bridge Prebuild Hook| DEST
|
||||
DEST -.->|files and extraResources| BUILDER
|
||||
BUILDER ===> OUTPUT
|
||||
|
||||
%% ─── Apply Styles ───
|
||||
class SOURCE webApp;
|
||||
class SCRIPT bridge;
|
||||
class DEST,BUILDER electronApp;
|
||||
class OUTPUT finalArtifact;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 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/<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 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:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
%% ─── Styling Definitions ───
|
||||
classDef request fill:#f59e0b,stroke:#b45309,stroke-width:2px,color:#ffffff
|
||||
classDef process fill:#e2e8f0,stroke:#64748b,stroke-width:2px,color:#0f172a
|
||||
classDef decision fill:#10b981,stroke:#047857,stroke-width:2px,color:#ffffff
|
||||
classDef success fill:#3b82f6,stroke:#1d4ed8,stroke-width:2px,color:#ffffff
|
||||
|
||||
%% ─── Nodes ───
|
||||
REQ([Request: app://-/settings/profile])
|
||||
DECODE[Decode URI and Normalize Path]
|
||||
CHECK{File exists in web-dist?}
|
||||
SERVE_FILE[Serve Asset with MIME + CSP]
|
||||
SERVE_FALLBACK[Heuristic Fallback: Serve index.html]
|
||||
REACT([React Router Handles Route])
|
||||
|
||||
%% ─── Flow ───
|
||||
REQ ---> DECODE
|
||||
DECODE ---> CHECK
|
||||
CHECK ===>|YES| SERVE_FILE
|
||||
CHECK -.->|NO| SERVE_FALLBACK
|
||||
SERVE_FALLBACK ---> REACT
|
||||
|
||||
%% ─── Apply Styles ───
|
||||
class REQ request;
|
||||
class DECODE process;
|
||||
class CHECK decision;
|
||||
class SERVE_FILE,SERVE_FALLBACK,REACT success;
|
||||
```
|
||||
|
||||
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 (
|
||||
<ThemeProvider colorScheme={colorScheme} density={density}>
|
||||
- <BrowserRouter>
|
||||
+ <HashRouter>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<Routes>
|
||||
{/* All route definitions remain unchanged */}
|
||||
</Routes>
|
||||
</Suspense>
|
||||
- </BrowserRouter>
|
||||
+ </HashRouter>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
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 `<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:;"
|
||||
/>
|
||||
```
|
||||
|
||||
### 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 `<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 |
|
||||
@@ -1,435 +0,0 @@
|
||||
[← Back to Root](../../../README.md)
|
||||
|
||||
# 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
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
%% ─── Styling Definitions (Dark-Mode Friendly Enterprise Palette) ───
|
||||
classDef trustedLayer fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#0f172a
|
||||
classDef gatewayLayer fill:#f0fdf4,stroke:#10b981,stroke-width:2px,color:#064e3b
|
||||
classDef untrustedLayer fill:#fef2f2,stroke:#ef4444,stroke-width:2px,color:#7f1d1d
|
||||
classDef functionNode fill:#e2e8f0,stroke:#64748b,stroke-width:2px,color:#0f172a
|
||||
|
||||
%% ─── Subgraphs ───
|
||||
subgraph Main ["MAIN PROCESS [Fully Trusted]"]
|
||||
M_DESC["Unrestricted Node.js Privileges"]
|
||||
IPC_MAIN_H[ipcMain.handle]
|
||||
IPC_MAIN_O[ipcMain.on]
|
||||
end
|
||||
|
||||
subgraph Preload ["PRELOAD SCRIPT [Secure Gateway]"]
|
||||
P_DESC["Hermetically Sealed Context (Interface Narrowing)"]
|
||||
CTX_BRIDGE{contextBridge.exposeInMainWorld}
|
||||
end
|
||||
|
||||
subgraph Renderer ["RENDERER [Zero-Trust Environment]"]
|
||||
R_DESC["Standard Browser Sandbox (No Node.js APIs)"]
|
||||
E_API([window.electronAPI])
|
||||
end
|
||||
|
||||
%% ─── Flow & Relationships ───
|
||||
E_API ===>|Only Authorized Vector| CTX_BRIDGE
|
||||
CTX_BRIDGE --->|Transforms to narrow IPC calls| IPC_MAIN_H
|
||||
CTX_BRIDGE --->|Transforms to narrow IPC calls| IPC_MAIN_O
|
||||
|
||||
%% ─── Apply Styles ───
|
||||
class Main trustedLayer;
|
||||
class Preload gatewayLayer;
|
||||
class Renderer untrustedLayer;
|
||||
class M_DESC,P_DESC,R_DESC,IPC_MAIN_H,IPC_MAIN_O functionNode;
|
||||
class CTX_BRIDGE gatewayLayer;
|
||||
class E_API untrustedLayer;
|
||||
|
||||
%% ─── Subgraph Backgrounds (Transparent for Native GitHub Support) ───
|
||||
style Main fill:transparent,stroke:#3b82f6,stroke-width:2px,stroke-dasharray: 5 5
|
||||
style Preload fill:transparent,stroke:#10b981,stroke-width:2px,stroke-dasharray: 5 5
|
||||
style Renderer fill:transparent,stroke:#ef4444,stroke-width:2px,stroke-dasharray: 5 5
|
||||
```
|
||||
|
||||
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<ResultType> => {
|
||||
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<EventDataType>('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<ResultType>;
|
||||
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<string> => {
|
||||
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<string>;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. React — Consume
|
||||
|
||||
```tsx
|
||||
function VersionBadge() {
|
||||
const [version, setVersion] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (window.electronAPI) {
|
||||
window.electronAPI.getAppVersion().then(setVersion);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!version) return null;
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 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 `<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
|
||||
// VIOLATION: Command Injection — Unauthenticated Code Execution
|
||||
ipcMain.handle('run-cmd', (_event, cmd: string) => {
|
||||
exec(cmd); // The renderer controls the command string
|
||||
});
|
||||
```
|
||||
|
||||
**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
|
||||
// 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) => {
|
||||
// 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');
|
||||
});
|
||||
```
|
||||
|
||||
**Principle:** Never trust data originating from the renderer. Validate types, constrain scope, and verify resolved paths.
|
||||
|
||||
---
|
||||
|
||||
### ✅ Return unsubscribe functions — Memory Leak Mitigation
|
||||
|
||||
```typescript
|
||||
// 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);
|
||||
|
||||
// Return an unsubscribe function — critical for React lifecycle
|
||||
return () => {
|
||||
ipcRenderer.removeListener(channel, handler);
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**In React's `useEffect`:**
|
||||
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
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.
|
||||
Reference in New Issue
Block a user