dsh-zen-remote 1.1.16 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +54 -9
- package/README.md +249 -190
- package/README.zh-CN.md +247 -201
- package/assets/home.png +0 -0
- package/assets/info.png +0 -0
- package/assets/pairing.png +0 -0
- package/assets/session.png +0 -0
- package/assets/sheet.png +0 -0
- package/cordis.patch.yml +12 -11
- package/cordis.patch.yml.example +24 -9
- package/docs/interface.md +57 -2
- package/docs/remote-access.en.md +99 -58
- package/docs/remote-access.md +99 -42
- package/dsh-push.mjs +67 -54
- package/lan-gate.mjs +88 -20
- package/lib/activity.js +168 -0
- package/lib/activity.js.map +1 -0
- package/lib/admin-routes.js +526 -0
- package/lib/admin-routes.js.map +1 -0
- package/lib/client-pairing.js +224 -0
- package/lib/client-pairing.js.map +1 -0
- package/lib/client-routes.js +674 -0
- package/lib/client-routes.js.map +1 -0
- package/lib/client.js +10051 -4777
- package/lib/config.js +346 -0
- package/lib/config.js.map +1 -0
- package/lib/fetch-route-intercept.js +418 -0
- package/lib/fetch-route-intercept.js.map +1 -0
- package/lib/fingerprint.js +363 -0
- package/lib/fingerprint.js.map +1 -0
- package/lib/http.js +69 -0
- package/lib/http.js.map +1 -0
- package/lib/index.js +675 -54
- package/lib/index.js.map +1 -1
- package/lib/intercept-shape.js +81 -0
- package/lib/intercept-shape.js.map +1 -0
- package/lib/intercept.js +3040 -0
- package/lib/intercept.js.map +1 -0
- package/lib/lan-gate-server.cjs +532 -121
- package/lib/merge-streams.js +1165 -0
- package/lib/merge-streams.js.map +1 -0
- package/lib/relay-access.js +496 -0
- package/lib/relay-access.js.map +1 -0
- package/lib/relay-client.js +1303 -0
- package/lib/relay-client.js.map +1 -0
- package/lib/relay-filter.js +282 -0
- package/lib/relay-filter.js.map +1 -0
- package/lib/relay-server.js +1792 -0
- package/lib/relay-server.js.map +1 -0
- package/lib/restart-fields.js +34 -0
- package/lib/restart-fields.js.map +1 -0
- package/lib/restart-watcher.js +106 -0
- package/lib/restart-watcher.js.map +1 -0
- package/lib/session-reference.js +157 -0
- package/lib/session-reference.js.map +1 -0
- package/lib/share-export.js +16 -1
- package/lib/share-export.js.map +1 -1
- package/lib/share-ops.js +86 -0
- package/lib/share-ops.js.map +1 -0
- package/lib/share-store.js +294 -0
- package/lib/share-store.js.map +1 -0
- package/lib/types/activity.d.ts +99 -0
- package/lib/types/activity.d.ts.map +1 -0
- package/lib/types/admin-routes.d.ts +129 -0
- package/lib/types/admin-routes.d.ts.map +1 -0
- package/lib/types/client/MobileDrawerFooter.d.ts.map +1 -1
- package/lib/types/client/MobileHome.d.ts +1 -1
- package/lib/types/client/MobileHome.d.ts.map +1 -1
- package/lib/types/client/MobileHomeChips.d.ts +1 -1
- package/lib/types/client/MobileHomeChips.d.ts.map +1 -1
- package/lib/types/client/MobileSessionHeader.d.ts +13 -2
- package/lib/types/client/MobileSessionHeader.d.ts.map +1 -1
- package/lib/types/client/MobileSessionInfo.d.ts +20 -3
- package/lib/types/client/MobileSessionInfo.d.ts.map +1 -1
- package/lib/types/client/RemoteComposerBanner.d.ts +42 -0
- package/lib/types/client/RemoteComposerBanner.d.ts.map +1 -0
- package/lib/types/client/RemoteHeaderIcon.d.ts +37 -0
- package/lib/types/client/RemoteHeaderIcon.d.ts.map +1 -0
- package/lib/types/client/RemoteShareMenu.d.ts +28 -0
- package/lib/types/client/RemoteShareMenu.d.ts.map +1 -0
- package/lib/types/client/RemoteStatusIcon.d.ts +36 -0
- package/lib/types/client/RemoteStatusIcon.d.ts.map +1 -0
- package/lib/types/client/compat/desktop.d.ts +21 -0
- package/lib/types/client/compat/desktop.d.ts.map +1 -0
- package/lib/types/client/compat/types.d.ts +57 -0
- package/lib/types/client/compat/types.d.ts.map +1 -1
- package/lib/types/client/current-session-report.d.ts +37 -0
- package/lib/types/client/current-session-report.d.ts.map +1 -0
- package/lib/types/client/effects/header-status.d.ts.map +1 -1
- package/lib/types/client/effects/keyboard-guard.d.ts.map +1 -1
- package/lib/types/client/effects/permission-sheet.d.ts +39 -0
- package/lib/types/client/effects/permission-sheet.d.ts.map +1 -0
- package/lib/types/client/effects/remote-session.d.ts +25 -0
- package/lib/types/client/effects/remote-session.d.ts.map +1 -0
- package/lib/types/client/effects/turn-fold.d.ts +9 -0
- package/lib/types/client/effects/turn-fold.d.ts.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +187 -2
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/remote-fetch.d.ts +51 -0
- package/lib/types/client/remote-fetch.d.ts.map +1 -0
- package/lib/types/client/remote-group-expansion.d.ts +41 -0
- package/lib/types/client/remote-group-expansion.d.ts.map +1 -0
- package/lib/types/client/remote-share-css.d.ts +19 -0
- package/lib/types/client/remote-share-css.d.ts.map +1 -0
- package/lib/types/client/remote-share-register.d.ts +113 -0
- package/lib/types/client/remote-share-register.d.ts.map +1 -0
- package/lib/types/client/remote-status-css.d.ts +15 -0
- package/lib/types/client/remote-status-css.d.ts.map +1 -0
- package/lib/types/client/remote-status-register.d.ts +46 -0
- package/lib/types/client/remote-status-register.d.ts.map +1 -0
- package/lib/types/client/settings/SettingsSection.d.ts +46 -0
- package/lib/types/client/settings/SettingsSection.d.ts.map +1 -0
- package/lib/types/client/settings/register-settings.d.ts +58 -0
- package/lib/types/client/settings/register-settings.d.ts.map +1 -0
- package/lib/types/client/settings/settings-css.d.ts +10 -0
- package/lib/types/client/settings/settings-css.d.ts.map +1 -0
- package/lib/types/client/share/share-preview.d.ts.map +1 -1
- package/lib/types/client/styles/composer.css.d.ts +1 -1
- package/lib/types/client/styles/composer.css.d.ts.map +1 -1
- package/lib/types/client/styles/header.css.d.ts +1 -1
- package/lib/types/client/styles/header.css.d.ts.map +1 -1
- package/lib/types/client/styles/info.css.d.ts +1 -1
- package/lib/types/client/styles/info.css.d.ts.map +1 -1
- package/lib/types/client/styles/remote-session.css.d.ts +2 -0
- package/lib/types/client/styles/remote-session.css.d.ts.map +1 -0
- package/lib/types/client-data/current-session.d.ts +41 -0
- package/lib/types/client-data/current-session.d.ts.map +1 -0
- package/lib/types/client-data/remote-group-expansion.d.ts +76 -0
- package/lib/types/client-data/remote-group-expansion.d.ts.map +1 -0
- package/lib/types/client-data/remote-status.d.ts +148 -0
- package/lib/types/client-data/remote-status.d.ts.map +1 -0
- package/lib/types/client-data/settings-form.d.ts +774 -0
- package/lib/types/client-data/settings-form.d.ts.map +1 -0
- package/lib/types/client-data/shares.d.ts +211 -0
- package/lib/types/client-data/shares.d.ts.map +1 -0
- package/lib/types/client-pairing.d.ts +114 -0
- package/lib/types/client-pairing.d.ts.map +1 -0
- package/lib/types/client-routes.d.ts +112 -0
- package/lib/types/client-routes.d.ts.map +1 -0
- package/lib/types/config.d.ts +200 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/fetch-route-intercept.d.ts +156 -0
- package/lib/types/fetch-route-intercept.d.ts.map +1 -0
- package/lib/types/fingerprint.d.ts +164 -0
- package/lib/types/fingerprint.d.ts.map +1 -0
- package/lib/types/http.d.ts +41 -0
- package/lib/types/http.d.ts.map +1 -0
- package/lib/types/index.d.ts +56 -24
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/intercept-shape.d.ts +32 -0
- package/lib/types/intercept-shape.d.ts.map +1 -0
- package/lib/types/intercept.d.ts +432 -0
- package/lib/types/intercept.d.ts.map +1 -0
- package/lib/types/merge-streams.d.ts +393 -0
- package/lib/types/merge-streams.d.ts.map +1 -0
- package/lib/types/relay-access.d.ts +310 -0
- package/lib/types/relay-access.d.ts.map +1 -0
- package/lib/types/relay-client.d.ts +274 -0
- package/lib/types/relay-client.d.ts.map +1 -0
- package/lib/types/relay-filter.d.ts +103 -0
- package/lib/types/relay-filter.d.ts.map +1 -0
- package/lib/types/relay-server.d.ts +208 -0
- package/lib/types/relay-server.d.ts.map +1 -0
- package/lib/types/restart-fields.d.ts +22 -0
- package/lib/types/restart-fields.d.ts.map +1 -0
- package/lib/types/restart-watcher.d.ts +83 -0
- package/lib/types/restart-watcher.d.ts.map +1 -0
- package/lib/types/session-reference.d.ts +66 -0
- package/lib/types/session-reference.d.ts.map +1 -0
- package/lib/types/share-export.d.ts +14 -1
- package/lib/types/share-export.d.ts.map +1 -1
- package/lib/types/share-ops.d.ts +79 -0
- package/lib/types/share-ops.d.ts.map +1 -0
- package/lib/types/share-store.d.ts +87 -0
- package/lib/types/share-store.d.ts.map +1 -0
- package/lib/types/virtual-id.d.ts +38 -0
- package/lib/types/virtual-id.d.ts.map +1 -0
- package/lib/virtual-id.js +52 -0
- package/lib/virtual-id.js.map +1 -0
- package/llms.txt +17 -14
- package/package.json +60 -50
- package/scripts/build-client.mjs +5 -3
- package/scripts/check-host-shadow.mjs +19 -2
- package/scripts/check-settings-form.mjs +933 -0
- package/scripts/check-shares.mjs +397 -0
- package/scripts/check-upload-endpoint.mjs +59 -3
- package/scripts/sync-doc-version.mjs +8 -15
- package/scripts/verify-fingerprint-live.mjs +111 -0
- package/src/activity.ts +218 -0
- package/src/admin-routes.ts +627 -0
- package/src/client/MobileAttachButton.tsx +2 -2
- package/src/client/MobileAttachChips.tsx +3 -3
- package/src/client/MobileDrawerFooter.tsx +7 -4
- package/src/client/MobileHome.tsx +20 -16
- package/src/client/MobileHomeChips.tsx +16 -16
- package/src/client/MobileNavOverlay.tsx +2 -2
- package/src/client/MobileNavToggle.tsx +3 -3
- package/src/client/MobileSessionHeader.tsx +63 -42
- package/src/client/MobileSessionInfo.tsx +118 -17
- package/src/client/RemoteComposerBanner.tsx +76 -0
- package/src/client/RemoteHeaderIcon.tsx +103 -0
- package/src/client/RemoteShareMenu.tsx +57 -0
- package/src/client/RemoteStatusIcon.tsx +91 -0
- package/src/client/compat/desktop.ts +22 -0
- package/src/client/compat/types.ts +62 -0
- package/src/client/current-session-report.ts +164 -0
- package/src/client/effects/header-status.ts +21 -17
- package/src/client/effects/keyboard-guard.ts +65 -1
- package/src/client/effects/permission-sheet.ts +149 -0
- package/src/client/effects/remote-session.ts +68 -0
- package/src/client/effects/turn-fold.ts +52 -0
- package/src/client/index.tsx +151 -36
- package/src/client/locales.ts +396 -2
- package/src/client/remote-fetch.ts +125 -0
- package/src/client/remote-group-expansion.ts +190 -0
- package/src/client/remote-share-css.ts +54 -0
- package/src/client/remote-share-register.ts +244 -0
- package/src/client/remote-status-css.ts +64 -0
- package/src/client/remote-status-register.ts +107 -0
- package/src/client/settings/SettingsSection.tsx +1229 -0
- package/src/client/settings/register-settings.ts +116 -0
- package/src/client/settings/settings-css.ts +236 -0
- package/src/client/share/share-preview.tsx +6 -3
- package/src/client/share/share-sheet.tsx +3 -3
- package/src/client/styles/composer.css.ts +119 -26
- package/src/client/styles/header.css.ts +7 -7
- package/src/client/styles/info.css.ts +74 -0
- package/src/client/styles/remote-session.css.ts +251 -0
- package/src/client-data/current-session.ts +57 -0
- package/src/client-data/remote-group-expansion.ts +145 -0
- package/src/client-data/remote-status.ts +374 -0
- package/src/client-data/settings-form.ts +1371 -0
- package/src/client-data/shares.ts +467 -0
- package/src/client-pairing.ts +244 -0
- package/src/client-routes.ts +732 -0
- package/src/config.ts +447 -0
- package/src/fetch-route-intercept.ts +510 -0
- package/src/fingerprint.ts +408 -0
- package/src/http.ts +68 -0
- package/src/index.ts +719 -62
- package/src/intercept-shape.ts +87 -0
- package/src/intercept.ts +3362 -0
- package/src/merge-streams.ts +1272 -0
- package/src/relay-access.ts +669 -0
- package/src/relay-client.ts +1514 -0
- package/src/relay-filter.ts +291 -0
- package/src/relay-server.ts +2025 -0
- package/src/restart-fields.ts +36 -0
- package/src/restart-watcher.ts +140 -0
- package/src/session-reference.ts +156 -0
- package/src/share-export.ts +26 -1
- package/src/share-ops.ts +114 -0
- package/src/share-store.ts +373 -0
- package/src/virtual-id.ts +59 -0
package/cordis.patch.yml
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
# dsh-zen-remote — bundle patch layer
|
|
1
|
+
# dsh-zen-remote — bundle patch layer(一个包,一行 insert)
|
|
2
2
|
#
|
|
3
3
|
# 当 profile 的 dsh.profile.bundles 列了这个包时整层生效
|
|
4
4
|
# (`dsh plugin --profile web add dsh-zen-remote` 会自动写进去)。
|
|
5
|
-
#
|
|
5
|
+
# 随包自带,用户不需要在自己的 profile patch 里手挂任何一行。
|
|
6
|
+
#
|
|
7
|
+
# 2.0.0 起网关(lan-gate.mjs)和推送(dsh-push.mjs)不再各占一行:它们的
|
|
8
|
+
# 行配置拿不到别的行的配置,而 2.0.0 要给插件加一个按行生效的 role 开关,
|
|
9
|
+
# 所以改由主入口读行配置里的 role,再按角色用 ctx.plugin() 加载这两个
|
|
10
|
+
# 子插件。role 缺省或非法值一律按 host,也就是原来的三行行为。旧的
|
|
11
|
+
# dsh-zen-remote-gateway / dsh-zen-remote-push 行如果还残留在你的 profile
|
|
12
|
+
# patch 里,loader 只会打一条警告并跳过,删掉即可。
|
|
6
13
|
- insert:
|
|
7
|
-
#
|
|
8
|
-
# + host
|
|
14
|
+
# 界面半边(浏览器端 app 外壳,经 package.json 的 dsh.client 发现)
|
|
15
|
+
# + host 侧路由(附件上传 / 客户端配置 / 分享导出)
|
|
16
|
+
# + 按角色加载的网关与推送子插件。
|
|
9
17
|
- id: dsh-zen-remote
|
|
10
18
|
name: dsh-zen-remote
|
|
11
|
-
# 通道半边:spawn 网关子进程(配对认证 / PWA 注入 / 反代到 DSH)。
|
|
12
|
-
- id: dsh-zen-remote-gateway
|
|
13
|
-
name: dsh-zen-remote/lan-gate.mjs
|
|
14
|
-
# 推送半边:agent 回合结束推送 + 模型工具 push_notify。
|
|
15
|
-
# 以前要用户自己在 profile 的 cordis.patch.yml 里手挂,现在随包自带。
|
|
16
|
-
- id: dsh-zen-remote-push
|
|
17
|
-
name: dsh-zen-remote/dsh-push.mjs
|
package/cordis.patch.yml.example
CHANGED
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
# dsh-zen-remote — 静态挂载示例
|
|
2
2
|
#
|
|
3
|
-
#
|
|
3
|
+
# 不想走插件管理(「插件」页或 `dsh plugin --profile <name> add`)的话,
|
|
4
|
+
# 把下面这段抄进你 DSH profile 的
|
|
4
5
|
# cordis.patch.yml,并把 <ABSOLUTE_PATH> 换成本仓库的 clone 目录。
|
|
5
|
-
#
|
|
6
|
-
# cordis.patch.yml
|
|
6
|
+
# 走插件管理的正常安装不需要这个文件——包自带的
|
|
7
|
+
# cordis.patch.yml 挂的就是这一行。
|
|
8
|
+
#
|
|
9
|
+
# 2.0.0 起整包只有这一行 insert:主入口(lib/index.js)读行配置里的
|
|
10
|
+
# role,host(默认)就绪地加载网关与推送子插件,client 只挂远程部件。
|
|
11
|
+
# 不要照 1.x 的老例子把网关行、推送行再各挂一行(行 id 以 -gateway /
|
|
12
|
+
# -push 结尾的那两行)——网关行拿不到主行的配置,两行会各拉起一个网关
|
|
13
|
+
# 抢 3088 端口。升级上来还残留着旧行的话,loader 会打一条警告并跳过,
|
|
14
|
+
# 把旧行删掉即可。
|
|
15
|
+
#
|
|
16
|
+
# 行配置(全部可省,走默认值;settings 页保存的值优先于这里手写的):
|
|
7
17
|
- insert:
|
|
8
18
|
- id: dsh-zen-remote
|
|
9
19
|
name: dsh-zen-remote
|
|
10
20
|
path: file:///ABSOLUTE_PATH/dsh-zen-remote/lib/index.js
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
config:
|
|
22
|
+
role: host # host(主服务端,默认)| client(子客户端)
|
|
23
|
+
serverName: My-Mac # 服务端显示名,出现在子客户端分组标题里(≤40 字)
|
|
24
|
+
port: 3088 # 网关端口
|
|
25
|
+
host: 127.0.0.1 # 网关监听地址
|
|
26
|
+
trustedProxies: "" # 反代不在本机时填它的出口 IP,逗号分隔
|
|
27
|
+
idleHours: 48 # 远程会话闲置休眠时长(小时)
|
|
28
|
+
autoShareNewSessions: false # 新建会话自动开启远程
|
|
29
|
+
# 子客户端角色要填的两项:
|
|
30
|
+
# serverUrl: https://dsh.example.com
|
|
31
|
+
# deviceToken: <配对拿到的设备令牌>
|
package/docs/interface.md
CHANGED
|
@@ -10,6 +10,48 @@ v1.0.0(fork 自 [mexiaosqwq/dsh-web-mobile](https://github.com/mexiaosqwq/dsh-
|
|
|
10
10
|
|
|
11
11
|
这次重写建立在上游 v1.0.0 打下的底子上(分主题 CSS 结构、构建脚本、平板兼容段大量沿用),在此向 [mexiaosqwq](https://github.com/mexiaosqwq/dsh-web-mobile) 的原始工作表示感谢。
|
|
12
12
|
|
|
13
|
+
## 2.0:远程会话与设置区块
|
|
14
|
+
|
|
15
|
+
2.0 起插件有「一个插件、两种角色」,界面半边随之多出两组部件:主服务端的共享部件与子客户端的远程部件。它们的共同点是**都注册在桌面端门(`dshDesktop`)之外**——见本节末尾的清单与缘由。
|
|
16
|
+
|
|
17
|
+
### 插件设置区块
|
|
18
|
+
|
|
19
|
+
设置区块挂在 DSH「插件」页的 `plugins.row.config` 槽位上(`src/client/settings/`),是 2.0 唯一的配置与管理界面:
|
|
20
|
+
|
|
21
|
+
- **主服务端区块**:角色(角色卡里带两角色共用的**设备名称**字段,即 `serverName`:host 上是其他设备侧边栏分组标题里的名字,与子客户端双向自动同步)→ 网关与反代(host/port/可信反代/限流)→ 配对(先选设备角色、显示书面指引,再生成)→ 设备列表(名称、角色、最近在线、是否订阅推送;改名 / `set-role` / 吊销)→ 推送(各策略字段 + 测试推送)→ 远程共享(闲置时长、自动开启、已共享会话列表与一键全关);
|
|
22
|
+
- **子客户端区块**:角色(设备名称同一字段,作为配对时注册到服务端的名字)→ 服务端地址与配对(配对按角色卡里的设备名称注册,名字非法时配对按钮禁用)→ 连接状态(已连接 / 离线 / 版本有差异 / 令牌已吊销,可解配)→ 诊断(接口指纹差异组、运行时不兼容记录);
|
|
23
|
+
- **经网关打开时只读**:手机或其它浏览器经网关打开设置页可以看,但生成配对码、改角色、吊销这些管理动作被拒绝(带 `x-zen-remote-via` 标记的请求一律拒绝 POST)——设备丢了也没法用它再配出新设备;
|
|
24
|
+
- 角色判定读的是 configForms 行文档里的已保存角色;行文档答不上来时回退探测 `/_dsh/mobile-nav/client-config`(该路由两角色都注册,报的是合并后的生效角色)。
|
|
25
|
+
|
|
26
|
+
### 主服务端:共享部件
|
|
27
|
+
|
|
28
|
+
- **标题行远程图标**(`conversation.session.header.actions`,order 25,排在官方任务入口之后):灰色为未开启,亮色为已开启,亮色带小圆点表示此刻有桌面应用端正在查看;悬停显示剩余闲置时间;点击开关当前会话的远程,带二次确认。
|
|
29
|
+
- **会话「…」菜单项**(`sidebar.workspaces.session.menu.item`,order 500,官方归档行之下、分组细线之后):侧边栏会话行的「…」菜单里直接开启 / 关闭该会话的远程。
|
|
30
|
+
- 这两组部件只在 host 角色渲染与轮询,角色判定与设置页同一套两级决策,角色切换回来时自动恢复。
|
|
31
|
+
|
|
32
|
+
### 子客户端:远程部件
|
|
33
|
+
|
|
34
|
+
- **远程分组**:侧边栏在本地工作区后面按「服务端名 · 工作区名」多出几组,默认收起,里面是服务端已开启远程的会话;服务端的工作区只有其中至少一个会话开启了远程才会成组——没动过的工作区不显示成空分组,已显示过的分组里会话全部关闭后空分组留到刷新页面(DSH 界面对移除过的工作区 id 不再接受,见根 README 已知限制);服务端离线时分组标注「(离线)」、输入禁用、后台退避重连;解配 / 吊销保留分组只换标注,这两种状态同样禁用输入、也不等自动重连(只能从设置页重新配对才恢复);仅当换到另一台服务端才整组移除;
|
|
35
|
+
- **标题行连接图标**(同一槽位,order 26):每个远程会话的连接状态——在线 / 离线 / 版本有差异;点击可关闭该会话的远程(二次确认,文案写明「关闭后需要在服务端重新开启」);
|
|
36
|
+
- **composer 横幅**(`conversation.input.dock`,order 15):四种状态——会话被休眠或服务端关闭时显示「远程已关闭」及原因;服务端离线显示「暂时只读,恢复连接后自动可用」;令牌被吊销、解除配对也显示横幅(这两者不会自己恢复,文案指向设置页重新配对)。横幅出现的同时**输入一律禁用**,禁用走宿主自己的 composer-block 契约(`ctx.conversation.blocks.set(sessionId, { reason })`,输入框、发送钮与模型控件一起禁掉,`reason` 成为占位文案),链接恢复或组件卸载时清除,不碰宿主 DOM;
|
|
37
|
+
- 面板长尾:远程会话里带会话 id 的调用自动走中继;改动摘要 / diff 是例外路径——它们不走 typert 网关而是 plain-HTTP GET,浏览器侧的 fetch 包装把带虚拟 id 的 `/api/changes.*` 请求改写到子客户端路由 `/_dsh/zen-remote/client/http/<route>` 再经中继;离线时写一律本地拒绝,`terminal/follow` 也按写处理(它的附带会接管终端的输入控制权,离线放行等于悄悄抢走一条不在线连接的控制权,所以离线即拒绝);认不准数据来源的面板直接隐藏,而不是显示本机错误数据(清单见根 README「已知限制」)。
|
|
38
|
+
- **关闭的会话与断线恢复**(CP4 / CP4-client-fix2):服务端关闭某会话的远程(或它被闲置休眠扫掉)时,该会话**从侧边栏列表隐藏**——按「已归档」处理(宿主 `sessionVisible` 在默认筛选下不显示 archived 里的会话),打开「显示已归档」筛选仍能找到。唯一的例外是此刻正打开的会话页:页面停在原处——关闭不重试,已出过帧的会话流也不再报错,静默保持到页面自己关掉它(宿主把会话流的任何终止错误都渲染成对话区顶部的「历史加载失败:…」,而关闭原因只该由横幅表达);若之后服务端重新共享、又有对它的调用成功,关闭记录随之清掉,这条流改为**正常结束**——宿主把它当作载具恢复、带着新历史自动重开。零帧的会话流、其他面板的流以及对它的 typert 单次请求仍以错误结束,但文字统一为「远程已关闭」,不透出服务端的内部报错(改动摘要 / diff 走的 plain-HTTP 转发例外:那里仍是只带错误码的 403)。composer 横幅显示「远程已关闭」及原因,输入经 composer-block 禁用;当前会话绝不进 archived(否则宿主守卫 `clearArchivedCurrent` 会把正打开的页踢回首页,横幅就无从显示),用户切到别的会话后(当前会话信号来自宿主持久化的选择存储,逐帧读取、另有 1 秒轮询兜底)它才转为隐藏。服务端重新共享(id 回到某工作区的 `sessionIds`)即从 archived 移出、回到正常位置。服务端断线期间已打开的远程会话页保持原状:会话流与任务列表类面板在中继恢复后自动续上——已出过帧的流以干净 end 结束,宿主界面把它当作载具错误立即重开、拿到恢复后的新快照;零帧的原位重开,重开同样走退避(1 秒起翻倍、上限 30 秒)。「远程已关闭」与「暂时只读」是真判决,不在续上之列;文件树变更与终端面板断线后需重新打开。
|
|
39
|
+
|
|
40
|
+
这些部件的数据源是 `GET /_dsh/zen-remote/client/remote-status`,该路由**两种角色都注册**(host 上应答空的 `{state:'unpaired'}` 结论,桌面端 App 里切角色不用换代码路径);client 前缀下其余路由(配对代理、连接状态、立即重连、解配、plain-HTTP 转发)只在 client 角色挂载,host 角色对它们一律 404(最小暴露面)。
|
|
41
|
+
|
|
42
|
+
### 桌面端门之外生效的部件清单
|
|
43
|
+
|
|
44
|
+
手机外壳有一个硬门:客户端检测到宿主的 `dshDesktop` 接口(0.2.0 桌面端窗口都有)时,`<768px` 的 app 化外壳**整体不生效**,与窗口宽度无关——桌面端窗口拉到多窄都不会变成手机布局。这扇门在 `src/client/compat/desktop.ts`,以下部件**有意留在门外**(在门判定之前注册),否则在桌面端里会跟着一起消失:
|
|
45
|
+
|
|
46
|
+
| 部件 | 槽位 | 出现在哪些角色 |
|
|
47
|
+
| --- | --- | --- |
|
|
48
|
+
| 插件设置区块 | `plugins.row.config` | 两角色(内容按角色不同) |
|
|
49
|
+
| 标题行远程图标 + 会话「…」菜单项 | `conversation.session.header.actions` / `sidebar.workspaces.session.menu.item` | host |
|
|
50
|
+
| 标题行连接图标 + composer 横幅 | 同上槽位 / `conversation.input.dock` | client(仅远程会话渲染) |
|
|
51
|
+
| 远程分组(流合并进侧边栏) | `workspace/follow` 全局流的运行时包装 | client |
|
|
52
|
+
|
|
53
|
+
门外留下的只有这些;手机外壳本体(两级页面栈、composer 重排、手势、调试徽章)仍受门与 `<768px` 断点的双重约束,桌面 ≥1024px 逐像素 no-op 的硬约束不变。
|
|
54
|
+
|
|
13
55
|
## 效果
|
|
14
56
|
|
|
15
57
|
| 会话列表主屏 | 会话页 | 会话信息卡 | composer 权限 sheet |
|
|
@@ -129,6 +171,16 @@ composer 最左的回形针打开的是**手机本地**的文件选择器(iOS
|
|
|
129
171
|
> `v2.0.0` 及更早的小节标题用的是手机端界面的代次,与 npm 包版本号不是一条线;
|
|
130
172
|
> 两套编号并行容易看混,从 `1.1.0` 起本表统一按 **npm 包版本**记录。
|
|
131
173
|
|
|
174
|
+
### 2.0.0
|
|
175
|
+
|
|
176
|
+
**新增**
|
|
177
|
+
|
|
178
|
+
- 一个插件、两种角色,界面半边多出两组部件(详见「2.0:远程会话与设置区块」一节):
|
|
179
|
+
- 插件设置区块:DSH「插件」页里的完整配置与管理界面,主服务端(网关 / 反代 / 配对 / 设备列表 / 推送 / 远程共享)与子客户端(服务端地址 / 连接状态 / 诊断)按角色不同;经网关打开时管理动作只读;
|
|
180
|
+
- 主服务端共享部件:会话页标题行远程图标(灰 / 亮 / 亮带圆点,悬停剩余闲置时间,点击开关带确认)与侧边栏会话「…」菜单项;手机会话信息卡片里的「远程访问」开关(只在卡片打开期间拉共享表);
|
|
181
|
+
- 子客户端远程部件:侧边栏远程分组(离线 / 解配 / 吊销 / 版本差异标注、离线禁写)、标题行连接图标、「远程已关闭」composer 横幅;
|
|
182
|
+
- 以上部件全部注册在桌面端门(`dshDesktop`)之外——桌面端 App 正是主服务端/子客户端的宿主,门的语义是「手机外壳不生效」,不是「远程部件不生效」。
|
|
183
|
+
|
|
132
184
|
### 未发布
|
|
133
185
|
|
|
134
186
|
**新增**
|
|
@@ -312,6 +364,7 @@ composer 最左的回形针打开的是**手机本地**的文件选择器(iOS
|
|
|
312
364
|
## 兼容插件
|
|
313
365
|
|
|
314
366
|
- [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar)(0.19+:它的全部 tab 都在 DSH 0.1.5 官方右侧栏里,手机上宿主自己切全屏,本插件在会话页头部给它留入口位、左缘回退手势收它、文件树里点 @ 引用后自动收起——面板全屏盖住会话页,草稿变化看不见,收起本身就是点击反馈;≤ 0.18 自绘右栏的老版本:全宽抽屉 + 安全区 + 底部关闭 pill,只在 DSH < 0.1.5 上还会走到)——本机实测 **0.19.1**(老版规则实测 0.15.0)
|
|
367
|
+
- dsh-better-sidebar 的**远程会话处理**:它的界面(文件浏览、编辑器、git 变动、子代理/任务、底部工作台、任务编辑与任务输出浮窗)都带 `{sessionId, cwd}` 语境调它自己的 `/sidebar/api/*` 接口,远程会话里这些请求落到子客户端本机——文件树会显示本机同名文件、编辑会真的写本机文件。远程会话(`html[data-zr-remote-session="1"]`)里按插件自己的挂载/渲染标记隐藏这些界面,选择器在 `src/client/styles/remote-session.css.ts`,未安装时一条都不命中;桌面门前注入,桌面 App 形态的子客户端同样生效。因为它接管了宿主的「文件」标签,远程会话里暂时没有侧栏文件浏览;聊天或交付物里的文件链接(`dsh-resource://file/**`)会被它的编辑器注册抢先接住,展开一个空的右栏(根治要等 zen-remote 自己的远程文件树)。定位是**隐藏而非代理**:中继不携带也不新增 `/sidebar` 路由;插件组件保持挂载并继续静默发只读请求,但所有写控件都在被隐藏的界面里,不会误写本机
|
|
315
368
|
- [@nanmicoder/dsh-agent-teams](https://github.com/NanmiCoder/dsh-agent-teams)(AgentTeams 活动浮层:手机端挪到会话头部下方、避开安全区,会话列表页隐藏;子代理会话头部保留可点的父会话面包屑用于切回)——本机实测 **0.1.9**
|
|
316
369
|
- [@ychris12138/dsh-usage-stats](https://github.com/Ychris12138/dsh-usage-stats)(用量与余额,主屏 chips 行可直接打开;旧包名 `dsh-usage-stats` 已废弃,两个名字同时留在 profile 里会因重复的 `usage-stats` 行让 DSH 起不来)——本机实测 **0.2.9**
|
|
317
370
|
- [@opendsh/dsh-plugin-scheduled-tasks](https://github.com/Ceelog/dsh-plugins)(定时任务,主屏 chips 行自动收割入口)——本机实测 **0.2.3**
|
|
@@ -322,11 +375,13 @@ composer 最左的回形针打开的是**手机本地**的文件选择器(iOS
|
|
|
322
375
|
|
|
323
376
|
## 安装
|
|
324
377
|
|
|
378
|
+
桌面端 App 与 Web 端都可以在左侧「插件」页 →「添加插件」→ 输入 `dsh-zen-remote` → 点「立即启用」;无头机器上的 Web 服务也可以用命令行:
|
|
379
|
+
|
|
325
380
|
```sh
|
|
326
|
-
dsh plugin add dsh-zen-remote
|
|
381
|
+
dsh plugin --profile web add dsh-zen-remote
|
|
327
382
|
```
|
|
328
383
|
|
|
329
|
-
|
|
384
|
+
装完重启桌面端 App 或 `dsh web`。命令行不接受 `--profile desktop`(桌面端 profile 由 App 独占管理);手动写法与本地开发装法见[根 README](../README.zh-CN.md#安装)。
|
|
330
385
|
|
|
331
386
|
## 构建
|
|
332
387
|
|
package/docs/remote-access.en.md
CHANGED
|
@@ -1,30 +1,28 @@
|
|
|
1
1
|
<h1 align="center">Remote access (gateway half)</h1>
|
|
2
|
-
<p align="center">Turn DeepSeek Harness into a mobile PWA you can safely reach from the public internet: pairing-code auth + token identity + real Web Push, with your own reverse proxy terminating TLS.</p>
|
|
2
|
+
<p align="center">Turn DeepSeek Harness into a mobile PWA you can safely reach from the public internet: pairing-code auth + token identity + real Web Push, with your own reverse proxy terminating TLS — plus device roles and the remote relay that serves paired desktop clients.</p>
|
|
3
3
|
|
|
4
|
-
> Full documentation for the gateway half of `dsh-zen-remote`: reverse proxy, pairing, environment variables, admin API, push, security boundary.
|
|
4
|
+
> Full documentation for the gateway half of `dsh-zen-remote`: reverse proxy, device roles and pairing, environment variables, admin API, the remote relay, push, security boundary.
|
|
5
5
|
> Installation and quick start live in the [root README](../README.md); interface-side detail in [interface.md](interface.md) (Chinese).
|
|
6
6
|
|
|
7
7
|
Built on the MIT [dsh-mobile-gate](https://github.com/Bernardxu123/dsh-mobile-gate) secure-gateway base, with PWA differentiation.
|
|
8
8
|
|
|
9
|
-
[](https://www.npmjs.com/package/the gateway half)
|
|
10
|
-
[](https://github.com/KyoMio/the gateway half/blob/main/LICENSE)
|
|
11
|
-
[](https://github.com/topics/dsh-plugin)
|
|
12
|
-
|
|
13
9
|
---
|
|
14
10
|
|
|
15
11
|
## Features
|
|
16
12
|
|
|
17
13
|
| Module | What |
|
|
18
14
|
| --- | --- |
|
|
19
|
-
| 🔑 **Public-internet identity** | The gateway listens on `127.0.0.1` only, sitting behind your own reverse proxy. New devices trade a pairing code for a long-lived device token (cookie `lg_device`) — identity follows the token, not the source IP |
|
|
15
|
+
| 🔑 **Public-internet identity** | The gateway listens on `127.0.0.1` only, sitting behind your own reverse proxy. New devices trade a pairing code for a long-lived device token (cookie `lg_device` for web devices, a JSON token for desktop clients) — identity follows the token, not the source IP |
|
|
16
|
+
| 👥 **Device roles** | A pairing code is minted for one role: **web app device** (phones/tablets/browsers through the gateway, see all sessions) or **desktop app device** (another DSH desktop running this plugin as the client role — relay prefix only, shared sessions only). A code offered to the wrong channel is refused with a clear message, not consumed |
|
|
17
|
+
| 📡 **Remote relay** | Desktop clients talk to the host over zen-remote's own relay protocol (HTTP invokes + NDJSON stream subscriptions); the server re-verifies session ownership per request against a per-method allowlist — data of an unshared session never leaves the box |
|
|
20
18
|
| 📱 **Real PWA** | `manifest.json` + service worker: once the proxy provides HTTPS, "Add to Home Screen" actually works — standalone full-screen app with icon, splash, theme-color, maskable assets |
|
|
21
19
|
| 🌐 **Offline** | SW v3: only the true static shell (manifest/icons/offline page) is cache-first, everything else (DSH client bundle JS/CSS, API, page HTML) is network-first — a new deploy is picked up immediately instead of lingering behind stale cached CSS |
|
|
22
20
|
| 👆 **Touch gestures** | Pinch-to-resize font (resettable); edge-swipe-back has been handed off to the interface half (see "Division of labor" below), pull-to-refresh has been removed entirely (an accidental overscroll used to fire a full reload mid-conversation) |
|
|
23
21
|
| 🔔 **Push when you're actually needed** | Real Web Push (VAPID-signed, aes128gcm-encrypted). By default it fires only when something is genuinely waiting on you: a tool needs authorization, or the model asked you a question. Plain turn-end is opt-in (`DSH_PUSH_TURN_END=1`). The notification carries no conversation content unless you ask for it |
|
|
24
|
-
| 🛎️ **`push_notify` tool** | A model-callable push tool (registered by
|
|
22
|
+
| 🛎️ **`push_notify` tool** | A model-callable push tool (registered by the push half): the model can decide mid-task that the user needs a decision, that a key milestone was reached, or that an error needs a human — and push straight to the lock screen instead of waiting for the turn to end. Usage discipline (don't call this often) is spelled out in the tool description; the host also enforces it with rate limits (max 1 per 60s per session, 20/hour globally) — over the limit, the call is silently dropped, never an error. Same aes128gcm end-to-end encryption, same lock-screen-only exposure. Turn it off entirely with `pushTool: false` (or `DSH_PUSH_TOOL=0`); it's also skipped automatically on hosts without a tool registry (`ctx.tools`), with no effect on the rest of the plugin |
|
|
25
23
|
| 📐 **Touch layout** | This repo now only keeps shell-level rules (iOS input-zoom fix, safe-area scroll padding, horizontal-scrolling code) — layout rules (44px targets, dialogs, composer chrome) moved to the interface half, see "Division of labor" below — desktop never affected |
|
|
26
24
|
| 🔒 **Desktop unaffected** | Every rule is rooted at `html:not([data-lan-device="desktop"])` (or an `@media(max-width:820px)` with the same exclusion) — an explicit "desktop" kind opts out, everything else (including a real phone's default "auto" kind) opts in |
|
|
27
|
-
| 🛡️ **Admin surface is local-only** | Generating pairing codes, managing devices, triggering pushes — these endpoints only accept direct local connections; anything arriving through the proxy gets 403 |
|
|
25
|
+
| 🛡️ **Admin surface is local-only** | Generating pairing codes, managing devices, triggering pushes — these endpoints only accept direct local connections; anything arriving through the proxy gets 403. Since 2.0.0 they are driven from the dsh-zen-remote settings block in the DSH Plugins page (the plugin backend calls them as the local machine); the `/lan-gate/admin` page is now just a notice pointing there |
|
|
28
26
|
|
|
29
27
|
---
|
|
30
28
|
|
|
@@ -36,18 +34,19 @@ Public device (phone/laptop) --HTTPS--> your own reverse proxy (nginx/Caddy, ter
|
|
|
36
34
|
▼
|
|
37
35
|
gateway (isolated Node child · listens on 127.0.0.1:3088 by default)
|
|
38
36
|
│
|
|
39
|
-
|
|
40
|
-
│
|
|
41
|
-
unpaired device
|
|
42
|
-
→ any path redirects
|
|
43
|
-
the pairing page
|
|
44
|
-
POST code -> token
|
|
45
|
-
|
|
37
|
+
┌───────────────────┬─────────────┴────────────┬──────────────────────────────┐
|
|
38
|
+
│ │ │ │
|
|
39
|
+
unpaired device web app device (paired) desktop app device (paired) direct-local request
|
|
40
|
+
→ any path redirects → forwarded to DSH (auto- → relay prefix only (no X-Forwarded-*)
|
|
41
|
+
to the pairing page discovered real port) /_dsh/zen-remote/relay/* → admin API / push trigger
|
|
42
|
+
POST code -> token with device marker headers with marker headers + /lan-gate/status /action /pair
|
|
43
|
+
+ bearer shared secret; the shared secret /pwa/push/send
|
|
44
|
+
any other path 403s
|
|
46
45
|
```
|
|
47
46
|
|
|
48
|
-
- The gateway is an isolated child process: if it crashes, DSH's main service is unaffected; it's torn down automatically when the plugin stops.
|
|
49
|
-
- DSH's own web server still binds `127.0.0.1` only. The gateway never touches DSH's config or its `/api` trust fence.
|
|
50
|
-
- The one IP-based trust left: a
|
|
47
|
+
- The gateway is an isolated child process: if it crashes, DSH's main service is unaffected; it's torn down automatically when the plugin stops. If the main process itself dies without a normal shutdown (crash, `kill -9`), the child checks the parent's liveness every 5 seconds and exits on its own once it is gone, instead of squatting on the port.
|
|
48
|
+
- DSH's own web server still binds `127.0.0.1` only. The gateway never touches DSH's config or its `/api` trust fence; the forward target port is discovered from the host's real listening port (desktop builds use a configurable one).
|
|
49
|
+
- The one IP-based trust left: a request is treated as the local user sitting at this machine — the only path into the admin surface — only when the socket is loopback, **no** `X-Forwarded-*` header is present, **and** the `Host` header, with the port stripped, is exactly `127.0.0.1` / `localhost` / `[::1]`. Requests that came through the proxy always carry forwarded headers, so they can never look local. The Host check is what blocks DNS rebinding: when an attacker's own domain resolves to 127.0.0.1, the request may still arrive over loopback, but its Host is not a loopback name and it never reaches the admin surface. **Mind the flip side**: in same-machine testing the client must reach the gateway over a LAN IP — a loopback address is treated as local-direct and skips token checks entirely.
|
|
51
50
|
|
|
52
51
|
---
|
|
53
52
|
|
|
@@ -63,21 +62,13 @@ Inside this single plugin, the boundary between the two halves is: **this repo o
|
|
|
63
62
|
|
|
64
63
|
### 1. Install the plugin
|
|
65
64
|
|
|
66
|
-
|
|
67
|
-
dsh plugin --profile web add github:KyoMio/the gateway half
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
The package declares a `dsh.bundle` manifest; restart `dsh web` after installing.
|
|
71
|
-
|
|
72
|
-
Local-directory install (for hacking on the code yourself):
|
|
65
|
+
In the desktop app or the web UI: **Plugins** page in the sidebar → **Add plugin** → type `dsh-zen-remote` → **Enable now**. A headless web service can also use the command line:
|
|
73
66
|
|
|
74
67
|
```bash
|
|
75
|
-
|
|
76
|
-
cd the gateway half
|
|
77
|
-
dsh plugin --profile web add ./the gateway half
|
|
68
|
+
dsh plugin --profile web add dsh-zen-remote
|
|
78
69
|
```
|
|
79
70
|
|
|
80
|
-
|
|
71
|
+
The package declares a `dsh.bundle` manifest with a single row; restart DSH (the desktop app, or `dsh web`) after installing. The CLI refuses `--profile desktop` (the desktop app manages that profile itself); manual install steps live in the [root README](../README.md#install).
|
|
81
72
|
|
|
82
73
|
### 2. Put your own reverse proxy in front
|
|
83
74
|
|
|
@@ -232,14 +223,17 @@ From **cellular data** (not your home Wi-Fi), open `https://your-domain/lan-gate
|
|
|
232
223
|
|
|
233
224
|
### 3. Generate a pairing code and pair devices
|
|
234
225
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
226
|
+
Since 2.0.0 pairing codes are minted from the dsh-zen-remote settings block in the DSH Plugins page (the old `/lan-gate/admin` page is just a notice pointing there, still local-direct-only):
|
|
227
|
+
|
|
228
|
+
1. With the proxy in place, open the DSH Plugins page **on the host machine** and expand the dsh-zen-remote settings block.
|
|
229
|
+
2. Pick the device role (web app device / desktop app device) and click generate — an 8-character code, valid for 10 minutes, single-use. **The code is bound to the role**: a web-device code offered to the desktop channel is refused and vice versa; a wrong-channel code is neither consumed nor counted toward the lockout.
|
|
230
|
+
3. Web app device: on the phone or another computer, open your proxy's HTTPS domain in a browser — you'll land on the pairing page. Enter the code (device name is optional).
|
|
231
|
+
4. Desktop app device: in the other computer's DSH desktop app, open the plugin settings block, switch the role to **client**, and enter the server URL plus the code. The URL must be `https://` unless it is a private-network host — LAN ranges (`192.168.x`, `10.x`, `172.16–31.x`), Tailscale/CGNAT (`100.64.x`), loopback, `localhost` / `*.local`, or IPv6 `::1` / `fc00::/7` / `fe80::/10`.
|
|
232
|
+
5. On success a web device is dropped straight into the DSH Web UI (PWA-injected); identity is stored in a long-lived cookie, so switching Wi-Fi/IP never logs you out.
|
|
233
|
+
6. On the phone, use the browser menu's "Add to Home Screen" to get a standalone app.
|
|
234
|
+
7. The page will prompt you to enable "agent-done push" — grant notification permission and you'll get a system notification when the agent finishes, even from another app.
|
|
241
235
|
|
|
242
|
-
The
|
|
236
|
+
The settings block can also rename devices, change a device's role (`set-role`), and revoke one or all. Revocation is immediate: the device's open connections and push subscription die with it. Desktop-client devices can never reach any admin endpoint — the gateway only ever forwards them into the relay prefix; every other path (including DSH pages and `/api`) gets 403 (`reason: 'relay-only'`).
|
|
243
237
|
|
|
244
238
|
---
|
|
245
239
|
|
|
@@ -254,9 +248,10 @@ The admin page also lets you set a device's kind (phone / desktop / auto layout)
|
|
|
254
248
|
| `LAN_GATE_TRUSTED_PROXIES` | empty | Comma-separated IP list. When the proxy and gateway aren't on the same host (i.e. not a loopback socket), list the proxy's egress IP here so the gateway trusts the `X-Forwarded-For`/`X-Forwarded-Proto` it sends |
|
|
255
249
|
| `LAN_GATE_VAPID_SUBJECT` | `mailto:admin@localhost` | VAPID contact for Web Push. **Set this to a real mailto: address or https:// URL**: Apple rejects placeholder subjects with `403 BadJwtToken`, silently killing push to every iOS device (Google/Mozilla do not check). The gateway warns at startup if it looks invalid |
|
|
256
250
|
| `LAN_GATE_LANG` | `auto` | Language of the pages the gateway serves itself (pairing, rate-limit, admin) and of the push opt-in card it injects into the app. `auto` follows the request's `Accept-Language` — the only language signal a pairing visitor ever volunteers; with no such header it falls back to Chinese. `zh`/`en` pin it and ignore the browser |
|
|
257
|
-
| `
|
|
251
|
+
| `LAN_GATE_RELAY_SECRET` | *(auto-generated)* | The relay shared secret: since 2.0.0 it is how a desktop client's forwarded request proves to the host's relay routes that the marker headers were written by this gateway. **Minted fresh by the plugin on every load** of the main entry (32 random bytes handed to the gateway child through the environment, unconditionally overwriting) and **not externally settable** — a value hand-placed in the host environment is overwritten or deleted at child startup; when the plugin doesn't supply one (client role, standalone load) the header feature is off and the gateway stamps nothing |
|
|
252
|
+
| `DSH_PUSH_LANG` | `zh` | Language of the notification copy itself (approval pending / question pending / turn finished). Since 2.0.0 it is an env override over the settings `lang`; with neither set the copy is Chinese. Deliberately **not** autodetected: a notification is produced host-side, where there is no request header and launchd hands the process no `LANG` (`Intl` reports `en-US` even on a Chinese user's machine). Set `en` explicitly for English |
|
|
258
253
|
|
|
259
|
-
Besides env vars, the
|
|
254
|
+
Besides env vars, **the recommended surface is the dsh-zen-remote settings block in the DSH Plugins page** (the full field table lives in the root README); the legacy `~/.dsh/lan-gate.config.json` keeps working. Per field the first legal value wins in this order: **environment variable > plugin row settings > `lan-gate.config.json` > default** — a value that fails its field's check makes that layer transparent (a hand-edited `port: "abc"` surfaces the file's port, not an error). File keys = env var names minus the prefix, camelCased:
|
|
260
255
|
|
|
261
256
|
```json
|
|
262
257
|
{
|
|
@@ -267,30 +262,72 @@ Besides env vars, the **recommended way is the config file** `~/.dsh/lan-gate.co
|
|
|
267
262
|
}
|
|
268
263
|
```
|
|
269
264
|
|
|
270
|
-
|
|
265
|
+
The push half adds `pushTurnEnd` / `pushEvents` / `pushDebounceMs` / `pushSummary` / `pushTool`. Language is a single shared key, `lang`: the gateway understands `auto` (follow the browser) / `zh` / `en`, the push half understands only `en` and treats everything else as Chinese — so `"lang": "auto"` means "pages follow the browser, notifications stay Chinese". Two more keys live outside the settings surface: `pushApprovalGraceMs` (env `DSH_PUSH_APPROVAL_GRACE_MS`). The 2.0 fields `role` (`host`/`client` — the one 2.0 field the file layer also reads), `serverName`, `idleHours`, `autoShareNewSessions`, `serverUrl` and `deviceToken` come from the settings page or the plugin row, not this file.
|
|
271
266
|
|
|
272
|
-
|
|
267
|
+
Saving a field that needs a gateway/push restart (`role`, `port`, `host`, `targetPort`, `rateLimit`, `trustedProxies`, `vapidSubject`, `lang`, and the push fields) makes the plugin detect the change and reload its own row — the gateway child restarts with it, no app restart needed.
|
|
273
268
|
|
|
274
|
-
|
|
275
|
-
- insert:
|
|
276
|
-
- id: the gateway half-push
|
|
277
|
-
name: the gateway half/dsh-push.mjs
|
|
278
|
-
```
|
|
269
|
+
Since 2.0.0 the gateway and the push half **no longer take one row each**: the package's own `cordis.patch.yml` mounts a single `dsh-zen-remote` row, and the main entry loads the two sub-plugins itself with `ctx.plugin()`, per `role`, handing them the merged effective config. Stale `dsh-zen-remote-gateway` / `dsh-zen-remote-push` rows left in a profile patch are warned about and skipped by the loader — delete them; re-adding a gateway row by hand would spawn a second gateway process fighting over the port.
|
|
279
270
|
|
|
280
271
|
---
|
|
281
272
|
|
|
282
273
|
## Admin API
|
|
283
274
|
|
|
284
|
-
All of the following endpoints are **local-direct-connection only**: the request's socket must be a loopback address
|
|
275
|
+
All of the following endpoints are **local-direct-connection only**: the request's socket must be a loopback address, carry no `X-Forwarded-*` headers at all, and name a loopback host (see Architecture). On top of that judgment sit two more gates aimed at browsers (a cross-site POST from a web page needs no preflight, so the socket/Host judgment alone cannot stop one): any request carrying an `Origin` header gets 403, and any request with a body whose `Content-Type` is not `application/json` gets 415 — the plugin backend's proxied calls and a scripted `curl` (JSON header, no Origin) are unaffected. Anything that came through the proxy (which always carries forwarded headers) gets 403 — the public internet can never reach these. Since 2.0.0 the caller is the plugin backend, not a browser admin page: requests from the settings page arrive on the same-origin routes `/_dsh/zen-remote/admin/*` and the host process calls the gateway below **as** the local machine (forwarded requests wearing the `x-zen-remote-via` marker may read status but mutations are refused — the one exception is toggling a session's remote access on `/_dsh/zen-remote/admin/shares`, which a web app device (a phone) may do; the phone session info card's remote access switch uses it).
|
|
285
276
|
|
|
286
277
|
| Endpoint | Method | What | Params |
|
|
287
278
|
| --- | --- | --- | --- |
|
|
288
|
-
| `/lan-gate/pair` | POST | Generate a new one-time pairing code (valid 10 minutes) |
|
|
289
|
-
| `/lan-gate/status` | GET | Read running state, the current pairing code, the list of paired devices | none |
|
|
290
|
-
| `/lan-gate/action` | POST | Manage a device | `action`: `set-kind` / `rename` / `revoke` / `revoke-all`; `id`: device id (not needed for `revoke-all`); `set-kind` also needs `kind` (`phone`/`desktop`/`auto`); `rename` also needs `name` |
|
|
279
|
+
| `/lan-gate/pair` | POST | Generate a new one-time pairing code (valid 10 minutes). **The code is role-bound** | `role` (optional): `desktop-client` mints a desktop-app-device code; absent/unparsable means a web-device code. The response carries `role` |
|
|
280
|
+
| `/lan-gate/status` | GET | Read running state, the current pairing code, the list of paired devices (each with its `role`) | none |
|
|
281
|
+
| `/lan-gate/action` | POST | Manage a device | `action`: `set-role` / `set-kind` / `rename` / `revoke` / `revoke-all`; `id`: device id (not needed for `revoke-all`); `set-role` also needs `role` (`web`/`desktop-client` — switching to `desktop-client` deletes the device's push subscription); `set-kind` also needs `kind` (`phone`/`desktop`/`auto`); `rename` also needs `name` |
|
|
291
282
|
| `/pwa/push/send` | POST | Send one push to every subscribed device | `title`, `body` (plain text, no conversation content) |
|
|
292
283
|
|
|
293
|
-
The
|
|
284
|
+
The redemption endpoints are the exception reachable from anywhere, protected by the code itself (single-use, 10-minute TTL) and a failure lockout (5 wrong codes locks that IP for 15 minutes) — each channel accepting only its own role:
|
|
285
|
+
|
|
286
|
+
- `/lan-gate/pair/claim` (POST) — the browser pairing page, **web-device** codes only, issues a cookie on success;
|
|
287
|
+
- `/lan-gate/pair/claim-desktop` (POST) — desktop-client pairing, **desktop-device** codes only, the token travels in the JSON body (`{ok, id, name, token}`), never as a cookie.
|
|
288
|
+
|
|
289
|
+
A role mismatch answers 403 `role-mismatch` with a localized message; the wrong-channel code is not consumed and does not count toward the lockout.
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## The remote relay (desktop clients' data channel)
|
|
294
|
+
|
|
295
|
+
A desktop client has no DSH login cookie, and the relay deliberately does not forward DSH's native `/api` or WebSocket connections. Instead the client and the host speak zen-remote's own relay protocol: **every single call is a plain HTTP POST, every streaming subscription is an HTTP streaming response**, all under the host plugin's dedicated prefix `/_dsh/zen-remote/relay/`, entering and leaving through the gateway. No hand-rolled WebSocket; the client aborts the request to cancel a subscription.
|
|
296
|
+
|
|
297
|
+
**Three gates, re-checked per request:**
|
|
298
|
+
|
|
299
|
+
1. **Gateway device auth** — the request must carry a valid desktop-client device token (Bearer), and the gateway forwards desktop-client traffic into the relay prefix only;
|
|
300
|
+
2. **Marker headers + shared secret** — the gateway stamps every admitted forward with `x-zen-remote-via: gateway`, `x-zen-remote-role`, `x-zen-remote-device` and `x-zen-remote-secret` (the shared secret); client-forged copies of that namespace are stripped at the gateway's entrance. The relay routes re-verify the secret and the marker headers on every request — any mismatch is 401. Without the secret, any local process could hit `127.0.0.1` directly and forge the markers;
|
|
301
|
+
3. **A per-method allowlist** — every allowed method is registered with EXACTLY the argument fields that locate its session; authorization looks at those fields and nothing else, and any method not in the table is refused. This is deliberately **not** "scan the arguments for session ids": DSH's parameter validation silently drops unknown names, so a hostile client could pad a decoy shared id into a call whose real ownership field goes unchecked.
|
|
302
|
+
|
|
303
|
+
**Routes** (prefix `/_dsh/zen-remote/relay`):
|
|
304
|
+
|
|
305
|
+
| Route | Method | What |
|
|
306
|
+
| --- | --- | --- |
|
|
307
|
+
| `/ping` | GET | Liveness probe, `{ok:true}` |
|
|
308
|
+
| `/v1/handshake` | POST | Handshake: exchanges the relay protocol version, server id / display name, DSH version, and the interface fingerprint tables. A relay-protocol-version mismatch refuses the connection; the answer carries `serverName` (the server's display name) and `deviceName` (this device's name in the server's records) |
|
|
309
|
+
| `/v1/device/name` | POST | Device rename: **handled by the gateway itself, never forwarded to DSH** — open to paired desktop app devices only, and it renames only the CALLING device's own record; body `{"name":"…"}` (1–40 chars). Renames reach the other side through the stream heartbeat (~15s) and the handshake refresh (~30s) |
|
|
310
|
+
| `/v1/invoke` | POST | One DSH remote call on a shared session (single-shot, JSON in and out) |
|
|
311
|
+
| `/v1/stream` | POST | Opens one DSH stream subscription. **NDJSON format**: one JSON value per line; `{"type":"ping"}` heartbeat lines (every 15s by default) keep reverse proxies from timing idle streams away, and each one carries the current `serverName` / `deviceName` (a server rename reaches every connected client within one heartbeat); when a share closes or a subscription loses its session the server writes `{"type":"error","error":{"code":"unshared"}}` and ends the response |
|
|
312
|
+
| `/v1/event-result` | POST | The answer half for approval/question events (`{eventId, result}`); the server matches it against what it forwarded, per device, before handing it over |
|
|
313
|
+
| `/v1/http` | POST | The plain-HTTP long tail of the changes summary / diff panels: the `{route, query}` body admits only `changes.summary` / `changes.diff` and forwards GETs only (the query string is this call's coordinate, never a request body to forward); the query is **normalized and rebuilt** along an allowlist — only `sessionId` (exactly once, share-checked) and `seq` / `index` (at most once each, decimal non-negative integers) survive, unknown parameters are dropped, and the synthetic URL is composed from the normalized result alone (a control-character parsing differential between the two URL parsers was once an authorization bypass) — then dispatched **in-process** through the host's shared `/api` fetch handler, no loopback HTTP |
|
|
314
|
+
| `/v1/upload` | POST | The **non-image attachment** upload of a remote session: the request body is a raw byte stream (not JSON); the query string is normalized and rebuilt along an allowlist — only `sessionId` (exactly once, share-checked, 403 when not shared) and `name` (at most once) survive; the cap is 100 MiB, enforced twice (a `Content-Length` pre-check plus a counting pump over the stream), over-limit answers 413 (the refusal is written before the tail bytes are drained); at most 8 concurrent uploads per device (429 past that); then, like `/v1/http`, dispatched **in-process** through the host's shared `/api` handler — the staged receipt lands under the same session the later `session/prompt` resolves the attachment against |
|
|
315
|
+
| `/v1/unshare` | POST | The client closes a session's remote access itself (table members only) |
|
|
316
|
+
|
|
317
|
+
**The allowlist** (everything else is `forbidden-method`): session reads and writes (`session/follow`, `session/page`, `session/prompt`, `session/cancel`, `session/rename`, `session/selectModel`, `session/updateQueue`, `session/attachment`, `session/projections`), creation and forking (`session/create`, `session/fork` — results auto-share), subagents (`subagents/prompt`, `subagents/interruptByParent`, judged by the parent), attachments and @ references (`fileUploads/upload`, `fileReferences/list`, `sessionReferenceResolver/candidates`), the panel long tail (`goals/*` five verbs, `commands/list|execute`, `agentPresets/select`, `sessionFeedback/record`), file tree and previews (`workspaceFiles/list|changes|read|readBytes|stat`), terminals (`terminal/*`), jobs (`job/list|follow|kill`), `skills/list`, message feedback (`messageFeedback/*`), `schedule/list`, workspace session ops (`workspace/pinSession` and friends), plus four global reads: the `session/control` stream, `session/list`, the `workspace/follow` stream and `session/modelCatalog`. The first three carry no session argument — their safety is **output filtering**: every frame / every result row is narrowed by the share table before it is written, so an unshared session's row never reaches the client; in `session/list` result rows, a `parentSessionId` naming an inaccessible session is deleted as well (the rest of the row passes through unchanged). `session/modelCatalog` is likewise parameter-less and globally read-only — it is the server's model-provider directory, group and model ids plus display names only, no session data; the server empties the `failures` array in the result (the host's per-group error texts may carry endpoint or credential details the client never shows anyway). `sessionReferenceResolver/candidates` (the @-mention resolver) answers with EVERY server session's title, cwd and a ready-made mention, so it travels through the same row-level filtering — only shared sessions' rows leave the box. Approval/question events ride a `$zr/events` subscription, each forwarded event judged by its `agentId`.
|
|
318
|
+
|
|
319
|
+
The `/v1/http` route is a second allowlist, keyed by **route name** (not part of the method table), under the same discipline: an unregistered route is 404 before any id is read, and a registered one is judged only along its declared session parameter. The client-side counterpart is `GET /_dsh/zen-remote/client/http/<route>` — the browser fetch wrapper redirects the `/api/changes.*` calls it intercepted (virtual session ids) here, the original id is restored before the relay round-trip, and the upstream Content-Type is held to JSON — anything else downgrades to `text/plain` + `nosniff`.
|
|
320
|
+
|
|
321
|
+
`/v1/upload` starts on the **sub-client's own machine**: DSH's non-image attachment upload rides a request issued from inside a Web Worker, which neither a `window.fetch` wrapper nor the typert interception can see, so the plugin instead wraps the upload entry (`/api/session/uploadFileBinary`) of the host connection service's `fetchRoutes` table in the local backend and catches the request there — an upload carrying a virtual session id streams through the relay to the server (a local id passes through untouched, its body never read by us), an over-cap upload is refused locally on the client, slow links are bounded by the client-side 300-second timeout, and an upload's timeout or transport failure never moves the connection state. A local failure is still answered in the host's own "200 + failure envelope" shape, so the readable message lands on the attachment card. Export (`/api/session.export`) is the other entry wrapped in the same pass: an export naming a virtual id is refused in the backend with 403 `remote-unsupported` (the UI menu item is hidden remotely too).
|
|
322
|
+
|
|
323
|
+
**Security boundary of the relay:**
|
|
324
|
+
|
|
325
|
+
- The server executes everything through the DSH gateway service's public invoke/stream methods, under the operator identity — **it never replaces DSH internals**;
|
|
326
|
+
- Subagent and fork sessions never enter the table; reachability walks the ancestor chain (a shared ancestor shares the family);
|
|
327
|
+
- `dsh-session:` references are scanned too: DSH injects the referenced session's content along **messages** (the content text blocks of `session/prompt` / `subagents/prompt`), **queue edits** (`session/updateQueue`'s edit content) and **slash commands** (EVERY string of a `commands/execute` call — a command handler steers its raw input in as the next user message), with no access check of its own at injection time, so a call whose reference names an unshared session is refused server-side with 403; the @-mention candidates (`sessionReferenceResolver/candidates`) likewise only ever list shared sessions' rows;
|
|
328
|
+
- Terminals are deliberate (a desktop client is a trusted device): what opens is a **server-side** PTY running as the server user, without the agent sandbox or approval restrictions; closing the session's remote access kills its terminal streams with it;
|
|
329
|
+
- `workspaceFiles/read` and friends are not contained to the session directory (DSH itself does not contain them) — anything the server process can read is readable, under the same trusted-device premise as terminals;
|
|
330
|
+
- Concurrent streams are budgeted per device (32), and so are invokes kept in flight at once — past the per-device cap the invoke answers 429 `too-many-invokes`. Error codes shaped `*/internal` (`gateway/internal` and friends) are returned as the bare code with no message — that kind of message quotes server-side internals. Event answers are matched against the subscription and device they were forwarded to. Approval/question forwarding is **first come, first served**: whichever of the server's own UI and the sub-client answers first wins; the later answer is refused by the gateway and the client syncs to "already handled". The relay **never answers on the client's behalf**: an unshared session's approval/question events are not forwarded at all, and with the server's own UI offline too, the approval simply keeps waiting — the gateway re-delivers still-pending events to the next `$events` subscriber. A wait, never a rejection (an abstention would fail every pending approval of the session at once and break the push → wake → approve flow). Notification-type events (EMIT frames: session-list summaries, account expirations and other server-wide state) of an unshared session are never forwarded.
|
|
294
331
|
|
|
295
332
|
---
|
|
296
333
|
|
|
@@ -310,14 +347,18 @@ The exception is `/lan-gate/pair/claim` (POST) — the one endpoint reachable fr
|
|
|
310
347
|
|
|
311
348
|
**What's covered:**
|
|
312
349
|
- Pairing-code brute force — the code is single-use with a 10-minute TTL, and 5 wrong attempts locks that source IP for 15 minutes.
|
|
313
|
-
-
|
|
314
|
-
-
|
|
315
|
-
-
|
|
350
|
+
- Role-bound codes — a web-device code cannot redeem through the desktop channel and vice versa; wrong-channel attempts neither consume the code nor count toward the lockout.
|
|
351
|
+
- Revocable tokens — lost device, lent-out machine, one click in the settings block and it stops working immediately (open connections, established HTTP relay streams and push subscriptions all die with it).
|
|
352
|
+
- Desktop clients are fenced into the relay prefix — every other path (DSH pages, `/api` included) gets 403.
|
|
353
|
+
- The relay's three gates — device token, gateway marker headers + the per-load shared secret (`LAN_GATE_RELAY_SECRET`, not externally settable), and the per-method allowlist with output filtering; an unshared session's data never leaves the box.
|
|
354
|
+
- Request volume — **unpaired** requests are rate-limited per resolved real client IP, 120/min by default, 429 past that; paired devices are exempt (desktop clients have the relay's own concurrency caps instead).
|
|
355
|
+
- The admin surface is local-only — generating pairing codes, managing devices, triggering pushes: all local-direct only (loopback socket, no forwarded headers, loopback Host), and anything through the proxy (always carries forwarded headers) gets 403; so do requests carrying an `Origin` header or a non-JSON body (403/415) — DNS rebinding and cross-site simple requests cannot get in; a forwarded request wearing the gateway marker may read but not mutate (toggling a session's remote access excepted, see above).
|
|
316
356
|
|
|
317
357
|
**What's not covered — your responsibility:**
|
|
318
358
|
- A misconfigured reverse proxy — e.g. accidentally exposing `127.0.0.1:3088/lan-gate/admin` on the public domain too, or a wrong `X-Forwarded-Proto` making the gateway misjudge the client's protocol. These are configuration mistakes the gateway can't defend against.
|
|
319
|
-
- A stolen or shared token — this is a single-user tool; the token is equivalent to full access, with no finer-grained permission tiers. Whoever has the token can use it — if you suspect a leak, revoke it and re-pair from the
|
|
320
|
-
-
|
|
359
|
+
- A stolen or shared token — this is a single-user tool; the token is equivalent to full access, with no finer-grained permission tiers. Whoever has the token can use it — if you suspect a leak, revoke it and re-pair from the settings page.
|
|
360
|
+
- **A desktop client is a trusted device** — its terminal runs as the server user without the agent sandbox or approval restrictions, file previews are not contained to the session directory, and it can create sessions in any server workspace. "Only shared sessions are visible" constrains session data, not the machine; giving a pairing code to a computer you don't trust is handing it the machine.
|
|
361
|
+
- DSH's own capability boundary — the gateway only forwards traffic to DSH safely; it can't and doesn't add security measures DSH itself doesn't have (DSH's own `/api` trust fence is DSH's concern).
|
|
321
362
|
- The state file `~/.dsh/lan-gate-state.json` stores the VAPID private key and every device's token in plaintext — this file *is* full access to your gateway. Mind its file permissions on the host, and don't sync `~/.dsh` into a shared drive or an untrusted backup location.
|
|
322
363
|
|
|
323
364
|
---
|
|
@@ -359,8 +400,8 @@ npm test # boots a mock upstream, runs the gateway/auth/push suites: proxy+inj
|
|
|
359
400
|
|
|
360
401
|
| Path | Role |
|
|
361
402
|
| --- | --- |
|
|
362
|
-
| `lan-gate.mjs` |
|
|
363
|
-
| `dsh-push.mjs` |
|
|
403
|
+
| `lan-gate.mjs` | Gateway sub-plugin entry, loaded by the main entry on the host role: spawns the gateway child process and manages its lifecycle |
|
|
404
|
+
| `dsh-push.mjs` | Push sub-plugin (loaded alongside the gateway on the host role): listens on the DSH event bus, calls the gateway's local `/pwa/push/send`, and registers the `push_notify` model tool |
|
|
364
405
|
| `lib/lan-gate-server.cjs` | The gateway itself: single-file CommonJS (Node stdlib + one runtime dependency, `web-push`) — HTTP/WebSocket reverse proxy, pairing/tokens, rate limiting, PWA injection, Web Push |
|
|
365
406
|
| `pwa/manifest.json` | PWA install manifest |
|
|
366
407
|
| `pwa/sw.js` | Service worker (offline caching + push notifications) |
|