// Preload bridge. Exposes a single `window.electronAPI` object to the // renderer that mirrors the CHANNELS surface from ipc-types.ts. Each // CHANNEL becomes a typed method that calls `ipcRenderer.invoke`; the // three event channels (shortcut fired/released + updater progress) get // `on*` subscribers that return an unsubscribe function. // // contextIsolation is ON so the renderer never sees `ipcRenderer` or // Node directly — only this curated surface. import { contextBridge, ipcRenderer } from 'electron'; import { CHANNELS, ELECTRON_RUNTIME_MARKER, type AudioLoopbackChunk, type AudioLoopbackStartResult, type FsPath, type FsRenameArgs, type FsWriteArgs, type NotifyArgs, type ScreenSource, type SecureStoreHandle, type SecureStoreOpenArgs, type ShortcutEvent, type ShortcutRegisterArgs, type SqlExecuteResult, type SqlSelectResult, type UpdateCheckResult, type UpdateProgress, } from './ipc-types'; type Unsubscribe = () => void; function on(channel: string, cb: (payload: T) => void): Unsubscribe { const handler = (_evt: Electron.IpcRendererEvent, payload: T): void => cb(payload); ipcRenderer.on(channel, handler); return () => ipcRenderer.removeListener(channel, handler); } const api = { platform: ELECTRON_RUNTIME_MARKER, osPlatform: process.platform as NodeJS.Platform, appVersion: process.env.npm_package_version ?? '0.0.0', // Screen sources --------------------------------------------------------- getScreenSources: (): Promise => ipcRenderer.invoke(CHANNELS.SCREEN_GET_SOURCES), getScreenThumbnail: (sourceId: string): Promise => ipcRenderer.invoke(CHANNELS.SCREEN_GET_THUMBNAIL, sourceId), /** Tell main which source the user picked in our Discord-style modal, * BEFORE calling getDisplayMedia. The display-media handler reads this * and routes the matching desktopCapturer Source into LiveKit. Pass * null on cancel/error to clear the pending state. */ setPendingShareSource: (sourceId: string | null): Promise => ipcRenderer.invoke(CHANNELS.SCREEN_SET_PENDING_SOURCE, sourceId), // Audio loopback --------------------------------------------------------- resolveLoopbackSource: (): Promise<{ sourceId: string } | null> => ipcRenderer.invoke(CHANNELS.AUDIO_LOOPBACK_RESOLVE_SOURCE), /** Native WASAPI process-loopback (Windows only). Captures all system * audio EXCEPT our own PID tree so peers don't hear themselves echoed * back. Throws on platforms where the addon isn't available — caller * is expected to fall back to the renderer-driven getUserMedia path * (see lib/screenAudio.ts). */ audioLoopback: { start: (): Promise => ipcRenderer.invoke(CHANNELS.AUDIO_LOOPBACK_START), /** Window-share variant: capture only the picked window's process * tree (INCLUDE_TARGET_PROCESS_TREE). hwnd is the decimal HWND * parsed from desktopCapturer's `window::0` source id. */ startForWindow: (hwnd: number): Promise => ipcRenderer.invoke(CHANNELS.AUDIO_LOOPBACK_START_FOR_WINDOW, { hwnd }), stop: (captureId: number): Promise => ipcRenderer.invoke(CHANNELS.AUDIO_LOOPBACK_STOP, captureId), onChunk: (cb: (payload: AudioLoopbackChunk) => void): Unsubscribe => on(CHANNELS.AUDIO_LOOPBACK_CHUNK, cb), }, // Shortcuts -------------------------------------------------------------- registerShortcut: (args: ShortcutRegisterArgs): Promise => ipcRenderer.invoke(CHANNELS.SHORTCUT_REGISTER, args), unregisterShortcut: (id: string): Promise => ipcRenderer.invoke(CHANNELS.SHORTCUT_UNREGISTER, id), isShortcutRegistered: (id: string): Promise => ipcRenderer.invoke(CHANNELS.SHORTCUT_IS_REGISTERED, id), onShortcutFired: (cb: (evt: ShortcutEvent) => void): Unsubscribe => on(CHANNELS.SHORTCUT_EVT_FIRED, cb), onShortcutReleased: (cb: (evt: ShortcutEvent) => void): Unsubscribe => on(CHANNELS.SHORTCUT_EVT_RELEASED, cb), // Notifications ---------------------------------------------------------- notify: (args: NotifyArgs): Promise => ipcRenderer.invoke(CHANNELS.NOTIFY_SHOW, args), getNotificationPermission: (): Promise<'granted' | 'denied' | 'default'> => ipcRenderer.invoke(CHANNELS.NOTIFY_PERMISSION), // Tray ------------------------------------------------------------------- // `badgeDataUrl` (optional): renderer-painted PNG (data:image/png;base64) // that main applies as the Windows taskbar overlay icon. We render in the // renderer because main has no Canvas2D; passing a finished image avoids // bundling a native canvas backend just for a 32×32 badge. setTrayUnread: (count: number, badgeDataUrl?: string | null): Promise => ipcRenderer.invoke(CHANNELS.TRAY_UNREAD, count, badgeDataUrl ?? null), // Secure store ----------------------------------------------------------- secureStoreOpen: (args: SecureStoreOpenArgs): Promise => ipcRenderer.invoke(CHANNELS.SECURE_STORE_OPEN, args), secureStoreGet: (handle: string, key: string): Promise => ipcRenderer.invoke(CHANNELS.SECURE_STORE_GET, handle, key), secureStoreSet: (handle: string, key: string, value: string): Promise => ipcRenderer.invoke(CHANNELS.SECURE_STORE_SET, handle, key, value), secureStoreRemove: (handle: string, key: string): Promise => ipcRenderer.invoke(CHANNELS.SECURE_STORE_REMOVE, handle, key), secureStoreClose: (handle: string): Promise => ipcRenderer.invoke(CHANNELS.SECURE_STORE_CLOSE, handle), // Filesystem ------------------------------------------------------------- fsRead: (p: FsPath): Promise => ipcRenderer.invoke(CHANNELS.FS_READ, p), fsWrite: (args: FsWriteArgs): Promise => ipcRenderer.invoke(CHANNELS.FS_WRITE, args), fsExists: (p: FsPath): Promise => ipcRenderer.invoke(CHANNELS.FS_EXISTS, p), fsMkdir: (p: FsPath): Promise => ipcRenderer.invoke(CHANNELS.FS_MKDIR, p), fsRename: (args: FsRenameArgs): Promise => ipcRenderer.invoke(CHANNELS.FS_RENAME, args), fsRemove: (p: FsPath): Promise => ipcRenderer.invoke(CHANNELS.FS_REMOVE, p), fsAppLocalDataDir: (): Promise => ipcRenderer.invoke(CHANNELS.FS_APP_LOCAL_DATA_DIR), // SQL -------------------------------------------------------------------- sqlLoad: (args: { name: string }): Promise => ipcRenderer.invoke(CHANNELS.SQL_LOAD, args), sqlExecute: (args: { handle: string; query: string; bindings?: unknown[]; }): Promise => ipcRenderer.invoke(CHANNELS.SQL_EXECUTE, args), sqlSelect: (args: { handle: string; query: string; bindings?: unknown[]; }): Promise => ipcRenderer.invoke(CHANNELS.SQL_SELECT, args), sqlClose: (handle: string): Promise => ipcRenderer.invoke(CHANNELS.SQL_CLOSE, handle), // Updater ---------------------------------------------------------------- checkForUpdate: (): Promise => ipcRenderer.invoke(CHANNELS.UPDATER_CHECK), downloadInstallUpdate: (): Promise => ipcRenderer.invoke(CHANNELS.UPDATER_DOWNLOAD_INSTALL), onUpdaterProgress: (cb: (p: UpdateProgress) => void): Unsubscribe => on(CHANNELS.UPDATER_EVT_PROGRESS, cb), // Autostart -------------------------------------------------------------- isAutoStartEnabled: (): Promise => ipcRenderer.invoke(CHANNELS.AUTOSTART_IS_ENABLED), setAutoStart: (enabled: boolean): Promise => ipcRenderer.invoke(CHANNELS.AUTOSTART_SET, enabled), // Window fullscreen ------------------------------------------------------ setFullscreen: (enabled: boolean): Promise => ipcRenderer.invoke(CHANNELS.WINDOW_SET_FULLSCREEN, enabled), } as const; export type ElectronAPI = typeof api; contextBridge.exposeInMainWorld('electronAPI', api);