Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
613dec9
build: src-node will be directly used from phoenix repo when developm…
abose Jan 28, 2026
41513d7
chore: add getPhNodePath and getSrcNodePath electron api
abose Jan 28, 2026
2f6be15
refacotor: put apis properly
abose Jan 28, 2026
5bd8292
chore: spawn error handling
abose Jan 28, 2026
1437496
feat: trust rin apis in electron edge
abose Jan 28, 2026
b3446e6
chore: pull in missing deps
abose Jan 28, 2026
8b078c2
feat: window management/label apis for electron
abose Jan 28, 2026
cae954c
chore: focus window the electron way
abose Jan 28, 2026
9c6bc1d
shell api migration to electron
abose Jan 28, 2026
caeb1fe
docs: for keeping sync with phoenix-fs repo
abose Jan 28, 2026
aad6edb
chore: single window handling in electron
abose Jan 28, 2026
990710c
chore: release aes trust on window close
abose Jan 28, 2026
69435fd
fix: electron app launching but not working
abose Jan 28, 2026
91ddd9a
chore: add file/folder drop support apis
abose Jan 28, 2026
47d82fc
ci: update cargo deps
abose Jan 28, 2026
5bf0971
fix: electron main files always open when starting app in editor due …
abose Jan 28, 2026
72f1c1d
chore: move to the actual dev location
abose Jan 28, 2026
c8e9207
chore: during dev npm install is preferred to pull in node deps in sr…
abose Jan 29, 2026
f242f39
chore: harden electron security
abose Jan 29, 2026
67870a7
fix: newly spawned untrusted windows shouldnt have electon api inject…
abose Jan 29, 2026
5e156c1
chore: better logging
abose Jan 29, 2026
f404a38
feat: restore window sizes
abose Jan 29, 2026
4ff612f
chore: window size and position persistance
abose Jan 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions src-build/serveForPlatform.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {getPlatformDetails} from "./utils.js";
import {execa} from "execa";
import chalk from "chalk";
import {resolve} from "path";

const {platform} = getPlatformDetails();

Expand Down Expand Up @@ -44,14 +45,18 @@ console.log(`Platform: ${platform}, target: ${target}`);
console.log('\nEnsure to start phoenix server at http://localhost:8000 for development.');
console.log('Follow https://github.com/phcode-dev/phoenix#running-phoenix for instructions.\n');

console.log('Setting up src-node...');
await execa("npm", ["run", "_make_src-node"], {stdio: "inherit"});

// Run platform-specific command
if (target === "tauri") {
console.log('Setting up src-node...');
await execa("npm", ["run", "_make_src-node"], {stdio: "inherit"});

console.log('Starting Tauri dev server...');
await execa("npx", ["tauri", "dev"], {stdio: "inherit"});
} else {
const srcNodePath = resolve("../phoenix/src-node");
console.log(`Running "npm install" in ${srcNodePath}`);
await execa("npm", ["install"], {cwd: srcNodePath, stdio: "inherit"});

console.log('Starting Electron...');
await execa("./src-electron/node_modules/.bin/electron", ["src-electron/main.js"], {stdio: "inherit"});
}
51 changes: 51 additions & 0 deletions src-electron/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Centralized Configuration Module
*
* This module provides a single source of truth for all configuration values.
* It reads from package.json and can apply stage-wise transforms as needed.
*
* Usage:
* const { stage, trustedElectronDomains, productName } = require('./config');
*/

const packageJson = require('./package.json');

// Core package.json values
const name = packageJson.name;
const identifier = packageJson.identifier;
const stage = packageJson.stage;
const version = packageJson.version;
const productName = packageJson.productName;
const description = packageJson.description;

// Security configuration
const trustedElectronDomains = packageJson.trustedElectronDomains || [];

/**
* Initialize configuration (call once at app startup if needed).
* Currently a no-op but can be extended for async config loading,
* environment variable overrides, or stage-wise transforms.
*/
function initConfig() {
// Future: Add stage-wise transforms, env overrides, etc.
// Example:
// if (stage === 'prod') {
// // Apply production-specific config
// }
}

module.exports = {
// Package info
name,
identifier,
stage,
version,
productName,
description,

// Security
trustedElectronDomains,

// Initialization
initConfig
};
91 changes: 91 additions & 0 deletions src-electron/ipc-security.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* IPC Security - Trusted Domain Validation
*
* This module implements security measures to ensure Electron APIs are only
* accessible from trusted origins. Trust is evaluated at window load/navigation
* time (not on every IPC call) for optimal performance.
*
* Trust rules:
* - Dev stage: trustedElectronDomains + all localhost URLs
* - Other stages (staging/prod): only trustedElectronDomains
*/

const { stage, trustedElectronDomains } = require('./config');

// Track trusted webContents IDs (Set for O(1) lookup)
const _trustedWebContents = new Set();

/**
* Check if a URL is trusted based on stage configuration.
* - Dev stage: trustedElectronDomains + all localhost URLs
* - Other stages: only trustedElectronDomains
*/
function isTrustedOrigin(url) {
if (!url) return false;

// Check against trustedElectronDomains
for (const domain of trustedElectronDomains) {
if (url.startsWith(domain)) {
return true;
}
}

// In dev stage, also allow localhost URLs
if (stage === 'dev') {
try {
const parsed = new URL(url);
if (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1') {
return true;
}
} catch {
return false;
}
}

return false;
}

