Files
trackgo-fe/apps/desktop/docs/IPC_ARCHITECTURE.md
T
Firman Ramdhani d3eb242ebe docs: Enhance documentation across multiple modules for clarity and structure
- Updated CONFIGURATION.md to improve navigation and added mermaid diagrams for better visualization of processes.
- Revised IPC_ARCHITECTURE.md to clarify the security model and added diagrams to illustrate the architecture.
- Improved README.md files in core-api, core-events, core-i18n, and core-storage for consistency and clarity, including better descriptions and structural enhancements.
2026-05-29 16:21:25 +07:00

19 KiB

← Back to Root

IPC Architecture & Security Model

The Secure Communication Blueprint.

This document defines the Hardened Security Perimeter and communication topology governing the Desktop Wrapper. Every native capability exposed to the renderer is mediated through a Non-Bypassable IPC Bridge, ensuring that the Node.js Main Process remains cryptographically and logically isolated from untrusted web content. Adherence to this document is mandatory — deviations constitute security violations subject to immediate remediation.


Table of Contents


Privilege Separation Model

The desktop wrapper enforces a strict privilege separation between three execution contexts, each operating under fundamentally different trust levels. This architecture ensures that a compromise in any single layer cannot escalate to full system access.

Trust Level Matrix

Context Trust Level Privilege Scope Security Guarantee
Main Process Fully Trusted Unrestricted Node.js access: filesystem, network, printers, OS APIs, child processes Only code authored by the engineering team executes here
Preload Script Controlled Restricted to ipcRenderer.invoke() and ipcRenderer.send() — no direct Node.js access Executes in a Hermetically Sealed Context — isolated from both the Main Process globals and the Renderer's DOM
Renderer Untrusted Standard browser sandbox — zero Node.js API surface Designated as a Zero-Trust Environment — may execute third-party code, npm packages, or XSS payloads

Process Topology

graph TD
    %% ─── Styling Definitions (Dark-Mode Friendly Enterprise Palette) ───
    classDef trustedLayer fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#0f172a
    classDef gatewayLayer fill:#f0fdf4,stroke:#10b981,stroke-width:2px,color:#064e3b
    classDef untrustedLayer fill:#fef2f2,stroke:#ef4444,stroke-width:2px,color:#7f1d1d
    classDef functionNode fill:#e2e8f0,stroke:#64748b,stroke-width:2px,color:#0f172a

    %% ─── Subgraphs ───
    subgraph Main ["MAIN PROCESS [Fully Trusted]"]
        M_DESC["Unrestricted Node.js Privileges"]
        IPC_MAIN_H[ipcMain.handle]
        IPC_MAIN_O[ipcMain.on]
    end

    subgraph Preload ["PRELOAD SCRIPT [Secure Gateway]"]
        P_DESC["Hermetically Sealed Context (Interface Narrowing)"]
        CTX_BRIDGE{contextBridge.exposeInMainWorld}
    end

    subgraph Renderer ["RENDERER [Zero-Trust Environment]"]
        R_DESC["Standard Browser Sandbox (No Node.js APIs)"]
        E_API([window.electronAPI])
    end

    %% ─── Flow & Relationships ───
    E_API ===>|Only Authorized Vector| CTX_BRIDGE
    CTX_BRIDGE --->|Transforms to narrow IPC calls| IPC_MAIN_H
    CTX_BRIDGE --->|Transforms to narrow IPC calls| IPC_MAIN_O

    %% ─── Apply Styles ───
    class Main trustedLayer;
    class Preload gatewayLayer;
    class Renderer untrustedLayer;
    class M_DESC,P_DESC,R_DESC,IPC_MAIN_H,IPC_MAIN_O functionNode;
    class CTX_BRIDGE gatewayLayer;
    class E_API untrustedLayer;

    %% ─── Subgraph Backgrounds (Transparent for Native GitHub Support) ───
    style Main fill:transparent,stroke:#3b82f6,stroke-width:2px,stroke-dasharray: 5 5
    style Preload fill:transparent,stroke:#10b981,stroke-width:2px,stroke-dasharray: 5 5
    style Renderer fill:transparent,stroke:#ef4444,stroke-width:2px,stroke-dasharray: 5 5

