npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aderra/tipc

v0.1.2

Published

Typesafe and runtime-validatable IPC for Electron

Readme

@aderra/tipc

Typesafe, runtime-validatable IPC for Electron applications.

What @aderra/tipc adds

@aderra/tipc turns nested Main-process functions into a typed asynchronous Renderer client. It also provides opt-in Zod validation, mandatory sender policies, a narrow context-bridge transport, typed Main-to-Renderer events and requests, structured errors, deterministic cleanup, and an optional TanStack React Query adapter.

Routes are ordinary zero- or one-argument business functions. There is no procedure builder and no source compatibility with @egoist/tipc.

Installation

npm install @aderra/tipc zod

The package requires Node.js 22.14 or newer, Electron 35 or newer, and Zod 4. Zod is a required peer because the root entrypoint re-exports z. For the optional React Query entrypoint, also install its peers:

npm install @tanstack/react-query react

Renderer -> Main RPC

Define a nested router from direct business functions in Main. A route may take zero or one argument; the Renderer always receives a Promise-returning client.

// src/main/ipc/router.ts
import { defineRouter, validated, z } from '@aderra/tipc';

const StartCaptureSchema = z.object({
    mode: z.enum(['region', 'window', 'screen']),
    scaleFactor: z.coerce.number().min(0.5).max(4).default(1),
});

const screenshot = {
    getState: () => ({ status: 'idle' as const }),
    startCapture: async (input: z.output<typeof StartCaptureSchema>) => ({
        status: 'capturing' as const,
        ...input,
    }),
};

export const desktopRouter = defineRouter({
    screenshot: {
        getState: screenshot.getState,
        startCapture: validated(StartCaptureSchema, screenshot.startCapture),
    },
});

export type DesktopRouter = typeof desktopRouter;

After registering the router and exposing the preload transport as shown below, create the Renderer client:

// src/renderer/ipc.ts
import { createClient } from '@aderra/tipc/renderer';
import type { DesktopRouter } from '../main/ipc/router';

export const desktop = createClient<DesktopRouter>({
    transport: window.desktopTransport,
});

const state = await desktop.screenshot.getState();
const capture = await desktop.screenshot.startCapture({ mode: 'region' });

Keep the router import type-only so Main code is erased from the Renderer bundle.

Opt-in Zod validation

Wrap only trust-sensitive routes with validated(schema, fn). It uses safeParseAsync, so coercion, defaults, transforms, and asynchronous refinements finish before the business function runs. The Renderer accepts z.input<typeof schema> while the Main function receives z.output<typeof schema>. Invalid input never reaches the function and is reported as INPUT_VALIDATION_ERROR.

Unwrapped routes remain statically typed but do not receive runtime input validation.

Sender validation

Every Main registration requires an explicit sender policy. Check both the target WebContents and its main frame:

// src/main/index.ts
import { ipcMain } from 'electron';
import { registerIpcMain } from '@aderra/tipc/main';
import { desktopRouter } from './ipc/router';

const disposeRpc = registerIpcMain(desktopRouter, {
    ipcMain,
    namespace: 'desktop',
    validateSender(event) {
        return (
            event.sender === mainWindow.webContents &&
            event.senderFrame === mainWindow.webContents.mainFrame
        );
    },
});

The policy runs before payload validation and route execution. Dispose the registration when its owning window or application lifecycle ends. allowAllSenders exists for isolated tests; do not use it as a production policy.

Safe preload transport

Create one preconstructed facade in the preload and expose only that facade:

// src/preload/index.ts
import { contextBridge, ipcRenderer } from 'electron';
import { createPreloadTransport } from '@aderra/tipc/preload';

const desktopTransport = createPreloadTransport({
    ipcRenderer,
    namespace: 'desktop',
});

contextBridge.exposeInMainWorld('desktopTransport', desktopTransport);

Add the matching Renderer type without importing runtime Main code:

// src/renderer/global.d.ts
import type { PreloadTransport } from '@aderra/tipc/preload';

declare global {
    interface Window {
        desktopTransport: PreloadTransport;
    }
}

export {};

PreloadTransport exposes only fixed, namespaced operations and removes Electron event objects from callbacks. Never expose raw ipcRenderer, arbitrary channel methods, or Electron event objects to web content.

Main -> Renderer send/listen

Define the bidirectional contract in a module that both processes can import:

// src/shared/desktop-events.ts
import { defineEvents } from '@aderra/tipc';

export interface ScreenshotState {
    readonly status: 'idle' | 'capturing' | 'completed';
}

export const desktopEvents = defineEvents<{
    screenshot: {
        changed: (state: ScreenshotState) => void;
        confirmReplace: (name: string) => boolean;
    };
}>();

A void-returning event maps to send in Main and listen in Renderer:

// Main
import { ipcMain } from 'electron';
import { createRendererClient } from '@aderra/tipc/main';
import { desktopEvents } from '../shared/desktop-events';

const renderer = createRendererClient<typeof desktopEvents>({
    ipcMain,
    webContents: mainWindow.webContents,
    namespace: 'desktop',
    timeoutMs: 10_000,
});

renderer.screenshot.changed.send({ status: 'completed' });
// Renderer
import { createEventClient } from '@aderra/tipc/renderer';
import { desktopEvents } from '../shared/desktop-events';

const events = createEventClient<typeof desktopEvents>({
    transport: window.desktopTransport,
});

const stopChanged = events.screenshot.changed.listen((state) => {
    console.log(state.status);
});

Main -> Renderer invoke/handle

