preload.jsjavascript
/**
* @file contextBridge surface: the whole renderer↔main API, exposed as
* window.ezl. Every method is a renderer→main invoke except onMenu, the sole
* inbound (main→renderer) direction. Auth tokens live in the main process;
* the renderer only ever sees the verified entitlement payload.
* @module preload
*/
const { contextBridge, ipcRenderer, webUtils } = require('electron');
contextBridge.exposeInMainWorld('ezl', {
/**
* Resolve a dropped/picked File object to its absolute path.
* @param {File} file Browser File object.
* @returns {string} Absolute filesystem path.
*/
getFilePath: (file) => webUtils.getPathForFile(file),
/**
* Ask the worker to measure a file's loudness.
* @param {string} path Absolute media path.
* @returns {Promise<object>} Worker analysis message.
*/
analyzeLoudness: (path) => ipcRenderer.invoke('analyze-loudness', path),
/**
* Transcode a file Chromium can't play (e.g. AIFF) to WAV for preview.
* @param {string} path Absolute audio path.
* @returns {Promise<Buffer>} WAV bytes (renderer wraps them in a Blob URL).
*/
getPreviewAudio: (path) => ipcRenderer.invoke('get-preview-audio', path),
/**
* Build a preview-only H.264 proxy for videos whose picture Chromium can't
* decode (ProRes etc.).
* @param {string} path Absolute video path.
* @returns {Promise<string>} The proxy's absolute path (renderer streams it
* via ezl-file://).
*/
getVideoPreviewProxy: (path) => ipcRenderer.invoke('get-video-preview-proxy', path),
/**
* Release the current video preview proxy so its disk space is reclaimed.
* @returns {Promise<void>}
*/
releaseVideoPreviewProxy: () => ipcRenderer.invoke('release-video-preview-proxy'),
/**
* Open a native directory chooser.
* @returns {Promise<string|null>} Chosen folder path, or null on cancel.
*/
showFolderPicker: () => ipcRenderer.invoke('show-folder-picker'),
/**
* Resolve the OS Downloads directory.
* @returns {Promise<string>}
*/
getDownloadsPath: () => ipcRenderer.invoke('get-downloads-path'),
/**
* App + OS info for the support diagnostics form.
* @returns {Promise<{version: string, platform: string, osVersion: string}>}
*/
getAppInfo: () => ipcRenderer.invoke('get-app-info'),
/**
* Load an app-authored modal partial's markup (allowlisted names only).
* @param {string} name Partial name ('settings' | 'user' | 'help' | 'help-md').
* @returns {Promise<string>} The partial's source text.
*/
loadPartial: (name) => ipcRenderer.invoke('load-partial', name),
/**
* Check whether a path exists on disk (overwrite warning).
* @param {string} path Absolute path.
* @returns {Promise<boolean>}
*/
pathExists: (path) => ipcRenderer.invoke('path-exists', path),
/**
* Reveal a path in Finder/Explorer.
* @param {string} path Absolute path.
* @returns {Promise<void>}
*/
revealInFolder: (path) => ipcRenderer.invoke('reveal-in-folder', path),
/**
* Open the web account portal in the default browser (SSO handoff as the
* signed-in user; minted in main).
* @returns {Promise<void>}
*/
openAccountPortal:() => ipcRenderer.invoke('open-account-portal'),
/**
* Run a batch layback export through the worker.
* @param {object} payload Export description (paths, levels, trims, output folder).
* @returns {Promise<object>} Worker result message.
*/
exportLaybacks: (payload) => ipcRenderer.invoke('export-laybacks', payload),
/**
* Stamp metadata tags onto a finished export.
* @param {object} payload Output path + tag values.
* @returns {Promise<object>} Worker result message.
*/
updateMetadata: (payload) => ipcRenderer.invoke('update-metadata', payload),
/**
* Submit the support form → POST /support via main, which holds the bearer
* token — tokens never reach the renderer.
* @param {{message: string, error: string, diagnostics: object}} payload
* @returns {Promise<{ok: boolean, status: number, reason: string}>}
*/
sendSupportRequest:(payload) => ipcRenderer.invoke('support-request', payload),
/**
* Restore the session. Auth tokens live in the main process; the renderer
* only sees the verified payload.
* @returns {Promise<{loggedIn: boolean, payload: object, offline: boolean}>}
*/
authSession: () => ipcRenderer.invoke('auth-session'),
/**
* Sign in with email + password.
* @param {{email: string, password: string}} args
* @returns {Promise<{ok: boolean, payload: object, error: string}>}
*/
authLogin: (args) => ipcRenderer.invoke('auth-login', args),
/**
* Sign out and clear local auth state.
* @returns {Promise<{ok: true}>}
*/
authLogout: () => ipcRenderer.invoke('auth-logout'),
/**
* Read the full settings object (defaults resolved).
* @returns {Promise<object>}
*/
getSettings: () => ipcRenderer.invoke('get-settings'),
/**
* Merge a partial settings patch; resolves with the full updated object.
* @param {object} patch Only the slice that changed.
* @returns {Promise<object>}
*/
setSettings: (patch) => ipcRenderer.invoke('set-settings', patch),
/**
* Whether OS-backed secure storage is available. Secrets expose a
* renderer-safe subset only — no plaintext get/set; those stay in main.
* @returns {Promise<boolean>}
*/
secretAvailable: () => ipcRenderer.invoke('secret-available'),
/**
* Whether a secret exists for a key (e.g. an integration is connected).
* @param {string} key Secret name.
* @returns {Promise<boolean>}
*/
secretHas: (key) => ipcRenderer.invoke('secret-has', key),
/**
* Delete a stored secret (disconnect an integration).
* @param {string} key Secret name.
* @returns {Promise<void>}
*/
secretDelete: (key) => ipcRenderer.invoke('secret-delete', key),
/**
* Open the native Save Project dialog for a .ezlproj path.
* @returns {Promise<string|null>} Chosen path, or null on cancel.
*/
saveProjectDialog: () => ipcRenderer.invoke('save-project-dialog'),
/**
* Open the native Open Project dialog for a .ezlproj file.
* @returns {Promise<string|null>} Chosen path, or null on cancel.
*/
openProjectDialog: () => ipcRenderer.invoke('open-project-dialog'),
/**
* Write the serialized project to a dialog-approved path.
* @param {string} p Dialog-approved .ezlproj path.
* @param {object} obj Project object to serialize.
* @returns {Promise<{ok: true}|{error: string}>}
*/
writeProjectFile: (p, obj) => ipcRenderer.invoke('write-project-file', p, obj),
/**
* Read + parse a project file from a dialog-approved path.
* @param {string} p Dialog-approved .ezlproj path.
* @returns {Promise<object|{error: string}>} Parsed project, or {error}.
*/
readProjectFile: (p) => ipcRenderer.invoke('read-project-file', p),
/**
* Subscribe to main→renderer events (menu clicks) — the ONLY inbound
* direction. The handler is wrapped so the raw IpcRendererEvent never leaks
* into renderer code.
* @param {string} channel Event channel (e.g. 'menu:save-project', 'auth:session').
* @param {function(any[]): void} handler Called with the event's payload args.
* @returns {function(): void} Unsubscribe function.
*/
onMenu: (channel, handler) => {
const wrapped = (_event, ...args) => handler(...args);
ipcRenderer.on(channel, wrapped);
return () => ipcRenderer.removeListener(channel, wrapped);
},
});