refactor(desktop-electron): enable contextIsolation and sandbox (#23523)
This commit is contained in:
@@ -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",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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()),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user