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);
  },
});