From 8ae136f402b559d26e47e64007bd00031482be1c Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Fri, 18 Sep 2026 01:46:45 +0800 Subject: [PATCH] fix(desktop): serve shell-owned update assets Ordinary and mandatory update windows still load dsh-app://shell documents after the standalone plugin manager was removed. The production protocol handler handled only the app hostname, so their existing HTML, scripts, and styles returned 404; mandatory-load failure deliberately retains the modal block. Route the shell hostname to the packaged renderer directory through the existing static-asset helper. Retain its GET/HEAD, path-containment, MIME, and missing-file rules; keep isolated preloads and owned-window IPC authorization unchanged. The regression imports the real main entry, captures its registered protocol callback, and reads all seven actual update assets through the real static helper. It fails with 404 before the fix and passes afterward, including HEAD, rejected methods, malformed paths, traversal, and unknown resources. Sixteen adjacent static/dialog/overlay tests pass, and the Desktop package builds from a fresh worktree. --- ...on-desktop-packaging-and-updates.i18n.yaml | 4 +-- ...-electron-desktop-packaging-and-updates.md | 2 ++ ...ectron-desktop-packaging-and-updates.zh.md | 2 ++ apps/desktop/README.i18n.yaml | 4 +-- apps/desktop/README.md | 2 ++ apps/desktop/README.zh.md | 2 ++ apps/desktop/src/main.ts | 1 + apps/desktop/tests/main-startup.spec.ts | 34 +++++++++++++++++++ 8 files changed, 47 insertions(+), 4 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.i18n.yaml index 9e913ccb88..6fa3c9e31b 100644 --- a/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.md -2026-08-25-electron-desktop-packaging-and-updates.md: c4940942a8d5188faf1846158873c052a43e1fc1 -2026-08-25-electron-desktop-packaging-and-updates.zh.md: 011624980f6f7239a33fb2b6c6803646b7d41dd3 +2026-08-25-electron-desktop-packaging-and-updates.md: f58e8dc471faeb717dd79d32df4afc1b5cc3458f +2026-08-25-electron-desktop-packaging-and-updates.zh.md: 2d2d5c68f28974b606693d5b6727c864c92a0763 diff --git a/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.md b/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.md index c4940942a8..f58e8dc471 100644 --- a/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.md +++ b/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.md @@ -40,6 +40,8 @@ The Desktop Host exposes the shared Web plugin manager for its reserved profile The renderer uses `nodeIntegration: false`, `contextIsolation: true`, and `sandbox: true`. Preload provides boot readiness and failure reporting, native directory selection, theme synchronization, and Windows menu and appearance adapters. It exposes no raw `ipcRenderer`, filesystem access, shell commands, or pnpm arguments. Electron menus and native dialogs use typed English or Chinese copy with English fallback; Windows follows the main document’s language. The shared Web plugin manager owns its client copy. +Update confirmations use the shell static origin because they must remain available without a ready Web Host. Their packaged documents and assets retain the same method and path restrictions as other local static assets. + ## Filesystem layout ```text diff --git a/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.zh.md b/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.zh.md index 011624980f..2d2d5c68f2 100644 --- a/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.zh.md @@ -40,6 +40,8 @@ Desktop Host 为保留 profile 提供共享 Web 插件管理器,并通过启 渲染进程使用 `nodeIntegration: false`、`contextIsolation: true` 和 `sandbox: true`。Preload 提供启动就绪与失败报告、原生目录选择、主题同步,以及 Windows 菜单和外观适配。它不暴露原始 `ipcRenderer`、文件系统访问、shell 命令或 pnpm 参数。Electron 菜单与原生对话框使用类型化中英文文案,并以英文回退;Windows 跟随主文档语言。共享 Web 插件管理器负责自身客户端文案。 +更新确认使用 shell 静态 origin,因为 Web Host 尚未就绪时也必须可用。其打包文档和资源保留与其他本地静态资源相同的方法和路径限制。 + ## 文件系统布局 ```text diff --git a/apps/desktop/README.i18n.yaml b/apps/desktop/README.i18n.yaml index 5469b1d2f7..3284b14418 100644 --- a/apps/desktop/README.i18n.yaml +++ b/apps/desktop/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write apps/desktop/README.md -README.md: fff15fb8200a776bd011e9f69815c5add75419b1 -README.zh.md: 9fd3fe71948200e520573cdf7958bfb03de83720 +README.md: 62d51b39c802f1c3d047c27890a3c3fb91e832bd +README.zh.md: a1e4ae4f1e3c6489ca49f7048ca8296327bdf434 diff --git a/apps/desktop/README.md b/apps/desktop/README.md index fff15fb820..62d51b39c8 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -48,6 +48,8 @@ Electron owns `$DSH_HOME/profiles/desktop`. Its `dependencies` contains packages The application preload exposes boot readiness, fatal startup reporting, and native directory selection. Product documents use the shared authenticated HTTP APIs and receive the Desktop marker, update presentation, and an action that opens native confirmation; they cannot choose artifacts or authorize installation. Plugin management uses the Web application's authenticated HTTP APIs. Electron exposes no plugin-management IPC or separate management document. No renderer receives filesystem access, raw Electron IPC, a shell, or arbitrary pnpm arguments. +The `dsh-app://shell/` origin serves packaged update documents, scripts, and styles without contacting the Host. Static requests retain GET/HEAD, path-containment, and MIME handling; each update document keeps its isolated preload and owned-window IPC checks. + The product UI retains Web actions, including "Open In..." through the shared authenticated HTTP routes. Desktop uses Web's automatic directory-picker selection and initializes new profiles with the shared Web template's bundles. Electron chooses typed English or Chinese shell copy from its application locale and falls back to English. On Windows, the main document's language updates desktop menus, recovery and update prompts. The repository Client UI i18n gate checks desktop sources. diff --git a/apps/desktop/README.zh.md b/apps/desktop/README.zh.md index 9fd3fe7194..a1e4ae4f1e 100644 --- a/apps/desktop/README.zh.md +++ b/apps/desktop/README.zh.md @@ -48,6 +48,8 @@ Electron 拥有 `$DSH_HOME/profiles/desktop`。其 `dependencies` 包含 pnpm 应用 preload 暴露启动就绪、致命启动失败上报和原生目录选择。产品页面还获得 Desktop 标记、更新展示数据和打开原生确认的操作,不能选择安装产物或授权安装。插件管理使用 Web 应用经过认证的 HTTP API;Electron 不提供插件管理 IPC 或独立管理页面。 +`dsh-app://shell/` 无需联系 Host 即可提供打包的更新文档、脚本和样式。静态请求保留 GET/HEAD、路径范围和 MIME 处理;每个更新文档继续使用隔离 preload 和所属窗口的 IPC 校验。 + 产品 UI 保留 Web 操作,包括通过共享认证 HTTP 路由执行的“打开方式…”。Desktop 使用 Web 的自动目录选择机制,并以共享 Web 模板的 bundle 列表初始化新 profile。 Electron 根据应用语言选择类型化的英文或中文 shell 文案,并回退到英文。在 Windows 上,主文档的语言会更新桌面菜单、恢复与更新提示。仓库 Client UI i18n 检查覆盖桌面端源码。 diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index d5bfb1e0b3..c57865e434 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -407,6 +407,7 @@ async function main(): Promise { } return forwardWebRequest(request, hostUrl, hostCookie) } + if (url.hostname === 'shell') return serveWebDocument(request, join(app.getAppPath(), 'renderer')) return Promise.resolve(new Response(null, { status: 404 })) }) diff --git a/apps/desktop/tests/main-startup.spec.ts b/apps/desktop/tests/main-startup.spec.ts index 246dcbcfdf..8b6f69bf1e 100644 --- a/apps/desktop/tests/main-startup.spec.ts +++ b/apps/desktop/tests/main-startup.spec.ts @@ -307,6 +307,40 @@ afterEach(async () => { }) describe('desktop main startup', () => { + it('routes shell update documents and assets through the registered main protocol handler', async () => { + const root = join(import.meta.dirname, '..') + vi.spyOn(harness.app, 'getAppPath').mockReturnValue(root) + const web = await import('../src/web-document.ts') + const actual = await vi.importActual('../src/web-document.ts') + vi.mocked(web.serveWebDocument).mockImplementation(actual.serveWebDocument) + try { + await readyForUpdate() + const { protocol } = await import('electron') + const handler = vi.mocked(protocol).handle.mock.calls.at(-1)?.[1] + if (handler === undefined) throw new Error('main did not register its protocol handler') + for (const [name, mime] of [ + ['update-dialog.html', 'text/html'], ['update-dialog.css', 'text/css'], ['update-dialog.js', 'text/javascript'], + ['mandatory-update.html', 'text/html'], ['mandatory-update.css', 'text/css'], ['mandatory-update.js', 'text/javascript'], + ['update-close.svg', 'image/svg+xml'], + ] as const) { + const response = await handler(new Request(`dsh-app://shell/${name}`)) + expect(response.status).toBe(200) + expect(response.headers.get('content-type')).toContain(mime) + expect(await response.text()).toBe(readFileSync(join(root, 'renderer', name), 'utf8')) + } + const head = await handler(new Request('dsh-app://shell/update-dialog.html', { method: 'HEAD' })) + expect(head.status).toBe(200) + expect(await head.text()).toBe('') + expect((await handler(new Request('dsh-app://shell/update-dialog.html', { method: 'POST' }))).status).toBe(405) + expect((await handler(new Request('dsh-app://shell/%'))).status).toBe(400) + expect((await handler(new Request('dsh-app://shell/%2e%2e%2fpackage.json'))).status).toBe(403) + expect((await handler(new Request('dsh-app://shell/missing.html'))).status).toBe(404) + expect((await handler(new Request('dsh-app://other/update-dialog.html'))).status).toBe(404) + } finally { + vi.mocked(web.serveWebDocument).mockReset() + } + }) + it.each([ ['darwin', true, 'en-US'], ['darwin', false, 'zh-CN'],