dsh-mobile 0.8.5 → 0.8.6

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,18 @@
2
2
 
3
3
  Notable changes are recorded here. GitHub Releases provide the downloadable packages and generated commit notes.
4
4
 
5
+ ## 0.8.6 - 2026-11-10
6
+
7
+ - Forward paired plugin `x-dsh-*` request headers through the mobile Gateway while continuing to strip internal `x-dsh-mobile-*` markers and the DSH account credential header. This restores third-party plugin write routes that validate their own request marker. Thanks @mekswhy for the reproduction in [#375](https://github.com/saya-ch/dsh-mobile/issues/375) and @lnuxe for [#376](https://github.com/saya-ch/dsh-mobile/pull/376).
8
+ - Send a bounded same-origin session route with Android task notifications and keep the full opaque Session ID instead of truncating it, so a tap can open the matching conversation. The page and Gateway half ships here; the native half is prepared as Android App 0.7.3 below and released separately. Thanks @owenzhao for the reproduction in [#371](https://github.com/saya-ch/dsh-mobile/issues/371), and @lnuxe for the implementation in [#374](https://github.com/saya-ch/dsh-mobile/pull/374).
9
+ - Add a background-only task event stream to the paired Gateway, so a native client can receive completion events after Android suspends the page instead of relying on the page stream. Credential renewal, pinned TLS, the persistent notification and the deliberate iroh rejection belong to the App half prepared below; the plugin alone does not deliver a background reminder.
10
+ - Keep the browser push subscription after an explicit web-push Disable so iOS can reuse it safely instead of creating an unusable replacement after a quick unsubscribe/subscribe cycle. Thanks @owenzhao for [#373](https://github.com/saya-ch/dsh-mobile/issues/373).
11
+ - Expose the trusted Host-plugin `mobilePush.notify` service for bounded, category-checked custom web-push title/body text and session targets while retaining opaque click references. Thanks @owenzhao for [#372](https://github.com/saya-ch/dsh-mobile/issues/372).
12
+ - Verify updated Windows firewall rules with a name query followed by property filtering, and retain the elevated child's failure text plus its numeric exit code in the plugin log. This removes the PowerShell parameter-set conflict that made managed LAN setup fail after creating only the TCP rule. Thanks @zustttt for the diagnosis in [#379](https://github.com/saya-ch/dsh-mobile/issues/379).
13
+ - Recover the longer iroh private-directory cause chain by following a generic `tls-prepare` failure with a bounded local-only host probe. This keeps the existing sidecar compatible while classifying the reported Windows ACL failure explicitly. Thanks @fgql for the follow-up reproduction in [#365](https://github.com/saya-ch/dsh-mobile/issues/365).
14
+ - Recognize the wrapper failure on any stderr line instead of requiring the whole buffer to equal it, so a preceding warning or an extra diagnostic no longer hides that cause chain. A failed managed-LAN firewall operation now keeps its own error when the scratch directory cannot be removed, and its detail omits the encoded command and local paths; the firewall diagnostics check the same rule profile as the managed setup before reporting a rule as ready.
15
+ - Plugin-only update. Android 0.7.2 remains the published App; the Android 0.7.3 changes below are prepared in this repository and are released separately.
16
+
5
17
  ## 0.8.5 - 2026-11-08
6
18
 
7
19
  - Update an existing Windows managed-LAN firewall rule in place and verify its resulting scope instead of deleting the previous rules first. Administrator cancellation or a policy rejection now retains the prior rule and records a bounded error in the plugin log. Thanks @lyc0110 for the reproduction in [#364](https://github.com/saya-ch/dsh-mobile/issues/364).
@@ -11,7 +23,13 @@ Notable changes are recorded here. GitHub Releases provide the downloadable pack
11
23
  - Add a narrow stale-while-revalidate cache for the two parameterless catalog RPCs (`llm/listProviders` and `session/modelCatalog`). Eligible warm requests share one bounded upstream fill, each caller keeps its own RPC ID, and catalog-affecting mutations invalidate the generation; `catalogCache: false` restores the ordinary proxy path. The cache does not patch upstream client internals or restore labels before the catalog is ready. Thanks @chintoleung for [#367](https://github.com/saya-ch/dsh-mobile/pull/367).
12
24
  - Resolve an in-app plugin update against the profile that loaded the running module when a Desktop host omits the current-profile service. The install-path fallback remains limited to profiles below `$DSH_HOME/profiles`, so a launcher that omits `--profile` can no longer silently update `web` instead of `desktop`. Thanks @Kizaemon for the detailed reproduction in [#368](https://github.com/saya-ch/dsh-mobile/issues/368).
13
25
  - Accept RFC 6598 `100.64.0.0/10` addresses as managed-LAN candidates, including networks whose router or ISP exposes CGNAT-style addressing for the local Ethernet or Wi-Fi segment. The admin Host-header check continues to reject CGNAT to preserve its DNS-rebinding defence. Thanks @Kizaemon for the diagnosis in [#369](https://github.com/saya-ch/dsh-mobile/issues/369).
14
- - Plugin-only preparation candidate. Android 0.7.2 remains the published App; no native App change is included.
26
+ - Plugin-only update. Android 0.7.2 remains the published App; no native App change is included.
27
+
28
+ ## Android 0.7.3 - Unreleased
29
+
30
+ - Make task notification navigation preserve the full bounded opaque Session ID and open the exact conversation after a cold or warm tap. Each notification identifies its tap target by its tag, so two notifications cannot overwrite one another's target even when their request codes collide. Only the built-in `dsh-session` route is accepted. Thanks @owenzhao for the report in [#371](https://github.com/saya-ch/dsh-mobile/issues/371) and @lnuxe for the original implementation in [#374](https://github.com/saya-ch/dsh-mobile/pull/374).
31
+ - Add an explicit, opt-in `dataSync` foreground service for completion alerts after Android suspends the WebView. It renews the paired credential, pins the configured CA, keeps a persistent notification, stops on device removal or revocation, and deliberately rejects iroh pairings until the native transport exposes the same authenticated identity. Notification permission remains a separate Android setting. Thanks @owenzhao for the background-delivery analysis in [#377](https://github.com/saya-ch/dsh-mobile/issues/377).
32
+ - A background-service start that the platform rejects no longer leaves the setting reported as enabled, and the service withdraws the opt-in when Android ends its foreground time budget instead of being killed as unresponsive.
15
33
 
16
34
  ## 0.8.4 - 2026-11-07
17
35
 
package/CONTRIBUTORS.md CHANGED
@@ -4,6 +4,12 @@ GitHub 的 [Contributors 图表](https://github.com/saya-ch/dsh-mobile/graphs/co
4
4
 
5
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. The repository maintainer and automated dependency updates remain visible in GitHub's history.
6
6
 
7
+ ## 下一版本贡献 / Contributions for the next release
8
+
9
+ 感谢 [@mekswhy](https://github.com/mekswhy) 在 [#375](https://github.com/saya-ch/dsh-mobile/issues/375) 报告插件请求头被移动网关丢弃,[@lnuxe](https://github.com/lnuxe) 提供 [#376](https://github.com/saya-ch/dsh-mobile/pull/376) 的网关修复与 [#374](https://github.com/saya-ch/dsh-mobile/pull/374) 的通知导航实现;维护者保留原作者提交并补充安全过滤、完整长度校验、并发与真机验收路径。感谢 [@owenzhao](https://github.com/owenzhao) 在 [#371](https://github.com/saya-ch/dsh-mobile/issues/371)、[#372](https://github.com/saya-ch/dsh-mobile/issues/372)、[#373](https://github.com/saya-ch/dsh-mobile/issues/373) 和 [#377](https://github.com/saya-ch/dsh-mobile/issues/377) 提供通知链路场景、复现和边界说明。感谢 [@zustttt](https://github.com/zustttt) 在 [#379](https://github.com/saya-ch/dsh-mobile/issues/379) 定位 Windows 局域网一键配置失败的 PowerShell 参数集冲突。
10
+
11
+ Thanks [@mekswhy](https://github.com/mekswhy) for reporting the mobile Gateway's dropped plugin request header in [#375](https://github.com/saya-ch/dsh-mobile/issues/375), and [@lnuxe](https://github.com/lnuxe) for the Gateway fix in [#376](https://github.com/saya-ch/dsh-mobile/pull/376) and notification navigation implementation in [#374](https://github.com/saya-ch/dsh-mobile/pull/374). Original commits are retained, with maintainer additions for security filtering, full-length validation, concurrency and device-acceptance paths. Thanks [@owenzhao](https://github.com/owenzhao) for the notification scenarios, reproductions and boundaries in [#371](https://github.com/saya-ch/dsh-mobile/issues/371), [#372](https://github.com/saya-ch/dsh-mobile/issues/372), [#373](https://github.com/saya-ch/dsh-mobile/issues/373) and [#377](https://github.com/saya-ch/dsh-mobile/issues/377). Thanks [@zustttt](https://github.com/zustttt) for diagnosing the PowerShell parameter-set conflict in [#379](https://github.com/saya-ch/dsh-mobile/issues/379).
12
+
7
13
  ## 0.8.5 贡献 / Contributions for 0.8.5
8
14
 
9
15
  感谢 [@lyc0110](https://github.com/lyc0110) 在 [#364](https://github.com/saya-ch/dsh-mobile/issues/364) 报告 Windows 防火墙先删后建的风险,[@fgql](https://github.com/fgql) 在 [#365](https://github.com/saya-ch/dsh-mobile/issues/365) 提供 iroh 私有目录失败的完整诊断,[@LunarFeller](https://github.com/LunarFeller) 在 [#355](https://github.com/saya-ch/dsh-mobile/pull/355) 提供 macOS 完全磁盘访问提示及真机验证,[@chenForbear](https://github.com/chenForbear) 在 [#363](https://github.com/saya-ch/dsh-mobile/issues/363) 提供模块批次测量,[@chintoleung](https://github.com/chintoleung) 提供 [#367](https://github.com/saya-ch/dsh-mobile/pull/367) 的 catalog 缓存,以及 [@Kizaemon](https://github.com/Kizaemon) 在 [#368](https://github.com/saya-ch/dsh-mobile/issues/368) 报告 Desktop 更新配置档错误、在 [#369](https://github.com/saya-ch/dsh-mobile/issues/369) 完成 CGNAT 局域网诊断。
@@ -24,13 +30,13 @@ Thanks [@xaxka](https://github.com/xaxka) for the shared-FRP username in [#344](
24
30
 
25
31
  ## 0.8.2 贡献 / Contributions for 0.8.2
26
32
 
27
- 感谢 [@LUgABeEEE](https://github.com/LUgABeEEE) 通过 [#343](https://github.com/saya-ch/dsh-mobile/pull/343) 修正 Android 固定证书的 WebSocket 地址判断。原作者提交保留;维护者补充完整证书决策组合回归。该原生改动归入独立编号的 Android 0.7.2 候选,不由 npm 升级替代。
33
+ 感谢 [@LUgABeEEE](https://github.com/LUgABeEEE) 通过 [#343](https://github.com/saya-ch/dsh-mobile/pull/343) 修正 Android 固定证书的 WebSocket 地址判断。原作者提交保留;维护者补充完整证书决策组合回归。该原生改动已随独立编号的 Android 0.7.2 发布,不由 npm 升级替代。
28
34
 
29
- Thanks [@LUgABeEEE](https://github.com/LUgABeEEE) for the Android pinned-WebSocket authority fix in [#343](https://github.com/saya-ch/dsh-mobile/pull/343). Original commits are retained alongside maintainer combined-certificate regressions. This native change belongs to the independently versioned Android 0.7.2 candidate, not an npm-only update.
35
+ Thanks [@LUgABeEEE](https://github.com/LUgABeEEE) for the Android pinned-WebSocket authority fix in [#343](https://github.com/saya-ch/dsh-mobile/pull/343). Original commits are retained alongside maintainer combined-certificate regressions. This native change shipped in the independently versioned Android 0.7.2 release, not an npm-only update.
30
36
 
31
- 感谢 [@LunarFeller](https://github.com/LunarFeller) 在 [#341](https://github.com/saya-ch/dsh-mobile/issues/341) 定位 FRP 接入模式的标题与 SSH 字段残留,并通过 [#342](https://github.com/saya-ch/dsh-mobile/pull/342) 提交修复;候选保留原作者提交,并补充浏览器回归。
37
+ 感谢 [@LunarFeller](https://github.com/LunarFeller) 在 [#341](https://github.com/saya-ch/dsh-mobile/issues/341) 定位 FRP 接入模式的标题与 SSH 字段残留,并通过 [#342](https://github.com/saya-ch/dsh-mobile/pull/342) 提交修复;发布保留原作者提交,并补充浏览器回归。
32
38
 
33
- Thanks [@LunarFeller](https://github.com/LunarFeller) for diagnosing the FRP attach-mode heading and SSH-field remnants in [#341](https://github.com/saya-ch/dsh-mobile/issues/341) and contributing the fix in [#342](https://github.com/saya-ch/dsh-mobile/pull/342). The candidate preserves the original commit and adds browser regression coverage.
39
+ Thanks [@LunarFeller](https://github.com/LunarFeller) for diagnosing the FRP attach-mode heading and SSH-field remnants in [#341](https://github.com/saya-ch/dsh-mobile/issues/341) and contributing the fix in [#342](https://github.com/saya-ch/dsh-mobile/pull/342). The release preserves the original commit and adds browser regression coverage.
34
40
 
35
41
  感谢 [@madlogos](https://github.com/madlogos) 在[后续反馈](https://github.com/saya-ch/dsh-mobile/issues/337#issuecomment-6593356950)中指出最小加载后的设置访问与全量恢复问题,推动旧选择重启恢复流程的验收和说明。
36
42
 
package/README.en.md CHANGED
@@ -28,7 +28,7 @@
28
28
  <a href="#compatibility">Compatibility</a> ·
29
29
  <a href="docs/README.md">Documentation</a> ·
30
30
  <a href="#contributors">Contributors</a> ·
31
- <a href="https://github.com/saya-ch/dsh-mobile/releases/tag/v0.8.5">Release notes</a> ·
31
+ <a href="https://github.com/saya-ch/dsh-mobile/releases/tag/v0.8.6">Release notes</a> ·
32
32
  <a href="README.md">简体中文</a>
33
33
  </p>
34
34
 
@@ -36,7 +36,7 @@
36
36
  >
37
37
  > **Using an iPhone or iPad?** Use Safari or [add DSH Mobile to your Home Screen as a web app (PWA)](<#iphone-and-ipad-home-screen-web-app-pwa>)—no native iOS download needed.
38
38
  >
39
- > **Current stable release: 0.8.5**. Preserve existing Windows firewall rules, fix Desktop update targeting and CGNAT LAN discovery, and add a bounded catalog cache plus a macOS permission notice. Plugin and Android App releases remain independent. [Plugin release notes and downloads](https://github.com/saya-ch/dsh-mobile/releases/tag/v0.8.5) · [Full changelog](CHANGELOG.md).
39
+ > **Current stable release: 0.8.6**. Forward paired third-party plugin request headers, keep the browser push subscription after a Disable, and expose bounded custom web-push text to trusted Host plugins; also fix Windows firewall verification, iroh directory diagnostics and the LAN diagnostic scope. Plugin and Android App releases remain independent; the background-reminder App update ships separately as Android 0.7.3. [Plugin release notes and downloads](https://github.com/saya-ch/dsh-mobile/releases/tag/v0.8.6) · [Full changelog](CHANGELOG.md).
40
40
  >
41
41
  > **Version policy**: since plugin **0.6.2**, the Android app and plugin use independent version numbers and releases. Their numbers do not need to match, and a plugin update does not imply an app update. [Compatibility notes](#compatibility).
42
42
 
@@ -445,6 +445,7 @@ On macOS, local network, cloudflared, self-hosted FRP and the own reverse proxy
445
445
 
446
446
  | DSH Mobile plugin | Verified DeepSeek Harness version |
447
447
  | --- | --- |
448
+ | `0.8.6` | On the `0.2.2-rc.1` packed target, forward paired third-party plugin `x-dsh-*` headers, keep the push subscription after a Disable, expose the trusted Host push-text service, and fix Windows firewall verification, iroh directory diagnostics and the LAN diagnostic scope. The Android App remains 0.7.2; background reminders ship with 0.7.3 separately |
448
449
  | `0.8.5` | Maintenance and performance update on the `0.2.2-rc.1` packed-DSH target: catalog cache, Desktop update targeting, CGNAT LAN discovery, firewall protection and a macOS permission notice. The Android App remains 0.7.2 |
449
450
  | `0.8.4` | Plugin maintenance update. The packed DSH test target remains `0.2.2-rc.1`, alongside existing compatibility checks. The upstream rc.1 npm CLI requires Node.js 24.9 or later |
450
451
  | `0.8.3` | Adapted to DSH 0.2.2-rc.1 with landscape, Plugins and Web Push settings layout fixes, shared FRP usernames and iroh pairing improvements |
package/README.md CHANGED
@@ -28,7 +28,7 @@
28
28
  <a href="#兼容性">兼容性</a> ·
29
29
  <a href="docs/README.md">文档</a> ·
30
30
  <a href="#贡献者">贡献者</a> ·
31
- <a href="https://github.com/saya-ch/dsh-mobile/releases/tag/v0.8.5">更新记录</a> ·
31
+ <a href="https://github.com/saya-ch/dsh-mobile/releases/tag/v0.8.6">更新记录</a> ·
32
32
  <a href="README.en.md">English</a>
33
33
  </p>
34
34
 
@@ -36,7 +36,7 @@
36
36
  >
37
37
  > **使用 iPhone 或 iPad?** 可以用 Safari 访问,或[添加到主屏幕作为 Web App(PWA)使用](<#iphone-与-ipad主屏幕-web-apppwa>),无需下载原生 iOS App。
38
38
  >
39
- > **当前正式版本:0.8.5**。保留 Windows 防火墙旧规则,修复 Desktop 插件更新目标与 CGNAT 局域网识别,并加入有界目录缓存和 macOS 权限提示。插件与 Android App 独立更新。[插件更新与下载](https://github.com/saya-ch/dsh-mobile/releases/tag/v0.8.5) · [完整更新记录](CHANGELOG.md)。
39
+ > **当前正式版本:0.8.6**。透传已配对第三方插件的请求头,修正网页推送停用后的订阅处理,并向受信 Host 插件开放推送文案接口;同时修复 Windows 防火墙校验、iroh 目录诊断与局域网诊断口径。插件与 Android App 独立更新,带后台提醒的 App 0.7.3 单独发布。[插件更新与下载](https://github.com/saya-ch/dsh-mobile/releases/tag/v0.8.6) · [完整更新记录](CHANGELOG.md)。
40
40
  >
41
41
  > **版本说明**:自插件 **0.6.2** 起,Android App 与插件独立编号、独立发布,版本号无需一致;插件升级不代表 App 需要同步升级。[兼容说明](#兼容性)。
42
42
 
@@ -447,6 +447,7 @@ macOS 上局域网、cloudflared、自建 FRP 与自有反向代理可用;Funn
447
447
 
448
448
  | DSH Mobile 插件 | 已验证的 DeepSeek Harness 版本与范围 |
449
449
  | --- | --- |
450
+ | `0.8.6` | 在 `0.2.2-rc.1` 打包目标上透传已配对第三方插件的 `x-dsh-*` 请求头、修正网页推送停用语义、向受信 Host 插件开放推送文案接口,并修复 Windows 防火墙校验、iroh 目录诊断与局域网诊断口径。Android App 仍为 0.7.2;带后台提醒的 0.7.3 单独发布 |
450
451
  | `0.8.5` | 插件维护与性能更新,继续使用 `0.2.2-rc.1` 的 DSH 打包验证目标;新增目录缓存、Desktop 更新目标修复、CGNAT 局域网识别、防火墙保护与 macOS 权限提示。Android App 仍为 0.7.2 |
451
452
  | `0.8.4` | 插件维护更新,继续使用 `0.2.2-rc.1` 的 DSH 打包验证目标,并保留既有版本检查。使用 rc.1 的 npm CLI 时,按上游要求使用 Node.js 24.9 或更新版本 |
452
453
  | `0.8.3` | 当时适配 DSH 0.2.2-rc.1,修复横屏、插件页和网页推送设置布局,支持共享 FRP 用户名,并改进 iroh 配对 |
@@ -184,6 +184,8 @@ The verified stable release is [Android 0.7.2](https://github.com/saya-ch/dsh-mo
184
184
 
185
185
  - Both public App 0.7.1 APKs passed checksum, manifest, established-signer, notice and alignment checks. The full APK additionally passed ABI and retained JVM TLS/JNI entry checks; compatibility contains no native libraries. There is no new physical-device or public-relay run for 0.7.1. Earlier arm64 development traffic remains historical evidence, not proof that a current connection uses the relay.
186
186
 
187
+ - A current-source Debug build was installed beside the official App on TB710FU (Android API 36) and paired to an isolated DSH 0.2.2-rc.1 gateway with a pinned local CA. The DSH page opened, system Back returned to the launcher, the task-notification permission flow opened, and enabling then disabling background reminders started and stopped a `dataSync` foreground service with its persistent notification. This was an isolated Debug package, not the official signer, and did not deliver a real task-completion event through a model run.
188
+
187
189
  - App 0.6.8's public APK passed identity, checksum and established-signer verification. A matching-signer Debug upgrade opened DSH and retained the saved device; controlled slow-link fault injection was interrupted before pairing and is not counted as passed.
188
190
 
189
191
  - App 0.6.7 Debug builds were checked on PJW110, Android 16 and WebView 153 with isolated DSH 0.2.1-rc.2 over USB-reversed pinned HTTPS. Actual automatic staged adoption retained a same-Session Unicode draft and kept target native zoom/renewal working without another page load. Confirmation cancellation, held-target Back cancellation and late responses, native renewal cancellation, keyboard Back and picker cancellation also passed; QA pairings and mappings were removed afterward. This was not a real Wi-Fi/cellular transition or an installation of the formally signed APK.
@@ -184,6 +184,8 @@ Set-Location apps/mobile/android
184
184
 
185
185
  - App 0.7.1 的两份公开 APK 均通过校验值、Manifest、既定正式签名、许可文本和对齐检查;完整版另检查两个 ABI 及保留的 JVM TLS/JNI 入口,兼容版确认没有原生库。本版未新增真机或公网中继回归。较早 arm64 开发构建的链路记录仍属历史证据,不能据此断言当前连接的数据已经过中继。
186
186
 
187
+ - 当前源码的 Debug 构建曾与正式 App 并存安装到 TB710FU(Android API 36),通过隔离的 DSH 0.2.2-rc.1 网关和固定本地 CA 配对;DSH 页面成功打开,系统返回键回到桌面,任务通知权限流程可打开,开启和关闭后台提醒分别启动、停止了带常驻通知的 `dataSync` 前台服务。这是隔离 Debug 包,不是正式签名 APK,也没有通过真实模型回合投递一条完成事件。
188
+
187
189
  - App 0.6.8 的公开 APK 已核验身份、校验值和既定正式签名。Debug 包同签名原位升级后可打开 DSH,并保留原设备记录;受控慢链路故障注入在配对前中断,不计为通过。
188
190
 
189
191
  - App 0.6.7 Debug 构建在 PJW110、Android 16、WebView 153 上,通过 USB 映射和固定 CA 的 HTTPS 连接隔离的 DSH 0.2.1-rc.2。真实自动接管保留同会话 Unicode 草稿,目标原生缩放/续期可用且不额外刷新;还通过确认取消、目标加载中按返回取消与迟到响应、原生续期取消、键盘返回和选择器取消。临时配对与映射已清理;这不是实际 Wi-Fi/蜂窝切网,也不是正式签名 APK 的装机验收。
@@ -40,7 +40,7 @@ Close, Escape and the App's Back action ask before discarding unsaved changes. A
40
40
 
41
41
  Computer pages and `?frontend=stock` pages are unaffected. DSH or community-plugin updates can change the catalog and dependencies. When a saved selection prevents the mobile page from starting, the page offers “Load all modules for this device and retry”; it changes this device’s selection only when you click, without changing computer defaults, other devices or pairing authorization. Settings APIs still reject invalid selections and enforce required-module and dependency checks.
42
42
 
43
- The plugin 0.8.2 candidate marks the installed General settings provider as required for boot, fixing old minimal selections that made the settings entry unavailable. If an old record excludes that provider, reopening the mobile page shows recovery without requiring settings access first. Only selecting Load all saves and retries; a failure retains the old selection for an explicit retry. Pairings need not be deleted. Version 0.8.2 is not released yet; see [CHANGELOG](../CHANGELOG.md) for release status.
43
+ Since plugin 0.8.2, the installed General settings provider has been required for boot, fixing old minimal selections that made the settings entry unavailable. If an old record excludes that provider, reopening the mobile page shows recovery without requiring settings access first. Only selecting Load all saves and retries; a failure retains the old selection for an explicit retry. Pairings need not be deleted.
44
44
 
45
45
  ## Measure sizes and minimize loading
46
46
 
@@ -40,7 +40,7 @@ DSH 或插件更新后,旧排除项可能已不在目录中,或变为启动
40
40
 
41
41
  电脑页面与 `?frontend=stock` 页面不受模块选择影响。DSH 或社区插件更新可能改变模块目录和依赖;已保存的选择阻止移动页面启动时,页面提供“为本设备加载全部模块并重试”,由你点击后恢复本设备的加载选择,不改变电脑默认、其他设备或配对授权。设置接口仍拒绝无效选择,不绕过启动必需或依赖检查。
42
42
 
43
- 插件 0.8.2 候选将已安装的“通用设置”入口组件列为启动必需项,修复旧最小加载选择导致设置入口不可用的问题。旧记录若排除了该组件,重新打开移动页面时会先显示恢复页,无需先进入设置。点击加载全部才会保存并重试;失败时保留原选择,可以明确重试,不需要删除配对。0.8.2 尚未发布,发布状态见 [CHANGELOG](../CHANGELOG.md)。
43
+ 自插件 0.8.2 起,已安装的“通用设置”入口组件被列为启动必需项,修复旧最小加载选择导致设置入口不可用的问题。旧记录若排除了该组件,重新打开移动页面时会先显示恢复页,无需先进入设置。点击加载全部才会保存并重试;失败时保留原选择,可以明确重试,不需要删除配对。
44
44
 
45
45
  ## 体积测量与最小加载
46
46
 
package/docs/README.md CHANGED
@@ -28,7 +28,7 @@
28
28
 
29
29
  | 功能 | 中文 | English | 使用状态 |
30
30
  | --- | --- | --- | --- |
31
- | 区分电脑、DNS、出口与手机的诊断证据 | [连接诊断](CONNECTION_DIAGNOSTICS.md) | [Connection diagnostics](CONNECTION_DIAGNOSTICS.en.md) | 分层报告为 0.8.2 候选;不代表手机路径已验收 |
31
+ | 区分电脑、DNS、出口与手机的诊断证据 | [连接诊断](CONNECTION_DIAGNOSTICS.md) | [Connection diagnostics](CONNECTION_DIAGNOSTICS.en.md) | 分层报告自 0.8.2 起提供;不代表手机路径已验收 |
32
32
  | 核对移动字号、字体与 App 页面缩放 | [显示大小](MOBILE_COMPOSER_TOOLBAR.md#显示大小与可用空间) | [Display size](MOBILE_COMPOSER_TOOLBAR.en.md#display-size-and-available-space) | 字体按主题能力显示;App 缩放与网页字号独立 |
33
33
  | 为移动页面选择加载模块 | [模块设置](CLIENT_MODULES.md) | [Module settings](CLIENT_MODULES.en.md) | 电脑默认与设备覆盖分别保存,不自动刷新会话 |
34
34
  | 允许受控路由网络访问 LAN Gateway | [额外可信网段](TRUSTED_NETWORKS.md) | [Trusted networks](TRUSTED_NETWORKS.en.md) | 不建立 VPN,不自动扩大防火墙规则 |
@@ -40,7 +40,7 @@ Safari and the installed app may have separate login state. This recovery is for
40
40
 
41
41
  Push is best effort, not an alarm service. Host sleep/shutdown, disabled origin, phone connectivity, Focus settings and platform scheduling can delay or prevent delivery.
42
42
 
43
- Closing the page does **not** disable saved push consent. Use **Disable** to stop future eligible sends; explicit logout and device revocation have the separate effects listed [below](<#consent-logout-and-revocation>). A notification tap may require login and never answers or approves anything; **Send test** opens the app root, not a conversation.
43
+ Closing the page does **not** disable saved push consent. **Disable** stops future eligible sends but retains the browser subscription, avoiding an unusable replacement after a quick iOS unsubscribe/subscribe cycle; the next explicit Enable reuses the existing subscription. Explicit logout and device revocation have the separate effects listed [below](<#consent-logout-and-revocation>). A notification tap may require login and never answers or approves anything; **Send test** opens the app root, not a conversation.
44
44
 
45
45
  ## What can notify
46
46
 
@@ -64,7 +64,7 @@ Lock-screen text is generic and localized in English, Simplified Chinese or Ital
64
64
  | Action/event | Effect |
65
65
  | --- | --- |
66
66
  | Close the page or let its short login session expire | Saved push consent remains while persistent pairing authorization and the origin remain valid. Opening DSH still requires authentication. |
67
- | Select Disable | The host durably stops sending first, then the page best-effort unsubscribes only a subscription it owns. A failed browser unsubscribe does not restore host consent. |
67
+ | Select Disable | Durably stop Host sends and revoke the registration while retaining the browser subscription in a non-delivering state for the next explicit Enable. Clear the subscription through browser site-data controls when required. |
68
68
  | Explicit Mobile logout | Disable this paired device's push on the current origin before clearing its short session; retain the pairing. A failed disable must not report a completed logout. |
69
69
  | Revoke/reset a device or let its persistent credential expire | Purge affected push registrations and queued work, including devices with no open page. |
70
70
  | Stop a Gateway/provider or change its origin | Stop that origin's sends. Other origins keep their own consent. Re-enabling on a new origin is explicit. |
@@ -80,6 +80,21 @@ After the public Workspace and Session lists are ready, Mobile rechecks that the
80
80
 
81
81
  The browser chooses warm versus cold navigation; do not assume unsent drafts or in-flight sends survive every notification navigation. A fresh DSH launch may initially show the most recently active conversation; select the intended session manually when needed. The plugin does not register or unregister a service worker or cache authenticated pages/API data.
82
82
 
83
+ ## Host-plugin custom text
84
+
85
+ A trusted computer-side plugin may depend on the `mobilePush` service and submit a Session, title and body for categories the user already enabled:
86
+
87
+ ```ts
88
+ await ctx.mobilePush.notify({
89
+ sessionId,
90
+ title: 'Conversation name',
91
+ body: 'The response is ready',
92
+ category: 'response',
93
+ })
94
+ ```
95
+
96
+ Titles are limited to 80 characters and bodies to 200; control characters become spaces. The result reports how many recipient deliveries were newly queued. The Host still checks Session existence, pairing authorization, each device's category preference, registration limits, expiry and concurrency. A tap keeps the Gateway-generated opaque reference instead of putting the raw Session ID in the URL. Custom text remains in the private push state until delivery or at most 30 minutes of expiry; do not submit passwords, Tokens or content that must never leave the computer.
97
+
83
98
  ## Limits and delivery failures
84
99
 
85
100
  - At most **4 registrations per paired device**, **256 registrations and 256 pending deliveries per authorization scope**, and **4 concurrent sends**. Overflow rejects new work rather than deleting another installation or blocking DSH.
@@ -97,6 +112,7 @@ The sender accepts only validated Apple HTTPS push endpoints, rejects redirects
97
112
  | No row / unavailable | Check required page-level capability, trusted HTTPS, authenticated dedicated browser context and DSH public services. There is no worker or old-iOS fallback. |
98
113
  | Install guidance | Launch the Home Screen icon rather than an ordinary browser tab. |
99
114
  | Permission denied | Change the site's notification permission in system/browser settings, return to the app and make a new explicit Enable action. No prompt is forced. |
115
+ | It says granted but the Home Screen app stays silent | iOS records notification permission at site level; a re-added Home Screen app can inherit `granted` without receiving an independent authorization. The settings row shows this site-level state. If Disable/Enable still produces no delivery, remove the Home Screen app, clear the site's data, then add and authorize it again; do not guess or silently recreate the current app's subscription. |
100
116
  | Registration failed / stale settings | Refresh/reopen to read current state, then explicitly retry. Do not repeatedly resubmit an old request. |
101
117
  | Existing subscription uses another application key | Do not automatically unsubscribe it. After a host key reset, retrying Enable alone will not fix this conflict; identify its owner and arrange deliberate browser-side repair. |
102
118
  | Suspended / repair-required | Check host key/configuration and private-file access. Do not delete paired-device state or silently replace VAPID keys. |
@@ -107,12 +123,12 @@ Lost/corrupt VAPID material with existing registrations fails closed. A host adm
107
123
 
108
124
  ## Independent channels and acceptance
109
125
 
110
- Android's native permission/SSE path and APK/signing remain unchanged. The independently maintained **dsh-ping** channel is not modified or automatically disabled; enabling both can produce separate alerts. Web Push adds no cross-channel deduplication or foreground suppression.
126
+ Android's native permission/SSE path stays independent of iOS Web Push. A newer Android App can explicitly enable background reminders below Task notifications: a foreground service keeps the paired connection available after Android suspends the page. This requires an additional persistent notification and remains subject to foreground-service time limits, battery policy and connectivity; iroh does not participate in this background service yet. The independently maintained **dsh-ping** channel is not modified or automatically disabled; enabling both can produce separate alerts. Web Push adds no cross-channel deduplication or foreground suppression.
111
127
 
112
128
  For everyday setup, send a test notification first, then check the categories you need. Keep the existing Home Screen installation and pairing data; reopen the page after updating the computer plugin to load its new interface. See [contribution checks](<../CONTRIBUTING.md#select-behavior-checks>) for packed runtime validation and maintenance commands.
113
129
 
114
130
  ## Verification status
115
131
 
116
- This release accepts the contributor's development testing on **iPhone 17 Pro Max / iOS 27.0.1**, covering six notification categories, background/locked/closed-page delivery, taps, disable/revocation, offline windows and host restarts. The maintainer has not independently repeated Apple-device tests; no corresponding physical iPad or other device/OS record is available.
132
+ Historical Web Push acceptance uses the contributor's development testing on **iPhone 17 Pro Max / iOS 27.0.1**, covering six notification categories, background/locked/closed-page delivery, taps, disable/revocation, offline windows and host restarts. The maintainer has not independently repeated Apple-device tests; no corresponding physical iPad or other device/OS record is available. Reusing a subscription after Disable and custom Host text still require physical iOS verification; Android background reminders have passed Debug-package foreground-service startup, persistent notification and enable/disable checks, but a real task-completion event and the official-signer APK remain unverified.
117
133
 
118
134
  Automation covers settings, authentication, encrypted payloads, tap ownership, restart and cleanup, not actual Apple delivery. The missing-target fixture uses a never-created session; a genuinely persisted-then-deleted session remains untested because the current public DSH API does not expose the needed delete operation. Push is best effort and does not replace manual confirmation of important tasks.
package/docs/WEB_PUSH.md CHANGED
@@ -40,7 +40,7 @@ Safari 与已安装 App 可能使用不同登录状态。本恢复步骤仅针
40
40
 
41
41
  推送是尽力而为,不是警报服务。主机睡眠/关机、Origin 停用、手机网络、专注模式和系统调度均可能延迟或阻止投递。
42
42
 
43
- 关闭页面**不会**停用已保存的推送同意。用**停用**阻止后续合格发送;明确退出与撤销设备各有不同效果,见[下表](<#同意状态退出与撤销>)。点击通知可能需要重新登录,绝不会代为回答或批准;**发送测试**的通知打开应用根入口,而不是某个会话。
43
+ 关闭页面**不会**停用已保存的推送同意。用**停用**阻止后续合格发送,但会保留浏览器订阅,避免 iOS 在快速退订/重订后得到无法投递的新订阅;下一次明确启用会复用原订阅。明确退出与撤销设备各有不同效果,见[下表](<#同意状态退出与撤销>)。点击通知可能需要重新登录,绝不会代为回答或批准;**发送测试**的通知打开应用根入口,而不是某个会话。
44
44
 
45
45
  ## 会提醒什么
46
46
 
@@ -64,7 +64,7 @@ Safari 与已安装 App 可能使用不同登录状态。本恢复步骤仅针
64
64
  | 操作/事件 | 效果 |
65
65
  | --- | --- |
66
66
  | 关闭页面或短登录会话过期 | 持久配对授权与 Origin 仍有效时保留推送同意;打开 DSH 仍需认证。 |
67
- | 点击停用 | 先持久保存主机停发,再尽力取消本功能拥有的浏览器订阅;浏览器取消失败不会恢复主机同意。 |
67
+ | 点击停用 | 持久保存主机停发并撤销注册;浏览器订阅保留为不投递状态,供下一次明确启用复用。需要清除订阅时,使用浏览器站点数据管理。 |
68
68
  | 明确退出 Mobile | 清除短会话前,先关闭此配对设备在当前 Origin 的推送,保留配对。停用保存失败不能报告退出已完成。 |
69
69
  | 撤销/重置设备,或持久凭据过期 | 清理相关注册和队列,包含没有打开页面的休眠设备。 |
70
70
  | 停止 Gateway/服务商或更改 Origin | 停止该 Origin 发送;其他 Origin 保留各自同意,新地址须重新明确启用。 |
@@ -80,6 +80,21 @@ Safari 与已安装 App 可能使用不同登录状态。本恢复步骤仅针
80
80
 
81
81
  冷启动还是复用页面由浏览器决定;不要假定所有通知导航都会保留未发送草稿或进行中的发送。DSH 全新启动时可能先显示最近活跃会话,此时可从会话列表手动选择目标。插件不注册/注销 Service Worker,也不缓存已认证页面或 API 内容。
82
82
 
83
+ ## Host 插件自定义文案
84
+
85
+ 受信任的电脑端插件可依赖 `mobilePush` 服务,为已启用的网页推送类别提交会话、标题和正文:
86
+
87
+ ```ts
88
+ await ctx.mobilePush.notify({
89
+ sessionId,
90
+ title: '会话名称',
91
+ body: '回复已准备好',
92
+ category: 'response',
93
+ })
94
+ ```
95
+
96
+ 标题最多 80 个字符,正文最多 200 个字符;控制字符会被替换为空格。接口返回本次新排队的接收端数量;服务端仍按会话存在性、配对授权、设备类别开关、注册数量、时效和并发上限检查。通知点击继续使用网关生成的不透明引用,不把原始会话 ID 写入点击 URL。自定义文案会在私有推送状态中保留到发送完成或最长 30 分钟过期;不要放入密码、Token 或不应离开电脑的敏感内容。
97
+
83
98
  ## 数量、时效与失败处理
84
99
 
85
100
  - 每个配对设备最多 **4 个注册**;每个授权范围最多 **256 个注册和 256 个待投递项**;最多 **4 个并发发送**。满额时拒绝新通知工作,不删除其他安装,也不阻塞 DSH。
@@ -97,6 +112,7 @@ Safari 与已安装 App 可能使用不同登录状态。本恢复步骤仅针
97
112
  | 没有入口/不可用 | 检查页面级能力、受信任 HTTPS、已认证的专用浏览器页面及 DSH 公共服务;没有 Worker 或旧 iOS 降级。 |
98
113
  | 提示安装 | 从主屏幕图标启动,不在普通标签页开启。 |
99
114
  | 权限拒绝 | 在系统/浏览器设置中修改该网站的通知权限,再返回 App 作新的明确启用;不会强制再次弹窗。 |
115
+ | 显示“已允许”但主屏幕 App 不响 | iOS 的通知权限按站点记录;重新添加的主屏幕 App 可能继承 `granted` 而没有独立授权。设置页会显示该站点级状态。若停用/重启用后仍无投递,移除主屏幕 App 并清除该站点数据,再重新添加和授权;不要对当前 App 猜测或静默重建订阅。 |
100
116
  | 保存失败/设置已过期 | 刷新或重开读取当前状态,再明确重试,不反复提交旧请求。 |
101
117
  | 现有订阅属于其他应用密钥 | 不自动取消它;主机密钥重置后仅重试启用无法解决,先确认归属,再安排明确的浏览器端修复。 |
102
118
  | 已暂停/需要修复 | 检查主机密钥、配置和私有文件访问;不要删除配对状态或静默更换 VAPID 密钥。 |
@@ -107,12 +123,12 @@ Safari 与已安装 App 可能使用不同登录状态。本恢复步骤仅针
107
123
 
108
124
  ## 独立通道与验收
109
125
 
110
- Android 原生权限/SSE 路径、APK 和签名保持不变。独立维护的 **dsh-ping** 不被修改或自动停用;同时开启可能收到不同通道的重复提醒。网页推送不增加跨通道去重,也不因页面在前台而压制通知。
126
+ Android 原生权限/SSE 路径与 iOS 网页推送相互独立。新版 Android App 可在“任务通知”下显式开启后台提醒:它会以前台服务保持配对连接,页面被系统挂起后仍接收任务完成事件。该选项需要额外常驻通知,受 Android 前台服务时限、电池策略和网络影响;iroh 暂不参与该后台服务。独立维护的 **dsh-ping** 不被修改或自动停用;同时开启可能收到不同通道的重复提醒。网页推送不增加跨通道去重,也不因页面在前台而压制通知。
111
127
 
112
128
  日常使用建议先发送一条测试通知,再检查自己关注的类别。保留现有主屏幕安装和配对数据;更新电脑端插件后重新打开页面即可加载新版界面。打包运行时检查与维护命令见[贡献指南](<../CONTRIBUTING.md#select-behavior-checks>)。
113
129
 
114
130
  ## 验证状态
115
131
 
116
- 本版采信贡献者在 **iPhone 17 Pro Max / iOS 27.0.1** 上的开发实测,覆盖六类提醒、后台/锁屏/关闭页面、通知点击、停用与撤销,以及离线和主机重启。维护者没有独立 Apple 真机复验;iPad 与其他设备/系统组合没有对应真机记录。
132
+ 网页推送的历史验收采信贡献者在 **iPhone 17 Pro Max / iOS 27.0.1** 上的开发实测,覆盖六类提醒、后台/锁屏/关闭页面、通知点击、停用与撤销,以及离线和主机重启。维护者没有独立 Apple 真机复验;iPad 与其他设备/系统组合没有对应真机记录。停用后复用订阅和自定义 Host 文案仍需真实 iOS 复验;Android 后台提醒已在隔离 Debug 包上验证前台服务启动、常驻通知和开关停止,真实任务完成事件与正式签名 APK 仍未验收。
117
133
 
118
134
  自动化检查覆盖设置、鉴权、加密载荷、点击归属、重启与清理,不代表 Apple 实际送达。缺失目标的自动化检查使用未创建的会话;真正“先持久化再删除”的会话受当前 DSH 公共接口限制,尚无同类检查。推送为尽力投递,不能替代重要任务的人工确认。
package/lib/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { chmod, lstat, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
- import { homedir, networkInterfaces } from "node:os";
2
+ import { chmod, lstat, mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
3
+ import { homedir, networkInterfaces, tmpdir } from "node:os";
4
4
  import { basename, dirname, isAbsolute, join, resolve, win32 } from "node:path";
5
5
  import { X509Certificate, createPrivateKey, createPublicKey, randomBytes } from "node:crypto";
6
6
  import { execFile } from "node:child_process";
@@ -9,6 +9,39 @@ import { performance } from "node:perf_hooks";
9
9
  import "@deepseek-ai/cordis";
10
10
  import "@deepseek-ai/schemastery";
11
11
  // src/exec-file.ts
12
+ /**
13
+ * Longest stream text attached to a failed subprocess result.
14
+ *
15
+ * The text exists for diagnostics only: a caller decides which part is safe to
16
+ * show. It is bounded here so a chatty child cannot turn one failure into an
17
+ * unbounded buffer in every future consumer of that rejection.
18
+ */
19
+ const MAX_SUBPROCESS_ERROR_STREAM_CHARS = 8192;
20
+ function boundedStream(value) {
21
+ if (typeof value !== "string" || value === "") return void 0;
22
+ return value.length <= 8192 ? value : value.slice(0, MAX_SUBPROCESS_ERROR_STREAM_CHARS);
23
+ }
24
+ /**
25
+ * Attach the captured streams to a subprocess failure, best effort.
26
+ *
27
+ * `execFile` reports a non-zero exit as an Error whose only text is the command
28
+ * line with stderr merged into `message`; the raw stream strings are handed to
29
+ * the callback and otherwise discarded, so a caller cannot read them from the
30
+ * rejection. They are copied onto it when it accepts properties. A frozen,
31
+ * exotic or primitive rejection value must keep `message` as its diagnostic
32
+ * rather than turn one failure into a different error, so every write is
33
+ * guarded and an existing property is never overwritten.
34
+ */
35
+ function attachCapturedStreams(error, stdout, stderr) {
36
+ if (typeof error !== "object" || error === null) return;
37
+ const target = error;
38
+ try {
39
+ const capturedStderr = boundedStream(stderr);
40
+ const capturedStdout = boundedStream(stdout);
41
+ if (target.stderr === void 0 && capturedStderr !== void 0) target.stderr = capturedStderr;
42
+ if (target.stdout === void 0 && capturedStdout !== void 0) target.stdout = capturedStdout;
43
+ } catch {}
44
+ }
12
45
  /** Capture both output streams even when a desktop host wraps execFile without Node's promisify metadata. */
13
46
  function execFileText(file, args, options = {}) {
14
47
  return new Promise((resolve, reject) => {
@@ -18,6 +51,7 @@ function execFileText(file, args, options = {}) {
18
51
  encoding: "utf8"
19
52
  }, (error, stdout, stderr) => {
20
53
  if (error !== null) {
54
+ attachCapturedStreams(error, stdout, stderr);
21
55
  reject(error);
22
56
  return;
23
57
  }
@@ -517,16 +551,71 @@ const MAX_MANAGED_PORT = 65535;
517
551
  function assertManagedPort(port, name) {
518
552
  if (!Number.isSafeInteger(port) || port < MIN_MANAGED_PORT || port > MAX_MANAGED_PORT) throw new Error(`${name} must be from ${String(MIN_MANAGED_PORT)} through ${String(MAX_MANAGED_PORT)}`);
519
553
  }
520
- function powerShellFailureDetail(error) {
554
+ /** Longest failure detail carried in the thrown message. */
555
+ const MAX_FAILURE_DETAIL_CHARS = 512;
556
+ /**
557
+ * Strip the elevated invocation and filesystem locations from PowerShell text.
558
+ *
559
+ * The launcher's stderr quotes the command it ran, including the base64
560
+ * `-EncodedCommand` payload, and the elevated script quotes its own scratch
561
+ * path. Neither helps an operator, and both can push the one useful line past
562
+ * the length bound, so they are replaced before the detail is bounded.
563
+ */
564
+ function redactPowerShellDetail(value) {
565
+ return value.replace(/EncodedCommand\s+\S+/giu, "EncodedCommand <redacted>").replace(/\b[A-Za-z0-9+/]{64,}={0,2}\b/gu, "<redacted>").replace(/(?<![A-Za-z])[A-Za-z]:[\\/][^\s'"]*/gu, "<path>");
566
+ }
567
+ /** Bounded, redacted detail for a failed elevated Windows operation. */
568
+ function powerShellFailureDetail(error, innerError = "") {
521
569
  if (!(error instanceof Error)) return "unknown failure";
522
- const code = "code" in error && typeof error.code === "string" ? ` (${error.code})` : "";
523
- const stderr = "stderr" in error && typeof error.stderr === "string" ? error.stderr.trim().replace(/\s+/gu, " ").slice(0, 512) : "";
524
- return `${code}${stderr === "" ? "" : `: ${stderr}`}`;
570
+ const code = "code" in error && (typeof error.code === "string" || typeof error.code === "number") ? ` (${String(error.code)})` : "";
571
+ const detail = redactPowerShellDetail(innerError.trim() === "" && "stderr" in error && typeof error.stderr === "string" ? error.stderr : innerError).trim().replace(/\s+/gu, " ").slice(0, MAX_FAILURE_DETAIL_CHARS);
572
+ return `${code}${detail === "" ? "" : `: ${detail}`}`;
573
+ }
574
+ /** Default sink for a scratch directory that could not be removed. */
575
+ function warnFirewallCleanupFailure(detail) {
576
+ process.emitWarning(`Windows firewall temporary cleanup failed (${detail})`, { code: "DSH_MOBILE_FIREWALL_CLEANUP_FAILED" });
577
+ }
578
+ /**
579
+ * Remove the elevated helper's scratch directory without touching the run's outcome.
580
+ *
581
+ * The directory is created before elevation and removed in a `finally`, so a
582
+ * cleanup failure must not replace the firewall error that explains the
583
+ * failure, and must not fail a run that already succeeded. It stays observable
584
+ * instead: the removal error's code is reported to the warning sink, whose own
585
+ * failure is ignored for the same reason.
586
+ * @param directory - scratch path created for this run.
587
+ * @param options - injectable removal and warning sink for tests.
588
+ */
589
+ async function discardFirewallScratch(directory, options = {}) {
590
+ const remove = options.remove ?? ((path) => rm(path, {
591
+ recursive: true,
592
+ force: true
593
+ }));
594
+ try {
595
+ await remove(directory);
596
+ } catch (error) {
597
+ const code = error !== null && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : "unknown";
598
+ const warn = options.warn ?? warnFirewallCleanupFailure;
599
+ try {
600
+ warn(code);
601
+ } catch {}
602
+ }
525
603
  }
526
604
  async function runElevatedPowerShell(script) {
605
+ const temporary = await mkdtemp(join(tmpdir(), "dsh-mobile-firewall-"));
606
+ const errorFile = join(temporary, "error.log");
607
+ const wrapped = [
608
+ `$__dshMobileErrorFile = '${errorFile.replaceAll("'", "''")}'`,
609
+ "try {",
610
+ script,
611
+ "} catch {",
612
+ " try { [IO.File]::WriteAllText($__dshMobileErrorFile, ($_ | Out-String)) } catch {}",
613
+ " throw",
614
+ "}"
615
+ ].join("\n");
527
616
  const launch = [
528
617
  "$ErrorActionPreference = 'Stop'; $process = Start-Process -FilePath 'powershell.exe' -Verb RunAs -WindowStyle Hidden -Wait -PassThru",
529
- ` -ArgumentList @('-NoProfile','-NonInteractive','-EncodedCommand','${Buffer.from(script, "utf16le").toString("base64")}')`,
618
+ ` -ArgumentList @('-NoProfile','-NonInteractive','-EncodedCommand','${Buffer.from(wrapped, "utf16le").toString("base64")}')`,
530
619
  "; exit $process.ExitCode"
531
620
  ].join(" ");
532
621
  try {
@@ -537,7 +626,13 @@ async function runElevatedPowerShell(script) {
537
626
  launch
538
627
  ], { windowsHide: true });
539
628
  } catch (error) {
540
- throw new Error(`Windows firewall operation failed${powerShellFailureDetail(error)}`, { cause: error });
629
+ let innerError = "";
630
+ try {
631
+ innerError = (await readFile(errorFile, "utf8")).replace(/^\uFEFF/u, "");
632
+ } catch {}
633
+ throw new Error(`Windows firewall operation failed${powerShellFailureDetail(error, innerError)}`, { cause: error });
634
+ } finally {
635
+ await discardFirewallScratch(temporary);
541
636
  }
542
637
  }
543
638
  function firewallRuleScript(name, protocol, port) {
@@ -552,7 +647,7 @@ function firewallRuleScript(name, protocol, port) {
552
647
  " $rule | Get-NetFirewallAddressFilter | Set-NetFirewallAddressFilter -RemoteAddress LocalSubnet",
553
648
  " }",
554
649
  "}",
555
- `$verified = @(Get-NetFirewallRule -DisplayName '${name}' -Enabled True -Direction Inbound -Action Allow -Profile Any -ErrorAction Stop)`,
650
+ `$verified = @(Get-NetFirewallRule -DisplayName '${name}' -ErrorAction Stop | Where-Object { $_.Enabled -eq 'True' -and $_.Direction -eq 'Inbound' -and $_.Action -eq 'Allow' -and $_.Profile -eq 'Any' })`,
556
651
  `$matchingPort = @($verified | Get-NetFirewallPortFilter | Where-Object { $_.Protocol -eq '${protocol}' -and $_.LocalPort -contains '${String(port)}' })`,
557
652
  `$matchingAddress = @($verified | Get-NetFirewallAddressFilter | Where-Object { $_.RemoteAddress -contains 'LocalSubnet' })`,
558
653
  `if ($verified.Count -eq 0 -or $matchingPort.Count -eq 0 -or $matchingAddress.Count -eq 0) { throw "DSH Mobile firewall rule '${name}' is not active with the expected scope" }`