/** * preload.js * * Runs in a privileged context but is sandboxed from the renderer. * Use contextBridge to expose a safe, narrow API surface to the renderer. * Never expose raw ipcRenderer — always whitelist specific channels. */ const { contextBridge, ipcRenderer } = require('electron') // ─── Allowed IPC channels ───────────────────────────────────────────────── // Only messages on these channels will be forwarded. const ALLOWED_SEND_CHANNELS = [ 'patient:update-status', 'patient:fetch-list', 'schedule:fetch', 'app:ready', 'pb:fetch-entries', 'pb:fetch-travellers', 'pb:create-entry', 'pb:update-entry', 'pb:delete-entry', 'pb:fetch-vaccines', 'pb:fetch-vaccine-records', 'pb:save-vaccine-record', 'pb:update-vaccine-record', 'audit:run-health-audit', 'juvonno:fetch-chart', ] const ALLOWED_RECEIVE_CHANNELS = [ 'patient:status-updated', 'patient:list-response', 'schedule:response', 'app:get-version', 'pb:fetch-entries', 'pb:fetch-travellers', 'pb:create-entry', 'pb:update-entry', 'pb:delete-entry', 'pb:fetch-vaccines', 'pb:fetch-vaccine-records', 'pb:save-vaccine-record', 'pb:update-vaccine-record', 'audit:run-health-audit', 'juvonno:fetch-chart', ] // ─── Exposed API ────────────────────────────────────────────────────────── contextBridge.exposeInMainWorld('api', { /** * PocketBase helper methods for renderer */ pb: { fetchEntries: () => ipcRenderer.invoke('pb:fetch-entries'), fetchTravellers: () => ipcRenderer.invoke('pb:fetch-travellers'), createEntry: (trip) => ipcRenderer.invoke('pb:create-entry', trip), updateEntry: (id, trip) => ipcRenderer.invoke('pb:update-entry', id, trip), deleteEntry: (id) => ipcRenderer.invoke('pb:delete-entry', id), fetchVaccines: () => ipcRenderer.invoke('pb:fetch-vaccines'), fetchVaccineRecords: () => ipcRenderer.invoke('pb:fetch-vaccine-records'), saveVaccineRecord: (data) => ipcRenderer.invoke('pb:save-vaccine-record', data), updateVaccineRecord: (id, data) => ipcRenderer.invoke('pb:update-vaccine-record', id, data), }, /** * Health Risk Audit helper methods */ audit: { runHealthAudit: (payload) => ipcRenderer.invoke('audit:run-health-audit', payload), }, /** * Juvonno EMR helper methods */ juvonno: { fetchChart: (emrId) => ipcRenderer.invoke('juvonno:fetch-chart', emrId), }, /** * Send a one-way message to the main process. * @param {string} channel - Must be in ALLOWED_SEND_CHANNELS * @param {*} data */ send(channel, data) { if (ALLOWED_SEND_CHANNELS.includes(channel)) { ipcRenderer.send(channel, data) } else { console.warn(`[preload] Blocked send on unknown channel: "${channel}"`) } }, /** * Invoke a main-process handler and await its response. * @param {string} channel - Must be in ALLOWED_SEND_CHANNELS * @param {...*} args * @returns {Promise<*>} */ invoke(channel, ...args) { if (ALLOWED_SEND_CHANNELS.includes(channel) || ALLOWED_RECEIVE_CHANNELS.includes(channel)) { return ipcRenderer.invoke(channel, ...args) } return Promise.reject(new Error(`[preload] Blocked invoke on unknown channel: "${channel}"`)) }, /** * Subscribe to messages from the main process. * @param {string} channel - Must be in ALLOWED_RECEIVE_CHANNELS * @param {Function} callback - Called with (event, ...args) on each message * @returns {Function} Unsubscribe function — call it to remove the listener */ receive(channel, callback) { if (ALLOWED_RECEIVE_CHANNELS.includes(channel)) { const handler = (_event, ...args) => callback(...args) ipcRenderer.on(channel, handler) // Return cleanup function so React effects can unsubscribe return () => ipcRenderer.removeListener(channel, handler) } console.warn(`[preload] Blocked receive on unknown channel: "${channel}"`) return () => {} }, })