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.
This commit is contained in:
1 parent
371c804336
commit
8ae136f402
8 files changed
+47
-4
No files matched your search
+2
-2
@@ -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
|
||||
+2
@@ -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
|
||||
|
||||
+2
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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 检查覆盖桌面端源码。
|
||||
|
||||
@@ -407,6 +407,7 @@ async function main(): Promise<void> {
|
||||
}
|
||||
return forwardWebRequest(request, hostUrl, hostCookie)
|
||||
}
|
||||
if (url.hostname === 'shell') return serveWebDocument(request, join(app.getAppPath(), 'renderer'))
|
||||
return Promise.resolve(new Response(null, { status: 404 }))
|
||||
})
|
||||
|
||||
|
||||
@@ -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<typeof import('../src/web-document.ts')>('../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'],
|
||||
|
||||
Reference in new issue
Block a user