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

This commit is contained in:
Brendan Allan
2026-04-21 12:38:59 +08:00
committed by GitHub
parent 4964ce480c
commit eb9906420f
10 changed files with 55 additions and 54 deletions
@@ -53,6 +53,10 @@ export default defineConfig({
build: { build: {
rollupOptions: { rollupOptions: {
input: { index: "src/preload/index.ts" }, input: { index: "src/preload/index.ts" },
output: {
format: "cjs",
entryFileNames: "[name].js",
},
}, },
}, },
}, },
+4 -7
View File
@@ -195,15 +195,10 @@ async function initialize() {
logger.log("loading task finished") logger.log("loading task finished")
})() })()
const globals = {
updaterEnabled: UPDATER_ENABLED,
deepLinks: pendingDeepLinks,
}
if (needsMigration) { if (needsMigration) {
const show = await Promise.race([loadingTask.then(() => false), delay(1_000).then(() => true)]) const show = await Promise.race([loadingTask.then(() => false), delay(1_000).then(() => true)])
if (show) { if (show) {
overlay = createLoadingWindow(globals) overlay = createLoadingWindow()
await delay(1_000) await delay(1_000)
} }
} }
@@ -215,7 +210,7 @@ async function initialize() {
await loadingComplete.promise await loadingComplete.promise
} }
mainWindow = createMainWindow(globals) mainWindow = createMainWindow()
wireMenu() wireMenu()
overlay?.close() overlay?.close()
@@ -252,6 +247,8 @@ registerIpcHandlers({
initEmitter.off("step", listener) initEmitter.off("step", listener)
} }
}, },
getWindowConfig: () => ({ updaterEnabled: UPDATER_ENABLED }),
consumeInitialDeepLinks: () => pendingDeepLinks.splice(0),
getDefaultServerUrl: () => getDefaultServerUrl(), getDefaultServerUrl: () => getDefaultServerUrl(),
setDefaultServerUrl: (url) => setDefaultServerUrl(url), setDefaultServerUrl: (url) => setDefaultServerUrl(url),
getWslConfig: () => Promise.resolve(getWslConfig()), getWslConfig: () => Promise.resolve(getWslConfig()),
+12 -1
View File
@@ -2,7 +2,14 @@ import { execFile } from "node:child_process"
import { BrowserWindow, Notification, app, clipboard, dialog, ipcMain, shell } from "electron" import { BrowserWindow, Notification, app, clipboard, dialog, ipcMain, shell } from "electron"
import type { IpcMainEvent, IpcMainInvokeEvent } 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 { getStore } from "./store"
import { setTitlebar } from "./windows" import { setTitlebar } from "./windows"
@@ -14,6 +21,8 @@ const pickerFilters = (ext?: string[]) => {
type Deps = { type Deps = {
killSidecar: () => void killSidecar: () => void
awaitInitialization: (sendStep: (step: InitStep) => void) => Promise<ServerReadyData> awaitInitialization: (sendStep: (step: InitStep) => void) => Promise<ServerReadyData>
getWindowConfig: () => Promise<WindowConfig> | WindowConfig
consumeInitialDeepLinks: () => Promise<string[]> | string[]
getDefaultServerUrl: () => Promise<string | null> | string | null getDefaultServerUrl: () => Promise<string | null> | string | null
setDefaultServerUrl: (url: string | null) => Promise<void> | void setDefaultServerUrl: (url: string | null) => Promise<void> | void
getWslConfig: () => Promise<WslConfig> getWslConfig: () => Promise<WslConfig>
@@ -37,6 +46,8 @@ export function registerIpcHandlers(deps: Deps) {
const send = (step: InitStep) => event.sender.send("init-step", step) const send = (step: InitStep) => event.sender.send("init-step", step)
return deps.awaitInitialization(send) 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("get-default-server-url", () => deps.getDefaultServerUrl())
ipcMain.handle("set-default-server-url", (_event: IpcMainInvokeEvent, url: string | null) => ipcMain.handle("set-default-server-url", (_event: IpcMainInvokeEvent, url: string | null) =>
deps.setDefaultServerUrl(url), deps.setDefaultServerUrl(url),
+1 -1
View File
@@ -47,7 +47,7 @@ export function createMenu(deps: Deps) {
{ {
label: "New Window", label: "New Window",
accelerator: "Cmd+Shift+N", accelerator: "Cmd+Shift+N",
click: () => createMainWindow({ updaterEnabled: UPDATER_ENABLED }), click: () => createMainWindow(),
}, },
{ type: "separator" }, { type: "separator" },
{ role: "close" }, { role: "close" },
+10 -27
View File
@@ -4,11 +4,6 @@ import { dirname, isAbsolute, join, relative, resolve } from "node:path"
import { fileURLToPath, pathToFileURL } from "node:url" import { fileURLToPath, pathToFileURL } from "node:url"
import type { TitlebarTheme } from "../preload/types" import type { TitlebarTheme } from "../preload/types"
type Globals = {
updaterEnabled: boolean
deepLinks?: string[]
}
const root = dirname(fileURLToPath(import.meta.url)) const root = dirname(fileURLToPath(import.meta.url))
const rendererRoot = join(root, "../renderer") const rendererRoot = join(root, "../renderer")
const rendererProtocol = "oc" const rendererProtocol = "oc"
@@ -68,7 +63,7 @@ export function setDockIcon() {
if (!icon.isEmpty()) app.dock?.setIcon(icon) if (!icon.isEmpty()) app.dock?.setIcon(icon)
} }
export function createMainWindow(globals: Globals) { export function createMainWindow() {
const state = windowState({ const state = windowState({
defaultWidth: 1280, defaultWidth: 1280,
defaultHeight: 800, defaultHeight: 800,
@@ -98,15 +93,16 @@ export function createMainWindow(globals: Globals) {
} }
: {}), : {}),
webPreferences: { webPreferences: {
preload: join(root, "../preload/index.mjs"), preload: join(root, "../preload/index.js"),
sandbox: false, contextIsolation: true,
nodeIntegration: false,
sandbox: true,
}, },
}) })
state.manage(win) state.manage(win)
loadWindow(win, "index.html") loadWindow(win, "index.html")
wireZoom(win) wireZoom(win)
injectGlobals(win, globals)
win.once("ready-to-show", () => { win.once("ready-to-show", () => {
win.show() win.show()
@@ -115,7 +111,7 @@ export function createMainWindow(globals: Globals) {
return win return win
} }
export function createLoadingWindow(globals: Globals) { export function createLoadingWindow() {
const mode = tone() const mode = tone()
const win = new BrowserWindow({ const win = new BrowserWindow({
width: 640, width: 640,
@@ -134,13 +130,14 @@ export function createLoadingWindow(globals: Globals) {
} }
: {}), : {}),
webPreferences: { webPreferences: {
preload: join(root, "../preload/index.mjs"), preload: join(root, "../preload/index.js"),
sandbox: false, contextIsolation: true,
nodeIntegration: false,
sandbox: true,
}, },
}) })
loadWindow(win, "loading.html") loadWindow(win, "loading.html")
injectGlobals(win, globals)
return win return win
} }
@@ -174,20 +171,6 @@ function loadWindow(win: BrowserWindow, html: string) {
void win.loadURL(`${rendererProtocol}://${rendererHost}/${html}`) 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) { function wireZoom(win: BrowserWindow) {
win.webContents.setZoomFactor(1) win.webContents.setZoomFactor(1)
win.webContents.on("zoom-changed", () => { win.webContents.on("zoom-changed", () => {
@@ -11,6 +11,8 @@ const api: ElectronAPI = {
ipcRenderer.removeListener("init-step", handler) 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"), getDefaultServerUrl: () => ipcRenderer.invoke("get-default-server-url"),
setDefaultServerUrl: (url) => ipcRenderer.invoke("set-default-server-url", url), setDefaultServerUrl: (url) => ipcRenderer.invoke("set-default-server-url", url),
getWslConfig: () => ipcRenderer.invoke("get-wsl-config"), getWslConfig: () => ipcRenderer.invoke("get-wsl-config"),
@@ -15,10 +15,16 @@ export type TitlebarTheme = {
mode: "light" | "dark" mode: "light" | "dark"
} }
export type WindowConfig = {
updaterEnabled: boolean
}
export type ElectronAPI = { export type ElectronAPI = {
killSidecar: () => Promise<void> killSidecar: () => Promise<void>
installCli: () => Promise<string> installCli: () => Promise<string>
awaitInitialization: (onStep: (step: InitStep) => void) => Promise<ServerReadyData> awaitInitialization: (onStep: (step: InitStep) => void) => Promise<ServerReadyData>
getWindowConfig: () => Promise<WindowConfig>
consumeInitialDeepLinks: () => Promise<string[]>
getDefaultServerUrl: () => Promise<string | null> getDefaultServerUrl: () => Promise<string | null>
setDefaultServerUrl: (url: string | null) => Promise<void> setDefaultServerUrl: (url: string | null) => Promise<void>
getWslConfig: () => Promise<WslConfig> getWslConfig: () => Promise<WslConfig>
-2
View File
@@ -4,8 +4,6 @@ declare global {
interface Window { interface Window {
api: ElectronAPI api: ElectronAPI
__OPENCODE__?: { __OPENCODE__?: {
updaterEnabled?: boolean
wsl?: boolean
deepLinks?: string[] deepLinks?: string[]
} }
} }
@@ -20,7 +20,6 @@ import { createEffect, createResource, onCleanup, onMount, Show } from "solid-js
import { render } from "solid-js/web" import { render } from "solid-js/web"
import pkg from "../../package.json" import pkg from "../../package.json"
import { initI18n, t } from "./i18n" import { initI18n, t } from "./i18n"
import { UPDATER_ENABLED } from "./updater"
import { webviewZoom } from "./webview-zoom" import { webviewZoom } from "./webview-zoom"
import "./styles.css" import "./styles.css"
import { useTheme } from "@opencode-ai/ui/theme" import { useTheme } from "@opencode-ai/ui/theme"
@@ -43,8 +42,7 @@ const emitDeepLinks = (urls: string[]) => {
} }
const listenForDeepLinks = () => { const listenForDeepLinks = () => {
const startUrls = window.__OPENCODE__?.deepLinks ?? [] void window.api.consumeInitialDeepLinks().then((urls) => emitDeepLinks(urls))
if (startUrls.length) emitDeepLinks(startUrls)
return window.api.onDeepLink((urls) => emitDeepLinks(urls)) return window.api.onDeepLink((urls) => emitDeepLinks(urls))
} }
@@ -57,13 +55,18 @@ const createPlatform = (): Platform => {
return undefined return undefined
})() })()
const isWslEnabled = async () => {
if (os !== "windows") return false
return window.api.getWslConfig().then((config) => config.enabled).catch(() => false)
}
const wslHome = async () => { const wslHome = async () => {
if (os !== "windows" || !window.__OPENCODE__?.wsl) return undefined if (!(await isWslEnabled())) return undefined
return window.api.wslPath("~", "windows").catch(() => undefined) return window.api.wslPath("~", "windows").catch(() => undefined)
} }
const handleWslPicker = async <T extends string | string[]>(result: T | null): Promise<T | null> => { 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)) { if (Array.isArray(result)) {
return Promise.all(result.map((path) => window.api.wslPath(path, "linux").catch(() => path))) as any 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") { if (os === "windows") {
const resolvedApp = app ? await window.api.resolveAppPath(app).catch(() => null) : null const resolvedApp = app ? await window.api.resolveAppPath(app).catch(() => null) : null
const resolvedPath = await (async () => { const resolvedPath = await (async () => {
if (window.__OPENCODE__?.wsl) { if (await isWslEnabled()) {
const converted = await window.api.wslPath(path, "windows").catch(() => null) const converted = await window.api.wslPath(path, "windows").catch(() => null)
if (converted) return converted if (converted) return converted
} }
@@ -159,12 +162,14 @@ const createPlatform = (): Platform => {
storage, storage,
checkUpdate: async () => { 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() return window.api.checkUpdate()
}, },
update: async () => { update: async () => {
if (!UPDATER_ENABLED()) return const config = await window.api.getWindowConfig().catch(() => ({ updaterEnabled: false }))
if (!config.updaterEnabled) return
await window.api.installUpdate() await window.api.installUpdate()
}, },
@@ -194,11 +199,7 @@ const createPlatform = (): Platform => {
return fetch(input, init) return fetch(input, init)
}, },
getWslEnabled: async () => { getWslEnabled: () => isWslEnabled(),
const next = await window.api.getWslConfig().catch(() => null)
if (next) return next.enabled
return window.__OPENCODE__!.wsl ?? false
},
setWslEnabled: async (enabled) => { setWslEnabled: async (enabled) => {
await window.api.setWslConfig({ enabled }) await window.api.setWslConfig({ enabled })
@@ -249,6 +250,7 @@ listenForDeepLinks()
render(() => { render(() => {
const platform = createPlatform() const platform = createPlatform()
const [windowConfig] = createResource(() => window.api.getWindowConfig().catch(() => ({ updaterEnabled: false })))
const loadLocale = async () => { const loadLocale = async () => {
const current = await platform.storage?.("opencode.global.dat").getItem("language") const current = await platform.storage?.("opencode.global.dat").getItem("language")
const legacy = current ? undefined : await platform.storage?.().getItem("language.v1") const legacy = current ? undefined : await platform.storage?.().getItem("language.v1")
@@ -325,7 +327,7 @@ render(() => {
return ( return (
<PlatformProvider value={platform}> <PlatformProvider value={platform}>
<AppBaseProviders locale={locale.latest}> <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 ( return (
<AppInterface <AppInterface
@@ -1,7 +1,5 @@
import { initI18n, t } from "./i18n" import { initI18n, t } from "./i18n"
export const UPDATER_ENABLED = () => window.__OPENCODE__?.updaterEnabled ?? false
export async function runUpdater({ alertOnFail }: { alertOnFail: boolean }) { export async function runUpdater({ alertOnFail }: { alertOnFail: boolean }) {
await initI18n() await initI18n()
try { try {