Skip to content

Commit 88acdbc

Browse files
committed
feat: make opencode web embeddable in iframes at a subpath
Enables a reverse-proxy / embedding host (e.g. a parent dashboard) to serve opencode web under an arbitrary URL prefix — the built SPA, the server's CSP, and the SDK's default-server-URL resolution all have to cooperate for a same-origin iframe mount to actually work. Three orthogonal changes: 1. packages/app/vite.config.ts — `base: './'` Emits relative asset paths in the built index.html and chunk imports (e.g. `./assets/foo.js` instead of `/assets/foo.js`), so a document loaded under `/some/deep/iframe-prefix/` can resolve its own asset URLs against that prefix rather than against the origin root. No effect on direct-serve at `/`; every Vite-base subpath story just works from one source build. 2. packages/opencode/src/server/routes/ui.ts — add `'unsafe-eval'` to the embedded-UI CSP's script-src directive Something in opencode's production bundle (best guess: a workerized runtime or a lib that compiles at runtime; we haven't bisected the exact call site) exercises `eval()` / `new Function()`. The existing CSP permitted `'wasm-unsafe-eval'` but not `'unsafe-eval'`, causing the browser to block the bundle under the stricter Firefox policy when served behind the embed proxy. Allowing `'unsafe-eval'` keeps the page functional. A better long-term fix is to bisect the eval caller and remove it, then tighten CSP back; this commit is the short-term unblocker. 3. packages/app/src/entry.tsx — `getCurrentUrl` honors the localStorage defaultServerUrl override `getCurrentUrl()` was previously hard-wired to `location.origin` (in production) for the initial `servers[0]` entry, while `getDefaultUrl()` would return the localStorage-set `defaultServerUrl` when present for the `defaultServer` key. The two disagreed: the server-context's `current` server resolves via `allServers().find(key === state.active) ?? allServers()[0]`, so if `state.active` pointed at the localStorage URL but that URL wasn't in `allServers()`, the code fell back to `allServers()[0]` — i.e. `location.origin` — and control-plane requests like `/global/config` and `/global/event` bypassed the override entirely. Having `getCurrentUrl` also honor the localStorage override keeps both entries aligned and makes the override globally effective. Together these let opencode web embed inside an iframe served from a foreign origin / subpath: assets load, the SPA bundle executes, and all SDK calls (including control-plane routes) honor the configured server URL.
1 parent 3175a3c commit 88acdbc

3 files changed

Lines changed: 14 additions & 2 deletions

File tree

packages/app/src/entry.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ if (!(root instanceof HTMLElement) && import.meta.env.DEV) {
9898
}
9999

100100
const getCurrentUrl = () => {
101+
// Honor the localStorage `defaultServerUrl` override if set so that the
102+
// initial `servers[0]` entry matches the `defaultServer` key; otherwise
103+
// `allServers().find(...)` in the server context falls back to
104+
// `allServers()[0]` and the SDK ends up calling `location.origin` for
105+
// control-plane ("/global/*") routes even when the user configured a
106+
// different server URL via localStorage.
107+
const lsDefault = readDefaultServerUrl()
108+
if (lsDefault) return lsDefault
101109
if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
102110
if (import.meta.env.DEV)
103111
return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`

packages/app/vite.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import desktopPlugin from "./vite"
33

44
export default defineConfig({
55
plugins: [desktopPlugin] as any,
6+
// Relative base so the built SPA can be served under any URL subpath
7+
// (iframe embedding via reverse proxy) without requiring absolute-path
8+
// asset resolution at the host origin. Also works for direct-serve at `/`.
9+
base: "./",
610
server: {
711
host: "0.0.0.0",
812
allowedHosts: true,

packages/opencode/src/server/routes/ui.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ const embeddedUIPromise = Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI
1111
import("opencode-web-ui.gen.ts").then((module) => module.default as Record<string, string>).catch(() => null)
1212

1313
const DEFAULT_CSP =
14-
"default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:"
14+
"default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:"
1515

1616
const csp = (hash = "") =>
17-
`default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:`
17+
`default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:`
1818

1919
export const UIRoutes = (): Hono =>
2020
new Hono().all("/*", async (c) => {

0 commit comments

Comments
 (0)