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

- Updated CONFIGURATION.md to reflect changes in target app orchestration, environment variables, and production routing.
- Enhanced IPC_ARCHITECTURE.md with a focus on privilege separation, standardized operating procedures, and critical audit checklists.
- Added detailed guidelines for extending the IPC bridge and maintaining security integrity.
- Introduced new package scripts for macOS, Windows, and Linux builds in package.json.
This commit is contained in:
Firman Ramdhani
2026-04-06 10:17:32 +07:00
parent 8cac18edf4
commit 75deeece9f
6 changed files with 773 additions and 367 deletions
+196 -108
View File
@@ -1,63 +1,71 @@
# Auto-Update System
This document covers the Electron auto-update system powered by `electron-updater`, including the release workflow, provider configuration, CI/CD requirements, and code signing.
The Unified Update Lifecycle.
> This document defines the strategic implementation of our cross-platform auto-update system. Powered by **electron-updater**, this architecture ensures seamless, background delivery of application patches, maintaining version parity across our global user base.
---
## Table of Contents
- [Architecture Overview](#architecture-overview)
- [Reactive Update Flow](#reactive-update-flow)
- [Current Provider: GitHub Releases](#current-provider-github-releases)
- [Release Workflow](#release-workflow)
- [Release Workflow: The Deterministic Pipeline](#release-workflow-the-deterministic-pipeline)
- [CI/CD Environment Variables](#cicd-environment-variables)
- [Switching to AWS S3](#switching-to-aws-s3)
- [Switching to a Generic File Server](#switching-to-a-generic-file-server)
- [Code Signing Requirements](#code-signing-requirements)
- [Troubleshooting](#troubleshooting)
- [Deployment Strategies](#deployment-strategies)
- [Code Signing: The Trust Boundary](#code-signing-the-trust-boundary)
- [Testing Updates in Development](#testing-updates-in-development)
- [Diagnostic Runbook](#diagnostic-runbook)
---
## Architecture Overview
## Reactive Update Flow
The auto-update flow involves three layers:
The following diagram illustrates the **Reactive Update Flow**, bridging the Node.js Main Process with the React UI layer through a secure IPC event stream.
```
┌─────────────────────────────────────────────────────────┐
│ Main Process (src/main/index.ts) │
│ │
│ autoUpdater.checkForUpdatesAndNotify() │
Events: checking → available → progress → downloaded
sendToRenderer('updater:*', data)
├─────────────────────────────────────────────────────────┤
Preload (src/preload/index.ts)
│ │
contextBridge: onUpdateAvailable, onDownloadProgress,
│ onUpdateDownloaded, checkForUpdates, │
installUpdate
├─────────────────────────────────────────────────────────┤
Renderer / React (apps/web)
useElectronUpdater() hook
→ status, progress, updateInfo, errorMessage
→ checkForUpdates(), installUpdate()
─────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────
│ Main Process (src/main/index.ts)
│ autoUpdater.checkForUpdatesAndNotify()
├─→ 'checking-for-update'
├─→ 'update-available' → { version, releaseDate }
├─→ 'download-progress' → { percent, bytesPerSecond }
│ ├─→ 'update-downloaded' → { version, releaseNotes } │
└─→ 'error' → { message }
sendToRenderer('updater:*', payload)
├──────────────────────── IPC Bridge ──────────────────────────────┤
Preload (src/preload/index.ts)
│ │
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateAvailable, onDownloadProgress,
onUpdateDownloaded, onUpdateError,
checkForUpdates, installUpdate
})
──────────────────────── Renderer ────────────────────────────────
│ React App (apps/web) │
│ │
│ useElectronUpdater() hook │
│ → Reactive state: status, progress, updateInfo, errorMessage │
│ → Actions: checkForUpdates(), installUpdate() │
└──────────────────────────────────────────────────────────────────┘
```
### Lifecycle
### Lifecycle Sequence
1. **App starts** → After a 3-second delay, `autoUpdater.checkForUpdatesAndNotify()` is called.
2. **Update available** → If `autoDownload` is `true` (default), downloads automatically.
3. **Download progress**`download-progress` events are forwarded to the renderer.
4. **Update downloaded** → The renderer shows a "Restart to Update" prompt.
5. **User clicks install**`autoUpdater.quitAndInstall()` restarts the app with the new version.
1. **App launch** → After a 3-second initialization delay, `autoUpdater.checkForUpdatesAndNotify()` is invoked.
2. **Update detected** → If `autoDownload` is `true` (default), the binary payload downloads in the background.
3. **Progress streaming**`download-progress` events are forwarded to the renderer via IPC in real-time.
4. **Download complete** → The renderer surfaces a "Restart to Update" prompt to the user.
5. **User-initiated install**`autoUpdater.quitAndInstall()` terminates the current process and launches the updated binary.
---
## Current Provider: GitHub Releases
The update provider is configured in `electron-builder.yml`:
The update provider is declared in `electron-builder.yml`:
```yaml
publish:
@@ -66,46 +74,45 @@ publish:
repo: YOUR_REPO_NAME
```
### How it Works
### Operational Mechanics
1. When you run `electron-builder --publish always`, it:
- Builds the app for your target platform.
1. When `electron-builder --publish always` executes, it:
- Compiles the application for the target platform.
- Uploads the installer(s) to a **GitHub Release** tagged with the version from `package.json`.
- Generates and uploads `latest.yml` (Windows), `latest-mac.yml` (macOS), or `latest-linux.yml` (Linux).
- Generates and uploads the platform-specific manifest: `latest.yml` (Windows), `latest-mac.yml` (macOS), or `latest-linux.yml` (Linux).
2. When the packaged app calls `checkForUpdates()`, `electron-updater`:
- Reads `app-update.yml` from the app's `resources/` directory (auto-generated during build).
- Fetches the appropriate `latest*.yml` from the configured GitHub release.
- Compares versions and downloads the update if a newer version exists.
### `app-update.yml`
This file is **automatically generated** by `electron-builder` during the build process. It contains the provider configuration and is placed in the packaged app's `resources/` directory. You do NOT need to create or manage this file manually.
2. When the packaged application calls `checkForUpdates()`, `electron-updater`:
- Reads `app-update.yml` from the app's `resources/` directory (auto-generated during build — never manually created).
- Fetches the appropriate `latest*.yml` manifest from the configured release endpoint.
- Performs a semantic version comparison and initiates the download if a newer version exists.
---
## Release Workflow
## Release Workflow: The Deterministic Pipeline
To maintain release integrity, follow this deterministic pipeline to synchronize web assets and native binaries.
### Manual Release
```bash
# 1. Bump the version
# 1. Version bump — semver discipline
cd apps/desktop
npm version patch # or minor, major
npm version patch # or: minor, major
# 2. Build the web app
# 2. Compile web assets
cd ../..
pnpm build --filter=web
# 3. Build & publish the Electron app
# 3. Synchronize, compile, and publish
cd apps/desktop
pnpm run prebuild
GH_TOKEN=your_token electron-builder --publish always --config electron-builder.yml
```
### Automated Release (GitHub Actions)
> [!CAUTION]
> **Treat `GH_TOKEN` as a critical secret.** It grants write access to your repository's release assets. Never commit it to version control, never log it in CI output, and always inject it via encrypted secrets or a vault.
A typical CI workflow:
### Automated Release (GitHub Actions)
```yaml
name: Release Desktop
@@ -152,30 +159,30 @@ jobs:
| Variable | Required | Platform | Description |
|---|---|---|---|
| `GH_TOKEN` | Yes | All | GitHub personal access token with `repo` scope. Used by electron-builder to create/upload releases. |
| `GH_TOKEN` | Yes | All | GitHub personal access token with `repo` scope. Authorizes release artifact uploads. |
| `CSC_LINK` | macOS/Windows | macOS, Windows | Base64-encoded `.p12` code signing certificate. Generate with: `base64 -i cert.p12 \| pbcopy` |
| `CSC_KEY_PASSWORD` | macOS/Windows | macOS, Windows | Password for the `.p12` certificate. |
| `APPLE_ID` | macOS only | macOS | Apple ID email for notarization. |
| `CSC_KEY_PASSWORD` | macOS/Windows | macOS, Windows | Passphrase for the `.p12` certificate. |
| `APPLE_ID` | macOS only | macOS | Apple ID email for notarization submission. |
| `APPLE_APP_SPECIFIC_PASSWORD` | macOS only | macOS | App-specific password generated at [appleid.apple.com](https://appleid.apple.com). |
| `APPLE_TEAM_ID` | macOS only | macOS | Your Apple Developer Team ID. |
| `WIN_CSC_LINK` | Windows only | Windows | Separate Windows code signing certificate (if different from `CSC_LINK`). |
| `WIN_CSC_KEY_PASSWORD` | Windows only | Windows | Password for the Windows certificate. |
| `WIN_CSC_KEY_PASSWORD` | Windows only | Windows | Passphrase for the Windows certificate. |
### Setting Secrets in GitHub Actions
### Configuring Secrets
1. Go to **Settings → Secrets and variables → Actions** in your repository.
1. Navigate to **Settings → Secrets and variables → Actions** in your GitHub repository.
2. Add each variable as a **Repository secret**.
3. Reference them in workflow files as `${{ secrets.VARIABLE_NAME }}`.
### Setting Variables in Turborepo
In `turbo.json`, the build task already has `env` awareness via `"inputs": ["$TURBO_DEFAULT$", ".env*"]`. For CI-specific variables, pass them through the environment — Turborepo does NOT manage CI secrets.
---
## Switching to AWS S3
## Deployment Strategies
To use a private S3 bucket instead of GitHub Releases, update `electron-builder.yml`:
### AWS S3 (Private Infrastructure)
For enterprise environments requiring private infrastructure, the system can be reconfigured to target an **AWS S3 Bucket** or a **CloudFront Distribution**.
Update `electron-builder.yml`:
```yaml
publish:
@@ -186,34 +193,31 @@ publish:
acl: private
```
### Additional AWS Environment Variables
**Additional environment variables:**
| Variable | Description |
|---|---|
| `AWS_ACCESS_KEY_ID` | IAM access key with S3 write permissions |
| `AWS_ACCESS_KEY_ID` | IAM access key with S3 `PutObject` and `GetObject` permissions |
| `AWS_SECRET_ACCESS_KEY` | IAM secret key |
### S3 Bucket Policy
The bucket must allow public read access to the update files, or you must configure a CloudFront distribution in front of it. `electron-updater` needs to `GET` the `latest*.yml` files without authentication.
Recommended bucket structure:
**Bucket structure:**
```
your-bucket/desktop-releases/
├── latest.yml (Windows)
├── latest-mac.yml (macOS)
├── latest-linux.yml (Linux)
├── latest.yml (Windows manifest)
├── latest-mac.yml (macOS manifest)
├── latest-linux.yml (Linux manifest)
├── EigenDesktop-Setup-0.2.0.exe
├── EigenDesktop-0.2.0.dmg
├── EigenDesktop-0.2.0-mac.zip
└── EigenDesktop-0.2.0.AppImage
```
---
> [!NOTE]
> The bucket must allow public read access to the manifest files (`latest*.yml`), or you must configure a CloudFront distribution. `electron-updater` performs unauthenticated `GET` requests to resolve the latest version.
## Switching to a Generic File Server
### Generic File Server (Self-Hosted)
For a self-hosted server (Nginx, Caddy, etc.):
For self-hosted infrastructure (Nginx, Caddy, etc.):
```yaml
publish:
@@ -221,9 +225,12 @@ publish:
url: https://updates.your-domain.com/desktop
```
Your server must host the same file structure as S3 above. On each release, upload the installer files and `latest*.yml` to the server.
Your server must host the same directory structure as the S3 layout above.
### Nginx Example
> [!IMPORTANT]
> **MIME Type Configuration**: Ensure your file server correctly serves `.yml` files with `text/yaml` and installer binaries with `application/octet-stream`. Incorrect MIME types will cause download corruption or silent update failures.
**Nginx reference:**
```nginx
server {
@@ -234,21 +241,27 @@ server {
alias /var/www/desktop-releases/;
autoindex off;
add_header Cache-Control "no-cache";
# MIME types for update manifests
types {
text/yaml yml;
application/octet-stream exe dmg AppImage zip;
}
}
}
```
---
## Code Signing Requirements
## Code Signing: The Trust Boundary
> [!WARNING]
> **macOS auto-updates will FAIL without code signing.** Apple's Gatekeeper will block unsigned apps, and `electron-updater` will refuse to apply updates to unsigned builds. This is enforced by the OS, not by Electron.
> **Code signing is not merely a requirement — it is the Trust Boundary established by the operating system.** macOS Gatekeeper will explicitly terminate unsigned applications or refuse background updates to maintain system integrity. Windows SmartScreen will display alarming warnings to users. Without valid signatures, `electron-updater` will **reject update payloads entirely**.
### macOS
- Requires an **Apple Developer ID Application** certificate.
- The `electron-builder.yml` is already configured with:
- Requires an **Apple Developer ID Application** certificate ($99/year Apple Developer Program).
- The `electron-builder.yml` is configured with:
```yaml
mac:
hardenedRuntime: true
@@ -259,7 +272,8 @@ server {
- You must create `apps/desktop/build/entitlements.mac.plist`:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
@@ -271,40 +285,114 @@ server {
</dict>
</plist>
```
- **Notarization** is required for macOS 10.15+. Provide `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, and `APPLE_TEAM_ID`.
- **Notarization** is mandatory for macOS 10.15+. Provide `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, and `APPLE_TEAM_ID`.
### Windows
- Requires an **EV Code Signing Certificate** or a standard code signing certificate.
- Without signing, Windows SmartScreen will show a warning to users.
- EV certificates eliminate SmartScreen warnings immediately; standard certificates build reputation over time.
- Requires an **EV Code Signing Certificate** or a standard code signing certificate from a trusted CA.
- Without signing, Windows SmartScreen warns users with "Windows protected your PC" — severely impacting adoption.
- **EV certificates** eliminate SmartScreen warnings immediately; standard certificates build trust reputation over time through Microsoft's telemetry.
### Linux
- Code signing is **not required** for Linux.
- AppImage files work without signatures. However, you can optionally sign with GPG for package managers that support it.
- Code signing is **not enforced** by the OS for AppImage distribution.
- Optional GPG signing is available for package managers that support it.
---
## Troubleshooting
## Testing Updates in Development
### "Update check failed" on startup
> [!NOTE]
> The auto-updater is **intentionally disabled** in development mode to prevent runtime crashes. Setting `forceDevUpdateConfig` requires a `dev-app-update.yml` file, which introduces unnecessary complexity during local development.
- **Cause**: The app is offline, or the update server is unreachable.
- **Impact**: None — the error is caught and logged. The app continues to function normally.
- **Verification**: Check the main process console for `[AutoUpdater] Startup check failed (possibly offline)`.
### What Happens in Dev Mode
### `app-update.yml` not found in production build
In `src/main/index.ts`, the `setupAutoUpdaterEvents()` function detects `IS_DEV` and returns early:
- **Cause**: The `publish` block in `electron-builder.yml` is missing or misconfigured.
- **Fix**: Ensure the `publish` block exists. Run `electron-builder --publish never` first to verify the file is generated in `release/*/resources/app-update.yml`.
```typescript
if (IS_DEV) {
autoUpdater.autoDownload = false;
return; // Skip event registration — no update server in dev
}
```
### "Cannot update: code signature is invalid" (macOS)
This means:
- No update check is performed on startup.
- No `electron-updater` events are emitted.
- The `useElectronUpdater()` hook will remain in `idle` status.
- **Cause**: The app was not signed or the signature is broken.
- **Fix**: Ensure `CSC_LINK` and `CSC_KEY_PASSWORD` are set correctly in CI. Verify with: `codesign --verify --deep --strict release/mac*/EigenDesktop.app`.
### How to Test Updates
### Updates work on Windows/Linux but not macOS
Auto-update can **only** be fully validated using a **packaged, signed build** distributed through a real update channel:
- **Cause**: macOS requires **both** a signed app AND notarization.
- **Fix**: Provide all Apple credential environment variables and ensure the `mac.hardenedRuntime` and entitlements are configured.
1. **Publish v0.1.0** → Package and release a signed build.
2. **Bump to v0.2.0** → Increment the version in `package.json`.
3. **Publish v0.2.0** → Package and release the updated build.
4. **Launch v0.1.0** → The app should detect v0.2.0, download it, and prompt the user to restart.
For rapid iteration, use the `generic` provider pointing to a local Nginx or Python HTTP server:
```yaml
# electron-builder.yml (temporary, for testing)
publish:
provider: generic
url: http://localhost:8080/updates
```
```bash
# Serve the release directory locally
cd apps/desktop/release
python3 -m http.server 8080 --directory .
```
---
## Diagnostic Runbook
### Issue: "Update check failed" on startup
| | |
|---|---|
| **Symptom** | Console logs `[AutoUpdater] Startup check failed (possibly offline)` |
| **Root Cause** | The machine is offline, or the update server (GitHub/S3/generic) is unreachable. |
| **Resolution** | No action required. The error is caught in a `try/catch` block, logged to the console, and the application continues to function normally. The next check will occur on the next app launch. |
---
### Issue: `app-update.yml` not found in production build
| | |
|---|---|
| **Symptom** | `electron-updater` throws "Cannot find app-update.yml" immediately after launch. |
| **Root Cause** | The `publish` block in `electron-builder.yml` is missing or misconfigured. `electron-builder` generates `app-update.yml` only when a valid provider is declared. |
| **Resolution** | Verify the `publish` block exists in `electron-builder.yml`. Run `electron-builder --publish never` and inspect `release/*/resources/app-update.yml` to confirm generation. |
---
### Issue: "Cannot update: code signature is invalid" (macOS)
| | |
|---|---|
| **Symptom** | The updater downloads a new version but refuses to apply it, logging a signature validation error. |
| **Root Cause** | The application was not signed, or the signing certificate has expired / been revoked. |
| **Resolution** | Ensure `CSC_LINK` and `CSC_KEY_PASSWORD` are correctly set in CI. Verify the packaged app with: `codesign --verify --deep --strict release/mac*/Desktop.app`. Re-sign and re-publish if the certificate was rotated. |
---
### Issue: Updates work on Windows/Linux but not macOS
| | |
|---|---|
| **Symptom** | Windows and Linux users receive updates, but macOS users see no update prompt. |
| **Root Cause** | macOS requires **both** a valid code signature AND Apple notarization. Without notarization, Gatekeeper silently quarantines the update payload. |
| **Resolution** | Provide all Apple credential environment variables (`APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, `APPLE_TEAM_ID`) and ensure `hardenedRuntime: true` is set in `electron-builder.yml`. Re-package and re-publish. |
---
### Issue: S3/Generic provider returns corrupted downloads
| | |
|---|---|
| **Symptom** | Users report that the update downloads but fails to install, or the downloaded file is 0 bytes. |
| **Root Cause** | The file server is serving update manifests or binaries with incorrect MIME types, or a CDN is caching stale `latest*.yml` files. |
| **Resolution** | Verify MIME types: `.yml` → `text/yaml`, `.exe`/`.dmg`/`.AppImage`/`.zip` → `application/octet-stream`. Add `Cache-Control: no-cache` headers to `latest*.yml` responses. Invalidate CDN cache after publishing a new release. |