The Preload Script functions as a Secure Gateway that performs Interface Narrowing — it transforms the broad, unrestricted IPC capabilities of the Main Process into a deliberately narrow, type-safe API surface. The renderer communicates with native functionality exclusively through this gateway. There are no alternative paths, no escape hatches, and no backdoors.

Enforcement Configuration

These settings are declared in BrowserWindow.webPreferences and are non-negotiable:

Setting Value Enforcement
contextIsolation true The Preload executes in a hermetically sealed V8 context. The renderer cannot access require(), Node.js globals, or any variable from the preload's scope.
nodeIntegration false Zero Node.js API surface in the renderer. fs, child_process, os, net, and all built-in modules are completely unavailable.
sandbox true The renderer process runs inside a Chromium OS-level sandbox, restricting system calls and file access at the kernel level.
webSecurity true The same-origin policy is strictly enforced, preventing cross-origin data exfiltration from the renderer.

Standard Operating Procedure: The Three-Step Bridge

Every native feature in this architecture must follow the Three-Step Bridge — a Standard Operating Procedure (SOP) that ensures traceability, type-safety, and auditability across the entire IPC surface.

[!IMPORTANT] > Deterministic Synchronization: Maintaining parity between the Main Process handler, the Preload Gateway exposure, and the TypeScript interface declaration is mandatory. A mismatch between any two of the three layers will result in either a Type-Safety Gap (silent failures in development) or a Runtime Regression (crashes in production).

Step 1: Register the Handler — Main Process

File: apps/desktop/src/main/index.ts

// COMMAND PATTERN: Use ipcMain.handle for request/response operations
// The handler returns a value to the renderer via a resolved Promise.
ipcMain.handle('feature:action', async (_event, arg1: string, arg2: number) => {
  // Validate inputs. Never trust data from the renderer.
  if (typeof arg1 !== 'string' || typeof arg2 !== 'number') {
    throw new Error('Invalid arguments');
  }
  const result = await someNativeAPI(arg1, arg2);
  return result;
});

// EVENT PATTERN: Use ipcMain.on for fire-and-forget operations
// No return value — the renderer does not wait for a response.
ipcMain.on('feature:fire', (_event, data: SomeType) => {
  performSideEffect(data);
});

Channel naming convention: namespace:action — examples: printer:get-list, updater:check, app:get-version. Namespaces must be unique, descriptive, and never generic.

Step 2: Expose via contextBridge — Preload Gateway

File: apps/desktop/src/preload/index.ts

const electronAPI = {
  // Command pattern exposure
  featureAction: (arg1: string, arg2: number): Promise<ResultType> => {
    return ipcRenderer.invoke('feature:action', arg1, arg2);
  },

  // Event pattern exposure
  featureFire: (data: SomeType): void => {
    ipcRenderer.send('feature:fire', data);
  },

  // Main→Renderer push events (with automatic lifecycle cleanup)
  onFeatureEvent: createEventSubscription<EventDataType>('feature:event'),
};

contextBridge.exposeInMainWorld('electronAPI', electronAPI);

Non-negotiable rules:

  • Never expose ipcRenderer directly — this is a Catastrophic Failure pattern.
  • Never expose ipcRenderer.on without cleanup — use createEventSubscription(), which returns an unsubscribe function for React useEffect lifecycle management.
  • Always declare explicit TypeScript types for all function signatures.

Step 3: Declare the Interface — React Application

File: apps/web/src/types/electron.d.ts

interface ElectronAPI {
  // ... existing methods ...

  featureAction: (arg1: string, arg2: number) => Promise<ResultType>;
  featureFire: (data: SomeType) => void;
  onFeatureEvent: (callback: (data: EventDataType) => void) => () => void;
}