/**
* Mark a webContents as trusted/untrusted based on its current URL.
* Call this when window loads or navigates.
*/
function updateTrustStatus(webContents) {
const url = webContents.getURL();
if (isTrustedOrigin(url)) {
_trustedWebContents.add(webContents.id);
} else {
_trustedWebContents.delete(webContents.id);
}
}

/**
* Remove trust tracking when webContents is destroyed.
*/
function cleanupTrust(webContentsId) {
_trustedWebContents.delete(webContentsId);
}

/**
* Fast check if webContents is trusted (O(1) lookup).
*/
function _isWebContentsTrusted(webContentsId) {
return _trustedWebContents.has(webContentsId);
}

/**
* Assert that IPC event comes from trusted webContents.
* Throws error if not trusted.
*/
function assertTrusted(event) {
if (!_isWebContentsTrusted(event.sender.id)) {
const url = event.senderFrame?.url || event.sender.getURL() || 'unknown';
throw new Error(`Blocked IPC from untrusted origin: ${url}`);
}
}

module.exports = {
isTrustedOrigin,
updateTrustStatus,
cleanupTrust,
assertTrusted
};
76 changes: 52 additions & 24 deletions src-electron/main-app-ipc.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,45 @@
/**
* IPC handlers for electronAppAPI
* Preload location: contextBridge.exposeInMainWorld('electronAppAPI', { ... })
*
* NOTE: This file is copied from phoenix-fs library. Do not modify without
* updating the source library. Only add new Phoenix-specific handlers to main-window-ipc.js.
*/

const { app, ipcMain } = require('electron');
const { spawn } = require('child_process');
const readline = require('readline');
const { productName } = require('./package.json');
const path = require('path');
const { productName } = require('./config');
const { assertTrusted } = require('./ipc-security');

let processInstanceId = 0;

// Path to main.js - used to filter it out from CLI args in dev mode
const mainScriptPath = path.resolve(__dirname, 'main.js');

/**
* Filter CLI args to remove internal Electron arguments.
* In dev mode, process.argv includes: [electron, main.js, ...userArgs]
* In production, it includes: [app, ...userArgs]
* This function filters out the main.js entry point in dev mode.
*/
function filterCliArgs(args) {
if (!args || args.length === 0) {
return args;
}

const normalizedMainScript = mainScriptPath.toLowerCase();

return args.filter(arg => {
// Resolve to handle both absolute and relative paths
const resolvedArg = path.resolve(arg).toLowerCase();
return resolvedArg !== normalizedMainScript;
});
}
// Map of instanceId -> { process, terminated }
const spawnedProcesses = new Map();

// In-memory key-value store shared across all windows (mirrors Tauri's put_item/get_all_items)
// Used for multi-window storage synchronization
const sharedStorageMap = new Map();

function waitForTrue(fn, timeout) {
return new Promise((resolve) => {
const startTime = Date.now();
Expand Down Expand Up @@ -45,6 +74,7 @@ function registerAppIpcHandlers() {
// Spawn a child process and forward stdio to the calling renderer.
// Returns an instanceId so the renderer can target the correct process.
ipcMain.handle('spawn-process', async (event, command, args) => {
assertTrusted(event);
const instanceId = ++processInstanceId;
const sender = event.sender;
console.log(`Spawning: ${command} ${args.join(' ')} (instance ${instanceId})`);
Expand Down Expand Up @@ -82,61 +112,59 @@ function registerAppIpcHandlers() {
});

childProcess.on('error', (err) => {
instance.terminated = true;
console.error(`Failed to start process (instance ${instanceId}):`, err);
if (!sender.isDestroyed()) {
sender.send('process-error', instanceId, { message: err.message, code: err.code });
}
});

return instanceId;
});

// Write data to a specific spawned process stdin
ipcMain.handle('write-to-process', (event, instanceId, data) => {
assertTrusted(event);
const instance = spawnedProcesses.get(instanceId);
if (instance && !instance.terminated) {
instance.process.stdin.write(data);
}
});

ipcMain.handle('quit-app', (event, exitCode) => {
assertTrusted(event);
console.log('Quit requested with exit code:', exitCode);
// This will be handled by the main module's gracefulShutdown
app.emit('quit-requested', exitCode);
});

ipcMain.on('console-log', (event, message) => {
assertTrusted(event);
console.log('Renderer:', message);
});

// CLI args (mirrors Tauri's cli.getMatches for --quit-when-done / -q)
ipcMain.handle('get-cli-args', () => {
return process.argv;
// Filter out internal Electron args (main.js in dev mode)
ipcMain.handle('get-cli-args', (event) => {
assertTrusted(event);
return filterCliArgs(process.argv);
});

// App path (repo root when running from source)
ipcMain.handle('get-app-path', () => {
ipcMain.handle('get-app-path', (event) => {
assertTrusted(event);
return app.getAppPath();
});

// App name from package.json
ipcMain.handle('get-app-name', () => {
ipcMain.handle('get-app-name', (event) => {
assertTrusted(event);
return productName;
});

// Set zoom factor on the webview (mirrors Tauri's zoom_window)
ipcMain.handle('zoom-window', (event, scaleFactor) => {
event.sender.setZoomFactor(scaleFactor);
});

// In-memory storage for multi-window sync (mirrors Tauri's put_item/get_all_items)
ipcMain.handle('put-item', (event, key, value) => {
sharedStorageMap.set(key, value);
});

ipcMain.handle('get-all-items', () => {
return Object.fromEntries(sharedStorageMap);
});
}

module.exports = {
registerAppIpcHandlers,
terminateAllProcesses
terminateAllProcesses,
filterCliArgs
};
Loading
Loading