Files
QuantumTerminal/electron_shell/preload.js
T

102 lines
5.1 KiB
JavaScript

// version: v5
// Electron preload — context bridge for privileged native APIs.
//
// Anything renderer-side that needs to talk to the OS (file save dialog,
// open new window, etc.) goes through here.
//
// v5 — F-24 (Security wave 2): exposes secureSavePassword /
// secureLoadPassword / secureClearPassword that route to main's
// safeStorage via IPC. Replaces consumer-side plaintext password
// in localStorage (audit F-24 vulnerability). LoginScreen.jsx v50
// consumes these; one-time migration in v50 reads any legacy
// localStorage.mk_saved_pass, encrypts it, then deletes the
// plaintext. See main.js v9 for IPC handler details + storage path.
// v4 — exposes downloadAndInstall(url, onProgress) for the in-app updater.
// The main process streams the installer to %TEMP%, fires
// "mk:installer-progress" events for the UI, then spawns the .exe
// detached and quits the terminal. Renderer-side callers wire the
// onProgress callback to a progress bar and await the return value.
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("mkElectron", {
isElectron: true,
// saveScreenshot(suggestedName, bytes)
// suggestedName: string filename ("US500_M15_2026-04-27.png")
// bytes: Uint8Array | ArrayBuffer of raw PNG bytes
// returns: Promise<{ saved: boolean, path?: string }>
//
// Bypasses Chromium's download manager — instead the main process pops
// a native dialog.showSaveDialog and fs.writeFile-s the buffer directly.
// Much faster than `link.click()` on a data URL.
saveScreenshot: async (suggestedName, bytes) => {
// Bytes can be Uint8Array (good) or ArrayBuffer (we wrap). IPC handles
// typed arrays as Buffer on the main side.
const buf = bytes instanceof ArrayBuffer ? new Uint8Array(bytes) : bytes;
return ipcRenderer.invoke("mk:save-screenshot", { suggestedName, bytes: buf });
},
// openInNewWindow(pageId)
// pageId: one of "quant" | "fundamental" | "flow" | "options" | "stress" | "learn"
// returns: Promise<{ ok: boolean, error?: string }>
//
// Asks the main process to spawn a child BrowserWindow that loads the
// same React app with `?detached=<pageId>` so the renderer boots into
// DetachedShell mode. Each child window has parent: mainWindow so it
// closes automatically when the user closes the main window.
openInNewWindow: (pageId) => ipcRenderer.invoke("mk:open-in-new-window", { pageId }),
// fetchTrumpNews({ force? })
// returns Promise<{ ok, items?: [{title, link, pubDate, source}], fetchedAt?, cached?, error? }>
//
// Main process pulls the Google News RSS feed (search=Trump), parses items,
// caches in-memory for 5 min. Pass { force: true } to bypass cache.
fetchTrumpNews: (opts) => ipcRenderer.invoke("mk:fetch-trump-news", opts || {}),
// openExternal(url)
// Opens the given URL in the user's default browser via shell.openExternal.
// Only http(s) URLs are allowed.
openExternal: (url) => ipcRenderer.invoke("mk:open-external", url),
// v5: secureSavePassword(email, password)
// F-24 — Replaces localStorage.mk_saved_pass with OS-keychain
// encrypted storage via main process safeStorage. Email kept plain
// (not sensitive); password encrypted at rest. Returns
// Promise<{ ok, error? }>. error="encryption_unavailable" on
// platforms where safeStorage can't access keychain (rare Linux).
secureSavePassword: (email, password) =>
ipcRenderer.invoke("mk:secure-save-password", { email, password }),
// v5: secureLoadPassword()
// Returns Promise<{ ok, email?, password?, error? }>. When no creds
// saved, returns { ok: true, email: "", password: "" }. Decryption
// failures (encrypted-but-keychain-rotated) return { ok: false,
// error: ... }. Callers should treat any error path as "no saved
// password" and prompt the user.
secureLoadPassword: () => ipcRenderer.invoke("mk:secure-load-password"),
// v5: secureClearPassword()
// Deletes the encrypted creds file. Returns Promise<{ ok, error? }>.
// Idempotent — succeeds even if no file exists.
secureClearPassword: () => ipcRenderer.invoke("mk:secure-clear-password"),
// v4: downloadAndInstall(url, onProgress?)
// Streams installer to %TEMP%, spawns it detached, then quits the
// running terminal. onProgress({ received, total, pct, done? }) is
// called periodically while downloading.
// Returns Promise<{ ok, file?, error? }> — terminal will quit ~800ms
// after { ok: true } so the renderer typically only sees the resolve
// moment briefly before the window closes.
downloadAndInstall: (url, onProgress) => {
const handler = (_e, data) => {
try { if (typeof onProgress === "function") onProgress(data || {}); } catch {}
};
ipcRenderer.on("mk:installer-progress", handler);
const cleanup = () => { try { ipcRenderer.removeListener("mk:installer-progress", handler); } catch {} };
return ipcRenderer.invoke("mk:download-and-install", url)
.then((res) => { cleanup(); return res; })
.catch((e) => { cleanup(); throw e; });
},
});