@linxin666/dsh-remote-web-ui 0.2.6 → 0.2.8

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.
Files changed (86) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +8 -1
  3. package/README.zh.md +2 -1
  4. package/lib/client.js +55 -10
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +12 -8
  7. package/lib/mobile.js +37 -18
  8. package/lib/mobile.js.map +1 -1
  9. package/lib/types/client/FenceNotice.d.ts +3 -2
  10. package/lib/types/client/FenceNotice.d.ts.map +1 -1
  11. package/lib/types/client/FooterRemoteEntry.js +1 -1
  12. package/lib/types/client/PluginSettingsCard.d.ts +1 -1
  13. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  14. package/lib/types/client/RemoteEntry.js +8 -8
  15. package/lib/types/client/RemotePanel.js +3 -3
  16. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  17. package/lib/types/client/RemoteSettingsCard.js +2 -2
  18. package/lib/types/client/UpdateEntry.d.ts.map +1 -1
  19. package/lib/types/client/UpdateEntry.js +2 -2
  20. package/lib/types/client/UpdatePanel.js +1 -1
  21. package/lib/types/client/deep-link.js +1 -1
  22. package/lib/types/client/index.d.ts.map +1 -1
  23. package/lib/types/client/index.js +45 -23
  24. package/lib/types/client/pair-api.d.ts +6 -0
  25. package/lib/types/client/pair-api.d.ts.map +1 -1
  26. package/lib/types/client/pair-api.js +11 -0
  27. package/lib/types/client/remote-channel.d.ts +25 -0
  28. package/lib/types/client/remote-channel.d.ts.map +1 -1
  29. package/lib/types/client/remote-channel.js +24 -0
  30. package/lib/types/client/settings-form.d.ts.map +1 -1
  31. package/lib/types/client/update-api.d.ts.map +1 -1
  32. package/lib/types/gate.js +1 -1
  33. package/lib/types/index.js +15 -15
  34. package/lib/types/invariant.d.ts.map +1 -1
  35. package/lib/types/loopback-proxy.js +1 -1
  36. package/lib/types/mobile/PairRequiredView.js +1 -1
  37. package/lib/types/mobile/PairRequiredView.test.js +1 -1
  38. package/lib/types/mobile/api.js +1 -1
  39. package/lib/types/mobile/index.js +5 -5
  40. package/lib/types/mobile/markdown.test.js +1 -1
  41. package/lib/types/mobile/messages.d.ts.map +1 -1
  42. package/lib/types/mobile/messages.js +5 -3
  43. package/lib/types/mobile/messages.test.js +21 -1
  44. package/lib/types/mobile/mobile-theme.test.js +2 -2
  45. package/lib/types/mobile/mux.d.ts +7 -4
  46. package/lib/types/mobile/mux.d.ts.map +1 -1
  47. package/lib/types/mobile/mux.js +44 -16
  48. package/lib/types/mobile/mux.test.js +58 -1
  49. package/lib/types/mobile/rpc.d.ts.map +1 -1
  50. package/lib/types/mobile/theme-toggle.js +1 -1
  51. package/lib/types/mobile/views/App.js +7 -7
  52. package/lib/types/mobile/views/App.test.js +2 -2
  53. package/lib/types/mobile/views/ChatView.js +6 -6
  54. package/lib/types/mobile/views/ChatView.test.js +3 -3
  55. package/lib/types/mobile/views/SessionListView.js +3 -3
  56. package/lib/types/mobile/views/SessionListView.test.js +2 -2
  57. package/lib/types/mobile/views/WorkspaceView.js +3 -3
  58. package/lib/types/mobile/views/WorkspaceView.test.js +3 -3
  59. package/lib/types/mobile-api.d.ts +1 -1
  60. package/lib/types/mobile-api.d.ts.map +1 -1
  61. package/lib/types/mobile-api.js +2 -2
  62. package/lib/types/pairing-access.d.ts.map +1 -1
  63. package/lib/types/pairing.d.ts.map +1 -1
  64. package/lib/types/pairing.persist.test.js +2 -2
  65. package/lib/types/remote-api.js +6 -6
  66. package/lib/types/remote-methods.d.ts.map +1 -1
  67. package/lib/types/routes.d.ts +9 -7
  68. package/lib/types/routes.d.ts.map +1 -1
  69. package/lib/types/routes.js +8 -5
  70. package/lib/types/tunnel.d.ts.map +1 -1
  71. package/lib/types/update-routes.d.ts +2 -2
  72. package/lib/types/update-routes.d.ts.map +1 -1
  73. package/lib/types/update-routes.js +1 -1
  74. package/package.json +15 -12
  75. package/src/client/FenceNotice.tsx +3 -2
  76. package/src/client/RemoteEntry.tsx +3 -2
  77. package/src/client/UpdateEntry.tsx +1 -0
  78. package/src/client/index.ts +40 -15
  79. package/src/client/pair-api.ts +16 -0
  80. package/src/client/remote-channel.ts +42 -0
  81. package/src/index.ts +1 -1
  82. package/src/mobile/messages.test.ts +23 -0
  83. package/src/mobile/messages.ts +5 -3
  84. package/src/mobile/mux.test.ts +64 -0
  85. package/src/mobile/mux.ts +44 -17
  86. package/src/routes.ts +7 -2
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # node scripts/verify-docs.mjs --write <dir>
5
- README.md: 290224ef41cca3f8d8820fc37b985edbfe2d6549
6
- README.zh.md: 10b01e05a61885c53b48b2e9542a269119674f8a
5
+ README.md: dc31fab1b49b32fe325ead616d52846435fa1180
6
+ README.zh.md: e5ccaf0f52d01e9b8f92e265947c22dff68e2a97
package/README.md CHANGED
@@ -65,7 +65,10 @@ that probes and runs the update.
65
65
  `/api/dsh-desktop-launcher/*` and `/api/dsh-web-ui-settings/*` control
