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:
@@ -1,113 +1,182 @@
|
||||
# Configuration Guide
|
||||
|
||||
This document covers how to configure the Electron desktop wrapper, manage the target web application, and handle production SPA routing.
|
||||
The Blueprint for Runtime Control.
|
||||
|
||||
> This guide defines the operational parameters of the Desktop Wrapper. It governs the orchestration of target applications, encapsulates the mechanics of our proprietary production routing, and provides a fail-safe **Break Glass Procedure** for emergency infrastructure transitions.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Target App Configuration](#target-app-configuration)
|
||||
- [Environment Variables](#environment-variables)
|
||||
- [Production SPA Routing (Custom `app://` Protocol)](#production-spa-routing-custom-app-protocol)
|
||||
- [Break Glass: Reverting to `file://` + HashRouter](#break-glass-reverting-to-file--hashrouter)
|
||||
- [Target App Orchestration](#target-app-orchestration)
|
||||
- [Environment Variables Registry](#environment-variables-registry)
|
||||
- [Deterministic Path Resolution](#deterministic-path-resolution)
|
||||
- [Production Routing: Overcoming Protocol Constraints](#production-routing-overcoming-protocol-constraints)
|
||||
- [Defense-in-Depth: Multi-Layered Protection](#defense-in-depth-multi-layered-protection)
|
||||
- [Break Glass Procedure: Disaster Recovery Protocol](#break-glass-procedure-disaster-recovery-protocol)
|
||||
|
||||
---
|
||||
|
||||
## Target App Configuration
|
||||
## Target App Orchestration
|
||||
|
||||
The desktop app wraps any web application in the monorepo. The target is configured via environment variables in `apps/desktop/.env`.
|
||||
The Desktop Wrapper is architected to embed **any** web application within the monorepo ecosystem. The target application is resolved at build time through a declarative configuration surface in `apps/desktop/.env`.
|
||||
|
||||
### `.env` File
|
||||
### `.env` Declaration
|
||||
|
||||
```env
|
||||
# The workspace name of the target web app to wrap.
|
||||
# Must match a directory under apps/ (e.g., "web", "docs-dev", "admin").
|
||||
# The workspace identifier of the target web application.
|
||||
# Must correspond to a directory under apps/ (e.g., "web", "docs-dev", "admin").
|
||||
DESKTOP_TARGET_APP=web
|
||||
|
||||
# The dev server URL for the target web app.
|
||||
# This is the URL that Vite serves during development.
|
||||
# The Vite development server endpoint for the target application.
|
||||
DESKTOP_DEV_SERVER_URL=http://localhost:5173
|
||||
```
|
||||
|
||||
### Switching to a Different App
|
||||
### Switching the Target Application
|
||||
|
||||
To wrap `apps/admin` instead of `apps/web`:
|
||||
To redirect the wrapper to a different application — for example, `apps/admin` — modify the configuration and re-execute the build pipeline:
|
||||
|
||||
1. Update `.env`:
|
||||
1. **Update the `.env` declaration:**
|
||||
```env
|
||||
DESKTOP_TARGET_APP=admin
|
||||
DESKTOP_DEV_SERVER_URL=http://localhost:3001
|
||||
```
|
||||
|
||||
2. Ensure the target app has a `build` script that outputs to `dist/`.
|
||||
2. **Verify the target app exports a `build` script** that emits static assets to `dist/`.
|
||||
|
||||
3. Run the desktop build:
|
||||
3. **Execute the deterministic build pipeline:**
|
||||
```bash
|
||||
pnpm build --filter=admin && cd apps/desktop && pnpm run build
|
||||
```
|
||||
|
||||
The `scripts/copy-web-dist.ts` prebuild script reads `DESKTOP_TARGET_APP` and copies `apps/<target>/dist/` into `apps/desktop/web-dist/`, which is then bundled by electron-builder.
|
||||
### The Deployment Bridge
|
||||
|
||||
### How Path Resolution Works
|
||||
The `prebuild` hook invokes `scripts/copy-web-dist.ts`, which serves as the **Deployment Bridge** between the web workspace and the native container. It reads `DESKTOP_TARGET_APP`, resolves the corresponding `apps/<target>/dist/` directory, and synchronizes the contents into `apps/desktop/web-dist/`. This bridge directory is then ingested by `electron-builder` via both the `files` and `extraResources` declarations in `electron-builder.yml`.
|
||||
|
||||
```
|
||||
Prebuild (copy-web-dist.ts):
|
||||
monorepo-root/apps/<DESKTOP_TARGET_APP>/dist/ → apps/desktop/web-dist/
|
||||
|
||||
Development (main process):
|
||||
__dirname (out/main/) → ../../ → apps/ → apps/<target>/dist/
|
||||
|
||||
Production (packaged app):
|
||||
process.resourcesPath → Contents/Resources/web-dist/
|
||||
┌─────────────────────────────┐ Deployment Bridge ┌──────────────────────────┐
|
||||
│ apps/<target>/dist/ │ ─── copy-web-dist.ts ────────→ │ apps/desktop/web-dist/ │
|
||||
│ (Vite build output) │ prebuild hook │ (Native container) │
|
||||
└─────────────────────────────┘ └──────────────────────────┘
|
||||
│
|
||||
▼
|
||||
electron-builder
|
||||
files + extraResources
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Packaged .app/.exe │
|
||||
│ resources/web-dist/ │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
## Environment Variables Registry
|
||||
|
||||
| Variable | Default | Used By | Description |
|
||||
| Variable | Default | Security Scope | Consumer | Description |
|
||||
|---|---|---|---|---|
|
||||
| `DESKTOP_TARGET_APP` | `web` | Build-time | `copy-web-dist.ts` | Workspace identifier of the web app to embed |
|
||||
| `DESKTOP_DEV_SERVER_URL` | `http://localhost:5173` | Runtime (dev) | `src/main/index.ts` | Dev server URL loaded in the Electron window during development |
|
||||
| `GH_TOKEN` | — | CI/CD | `electron-builder` | GitHub personal access token for publishing releases |
|
||||
| `CSC_LINK` | — | CI/CD | `electron-builder` | Base64-encoded `.p12` code signing certificate |
|
||||
| `CSC_KEY_PASSWORD` | — | CI/CD | `electron-builder` | Passphrase for the `.p12` certificate |
|
||||
| `APPLE_ID` | — | CI/CD (macOS) | `electron-builder` | Apple ID email for notarization submission |
|
||||
| `APPLE_APP_SPECIFIC_PASSWORD` | — | CI/CD (macOS) | `electron-builder` | App-specific password for notarization |
|
||||
| `APPLE_TEAM_ID` | — | CI/CD (macOS) | `electron-builder` | Apple Developer Team ID |
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Build-time** variables are consumed during the `prebuild` phase and baked into the artifact. **Runtime** variables are read by the Electron main process at launch. **CI/CD** variables are secrets injected exclusively in the deployment environment — they must never appear in source control or local `.env` files.
|
||||
|
||||
---
|
||||
|
||||
## Deterministic Path Resolution
|
||||
|
||||
The following matrix defines how the target app's static assets are resolved across every phase of the application lifecycle. Each path is **deterministic** — there is no runtime ambiguity.
|
||||
|
||||
| Phase | Resolution Strategy | Resolved Path | Context |
|
||||
|---|---|---|---|
|
||||
| `DESKTOP_TARGET_APP` | `web` | `copy-web-dist.ts` | Workspace name of the web app to embed |
|
||||
| `DESKTOP_DEV_SERVER_URL` | `http://localhost:5173` | `src/main/index.ts` | URL of the target app's Vite dev server |
|
||||
| `GH_TOKEN` | — | `electron-builder` | GitHub token for publishing releases |
|
||||
| `CSC_LINK` | — | `electron-builder` | Base64-encoded code signing certificate |
|
||||
| `CSC_KEY_PASSWORD` | — | `electron-builder` | Password for the signing certificate |
|
||||
| **Prebuild** | `copy-web-dist.ts` reads `DESKTOP_TARGET_APP` | `monorepo-root/apps/<target>/dist/` → `apps/desktop/web-dist/` | Deployment Bridge: build-time synchronization |
|
||||
| **Development** | `__dirname` relative traversal from `out/main/` | `apps/desktop/out/main/` → `../../` → `apps/` → `<target>/dist/` | Direct filesystem access to the web app's build output |
|
||||
| **Production** | `process.resourcesPath` | `Contents/Resources/web-dist/` (macOS) / `resources/web-dist/` (Windows/Linux) | OS-specific resource directory within the packaged binary |
|
||||
|
||||
> [!NOTE]
|
||||
> The development path relies on `__dirname` pointing to `apps/desktop/out/main/` at runtime. If electron-vite's output directory is ever reconfigured, this traversal must be updated in `getWebDistPath()` within `src/main/index.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Production SPA Routing (Custom `app://` Protocol)
|
||||
## Production Routing: Overcoming Protocol Constraints
|
||||
|
||||
### The Problem
|
||||
### The Constraint
|
||||
|
||||
React apps using `BrowserRouter` rely on the server to always return `index.html` for any URL path (e.g., `/dashboard`, `/auth/login`). With Electron's `file://` protocol, requesting `file:///app/dashboard` looks for an actual file at that path — which doesn't exist — resulting in a blank screen or "file not found" error.
|
||||
React applications using `BrowserRouter` rely on a fundamental server-side contract: **every URL path must return `index.html`**. Paths like `/dashboard`, `/auth/login`, and `/settings/profile` do not correspond to physical files — they are virtual routes resolved entirely by the client-side router.
|
||||
|
||||
### The Solution
|
||||
Electron's default `file://` protocol breaks this contract. Requesting `file:///app/dashboard` triggers a literal filesystem lookup for a file named `dashboard`, which does not exist, resulting in a blank screen or an OS-level "file not found" error.
|
||||
|
||||
The main process registers a custom `app://` protocol with a handler that:
|
||||
### The Solution: A Privileged Virtual File System
|
||||
|
||||
1. Receives a request like `app://-/dashboard`.
|
||||
2. Strips the protocol and hostname to get the path: `dashboard`.
|
||||
3. Checks if a real file exists at `web-dist/dashboard`.
|
||||
4. **If yes** → serves the file with the correct MIME type.
|
||||
5. **If no** → serves `web-dist/index.html` instead (SPA fallback).
|
||||
The `app://` scheme is a **Privileged Virtual File System** that resolves SPA routing conflicts by implementing a **Heuristic Resource Loader**. It operates as follows:
|
||||
|
||||
This allows React Router to handle all client-side routing normally. Deep links, page refreshes, and direct URL entry all work because every unknown path falls back to `index.html`.
|
||||
```
|
||||
Request: app://-/settings/profile
|
||||
│
|
||||
├─ Decode URI → "settings/profile"
|
||||
│
|
||||
├─ Normalize + validate path (security boundary check)
|
||||
│
|
||||
├─ Does web-dist/settings/profile exist as a file?
|
||||
│ ├─ YES → Serve with correct MIME type + CSP headers
|
||||
│ └─ NO → Heuristic Fallback: serve web-dist/index.html
|
||||
│
|
||||
└─ React Router resolves /settings/profile client-side
|
||||
```
|
||||
|
||||
### Security Measures
|
||||
If a requested URI does not map to a physical asset, the handler intelligently intercepts the request to serve the `index.html` entry point, allowing React Router to maintain stateful client-side navigation. This ensures that deep links, page refreshes, and direct URL entry all function without modification to the React app's routing configuration.
|
||||
|
||||
- **Path traversal protection**: The resolved file path is validated to stay within `web-dist/` using `normalize()` + `startsWith()` check. Requests like `app://-/../../etc/passwd` return `403 Forbidden`.
|
||||
- **CSP headers**: Content-Security-Policy headers are injected on every HTML response served by the protocol handler.
|
||||
- **Scheme privileges**: The `app` scheme is registered with `standard: true`, `secure: true`, `supportFetchAPI: true`, and `corsEnabled: true` — making it behave like `https://` to the renderer process.
|
||||
### Scheme Registration
|
||||
|
||||
The scheme **must** be registered synchronously at module load time, before `app.whenReady()`. This is a Chromium requirement — deferred registration will silently fail:
|
||||
|
||||
```typescript
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{
|
||||
scheme: 'app',
|
||||
privileges: {
|
||||
standard: true, // Enables URL parsing (host, path, query)
|
||||
secure: true, // Treated as a secure origin (HTTPS equivalent)
|
||||
supportFetchAPI: true, // Allows fetch() from this scheme
|
||||
corsEnabled: true, // Enables CORS for cross-origin requests
|
||||
stream: true, // Supports streaming responses
|
||||
},
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Break Glass: Reverting to `file://` + HashRouter
|
||||
## Defense-in-Depth: Multi-Layered Protection
|
||||
|
||||
If the custom `app://` protocol ever causes issues (e.g., a third-party library incompatibility), you can fall back to the standard `file://` protocol with `HashRouter`. This requires two changes.
|
||||
The custom protocol handler enforces a **multi-layered defense perimeter** that goes beyond standard Electron security defaults.
|
||||
|
||||
### Step 1: Switch Router in the React App
|
||||
| Layer | Technique | Implementation | Threat Mitigated |
|
||||
|---|---|---|---|
|
||||
| **I/O Sanitization** | Path traversal guard | `normalize()` + `startsWith()` validation against `web-dist/` boundary | Directory traversal attacks (`../../etc/passwd`) → `403 Forbidden` |
|
||||
| **In-Flight Policy Injection** | CSP response headers | `Content-Security-Policy` injected as HTTP response headers on every HTML payload | XSS execution via script injection |
|
||||
| **Cryptographic Isolation** | Privileged scheme registration | `app` scheme registered with `standard`, `secure`, `supportFetchAPI`, `corsEnabled` | Scheme downgrade attacks; the renderer treats `app://` identically to `https://` |
|
||||
| **Resource Type Validation** | `statSync.isFile()` check | Only regular files are served; directories return the SPA fallback | Information disclosure via directory listing |
|
||||
| **Origin Sanitization** | CORS bypass proxy | `webRequest.onBeforeSendHeaders` strips `app://` Origin headers on outgoing requests | Backend CORS rejection of non-standard origins |
|
||||
| **Navigation Confinement** | `will-navigate` guard | Blocks navigation to URLs outside `app://` and the authorized dev server | Phishing via in-app redirect to malicious sites |
|
||||
|
||||
In the target web app (e.g., `apps/web/src/apps/index.tsx`):
|
||||
---
|
||||
|
||||
## Break Glass Procedure: Disaster Recovery Protocol
|
||||
|
||||
> [!CAUTION]
|
||||
> **This is a formal Disaster Recovery Protocol.** Execute only if the custom `app://` protocol causes an irrecoverable failure — for example, a critical third-party library that refuses to operate under a non-standard URI scheme. This procedure requires coordinated changes across both the React application and the Electron main process. Estimated recovery time: **15 minutes**.
|
||||
|
||||
### Step 1: Switch the Router — React Application
|
||||
|
||||
In the target web app's entry point (e.g., `apps/web/src/apps/index.tsx`):
|
||||
|
||||
```diff
|
||||
- import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
||||
@@ -130,29 +199,22 @@ In the target web app (e.g., `apps/web/src/apps/index.tsx`):
|
||||
}
|
||||
```
|
||||
|
||||
Routes will now use hash-based URLs: `#/app/dashboard`, `#/auth/login`, `#/showcase`.
|
||||
All routes transition to hash-based addressing: `#/app/dashboard`, `#/auth/login`, `#/showcase`.
|
||||
|
||||
### Step 2: Switch to `file://` in the Main Process
|
||||
### Step 2: Decommission the Custom Protocol — Main Process
|
||||
|
||||
In `apps/desktop/src/main/index.ts`:
|
||||
|
||||
**a)** Remove the scheme registration at the top of the file:
|
||||
In `apps/desktop/src/main/index.ts`, execute the following surgical removals:
|
||||
|
||||
**a)** Remove the scheme registration block at the top of the file:
|
||||
```diff
|
||||
- protocol.registerSchemesAsPrivileged([
|
||||
- {
|
||||
- scheme: 'app',
|
||||
- privileges: { standard: true, secure: true, ... },
|
||||
- },
|
||||
- ]);
|
||||
- protocol.registerSchemesAsPrivileged([ ... ]);
|
||||
```
|
||||
|
||||
**b)** Remove the `registerAppProtocol()` function entirely.
|
||||
**b)** Delete the entire `registerAppProtocol()` function.
|
||||
|
||||
**c)** Remove the `registerAppProtocol()` call in `app.whenReady()`.
|
||||
|
||||
**d)** Change the production content loading in `createWindow()`:
|
||||
**c)** Remove the `registerAppProtocol()` invocation inside `app.whenReady()`.
|
||||
|
||||
**d)** Redirect production content loading in `createWindow()`:
|
||||
```diff
|
||||
if (IS_DEV) {
|
||||
mainWindow.loadURL(DEV_SERVER_URL);
|
||||
@@ -164,20 +226,24 @@ In `apps/desktop/src/main/index.ts`:
|
||||
}
|
||||
```
|
||||
|
||||
**e)** Add a CSP `<meta>` tag to the web app's `index.html` since there's no protocol handler to inject headers:
|
||||
|
||||
**e)** Inject a CSP `<meta>` tag into the web app's `index.html`, since the In-Flight Policy Injection layer is no longer available:
|
||||
```html
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self' file:; script-src 'self' file:; style-src 'self' 'unsafe-inline' file:; connect-src 'self' https:; img-src 'self' file: data: https:; font-src 'self' file: data:;" />
|
||||
content="default-src 'self' file:; script-src 'self' file:;
|
||||
style-src 'self' 'unsafe-inline' file:;
|
||||
connect-src 'self' https:;
|
||||
img-src 'self' file: data: https:;
|
||||
font-src 'self' file: data:;" />
|
||||
```
|
||||
|
||||
### Comparison
|
||||
### Trade-off Analysis
|
||||
|
||||
| Aspect | Custom `app://` | `file://` + HashRouter |
|
||||
| Dimension | Custom `app://` Protocol | `file://` + HashRouter |
|
||||
|---|---|---|
|
||||
| URL appearance | `/app/dashboard` | `#/app/dashboard` |
|
||||
| React Router | `BrowserRouter` (no change) | Must use `HashRouter` |
|
||||
| Deep linking | Full support | Hash-based |
|
||||
| Implementation complexity | Higher | Lower |
|
||||
| CSP delivery | Via response headers | Via `<meta>` tag |
|
||||
| Third-party compatibility | Rare edge cases | Maximum compatibility |
|
||||
| **Aesthetic Integrity** | Clean URLs: `/app/dashboard` | Hash prefix: `#/app/dashboard` |
|
||||
| **Router Compatibility** | `BrowserRouter` — zero changes required | Must migrate to `HashRouter` |
|
||||
| **Deep Linking** | Full, native-style support | Hash-based only |
|
||||
| **Protocol-Native Compatibility** | Rare edge cases with non-standard scheme detection | Maximum third-party compatibility |
|
||||
| **Security Delivery Vector** | CSP via response headers (strongest enforcement) | CSP via `<meta>` tag (bypassable by early script execution) |
|
||||
| **Implementation Complexity** | Higher (custom protocol handler + security layers) | Lower (no custom protocol infrastructure) |
|
||||
| **Recovery Time** | — | ~15 minutes, 2 files |
|
||||
|
||||
Reference in New Issue
Block a user