Electron Desktop Target
Phase ID: PHASE-C
Priority: 🟡 Medium (Second Priority)
Estimated Duration: 3-4 weeks
Status: Planning
Dependencies: Phase A (BYOB Backend), Phase E (Target System Core)
Last Updated: 2025-12-28
Executive Summary
Enable Noodl users to build native desktop applications for Windows, macOS, and Linux using Electron. Desktop apps unlock capabilities impossible in web browsers: file system access, system processes, native dialogs, and offline-first operation.
Strategic Advantage
Noodl's editor is already built on Electron (packages/noodl-platform-electron/), providing deep institutional knowledge of Electron patterns, IPC communication, and native integration.
Value Proposition
| Capability | Web | Electron Desktop |
|---|---|---|
| File System Access | ❌ Limited (File API) | ✅ Full read/write/watch |
| Run System Processes | ❌ No | ✅ Spawn any executable (FFmpeg, Ollama, Python) |
| Native Dialogs | ❌ Browser dialogs | ✅ OS-native file pickers, alerts |
| System Tray | ❌ No | ✅ Tray icon with menu |
| Desktop Notifications | ⚠️ Limited | ✅ Native OS notifications |
| Offline Operation | ⚠️ PWA only | ✅ Full offline support |
| No CORS Restrictions | ❌ Blocked | ✅ Direct API access |
| Auto-Updates | ❌ No | ✅ Built-in updater |
Use Cases
- Local AI Applications - Run Ollama, LM Studio, or other local LLMs
- File Processing Tools - Batch rename, image conversion, video encoding
- Developer Tools - Code generators, project scaffolders, CLI wrappers
- Data Analysis - Process local CSV/Excel files, generate reports
- Automation Tools - File watchers, backup utilities, sync tools
- Kiosk Applications - Point of sale, digital signage, information displays
Technical Architecture
Electron Process Model
┌─────────────────────────────────────────────────────────────────────┐
│ ELECTRON APP │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ MAIN PROCESS │ │
│ │ (Node.js - full system access) │ │
│ │ │ │
│ │ • File system operations (fs) │ │
│ │ • Spawn child processes (child_process) │ │
│ │ • System dialogs (dialog) │ │
│ │ • System tray (Tray) │ │
│ │ • Auto-updates (autoUpdater) │ │
│ │ • Native menus (Menu) │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │ IPC │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ RENDERER PROCESS │ │
│ │ (Chromium - your Noodl app) │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ NOODL APP │ │ │
│ │ │ [File Read Node] ──▶ IPC ──▶ [Main Process] ──▶ fs.read │ │ │
│ │ │ [Run Process] ──▶ IPC ──▶ [Main Process] ──▶ spawn │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Preload Script (Bridge between renderer and main) │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
Security Model
Electron's security is critical. We use context isolation and preload scripts:
// main.ts - Main process
const win = new BrowserWindow({
webPreferences: {
// Security settings
nodeIntegration: false, // Don't expose Node in renderer
contextIsolation: true, // Isolate preload from renderer
sandbox: true, // Sandbox renderer process
preload: path.join(__dirname, 'preload.js'),
}
});
// preload.ts - Secure bridge
const { contextBridge, ipcRenderer } = require('electron');
// Expose ONLY specific, validated APIs
contextBridge.exposeInMainWorld('electronAPI', {
// File operations (validated paths only)
readFile: (filePath: string) => {
// Validate path is within allowed directories
if (!isPathAllowed(filePath)) {
throw new Error('Access denied: path outside allowed directories');
}
return ipcRenderer.invoke('fs:readFile', filePath);
},
writeFile: (filePath: string, content: string) => {
if (!isPathAllowed(filePath)) {
throw new Error('Access denied');
}
return ipcRenderer.invoke('fs:writeFile', filePath, content);
},
// Dialog operations (safe - user-initiated)
showOpenDialog: (options) => ipcRenderer.invoke('dialog:open', options),
showSaveDialog: (options) => ipcRenderer.invoke('dialog:save', options),
// Process operations (controlled)
runProcess: (command: string, args: string[]) => {
// Only allow whitelisted commands
if (!isCommandAllowed(command)) {
throw new Error('Command not allowed');
}
return ipcRenderer.invoke('process:run', command, args);
},
});
Electron-Specific Nodes
File System Nodes
Read File
┌─────────────────────────────────────────────────────────────────────┐
│ 📄 Read File [🖥️] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ INPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Read ○──── (Signal) │
│ │
│ File Path: [ ] [📁] │
│ Or connect from File Picker output │
│ │
│ Encoding: [UTF-8 ▾] │
│ ├─ UTF-8 │
│ ├─ ASCII │
│ ├─ Base64 │
│ └─ Binary (Buffer) │
│ │
│ OUTPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Content ────○ (String or Buffer) │
│ File Name ────○ (String) │
│ File Size ────○ (Number - bytes) │
│ Last Modified ────○ (Date) │
│ │
│ Success ────○ (Signal) │
│ Error ────○ (Signal) │
│ Error Message ────○ (String) │
│ │
└─────────────────────────────────────────────────────────────────────┘
Write File
┌─────────────────────────────────────────────────────────────────────┐
│ 💾 Write File [🖥️] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ INPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Write ○──── (Signal) │
│ │
│ File Path: [ ] [📁] │
│ Content: ○──── (String or Buffer) │
│ │
│ Encoding: [UTF-8 ▾] │
│ Create Directories: [✓] (Create parent dirs if missing) │
│ Overwrite: [✓] (Overwrite if exists) │
│ │
│ OUTPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Written Path ────○ (String - full path) │
│ Success ────○ (Signal) │
│ Error ────○ (Signal) │
│ Error Message ────○ (String) │
│ │
└─────────────────────────────────────────────────────────────────────┘
Watch Directory
┌─────────────────────────────────────────────────────────────────────┐
│ 👁️ Watch Directory [🖥️] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ INPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Start Watching ○──── (Signal) │
│ Stop Watching ○──── (Signal) │
│ │
│ Directory Path: [ ] [📁] │
│ │
│ Watch Subdirectories: [✓] │
│ File Filter: [*.* ] (glob pattern) │
│ Debounce (ms): [100 ] │
│ │
│ OUTPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ File Path ────○ (String - changed file) │
│ Event Type ────○ (String - add/change/unlink) │
│ │
│ File Added ────○ (Signal) │
│ File Changed ────○ (Signal) │
│ File Removed ────○ (Signal) │
│ │
│ Is Watching ────○ (Boolean) │
│ Error ────○ (Signal) │
│ │
└─────────────────────────────────────────────────────────────────────┘
Native File Picker
┌─────────────────────────────────────────────────────────────────────┐
│ 📂 File Picker [🖥️] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ INPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Open Picker ○──── (Signal) │
│ │
│ Mode: [Open File ▾] │
│ ├─ Open File │
│ ├─ Open Multiple Files │
│ ├─ Open Directory │
│ └─ Save File │
│ │
│ Title: [Select a file ] │
│ │
│ File Types: [Images ▾] [+ Add] │
│ ┌─────────────────────────────────────┐ │
│ │ Name: Images │ │
│ │ Extensions: jpg, jpeg, png, gif │ │
│ └─────────────────────────────────────┘ │
│ │
│ Default Path: [/Users/richard/Documents ] [📁] │
│ │
│ OUTPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Selected Path ────○ (String) │
│ Selected Paths ────○ (Array - for multi-select) │
│ File Name ────○ (String - just filename) │
│ │
│ Selected ────○ (Signal - user made selection) │
│ Cancelled ────○ (Signal - user cancelled) │
│ │
└─────────────────────────────────────────────────────────────────────┘
Process Nodes
Run Process
┌─────────────────────────────────────────────────────────────────────┐
│ ⚙️ Run Process [🖥️] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ INPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Run ○──── (Signal) │
│ Kill ○──── (Signal - terminate process) │
│ │
│ Command: [ffmpeg ] │
│ │
│ Arguments: (Array of strings) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ [-i, input.mp4, -c:v, libx264, output.mp4] │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ Or connect from Array node │
│ │
│ Working Directory: [/Users/richard/projects] [📁] │
│ │
│ Environment Variables: (Object) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ { "PATH": "/usr/local/bin", "NODE_ENV": "production" } │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ Shell: [✓] (Run in shell - enables pipes) │
│ Timeout (ms): [0 ] (0 = no timeout) │
│ │
│ OUTPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ stdout ────○ (String - standard output) │
│ stderr ────○ (String - standard error) │
│ Exit Code ────○ (Number) │
│ │
│ On Output ────○ (Signal - fires on each line) │
│ Output Line ────○ (String - current output line) │
│ │
│ Started ────○ (Signal) │
│ Completed ────○ (Signal - exit code 0) │
│ Failed ────○ (Signal - non-zero exit) │
│ Is Running ────○ (Boolean) │
│ │
└─────────────────────────────────────────────────────────────────────┘
Implementation:
// packages/noodl-runtime/src/nodes/electron/RunProcessNode.ts
import { spawn, ChildProcess } from 'child_process';
import { createNodeDefinition } from '../../nodeDefinition';
export const RunProcessNode = createNodeDefinition({
name: 'Run Process',
category: 'System',
color: 'red',
inputs: {
run: { type: 'signal', displayName: 'Run' },
kill: { type: 'signal', displayName: 'Kill' },
command: { type: 'string', displayName: 'Command' },
args: { type: 'array', displayName: 'Arguments', default: [] },
cwd: { type: 'string', displayName: 'Working Directory' },
env: { type: 'object', displayName: 'Environment Variables' },
shell: { type: 'boolean', displayName: 'Shell', default: false },
timeout: { type: 'number', displayName: 'Timeout (ms)', default: 0 },
},
outputs: {
stdout: { type: 'string', displayName: 'stdout' },
stderr: { type: 'string', displayName: 'stderr' },
exitCode: { type: 'number', displayName: 'Exit Code' },
onOutput: { type: 'signal', displayName: 'On Output' },
outputLine: { type: 'string', displayName: 'Output Line' },
started: { type: 'signal', displayName: 'Started' },
completed: { type: 'signal', displayName: 'Completed' },
failed: { type: 'signal', displayName: 'Failed' },
isRunning: { type: 'boolean', displayName: 'Is Running' },
},
targetCompatibility: ['electron'],
state: {
process: null as ChildProcess | null,
stdoutBuffer: '',
stderrBuffer: '',
},
signalHandlers: {
run: async function(inputs, outputs, state) {
// Validate command against whitelist (security)
if (!await this.validateCommand(inputs.command)) {
outputs.stderr = 'Command not allowed by security policy';
outputs.failed.trigger();
return;
}
const options = {
cwd: inputs.cwd || process.cwd(),
env: { ...process.env, ...inputs.env },
shell: inputs.shell,
timeout: inputs.timeout || undefined,
};
state.stdoutBuffer = '';
state.stderrBuffer = '';
outputs.isRunning = true;
state.process = spawn(inputs.command, inputs.args, options);
outputs.started.trigger();
state.process.stdout?.on('data', (data) => {
const text = data.toString();
state.stdoutBuffer += text;
outputs.stdout = state.stdoutBuffer;
// Emit line-by-line
const lines = text.split('\n');
for (const line of lines) {
if (line.trim()) {
outputs.outputLine = line;
outputs.onOutput.trigger();
}
}
});
state.process.stderr?.on('data', (data) => {
state.stderrBuffer += data.toString();
outputs.stderr = state.stderrBuffer;
});
state.process.on('close', (code) => {
outputs.exitCode = code ?? -1;
outputs.isRunning = false;
state.process = null;
if (code === 0) {
outputs.completed.trigger();
} else {
outputs.failed.trigger();
}
});
state.process.on('error', (err) => {
outputs.stderr = err.message;
outputs.isRunning = false;
outputs.failed.trigger();
});
},
kill: function(inputs, outputs, state) {
if (state.process) {
state.process.kill();
outputs.isRunning = false;
}
},
},
});
Window Nodes
Window Control
┌─────────────────────────────────────────────────────────────────────┐
│ 🪟 Window Control [🖥️] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ INPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Minimize ○──── (Signal) │
│ Maximize ○──── (Signal) │
│ Restore ○──── (Signal) │
│ Close ○──── (Signal) │
│ Toggle Fullscreen ○──── (Signal) │
│ │
│ Set Size ○──── (Signal) │
│ Width: [800 ] │
│ Height: [600 ] │
│ │
│ Set Position ○──── (Signal) │
│ X: [100 ] │
│ Y: [100 ] │
│ │
│ Always On Top: [○] │
│ Resizable: [✓] │
│ │
│ OUTPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Current Width ────○ (Number) │
│ Current Height ────○ (Number) │
│ Is Maximized ────○ (Boolean) │
│ Is Fullscreen ────○ (Boolean) │
│ Is Focused ────○ (Boolean) │
│ │
└─────────────────────────────────────────────────────────────────────┘
System Tray
┌─────────────────────────────────────────────────────────────────────┐
│ 🔔 System Tray [🖥️] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ INPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Show ○──── (Signal) │
│ Hide ○──── (Signal) │
│ │
│ Icon: [ ] [📁 Select Image] │
│ Tooltip: [My App ] │
│ │
│ Menu Items: (Array of menu items) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ [ │ │
│ │ { "label": "Show Window", "action": "show" }, │ │
│ │ { "type": "separator" }, │ │
│ │ { "label": "Settings", "action": "settings" }, │ │
│ │ { "type": "separator" }, │ │
│ │ { "label": "Quit", "action": "quit" } │ │
│ │ ] │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ OUTPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Menu Action ────○ (String - action from clicked item) │
│ Clicked ────○ (Signal - tray icon clicked) │
│ Double Clicked ────○ (Signal) │
│ │
└─────────────────────────────────────────────────────────────────────┘
Additional Electron Nodes
| Node | Category | Description |
|---|---|---|
| Native Notification | System | OS-native notifications with actions |
| Clipboard | Utility | Read/write clipboard (text, images) |
| Screen Info | System | Get display info, cursor position |
| Power Monitor | System | Battery status, suspend/resume events |
| App Info | System | Get app version, paths, locale |
| Protocol Handler | System | Register custom URL protocols |
| Auto Launch | System | Start app on system boot |
| Global Shortcut | Input | Register system-wide hotkeys |
Preview Mode
Electron Preview in Editor
When Electron target is selected, preview can run with Node.js integration enabled:
┌─────────────────────────────────────────────────────────────────────┐
│ Preview Mode: [Electron (Desktop) ▾] │
│ ├─ Web (Browser) │
│ └─ Electron (Desktop) ◀ │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ⚠️ ELECTRON PREVIEW MODE │
│ │
│ Preview is running with full desktop capabilities enabled. │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ✓ File system access enabled │ │
│ │ ✓ Process execution enabled │ │
│ │ ✓ Native dialogs enabled │ │
│ │ ✓ System tray available │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ Security: Operations are sandboxed to your project directory │
│ │
│ [Switch to Web Preview] │
│ │
└─────────────────────────────────────────────────────────────────────┘
Since Noodl editor already runs in Electron, enabling desktop features in preview is straightforward:
// In viewer.js when Electron preview mode is enabled
if (previewMode === 'electron') {
// Enable IPC bridge to main process
window.noodlElectronBridge = {
readFile: async (path) => ipcRenderer.invoke('fs:readFile', path),
writeFile: async (path, content) => ipcRenderer.invoke('fs:writeFile', path, content),
// ... other APIs
};
}
Export Pipeline
Export Dialog
┌─────────────────────────────────────────────────────────────────────┐
│ Export Desktop App [×] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ PLATFORM │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ [Apple] │ │ [Windows] │ │ [Linux] │ │
│ │ macOS │ │ Windows │ │ Linux │ │
│ │ ✓ Selected │ │ ✓ Selected │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ APP CONFIGURATION │
│ │
│ App Name: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ My Desktop App │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ App ID: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ com.mycompany.mydesktopapp │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ Version: Build Number: │
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ │ 1.0.0 │ │ 1 │ │
│ └─────────────────────────┘ └─────────────────────────┘ │
│ │
│ ▶ macOS Settings │
│ Category: [Productivity ▾] │
│ Code Signing: [Developer ID ▾] │
│ Notarization: [✓] (Required for distribution) │
│ │
│ ▶ Windows Settings │
│ Code Signing: [None ▾] │
│ Installer Type: [NSIS ▾] │
│ │
│ ▶ App Icons │
│ ┌─────────────────┐ │
│ │ [App Icon] │ [📁 Select Icon] │
│ │ 512x512 PNG │ │
│ └─────────────────┘ │
│ ⓘ Will be converted to .icns (macOS) and .ico (Windows) │
│ │
│ ▶ Permissions │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ☑ File System Access (Required by: Read File, ...) │ │
│ │ ☑ Process Execution (Required by: Run Process) │ │
│ │ ☐ Camera Access │ │
│ │ ☐ Microphone Access │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ OUTPUT │
│ ● Installable Package (.dmg, .exe, .deb) RECOMMENDED │
│ ○ Portable App (zip folder) │
│ ○ Development Build (unpackaged, for testing) │
│ │
│ [Cancel] [Build for macOS] │
│ [Build for Windows] │
│ [Build All] │
└─────────────────────────────────────────────────────────────────────┘
Generated Project Structure
my-app-electron/
├── package.json # Electron app dependencies
├── electron-builder.yml # Build configuration
├── src/
│ ├── main/
│ │ ├── main.ts # Main process entry
│ │ ├── preload.ts # Preload script (IPC bridge)
│ │ ├── ipc/
│ │ │ ├── fs.ts # File system handlers
│ │ │ ├── dialog.ts # Dialog handlers
│ │ │ ├── process.ts # Process handlers
│ │ │ └── index.ts
│ │ └── menu.ts # Application menu
│ └── renderer/
│ ├── index.html # App entry point
│ ├── main.js # Noodl runtime bundle
│ ├── styles.css
│ └── assets/
├── resources/
│ ├── icon.icns # macOS icon
│ ├── icon.ico # Windows icon
│ └── icon.png # Linux icon
└── dist/ # Build output
├── mac/
│ └── My App.dmg
└── win/
└── My App Setup.exe
Implementation Phases
Phase C.1: Electron Runtime Package (1 week)
Goal: Create separate runtime package for Electron-specific functionality.
Tasks:
- Create
packages/noodl-runtime-electron/package - Implement secure IPC bridge (preload.ts)
- Implement main process handlers (fs, dialog, process)
- Create sandbox validation utilities
- Set up security policy configuration
Files to Create:
packages/noodl-runtime-electron/
├── package.json
├── tsconfig.json
├── src/
│ ├── index.ts
│ ├── main/
│ │ ├── main.ts # Main process entry
│ │ ├── preload.ts # Preload script
│ │ └── handlers/
│ │ ├── fs.ts
│ │ ├── dialog.ts
│ │ ├── process.ts
│ │ ├── window.ts
│ │ └── system.ts
│ ├── security/
│ │ ├── pathValidator.ts # Validate file paths
│ │ ├── commandWhitelist.ts # Allowed commands
│ │ └── permissions.ts
│ └── types.ts
└── test/
Phase C.2: Electron-Specific Nodes (1 week)
Goal: Implement desktop capability nodes.
Tasks:
- File System nodes (Read, Write, Watch, Picker)
- Process nodes (Run, Kill)
- Window nodes (Control, Tray)
- System nodes (Notification, Clipboard, App Info)
- Register nodes in Electron target only
Files to Create:
packages/noodl-runtime/src/nodes/electron/
├── index.ts
├── fs/
│ ├── ReadFileNode.ts
│ ├── WriteFileNode.ts
│ ├── WatchDirectoryNode.ts
│ └── FilePickerNode.ts
├── process/
│ ├── RunProcessNode.ts
│ └── ProcessInfoNode.ts
├── window/
│ ├── WindowControlNode.ts
│ └── SystemTrayNode.ts
└── system/
├── NotificationNode.ts
├── ClipboardNode.ts
└── AppInfoNode.ts
Phase C.3: Electron Preview Mode (3-4 days)
Goal: Enable desktop features in editor preview.
Tasks:
- Add Electron preview mode option
- Enable IPC bridge in preview window
- Create security sandbox for preview
- Add visual indicators for Electron mode
- Test all nodes in preview context
Phase C.4: Electron Packaging (1 week)
Goal: Export production-ready desktop applications.
Tasks:
- Integrate electron-builder
- Generate main.ts from project configuration
- Generate preload.ts with used features
- Bundle Noodl app as renderer
- Configure code signing (macOS, Windows)
- Generate installer packages
- Create auto-update configuration
Files to Create:
packages/noodl-editor/src/editor/src/export/electron/
├── ElectronExporter.ts
├── mainGenerator.ts
├── preloadGenerator.ts
├── builderConfig.ts
├── iconConverter.ts
└── templates/
├── main.ts.template
├── preload.ts.template
└── electron-builder.yml.template
Security Considerations
Path Validation
All file system operations must validate paths:
class PathValidator {
private allowedPaths: string[];
constructor(projectPath: string) {
this.allowedPaths = [
projectPath,
app.getPath('documents'),
app.getPath('downloads'),
app.getPath('temp'),
];
}
isPathAllowed(targetPath: string): boolean {
const resolved = path.resolve(targetPath);
// Check if path is within allowed directories
return this.allowedPaths.some(allowed =>
resolved.startsWith(path.resolve(allowed))
);
}
// Prevent path traversal attacks
sanitizePath(inputPath: string): string {
// Remove .. and normalize
return path.normalize(inputPath).replace(/\.\./g, '');
}
}
Command Whitelist
Only allow specific commands to be executed:
const ALLOWED_COMMANDS = [
// Media processing
'ffmpeg',
'ffprobe',
'imagemagick',
// AI/ML
'ollama',
'python',
'python3',
// Utilities
'git',
'npm',
'npx',
'node',
];
function isCommandAllowed(command: string): boolean {
const base = path.basename(command);
return ALLOWED_COMMANDS.includes(base);
}
Permission System
interface ElectronPermissions {
fileSystem: {
read: boolean;
write: boolean;
allowedPaths: string[];
};
process: {
execute: boolean;
allowedCommands: string[];
};
window: {
control: boolean;
tray: boolean;
};
system: {
notifications: boolean;
clipboard: boolean;
autoLaunch: boolean;
};
}
Success Criteria
| Criteria | Target |
|---|---|
| Build time | < 2 minutes for production build |
| App size | < 150MB for minimal app |
| Startup time | < 3 seconds to first render |
| File operations | < 50ms overhead vs raw Node.js |
| All nodes tested | On macOS, Windows, Linux |
Future Enhancements
- Native Node Modules - Allow npm packages with native code
- Auto-Update System - Built-in update mechanism
- Crash Reporting - Integrate crash reporting service
- Hardware Access - Serial ports, USB devices, Bluetooth
- Multiple Windows - Open additional windows from nodes