Verified Channel Manifest

The following is the complete, authoritative registry of all authorized IPC channels. These channels are the only permitted vectors for native interaction. Any IPC channel not listed here is unauthorized and must be treated as a security anomaly.

Printer Subsystem

Channel Direction Pattern Payload Access Control
printer:get-list Renderer → Main → Renderer invoke / handle Returns ElectronPrinterInfo[] Read-only hardware enumeration
printer:print Renderer → Main → Renderer invoke / handle Accepts ElectronPrintOptions, returns { success, failureReason? } Controlled hardware invocation

Main process handler: setupPrinterIPC() in src/main/index.ts

Preload Gateway surface:

getPrinters: () => ipcRenderer.invoke('printer:get-list');
print: (options?) => ipcRenderer.invoke('printer:print', options);

React consumption hook: useElectronPrinter() in apps/web/src/hooks/use-electron-printer.ts


Auto-Updater Subsystem

Channel Direction Pattern Payload Access Control
updater:check Renderer → Main invoke / handle Returns update check result Read-only version query
updater:install Renderer → Main send / on No payload Privileged: quits app and installs
updater:checking Main → Renderer send No payload Status notification
updater:available Main → Renderer send UpdateInfo { version, releaseDate, releaseNotes } Status notification
updater:not-available Main → Renderer send UpdateInfo Status notification
updater:progress Main → Renderer send ProgressInfo { percent, bytesPerSecond, transferred, total } Progress telemetry
updater:downloaded Main → Renderer send UpdateInfo Status notification
updater:error Main → Renderer send Error message string Error telemetry

Main process handlers: setupAutoUpdaterIPC() + setupAutoUpdaterEvents() in src/main/index.ts

React consumption hook: useElectronUpdater() in apps/web/src/hooks/use-electron-updater.ts

Note

The updater:install channel is the highest-privilege IPC operation in the system — it terminates the running process and launches a new binary. It should only be triggered by an explicit user action, never automatically.


Extending the Bridge: Guided Walkthrough

Scenario: Expose the application version to the React UI.

1. Main Process — Register Handler

// In app.whenReady() callback, src/main/index.ts
ipcMain.handle('app:get-version', () => {
  return app.getVersion();
});

2. Preload Gateway — Expose Method

// Add to the electronAPI object, src/preload/index.ts
const electronAPI = {
  // ... existing methods ...
  getAppVersion: (): Promise<string> => {
    return ipcRenderer.invoke('app:get-version');
  },
};

3. TypeScript Interface — Declare Type

// Add to ElectronAPI interface, apps/web/src/types/electron.d.ts
interface ElectronAPI {
  // ... existing methods ...
  getAppVersion: () => Promise<string>;
}

4. React — Consume

function VersionBadge() {
  const [version, setVersion] = useState('');

  useEffect(() => {
    if (window.electronAPI) {
      window.electronAPI.getAppVersion().then(setVersion);
    }
  }, []);

  if (!version) return null;
  return <span className="version-badge">v{version}</span>;
}

5. Update This Manifest

After implementing a new channel, add it to the Verified Channel Manifest in this document. Undocumented channels are unauthorized channels.


Critical Audit Checklist: Anti-Patterns

The following patterns constitute critical security violations. Each one expands the attack surface from "browser-level sandboxed web content" to "unrestricted OS-level code execution." Their presence in production code warrants immediate incident response.


Exposing raw ipcRenderer

// VIOLATION: Catastrophic Failure — Total Attack Surface Expansion
contextBridge.exposeInMainWorld('ipc', ipcRenderer);

Threat: The renderer gains unrestricted IPC access — it can invoke any channel, including channels that were never intended to be callable from the renderer. A single XSS vulnerability escalates to arbitrary native code execution.

Classification: Total System Compromise


Exposing require or Node.js APIs

// VIOLATION: Unauthenticated Code Execution
contextBridge.exposeInMainWorld('require', require);

