
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
electron-xpc
Advanced tools
Type-safe cross-process communication for Electron (main ↔ preload ↔ renderer)
Async/Await Style Cross-Process Communication, built on semaphore-based flow control.
Unlike Electron's built-in ipcRenderer.invoke / ipcMain.handle, which only supports renderer-to-main request–response, XPC enables any process (renderer or main) to call handlers registered in any other process with full async/await semantics — including renderer <-> renderer and main <-> renderer invocations.
yarn add electron-xpc
# or
npm install electron-xpc
Features:
async/await, complex multi-step workflows that span multiple processes can be orchestrated with straightforward sequential logic, eliminating deeply nested callbacks or manual event coordination.不同于 Electron 内置的 ipcRenderer.invoke / ipcMain.handle 仅支持渲染进程到主进程的请求-响应模式,XPC 允许任意进程(渲染进程或主进程)以完整的 async/await 语义调用任意其他进程中注册的handler——包括 renderer <-> renderer 和 main <-> renderer 的调用。
特性:
async/await,跨多个进程的复杂多步作业流程可以用简洁的顺序逻辑编排,无需深层嵌套回调或手动事件协调。XPC distinguishes three process layers in an Electron app:
| Layer | Environment | Import Path |
|---|---|---|
| Main Layer | Node.js main process | electron-xpc/main |
| Preload Layer | Renderer preload script (has electron access) | electron-xpc/preload |
| Web Layer | Renderer web page (no electron access, uses window.xpcRenderer) | electron-xpc/renderer |
Although preload belongs to the renderer layer, it contains an isolated Node.js context, so it is treated as a separate layer in the architecture.
This is the low-level API where you manually specify channel name strings.
// src/main/index.ts
import { xpcCenter } from 'electron-xpc/main';
xpcCenter.init();
import { xpcMain } from 'electron-xpc/main';
// Register a handler
xpcMain.handle('my/mainChannel', async (payload) => {
console.log('Main received:', payload.params);
return { message: 'Hello from main' };
});
// Send to any registered handler (main or renderer)
const result = await xpcMain.send('my/channel', { foo: 'bar' });
// Preload script — has direct electron access
import { xpcRenderer } from 'electron-xpc/preload';
// Register a handler
xpcRenderer.handle('my/channel', async (payload) => {
console.log('Received params:', payload.params);
return { message: 'Hello from preload' };
});
// Send to other handlers
const result = await xpcRenderer.send('other/channel', { foo: 'bar' });
// Web page — no electron access, uses window.xpcRenderer
import { xpcRenderer } from 'electron-xpc/renderer';
// Register a handler
xpcRenderer.handle('my/webChannel', async (payload) => {
return { message: 'Hello from web' };
});
// Send to other handlers
const result = await xpcRenderer.send('my/channel', { foo: 'bar' });
xpcRenderer.removeHandle('my/channel');
The Handler/Emitter pattern provides type-safe, auto-registered channels. Channel names are automatically generated from class and method names — no hard-coded strings needed.
Channel naming convention: xpc:ClassName/methodName
⚠️ Important: Handler methods accept at most 1 parameter. Since
send()can only carry a singleparamsvalue, multi-parameter methods are not supported. The type system enforces this constraint — methods with 2+ parameters are mapped toneverin the Emitter type, causing a compile error.
import { XpcMainHandler, createXpcMainEmitter } from 'electron-xpc/main';
// --- Define Handler ---
class UserService extends XpcMainHandler {
// ✅ 0 params — valid
async getCount(): Promise<number> {
return 42;
}
// ✅ 1 param — valid
async getUserList(params: { page: number }): Promise<any[]> {
return db.query('SELECT * FROM users LIMIT ?', [params.page]);
}
// ❌ 2+ params — compile error on the Emitter side
// async search(keyword: string, page: number): Promise<any> { ... }
}
// Instantiate — auto-registers:
// xpc:UserService/getCount
// xpc:UserService/getUserList
const userService = new UserService();
// --- Use Emitter (can be used from any layer) ---
import { createXpcMainEmitter } from 'electron-xpc/main';
import type { UserService } from './somewhere';
const userEmitter = createXpcMainEmitter<UserService>('UserService');
const count = await userEmitter.getCount(); // sends to xpc:UserService/getCount
const list = await userEmitter.getUserList({ page: 1 }); // sends to xpc:UserService/getUserList
import { XpcPreloadHandler, createXpcPreloadEmitter } from 'electron-xpc/preload';
// --- Define Handler ---
class MessageTable extends XpcPreloadHandler {
async getMessageList(params: { chatId: string }): Promise<any[]> {
return sqlite.query('SELECT * FROM messages WHERE chatId = ?', [params.chatId]);
}
}
// Instantiate — auto-registers: xpc:MessageTable/getMessageList
const messageTable = new MessageTable();
// --- Use Emitter (from other preload or web layer) ---
import { createXpcPreloadEmitter } from 'electron-xpc/preload';
import type { MessageTable } from './somewhere';
const messageEmitter = createXpcPreloadEmitter<MessageTable>('MessageTable');
const messages = await messageEmitter.getMessageList({ chatId: '123' });
import { XpcRendererHandler, createXpcRendererEmitter } from 'electron-xpc/renderer';
// --- Define Handler ---
class UINotification extends XpcRendererHandler {
async showToast(params: { text: string }): Promise<void> {
toast.show(params.text);
}
}
const uiNotification = new UINotification();
// --- Use Emitter (from other layers) ---
import { createXpcRendererEmitter } from 'electron-xpc/renderer';
import type { UINotification } from './somewhere';
const notifyEmitter = createXpcRendererEmitter<UINotification>('UINotification');
await notifyEmitter.showToast({ text: 'Hello!' });
Fire-and-forget one-to-many notifications for event-driven communication.
payload.params, not direct paramsimport { xpcMain } from 'electron-xpc/main';
// Broadcast to all renderer windows (main won't receive this)
xpcMain.broadcast('language/changed', { lang: 'en' });
// Subscribe to renderer broadcasts
xpcMain.subscribe('language/changed', (payload) => {
const { lang } = payload.params; // Access via payload.params
console.log('Language:', lang);
});
import { xpcRenderer } from 'electron-xpc/renderer';
// Broadcast to all OTHER renderers + main (sender won't receive)
xpcRenderer.broadcast('language/changed', { lang: 'zh' });
// Subscribe to broadcasts from main or other renderers
xpcRenderer.subscribe('language/changed', (payload) => {
const { lang } = payload.params; // Access via payload.params
console.log('Language:', lang);
});
| Sender | API | Receivers |
|---|---|---|
| Main | xpcMain.broadcast(event, params) | All renderer windows (NOT main) |
| Renderer | xpcRenderer.broadcast(event, params) | All OTHER renderers + main (NOT sender) |
Electron's Utility Process runs in a sandboxed Node.js environment, ideal for CPU-intensive or I/O-heavy work. electron-xpc integrates utility processes into the same XPC communication fabric — any renderer or main process can call a utility process handler using the same send() API.
| Layer | Import Path |
|---|---|
| Main (create & manage) | electron-xpc/main |
| Utility Process (handle & send) | electron-xpc/utilityProcess |
// src/main/index.ts
import { createUtilityProcess } from 'electron-xpc/main';
import * as path from 'path';
app.whenReady().then(() => {
xpcCenter.init();
const worker = createUtilityProcess({
modulePath: path.join(__dirname, 'worker.js'),
serviceName: 'my-worker',
});
// Optional: pipe stdout/stderr to see console.log from utility process
worker.child.stdout?.on('data', (data) => console.log('[worker]', data.toString()));
worker.child.stderr?.on('data', (data) => console.error('[worker]', data.toString()));
});
Handlers registered via xpcUtilityProcess.handle() are automatically forwarded to xpcCenter in the main process and become callable from any layer (renderer, preload, main).
// src/utility/worker.ts
import { xpcUtilityProcess } from 'electron-xpc/utilityProcess';
// Handlers can be registered at top-level before the MessagePort is initialized.
// They are queued and replayed automatically once the port is ready.
xpcUtilityProcess.handle('worker/processData', async (payload) => {
console.log('[worker] received:', payload.params);
const result = heavyComputation(payload.params.input);
return { result };
});
xpcUtilityProcess.handle('worker/getStatus', async () => {
return { status: 'idle', uptime: process.uptime() };
});
Once registered, any process can call the handler with the same send() API:
From Renderer / Preload:
import { xpcRenderer } from 'electron-xpc/preload'; // or 'electron-xpc/renderer'
const result = await xpcRenderer.send('worker/processData', { input: 'hello' });
console.log(result); // { result: '...' }
From Main Process:
import { xpcMain } from 'electron-xpc/main';
const status = await xpcMain.send('worker/getStatus');
console.log(status); // { status: 'idle', uptime: 42 }
The utility process can also call handlers registered in the main or renderer processes:
// src/utility/worker.ts
import { xpcUtilityProcess } from 'electron-xpc/utilityProcess';
xpcUtilityProcess.handle('worker/doSomething', async (payload) => {
// Call a handler registered in renderer or main
const rendererResult = await xpcUtilityProcess.send('renderer/hello', { from: 'worker' });
console.log('[worker] renderer replied:', rendererResult);
return { done: true };
});
Renderer side:
import { xpcRenderer } from 'electron-xpc/preload';
xpcRenderer.handle('renderer/hello', async (payload) => {
console.log('[renderer] called by worker with:', payload.params);
return 'hello from renderer';
});
Renderer / Main Main Process (xpcCenter) Utility Process
| | |
| xpcRenderer.handle(...) | |
| __xpc_register__ -----------> | |
| | |
| | xpcUtilityProcess.handle() |
| | <-- __xpc_register__ (port2) -|
| | registerPortHandler(name) |
| | |
| xpcRenderer.send('worker/x') | |
| __xpc_exec__ ---------------> | |
| | port2.postMessage(exec) ---> |
| | [semaphore blocks] | execute handler
| | <-- port1.postMessage(finish)-|
| | [semaphore unblocks] |
| <---- return result --------- | |
| | |
| | xpcUtilityProcess.send(...) |
| | <-- __xpc_exec__ (port1) ---- |
| <---- forward(name) --------- | |
| execute handler | |
| __xpc_finish__ ------------> | |
| | ----> return result (port2) -|
Preload A / Web A Main Process Preload B / Web B
| | |
| handle(name, handler) ----> | |
| __xpc_register__ | |
| | <---- send(name, params) |
| | __xpc_exec__ |
| <---- forward(name) ---- | |
| execute handler | |
| ---- __xpc_finish__ ----> | |
| | ----> return result |
Main Process (xpcMain)
| |
| handle(name, handler) | -- register in xpcCenter registry (id=0)
| send(name, params) -------> | -- delegate to xpcCenter.exec()
| | id=0: call local handler directly
| | else: forward to renderer, block until done
MIT
FAQs
Type-safe cross-process communication for Electron (main ↔ preload ↔ renderer)
We found that electron-xpc demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.