[← 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//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//dist/` → `apps/desktop/web-dist/` | Deployment Bridge: build-time synchronization | | **Development** | `__dirname` relative traversal from `out/main/` | `apps/desktop/out/main/` → `../../` → `apps/` → `/dist/` | Direct filesystem access to the web app's build output | | **Production** | `process.resourcesPath` | `Contents/Resources/web-dist/` (macOS) / `resources/web-dist/` (Windows/Linux) | OS-specific resource directory within the packaged binary | > [!NOTE] > The development path relies on `__dirname` pointing to `apps/desktop/out/main/` at runtime. If electron-vite's output directory is ever reconfigured, this traversal must be updated in `getWebDistPath()` within `src/main/index.ts`. --- ## 🚀 Production Routing: Overcoming Protocol Constraints ### The Constraint React applications using `BrowserRouter` rely on a fundamental server-side contract: **every URL path must return `index.html`**. Paths like `/dashboard`, `/auth/login`, and `/settings/profile` do not correspond to physical files — they are virtual routes resolved entirely by the client-side router. Electron's default `file://` protocol breaks this contract. Requesting `file:///app/dashboard` triggers a literal filesystem lookup for a file named `dashboard`, which does not exist, resulting in a blank screen or an OS-level "file not found" error. ### The Solution: A Privileged Virtual File System The `app://` scheme is a **Privileged Virtual File System** that resolves SPA routing conflicts by implementing a **Heuristic Resource Loader**. It operates as follows: ```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 ( - + Loading...}> {/* All route definitions remain unchanged */} - + ); } ``` All routes transition to hash-based addressing: `#/app/dashboard`, `#/auth/login`, `#/showcase`. ### Step 2: Decommission the Custom Protocol — Main Process In `apps/desktop/src/main/index.ts`, execute the following surgical removals: **a)** Remove the scheme registration block at the top of the file: ```diff - protocol.registerSchemesAsPrivileged([ ... ]); ``` **b)** Delete the entire `registerAppProtocol()` function. **c)** Remove the `registerAppProtocol()` invocation inside `app.whenReady()`. **d)** Redirect production content loading in `createWindow()`: ```diff if (IS_DEV) { mainWindow.loadURL(DEV_SERVER_URL); mainWindow.webContents.openDevTools({ mode: 'detach' }); } else { - mainWindow.loadURL('app://-/index.html'); + const webDistPath = getWebDistPath(); + mainWindow.loadFile(join(webDistPath, 'index.html')); } ``` **e)** Inject a CSP `` tag into the web app's `index.html`, since the In-Flight Policy Injection layer is no longer available: ```html ``` ### Trade-off Analysis | Dimension | Custom `app://` Protocol | `file://` + HashRouter | | --------------------------------- | -------------------------------------------------- | ----------------------------------------------------------- | | **Aesthetic Integrity** | Clean URLs: `/app/dashboard` | Hash prefix: `#/app/dashboard` | | **Router Compatibility** | `BrowserRouter` — zero changes required | Must migrate to `HashRouter` | | **Deep Linking** | Full, native-style support | Hash-based only | | **Protocol-Native Compatibility** | Rare edge cases with non-standard scheme detection | Maximum third-party compatibility | | **Security Delivery Vector** | CSP via response headers (strongest enforcement) | CSP via `` tag (bypassable by early script execution) | | **Implementation Complexity** | Higher (custom protocol handler + security layers) | Lower (no custom protocol infrastructure) | | **Recovery Time** | — | ~15 minutes, 2 files |