@api.global/typedelectron
a package made for ipc communication in electron
Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.
Install
pnpm add @api.global/typedelectron @api.global/typedrequest @api.global/typedrequest-interfaces
electron (>=33.1) is a peer dependency. TypedElectron runs on @api.global/typedrequest 9 and @api.global/typedrequest-interfaces 7: register handlers built from the same typedrequest major as the router they are added to.
Usage
The package has two entry points:
| Entry point | Process | Exports |
|---|---|---|
@api.global/typedelectron/ts |
main process | TypedElectronBackend, ITypedElectronBackendOptions, getPreloadScriptPath() |
@api.global/typedelectron/ts_web |
renderer | TypedElectronFrontend, ITypedElectronIpc |
Both sides exchange TypedRequest envelopes over the IPC channel typedrequest. The renderer reaches it through window.typedElectronIpc, which the bundled preload script exposes, so every window that talks to the backend must load that preload script with context isolation. The backend answers only the windows it was told to trust, while they show a document under one of its trusted URLs (see Security).
Shared request contracts
import type { ITypedRequest } from '@api.global/typedrequest-interfaces';
export interface IReq_Hello extends ITypedRequest {
method: 'hello';
request: { name: string };
response: { greeting: string };
}
export interface IReq_Ping extends ITypedRequest {
method: 'ping';
request: { sequence: number };
response: { pong: number };
}
Main process
import { app, BrowserWindow } from 'electron';
import { TypedHandler } from '@api.global/typedrequest';
import { TypedElectronBackend, getPreloadScriptPath } from '@api.global/typedelectron/ts';
import type { IReq_Hello, IReq_Ping } from './interfaces.js';
app.on('ready', async () => {
const backend = await TypedElectronBackend.createTypedElectronBackend({
// every document below the app's html directory; the trailing slash makes it a directory
trustedUrls: [new URL('../html/', import.meta.url).href],
});
backend.typedrouter.addTypedHandler(
new TypedHandler<IReq_Hello>('hello', async (requestArg) => {
return { greeting: `hello ${requestArg.name}` };
})
);
const mainWindow = new BrowserWindow({
webPreferences: {
preload: getPreloadScriptPath(),
contextIsolation: true,
},
});
backend.trustWebContents(mainWindow.webContents);
await mainWindow.loadURL(new URL('../html/index.html', import.meta.url).href);
// the main process can call handlers that the renderer registered
const { pong } = await backend
.createTypedRequest<IReq_Ping>('ping', mainWindow)
.fire({ sequence: 1 });
});
Renderer
import { TypedHandler } from '@api.global/typedrequest';
import { TypedElectronFrontend } from '@api.global/typedelectron/ts_web';
import type { IReq_Hello, IReq_Ping } from './interfaces.js';
const frontend = await TypedElectronFrontend.createTypedElectronFrontend();
frontend.typedrouter.addTypedHandler(
new TypedHandler<IReq_Ping>('ping', async (requestArg) => {
return { pong: requestArg.sequence };
})
);
const { greeting } = await frontend.createTypedRequest<IReq_Hello>('hello').fire({ name: 'renderer' });
Answers
Each side routes every incoming message through its typedrouter (the backend only messages from trusted senders) and sends back the answer to a request. A message that has nothing to answer (a response to a request of its own, or anything that is not a routable request) is not answered: no side ever sends null over the channel.
Security
The renderer is the less trusted side of an Electron app: a window can navigate to remote content, load an injected script, or host a <webview> or <iframe>. TypedElectron therefore admits a message only from
- a webContents the app passed to
backend.trustWebContents()(it is forgotten once destroyed), - its main frame (subframes, and frames that navigated away, were destroyed or are detached, are refused), and
- while that frame shows a document under one of the backend's
trustedUrls.
trustedUrls entries are URLs, compared after WHATWG URL parsing. An entry whose parsed path ends in / admits every document below it, for example file:///opt/app/html/; any other entry admits exactly that document. A bare origin always parses to the path /, so http://localhost:3000 and http://localhost:3000/ both admit the whole origin. Protocol, host, port and path are compared; query and hash are not. Paths are normalized first, so ../ cannot leave a trusted directory, and a sibling such as file:///opt/app/html-other/ is not below file:///opt/app/html/. Paths are compared case-sensitively, Windows drive letters included: build file:// entries the way Electron reports the loaded document (for example with pathToFileURL() on the same path the window loads). A case mismatch can only refuse a document, never admit one.
A message from any other sender is dropped without an answer; the backend logs one warning per offending webContents. The sender is checked again before an answer is sent: the answer is dropped unless the asking frame still shows a trusted document, so a window that navigated to an untrusted document or was closed while its request ran receives nothing, and every answer goes to the frame that asked (event.reply()). An answer that cannot be delivered (for example one IPC cannot serialize) is logged as an error rather than left as an unhandled rejection. In the other direction, createTypedRequest() refuses to send to a window that is destroyed, not trusted or not showing a trusted document: the request rejects instead.
The preload script exposes nothing but window.typedElectronIpc.send(payload) and .receive(listener) on the fixed typedrequest channel: the renderer cannot reach any other IPC channel of the app through it, and it never sees the IPC event. It only needs require('electron'), so it runs in sandboxed renderers.
Migration from 2.x
TypedElectronBackend.createTypedElectronBackend()andnew TypedElectronBackend()take{ trustedUrls: string[] }, the documents that may talk to the backend; for a window that loads the app's own html, pass that directory as afile://URL ending in/.- Call
backend.trustWebContents(window.webContents)for every window that talks to the backend, before its document sends the first request. Until then its messages are dropped without an answer. - Messages from subframes,
<webview>s, untrusted windows and documents outsidetrustedUrlsare dropped;backend.createTypedRequest()rejects for such a window instead of sending to it. - The preload script exposes
window.typedElectronIpc(send(payload),receive(listener)) instead ofwindow.electronApi(sendMessage(channel, payload),receiveMessage(channel, listener)); it no longer forwards arbitrary channels.TypedElectronFrontenduses the new API and throws when the preload script is missing. electron>=33.1 is required (WebFrameMain.detachedfirst ships in 33.1.0).
Migration from 1.x
- Depend on
@api.global/typedrequest^9 and@api.global/typedrequest-interfaces^7. ThetypedrouterofTypedElectronBackendandTypedElectronFrontendis a typedrequest 9TypedRouter, soTypedHandlers, request contracts and routers you pass to it must come from those majors. Read the typedrequest 9.0.0 changelog: a handler's falsy return value is now answered as returned, androuteAndAddResponse()resolvesT | null. - The renderer now answers requests from the main process (
TypedElectronBackend.createTypedRequest()); 1.x routed them in the renderer but never sent the answer back, so such requests never resolved. - The main process no longer sends
nullback for a message that has nothing to answer. - The entry points are unchanged:
@api.global/typedelectron/tsand@api.global/typedelectron/ts_web.
License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the license file within this repository.
Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
Company Information
Task Venture Capital GmbH
Registered at District court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.