SDK · PC/Electron

The Electron adapter keeps the real ImcoreClient, JWT/login provider, local cache, offline queue, and media upload in the main process. A narrow preload transport gives the renderer the same typed dm, group, room, community, ai, rtc, and operations APIs without exposing credentials or Electron’s ipcRenderer object.

Install

npm install @imcore/sdk ws

The adapter has no runtime dependency on Electron and does not bundle it. The main and renderer entry points are deliberately separate so node:fs never enters the renderer dependency graph.

Main process

Create the client after app.whenReady(). Keep token refresh and login identity here; do not accept them from renderer IPC.

import { app, BrowserWindow, ipcMain } from 'electron';
import { join } from 'node:path';
import WebSocket from 'ws';
import { ImcoreClient } from '@imcore/sdk';
import {
  createElectronFileStorage,
  registerElectronMainBridge,
} from '@imcore/sdk/electron/main';

await app.whenReady();

const win = new BrowserWindow({
  webPreferences: {
    preload: join(import.meta.dirname, 'preload.js'),
    contextIsolation: true,
    nodeIntegration: false,
    sandbox: true,
  },
});

const storage = createElectronFileStorage({
  filePath: join(app.getPath('userData'), 'imcore-state.json'),
});
const client = new ImcoreClient({
  url: 'wss://your-host/acc',
  webSocketImpl: WebSocket,
  auth: {
    getToken: () => readTokenFromKeychain(),
    getLogin: () => ({ userID: currentUserID(), userName: currentUserName() }),
  },
  storage,
});

const bridge = registerElectronMainBridge({
  client,
  ipcMain,
  webContents: () => win.webContents,
});

win.on('closed', () => bridge.dispose());

createElectronFileStorage() serializes and atomically renames writes. Put the file under app.getPath('userData'); it stores SDK cache/offline state, not the JWT. Store credentials in the operating-system keychain (for example through Electron safeStorage or your existing credential service).

registerElectronMainBridge() accepts only commands in the generated protocol catalog and only messages from the configured webContents. Pass allowedCommands to narrow that set further for an untrusted renderer.

Preload

Bundle this preload file. It exposes a small transport, not ipcRenderer:

import { contextBridge, ipcRenderer } from 'electron';
import { exposeElectronRendererTransport } from '@imcore/sdk/electron/renderer';

exposeElectronRendererTransport(contextBridge, ipcRenderer);

Renderer

Create the typed facade from the exposed transport:

import {
  createElectronRendererClient,
  type ElectronRendererTransport,
} from '@imcore/sdk/electron/renderer';

declare global {
  interface Window {
    imcoreTransport: ElectronRendererTransport;
  }
}

const im = createElectronRendererClient(window.imcoreTransport);

await im.connect();
await im.dm.send({ targetUserID: '1002', text: 'hello from desktop' });

const off = im.on('chat_message', (message) => renderMessage(message));
im.onStatus((status) => renderConnectionStatus(status));

// File/Blob bytes cross IPC; the HTTP upload and Authorization header stay main-side.
await im.media.upload({ file, type: 'file', scene: 'dm', targetUserID: '1002' });

// On renderer teardown:
off();
im.dispose();

Errors are reconstructed in the renderer as ElectronImcoreError; server and local precheck errors keep their numeric code. send() is asynchronous on the Electron facade because its sequence number crosses IPC; typed feature methods already return promises and retain the regular SDK shape.

Multiple windows

Use one bridge with a unique channelPrefix per authenticated window. Pair the same prefix in registerElectronMainBridge() and exposeElectronRendererTransport(). A bridge rejects calls from any other webContents ID and dispose() removes its handler and subscriptions.