2026-10-04 20:41:15 +00:00
2026-10-04 20:41:15 +00:00
2023-12-05 19:52:04 +01:00
2026-10-04 20:41:15 +00:00
2026-10-04 20:41:15 +00:00

@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

  1. a webContents the app passed to backend.trustWebContents() (it is forgotten once destroyed),
  2. its main frame (subframes, and frames that navigated away, were destroyed or are detached, are refused), and
  3. 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() and new 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 a file:// 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 outside trustedUrls are dropped; backend.createTypedRequest() rejects for such a window instead of sending to it.
  • The preload script exposes window.typedElectronIpc (send(payload), receive(listener)) instead of window.electronApi (sendMessage(channel, payload), receiveMessage(channel, listener)); it no longer forwards arbitrary channels. TypedElectronFrontend uses the new API and throws when the preload script is missing.
  • electron >=33.1 is required (WebFrameMain.detached first ships in 33.1.0).

Migration from 1.x

  • Depend on @api.global/typedrequest ^9 and @api.global/typedrequest-interfaces ^7. The typedrouter of TypedElectronBackend and TypedElectronFrontend is a typedrequest 9 TypedRouter, so TypedHandlers, 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, and routeAndAddResponse() resolves T | 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 null back for a message that has nothing to answer.
  • The entry points are unchanged: @api.global/typedelectron/ts and @api.global/typedelectron/ts_web.

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.

S
Description
No description provided
Readme
493 KiB
Languages
TypeScript 98.8%
JavaScript 1.2%