diff --git a/apps/desktop/docs/AUTO_UPDATER.md b/apps/desktop/docs/AUTO_UPDATER.md index 2f1ade4..686a629 100644 --- a/apps/desktop/docs/AUTO_UPDATER.md +++ b/apps/desktop/docs/AUTO_UPDATER.md @@ -1,56 +1,56 @@ -# Auto-Update System +[โ† Back to Root](../../../README.md) -The Unified Update Lifecycle. +# Desktop Auto-Update System -> 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. +`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. --- -## Table of Contents - -- [Reactive Update Flow](#reactive-update-flow) -- [Current Provider: GitHub Releases](#current-provider-github-releases) -- [Release Workflow: The Deterministic Pipeline](#release-workflow-the-deterministic-pipeline) -- [CI/CD Environment Variables](#cicd-environment-variables) -- [Deployment Strategies](#deployment-strategies) -- [Code Signing: The Trust Boundary](#code-signing-the-trust-boundary) -- [Testing Updates in Development](#testing-updates-in-development) -- [Diagnostic Runbook](#diagnostic-runbook) - ---- - -## Reactive Update Flow +## ๐Ÿ— Reactive Update Flow The following diagram illustrates the **Reactive Update Flow**, bridging the Node.js Main Process with the React UI layer through a secure IPC event stream. -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Main Process (src/main/index.ts) โ”‚ -โ”‚ โ”‚ -โ”‚ autoUpdater.checkForUpdatesAndNotify() โ”‚ -โ”‚ โ”‚ โ”‚ -โ”‚ โ”œโ”€โ†’ 'checking-for-update' โ”‚ -โ”‚ โ”œโ”€โ†’ 'update-available' โ†’ { version, releaseDate } โ”‚ -โ”‚ โ”œโ”€โ†’ 'download-progress' โ†’ { percent, bytesPerSecond } โ”‚ -โ”‚ โ”œโ”€โ†’ 'update-downloaded' โ†’ { version, releaseNotes } โ”‚ -โ”‚ โ””โ”€โ†’ 'error' โ†’ { message } โ”‚ -โ”‚ โ”‚ -โ”‚ sendToRenderer('updater:*', payload) โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ IPC Bridge โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ Preload (src/preload/index.ts) โ”‚ -โ”‚ โ”‚ -โ”‚ contextBridge.exposeInMainWorld('electronAPI', { โ”‚ -โ”‚ onUpdateAvailable, onDownloadProgress, โ”‚ -โ”‚ onUpdateDownloaded, onUpdateError, โ”‚ -โ”‚ checkForUpdates, installUpdate โ”‚ -โ”‚ }) โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Renderer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ React App (apps/web) โ”‚ -โ”‚ โ”‚ -โ”‚ useElectronUpdater() hook โ”‚ -โ”‚ โ†’ Reactive state: status, progress, updateInfo, errorMessage โ”‚ -โ”‚ โ†’ Actions: checkForUpdates(), installUpdate() โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +```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 @@ -63,7 +63,7 @@ The following diagram illustrates the **Reactive Update Flow**, bridging the Nod --- -## Current Provider: GitHub Releases +## ๐ŸŒ Current Provider: GitHub Releases The update provider is declared in `electron-builder.yml`: @@ -77,6 +77,7 @@ publish: ### 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). @@ -88,7 +89,7 @@ publish: --- -## Release Workflow: The Deterministic Pipeline +## ๐Ÿš€ Release Workflow: The Deterministic Pipeline To maintain release integrity, follow this deterministic pipeline to synchronize web assets and native binaries. @@ -109,8 +110,7 @@ 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. +> [!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) @@ -155,18 +155,18 @@ jobs: --- -## CI/CD Environment Variables +## ๐Ÿ” 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. | +| 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 @@ -176,7 +176,7 @@ jobs: --- -## Deployment Strategies +## โ˜๏ธ Deployment Strategies ### AWS S3 (Private Infrastructure) @@ -195,13 +195,14 @@ publish: **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 | +| 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) @@ -222,13 +223,12 @@ For self-hosted infrastructure (Nginx, Caddy, etc.): ```yaml publish: provider: generic - url: https://updates.your-domain.com/desktop + 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. +> [!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:** @@ -253,10 +253,9 @@ server { --- -## Code Signing: The Trust Boundary +## ๐Ÿ›ก๏ธ 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**. +> [!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 @@ -273,7 +272,7 @@ server { ```xml + "[http://www.apple.com/DTDs/PropertyList-1.0.dtd](http://www.apple.com/DTDs/PropertyList-1.0.dtd)"> com.apple.security.cs.allow-jit @@ -300,7 +299,7 @@ server { --- -## Testing Updates in Development +## ๐Ÿงช 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. @@ -317,6 +316,7 @@ if (IS_DEV) { ``` This means: + - No update check is performed on startup. - No `electron-updater` events are emitted. - The `useElectronUpdater()` hook will remain in `idle` status. @@ -347,52 +347,34 @@ python3 -m http.server 8080 --directory . --- -## Diagnostic Runbook +## โš ๏ธ 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. | - ---- +| 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** | `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. | - ---- +| 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** | 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. | - ---- +| 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** | 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. | - ---- +| 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** | 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. | +| 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. | diff --git a/apps/desktop/docs/CONFIGURATION.md b/apps/desktop/docs/CONFIGURATION.md index b2f3533..208eb23 100644 --- a/apps/desktop/docs/CONFIGURATION.md +++ b/apps/desktop/docs/CONFIGURATION.md @@ -1,4 +1,6 @@ -# Configuration Guide +[โ† Back to Root](../../../README.md) + +# Desktop Configuration Guide The Blueprint for Runtime Control. @@ -17,7 +19,7 @@ The Blueprint for Runtime Control. --- -## Target App Orchestration +## ๐ŸŽฏ 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`. @@ -37,6 +39,7 @@ DESKTOP_DEV_SERVER_URL=http://localhost:5173 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 @@ -51,61 +54,71 @@ To redirect the wrapper to a different application โ€” for example, `apps/admin` ### The Deployment Bridge -The `prebuild` hook invokes `scripts/copy-web-dist.ts`, which serves as the **Deployment Bridge** between the web workspace and the native container. It reads `DESKTOP_TARGET_APP`, resolves the corresponding `apps//dist/` directory, and synchronizes the contents into `apps/desktop/web-dist/`. This bridge directory is then ingested by `electron-builder` via both the `files` and `extraResources` declarations in `electron-builder.yml`. +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`. -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” Deployment Bridge โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ apps//dist/ โ”‚ โ”€โ”€โ”€ copy-web-dist.ts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ†’ โ”‚ apps/desktop/web-dist/ โ”‚ -โ”‚ (Vite build output) โ”‚ prebuild hook โ”‚ (Native container) โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ–ผ - electron-builder - files + extraResources - โ”‚ - โ–ผ - โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” - โ”‚ Packaged .app/.exe โ”‚ - โ”‚ resources/web-dist/ โ”‚ - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +```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 +## ๐Ÿ” 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 | +| 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. +> [!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 +## ๐Ÿ“‚ 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 | +| 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 +## ๐Ÿš€ Production Routing: Overcoming Protocol Constraints ### The Constraint @@ -117,18 +130,34 @@ Electron's default `file://` protocol breaks this contract. Requesting `file:/// The `app://` scheme is a **Privileged Virtual File System** that resolves SPA routing conflicts by implementing a **Heuristic Resource Loader**. It operates as follows: -``` -Request: app://-/settings/profile - โ”‚ - โ”œโ”€ Decode URI โ†’ "settings/profile" - โ”‚ - โ”œโ”€ Normalize + validate path (security boundary check) - โ”‚ - โ”œโ”€ Does web-dist/settings/profile exist as a file? - โ”‚ โ”œโ”€ YES โ†’ Serve with correct MIME type + CSP headers - โ”‚ โ””โ”€ NO โ†’ Heuristic Fallback: serve web-dist/index.html - โ”‚ - โ””โ”€ React Router resolves /settings/profile client-side +```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. @@ -142,11 +171,11 @@ protocol.registerSchemesAsPrivileged([ { scheme: 'app', privileges: { - standard: true, // Enables URL parsing (host, path, query) - secure: true, // Treated as a secure origin (HTTPS equivalent) + 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 + corsEnabled: true, // Enables CORS for cross-origin requests + stream: true, // Supports streaming responses }, }, ]); @@ -154,25 +183,24 @@ protocol.registerSchemesAsPrivileged([ --- -## Defense-in-Depth: Multi-Layered Protection +## ๐Ÿ›ก๏ธ 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 | +| 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 +## โš ๏ธ 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**. +> [!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 @@ -206,6 +234,7 @@ All routes transition to hash-based addressing: `#/app/dashboard`, `#/auth/login 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([ ... ]); ``` @@ -215,6 +244,7 @@ In `apps/desktop/src/main/index.ts`, execute the following surgical removals: **c)** Remove the `registerAppProtocol()` invocation inside `app.whenReady()`. **d)** Redirect production content loading in `createWindow()`: + ```diff if (IS_DEV) { mainWindow.loadURL(DEV_SERVER_URL); @@ -227,23 +257,26 @@ In `apps/desktop/src/main/index.ts`, execute the following surgical removals: ``` **e)** Inject a CSP `` tag into the web app's `index.html`, since the In-Flight Policy Injection layer is no longer available: + ```html - + 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 `` 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 | +| Dimension | Custom `app://` Protocol | `file://` + HashRouter | +| --------------------------------- | -------------------------------------------------- | ----------------------------------------------------------- | +| **Aesthetic Integrity** | Clean URLs: `/app/dashboard` | Hash prefix: `#/app/dashboard` | +| **Router Compatibility** | `BrowserRouter` โ€” zero changes required | Must migrate to `HashRouter` | +| **Deep Linking** | Full, native-style support | Hash-based only | +| **Protocol-Native Compatibility** | Rare edge cases with non-standard scheme detection | Maximum third-party compatibility | +| **Security Delivery Vector** | CSP via response headers (strongest enforcement) | CSP via `` tag (bypassable by early script execution) | +| **Implementation Complexity** | Higher (custom protocol handler + security layers) | Lower (no custom protocol infrastructure) | +| **Recovery Time** | โ€” | ~15 minutes, 2 files | diff --git a/apps/desktop/docs/IPC_ARCHITECTURE.md b/apps/desktop/docs/IPC_ARCHITECTURE.md index 6923f21..b36d79f 100644 --- a/apps/desktop/docs/IPC_ARCHITECTURE.md +++ b/apps/desktop/docs/IPC_ARCHITECTURE.md @@ -1,3 +1,5 @@ +[โ† Back to Root](../../../README.md) + # IPC Architecture & Security Model The Secure Communication Blueprint. @@ -23,44 +25,56 @@ The desktop wrapper enforces a **strict privilege separation** between three exe ### 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 | +| Context | Trust Level | Privilege Scope | Security Guarantee | +| ------------------ | ------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| **Main Process** | Fully Trusted | Unrestricted Node.js access: filesystem, network, printers, OS APIs, child processes | Only code authored by the engineering team executes here | +| **Preload Script** | Controlled | Restricted to `ipcRenderer.invoke()` and `ipcRenderer.send()` โ€” no direct Node.js access | Executes in a **Hermetically Sealed Context** โ€” isolated from both the Main Process globals and the Renderer's DOM | +| **Renderer** | Untrusted | Standard browser sandbox โ€” zero Node.js API surface | Designated as a **Zero-Trust Environment** โ€” may execute third-party code, npm packages, or XSS payloads | ### Process Topology -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ MAIN PROCESS [Fully Trusted] โ”‚ -โ”‚ โ”‚ -โ”‚ UNRESTRICTED PRIVILEGES โ”‚ -โ”‚ Filesystem ยท Network ยท Printers ยท Native APIs ยท Child Processes โ”‚ -โ”‚ Auto-Updater ยท OS Integration ยท System Notifications โ”‚ -โ”‚ โ”‚ -โ”‚ ipcMain.handle('channel', handler) โ† Command handlers โ”‚ -โ”‚ ipcMain.on('channel', handler) โ† Event listeners โ”‚ -โ”‚ webContents.send('channel', data) โ† Downstream push โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Non-Bypassable Isolation Boundary โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ PRELOAD SCRIPT [Secure Gateway] โ”‚ -โ”‚ โ”‚ -โ”‚ HERMETICALLY SEALED CONTEXT โ”‚ -โ”‚ Performs Interface Narrowing: transforms broad IPC capabilities โ”‚ -โ”‚ into a minimal, auditable API surface. Acts as the sole โ”‚ -โ”‚ authorized mediator between trusted and untrusted contexts. โ”‚ -โ”‚ โ”‚ -โ”‚ contextBridge.exposeInMainWorld('electronAPI', { ... }) โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Non-Bypassable Isolation Boundary โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ RENDERER [Zero-Trust Environment] โ”‚ -โ”‚ โ”‚ -โ”‚ UNTRUSTED WEB CONTENT โ”‚ -โ”‚ Standard browser sandbox. Zero access to: require, __dirname, โ”‚ -โ”‚ process, fs, child_process, net, os, ipcRenderer. โ”‚ -โ”‚ โ”‚ -โ”‚ ONLY authorized interaction vector: โ”‚ -โ”‚ window.electronAPI.methodName(args) โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +```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. @@ -69,12 +83,12 @@ The Preload Script functions as a **Secure Gateway** that performs **Interface N 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. | +| 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. | --- @@ -82,8 +96,7 @@ These settings are declared in `BrowserWindow.webPreferences` and are **non-nego 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). +> [!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 @@ -134,6 +147,7 @@ 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. @@ -160,17 +174,18 @@ The following is the **complete, authoritative registry** of all authorized IPC ### 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 | +| 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) +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` @@ -179,16 +194,16 @@ print: (options?) => ipcRenderer.invoke('printer:print', options) ### 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 | +| 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` @@ -283,7 +298,7 @@ contextBridge.exposeInMainWorld('ipc', ipcRenderer); 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**. +**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** @@ -294,7 +309,7 @@ contextBridge.exposeInMainWorld('require', require); ```typescript // VIOLATION: Catastrophic Failure โ€” Complete Boundary Collapse new BrowserWindow({ - webPreferences: { nodeIntegration: true, contextIsolation: false } + webPreferences: { nodeIntegration: true, contextIsolation: false }, }); ``` diff --git a/apps/landing/.env.example b/apps/landing/.env.example new file mode 100644 index 0000000..36c47e4 --- /dev/null +++ b/apps/landing/.env.example @@ -0,0 +1 @@ +VITE_CMS_API_URL=http://localhost:8001/cms diff --git a/apps/landing/src/environment/env.ts b/apps/landing/src/environment/env.ts new file mode 100644 index 0000000..b54810f --- /dev/null +++ b/apps/landing/src/environment/env.ts @@ -0,0 +1,6 @@ +/** + * Type-safe Environment Wrapper for apps/landing. + */ +export const ENV = { + CMS_API_URL: import.meta.env.VITE_CMS_API_URL || 'http://localhost:8001/cms', +} as const; diff --git a/apps/web/.env.example b/apps/web/.env.example new file mode 100644 index 0000000..dcd4573 --- /dev/null +++ b/apps/web/.env.example @@ -0,0 +1,5 @@ +VITE_API_BASE_URL=http://localhost:8000/api +VITE_APP_ENV=development +VITE_COUCHDB_BASE_URL=http://202.146.229.134:7700 +VITE_COUCHDB_USERNAME=root +VITE_COUCHDB_PASSWORD=password diff --git a/apps/web/package.json b/apps/web/package.json index fab2c64..c959bda 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -21,7 +21,9 @@ "@repo/utils": "workspace:*", "@tailwindcss/vite": "^4.1.18", "dayjs": "^1.11.19", + "events": "^3.3.0", "i18next": "^24.2.2", + "lucide-react": "^1.17.0", "react": "^19.2.3", "react-dom": "^19.2.3", "react-i18next": "^15.4.0", diff --git a/apps/web/src/apps/showcase/pouch-sample.tsx b/apps/web/src/apps/showcase/pouch-sample.tsx new file mode 100644 index 0000000..08ccfe4 --- /dev/null +++ b/apps/web/src/apps/showcase/pouch-sample.tsx @@ -0,0 +1,237 @@ +import { useEffect, useState, useCallback } from 'react'; +import { + Button, + Card, + Group, + Stack, + Title, + Text, + Table, + Badge, +} from '@repo/ui/components'; +import { itemDB, posConfigDB } from '../../core/db'; +import type { Item, POSConfiguration } from '../../core/db/types'; + +export default function PouchSample() { + const [configs, setConfigs] = useState([]); + const [items, setItems] = useState([]); + + // Load initial data + const loadData = useCallback(async () => { + try { + const allConfigs = await posConfigDB.find({ selector: {} }); + setConfigs(allConfigs); + + const allItems = await itemDB.find({ selector: {} }); + setItems(allItems); + console.log({allConfigs, allItems}) + } catch (err) { + console.error('Failed to load PouchDB data', err); + } + }, []); + + useEffect(() => { + // 1. Initial Data Load + loadData(); + + // 2. Setup Real-Time Listeners + const unsubscribeItems = itemDB.onChange(() => { + loadData(); + }); + + const unsubscribePos = posConfigDB.onChange(() => { + loadData(); + }); + + // 3. CRITICAL: Cleanup to prevent memory leaks on unmount + return () => { + unsubscribeItems(); + unsubscribePos(); + }; + }, [loadData]); + + // โ”€โ”€โ”€ POS Configuration Handlers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + const handleSeedConfig = async () => { + try { + const id = `pos-${Date.now()}`; + await posConfigDB.create({ + _id: id, + pos_number: '1111111111666', + pos_name: 'Premium Test POS TESTING COUNCH', + items: items, // mapping current items + payment_methods: [{ id: 'cash', name: 'Cash' }], + }); + loadData(); + } catch (err) { + console.error('Failed to seed config', err); + } + }; + + const handleDeleteConfig = async (id: string) => { + try { + await posConfigDB.delete(id); + loadData(); + } catch (err) { + console.error('Failed to delete config', err); + } + }; + + // โ”€โ”€โ”€ Items Inventory Handlers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + const handleAddItem = async () => { + try { + const id = `item-${Date.now()}`; + await itemDB.create({ + _id: id, + name: 'PLAYGROUND ALL DAY TESTING POUCH', + base_price: '75000', + item_type: 'wahana', + usage_type: 'ticket', + item_category: [{ name: 'Entertainment' }], + item_rates: [ + { season_period: 'weekday', price: 50000 }, + { season_period: 'weekend', price: 75000 }, + ], + }); + + loadData(); + } catch (err) { + console.error('Failed to add item', err); + } + }; + + const handleDeleteItem = async (id: string) => { + try { + await itemDB.delete(id); + loadData(); + } catch (err) { + console.error('Failed to delete item', err); + } + }; + + const handleClearAll = async () => { + try { + await posConfigDB.cleanAllData(); + await itemDB.cleanAllData(); + loadData(); + } catch (err) { + console.error('Failed to clear data', err); + } + }; + + return ( + + + Enterprise PouchDB Sync + + + + {/* Items Inventory Table */} + + + Items Database + + + +
+ + + + ID + Name + Type + Base Price + Rates Count + Actions + + + + {items.length > 0 ? ( + items.map((item) => ( + + {item._id} + {item.name} + + + {item.item_type} + + + ${Number(item.base_price).toFixed(2)} + {item.item_rates?.length || 0} + + + + + )) + ) : ( + + + No items found. + + + )} + +
+
+
+ + {/* POS Configuration Table */} + + + POS Configurations + + + +
+ + + + ID + POS Name + POS Number + Mapped Items + Actions + + + + {configs.length > 0 ? ( + configs.map((cfg) => ( + + {cfg._id} + {cfg.pos_name} + {cfg.pos_number} + + + {cfg.items?.length || 0} Items + + + + + + + )) + ) : ( + + + No configurations found. + + + )} + +
+
+
+
+ ); +} diff --git a/apps/web/src/apps/showcase/showcase-view.tsx b/apps/web/src/apps/showcase/showcase-view.tsx index 5cf52bf..eda7047 100644 --- a/apps/web/src/apps/showcase/showcase-view.tsx +++ b/apps/web/src/apps/showcase/showcase-view.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { ColorSchemeType, DensityType } from '@repo/ui/provider'; import { Button, @@ -18,10 +19,15 @@ import { Table, Badge, Divider, + Tabs, + Box, + Paper, } from '@repo/ui/components'; +import { ShieldCheck, Database, Lock, Layout, Activity, Printer } from 'lucide-react'; import PrinterList from './printer-list'; import ExamplePage from './example/example.page'; import EventsDemoPage from './events-demo'; +import PouchSample from './pouch-sample'; interface ShowcaseViewProps { colorScheme: ColorSchemeType; @@ -31,6 +37,8 @@ interface ShowcaseViewProps { } export default function ShowcaseView({ colorScheme, setColorScheme, density, setDensity }: ShowcaseViewProps) { + const [activeTab, setActiveTab] = useState('ui-components'); + // Mock data for the table const tableData = [ { id: 'ORD-001', customer: 'John Doe', status: 'Shipped', total: '$120.00' }, @@ -38,194 +46,245 @@ export default function ShowcaseView({ colorScheme, setColorScheme, density, set { id: 'ORD-003', customer: 'Acme Corp', status: 'Delivered', total: '$1,250.00' }, ]; + const getSubtitle = () => { + switch (activeTab) { + case 'rbac': return 'Role-Based Access Control and Permissions'; + case 'storage': return 'Offline-First PouchDB Synchronization'; + case 'auth': return 'Authentication & Security Layers'; + case 'ui-components': return 'Theme, Typography, Forms & Data Grids'; + case 'events': return 'Global Event Bus Synchronization'; + case 'hardware': return 'Hardware Integration & Printers'; + default: return 'Architecture Showcase'; + } + }; + return ( - - - Super App UI Showcase + + + + + Eigen ERP + Architecture Showcase + - {/* ========================================= - CONTROL PANEL - ========================================= */} - - - Theme Controls - - - setDensity((val as DensityType) || 'standard')} - data={[ - { value: 'compact', label: 'Compact (ERP Mode)' }, - { value: 'standard', label: 'Standard (UI Mode)' }, - ]} - /> - - + }> + UI Components + + }> + Offline Storage + + }> + RBAC Engine + + }> + Auth & Security + + }> + Events + + }> + Hardware + + - {/* ========================================= - TAILWIND V4 BRIDGE TEST - ========================================= */} - - - Tailwind v4 Synchronization - - {/* This div purely uses Tailwind classes to prove it inherits Mantine's variables */} -
- Tailwind works! The padding (p-md), border-radius (rounded-md), text size - (text-base), and background color of this box are entirely controlled by the ThemeProvider's current state. -
-
- - {/* ========================================= - TYPOGRAPHY & BUTTONS - ========================================= */} - - -
- - Typography & Badges - - - This is dimmed small text indicating a subtitle. - - - This is standard text describing the components below. Watch how the font changes when you switch - density. - - - Brand Badge - - Success Status - - - Error State - - -
- - - -
- - Buttons - - - - - - - -
-
-
- - {/* ========================================= - COMPLEX FORMS (ERP STYLE) - ========================================= */} - - - Form Elements - - - - - + + {/* Header */} + + + + Architecture Showcase + {getSubtitle()} + + - - - - + {/* Scrollable Content Area */} + + + + {/* --- UI COMPONENTS TAB --- */} + {activeTab === 'ui-components' && ( + + {/* Control Panel */} + + Theme Controls + + setDensity((val as DensityType) || 'standard')} + data={[{ value: 'compact', label: 'Compact (ERP Mode)' }, { value: 'standard', label: 'Standard (UI Mode)' }]} + /> + + -