66
66
  endpoints stay loopback-only. Unpaired desktop browsers get a persistent
67
67
  full-page block instead of data (the page keys off the `unpaired` error
68
- code, not every 403).
68
+ code, not every 403). The block retires once a gated call succeeds or the
69
+ channel itself is torn down — turning `requirePairingForLan` off (or the
70
+ plugin off) also clears a notice raised while the channel was briefly
71
+ active.
69
72
  - **Posture probe**: the plugin probes the SDK `/api` fence with forged
70
73
  Host headers (the public base and every LAN base). A 403 is the default
71
74
  stance (fence closed; remote access goes through pairing). Anything other
@@ -473,6 +476,10 @@ opens at `http://127.0.0.1`.
473
476
  unpair one at a time. Credential-bearing device ids and raw User-Agent
474
477
  values are not rendered. `/api/pair/status` never returns the device id
475
478
  list, even to a paired phone.
479
+ - **The desktop gate policy is public**: `/api/pair/status` exposes only the
480
+ boolean `requirePairingForLan` policy so a remote desktop can choose the
481
+ correct transport before its settings scope is available. This field is
482
+ not a credential and does not expose tokens, devices, counts, or tunnel URLs.
476
483
  - **Quick-tunnel hostnames change per run**: a `trycloudflare.com` URL is
477
484
  random on every `cloudflared` start, so `publicBaseUrl` must be updated
478
485
  whenever the tunnel restarts. A named tunnel (fixed hostname) avoids the
