chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* OmniRoute Electron Desktop App - Preload Script
|
||||
*
|
||||
* Secure bridge between renderer (Next.js) and main process (Electron).
|
||||
* Uses contextIsolation: true for maximum security.
|
||||
*
|
||||
* Code Review Fixes Applied:
|
||||
* #6 Listener accumulation — return disposer functions instead of using removeAllListeners
|
||||
* #16 Simplified channel validation — generic wrapper reduces boilerplate
|
||||
*/
|
||||
|
||||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
|
||||
const MAC_DRAG_STYLE_ID = "omniroute-electron-drag-region-style";
|
||||
const MAC_DRAG_FALLBACK_ID = "omniroute-electron-drag-region";
|
||||
const MAC_DRAG_OBSERVER_KEY = "__omnirouteMacDragRegionObserver";
|
||||
|
||||
function installMacDragRegion() {
|
||||
if (process.platform !== "darwin") return;
|
||||
|
||||
const attach = () => {
|
||||
if (!document.head || !document.body) return;
|
||||
|
||||
document.getElementById(MAC_DRAG_STYLE_ID)?.remove();
|
||||
document.getElementById(MAC_DRAG_FALLBACK_ID)?.remove();
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.id = MAC_DRAG_STYLE_ID;
|
||||
style.textContent = `
|
||||
header,
|
||||
.omniroute-electron-drag-region {
|
||||
app-region: drag;
|
||||
-webkit-app-region: drag;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
header a,
|
||||
header button,
|
||||
header input,
|
||||
header select,
|
||||
header textarea,
|
||||
header [role="button"],
|
||||
header [role="link"],
|
||||
header [tabindex]:not([tabindex="-1"]) {
|
||||
app-region: no-drag;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.omniroute-electron-drag-region {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 96px;
|
||||
right: 180px;
|
||||
height: 46px;
|
||||
z-index: 9999;
|
||||
}
|
||||
`;
|
||||
|
||||
const dragRegion = document.createElement("div");
|
||||
dragRegion.id = MAC_DRAG_FALLBACK_ID;
|
||||
dragRegion.className = "omniroute-electron-drag-region";
|
||||
dragRegion.setAttribute("aria-hidden", "true");
|
||||
|
||||
document.head.appendChild(style);
|
||||
document.body.appendChild(dragRegion);
|
||||
|
||||
const syncDragFallback = () => {
|
||||
const hasHeader = Boolean(document.querySelector("header"));
|
||||
dragRegion.hidden = hasHeader;
|
||||
if (hasHeader) observer.disconnect();
|
||||
};
|
||||
const previousObserver = window[MAC_DRAG_OBSERVER_KEY];
|
||||
if (previousObserver) previousObserver.disconnect();
|
||||
|
||||
const observer = new MutationObserver(syncDragFallback);
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
window[MAC_DRAG_OBSERVER_KEY] = observer;
|
||||
window.setTimeout(() => observer.disconnect(), 5000);
|
||||
window.addEventListener("pagehide", () => observer.disconnect(), { once: true });
|
||||
syncDragFallback();
|
||||
};
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
window.addEventListener("DOMContentLoaded", attach, { once: true });
|
||||
} else {
|
||||
attach();
|
||||
}
|
||||
}
|
||||
|
||||
installMacDragRegion();
|
||||
|
||||
// ── Channel Whitelist ──────────────────────────────────────
|
||||
const VALID_CHANNELS = {
|
||||
invoke: [
|
||||
"get-app-info",
|
||||
"open-external",
|
||||
"get-data-dir",
|
||||
"restart-server",
|
||||
"check-for-updates",
|
||||
"download-update",
|
||||
"install-update",
|
||||
"get-app-version",
|
||||
"get-autostart-status",
|
||||
"enable-autostart",
|
||||
"disable-autostart",
|
||||
"login:start",
|
||||
"login:cancel",
|
||||
"login:status",
|
||||
],
|
||||
send: ["window-minimize", "window-maximize", "window-close"],
|
||||
receive: ["server-status", "port-changed", "update-status", "login:status"],
|
||||
};
|
||||
|
||||
// ── Fix #16: Generic IPC wrappers ──────────────────────────
|
||||
function safeInvoke(channel, ...args) {
|
||||
if (!VALID_CHANNELS.invoke.includes(channel)) {
|
||||
return Promise.reject(new Error(`Blocked IPC invoke: ${channel}`));
|
||||
}
|
||||
return ipcRenderer.invoke(channel, ...args);
|
||||
}
|
||||
|
||||
function safeSend(channel, ...args) {
|
||||
if (VALID_CHANNELS.send.includes(channel)) {
|
||||
ipcRenderer.send(channel, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
// Fix #6: Return disposer function for proper listener cleanup
|
||||
function safeOn(channel, callback) {
|
||||
if (!VALID_CHANNELS.receive.includes(channel)) return () => {};
|
||||
const handler = (_event, data) => callback(data);
|
||||
ipcRenderer.on(channel, handler);
|
||||
// Return a disposer — caller removes only THIS specific listener
|
||||
return () => ipcRenderer.removeListener(channel, handler);
|
||||
}
|
||||
|
||||
// ── Expose API to Renderer ─────────────────────────────────
|
||||
contextBridge.exposeInMainWorld("electronAPI", {
|
||||
// ── Invoke (async, returns Promise) ──────────────────────
|
||||
getAppInfo: () => safeInvoke("get-app-info"),
|
||||
openExternal: (url) => safeInvoke("open-external", url),
|
||||
getDataDir: () => safeInvoke("get-data-dir"),
|
||||
restartServer: () => safeInvoke("restart-server"),
|
||||
getAppVersion: () => safeInvoke("get-app-version"),
|
||||
|
||||
// ── Auto-Update ──────────────────────────────────────────
|
||||
checkForUpdates: () => safeInvoke("check-for-updates"),
|
||||
downloadUpdate: () => safeInvoke("download-update"),
|
||||
installUpdate: () => safeInvoke("install-update"),
|
||||
|
||||
// ── Autostart ────────────────────────────────────────────
|
||||
getAutostartStatus: () => safeInvoke("get-autostart-status"),
|
||||
enableAutostart: () => safeInvoke("enable-autostart"),
|
||||
disableAutostart: () => safeInvoke("disable-autostart"),
|
||||
|
||||
// ── Send (fire-and-forget) ───────────────────────────────
|
||||
minimizeWindow: () => safeSend("window-minimize"),
|
||||
maximizeWindow: () => safeSend("window-maximize"),
|
||||
closeWindow: () => safeSend("window-close"),
|
||||
|
||||
// ── Receive (event listeners) ────────────────────────────
|
||||
// Fix #6: Returns a disposer function for precise cleanup
|
||||
onServerStatus: (callback) => safeOn("server-status", callback),
|
||||
onPortChanged: (callback) => safeOn("port-changed", callback),
|
||||
onUpdateStatus: (callback) => safeOn("update-status", callback),
|
||||
|
||||
// ── Web-Cookie Login ──────────────────────────────────────
|
||||
startLogin: (providerId, options) => safeInvoke("login:start", providerId, options),
|
||||
cancelLogin: () => safeInvoke("login:cancel"),
|
||||
getLoginStatus: () => safeInvoke("login:status"),
|
||||
onLoginStatus: (callback) => safeOn("login:status", callback),
|
||||
|
||||
// ── Static Properties ────────────────────────────────────
|
||||
isElectron: true,
|
||||
platform: process.platform,
|
||||
});
|
||||
Reference in New Issue
Block a user