dsh-mobile 0.4.7 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  Notable changes to DSH Mobile are recorded here. GitHub Releases remain the source for downloadable packages and complete generated commit notes.
4
4
 
5
+ ## 0.5.0 - 2026-09-26
6
+
7
+ - Recognize the official DSH Desktop `dsh-app://app` page as a Mobile administration surface, while requiring its signed DSH browser session for forwarded management requests (thanks @Sanksu for [PR #118](https://github.com/saya-ch/dsh-mobile/pull/118) and @up-and-down-0618 for [#115](https://github.com/saya-ch/dsh-mobile/issues/115)).
8
+ - Send paired-page file uploads through the authenticated Mobile transport so PDF and other binary attachments reach DSH without bypassing the gateway's CSRF checks (thanks @ayiejosh for [PR #116](https://github.com/saya-ch/dsh-mobile/pull/116)).
9
+ - Allow the paired page to request microphone access for DSH voice input. App 0.5.0 asks for Android microphone permission on first use and grants audio-only capture to the paired HTTPS Origin (thanks @ayiejosh for [PR #119](https://github.com/saya-ch/dsh-mobile/pull/119)).
10
+ - Let narrow phone headers reveal hidden controls by horizontal drag or keyboard focus while keeping menus accessible; remove the redundant header arrow (thanks @ayiejosh for [PR #117](https://github.com/saya-ch/dsh-mobile/pull/117)).
11
+ - Keep composer statistics readable beside the context ring on narrow screens instead of clipping the values (thanks @ayiejosh for [PR #120](https://github.com/saya-ch/dsh-mobile/pull/120)).
12
+ - On touch devices, switching sessions no longer opens the soft keyboard automatically; tapping the composer still opens it normally.
13
+ - Explain cpolar download, extraction and private-storage failures more clearly and preserve the previous component if replacement fails ([#114](https://github.com/saya-ch/dsh-mobile/issues/114)). Diagnostics also flag an active, competing third-party remote pairing channel without modifying its traffic ([#111](https://github.com/saya-ch/dsh-mobile/issues/111)).
14
+
5
15
  ## 0.4.7 - 2026-09-25
6
16
 
7
17
  - Expose `panelInfo` on the mobile layout service for DSH `0.1.7-rc.2`, so built-in panels such as Plugin Manager can mount without breaking the dedicated mobile frontend (thanks @chintoleung for [PR #104](https://github.com/saya-ch/dsh-mobile/pull/104) and [#103](https://github.com/saya-ch/dsh-mobile/issues/103)).
package/CONTRIBUTORS.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  GitHub 的 [Contributors 图表](https://github.com/saya-ch/dsh-mobile/graphs/contributors) 按进入默认分支的提交自动统计,无法手动加入仅提交 issue 或 PR 提案的成员。本页按实际参与方式感谢社区成员;仓库维护者与自动依赖更新仍可在 GitHub 历史记录中查看。
4
4
 
5
- GitHub's Contributors graph counts commits on the default branch; issue reports and PR proposals cannot be manually added to it. This page credits community members by how they contributed, through 2026-09-25. The repository maintainer and automated dependency updates remain visible in GitHub's history.
5
+ GitHub's Contributors graph counts commits on the default branch; issue reports and PR proposals cannot be manually added to it. This page credits community members by how they contributed, through 2026-09-26. The repository maintainer and automated dependency updates remain visible in GitHub's history.
6
6
 
7
7
  ## 已发布版本中的合并 PR / Merged PRs in released versions
8
8
 
@@ -10,13 +10,14 @@ GitHub's Contributors graph counts commits on the default branch; issue reports
10
10
  | --- | --- |
11
11
  | [@1624318455](https://github.com/1624318455) | [#93](https://github.com/saya-ch/dsh-mobile/pull/93) |
12
12
  | [@abworks-dev](https://github.com/abworks-dev) | [#91](https://github.com/saya-ch/dsh-mobile/pull/91), [#98](https://github.com/saya-ch/dsh-mobile/pull/98), [#99](https://github.com/saya-ch/dsh-mobile/pull/99) |
13
- | [@ayiejosh](https://github.com/ayiejosh) | [#109](https://github.com/saya-ch/dsh-mobile/pull/109), [#110](https://github.com/saya-ch/dsh-mobile/pull/110) |
13
+ | [@ayiejosh](https://github.com/ayiejosh) | [#109](https://github.com/saya-ch/dsh-mobile/pull/109), [#110](https://github.com/saya-ch/dsh-mobile/pull/110), [#116](https://github.com/saya-ch/dsh-mobile/pull/116), [#117](https://github.com/saya-ch/dsh-mobile/pull/117), [#119](https://github.com/saya-ch/dsh-mobile/pull/119) |
14
14
  | [@chintoleung](https://github.com/chintoleung) | [#104](https://github.com/saya-ch/dsh-mobile/pull/104) |
15
15
  | [@idoall](https://github.com/idoall) | [#61](https://github.com/saya-ch/dsh-mobile/pull/61), [#66](https://github.com/saya-ch/dsh-mobile/pull/66), [#67](https://github.com/saya-ch/dsh-mobile/pull/67), [#77](https://github.com/saya-ch/dsh-mobile/pull/77) |
16
16
  | [@JackRushante](https://github.com/JackRushante) | [#16](https://github.com/saya-ch/dsh-mobile/pull/16) |
17
17
  | [@liudasheng](https://github.com/liudasheng) | [#100](https://github.com/saya-ch/dsh-mobile/pull/100) |
18
18
  | [@longisland-icetea](https://github.com/longisland-icetea) | [#59](https://github.com/saya-ch/dsh-mobile/pull/59), [#60](https://github.com/saya-ch/dsh-mobile/pull/60), [#63](https://github.com/saya-ch/dsh-mobile/pull/63) |
19
19
  | [@qzyqmzn](https://github.com/qzyqmzn) | [#38](https://github.com/saya-ch/dsh-mobile/pull/38), [#75](https://github.com/saya-ch/dsh-mobile/pull/75) |
20
+ | [@Sanksu](https://github.com/Sanksu) | [#118](https://github.com/saya-ch/dsh-mobile/pull/118) |
20
21
  | [@StrawberryAO](https://github.com/StrawberryAO) | [#34](https://github.com/saya-ch/dsh-mobile/pull/34), [#40](https://github.com/saya-ch/dsh-mobile/pull/40) |
21
22
  | [@xhwxt](https://github.com/xhwxt) | [#105](https://github.com/saya-ch/dsh-mobile/pull/105) |
22
23
  | [@xingleiwu](https://github.com/xingleiwu) | [#79](https://github.com/saya-ch/dsh-mobile/pull/79), [#84](https://github.com/saya-ch/dsh-mobile/pull/84), [#85](https://github.com/saya-ch/dsh-mobile/pull/85), [#86](https://github.com/saya-ch/dsh-mobile/pull/86) |
@@ -41,6 +42,7 @@ GitHub's Contributors graph counts commits on the default branch; issue reports
41
42
  | [@CharlesLueng](https://github.com/CharlesLueng) | [#68](https://github.com/saya-ch/dsh-mobile/issues/68) |
42
43
  | [@chintoleung](https://github.com/chintoleung) | [#103](https://github.com/saya-ch/dsh-mobile/issues/103) |
43
44
  | [@chmzs](https://github.com/chmzs) | [#73](https://github.com/saya-ch/dsh-mobile/issues/73) |
45
+ | [@hairyf](https://github.com/hairyf) | [#114](https://github.com/saya-ch/dsh-mobile/issues/114) |
44
46
  | [@idoall](https://github.com/idoall) | [#42](https://github.com/saya-ch/dsh-mobile/issues/42), [#45](https://github.com/saya-ch/dsh-mobile/issues/45), [#46](https://github.com/saya-ch/dsh-mobile/issues/46), [#47](https://github.com/saya-ch/dsh-mobile/issues/47), [#62](https://github.com/saya-ch/dsh-mobile/issues/62), [#64](https://github.com/saya-ch/dsh-mobile/issues/64) |
45
47
  | [@IvyC-zz](https://github.com/IvyC-zz) | [#58](https://github.com/saya-ch/dsh-mobile/issues/58), [#78](https://github.com/saya-ch/dsh-mobile/issues/78) |
46
48
  | [@jillkayat](https://github.com/jillkayat) | [#10](https://github.com/saya-ch/dsh-mobile/issues/10) |
@@ -56,6 +58,7 @@ GitHub's Contributors graph counts commits on the default branch; issue reports
56
58
  | [@sortjiajun](https://github.com/sortjiajun) | [#90](https://github.com/saya-ch/dsh-mobile/issues/90) |
57
59
  | [@StrawberryAO](https://github.com/StrawberryAO) | [#39](https://github.com/saya-ch/dsh-mobile/issues/39) |
58
60
  | [@Surpass567](https://github.com/Surpass567) | [#80](https://github.com/saya-ch/dsh-mobile/issues/80), [#81](https://github.com/saya-ch/dsh-mobile/issues/81) |
61
+ | [@up-and-down-0618](https://github.com/up-and-down-0618) | [#115](https://github.com/saya-ch/dsh-mobile/issues/115) |
59
62
  | [@taki-ohh](https://github.com/taki-ohh) | [#19](https://github.com/saya-ch/dsh-mobile/issues/19) |
60
63
  | [@Xingcheng-wang](https://github.com/Xingcheng-wang) | [#76](https://github.com/saya-ch/dsh-mobile/issues/76) |
61
64
  | [@xhwxt](https://github.com/xhwxt) | [#107](https://github.com/saya-ch/dsh-mobile/issues/107), [#108](https://github.com/saya-ch/dsh-mobile/issues/108) |
package/README.en.md CHANGED
@@ -31,14 +31,14 @@
31
31
 
32
32
  > DSH Mobile is a DeepSeek Harness community plugin; the native app supports Android only.
33
33
  >
34
- > **Current version: 0.4.7**. It supports DSH `0.1.7-rc.2`, avoids duplicate mobile boot downloads, and improves Android recovery, the phone-width sidebar, and input handling. [Release notes](CHANGELOG.md#047---2026-09-25).
34
+ > **Current version: 0.5.0**. It supports the official DSH Desktop, paired-page file uploads and microphone permissions for voice input, with improved narrow-screen controls and remote diagnostics. [Release notes](CHANGELOG.md#050---2026-09-26).
35
35
  >
36
- > **Upgrade reminder**: update both the plugin and Android app to 0.4.7 when practical. Existing pairings remain intact. The self-signed FRP entry requires at least the 0.4.6 app; older apps can still use existing LAN and trusted-certificate remote connections. [Compatibility notes](#compatibility).
36
+ > **Upgrade reminder**: update both the plugin and Android app to 0.5.0 when practical. Existing pairings remain intact. App microphone permission support requires 0.5.0; older apps keep their existing connection methods. [Compatibility notes](#compatibility).
37
37
 
38
38
  <p align="center">
39
- <a href="https://github.com/saya-ch/dsh-mobile/releases/download/v0.4.7/dsh-mobile-android-v0.4.7.apk"><img src="assets/brand/app-icon-rounded.svg" alt="DSH Mobile Android app icon" width="72" height="72"></a><br>
40
- <a href="https://github.com/saya-ch/dsh-mobile/releases/download/v0.4.7/dsh-mobile-android-v0.4.7.apk"><strong>Download Android app 0.4.7</strong></a><br>
41
- <sub><a href="https://github.com/saya-ch/dsh-mobile/releases/tag/v0.4.7">Release notes and checksums</a></sub>
39
+ <a href="https://github.com/saya-ch/dsh-mobile/releases/download/v0.5.0/dsh-mobile-android-v0.5.0.apk"><img src="assets/brand/app-icon-rounded.svg" alt="DSH Mobile Android app icon" width="72" height="72"></a><br>
40
+ <a href="https://github.com/saya-ch/dsh-mobile/releases/download/v0.5.0/dsh-mobile-android-v0.5.0.apk"><strong>Download Android app 0.5.0</strong></a><br>
41
+ <sub><a href="https://github.com/saya-ch/dsh-mobile/releases/tag/v0.5.0">Release notes and checksums</a></sub>
42
42
  </p>
43
43
 
44
44
  DSH Mobile is a DeepSeek Harness plugin that lets a mobile browser or the Android app connect over a protected LAN or an optional Tailscale Funnel, cpolar, cloudflared, self-hosted FRP, or own reverse-proxy remote path. Local and remote access keep the same sessions, Workspaces, messages, and tools while using separate switches and paired-device stores without modifying DeepSeek Harness source.
@@ -216,7 +216,7 @@ Compatibility and WebSocket rules:
216
216
 
217
217
  Proxied pages allow HTTP frames for compatibility with some community plugins; those pages are unencrypted and can be altered, and browsers may still block them as mixed content. Use HTTPS for sensitive work. The same warning appears at the top of the remote panel when it is opened over HTTPS.
218
218
 
219
- - Version 0.4.7 has been contract-checked and boot-tested against DSH `0.1.7-alpha.2`, `0.1.7-rc.1`, and `0.1.7-rc.2` (renderer-v2). The DSH page must expose the standard session, `main`/`panelInfo`, and `rightbar` slots; the community plugin must register its panel or sidebar content through DSH's standard entry points.
219
+ - Version 0.5.0 continues contract and boot checks against DSH `0.1.7-alpha.2`, `0.1.7-rc.1`, and `0.1.7-rc.2`. Official DSH Desktop `0.1.7-rc.2` uses a `dsh-app://app/` page; the Mobile administration surface still requires Desktop's forwarded authenticated DSH session and does not loosen ordinary Web administration requests.
220
220
  - The gateway allows first-party DSH WebSocket paths by default, including `/sidebar/ws/terminal`. Other paths used by community sidebar plugins are blocked by default and appear in Diagnostics; the `/sidebar/ws/agent-opens` and `/sidebar/ws/agent-terminals` paths in the image are examples that must be reviewed for the actual plugin.
221
221
  - In **Connection diagnostics → Third-party WebSocket paths**, select **Allow** only for an exact path you have verified. Query strings and fuzzy prefixes are rejected; **Allow all** is not recommended. Approved paths can be removed at any time, and the same policy applies to LAN and remote connections.
222
222
  - Approval only lets that path pass through the authenticated, same-origin DSH Mobile gateway. It does not open arbitrary TCP/UDP ports or bypass device pairing. If a community plugin still fails, check the path recorded by Diagnostics and approve one path at a time.
@@ -288,6 +288,7 @@ See [SECURITY.md](SECURITY.md).
288
288
 
289
289
  ## Troubleshooting
290
290
 
291
+ - **The phone shows another plugin’s pairing-code page**: `dsh-remote-web-ui` and DSH Mobile have separate remote channels and pairing codes; they cannot be mixed. If DSH Mobile diagnostics warn about a competing remote plugin, turn off that plugin’s remote access on the computer, refresh, and scan the current DSH Mobile QR code. Merely installing the other plugin without enabling its remote channel does not trigger this warning.
291
292
  - **Development diagnostics say “reachable through the computer's proxy”**: the check tries a direct request first, then an HTTP proxy from the DSH process environment only if direct access fails; `NO_PROXY` may exclude the target. This proves only that the computer completed an HTTPS probe through the proxy, not that the phone or actual tunnel can connect. Test from the phone's mobile network. If both paths fail, diagnostics continue to report the endpoint unreachable instead of treating an offline route as ready.
292
293
  - **Boot fails with `saved LAN interface "XXX" is not connected`**: the
293
294
  computer switched networks (Wi-Fi/Ethernet/dock) and the previously saved
@@ -316,6 +317,7 @@ On macOS, local network, self-hosted FRP, and the own reverse proxy work; the th
316
317
 
317
318
  | DSH Mobile plugin | Verified DeepSeek Harness version |
318
319
  | --- | --- |
320
+ | `0.5.0` | `0.1.7-alpha.2`, `0.1.7-rc.1`, and `0.1.7-rc.2`; official Desktop `0.1.7-rc.2` (administration surface) |
319
321
  | `0.4.7` | `0.1.7-alpha.2`, `0.1.7-rc.1`, and `0.1.7-rc.2` (source contract, isolated pairing, and workspace baseline over WebSocket) |
320
322
  | `0.4.6` | `0.1.7-alpha.2` and `0.1.7-rc.1` (source contract, isolated pairing, and workspace baseline over WebSocket) |
321
323
  | `0.4.5` | `0.1.7-alpha.2` (source contract check, local and remote gateway integration) |
package/README.md CHANGED
@@ -31,14 +31,14 @@
31
31
 
32
32
  > DSH Mobile 是 DeepSeek Harness 社区插件,原生 App 仅支持 Android。
33
33
  >
34
- > **当前版本:0.4.7**。适配 DSH `0.1.7-rc.2`,减少移动端启动资源的重复下载,并改善 Android 连接恢复、窄屏侧栏与输入体验。[更新记录](CHANGELOG.md#047---2026-09-25)。
34
+ > **当前版本:0.5.0**。适配 DSH 官方桌面端,支持移动端文件上传与语音输入权限,并优化窄屏操作和远程故障提示。[更新记录](CHANGELOG.md#050---2026-09-26)。
35
35
  >
36
- > **升级提醒**:建议插件与 Android App 同步更新至 0.4.7,已有配对会保留。自签 FRP 入口需要至少 0.4.6 App;更早的 App 仍可使用原有局域网和受信任证书的远程连接。[兼容说明](#兼容性)。
36
+ > **升级提醒**:建议插件与 Android App 同步更新至 0.5.0,已有配对会保留。App 的麦克风权限适配需要 0.5.0;旧版 App 的既有连接方式不受影响。[兼容说明](#兼容性)。
37
37
 
38
38
  <p align="center">
39
- <a href="https://github.com/saya-ch/dsh-mobile/releases/download/v0.4.7/dsh-mobile-android-v0.4.7.apk"><img src="assets/brand/app-icon-rounded.svg" alt="DSH Mobile 安卓应用图标" width="72" height="72"></a><br>
40
- <a href="https://github.com/saya-ch/dsh-mobile/releases/download/v0.4.7/dsh-mobile-android-v0.4.7.apk"><strong>下载 Android App 0.4.7</strong></a><br>
41
- <sub><a href="https://github.com/saya-ch/dsh-mobile/releases/tag/v0.4.7">版本说明与校验文件</a></sub>
39
+ <a href="https://github.com/saya-ch/dsh-mobile/releases/download/v0.5.0/dsh-mobile-android-v0.5.0.apk"><img src="assets/brand/app-icon-rounded.svg" alt="DSH Mobile 安卓应用图标" width="72" height="72"></a><br>
40
+ <a href="https://github.com/saya-ch/dsh-mobile/releases/download/v0.5.0/dsh-mobile-android-v0.5.0.apk"><strong>下载 Android App 0.5.0</strong></a><br>
41
+ <sub><a href="https://github.com/saya-ch/dsh-mobile/releases/tag/v0.5.0">版本说明与校验文件</a></sub>
42
42
  </p>
43
43
 
44
44
  DSH Mobile 是一个 DeepSeek Harness 插件,让手机浏览器或 Android App 通过局域网,或可选的 Tailscale Funnel、cpolar、cloudflared、自建 FRP 或自有反向代理远程通道连接电脑,继续使用同一份会话、工作区、消息和工具。局域网与远程访问分别启停、分别管理设备,且都不修改 DeepSeek Harness 源码。
@@ -216,7 +216,7 @@ Android App 用一个“已配对设备”列表同时显示多台电脑:局
216
216
 
217
217
  代理页面为兼容部分社区插件允许嵌入 HTTP 页面;这类内容未加密,可能被篡改,浏览器也可能因混合内容策略拦截。处理敏感内容时请使用 HTTPS。通过 HTTPS 管理入口打开远程面板时,页面顶部会显示相同提醒。
218
218
 
219
- - 0.4.7 已按 DSH `0.1.7-alpha.2`、`0.1.7-rc.1` 和 `0.1.7-rc.2` 做 renderer-v2 契约检查与隔离启动联调。DSH 页面需要提供标准的会话、`main`/`panelInfo` 和 `rightbar` 插槽;插件自身需要通过 DSH 的标准面板或侧边栏入口注册内容。
219
+ - 0.5.0 延续对 DSH `0.1.7-alpha.2`、`0.1.7-rc.1` 和 `0.1.7-rc.2` 的契约及启动检查。官方 DSH Desktop `0.1.7-rc.2` 使用 `dsh-app://app/` 页面;移动访问管理入口仍要求桌面端转发的已认证 DSH 会话,不放宽普通 Web 页面的管理请求。
220
220
  - 网关默认只允许 DSH 内置的第一方 WebSocket 路径。社区侧边栏插件使用的其他路径默认拦截,通常会在诊断页显示为待处理项目;截图中的`/sidebar/ws/agent-opens` 和`/sidebar/ws/agent-terminals` 就属于这类需要按实际插件确认的路径。
221
221
  - 在 **连接诊断 → 第三方 WebSocket 路径** 中,只对确认过的精确路径点击 **允许**。系统不接受带查询字符串或模糊前缀的路径;不建议使用“全部允许”。已允许的路径可以随时移除,局域网和远程连接使用同一套规则。
222
222
  - 放行只代表该路径可以通过已认证、同源的 DSH Mobile 网关,不会开放任意 TCP/UDP 端口,也不会绕过设备配对。若社区插件仍然连接失败,先看诊断页的实际拦截路径,再按一条路径放行。
@@ -290,6 +290,7 @@ flowchart LR
290
290
 
291
291
  ## 故障排查
292
292
 
293
+ - **手机出现另一插件的“输入配对码”页面**:`dsh-remote-web-ui` 与 DSH Mobile 各有独立的远程通道和配对码,不能交叉使用。若电脑端连接诊断提示“第三方远程插件”,先关闭该插件的远程访问并刷新,再扫描 DSH Mobile 当前二维码。仅安装该插件而未启用它的远程接管,不会触发这条诊断。
293
294
  - **开发版远程诊断显示“电脑经代理可访问”**:诊断先直连,失败后才尝试 DSH 进程环境变量中的 HTTP 代理;`NO_PROXY` 可排除目标。此结果只说明电脑经代理完成 HTTPS 探测,不代表手机或实际隧道可用。请用手机流量实际连接;若直连和代理都失败,报告仍为不可达,不会把离线误报为就绪。
294
295
  - **启动报 `saved LAN interface "XXX" is not connected` 且 DSH 起不来**:
295
296
  电脑切换过网络(WiFi/有线/扩展坞),之前保存的网卡当前未连接。处理方式
@@ -316,6 +317,7 @@ macOS 上局域网、自建 FRP 与自有反向代理可用;三个托管组件
316
317
 
317
318
  | DSH Mobile 插件 | 验证支持的 DeepSeek Harness 版本 |
318
319
  | ----------------------------------------- | -------------------------------------------------------------- |
320
+ | `0.5.0` | `0.1.7-alpha.2`、`0.1.7-rc.1`、`0.1.7-rc.2`;官方 Desktop `0.1.7-rc.2`(桌面管理入口) |
319
321
  | `0.4.7` | `0.1.7-alpha.2`、`0.1.7-rc.1`、`0.1.7-rc.2`(源码契约、隔离配对及 WebSocket 工作区读取) |
320
322
  | `0.4.6` | `0.1.7-alpha.2`、`0.1.7-rc.1`(源码契约、隔离配对及 WebSocket 工作区读取) |
321
323
  | `0.4.5` | `0.1.7-alpha.2`(源码契约检查、本机局域网及远程网关联调) |
package/SECURITY.md CHANGED
@@ -16,7 +16,7 @@ The maintainer will acknowledge a complete report within seven days. Publication
16
16
 
17
17
  - Keep the ordinary DSH Web listener on loopback.
18
18
  - Expose only the plugin-owned HTTPS listener to the LAN.
19
- - The desktop admin API may accept an RFC1918 or IPv4 link-local Host only when the TCP peer is loopback, for a trusted local reverse proxy or headless host. Do not publicly forward `/api/mobile-access`; Host and loopback checks are not a replacement for proxy access control. Mutating admin requests must carry a same-origin `Origin` header.
19
+ - The desktop admin API may accept an RFC1918 or IPv4 link-local Host only when the TCP peer is loopback, for a trusted local reverse proxy or headless host. Do not publicly forward `/api/mobile-access`; Host and loopback checks are not a replacement for proxy access control. Mutating admin requests require a same-origin `Origin` header, except official DSH Desktop requests: its forwarder strips `Origin` and `Sec-Fetch-Site` and supplies a signed Host browser-session Cookie, so the plugin client sends an exact non-simple marker that survives forwarding. The plugin requires both that marker and Connection's successful browser-session verification before accepting an Origin-less write.
20
20
  - DNS-SD/mDNS, periodic UDP announcements, active UDP query replies, and HTTPS discovery return only the device name, public HTTPS origin, port, protocol version, and stable non-secret installation identifier. Discovery never returns the CA, a pairing key, a device token, Cookies, credentials, or private configuration.
21
21
  - For LAN pairing, only after a user selects a device and enters the fingerprint-bound pairing key may Android fetch the public CA from that exact HTTPS origin. The bootstrap GET sends no key or credential. The app retains the CA in its encrypted credential record and never adds it to Android's system trust settings. Native requests use a private trust store; WebView accepts only the otherwise-untrusted leaf signed by that CA, for the exact origin and validity period. Every other TLS error is cancelled.
22
22
  - Public remote origins provided by Funnel, cpolar, cloudflared, the FRP public-CA entry, or an operator-managed reverse proxy use platform-trusted HTTPS. Android stores no private CA for those credentials and cancels every TLS error. It sends a persisted device token only to the exact Origin that previously received it; a changed remote Origin requires a current one-time pairing token before the app replaces the saved credential. A QR-provided installation identifier alone never authorizes credential renewal. Browser clients likewise require a certificate trusted by their platform.
package/lib/client.js CHANGED
@@ -513,6 +513,30 @@ window.__ModuleLoader__.load({
513
513
  wsPathsAnnounce: "{n} 条插件连接等待放行",
514
514
  wsPathsAdvancedHint: "通常不需要手动填写:用上方按钮即可。只有当你确切知道所需路径时才填。"
515
515
  });
516
+ Object.assign(MOBILE_CONTROL_MESSAGES.en, {
517
+ cpolarDownloadFailed: "Could not download cpolar from its official site. Check this computer’s network or proxy, then retry.",
518
+ cpolarDownloadTimeout: "The cpolar download timed out. Check this computer’s network or proxy, then retry.",
519
+ cpolarDownloadHttp: "The official cpolar download returned HTTP {status}. Retry later or check the official download page.",
520
+ cpolarIntegrityFailed: "The cpolar download did not match the pinned build, so it was not installed. Retry later or update DSH Mobile; do not bypass verification.",
521
+ cpolarExtractFailed: "The cpolar download completed, but extraction failed. Check system extraction tools and security software, then retry.",
522
+ cpolarStorageFailed: "Could not write the cpolar component in DSH Mobile’s private directory. Check disk space, permissions, and security software, then retry."
523
+ });
524
+ Object.assign(MOBILE_CONTROL_MESSAGES.it, {
525
+ cpolarDownloadFailed: "Impossibile scaricare cpolar dal sito ufficiale. Controlla rete o proxy del computer e riprova.",
526
+ cpolarDownloadTimeout: "Download di cpolar scaduto. Controlla rete o proxy del computer e riprova.",
527
+ cpolarDownloadHttp: "Il download ufficiale di cpolar ha restituito HTTP {status}. Riprova più tardi o controlla la pagina ufficiale.",
528
+ cpolarIntegrityFailed: "Il pacchetto cpolar non corrisponde alla versione verificata e non è stato installato. Riprova più tardi o aggiorna DSH Mobile; non aggirare la verifica.",
529
+ cpolarExtractFailed: "Download di cpolar completato, ma estrazione non riuscita. Controlla gli strumenti di estrazione e il software di sicurezza, poi riprova.",
530
+ cpolarStorageFailed: "Impossibile scrivere cpolar nella directory privata di DSH Mobile. Controlla spazio, permessi e software di sicurezza, poi riprova."
531
+ });
532
+ Object.assign(MOBILE_CONTROL_MESSAGES.zh, {
533
+ cpolarDownloadFailed: "无法从 cpolar 官网下载组件。请检查这台电脑的网络或代理,然后重试。",
534
+ cpolarDownloadTimeout: "下载 cpolar 组件超时。请检查这台电脑的网络或代理,然后重试。",
535
+ cpolarDownloadHttp: "cpolar 官网下载返回 HTTP {status}。请稍后重试,或检查官网下载页。",
536
+ cpolarIntegrityFailed: "下载的 cpolar 组件未通过固定版本校验,因此没有安装。请稍后重试或更新 DSH Mobile,不要跳过校验。",
537
+ cpolarExtractFailed: "cpolar 下载完成,但解包失败。请检查系统解压组件和安全软件,然后重试。",
538
+ cpolarStorageFailed: "无法把 cpolar 写入 DSH Mobile 私有目录。请检查磁盘空间、目录权限和安全软件,然后重试。"
539
+ });
516
540
  Object.assign(MOBILE_CONTROL_MESSAGES.en, {
517
541
  lanSetupTitle: "Finish local network setup",
518
542
  lanSetupIntro: "Choose the network shared by this computer and your phone. DSH Mobile creates private certificates only after you confirm.",
@@ -1402,6 +1426,7 @@ window.__ModuleLoader__.load({
1402
1426
  "remote-needs-login": ["Tailscale is waiting for login to finish.", "Return to Remote access and continue login."],
1403
1427
  "remote-connecting": ["The {provider} remote channel is still connecting.", "Wait briefly, then check again."],
1404
1428
  "remote-controller-error": ["The {provider} controller reported {controllerCode}.", "Return to Remote access and reconnect."],
1429
+ "competing-remote-channel": ["Another remote plugin is injecting its own request channel; the phone may show that plugin’s pairing screen.", "Turn off dsh-remote-web-ui remote access on the computer, refresh, and pair using the current DSH Mobile QR code. Do not mix pairing codes."],
1405
1430
  "phone-network-unknown": ["The computer cannot determine whether the router isolates the phone.", "Confirm the phone and computer use the same network, then disable guest-network or AP isolation."]
1406
1431
  },
1407
1432
  it: {
@@ -1426,6 +1451,7 @@ window.__ModuleLoader__.load({
1426
1451
  "remote-needs-login": ["Tailscale attende il completamento dell’accesso.", "Torna ad Accesso remoto e continua l’accesso."],
1427
1452
  "remote-connecting": ["Il canale remoto {provider} è ancora in connessione.", "Attendi qualche istante e ripeti il controllo."],
1428
1453
  "remote-controller-error": ["Il controller {provider} ha segnalato {controllerCode}.", "Torna ad Accesso remoto e riconnettiti."],
1454
+ "competing-remote-channel": ["Un altro plugin remoto sta inserendo il proprio canale di richieste; sul telefono potrebbe apparire la sua schermata di abbinamento.", "Disattiva l’accesso remoto di dsh-remote-web-ui sul computer, aggiorna la pagina e usa il codice QR corrente di DSH Mobile. Non mescolare i codici."],
1429
1455
  "phone-network-unknown": ["Il computer non può stabilire se il router isola il telefono.", "Verifica che telefono e computer usino la stessa rete, poi disattiva rete ospiti o isolamento AP."]
1430
1456
  },
1431
1457
  zh: {
@@ -1450,6 +1476,7 @@ window.__ModuleLoader__.load({
1450
1476
  "remote-needs-login": ["Tailscale 正在等待完成登录。", "返回远程访问并继续登录。"],
1451
1477
  "remote-connecting": ["{provider} 远程通道仍在连接。", "等待片刻后重新检查。"],
1452
1478
  "remote-controller-error": ["{provider} 控制器报告 {controllerCode}。", "返回远程访问并重新连接。"],
1479
+ "competing-remote-channel": ["检测到另一个远程插件注入自己的请求通道;手机可能显示它的配对页面。", "在电脑端关闭 dsh-remote-web-ui 的远程访问并刷新,再用 DSH Mobile 当前二维码配对;不要混用配对码。"],
1453
1480
  "phone-network-unknown": ["电脑无法判断路由器是否隔离了手机。", "确认手机与电脑使用同一网络,并关闭访客网络或 AP 隔离。"]
1454
1481
  }
1455
1482
  };
@@ -2599,12 +2626,13 @@ window.__ModuleLoader__.load({
2599
2626
  [data-dsh-mobile-center] { grid-column:2 !important; width:100vw !important; min-width:0 !important; }
2600
2627
  [data-dsh-mobile-center] > * { min-width:0 !important; }
2601
2628
  [data-dsh-mobile-header] { box-sizing:border-box !important; width:calc(100% - 16px) !important; margin:0 8px !important; min-width:0; padding-top:max(4px,env(safe-area-inset-top)) !important; padding-right:8px !important; padding-left:42px !important; }
2629
+ [data-dsh-mobile-header] :is([class*="_titleRow"],[class*="_headerLeading"]) { touch-action:pan-y; }
2602
2630
  [data-dsh-mobile-header] [class*="_titleRow"] { box-sizing:border-box !important; display:flex !important; align-items:center !important; min-width:0; min-height:32px !important; height:32px !important; gap:6px !important; padding:0 6px !important; }
2603
2631
  [data-dsh-mobile-header] [class*="_titleCluster"] { min-width:0; }
2604
2632
  [data-dsh-mobile-header] [class*="_crumbs"] { min-width:0; overflow:hidden; }
2605
2633
  [data-dsh-mobile-header] [class*="_crumb"] { max-width:46vw; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
2606
- [data-dsh-mobile-header] [class*="_headerActions"] { min-width:0; overflow-x:auto; scrollbar-width:none; }
2607
- [data-dsh-mobile-header] [class*="_headerActions"]::-webkit-scrollbar { display:none; }
2634
+ /* Nested header actions (including the Jobs menu) must paint outside the row. */
2635
+ [data-dsh-mobile-header] [class*="_headerActions"] { flex:none; min-width:max-content; overflow:visible; }
2608
2636
  [data-dsh-mobile-header] [class*="_headerUtilities"] { gap:2px !important; }
2609
2637
  [data-dsh-mobile-header] [class*="_sessionLogButton"] { width:40px; min-width:40px; padding:0 !important; overflow:hidden; color:transparent; font-size:0 !important; }
2610
2638
  [data-dsh-mobile-header] [class*="_sessionLogButton"] > * { display:none !important; }
@@ -2691,6 +2719,15 @@ window.__ModuleLoader__.load({
2691
2719
  [data-dsh-mobile-center] [data-composer-card] ~ * [class*="_root"] [class*="_sep"] { margin:0 6px !important; }
2692
2720
  /* Composer dock stats strip (turns/steps/tokens) reads small on phones. */
2693
2721
  [data-dsh-mobile-center] [data-slot="conversation.composer.dock"] [class*="_root"] { font-size:10px !important; line-height:16px !important; }
2722
+ /* The context ring is a composer-metadata _root too, so the metadata rule
2723
+ above stretched it across the whole dock. It cannot shrink, which left the
2724
+ session statistics beside it a few pixels wide, every number clipped to an
2725
+ ellipsis. Put the numbers in a wrapping left column and the ring in a right
2726
+ column it cannot grow out of: a phone then shows every number in full, and
2727
+ a wide screen keeps the ring beside them on one line. */
2728
+ [data-dsh-mobile-center] [data-composer-card] ~ * [class*="_root"]:has(> [class*="_trigger"][aria-haspopup="dialog"]) { width:auto !important; max-width:none !important; flex:0 0 auto !important; }
2729
+ [data-dsh-mobile-center] [class*="_dock"]:has([data-composer-stats]) { display:grid !important; grid-template-columns:minmax(0,1fr) max-content !important; align-items:center !important; column-gap:12px !important; }
2730
+ [data-dsh-mobile-center] [data-composer-card] ~ * [class*="_root"][data-composer-stats] { box-sizing:border-box !important; width:auto !important; max-width:none !important; flex:0 1 auto !important; flex-wrap:wrap !important; justify-content:flex-start !important; row-gap:2px !important; }
2694
2731
  /* Message runtime details are inline on desktop. Give the clock/runtime
2695
2732
  label its own wrapping row on narrow screens so TTFT and throughput do
2696
2733
  not push the action buttons or clip at the viewport edge. */
@@ -2739,7 +2776,6 @@ window.__ModuleLoader__.load({
2739
2776
  @keyframes dsh-mobile-panel-in { from { opacity:.72; transform:translateY(6px); } }
2740
2777
  @keyframes dsh-mobile-view-in { from { opacity:.58; transform:translateY(5px); } }
2741
2778
  @media (max-width:420px) {
2742
- [data-dsh-mobile-header] [class*="_headerActions"] { max-width:42vw; }
2743
2779
  [data-dsh-mobile-settings-options] [data-slot="settings.general.item"] [class*="_selector"] { align-self:stretch !important; width:100% !important; }
2744
2780
  [data-dsh-mobile-message-column] [data-context-fields] > * { grid-template-columns:1fr !important; }
2745
2781
  }
@@ -2782,6 +2818,93 @@ window.__ModuleLoader__.load({
2782
2818
  return marked;
2783
2819
  }
2784
2820
  const AUTO_HISTORY_THRESHOLD_PX = 64;
2821
+ /** Travel, in CSS pixels, that separates a header strip pan from a tap on a chip. */
2822
+ const STRIP_DRAG_THRESHOLD_PX = 8;
2823
+ /** Measure flowing title controls without counting an open absolute-positioned menu. */
2824
+ function measureHeaderStripOverflow(row) {
2825
+ const left = row.getBoundingClientRect().left;
2826
+ const controls = Array.from(row.children);
2827
+ const actions = row.querySelector(":scope > [class*=\"_titleCluster\"] [class*=\"_headerActions\"]");
2828
+ if (actions !== null) controls.push(actions);
2829
+ const right = Math.max(left, ...controls.map((control) => control.getBoundingClientRect().right));
2830
+ return Math.max(0, right - left - row.clientWidth);
2831
+ }
2832
+ /** Touch-only pan state; browser pointer cancellation does not interrupt the touch stream. */
2833
+ function createHeaderStripPanController(options) {
2834
+ let offset = 0;
2835
+ let drag;
2836
+ let suppressUntil = 0;
2837
+ const clamp = (value) => Math.min(Math.max(0, options.range()), Math.max(0, value));
2838
+ const setOffset = (value) => {
2839
+ const next = clamp(value);
2840
+ if (next === offset) return;
2841
+ offset = next;
2842
+ options.render(offset);
2843
+ };
2844
+ const end = () => {
2845
+ if (drag === void 0) return;
2846
+ if (drag.claimed) suppressUntil = options.now() + 400;
2847
+ drag = void 0;
2848
+ };
2849
+ const cancel = () => {
2850
+ if (drag?.claimed) suppressUntil = options.now() + 400;
2851
+ drag = void 0;
2852
+ };
2853
+ return {
2854
+ offset: () => offset,
2855
+ start: (x, y) => {
2856
+ drag = {
2857
+ x,
2858
+ y,
2859
+ offset,
2860
+ claimed: false
2861
+ };
2862
+ suppressUntil = 0;
2863
+ },
2864
+ move: (x, y) => {
2865
+ if (drag === void 0) return false;
2866
+ const travel = x - drag.x;
2867
+ if (!drag.claimed) {
2868
+ const drift = y - drag.y;
2869
+ if (Math.abs(travel) < STRIP_DRAG_THRESHOLD_PX && Math.abs(drift) < STRIP_DRAG_THRESHOLD_PX) return false;
2870
+ if (Math.abs(drift) > Math.abs(travel)) {
2871
+ drag = void 0;
2872
+ return false;
2873
+ }
2874
+ drag.claimed = true;
2875
+ }
2876
+ setOffset(drag.offset - travel);
2877
+ return true;
2878
+ },
2879
+ end,
2880
+ cancel,
2881
+ sync: () => {
2882
+ if (options.range() === 0) {
2883
+ drag = void 0;
2884
+ suppressUntil = 0;
2885
+ }
2886
+ setOffset(offset);
2887
+ },
2888
+ reveal: (left, right, visibleLeft, visibleRight) => {
2889
+ if (left < visibleLeft) setOffset(offset - (visibleLeft - left));
2890
+ else if (right > visibleRight) setOffset(offset + right - visibleRight);
2891
+ },
2892
+ resetClickSuppression: () => {
2893
+ suppressUntil = 0;
2894
+ },
2895
+ suppressClick: (inHeader, detail) => {
2896
+ if (suppressUntil === 0 || detail === 0) return false;
2897
+ const suppress = inHeader && options.now() <= suppressUntil;
2898
+ suppressUntil = 0;
2899
+ return suppress;
2900
+ },
2901
+ dispose: () => {
2902
+ drag = void 0;
2903
+ suppressUntil = 0;
2904
+ setOffset(0);
2905
+ }
2906
+ };
2907
+ }
2785
2908
  function controlledFileDragEvent(type, files, initialDropEffect) {
2786
2909
  const event = new Event(type, {
2787
2910
  bubbles: true,
@@ -3180,6 +3303,101 @@ window.__ModuleLoader__.load({
3180
3303
  if (branch === null || branch.hasAttribute("disabled") || branch.getAttribute("aria-disabled") === "true") return;
3181
3304
  window.setTimeout(showBranchToast, 80);
3182
3305
  };
3306
+ let stripHeader;
3307
+ const stripParts = () => {
3308
+ const row = stripHeader?.querySelector("[class*=\"_titleRow\"]");
3309
+ if (row === void 0 || row === null) return void 0;
3310
+ return {
3311
+ row,
3312
+ lead: stripHeader?.querySelector("[class*=\"_headerLeading\"]") ?? void 0
3313
+ };
3314
+ };
3315
+ const stripRange = () => {
3316
+ const parts = stripParts();
3317
+ if (!overlayQuery.matches || parts === void 0) return 0;
3318
+ const overflow = measureHeaderStripOverflow(parts.row);
3319
+ return overflow > 1 ? overflow : 0;
3320
+ };
3321
+ const applyStrip = (offset) => {
3322
+ const parts = stripParts();
3323
+ if (parts === void 0) return;
3324
+ const value = offset === 0 ? "" : `translateX(${-offset}px)`;
3325
+ if (parts.row.style.transform !== value) parts.row.style.transform = value;
3326
+ if (parts.lead !== void 0 && parts.lead.style.transform !== value) parts.lead.style.transform = value;
3327
+ };
3328
+ const pan = createHeaderStripPanController({
3329
+ range: stripRange,
3330
+ render: applyStrip,
3331
+ now: () => performance.now()
3332
+ });
3333
+ const syncPanHeader = (header) => {
3334
+ if (header !== stripHeader) {
3335
+ pan.dispose();
3336
+ stripHeader = header;
3337
+ }
3338
+ pan.sync();
3339
+ applyStrip(pan.offset());
3340
+ };
3341
+ let activeTouchId;
3342
+ const onStripTouchStart = (event) => {
3343
+ if (activeTouchId !== void 0) pan.cancel();
3344
+ activeTouchId = void 0;
3345
+ pan.resetClickSuppression();
3346
+ if (event.touches.length !== 1) return;
3347
+ if (!overlayQuery.matches) return;
3348
+ if (!(event.target instanceof Element)) return;
3349
+ if (event.target.closest("[data-dsh-mobile-header]") !== stripHeader) return;
3350
+ if (event.target.closest("[data-dsh-mobile-header] [class*=\"_titleRow\"],[data-dsh-mobile-header] [class*=\"_headerLeading\"]") === null) return;
3351
+ if (stripRange() === 0) return;
3352
+ const touch = event.touches[0];
3353
+ if (touch === void 0) return;
3354
+ activeTouchId = touch.identifier;
3355
+ pan.start(touch.clientX, touch.clientY);
3356
+ };
3357
+ const onStripTouchMove = (event) => {
3358
+ if (activeTouchId === void 0) return;
3359
+ if (event.touches.length !== 1) {
3360
+ activeTouchId = void 0;
3361
+ pan.cancel();
3362
+ return;
3363
+ }
3364
+ const touch = event.touches[0];
3365
+ if (touch === void 0 || touch.identifier !== activeTouchId) return;
3366
+ if (pan.move(touch.clientX, touch.clientY)) event.preventDefault();
3367
+ };
3368
+ const onStripTouchEnd = (event) => {
3369
+ if (activeTouchId === void 0 || !Array.from(event.changedTouches).some((touch) => touch.identifier === activeTouchId)) return;
3370
+ activeTouchId = void 0;
3371
+ pan.end();
3372
+ };
3373
+ const onStripTouchCancel = (event) => {
3374
+ if (activeTouchId === void 0 || !Array.from(event.changedTouches).some((touch) => touch.identifier === activeTouchId)) return;
3375
+ activeTouchId = void 0;
3376
+ pan.cancel();
3377
+ };
3378
+ const onStripFocus = (event) => {
3379
+ if (!(event.target instanceof HTMLElement)) return;
3380
+ const header = event.target.closest("[data-dsh-mobile-header]");
3381
+ if (header === null || header !== stripHeader || stripRange() === 0 || event.target.closest("[class*=\"_titleRow\"],[class*=\"_headerLeading\"]") === null) return;
3382
+ const focused = event.target.getBoundingClientRect();
3383
+ const visible = header.getBoundingClientRect();
3384
+ pan.reveal(focused.left, focused.right, visible.left + 42, visible.right - 8);
3385
+ };
3386
+ const onStripClickCapture = (event) => {
3387
+ const inHeader = event.target instanceof Element && event.target.closest("[data-dsh-mobile-header]") !== null;
3388
+ if (!pan.suppressClick(inHeader, event.detail)) return;
3389
+ event.preventDefault();
3390
+ event.stopPropagation();
3391
+ };
3392
+ document.addEventListener("touchstart", onStripTouchStart, true);
3393
+ document.addEventListener("touchmove", onStripTouchMove, {
3394
+ capture: true,
3395
+ passive: false
3396
+ });
3397
+ document.addEventListener("touchend", onStripTouchEnd, true);
3398
+ document.addEventListener("touchcancel", onStripTouchCancel, true);
3399
+ document.addEventListener("focusin", onStripFocus, true);
3400
+ document.addEventListener("click", onStripClickCapture, true);
3183
3401
  document.addEventListener("click", onBranchClick, true);
3184
3402
  let frame;
3185
3403
  let sidebar;
@@ -3280,6 +3498,7 @@ window.__ModuleLoader__.load({
3280
3498
  const handle = frame === void 0 ? void 0 : firstByClassSuffix(frame, "_handle");
3281
3499
  markNativeMobileSettings(document);
3282
3500
  if (center === void 0) {
3501
+ syncPanHeader(void 0);
3283
3502
  syncComposerEnterNewline(null);
3284
3503
  bindHistoryScroller(void 0);
3285
3504
  syncMediaBinding(null);
@@ -3289,7 +3508,9 @@ window.__ModuleLoader__.load({
3289
3508
  }
3290
3509
  if (center !== void 0) {
3291
3510
  center.dataset.dshMobileCenter = "true";
3292
- center.querySelector("header")?.setAttribute("data-dsh-mobile-header", "true");
3511
+ const header = center.querySelector("header") ?? void 0;
3512
+ header?.setAttribute("data-dsh-mobile-header", "true");
3513
+ syncPanHeader(header);
3293
3514
  viewArea = firstByClassSuffix(center, "_viewArea");
3294
3515
  if (viewArea !== void 0) viewArea.dataset.dshMobileView = "true";
3295
3516
  const conversation = center.querySelector("[data-conversation-scroll]");
@@ -3421,6 +3642,14 @@ window.__ModuleLoader__.load({
3421
3642
  observer.disconnect();
3422
3643
  overlayQuery.removeEventListener("change", schedule);
3423
3644
  document.removeEventListener("click", onBranchClick, true);
3645
+ document.removeEventListener("touchstart", onStripTouchStart, true);
3646
+ document.removeEventListener("touchmove", onStripTouchMove, true);
3647
+ document.removeEventListener("touchend", onStripTouchEnd, true);
3648
+ document.removeEventListener("touchcancel", onStripTouchCancel, true);
3649
+ document.removeEventListener("focusin", onStripFocus, true);
3650
+ document.removeEventListener("click", onStripClickCapture, true);
3651
+ pan.dispose();
3652
+ applyStrip(0);
3424
3653
  cameraButton.removeEventListener("pointerdown", quietMediaPointer);
3425
3654
  cameraButton.removeEventListener("click", takePhoto);
3426
3655
  if (branchToastTimer !== 0) window.clearTimeout(branchToastTimer);
@@ -3445,6 +3674,16 @@ window.__ModuleLoader__.load({
3445
3674
  //#endregion
3446
3675
  //#region src/local-admin-host.ts
3447
3676
  /** Browser-safe desktop-admin Host checks. Do not import Node APIs here. */
3677
+ const DESKTOP_ADMIN_HEADER = "x-dsh-mobile-desktop-admin";
3678
+ const DESKTOP_ADMIN_MARKER = "dsh-app://app";
3679
+ /** Add the forwarded Desktop marker only to this plugin's mutating admin requests. */
3680
+ function localAdminRequestHeaders(init, location) {
3681
+ const headers = new Headers(init?.headers);
3682
+ if (!headers.has("content-type")) headers.set("content-type", "application/json");
3683
+ headers.delete(DESKTOP_ADMIN_HEADER);
3684
+ if (init?.method?.toUpperCase() === "POST" && location.protocol === "dsh-app:" && location.hostname === "app") headers.set(DESKTOP_ADMIN_HEADER, DESKTOP_ADMIN_MARKER);
3685
+ return headers;
3686
+ }
3448
3687
  function unwrapBrackets(hostname) {
3449
3688
  return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
3450
3689
  }
@@ -3485,11 +3724,12 @@ window.__ModuleLoader__.load({
3485
3724
  /**
3486
3725
  * Whether the current browser document should mount the desktop Mobile access
3487
3726
  * control. Dedicated Mobile HTTPS (the phone surface) stays native even when
3488
- * the Host is a private LAN address.
3727
+ * the Host is a private LAN address. DSH Desktop (dsh-app://app) has neither,
3728
+ * so its protocol gated on the fixed hostname "app" counts too.
3489
3729
  */
3490
- function isDesktopAdminSurface(hostname, search = "", frontend) {
3730
+ function isDesktopAdminSurface(hostname, search = "", frontend, protocol = "") {
3491
3731
  const query = search.startsWith("?") ? search.slice(1) : search;
3492
- return isLocalAdminHostname(hostname) && frontend !== "dedicated" && !new URLSearchParams(query).has("dsh-mobile-preview");
3732
+ return (protocol === "dsh-app:" ? hostname === "app" : (protocol === "" || protocol === "http:" || protocol === "https:") && isLocalAdminHostname(hostname)) && frontend !== "dedicated" && !new URLSearchParams(query).has("dsh-mobile-preview");
3493
3733
  }
3494
3734
  //#endregion
3495
3735
  //#region src/client.ts
@@ -3810,10 +4050,7 @@ window.__ModuleLoader__.load({
3810
4050
  const response = await fetch(path, {
3811
4051
  ...init,
3812
4052
  signal: controller.signal,
3813
- headers: {
3814
- "content-type": "application/json",
3815
- ...init?.headers
3816
- }
4053
+ headers: localAdminRequestHeaders(init, location)
3817
4054
  });
3818
4055
  const body = await response.json();
3819
4056
  if (!response.ok) throw new Error(typeof body.error === "string" ? body.error : `HTTP ${String(response.status)}`);
@@ -3913,6 +4150,18 @@ window.__ModuleLoader__.load({
3913
4150
  control_channel_failed: "controlChannelFailed",
3914
4151
  cpolar_component_missing: "cpolarMissing",
3915
4152
  cpolar_component_invalid: "cpolarInvalid",
4153
+ cpolar_download_failed: "cpolarDownloadFailed",
4154
+ cpolar_download_timeout: "cpolarDownloadTimeout",
4155
+ cpolar_download_size_mismatch: "cpolarIntegrityFailed",
4156
+ cpolar_download_hash_mismatch: "cpolarIntegrityFailed",
4157
+ cpolar_executable_hash_mismatch: "cpolarIntegrityFailed",
4158
+ cpolar_download_redirect_missing: "cpolarDownloadFailed",
4159
+ cpolar_download_redirect_invalid: "cpolarDownloadFailed",
4160
+ cpolar_download_redirect_rejected: "cpolarDownloadFailed",
4161
+ cpolar_installer_missing: "cpolarExtractFailed",
4162
+ cpolar_executable_missing: "cpolarExtractFailed",
4163
+ cpolar_extract_failed: "cpolarExtractFailed",
4164
+ cpolar_storage_failed: "cpolarStorageFailed",
3916
4165
  cpolar_config_missing: "cpolarConfigMissing",
3917
4166
  cpolar_config_invalid: "cpolarConfigInvalid",
3918
4167
  cpolar_port_unavailable: "cpolarPortUnavailable",
@@ -4048,6 +4297,8 @@ window.__ModuleLoader__.load({
4048
4297
  */
4049
4298
  const remoteFailureTextFor = (error, fallbackKey) => {
4050
4299
  const message = error instanceof Error ? error.message : String(error);
4300
+ const cpolarHttpStatus = /^cpolar_download_http_(\d{3})$/u.exec(message)?.[1];
4301
+ if (cpolarHttpStatus !== void 0) return t("cpolarDownloadHttp", { status: cpolarHttpStatus });
4051
4302
  const key = REMOTE_ERROR_MESSAGE_KEYS[message];
4052
4303
  return key === void 0 ? t(fallbackKey, { error: message }) : t(key);
4053
4304
  };
@@ -8286,7 +8537,7 @@ window.__ModuleLoader__.load({
8286
8537
  return trustAuthenticatedGatewayConnection(ctx.get("connection"));
8287
8538
  }, "dsh-mobile: authenticated gateway client trust");
8288
8539
  ctx.effect(() => {
8289
- const desktopAdmin = isDesktopAdminSurface(location.hostname, location.search, window.__DSH_MOBILE_FRONTEND__);
8540
+ const desktopAdmin = isDesktopAdminSurface(location.hostname, location.search, window.__DSH_MOBILE_FRONTEND__, location.protocol);
8290
8541
  const style = element("style");
8291
8542
  style.dataset.plugin = "dsh-mobile";
8292
8543
  style.textContent = desktopAdmin ? CONTROL_STYLES : NATIVE_MOBILE_STYLES;