package/README.zh.md CHANGED
@@ -13,7 +13,7 @@
13
13
  - **手机侧**:扫码将手机与一次性、限时令牌绑定,并落地到 **`/m/` 独立移动端界面**——一款专为小屏设计的轻客户端(见[截图](#截图)),而不是把桌面 UI 塞进手机。该页面可安装为 PWA;每个已安装应用使用自己的已配对设备 cookie,存储隔离的移动端 Web App 需要在该上下文打开新的二维码链接,或粘贴桌面端新复制的链接完成配对。链接携带 `workspace` 参数,手机落地到桌面正在查看的同一工作区。
14
14
  - **PC 侧(远程桌面配对)**:同一份二维码链接和配对令牌也能配对 PC 浏览器——把手机侧配对流扩展到桌面 Web GUI。从面板复制链接,在另一台电脑的浏览器打开(局域网 URL 或隧道 URL 均可);接受往返后,完整 Web GUI 在那台设备上运行——工作区、会话、聊天、切模型——受围栏保护的同源流量走门控的 `/remote` 通道,而不是直接调用仅 loopback 的 host 路由。手机进 `/m/`,PC 进桌面 UI:一枚令牌、一套配对流、两种界面。未配对的 PC 只看到不渲染底层数据的完整阻断页,其中提供电脑配对步骤与「重新检测」操作。
15
15
  - **安全**:一个有效的一次性令牌(刷新会使旧链接失效;已接受的令牌不可复用;令牌会过期)。停止会撤销每一台已配对设备与当前令牌——已配对设备在下一次请求时被切断。配对是本插件对自己远程通道的访问控制:手机走 `/m/api`,非 loopback origin 打开的桌面浏览器走 `/remote`。未配对调用方在读取请求体之前就被 `/remote` 拒绝。loopback(127.0.0.1)继续直接使用 `/api`。默认的远程桌面路径不使用 `--trusted-host`:连接插件的 `/api` 围栏对公网和局域网主机保持关闭,已配对 PC 改走 `/remote`。`--trusted-host` 是 SDK 的另一种用法,会让该主机被信任访问 `/api` 本身——配对不门控 `/api`(任何插件都无法做到;围栏是 SDK 自己的接缝)。当 `/api` 无需配对即可到达时,下方姿态探测会报告该姿态。
16
- - **远程桌面通道**:`requirePairingForLan` 开启(默认)时,经局域网 URL 或隧道打开的桌面 Web GUI 透明地改走 `/remote`——同一套 UI,同一枚配对 cookie 门控。浏览器中 `/api`、`/sidebar`、`/git`、`/pet` 下的请求,以及已知的事件、终端与 SSH WebSocket,会在不转发远程 Origin 或配对 cookie 的前提下重新发给本机 Web Server;认证反代会为兄弟路由围栏提供自己生成的同源浏览器标记。SDK 的 loopback 专属特权方法(原生对话框、settings 与 credentials 面)对已配对远程桌面保持不可达,`/api/pair/*`、`/api/update/*`、`/api/plugin-manager/*`、`/api/dsh-desktop-launcher/*` 与 `/api/dsh-web-ui-settings/*` 控制端点保持仅 loopback。未配对的桌面浏览器看到持久的全页阻断界面而不是数据(阻断页按错误码 `unpaired` 触发,不是任意 403)。
16
+ - **远程桌面通道**:`requirePairingForLan` 开启(默认)时,经局域网 URL 或隧道打开的桌面 Web GUI 透明地改走 `/remote`——同一套 UI,同一枚配对 cookie 门控。浏览器中 `/api`、`/sidebar`、`/git`、`/pet` 下的请求,以及已知的事件、终端与 SSH WebSocket,会在不转发远程 Origin 或配对 cookie 的前提下重新发给本机 Web Server;认证反代会为兄弟路由围栏提供自己生成的同源浏览器标记。SDK 的 loopback 专属特权方法(原生对话框、settings 与 credentials 面)对已配对远程桌面保持不可达,`/api/pair/*`、`/api/update/*`、`/api/plugin-manager/*`、`/api/dsh-desktop-launcher/*` 与 `/api/dsh-web-ui-settings/*` 控制端点保持仅 loopback。未配对的桌面浏览器看到持久的全页阻断界面而不是数据(阻断页按错误码 `unpaired` 触发,不是任意 403)。阻断页在任一门控调用成功后,或通道本身被拆除时退出——把 `requirePairingForLan` 关掉(或关闭插件)也会清掉通道短暂激活期间弹出的提示。
17
17
  - **姿态探测**:插件用伪造 Host 头(公网 base 与每个 LAN base)探测 SDK 的 `/api` 围栏。403 是默认姿态(围栏关闭,远程访问走配对)。任何非 403 结果——`--trusted-host`,或 `--host 0.0.0.0` 下 SDK 的局域网自动信任——都会以 CRITICAL 日志与配对面板红色横幅呈现,因此 SDK 的 `/api` 信任姿态是被看见的,而不是被假设的。探测失败会丢掉进行中的目标缓存,相同 origin 会在下次触发时重试。
18
18
  - **实时状态**:桌面面板经 SSE 流实时镜像配对状态(等待 → 已连接 → 已断开),并展示已授权设备名单。
19
19
  - **远程更新**:侧边栏加载后,底部的下载触发按钮(手机图标左侧)会静默探测 npm registry 上已安装的 `@linxin666/dsh-*` 全家桶版本;发现可自动更新的新版本时,按钮显示圆点并提供“发现新版本,检查更新”提示。点击按钮打开更新面板;未安装聚合包时,检查和更新覆盖 profile 中 registry 管理的全部 `@linxin666/*` 直接依赖,本地 link / file 开发依赖会被跳过。当存在较新版本时,面板展示版本对比并等待确认——点击「开始更新」后执行更新(在所属 dsh profile 内 `pnpm update --latest`;pnpm 缺失时依次回退 `corepack pnpm`、`npx --yes pnpm`,Windows 上经 `cmd.exe` 执行以解析 npm 安装的 `.cmd` shim;由仅 loopback 的 `/api/update/status` + `/api/update/run` 端点驱动)并请求重启 dsh web 以生效。pnpm 绿色退出后还会对照 registry 复核已装版本:绿色退出但版本纹丝不动(例如 pnpm 的 `minimumReleaseAge` 门禁静默跳过同日发布的新版本)会报告为「未更新成功」并附配置指引,而不是误报成功。锚点自身是本地 link 安装(开发模式)时,只报告 npm 状态而不更新。
@@ -173,6 +173,7 @@ pnpm run build
173
173
  - **撤销是逐请求的**:已配对手机请求已在 停止 落地时在途,完成该请求;下一个 403。
174
174
  - **已配对设备会话默认落盘**:设备会话(不含一次性二维码 token)写入 `$DSH_HOME/remote-web-ui-devices.json`(0600,临时文件加原子 rename)。已配对 cookie 在重启 `dsh web` 后仍然有效。刷新二维码仍会签发新 token;重启不会恢复当前二维码。点「停止」或单台「取消配对」会立即撤销并同步落盘。超过 `idleExpireMs`(默认 7 天,无心跳或门控请求)的会话会被删除,必须重新扫码。设备 id 即会话凭证(网关凭 cookie 中的设备 id 放行请求)。需要时可用 `devicesFile` 覆盖为其他绝对路径。变更 `cookieName` 会使旧设备失效(预期行为)。
175
175
  - **设备名单仅本机可见**:配对面板根据 User-Agent 显示精简设备名称(例如 `Windows · Chrome`)、在线/离线状态与最近活动时间,并可单台取消配对。界面不渲染作为会话凭据的设备 id 或原始 User-Agent。`/api/pair/status` 即使对已配对手机也不返回设备 id 名单。
176
+ - **桌面门控策略是公开字段**:`/api/pair/status` 只公开布尔值 `requirePairingForLan`,让远程桌面在设置作用域不可用前选择正确传输通道。该字段不是凭据,也不暴露令牌、设备、计数或隧道 URL。
176
177
  - **Quick-tunnel hostname 每次运行变化**:`trycloudflare.com` URL 每次 `cloudflared` 启动随机,所以隧道重启时 `publicBaseUrl` 必须随之更新。named tunnel(固定 hostname)避免这种抖动,也是持久安装 PWA 地址所必需的。
177
178
  - **PWA 为在线优先**:只缓存静态壳和离线页。全部移动端远程控制能力仍要求运行中的 DSH host;离线时不提供会话、API 响应或命令。
178
179
  - **开发 HMR**:`dsh web --dev` 按路径轮询每个 roster bundle,因此重建本包(其自己的 `tsdown --watch`)会热重载 client bundle;无 harness 侧 watcher。
package/lib/client.js CHANGED
@@ -1230,6 +1230,14 @@ window.__ModuleLoader__.load({
1230
1230
  QRCodeSVG.displayName = "QRCodeSVG";
1231
1231
  //#endregion
1232
1232
  //#region src/client/pair-api.ts
1233
+ /** Read the host-authoritative desktop pairing policy. */
1234
+ async function readPairGatePolicy() {
1235
+ const response = await fetch("/api/pair/status");
1236
+ if (!response.ok) throw new Error(`remote-web-ui: status failed with ${String(response.status)}`);
1237
+ const value = await response.json();
1238
+ if (typeof value.requirePairingForLan !== "boolean") throw new Error("remote-web-ui: status omitted requirePairingForLan");
1239
+ return { requirePairingForLan: value.requirePairingForLan };
1240
+ }
1233
1241
  /**
1234
1242
  * Mint a fresh pairing token (one active token at a time — this invalidates
1235
1243
  * any previous link).
@@ -2162,6 +2170,7 @@ window.__ModuleLoader__.load({
2162
2170
  "data-wide": wide ? void 0 : "rail",
2163
2171
  "data-update-available": updateAvailable ? "true" : void 0,
2164
2172
  "aria-label": updateLabel,
2173
+ "aria-expanded": open,
2165
2174
  title: updateLabel,
2166
2175
  onClick: openPanel,
2167
2176
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDownloadOutline16, { size: wide ? 16 : 18 })
@@ -2358,7 +2367,8 @@ window.__ModuleLoader__.load({
2358
2367
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipAnchor, {
2359
2368
  wide,
2360
2369
  label: t("entry.label"),
2361
- onClick: openPanel
2370
+ onClick: openPanel,
2371
+ expanded: open
2362
2372
  })]
2363
2373
  }), open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2364
2374
  className: remote_module_css_default.overlay,
@@ -2382,12 +2392,13 @@ window.__ModuleLoader__.load({
2382
2392
  }), document.body)] });
2383
2393
  }
2384
2394
  /** The trigger: an icon-only control with a persistent accessible label. */
2385
- function TooltipAnchor({ wide, label, onClick }) {
2395
+ function TooltipAnchor({ wide, label, onClick, expanded }) {
2386
2396
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2387
2397
  type: "button",
2388
2398
  className: remote_module_css_default.trigger,
2389
2399
  "data-wide": wide ? "wide" : "rail",
2390
2400
  "aria-label": label,
2401
+ "aria-expanded": expanded,
2391
2402
  title: label,
2392
2403
  onClick,
2393
2404
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PhoneIcon, { size: wide ? 16 : 18 })
@@ -3813,6 +3824,12 @@ window.__ModuleLoader__.load({
3813
3824
  "/sidebar/ws/agent-terminals",
3814
3825
  "/api/dsh-ssh/terminal"
3815
3826
  ]);
3827
+ /** Decide whether a remote desktop channel is required from local or host policy. */
3828
+ function remoteChannelRequired(hostname, snapshot, hostPairingPolicy) {
3829
+ if (isLoopbackHostname(hostname)) return false;
3830
+ if (snapshot.status === "ready") return (snapshot.value?.enabled ?? true) && (snapshot.value?.requirePairingForLan ?? true);
3831
+ return hostPairingPolicy !== false;
3832
+ }
3816
3833
  /**
3817
3834
  * Browser-safe loopback classification for the page origin (the SDK client
3818
3835
  * exports its own; this copy keeps the module dependency-free).
@@ -3984,6 +4001,17 @@ window.__ModuleLoader__.load({
3984
4001
  for (const restore of restoreSrc) restore();
3985
4002
  };
3986
4003
  }
4004
+ /**
4005
+ * Decide what the channel lifecycle must do next.
4006
+ * @param active - whether the gated remote channel should be running now.
4007
+ * @param installed - whether it currently is (disposer !== undefined).
4008
+ * @returns the transition to apply.
4009
+ */
4010
+ function channelTransition(active, installed) {
4011
+ if (active && !installed) return "install";
4012
+ if (!active && installed) return "retire";
4013
+ return "none";
4014
+ }
3987
4015
  //#endregion
3988
4016
  //#region src/client/FenceNotice.tsx
3989
4017
  /**
@@ -4154,6 +4182,8 @@ window.__ModuleLoader__.load({
4154
4182
  settingsScope.subscribe(syncRuntime);
4155
4183
  syncRuntime();
4156
4184
  let disposeChannel;
4185
+ let hostPairingPolicy;
4186
+ let unpairedWhilePolicyPending = false;
4157
4187
  let fenceNotice;
4158
4188
  const showFenceNotice = () => {
4159
4189
  if (fenceNotice !== void 0) return;
@@ -4178,26 +4208,41 @@ window.__ModuleLoader__.load({
4178
4208
  fenceNotice?.unmount();
4179
4209
  fenceNotice = void 0;
4180
4210
  };
4181
- const channelActive = () => {
4182
- if (isLoopbackHostname(window.location.hostname)) return false;
4183
- const snapshot = settingsScope.getSnapshot();
4184
- const value = snapshot.status === "ready" ? snapshot.value : void 0;
4185
- return (value?.enabled ?? true) && (value?.requirePairingForLan ?? true);
4211
+ const handleUnpaired = () => {
4212
+ if (settingsScope.getSnapshot().status !== "ready" && hostPairingPolicy === void 0) {
4213
+ unpairedWhilePolicyPending = true;
4214
+ return;
4215
+ }
4216
+ showFenceNotice();
4186
4217
  };
4218
+ const channelActive = () => remoteChannelRequired(window.location.hostname, settingsScope.getSnapshot(), hostPairingPolicy);
4187
4219
  const syncChannel = () => {
4188
- if (channelActive() && disposeChannel === void 0) disposeChannel = ctx.effect(() => {
4220
+ const transition = channelTransition(channelActive(), disposeChannel !== void 0);
4221
+ if (transition === "install") disposeChannel = ctx.effect(() => {
4189
4222
  return installRemoteChannel(window, {
4190
- onUnpaired: showFenceNotice,
4223
+ onUnpaired: handleUnpaired,
4191
4224
  onPaired: hideFenceNotice
4192
4225
  });
4193
4226
  }, "remote-web-ui: remote desktop channel");
4194
- else if (!channelActive() && disposeChannel !== void 0) {
4227
+ else if (transition === "retire" && disposeChannel !== void 0) {
4195
4228
  disposeChannel();
4196
4229
  disposeChannel = void 0;
4230
+ hideFenceNotice();
4197
4231
  }
4198
4232
  };
4199
4233
  settingsScope.subscribe(syncChannel);
4200
4234
  syncChannel();
4235
+ if (!isLoopbackHostname(window.location.hostname) && settingsScope.getSnapshot().status !== "ready") readPairGatePolicy().then((policy) => {
4236
+ hostPairingPolicy = policy.requirePairingForLan;
4237
+ syncChannel();
4238
+ if (hostPairingPolicy && unpairedWhilePolicyPending) showFenceNotice();
4239
+ unpairedWhilePolicyPending = false;
4240
+ }).catch(() => {
4241
+ hostPairingPolicy = true;
4242
+ syncChannel();
4243
+ if (unpairedWhilePolicyPending) showFenceNotice();
4244
+ unpairedWhilePolicyPending = false;
4245
+ });
4201
4246
  ctx.effect(() => {
4202
4247
  const timer = window.setTimeout(() => {
4203
4248
  if (sessionStorage.getItem("dsh-remote-pair-failed") === null) return;