@linxin666/dsh-remote-web-ui 0.3.22 → 0.3.23
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/README.i18n.yaml +2 -2
- package/README.md +4 -0
- package/README.zh.md +4 -0
- package/lib/client.js +45 -2
- package/lib/client.js.map +1 -1
- package/lib/index.js +138 -24
- package/lib/types/client/remote-channel.d.ts +35 -0
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +55 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +26 -22
- package/lib/types/public-base.d.ts +67 -0
- package/lib/types/public-base.d.ts.map +1 -0
- package/lib/types/public-base.js +110 -0
- package/lib/types/remote-channel-boot.d.ts +5 -2
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +28 -2
- package/lib/types/remote-channel-rules.d.ts +4 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -1
- package/lib/types/remote-channel-rules.js +2 -0
- package/package.json +1 -1
- package/src/client/remote-channel.ts +64 -0
- package/src/index.ts +26 -22
- package/src/public-base.ts +122 -0
- package/src/remote-channel-boot.ts +28 -2
- package/src/remote-channel-rules.ts +6 -0
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:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 0b2dd290c6a1f829ffc74c5fb465b4afe860f34a
|
|
6
|
+
README.zh.md: 174d0ccc8b1c5d7ff5d98f9301b6b9a016180577
|
package/README.md
CHANGED
|
@@ -66,6 +66,8 @@ Restart the profile (`dsh web`), then open the phone icon in the sidebar foot. T
|
|
|
66
66
|
|
|
67
67
|
## Remote access over the internet (tunnels)
|
|
68
68
|
|
|
69
|
+
A tunnel process restart — most often a mobile network dropping and reconnecting — used to drop the public host from the pairing fence the moment the tunnel left `running`, so the address still printed in the QR code answered 403, including on the deliberately public `/api/pair/status` (issue #1547). A named tunnel's fixed hostname and a registered relay origin never change, so both now stay trusted throughout the reconnect; a quick tunnel's previous host stays trusted for a 60 s grace window, because the edge may still be delivering a connection the phone already opened, and is dropped only if the tunnel has not come back by then.
|
|
70
|
+
|
|
69
71
|
### One-click public tunnel (recommended)
|
|
70
72
|
|
|
71
73
|
Set **自动公网隧道** (autoTunnel) in the settings card. The plugin runs its own Cloudflare quick tunnel (`cloudflared` ships with the package), feeds the minted public URL into the QR base and the pairing fence dynamically, and keeps the posture probe informed — a phone anywhere can pair at any time. The manual public address and the named-tunnel token below are ignored while this is on. The minted hostname is ephemeral and changes on every `dsh web` restart — the stable-hostname relay below keeps the phone's origin fixed across those restarts.
|
|
@@ -134,6 +136,7 @@ Pinned to the 0.1.2-alpha.2 line; the seams this build relies on:
|
|
|
134
136
|
- **`sidebar.footer.action` foot seat** (the 0.1.2 shell composition): the sidebar declares and renders the seat the remote entry occupies.
|
|
135
137
|
- **`ctx.layout.toggleSidebar()`** (packages/client/ui-layout): the whale button expands the collapsed sidebar through the official panel-action face.
|
|
136
138
|
- **`ctx.connection.authenticatedUrl()`** (packages/client/connection): the sanctioned launch-token seam the proxy redeems once for its inner credential (`src/inner-auth.ts`), so re-issued `/api` calls satisfy the harness browser-auth check.
|
|
139
|
+
- **`__DSH_FILE_UPLOAD__`** (file-upload client hooks): the optional pre-Cordis transport the upload service reads once at construction. The remote boot patch publishes it so background uploads stay on the rewritten main-thread fetch instead of a Web Worker that escapes the channel (issue #1580).
|
|
137
140
|
- **`__DSH_TRANSPORT__.ownsHost`** (client-connection transport hooks): the host-mode flip for paired remote desktops. There is no host-side per-method privilege pin on this line — the configuration plane branches on `connection.isLoopback` in the client — and no `api/gate` waterfall either (the gate listener stays mounted for deployments that gain the seam; pairing enforcement lives on this plugin's own `/remote` channel).
|
|
138
141
|
- **User-patch bind semantics**: same-id patch rows replace the row config wholesale, and the user patch layer cannot evaluate `webStartup`-dependent `!!js` expressions reliably — the LAN bind block therefore materializes static values and the plugin re-asserts it every boot.
|
|
139
142
|
|
|
@@ -158,6 +161,7 @@ The public path is the same round trip through a tunnel (see "Remote access over
|
|
|
158
161
|
- **Cohort reality: pairing does not gate direct `/api`.** On the pinned 0.1.2-alpha.2 line nothing emits the `api/gate` seam, so a direct `/api` call from a LAN origin is governed solely by the harness fence (which auto-trusts LAN literals under a `0.0.0.0` bind) plus the harness browser-auth cookie. A browser credential a device has already redeemed therefore survives 停止/取消配对 until its natural expiry (30 days) — revocation binds the `/remote` channel and the pairing cookie, not that credential. The plugin probes the `/api` posture and warns loudly; treat a LAN-exposed bind as a deliberate decision, and prefer loopback plus tunnels when the machine is shared.
|
|
159
162
|
- **A paired device is a full-control credential.** With host mode active it reaches the complete host API — chat, sessions, settings, credentials, agent presets, deliverables — mirroring the SDK's own stance that a loopback desktop is trusted. Only the three control planes (pairing, self-update, plugin install/remove) stay physically local. Only pair devices you control; 停止 or per-device 取消配对 revokes immediately.
|
|
160
163
|
- **Control endpoints stay loopback-only**: mint/stop/revoke, the device roster, the lan-bind status, and the update endpoints answer only to loopback. A LAN-origin browser sees the "配对面板仅限本机使用" banner.
|
|
164
|
+
- **Background file uploads ride the channel too.** The official upload service prefers a Web Worker carrier whose own globals no main-thread patch reaches, so the boot patch (and the browser patch as a fallback) publishes the official pre-Cordis hook `__DSH_FILE_UPLOAD__` and hands it the patched `fetch`: the raw `/api/session/uploadFileBinary` POST is rewritten onto `/remote` and carries the device credential exactly like every other gated call. Without the hook a paired browser's uploads bypass the channel and the harness browser-auth fence answers 401 (issue #1580). The hook is only published on non-loopback origins, only while the channel is installed, and never overwrites a hook the page already owns.
|
|
161
165
|
- **The app landing is cookieless-optional.** After pairing, the QR lands the device on `/pair-app`, served by this plugin: the official shell is delivered without passing the harness index gate, and the device credential rides a `x-dsh-remote-device` header (fetch) / `device` query (WebSocket upgrades) that the boot patch attaches from sessionStorage. The flow therefore works when the device browser blocks all cookies; the pairing cookie remains the primary credential where the browser stores it, and the harness browser-auth cookie is no longer required on the phone path at all.
|
|
162
166
|
- **Reopens are owned by a service worker (https origins).** A paired phone returning from history, a bookmark, or tab restore navigates to bare `/` — a path the plugin does not own, where the harness fallback seat answers with its browser-auth 401 (the cookieless flow never holds that credential). The app shell therefore registers `/pair-app.sw.js` (fenced like `/pair-app`; the script is inert logic with no secrets): it intercepts navigations to `/` only, re-serves the shell network-first through `/pair-app` — which re-validates the device cookie and refreshes its presence, so every reopen also keeps the session alive — falls back to the cached shell offline, and passes the navigation through when the plugin no longer answers (a revoked device then sees the harness response or the bilingual re-scan page). Plain-HTTP LAN origins are not secure contexts and never register the worker; there a reopen means scanning a fresh QR.
|
|
163
167
|
- **Revocation is per-request**: a paired device whose request is already in flight when 停止 lands completes that request; the next one 403s.
|
package/README.zh.md
CHANGED
|
@@ -66,6 +66,8 @@ dsh plugin --profile web add link:$(pwd)/packages/dsh-remote-web-ui
|
|
|
66
66
|
|
|
67
67
|
## 通过互联网远程访问(隧道)
|
|
68
68
|
|
|
69
|
+
隧道进程重启(最常见的原因是移动网络闪断重连)过去会让插件在隧道离开 `running` 的瞬间把公网主机移出配对栅栏,于是二维码上仍显示着的地址会返回 403——连有意公开的 `/api/pair/status` 也不例外(issue #1547)。命名隧道的固定主机名与已注册的中继源本来就不会变,现在在整个重连期间保持受信任;快速隧道的上一个地址保留 60 秒宽限期(边缘可能仍在投递手机已经建立的连接),只有到期仍未恢复才彻底失效。
|
|
70
|
+
|
|
69
71
|
### 一键公网隧道(推荐)
|
|
70
72
|
|
|
71
73
|
在设置卡片打开**自动公网隧道**(autoTunnel)。插件运行自己的 Cloudflare 快速隧道(`cloudflared` 随包分发),把铸造的公网 URL 动态喂给二维码与配对栅栏,并同步姿态探测——手机随时随地都能配对。开启期间忽略下方手动公网地址与命名隧道令牌。铸造的域名是临时的,每次 `dsh web` 重启都会变化——下方的固定域名中继会把手机的访问源钉在固定地址上,重启不再需要重新扫码。
|
|
@@ -134,6 +136,7 @@ pnpm run build
|
|
|
134
136
|
- **`sidebar.footer.action` 底部席位**(0.1.2 shell 组合):侧栏声明并渲染远程入口占据的席位。
|
|
135
137
|
- **`ctx.layout.toggleSidebar()`**(packages/client/ui-layout):鲸鱼按钮经官方面板动作面展开折叠侧栏。
|
|
136
138
|
- **`ctx.connection.authenticatedUrl()`**(packages/client/connection):代理为内部凭据一次性兑换启动令牌的官方接缝(`src/inner-auth.ts`),使再发起的 `/api` 调用满足 harness 浏览器认证校验。
|
|
139
|
+
- **`__DSH_FILE_UPLOAD__`**(file-upload 客户端钩子):上传服务在构造时读取一次的可选启动前传输。远程引导补丁会发布它,使后台上传留在被改写的主线程 fetch 上,而不是逃出通道的 Web Worker(issue #1580)。
|
|
137
140
|
- **`__DSH_TRANSPORT__.ownsHost`**(client-connection 传输钩子):配对远程桌面的 host 模式翻转。本线没有 host 侧按方法特权锁定——配置面在客户端按 `connection.isLoopback` 分支——也没有 `api/gate` 瀑布(gate 监听器保持挂载,待未来部署获得该接缝;配对强制在插件自己的 `/remote` 通道上)。
|
|
138
141
|
- **用户补丁绑定语义**:同 id 补丁行整行替换 config,且用户补丁层无法可靠求值依赖 `webStartup` 的 `!!js` 表达式——局域网绑定块因此落静态值,插件每次启动重断言。
|
|
139
142
|
|
|
@@ -158,6 +161,7 @@ pnpm run build
|
|
|
158
161
|
- **本 cohort 的现实:配对不门控直连 `/api`。** 在锚定的 0.1.2-alpha.2 线上,没有任何组件发出 `api/gate` seam,因此来自局域网源头的直连 `/api` 仅由 harness 围栏(`0.0.0.0` 绑定下自动信任局域网字面量)加 harness 浏览器认证 cookie 约束。设备已经兑换过的浏览器凭据在停止/取消配对后仍然有效,直到其自然过期(30 天)——撤销约束的是 `/remote` 通道与配对 cookie,而不是那个凭据。插件会对 `/api` 姿态做探测并大声告警;请把局域网绑定当作深思熟虑的决定,在共享机器上优先回环加隧道。
|
|
159
162
|
- **配对设备是完全控制凭据。** host 模式下它可达完整 host API——聊天、会话、设置、凭据、Agent 预设、产出物——与 SDK 对回环桌面的信任一致。只有三个控制面(配对、自更新、插件安装/卸载)保持物理本地。只配对你控制的设备;停止或逐设备取消配对立即撤销。
|
|
160
163
|
- **控制端点仅限回环**:铸造/停止/撤销、设备列表、lan-bind 状态与更新端点只应答回环。局域网源浏览器看到「配对面板仅限本机使用」横幅。
|
|
164
|
+
- **后台文件上传同样走通道。** 官方上传服务优先使用 Web Worker 载体,其独立全局对象不受主线程补丁影响;因此引导补丁(以及作为兜底的浏览器补丁)会发布官方启动前钩子 `__DSH_FILE_UPLOAD__`,并把打过补丁的 `fetch` 交给它:原始 `/api/session/uploadFileBinary` POST 会被改写到 `/remote`,并像其他受门控调用一样携带设备凭据。没有该钩子时,配对浏览器的上传会绕过通道,被 harness 浏览器认证围栏拒为 401(issue #1580)。该钩子仅在非回环源、且通道安装期间发布,且永不覆盖页面已有的钩子。
|
|
161
165
|
- **应用落地页不依赖 cookie。** 配对后二维码把设备带到 `/pair-app`——由本插件直接交付官方应用壳,不经过 harness 索引认证门;设备凭据经 `x-dsh-remote-device` 请求头(fetch)与 `device` 查询参数(WebSocket 升级)由引导补丁从 sessionStorage 挂载。因此手机浏览器完全禁用 cookie 时链路依然成立;有 cookie 时配对 cookie 仍是主凭据,手机路径不再需要 harness 浏览器认证 cookie。
|
|
162
166
|
- **重开由 service worker 接管(仅 https 源)。** 配对过的手机从历史、书签或标签恢复回来时导航到裸 `/`——插件不拥有的路径,harness 兜底座会用浏览器认证 401 应答(不依赖 cookie 的流程永远拿不到那份凭据)。应用壳因此注册 `/pair-app.sw.js`(与 `/pair-app` 同一栅栏;脚本是不含任何秘密的惰性逻辑):只拦截对 `/` 的导航,经 `/pair-app` 网络优先地重发应用壳——同时校验设备 cookie 并刷新其活跃时间,每次重开也在为会话续期——离线时回退缓存的壳,插件不再应答时把导航原样放行(被撤销的设备随后看到 harness 应答或双语重扫页)。纯 HTTP 的局域网源不是安全上下文,永远不会注册该 worker;那里的重开意味着重新扫码。
|
|
163
167
|
- **撤销按请求生效**:停止落地时已在途的请求会完成;下一个请求 403。
|
package/lib/client.js
CHANGED
|
@@ -4070,7 +4070,9 @@ window.__ModuleLoader__.load({
|
|
|
4070
4070
|
],
|
|
4071
4071
|
deviceHeader: REMOTE_DEVICE_HEADER,
|
|
4072
4072
|
deviceKey: "dsh-remote-device",
|
|
4073
|
-
deviceQuery: REMOTE_DEVICE_QUERY
|
|
4073
|
+
deviceQuery: REMOTE_DEVICE_QUERY,
|
|
4074
|
+
uploadPath: "/api/session/uploadFileBinary",
|
|
4075
|
+
uploadHookGlobal: "__DSH_FILE_UPLOAD__"
|
|
4074
4076
|
};
|
|
4075
4077
|
/** The window global the boot patch publishes its seat under. */
|
|
4076
4078
|
const REMOTE_CHANNEL_BOOT_GLOBAL = "__DSH_REMOTE_CHANNEL_BOOT__";
|
|
@@ -4218,6 +4220,7 @@ window.__ModuleLoader__.load({
|
|
|
4218
4220
|
const originalFetch = window.fetch;
|
|
4219
4221
|
const OriginalWebSocket = window.WebSocket;
|
|
4220
4222
|
const OriginalEventSource = window.EventSource;
|
|
4223
|
+
const restoreUploadHook = installFileUploadHook(window);
|
|
4221
4224
|
const sameOrigin = (url) => url.origin === window.location.origin;
|
|
4222
4225
|
const rewrite = (raw) => rewriteRawUrl(raw, window.location.href, window.location.origin);
|
|
4223
4226
|
const device = (() => {
|
|
@@ -4303,6 +4306,46 @@ window.__ModuleLoader__.load({
|
|
|
4303
4306
|
window.WebSocket = OriginalWebSocket;
|
|
4304
4307
|
if (OriginalEventSource !== void 0) window.EventSource = OriginalEventSource;
|
|
4305
4308
|
for (const restore of restoreSrc) restore();
|
|
4309
|
+
restoreUploadHook();
|
|
4310
|
+
};
|
|
4311
|
+
}
|
|
4312
|
+
/**
|
|
4313
|
+
* Publish the official pre-Cordis upload hook so background uploads keep
|
|
4314
|
+
* riding the patched main-thread fetch (issue #1580).
|
|
4315
|
+
*
|
|
4316
|
+
* `@deepseek-ai/dsh-client-file-upload` reads `globalThis.__DSH_FILE_UPLOAD__`
|
|
4317
|
+
* once when its runtime is constructed; without it the carrier is a Web
|
|
4318
|
+
* Worker, whose own globals no main-thread patch reaches. That worker's XHR
|
|
4319
|
+
* goes straight to `<origin>/api/session/uploadFileBinary` with neither the
|
|
4320
|
+
* `/remote` rewrite nor the cookieless device credential, so the harness
|
|
4321
|
+
* browser-auth fence answers 401 and every upload from a paired browser
|
|
4322
|
+
* fails. Rewriting the worker URL cannot fix it: a worker context carries
|
|
4323
|
+
* neither the pairing cookie nor the device header.
|
|
4324
|
+
*
|
|
4325
|
+
* The hook hands the runtime the same transport the rest of the page uses -
|
|
4326
|
+
* the boot script publishes an identical one (remote-channel-boot.ts), and
|
|
4327
|
+
* this is the fallback for pages served without it.
|
|
4328
|
+
*
|
|
4329
|
+
* @param window - the browser window (or a test double), BEFORE the channel patch.
|
|
4330
|
+
* @returns a function retiring the hook (a pre-existing one is left alone).
|
|
4331
|
+
*/
|
|
4332
|
+
function installFileUploadHook(window) {
|
|
4333
|
+
if (window.__DSH_FILE_UPLOAD__ !== void 0) return () => {};
|
|
4334
|
+
const originalFetch = window.fetch;
|
|
4335
|
+
const hook = { fetch: (input, init) => {
|
|
4336
|
+
const raw = typeof input === "string" ? input : input.href;
|
|
4337
|
+
let url;
|
|
4338
|
+
try {
|
|
4339
|
+
url = new URL(raw, window.location.href);
|
|
4340
|
+
} catch {
|
|
4341
|
+
return originalFetch.call(window, input, init);
|
|
4342
|
+
}
|
|
4343
|
+
if (url.origin === window.location.origin && url.pathname === RULES.uploadPath) return window.fetch.call(window, raw, init);
|
|
4344
|
+
return originalFetch.call(window, input, init);
|
|
4345
|
+
} };
|
|
4346
|
+
window.__DSH_FILE_UPLOAD__ = hook;
|
|
4347
|
+
return () => {
|
|
4348
|
+
if (window.__DSH_FILE_UPLOAD__ === hook) delete window.__DSH_FILE_UPLOAD__;
|
|
4306
4349
|
};
|
|
4307
4350
|
}
|
|
4308
4351
|
/**
|
|
@@ -4453,7 +4496,7 @@ window.__ModuleLoader__.load({
|
|
|
4453
4496
|
/** The building package's version, when the bundle carries it. */
|
|
4454
4497
|
function bakedVersion() {
|
|
4455
4498
|
try {
|
|
4456
|
-
return "0.3.
|
|
4499
|
+
return "0.3.23";
|
|
4457
4500
|
} catch {
|
|
4458
4501
|
return;
|
|
4459
4502
|
}
|