Threat: The renderer can require('child_process').exec('rm -rf /'). A single XSS vulnerability in any dependency — including transitive ones — escalates to full filesystem access, credential theft, reverse shells, and data exfiltration.

Classification: Total System Compromise


Enabling nodeIntegration

// VIOLATION: Catastrophic Failure — Complete Boundary Collapse
new BrowserWindow({
  webPreferences: { nodeIntegration: true, contextIsolation: false },
});

Threat: Every <script> tag in the renderer — including XSS payloads, compromised npm packages, and injected analytics scripts — gains full Node.js capabilities. The isolation boundary ceases to exist.

Classification: Total System Compromise


Passing unsanitized IPC data to shell commands

// VIOLATION: Command Injection — Unauthenticated Code Execution
ipcMain.handle('run-cmd', (_event, cmd: string) => {
  exec(cmd); // The renderer controls the command string
});

Threat: The renderer can execute arbitrary system commands with the privileges of the Electron main process (typically the current user). This is the most direct path from XSS to OS-level compromise.

Classification: Unauthenticated Code Execution


Registering overly broad IPC channels

// VIOLATION: Attack Surface Expansion — Unrestricted File Read
ipcMain.handle('file:read', (_event, path: string) => {
  return readFileSync(path, 'utf-8'); // No validation
});

Threat: The renderer can read any file on the filesystem — SSH keys, environment files, database credentials, browser cookies. Input validation is not optional.

Classification: Sensitive Data Exfiltration


The Gold Standard for Native Integration

The following patterns represent the mandatory standard for all IPC implementations. Adherence is non-negotiable.

Validate and constrain all IPC arguments

// GOLD STANDARD: Input validation, path confinement, scope restriction
ipcMain.handle('file:read', async (_event, filename: string) => {
  // Reject path separators — confine to a single directory
  if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
    throw new Error('Invalid filename');
  }

  // Resolve within a controlled directory only
  const safePath = join(app.getPath('userData'), 'data', filename);

  // Verify the resolved path stays within bounds
  if (!safePath.startsWith(join(app.getPath('userData'), 'data'))) {
    throw new Error('Path traversal detected');
  }

  return readFileSync(safePath, 'utf-8');
});

Principle: Never trust data originating from the renderer. Validate types, constrain scope, and verify resolved paths.


Return unsubscribe functions — Memory Leak Mitigation

// GOLD STANDARD: The createEventSubscription helper ensures automatic cleanup
function createEventSubscription<T>(channel: string) {
  return (callback: (data: T) => void): (() => void) => {
    const handler = (_event: Electron.IpcRendererEvent, data: T) => callback(data);
    ipcRenderer.on(channel, handler);

    // Return an unsubscribe function — critical for React lifecycle
    return () => {
      ipcRenderer.removeListener(channel, handler);
    };
  };
}

In React's useEffect:

useEffect(() => {
  if (!window.electronAPI) return;

  // Subscribe — handler is registered in the Preload's IPC layer
  const unsub = window.electronAPI.onSomeEvent((data) => {
    setState(data);
  });

  // Cleanup on unmount — prevents listener accumulation
  return () => unsub();
}, []);

Principle: Without the unsubscribe pattern, every component mount adds a new IPC listener that persists after unmount. Over time — especially with React's StrictMode double-mounting in development — this causes memory leaks, duplicate event handling, and performance degradation. The createEventSubscription helper enforces automatic, deterministic cleanup tied to React's component lifecycle.


Gate all Electron calls behind runtime detection

// GOLD STANDARD: Environment-safe consumption
function useElectronFeature() {
  const isElectron = typeof window !== 'undefined' && !!window.electronAPI;

  const doSomething = useCallback(() => {
    if (!window.electronAPI) return; // No-op in browser
    window.electronAPI.someMethod();
  }, []);

  return { isElectron, doSomething };
}

Principle: The React app must run identically in both Electron and standard browser environments. All window.electronAPI access must be gated behind a runtime check. Never assume the IPC bridge exists.