فهرست منبع

refactor(desktop-electron): enable contextIsolation and sandbox (#23523)

Brendan Allan 13 ساعت پیش
والد
کامیت
eb9906420f

+ 4 - 0
packages/desktop-electron/electron.vite.config.ts

@@ -53,6 +53,10 @@ export default defineConfig({
     build: {
       rollupOptions: {
         input: { index: "src/preload/index.ts" },
+        output: {
+          format: "cjs",
+          entryFileNames: "[name].js",
+        },
       },
     },
   },

+ 4 - 7
packages/desktop-electron/src/main/index.ts

@@ -195,15 +195,10 @@ async function initialize() {
     logger.log("loading task finished")
   })()
 
-  const globals = {
-    updaterEnabled: UPDATER_ENABLED,
-    deepLinks: pendingDeepLinks,
-  }
-
   if (needsMigration) {
     const show = await Promise.race([loadingTask.then(() => false), delay(1_000).then(() => true)])
     if (show) {
-      overlay = createLoadingWindow(globals)
+      overlay = createLoadingWindow()
       await delay(1_000)
     }
   }
@@ -215,7 +210,7 @@ async function initialize() {
     await loadingComplete.promise
   }
 
-  mainWindow = createMainWindow(globals)
+  mainWindow = createMainWindow()
   wireMenu()
 
   overlay?.close()
@@ -252,6 +247,8 @@ registerIpcHandlers({
       initEmitter.off("step", listener)
     }
   },
+  getWindowConfig: () => ({ updaterEnabled: UPDATER_ENABLED }),
+  consumeInitialDeepLinks: () => pendingDeepLinks.splice(0),
   getDefaultServerUrl: () => getDefaultServerUrl(),
   setDefaultServerUrl: (url) => setDefaultServerUrl(url),
   getWslConfig: () => Promise.resolve(getWslConfig()),

+ 12 - 1
packages/desktop-electron/src/main/ipc.ts

@@ -2,7 +2,14 @@ import { execFile } from "node:child_process"
 import { BrowserWindow, Notification, app, clipboard, dialog, ipcMain, shell } from "electron"
 import type { IpcMainEvent, IpcMainInvokeEvent } from "electron"
 
-import type { InitStep, ServerReadyData, SqliteMigrationProgress, TitlebarTheme, WslConfig } from "../preload/types"
+import type {
+  InitStep,
+  ServerReadyData,
+  SqliteMigrationProgress,
+  TitlebarTheme,
+  WindowConfig,
+  WslConfig,
+} from "../preload/types"
 import { getStore } from "./store"
 import { setTitlebar } from "./windows"
 
@@ -14,6 +21,8 @@ const pickerFilters = (ext?: string[]) => {
 type Deps = {
   killSidecar: () => void
   awaitInitialization: (sendStep: (step: InitStep) => void) => Promise<ServerReadyData>
+  getWindowConfig: () => Promise<WindowConfig> | WindowConfig
+  consumeInitialDeepLinks: () => Promise<string[]> | string[]
   getDefaultServerUrl: () => Promise<string | null> | string | null
   setDefaultServerUrl: (url: string | null) => Promise<void> | void
   getWslConfig: () => Promise<WslConfig>
@@ -37,6 +46,8 @@ export function registerIpcHandlers(deps: Deps) {
     const send = (step: InitStep) => event.sender.send("init-step", step)
     return deps.awaitInitialization(send)
   })
+  ipcMain.handle("get-window-config", () => deps.getWindowConfig())
+  ipcMain.handle("consume-initial-deep-links", () => deps.consumeInitialDeepLinks())
   ipcMain.handle("get-default-server-url", () => deps.getDefaultServerUrl())
   ipcMain.handle("set-default-server-url", (_event: IpcMainInvokeEvent, url: string | null) =>
     deps.setDefaultServerUrl(url),

+ 1 - 1
packages/desktop-electron/src/main/menu.ts

@@ -47,7 +47,7 @@ export function createMenu(deps: Deps) {
         {
           label: "New Window",
           accelerator: "Cmd+Shift+N",
-          click: () => createMainWindow({ updaterEnabled: UPDATER_ENABLED }),
+          click: () => createMainWindow(),
         },
         { type: "separator" },
         { role: "close" },

+ 10 - 27
packages/desktop-electron/src/main/windows.ts

@@ -4,11 +4,6 @@ import { dirname, isAbsolute, join, relative, resolve } from "node:path"
 import { fileURLToPath, pathToFileURL } from "node:url"
 import type { TitlebarTheme } from "../preload/types"
 
-type Globals = {
-  updaterEnabled: boolean
-  deepLinks?: string[]
-}
-
 const root = dirname(fileURLToPath(import.meta.url))
 const rendererRoot = join(root, "../renderer")
 const rendererProtocol = "oc"
@@ -68,7 +63,7 @@ export function setDockIcon() {
   if (!icon.isEmpty()) app.dock?.setIcon(icon)
 }
 
-export function createMainWindow(globals: Globals) {
+export function createMainWindow() {
   const state = windowState({
     defaultWidth: 1280,
     defaultHeight: 800,
@@ -98,15 +93,16 @@ export function createMainWindow(globals: Globals) {
         }
       : {}),
     webPreferences: {
-      preload: join(root, "../preload/index.mjs"),
-      sandbox: false,
+      preload: join(root, "../preload/index.js"),
+      contextIsolation: true,
+      nodeIntegration: false,
+      sandbox: true,
     },
   })
 
   state.manage(win)
   loadWindow(win, "index.html")
   wireZoom(win)
-  injectGlobals(win, globals)
 
   win.once("ready-to-show", () => {
     win.show()
@@ -115,7 +111,7 @@ export function createMainWindow(globals: Globals) {
   return win
 }
 
-export function createLoadingWindow(globals: Globals) {
+export function createLoadingWindow() {
   const mode = tone()
   const win = new BrowserWindow({
     width: 640,
@@ -134,13 +130,14 @@ export function createLoadingWindow(globals: Globals) {
         }
       : {}),
     webPreferences: {
-      preload: join(root, "../preload/index.mjs"),
-      sandbox: false,
+      preload: join(root, "../preload/index.js"),
+      contextIsolation: true,
+      nodeIntegration: false,
+      sandbox: true,
     },
   })
 
   loadWindow(win, "loading.html")
-  injectGlobals(win, globals)
 
   return win
 }
@@ -174,20 +171,6 @@ function loadWindow(win: BrowserWindow, html: string) {
 
   void win.loadURL(`${rendererProtocol}://${rendererHost}/${html}`)
 }
-
-function injectGlobals(win: BrowserWindow, globals: Globals) {
-  win.webContents.on("dom-ready", () => {
-    const deepLinks = globals.deepLinks ?? []
-    const data = {
-      updaterEnabled: globals.updaterEnabled,
-      deepLinks: Array.isArray(deepLinks) ? deepLinks.splice(0) : deepLinks,
-    }
-    void win.webContents.executeJavaScript(
-      `window.__OPENCODE__ = Object.assign(window.__OPENCODE__ ?? {}, ${JSON.stringify(data)})`,
-    )
-  })
-}
-
 function wireZoom(win: BrowserWindow) {
   win.webContents.setZoomFactor(1)
   win.webContents.on("zoom-changed", () => {

+ 2 - 0
packages/desktop-electron/src/preload/index.ts

@@ -11,6 +11,8 @@ const api: ElectronAPI = {
       ipcRenderer.removeListener("init-step", handler)
     })
   },
+  getWindowConfig: () => ipcRenderer.invoke("get-window-config"),
+  consumeInitialDeepLinks: () => ipcRenderer.invoke("consume-initial-deep-links"),
   getDefaultServerUrl: () => ipcRenderer.invoke("get-default-server-url"),
   setDefaultServerUrl: (url) => ipcRenderer.invoke("set-default-server-url", url),
   getWslConfig: () => ipcRenderer.invoke("get-wsl-config"),

+ 6 - 0
packages/desktop-electron/src/preload/types.ts

@@ -15,10 +15,16 @@ export type TitlebarTheme = {
   mode: "light" | "dark"
 }
 
+export type WindowConfig = {
+  updaterEnabled: boolean
+}
+
 export type ElectronAPI = {
   killSidecar: () => Promise<void>
   installCli: () => Promise<string>
   awaitInitialization: (onStep: (step: InitStep) => void) => Promise<ServerReadyData>
+  getWindowConfig: () => Promise<WindowConfig>
+  consumeInitialDeepLinks: () => Promise<string[]>
   getDefaultServerUrl: () => Promise<string | null>
   setDefaultServerUrl: (url: string | null) => Promise<void>
   getWslConfig: () => Promise<WslConfig>

+ 0 - 2
packages/desktop-electron/src/renderer/env.d.ts

@@ -4,8 +4,6 @@ declare global {
   interface Window {
     api: ElectronAPI
     __OPENCODE__?: {
-      updaterEnabled?: boolean
-      wsl?: boolean
       deepLinks?: string[]
     }
   }

+ 16 - 14
packages/desktop-electron/src/renderer/index.tsx

@@ -20,7 +20,6 @@ import { createEffect, createResource, onCleanup, onMount, Show } from "solid-js
 import { render } from "solid-js/web"
 import pkg from "../../package.json"
 import { initI18n, t } from "./i18n"
-import { UPDATER_ENABLED } from "./updater"
 import { webviewZoom } from "./webview-zoom"
 import "./styles.css"
 import { useTheme } from "@opencode-ai/ui/theme"
@@ -43,8 +42,7 @@ const emitDeepLinks = (urls: string[]) => {
 }
 
 const listenForDeepLinks = () => {
-  const startUrls = window.__OPENCODE__?.deepLinks ?? []
-  if (startUrls.length) emitDeepLinks(startUrls)
+  void window.api.consumeInitialDeepLinks().then((urls) => emitDeepLinks(urls))
   return window.api.onDeepLink((urls) => emitDeepLinks(urls))
 }
 
@@ -57,13 +55,18 @@ const createPlatform = (): Platform => {
     return undefined
   })()
 
+  const isWslEnabled = async () => {
+    if (os !== "windows") return false
+    return window.api.getWslConfig().then((config) => config.enabled).catch(() => false)
+  }
+
   const wslHome = async () => {
-    if (os !== "windows" || !window.__OPENCODE__?.wsl) return undefined
+    if (!(await isWslEnabled())) return undefined
     return window.api.wslPath("~", "windows").catch(() => undefined)
   }
 
   const handleWslPicker = async <T extends string | string[]>(result: T | null): Promise<T | null> => {
-    if (!result || !window.__OPENCODE__?.wsl) return result
+    if (!result || !(await isWslEnabled())) return result
     if (Array.isArray(result)) {
       return Promise.all(result.map((path) => window.api.wslPath(path, "linux").catch(() => path))) as any
     }
@@ -137,7 +140,7 @@ const createPlatform = (): Platform => {
       if (os === "windows") {
         const resolvedApp = app ? await window.api.resolveAppPath(app).catch(() => null) : null
         const resolvedPath = await (async () => {
-          if (window.__OPENCODE__?.wsl) {
+          if (await isWslEnabled()) {
             const converted = await window.api.wslPath(path, "windows").catch(() => null)
             if (converted) return converted
           }
@@ -159,12 +162,14 @@ const createPlatform = (): Platform => {
     storage,
 
     checkUpdate: async () => {
-      if (!UPDATER_ENABLED()) return { updateAvailable: false }
+      const config = await window.api.getWindowConfig().catch(() => ({ updaterEnabled: false }))
+      if (!config.updaterEnabled) return { updateAvailable: false }
       return window.api.checkUpdate()
     },
 
     update: async () => {
-      if (!UPDATER_ENABLED()) return
+      const config = await window.api.getWindowConfig().catch(() => ({ updaterEnabled: false }))
+      if (!config.updaterEnabled) return
       await window.api.installUpdate()
     },
 
@@ -194,11 +199,7 @@ const createPlatform = (): Platform => {
       return fetch(input, init)
     },
 
-    getWslEnabled: async () => {
-      const next = await window.api.getWslConfig().catch(() => null)
-      if (next) return next.enabled
-      return window.__OPENCODE__!.wsl ?? false
-    },
+    getWslEnabled: () => isWslEnabled(),
 
     setWslEnabled: async (enabled) => {
       await window.api.setWslConfig({ enabled })
@@ -249,6 +250,7 @@ listenForDeepLinks()
 
 render(() => {
   const platform = createPlatform()
+  const [windowConfig] = createResource(() => window.api.getWindowConfig().catch(() => ({ updaterEnabled: false })))
   const loadLocale = async () => {
     const current = await platform.storage?.("opencode.global.dat").getItem("language")
     const legacy = current ? undefined : await platform.storage?.().getItem("language.v1")
@@ -325,7 +327,7 @@ render(() => {
   return (
     <PlatformProvider value={platform}>
       <AppBaseProviders locale={locale.latest}>
-        <Show when={!defaultServer.loading && !sidecar.loading && !windowCount.loading && !locale.loading}>
+        <Show when={!defaultServer.loading && !sidecar.loading && !windowConfig.loading && !windowCount.loading && !locale.loading}>
           {(_) => {
             return (
               <AppInterface

+ 0 - 2
packages/desktop-electron/src/renderer/updater.ts

@@ -1,7 +1,5 @@
 import { initI18n, t } from "./i18n"
 
-export const UPDATER_ENABLED = () => window.__OPENCODE__?.updaterEnabled ?? false
-
 export async function runUpdater({ alertOnFail }: { alertOnFail: boolean }) {
   await initI18n()
   try {