dsh-mobile 0.4.3 → 0.4.4
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 +6 -0
- package/CONTRIBUTORS.md +53 -0
- package/README.en.md +12 -6
- package/README.md +12 -6
- package/lib/index.d.mts +7 -1
- package/lib/index.mjs +95 -23
- package/lib/index.mjs.map +1 -1
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
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.4.4 - 2026-09-20
|
|
6
|
+
|
|
7
|
+
- Keep DSH running if mDNS response callbacks, the multicast-dns emitter, or the already-bound UDP discovery socket reports a network error. LAN discovery records the degraded channel and logs the error code while the authenticated HTTP and WebSocket gateway remains available (thanks @KMGTPEZY for [#95](https://github.com/saya-ch/dsh-mobile/issues/95)).
|
|
8
|
+
- Accept pinned cpolar and cloudflared component downloads when a proxy omits `Content-Length`, and stop reading as soon as the response exceeds the pinned size.
|
|
9
|
+
- Credit community PR authors, including work adapted after a PR was closed, and every historical issue author by contribution type in [CONTRIBUTORS.md](CONTRIBUTORS.md), separately from GitHub's commit-based Contributors panel.
|
|
10
|
+
|
|
5
11
|
## 0.4.3 - 2026-09-19
|
|
6
12
|
|
|
7
13
|
- Support Linux for the Funnel, cpolar and cloudflared remote providers, including x64 and arm64 Funnel host binaries.
|
package/CONTRIBUTORS.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# 贡献者 / Contributors
|
|
2
|
+
|
|
3
|
+
GitHub 的 [Contributors 图表](https://github.com/saya-ch/dsh-mobile/graphs/contributors) 按进入默认分支的提交自动统计,无法手动加入仅提交 issue 或 PR 提案的成员。本页按实际参与方式感谢社区成员;仓库维护者与自动依赖更新仍可在 GitHub 历史记录中查看。
|
|
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-20. The repository maintainer and automated dependency updates remain visible in GitHub's history.
|
|
6
|
+
|
|
7
|
+
## 已合并 PR / Merged pull requests
|
|
8
|
+
|
|
9
|
+
| 社区成员 / Community member | 已合并 PR / Merged PRs |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| [@1624318455](https://github.com/1624318455) | [#93](https://github.com/saya-ch/dsh-mobile/pull/93) |
|
|
12
|
+
| [@abworks-dev](https://github.com/abworks-dev) | [#91](https://github.com/saya-ch/dsh-mobile/pull/91) |
|
|
13
|
+
| [@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) |
|
|
14
|
+
| [@JackRushante](https://github.com/JackRushante) | [#16](https://github.com/saya-ch/dsh-mobile/pull/16) |
|
|
15
|
+
| [@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) |
|
|
16
|
+
| [@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) |
|
|
17
|
+
| [@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) |
|
|
18
|
+
| [@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) |
|
|
19
|
+
|
|
20
|
+
## 已吸收但未直接合并的 PR / Incorporated without a direct merge
|
|
21
|
+
|
|
22
|
+
[@BlueandwhiteXD](https://github.com/BlueandwhiteXD) 的 [#15](https://github.com/saya-ch/dsh-mobile/pull/15) 提出了并实现了 Android 键盘遮挡修复。由于发布分支已有重叠的状态栏改动,该方案改写后进入主线提交 [`09ef315`](https://github.com/saya-ch/dsh-mobile/commit/09ef31590219f2a436bd801214703338c215b00d),并在 [`04e6a0d`](https://github.com/saya-ch/dsh-mobile/commit/04e6a0da030353d4f0897419d0f692dc538a752d) 以 `Co-authored-by` 致谢。PR 的 Closed 状态不应掩盖这项已采用的代码贡献。
|
|
23
|
+
|
|
24
|
+
[@qzyqmzn](https://github.com/qzyqmzn) 的 [#32](https://github.com/saya-ch/dsh-mobile/pull/32) 和 [#37](https://github.com/saya-ch/dsh-mobile/pull/37) 被后续已合并的 [#38](https://github.com/saya-ch/dsh-mobile/pull/38) 取代;其正式贡献已列在上方。
|
|
25
|
+
|
|
26
|
+
[@BlueandwhiteXD](https://github.com/BlueandwhiteXD)'s [#15](https://github.com/saya-ch/dsh-mobile/pull/15) supplied the Android keyboard-overlap fix. It was adapted into [`09ef315`](https://github.com/saya-ch/dsh-mobile/commit/09ef31590219f2a436bd801214703338c215b00d) alongside overlapping status-bar work and credited with `Co-authored-by` in [`04e6a0d`](https://github.com/saya-ch/dsh-mobile/commit/04e6a0da030353d4f0897419d0f692dc538a752d). Its Closed PR status does not mean the code contribution was rejected. @qzyqmzn's [#32](https://github.com/saya-ch/dsh-mobile/pull/32) and [#37](https://github.com/saya-ch/dsh-mobile/pull/37) were superseded by merged [#38](https://github.com/saya-ch/dsh-mobile/pull/38).
|
|
27
|
+
|
|
28
|
+
## 问题反馈与建议 / Issue reports and ideas
|
|
29
|
+
|
|
30
|
+
以下名单包括提出问题、复现缺陷和建议功能的成员;出现在本表不表示提交过代码。 / These members opened repository issues; listing them does not imply code authorship.
|
|
31
|
+
|
|
32
|
+
| 反馈贡献者 / Issue reporter | 提交的问题 / Issues |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| [@1624318455](https://github.com/1624318455) | [#92](https://github.com/saya-ch/dsh-mobile/issues/92) |
|
|
35
|
+
| [@CESAEREE](https://github.com/CESAEREE) | [#1](https://github.com/saya-ch/dsh-mobile/issues/1) |
|
|
36
|
+
| [@CharlesLueng](https://github.com/CharlesLueng) | [#68](https://github.com/saya-ch/dsh-mobile/issues/68) |
|
|
37
|
+
| [@chmzs](https://github.com/chmzs) | [#73](https://github.com/saya-ch/dsh-mobile/issues/73) |
|
|
38
|
+
| [@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) |
|
|
39
|
+
| [@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) |
|
|
40
|
+
| [@jillkayat](https://github.com/jillkayat) | [#10](https://github.com/saya-ch/dsh-mobile/issues/10) |
|
|
41
|
+
| [@KMGTPEZY](https://github.com/KMGTPEZY) | [#95](https://github.com/saya-ch/dsh-mobile/issues/95) |
|
|
42
|
+
| [@LWping](https://github.com/LWping) | [#94](https://github.com/saya-ch/dsh-mobile/issues/94) |
|
|
43
|
+
| [@oliverwan97](https://github.com/oliverwan97) | [#26](https://github.com/saya-ch/dsh-mobile/issues/26) |
|
|
44
|
+
| [@rgshendong](https://github.com/rgshendong) | [#87](https://github.com/saya-ch/dsh-mobile/issues/87) |
|
|
45
|
+
| [@RonaldinhoL](https://github.com/RonaldinhoL) | [#89](https://github.com/saya-ch/dsh-mobile/issues/89) |
|
|
46
|
+
| [@shengyvself](https://github.com/shengyvself) | [#13](https://github.com/saya-ch/dsh-mobile/issues/13), [#14](https://github.com/saya-ch/dsh-mobile/issues/14) |
|
|
47
|
+
| [@sortjiajun](https://github.com/sortjiajun) | [#90](https://github.com/saya-ch/dsh-mobile/issues/90) |
|
|
48
|
+
| [@StrawberryAO](https://github.com/StrawberryAO) | [#39](https://github.com/saya-ch/dsh-mobile/issues/39) |
|
|
49
|
+
| [@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) |
|
|
50
|
+
| [@taki-ohh](https://github.com/taki-ohh) | [#19](https://github.com/saya-ch/dsh-mobile/issues/19) |
|
|
51
|
+
| [@Xingcheng-wang](https://github.com/Xingcheng-wang) | [#76](https://github.com/saya-ch/dsh-mobile/issues/76) |
|
|
52
|
+
| [@Yurzi](https://github.com/Yurzi) | [#17](https://github.com/saya-ch/dsh-mobile/issues/17) |
|
|
53
|
+
| [@Ztyss](https://github.com/Ztyss) | [#22](https://github.com/saya-ch/dsh-mobile/issues/22), [#23](https://github.com/saya-ch/dsh-mobile/issues/23) |
|
package/README.en.md
CHANGED
|
@@ -24,20 +24,21 @@
|
|
|
24
24
|
<a href="#third-party-plugin-compatibility">Third-party plugins</a> ·
|
|
25
25
|
<a href="#security">Security</a> ·
|
|
26
26
|
<a href="#compatibility">Compatibility</a> ·
|
|
27
|
+
<a href="#contributors">Contributors</a> ·
|
|
27
28
|
<a href="CHANGELOG.md">Changelog</a> ·
|
|
28
29
|
<a href="README.md">简体中文</a>
|
|
29
30
|
</p>
|
|
30
31
|
|
|
31
32
|
> DSH Mobile is a DeepSeek Harness community plugin; the native app supports Android only.
|
|
32
33
|
>
|
|
33
|
-
> **0.4.
|
|
34
|
+
> **0.4.4 update**: runtime mDNS or UDP discovery errors no longer take down DSH, and component downloads accept proxy responses without `Content-Length`. [Details](CHANGELOG.md).
|
|
34
35
|
>
|
|
35
|
-
> **Upgrade reminder**: the 0.4.
|
|
36
|
+
> **Upgrade reminder**: the 0.4.4 plugin continues to work with existing Android apps and paired devices do not need re-pairing. Install the 0.4.4 app if you want this Android build. [Compatibility notes](#compatibility).
|
|
36
37
|
|
|
37
38
|
<p align="center">
|
|
38
|
-
<a href="https://github.com/saya-ch/dsh-mobile/releases/download/v0.4.
|
|
39
|
-
<a href="https://github.com/saya-ch/dsh-mobile/releases/download/v0.4.
|
|
40
|
-
<sub><a href="https://github.com/saya-ch/dsh-mobile/releases/tag/v0.4.
|
|
39
|
+
<a href="https://github.com/saya-ch/dsh-mobile/releases/download/v0.4.4/dsh-mobile-android-v0.4.4.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.4/dsh-mobile-android-v0.4.4.apk"><strong>Download Android app 0.4.4</strong></a><br>
|
|
41
|
+
<sub><a href="https://github.com/saya-ch/dsh-mobile/releases/tag/v0.4.4">Release notes and checksums</a></sub>
|
|
41
42
|
</p>
|
|
42
43
|
|
|
43
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.
|
|
@@ -215,7 +216,7 @@ Compatibility and WebSocket rules:
|
|
|
215
216
|
|
|
216
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.
|
|
217
218
|
|
|
218
|
-
-
|
|
219
|
+
- Version 0.4.4 is contract-checked against DSH `0.1.6-alpha.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
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.
|
|
220
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.
|
|
221
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.
|
|
@@ -302,6 +303,7 @@ On macOS, local network, self-hosted FRP, and the own reverse proxy work; the th
|
|
|
302
303
|
|
|
303
304
|
| DSH Mobile plugin | Verified DeepSeek Harness version |
|
|
304
305
|
| --- | --- |
|
|
306
|
+
| `0.4.4` | `0.1.6-alpha.2` (local source and renderer-v2 contract check) |
|
|
305
307
|
| `0.4.3` | `0.1.6-alpha.2` (local source and renderer-v2 contract check) |
|
|
306
308
|
| `0.4.2` | `0.1.6-alpha.1` (local source and renderer-v2 contract check) |
|
|
307
309
|
| `0.4.1` | `0.1.6-alpha.1` (local source and renderer-v2 contract check) |
|
|
@@ -330,6 +332,10 @@ dsh plugin --profile web remove dsh-mobile
|
|
|
330
332
|
|
|
331
333
|
Source users replace `dsh` with `pnpm dsh`.
|
|
332
334
|
|
|
335
|
+
## Contributors
|
|
336
|
+
|
|
337
|
+
Thanks to everyone who submitted PRs, reproduced issues, or proposed improvements. [The contributors list](CONTRIBUTORS.md) credits merged PRs, PR work later incorporated without a direct merge, and issue reports separately. GitHub's sidebar Contributors panel is generated from commits on the default branch and cannot be manually extended with issue-only contributors.
|
|
338
|
+
|
|
333
339
|
## Development
|
|
334
340
|
|
|
335
341
|
```powershell
|
package/README.md
CHANGED
|
@@ -24,20 +24,21 @@
|
|
|
24
24
|
<a href="#第三方插件适配">第三方插件适配</a> ·
|
|
25
25
|
<a href="#安全">安全</a> ·
|
|
26
26
|
<a href="#兼容性">兼容性</a> ·
|
|
27
|
+
<a href="#贡献者">贡献者</a> ·
|
|
27
28
|
<a href="CHANGELOG.md">更新记录</a> ·
|
|
28
29
|
<a href="README.en.md">English</a>
|
|
29
30
|
</p>
|
|
30
31
|
|
|
31
32
|
> DSH Mobile 是 DeepSeek Harness 社区插件,原生 App 仅支持 Android。
|
|
32
33
|
>
|
|
33
|
-
> **0.4.
|
|
34
|
+
> **0.4.4 更新**:局域网发现遇到运行期 mDNS/UDP 错误时不再带崩 DSH;组件下载也能处理缺少 `Content-Length` 的代理响应。[详细记录](CHANGELOG.md)。
|
|
34
35
|
>
|
|
35
|
-
> **升级提醒**:0.4.
|
|
36
|
+
> **升级提醒**:0.4.4 插件可继续使用现有 Android App,已有设备无需重新配对。若要使用本次 Android 构建,请安装 0.4.4 App。[兼容说明](#兼容性)。
|
|
36
37
|
|
|
37
38
|
<p align="center">
|
|
38
|
-
<a href="https://github.com/saya-ch/dsh-mobile/releases/download/v0.4.
|
|
39
|
-
<a href="https://github.com/saya-ch/dsh-mobile/releases/download/v0.4.
|
|
40
|
-
<sub><a href="https://github.com/saya-ch/dsh-mobile/releases/tag/v0.4.
|
|
39
|
+
<a href="https://github.com/saya-ch/dsh-mobile/releases/download/v0.4.4/dsh-mobile-android-v0.4.4.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.4/dsh-mobile-android-v0.4.4.apk"><strong>下载 Android App 0.4.4</strong></a><br>
|
|
41
|
+
<sub><a href="https://github.com/saya-ch/dsh-mobile/releases/tag/v0.4.4">版本说明与校验文件</a></sub>
|
|
41
42
|
</p>
|
|
42
43
|
|
|
43
44
|
DSH Mobile 是一个 DeepSeek Harness 插件,让手机浏览器或 Android App 通过局域网,或可选的 Tailscale Funnel、cpolar、cloudflared、自建 FRP 或自有反向代理远程通道连接电脑,继续使用同一份会话、工作区、消息和工具。局域网与远程访问分别启停、分别管理设备,且都不修改 DeepSeek Harness 源码。
|
|
@@ -215,7 +216,7 @@ Android App 用一个“已配对设备”列表同时显示多台电脑:局
|
|
|
215
216
|
|
|
216
217
|
代理页面为兼容部分社区插件允许嵌入 HTTP 页面;这类内容未加密,可能被篡改,浏览器也可能因混合内容策略拦截。处理敏感内容时请使用 HTTPS。通过 HTTPS 管理入口打开远程面板时,页面顶部会显示相同提醒。
|
|
217
218
|
|
|
218
|
-
-
|
|
219
|
+
- 0.4.4 按 DSH `0.1.6-alpha.2` 做 renderer-v2 契约检查。DSH 页面需要提供标准的会话、`main`/`panelInfo` 和 `rightbar` 插槽;插件自身需要通过 DSH 的标准面板或侧边栏入口注册内容。
|
|
219
220
|
- 网关默认只允许 DSH 内置的第一方 WebSocket 路径。社区侧边栏插件使用的其他路径默认拦截,通常会在诊断页显示为待处理项目;截图中的`/sidebar/ws/agent-opens` 和`/sidebar/ws/agent-terminals` 就属于这类需要按实际插件确认的路径。
|
|
220
221
|
- 在 **连接诊断 → 第三方 WebSocket 路径** 中,只对确认过的精确路径点击 **允许**。系统不接受带查询字符串或模糊前缀的路径;不建议使用“全部允许”。已允许的路径可以随时移除,局域网和远程连接使用同一套规则。
|
|
221
222
|
- 放行只代表该路径可以通过已认证、同源的 DSH Mobile 网关,不会开放任意 TCP/UDP 端口,也不会绕过设备配对。若社区插件仍然连接失败,先看诊断页的实际拦截路径,再按一条路径放行。
|
|
@@ -303,6 +304,7 @@ macOS 上局域网、自建 FRP 与自有反向代理可用;三个托管组件
|
|
|
303
304
|
|
|
304
305
|
| DSH Mobile 插件 | 验证支持的 DeepSeek Harness 版本 |
|
|
305
306
|
| ----------------------------------------- | -------------------------------------------------------------- |
|
|
307
|
+
| `0.4.4` | `0.1.6-alpha.2`(本机源码与 renderer-v2 契约检查) |
|
|
306
308
|
| `0.4.3` | `0.1.6-alpha.2`(本机源码与 renderer-v2 契约检查) |
|
|
307
309
|
| `0.4.2` | `0.1.6-alpha.1`(本机源码与 renderer-v2 契约检查) |
|
|
308
310
|
| `0.4.1` | `0.1.6-alpha.1`(本机源码与 renderer-v2 契约检查) |
|
|
@@ -331,6 +333,10 @@ dsh plugin --profile web remove dsh-mobile
|
|
|
331
333
|
|
|
332
334
|
源码模式把上述 `dsh` 换成 `pnpm dsh`。
|
|
333
335
|
|
|
336
|
+
## 贡献者
|
|
337
|
+
|
|
338
|
+
感谢提交 PR、复现问题和提出建议的社区成员。[贡献者名单](CONTRIBUTORS.md)按已合并 PR、后来吸收的 PR 工作和 issue 反馈分别致谢;GitHub 右侧的 Contributors 区域由进入默认分支的提交自动生成,不能手动加入仅反馈问题的成员。
|
|
339
|
+
|
|
334
340
|
## 开发
|
|
335
341
|
|
|
336
342
|
```powershell
|
package/lib/index.d.mts
CHANGED
|
@@ -597,6 +597,7 @@ declare class MobileAccessGateway {
|
|
|
597
597
|
private readonly upstreamAuthenticatedUrl?;
|
|
598
598
|
private readonly extraWebSocketPaths?;
|
|
599
599
|
private readonly blockedUpgradeLog?;
|
|
600
|
+
private readonly onDiscoveryDegraded?;
|
|
600
601
|
readonly access: AccessController;
|
|
601
602
|
private readonly listenerTlsEnabled;
|
|
602
603
|
private readonly tlsEnabled;
|
|
@@ -611,6 +612,7 @@ declare class MobileAccessGateway {
|
|
|
611
612
|
* it, "the phone cannot find this computer" has no diagnosable cause on the host.
|
|
612
613
|
*/
|
|
613
614
|
private discoveryError;
|
|
615
|
+
private mdnsError;
|
|
614
616
|
/** Names of the bundled mobile assets that could not be read, if any. */
|
|
615
617
|
private mobileAssetError;
|
|
616
618
|
private bonjour;
|
|
@@ -644,10 +646,13 @@ declare class MobileAccessGateway {
|
|
|
644
646
|
private readonly probeLimiter;
|
|
645
647
|
constructor(config: ResolvedGatewayConfig, store: DeviceStore, extensions?: MobileAccessService | undefined, upstreamAuthenticatedUrl?: string | undefined, extraWebSocketPaths?: {
|
|
646
648
|
has(pathname: string): boolean;
|
|
647
|
-
} | undefined, blockedUpgradeLog?: BlockedUpgradePathLog | undefined);
|
|
649
|
+
} | undefined, blockedUpgradeLog?: BlockedUpgradePathLog | undefined, onDiscoveryDegraded?: ((source: 'broadcast' | 'mdns', code: string) => void) | undefined);
|
|
648
650
|
/** Initialize durable state, validate TLS, and bind the externally reachable listener. */
|
|
649
651
|
start(): Promise<void>;
|
|
650
652
|
private startDiscovery;
|
|
653
|
+
private reportDiscoveryDegraded;
|
|
654
|
+
private recordBroadcastFailure;
|
|
655
|
+
private recordMdnsFailure;
|
|
651
656
|
private discoveryAnnouncement;
|
|
652
657
|
private closeFailedStart;
|
|
653
658
|
private closeBonjour;
|
|
@@ -668,6 +673,7 @@ declare class MobileAccessGateway {
|
|
|
668
673
|
readonly broadcast: boolean;
|
|
669
674
|
readonly mdns: boolean;
|
|
670
675
|
readonly errorCode?: string;
|
|
676
|
+
readonly mdnsErrorCode?: string;
|
|
671
677
|
readonly mobileAssetsErrorCode?: string;
|
|
672
678
|
};
|
|
673
679
|
private requirePolicy;
|
package/lib/index.mjs
CHANGED
|
@@ -2991,6 +2991,13 @@ function discoveryBroadcastTargets(cidrs) {
|
|
|
2991
2991
|
}
|
|
2992
2992
|
return [...targets];
|
|
2993
2993
|
}
|
|
2994
|
+
function discoveryFailure(error) {
|
|
2995
|
+
const code = error instanceof Error && typeof error.code === "string" ? error.code : "unknown";
|
|
2996
|
+
return Object.freeze({
|
|
2997
|
+
code,
|
|
2998
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2999
|
+
});
|
|
3000
|
+
}
|
|
2994
3001
|
function extensionTarget(pathname) {
|
|
2995
3002
|
const prefix = `${AUTH_PREFIX}/extensions`;
|
|
2996
3003
|
if (pathname === prefix || pathname === `${prefix}/` || pathname === `${prefix}/manifest`) return { kind: "manifest" };
|
|
@@ -3088,6 +3095,7 @@ var MobileAccessGateway = class {
|
|
|
3088
3095
|
upstreamAuthenticatedUrl;
|
|
3089
3096
|
extraWebSocketPaths;
|
|
3090
3097
|
blockedUpgradeLog;
|
|
3098
|
+
onDiscoveryDegraded;
|
|
3091
3099
|
access;
|
|
3092
3100
|
listenerTlsEnabled;
|
|
3093
3101
|
tlsEnabled;
|
|
@@ -3102,6 +3110,7 @@ var MobileAccessGateway = class {
|
|
|
3102
3110
|
* it, "the phone cannot find this computer" has no diagnosable cause on the host.
|
|
3103
3111
|
*/
|
|
3104
3112
|
discoveryError;
|
|
3113
|
+
mdnsError;
|
|
3105
3114
|
/** Names of the bundled mobile assets that could not be read, if any. */
|
|
3106
3115
|
mobileAssetError;
|
|
3107
3116
|
bonjour;
|
|
@@ -3133,12 +3142,13 @@ var MobileAccessGateway = class {
|
|
|
3133
3142
|
removeExtensionContentListener;
|
|
3134
3143
|
renewLimiter;
|
|
3135
3144
|
probeLimiter;
|
|
3136
|
-
constructor(config, store, extensions, upstreamAuthenticatedUrl, extraWebSocketPaths, blockedUpgradeLog) {
|
|
3145
|
+
constructor(config, store, extensions, upstreamAuthenticatedUrl, extraWebSocketPaths, blockedUpgradeLog, onDiscoveryDegraded) {
|
|
3137
3146
|
this.config = config;
|
|
3138
3147
|
this.extensions = extensions;
|
|
3139
3148
|
this.upstreamAuthenticatedUrl = upstreamAuthenticatedUrl;
|
|
3140
3149
|
this.extraWebSocketPaths = extraWebSocketPaths;
|
|
3141
3150
|
this.blockedUpgradeLog = blockedUpgradeLog;
|
|
3151
|
+
this.onDiscoveryDegraded = onDiscoveryDegraded;
|
|
3142
3152
|
this.listenerTlsEnabled = config.tls.mode === "provided";
|
|
3143
3153
|
this.tlsEnabled = config.publicTls;
|
|
3144
3154
|
this.access = new AccessController(store, {
|
|
@@ -3252,33 +3262,56 @@ var MobileAccessGateway = class {
|
|
|
3252
3262
|
const socket = createSocket("udp4");
|
|
3253
3263
|
this.discoverySocket = socket;
|
|
3254
3264
|
const announcement = this.discoveryAnnouncement(port);
|
|
3265
|
+
let binding = true;
|
|
3266
|
+
socket.on("error", (error) => {
|
|
3267
|
+
if (!binding) this.recordBroadcastFailure(error, true);
|
|
3268
|
+
});
|
|
3269
|
+
const sendAnnouncement = (targetPort, address) => {
|
|
3270
|
+
if (this.closing || this.discoveryError !== void 0) return;
|
|
3271
|
+
try {
|
|
3272
|
+
socket.send(announcement, targetPort, address, (error) => {
|
|
3273
|
+
if (error !== null) this.recordBroadcastFailure(error, true);
|
|
3274
|
+
});
|
|
3275
|
+
} catch (error) {
|
|
3276
|
+
this.recordBroadcastFailure(error, true);
|
|
3277
|
+
}
|
|
3278
|
+
};
|
|
3255
3279
|
socket.on("message", (message, remote) => {
|
|
3256
|
-
if (this.closing || !message.equals(DISCOVERY_QUERY) || !addressAllowed(remote.address, this.config.allowedCidrs)) return;
|
|
3257
|
-
|
|
3280
|
+
if (this.closing || this.discoveryError !== void 0 || !message.equals(DISCOVERY_QUERY) || !addressAllowed(remote.address, this.config.allowedCidrs)) return;
|
|
3281
|
+
sendAnnouncement(remote.port, remote.address);
|
|
3258
3282
|
});
|
|
3259
3283
|
const bindHost = isIP(this.config.listenHost) === 4 && isLoopbackAddress(this.config.listenHost) ? this.config.listenHost : "0.0.0.0";
|
|
3260
3284
|
if (await new Promise((resolve) => {
|
|
3261
3285
|
const onBindError = (error) => {
|
|
3262
3286
|
socket.off("error", onBindError);
|
|
3263
|
-
this.
|
|
3264
|
-
code: typeof error.code === "string" ? error.code : "unknown",
|
|
3265
|
-
message: typeof error.message === "string" ? error.message : String(error)
|
|
3266
|
-
});
|
|
3287
|
+
this.recordBroadcastFailure(error, false);
|
|
3267
3288
|
resolve(false);
|
|
3268
3289
|
};
|
|
3269
3290
|
socket.once("error", onBindError);
|
|
3270
|
-
|
|
3271
|
-
socket.
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3291
|
+
try {
|
|
3292
|
+
socket.bind(port, bindHost, () => {
|
|
3293
|
+
socket.off("error", onBindError);
|
|
3294
|
+
binding = false;
|
|
3295
|
+
try {
|
|
3296
|
+
socket.setBroadcast(true);
|
|
3297
|
+
resolve(true);
|
|
3298
|
+
} catch (error) {
|
|
3299
|
+
this.recordBroadcastFailure(error, true);
|
|
3300
|
+
resolve(false);
|
|
3301
|
+
}
|
|
3302
|
+
});
|
|
3303
|
+
} catch (error) {
|
|
3304
|
+
onBindError(error);
|
|
3305
|
+
}
|
|
3306
|
+
}) && this.discoveryError === void 0) {
|
|
3276
3307
|
const announce = () => {
|
|
3277
|
-
for (const target of discoveryBroadcastTargets(this.config.allowedCidrs))
|
|
3308
|
+
for (const target of discoveryBroadcastTargets(this.config.allowedCidrs)) sendAnnouncement(port, target);
|
|
3278
3309
|
};
|
|
3279
3310
|
announce();
|
|
3280
|
-
this.
|
|
3281
|
-
|
|
3311
|
+
if (this.discoveryError === void 0) {
|
|
3312
|
+
this.discoveryTimer = setInterval(announce, DISCOVERY_INTERVAL_MS);
|
|
3313
|
+
this.discoveryTimer.unref();
|
|
3314
|
+
}
|
|
3282
3315
|
} else {
|
|
3283
3316
|
this.discoverySocket = void 0;
|
|
3284
3317
|
try {
|
|
@@ -3286,8 +3319,12 @@ var MobileAccessGateway = class {
|
|
|
3286
3319
|
} catch {}
|
|
3287
3320
|
}
|
|
3288
3321
|
const deviceName = discoveryDeviceName();
|
|
3289
|
-
const
|
|
3322
|
+
const onMdnsError = (error) => {
|
|
3323
|
+
this.recordMdnsFailure(error);
|
|
3324
|
+
};
|
|
3325
|
+
const bonjour = new Bonjour({ disableIPv6: true }, onMdnsError);
|
|
3290
3326
|
this.bonjour = bonjour;
|
|
3327
|
+
bonjour.server.mdns.on("error", onMdnsError);
|
|
3291
3328
|
bonjour.publish({
|
|
3292
3329
|
name: `${deviceName} (${this.config.instanceId.slice(0, 8)})`,
|
|
3293
3330
|
type: MDNS_SERVICE_TYPE,
|
|
@@ -3303,6 +3340,25 @@ var MobileAccessGateway = class {
|
|
|
3303
3340
|
}
|
|
3304
3341
|
});
|
|
3305
3342
|
}
|
|
3343
|
+
reportDiscoveryDegraded(source, code) {
|
|
3344
|
+
try {
|
|
3345
|
+
this.onDiscoveryDegraded?.(source, code);
|
|
3346
|
+
} catch (error) {
|
|
3347
|
+
process.emitWarning(`DSH Mobile could not log ${source} discovery error: ${String(error)}`, { code: "DSH_MOBILE_DISCOVERY_LOG_FAILED" });
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
recordBroadcastFailure(error, notify) {
|
|
3351
|
+
if (this.closing || this.discoveryError !== void 0) return;
|
|
3352
|
+
this.discoveryError = discoveryFailure(error);
|
|
3353
|
+
if (this.discoveryTimer !== void 0) clearInterval(this.discoveryTimer);
|
|
3354
|
+
this.discoveryTimer = void 0;
|
|
3355
|
+
if (notify) this.reportDiscoveryDegraded("broadcast", this.discoveryError.code);
|
|
3356
|
+
}
|
|
3357
|
+
recordMdnsFailure(error) {
|
|
3358
|
+
if (this.closing || this.mdnsError !== void 0) return;
|
|
3359
|
+
this.mdnsError = discoveryFailure(error);
|
|
3360
|
+
this.reportDiscoveryDegraded("mdns", this.mdnsError.code);
|
|
3361
|
+
}
|
|
3306
3362
|
discoveryAnnouncement(port) {
|
|
3307
3363
|
return Buffer.from(JSON.stringify({
|
|
3308
3364
|
deviceName: discoveryDeviceName(),
|
|
@@ -3360,8 +3416,9 @@ var MobileAccessGateway = class {
|
|
|
3360
3416
|
discoveryStatus() {
|
|
3361
3417
|
return Object.freeze({
|
|
3362
3418
|
broadcast: this.discoverySocket !== void 0 && this.discoveryTimer !== void 0,
|
|
3363
|
-
mdns: this.bonjour !== void 0,
|
|
3419
|
+
mdns: this.bonjour !== void 0 && this.mdnsError === void 0,
|
|
3364
3420
|
...this.discoveryError === void 0 ? {} : { errorCode: `discovery_broadcast_${this.discoveryError.code}` },
|
|
3421
|
+
...this.mdnsError === void 0 ? {} : { mdnsErrorCode: `discovery_mdns_${this.mdnsError.code}` },
|
|
3365
3422
|
...this.mobileAssetError === void 0 ? {} : { mobileAssetsErrorCode: this.mobileAssetError }
|
|
3366
3423
|
});
|
|
3367
3424
|
}
|
|
@@ -6721,10 +6778,23 @@ async function attemptDownload(request) {
|
|
|
6721
6778
|
});
|
|
6722
6779
|
const response = first.status >= 300 && first.status < 400 ? await followValidatedRedirect(first, request) : first;
|
|
6723
6780
|
if (!response.ok) throw new Error(`${request.errorPrefix}_download_http_${String(response.status)}`);
|
|
6724
|
-
const
|
|
6725
|
-
if (
|
|
6726
|
-
|
|
6727
|
-
|
|
6781
|
+
const contentLength = response.headers.get("content-length");
|
|
6782
|
+
if (contentLength !== null && (!/^\d+$/u.test(contentLength) || Number(contentLength) !== request.expectedBytes)) throw new Error(`${request.errorPrefix}_download_size_mismatch`);
|
|
6783
|
+
if (response.body === null) throw new Error(`${request.errorPrefix}_download_size_mismatch`);
|
|
6784
|
+
const bytes = new Uint8Array(request.expectedBytes);
|
|
6785
|
+
const reader = response.body.getReader();
|
|
6786
|
+
let received = 0;
|
|
6787
|
+
while (true) {
|
|
6788
|
+
const chunk = await reader.read();
|
|
6789
|
+
if (chunk.done) break;
|
|
6790
|
+
if (chunk.value.byteLength > bytes.byteLength - received) {
|
|
6791
|
+
await reader.cancel();
|
|
6792
|
+
throw new Error(`${request.errorPrefix}_download_size_mismatch`);
|
|
6793
|
+
}
|
|
6794
|
+
bytes.set(chunk.value, received);
|
|
6795
|
+
received += chunk.value.byteLength;
|
|
6796
|
+
}
|
|
6797
|
+
if (received !== request.expectedBytes) throw new Error(`${request.errorPrefix}_download_size_mismatch`);
|
|
6728
6798
|
return bytes;
|
|
6729
6799
|
}
|
|
6730
6800
|
/**
|
|
@@ -11312,7 +11382,9 @@ async function apply(ctx, config) {
|
|
|
11312
11382
|
...candidateConfig,
|
|
11313
11383
|
upstreamOrigin: template.upstreamOrigin.origin
|
|
11314
11384
|
});
|
|
11315
|
-
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl, webSocketPaths, blockedUpgradePaths)
|
|
11385
|
+
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl, webSocketPaths, blockedUpgradePaths, (source, code) => {
|
|
11386
|
+
logger.warn("%s discovery degraded while DSH remains available: %s", source, code);
|
|
11387
|
+
});
|
|
11316
11388
|
await candidate.start();
|
|
11317
11389
|
lanGateway = candidate;
|
|
11318
11390
|
const removeTaskSink = taskEventHub.add(candidate);
|