A non-void event maps to invoke in Main and handle in Renderer:

// Renderer: register before Main invokes it.
const stopConfirm = events.screenshot.confirmReplace.handle(async (name) =>
    window.confirm(`Replace ${name}?`),
);

// Main
const confirmed =
    await renderer.screenshot.confirmReplace.invoke('capture.png');

invoke defaults to a 10-second timeout. Responses are accepted only from the target WebContents and the captured webContents.mainFrame; a child frame cannot settle the request. listen and handle return idempotent cleanup functions; both event clients also expose an idempotent dispose() method:

stopChanged();
stopConfirm();
events.dispose();
renderer.dispose();
disposeRpc();

Create only one request-dispatching event client for a given preload transport and reuse it for all event routes.

React Query

The optional adapter wraps the regular Renderer client; it does not create a second transport.

import { createReactQueryClient } from '@aderra/tipc/react-query';
import type { DesktopRouter } from '../main/ipc/router';
import { desktop } from './ipc';

const api = createReactQueryClient<DesktopRouter>({ client: desktop });

export function CapturePanel() {
    // Route input is always first; React Query options are always second.
    // A no-input route therefore uses `undefined` when options are present.
    const state = api.screenshot.getState.useQuery(undefined, {
        staleTime: 1_000,
    });
    const capture = api.screenshot.startCapture.useMutation({ retry: false });

    return (
        <button
            disabled={capture.isPending}
            onClick={() => capture.mutate({ mode: 'region' })}
        >
            {state.data?.status ?? 'Loading'}
        </button>
    );
}

For an input route the query shape is useQuery(input, options), never useQuery(options). Mutation hook options go to useMutation(options) and route input goes to mutate(input) or mutateAsync(input). Query keys are derived from the exact route and input. The adapter owns queryKey, queryFn, mutationKey, and mutationFn; use api.useUtils() for typed cache helpers.

Errors

Business functions may throw a genuine TipcError to expose a public code, message, and structured-clone-safe details:

import { TipcError } from '@aderra/tipc';

throw new TipcError('CAPTURE_BUSY', 'Capture is already running', {
    activeMode: 'region',
});

Renderer clients reject with TipcClientError:

import { TipcClientError } from '@aderra/tipc';

try {
    await desktop.screenshot.startCapture({ mode: 'region' });
} catch (error) {
    if (error instanceof TipcClientError) {
        console.error(error.code, error.message, error.details);
    }
}

Built-in codes are INPUT_VALIDATION_ERROR, UNTRUSTED_SENDER, ROUTE_NOT_FOUND, RENDERER_UNAVAILABLE, RENDERER_TIMEOUT, INVALID_REQUEST, and INTERNAL_ERROR. Unknown exceptions become a constant INTERNAL_ERROR and do not expose stacks or private fields. Non-serializable success values also become INTERNAL_ERROR; non-serializable public error details are omitted.

Security checklist

  • Use contextIsolation: true, sandbox: true, and nodeIntegration: false for production windows.
  • Validate event.sender and event.senderFrame against the expected window and webContents.mainFrame on every Renderer-to-Main registration.
  • Expose only a preconstructed PreloadTransport; never expose raw ipcRenderer, arbitrary channels, or Electron event objects.
  • Block untrusted navigation and window creation. Sender validation is one layer, not a replacement for Electron's window security controls.
  • Apply validated(schema, fn) to untrusted input and send only structured-clone-safe values.
  • Dispose registrations, listeners, handlers, and clients with their owner.
  • nodeIntegrationInSubFrames: true appears only in the adversarial Electron smoke fixture so a child frame can attempt a spoofed response. It is smoke-test-only and must not be copied into production configuration.

API entrypoints

| Entrypoint | Purpose | | -------------------------- | -------------------------------------------------------------------------------------- | | @aderra/tipc | Routers, event definitions, Zod, structured errors, protocol helpers, and shared types | | @aderra/tipc/main | registerIpcMain, createRendererClient, and the test-only allowAllSenders helper | | @aderra/tipc/preload | createPreloadTransport and PreloadTransport | | @aderra/tipc/renderer | createClient and createEventClient | | @aderra/tipc/react-query | Optional createReactQueryClient hook and cache integration |

All entrypoints ship ESM, CommonJS, and matching declarations. Import the smallest process-specific entrypoint instead of pulling Electron adapters into the wrong bundle.

Development

npm ci
npm run verify

verify runs formatting, lint, type checks, runtime and declaration tests, fresh ESM/CommonJS builds, packed-entrypoint audits, package linters, and the real Electron 35 smoke test. npm run test:electron uses the already installed Electron binary through Playwright's Electron launcher and does not download Playwright Chromium.

The repository .npmrc points npm packages and Electron binary installation at npmmirror. That mirror affects installation only: it does not change IPC runtime behavior, and package publishing remains pinned to the public npm registry by publishConfig.

Publishing

Publishing is manual. From a clean release checkout, the package owner should run:

npm whoami --registry https://registry.npmjs.org/
npm run verify
npm pack --dry-run
npm publish --dry-run --access public
npm publish --access public

Before the final command, confirm that the dry-run targets https://registry.npmjs.org/ and that the tarball contains only dist, README.md, LICENSE, NOTICE, and package metadata. Automation and contributors prepare and audit the package; the user performs the final real npm publish.

License and upstream attribution

MIT licensed. @aderra/tipc is derived from @egoist/tipc; the upstream EGOIST copyright and MIT license are preserved in LICENSE, with fork attribution in NOTICE.