
Security News
Deno Goes the Way of the Dinosaur
Cloudflare is acquiring Deno’s team, which will provide another year of bug fixes and security updates before ending support for the runtime.
@marshallofsound/ipc
Advanced tools
Type-safe, validated, and secure Electron IPC with zero boilerplate
Electron apps typically expose APIs from the main process to renderer processes using ipcMain.handle and ipcRenderer.invoke. This works, but leads to:
This module solves all of these problems with a schema-first approach that generates fully typed, validated, and secure IPC code.
npm install @marshallofsound/ipc
# or
yarn add @marshallofsound/ipc
Requires Electron >= 18.0.3
Create schemas/api.eipc:
module myapp
validator OnlyMyApp = AND(
origin is "https://myapp.com"
is_main_frame is true
)
[RendererAPI]
[Validator=OnlyMyApp]
[ContextBridge]
interface FileSystem {
ReadConfig() -> string
WriteConfig(content: string) -> boolean
}
Add a script to your package.json:
{
"scripts": {
"generate:ipc": "generate-ipc schemas src/ipc"
}
}
Then run it:
npm run generate:ipc
Add src/ipc/ to your .gitignore.
// main.ts
import { FileSystem } from './ipc/browser/myapp';
import fs from 'fs';
FileSystem.for(mainWindow.webContents.mainFrame).setImplementation({
ReadConfig(path) {
// Ensure you
return fs.readFileSync(configPath, 'utf-8');
},
WriteConfig(content) {
fs.writeFileSync(configPath, content);
return true;
},
});
// preload.ts
import './ipc/preload/myapp';
The preload script must be bundled before use. This is required because:
Use esbuild or another bundler:
import esbuild from 'esbuild';
// For sandbox: false - use ESM format with .mjs extension
await esbuild.build({
entryPoints: ['preload.ts'],
bundle: true,
platform: 'node',
outfile: 'dist/preload.mjs',
external: ['electron', 'electron/renderer'],
format: 'esm',
});
// For sandbox: true - use CJS format with .cjs extension
await esbuild.build({
entryPoints: ['preload.ts'],
bundle: true,
platform: 'node',
outfile: 'dist/preload.cjs',
external: ['electron', 'electron/renderer'],
format: 'cjs',
});
Then reference the correct preload in your BrowserWindow:
new BrowserWindow({
webPreferences: {
sandbox: false,
preload: path.join(__dirname, 'preload.mjs'), // ESM for sandbox: false
// OR
sandbox: true,
preload: path.join(__dirname, 'preload.cjs'), // CJS for sandbox: true
},
});
Note: The
.cjsextension also works withsandbox: false, so you can use a single CJS bundle for both modes if preferred.
// renderer.ts
import { FileSystem } from '../ipc/renderer/myapp';
const content = await FileSystem.ReadConfig();
Every schema file must start with a module declaration:
module company.product
The module name becomes the namespace on window (e.g., window['company.product']).
Validators control when APIs are exposed and when calls are allowed. They run both at preload time (to decide whether to expose the API) and at call time (to verify each request).
validator MyValidator = AND(
condition1
condition2
OR(
condition3
condition4
)
)
| Condition | Description |
|---|---|
is_packaged is true/false | Check if app is packaged (production) or running from source |
is_main_frame is true/false | Check if request comes from main frame (not iframe) |
origin is "https://example.com" | Check the page origin (supports custom protocols like app://) |
hostname is "localhost" | Check the hostname |
protocol is "https:" | Check the protocol |
dynamic_global(flagName) | Check if global.flagName is truthy in main process |
Define different rules for different environments:
validator MyValidator = {
production: AND(
is_packaged is true
origin is "https://myapp.com"
)
development: AND(
is_packaged is false
hostname is "localhost"
)
}
The environment is determined by EIPC_ENV or NODE_ENV at build time. This is not a runtime flag.
Define validated string or number types:
subtype Username = string(
minLength: 3
maxLength: 20
)
subtype HttpsUrl = string(
startsWith: "https://"
)
subtype Percentage = number(
minValue: 0
maxValue: 100
)
subtype PositiveInt = number(
minValue: 0
)
Arguments using these subtypes are validated at runtime before reaching your implementation.
When subtypes aren't expressive enough, you can reference external Zod schemas for complex validation:
zod_reference Email {
import = "./schemas"
type = "Email"
schema = "emailSchema"
}
This requires a corresponding TypeScript file:
// schemas.ts
import { z } from 'zod';
export const emailSchema = z.string().email();
export type Email = z.infer<typeof emailSchema>;
The generated code will:
schema.safeParse() for runtime validationNote: Import paths are relative to the generated
ipc/_internal/directory, not your schema file.
enum Platform {
MacOS = "darwin"
Windows = "win32"
Linux = "linux"
}
Values are optional - if omitted, the enum name is used as the value.
structure UserInfo {
id: number
name: string
email?: string // Optional field
metadata: { // Nested inline structure
createdAt: number
updatedAt: number
}
}
Interfaces define the actual APIs exposed to renderers.
[RendererAPI]
[Validator=MyValidator]
[ContextBridge]
interface MyAPI {
// Async method (default)
GetData(id: number) -> string
// Sync method
[Sync]
GetDataSync(id: number) -> string
// Method with optional return
FindUser(name: string) -> UserInfo?
// Event (main -> renderer)
[Event]
OnDataChanged(newData: string)
// Store (reactive state with React hooks)
[Store]
currentUser() -> UserInfo
// Placeholder for future features
[NotImplemented]
FutureMethod() -> string
}
| Attribute | Description |
|---|---|
[RendererAPI] | API called from renderer, implemented in main |
[Validator=Name] | Apply a validator to all methods |
[ContextBridge] | Auto-expose via contextBridge |
| Attribute | Description |
|---|---|
[Sync] | Synchronous IPC (blocks renderer) |
[Event] | Event dispatched from main to renderer |
[Store] | Reactive state with getState(), getStateSync(), onStateChange() |
[NotImplemented] | Placeholder - throws if called, used to generate types for old methods that are no longer implemented |
| Type | Description |
|---|---|
string | String value |
number | Number value |
boolean | Boolean value |
Type? | Optional/nullable type |
CustomType | Reference to enum, structure, or subtype |
src/ipc/
├── browser/ # Main process - import from here
│ └── myapp.ts
├── preload/ # Preload scripts - import to initialize
│ └── myapp.ts
├── renderer/ # Renderer process - for type-safe access
│ └── myapp.ts
├── renderer-hooks/ # React hooks for stores
│ └── myapp.ts
├── common/ # Shared types - import from anywhere
│ └── myapp.ts
└── _internal/ # Generated internals - don't import directly
import { MyAPI } from '../ipc/browser/myapp';
// Set up handlers for a specific frame
const dispatcher = MyAPI.for(mainWindow.webContents.mainFrame).setImplementation({
GetData(id) {
return `Data for ${id}`;
},
getInitialCurrentUserState() {
return { id: 1, name: 'Guest' };
},
});
// Dispatch events
dispatcher.dispatchOnDataChanged('new data');
// Update store state
dispatcher.updateCurrentUserStore({ id: 2, name: 'User' });
// Get existing dispatcher
const existing = MyAPI.getDispatcher(frame);
For [Store] methods, React hooks are generated:
import { useCurrentUserStore } from '../ipc/renderer-hooks/myapp';
function UserDisplay() {
const state = useCurrentUserStore();
if (state.state === 'loading') return <div>Loading...</div>;
if (state.state === 'error') return <div>Error: {state.error.message}</div>;
return <div>Hello, {state.result.name}!</div>;
}
The hook returns:
{ state: 'loading' } - Initial load in progress{ state: 'ready', result: T } - Data available{ state: 'error', error: Error } - Load failed# Unit tests
yarn test
# E2E tests (Electron + Playwright)
yarn test:e2e
# E2E with visible windows (for debugging)
DEBUG_E2E_TEST=1 yarn test:e2e
FAQs
Typesafe, usable and clever Electron IPC
We found that @marshallofsound/ipc 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.

Security News
Cloudflare is acquiring Deno’s team, which will provide another year of bug fixes and security updates before ending support for the runtime.

Security News
Anthropic’s free OSS Scanner uses Mythos and other Claude models to find open source vulnerabilities and send maintainers reports with proposed fixes.

Security News
On Front Lines’ Early Adopters: CISO podcast, Andrew Becherer shares lessons from Datadog, weighs the long-term costs of AI-built tools, and explains how founders can actually gain a CISO’s trust.