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.