@linxin666/dsh-remote-web-ui 0.4.1 → 0.4.2

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 (46) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +1 -1
  3. package/README.zh.md +1 -1
  4. package/lib/client.js +132 -29
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +117 -15
  7. package/lib/types/client/RemoteEntry.js +1 -1
  8. package/lib/types/client/index.d.ts.map +1 -1
  9. package/lib/types/client/index.js +5 -3
  10. package/lib/types/client/locales.js +2 -2
  11. package/lib/types/client/pair-api.d.ts +4 -0
  12. package/lib/types/client/pair-api.d.ts.map +1 -1
  13. package/lib/types/client/pair-api.js +11 -7
  14. package/lib/types/client/remote-channel.d.ts +27 -7
  15. package/lib/types/client/remote-channel.d.ts.map +1 -1
  16. package/lib/types/client/remote-channel.js +25 -17
  17. package/lib/types/client/update-api.js +2 -2
  18. package/lib/types/lan-bind.d.ts +4 -0
  19. package/lib/types/lan-bind.d.ts.map +1 -1
  20. package/lib/types/lan-bind.js +64 -1
  21. package/lib/types/pairing.d.ts +15 -2
  22. package/lib/types/pairing.d.ts.map +1 -1
  23. package/lib/types/pairing.js +26 -14
  24. package/lib/types/pairing.persist.test.js +43 -3
  25. package/lib/types/remote-channel-boot.d.ts.map +1 -1
  26. package/lib/types/remote-channel-boot.js +13 -3
  27. package/lib/types/remote-channel-rules.d.ts +74 -0
  28. package/lib/types/remote-channel-rules.d.ts.map +1 -1
  29. package/lib/types/remote-channel-rules.js +104 -0
  30. package/lib/types/update.d.ts +10 -0
  31. package/lib/types/update.d.ts.map +1 -1
  32. package/lib/types/update.js +32 -3
  33. package/package.json +20 -19
  34. package/src/client/RemoteEntry.tsx +1 -1
  35. package/src/client/index.ts +5 -3
  36. package/src/client/locales.ts +2 -2
  37. package/src/client/pair-api.ts +11 -7
  38. package/src/client/remote-channel.ts +38 -15
  39. package/src/client/remote.module.css +18 -0
  40. package/src/client/update-api.ts +2 -2
  41. package/src/lan-bind.ts +62 -1
  42. package/src/pairing.persist.test.ts +47 -3
  43. package/src/pairing.ts +26 -12
  44. package/src/remote-channel-boot.ts +13 -3
  45. package/src/remote-channel-rules.ts +113 -0
  46. package/src/update.ts +36 -3
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: 6b44be832e6723fa9e75ecb1555e6e423b26adc5
6
- README.zh.md: 42bbfd2dc3b12a7d17987b86f260679af14fcd84
5
+ README.md: ed7f727826fd6d69a02c2fbb6ebacf0594c9ac33
6
+ README.zh.md: d76b98c19fb7c8996e8e499f912f7f71bd6a8163
package/README.md CHANGED
@@ -11,7 +11,7 @@ This repository is an external plugin package for DeepSeek Harness (DSH). It is
11
11
  - **Panel**: "Remote access" title, a "Pair a device" card with the status area ("Waiting for a device" + status badge), a large QR code, the pairing link with its copy button, Stop / Refresh QR actions, and the authorized-device roster (a device name inferred from User-Agent, online/offline, last active time, per-device unpair). Credential-bearing device ids and raw User-Agent values are never rendered. Both the phone and the PC use the same time-limited link: until it expires or the QR is refreshed, that link can pair several devices, each with its own device session.
12
12
  - **Phone side**: scanning the QR binds the phone with a time-limited token and reloads into the **official Web GUI** — there is no second interface to drift from. While the phone is in portrait, the plugin injects a touch-adaptation layer over the running UI (see "The mobile adaptation layer" below). The accept chain is cookieless (`/pair-accept` → `/pair-app`): the plugin itself serves the official shell, so the phone needs no harness browser-auth cookie at all. A reopen service worker (https origins) then owns later navigations to `/` from history, bookmarks, or tab restore, so the phone reopens straight into the app instead of a 401 dead end (see Security model).
13
13
  - **PC side**: the same link opens the full desktop Web GUI on another computer over the gated `/remote` channel; unpaired PCs see a guided blocking page (with a manual pair-token input) and no workspace data behind it.
14
- - **Security**: one active token at a time (a refresh invalidates the old link; the link stays re-usable within its expiry window so a scan handed between browsers can complete pairing; it expires). 停止 revokes every paired device and the current token — the `/remote` channel cuts them off on their next request. Pairing is this plugin's access control for the `/remote` channel; direct `/api` on a LAN-exposed bind is governed by the harness fence + browser auth (see Security model). Loopback (127.0.0.1) keeps using `/api` directly. A paired device is a **full-control credential** (see Security model).
14
+ - **Security**: one active token at a time (a refresh invalidates the old link; the link stays re-usable within its expiry window so a scan handed between browsers can complete pairing; it expires). 停止 revokes every paired device and the current token — the `/remote` channel cuts them off on their next request. Pairing is this plugin's access control for the `/remote` channel; direct `/api` on a LAN-exposed bind is governed by the harness fence + browser auth (see Security model). Loopback (127.0.0.1) keeps using `/api` directly, and so does a page an application on this machine delivered — the desktop shell's `dsh-app://app/` or a `file:` page — because a pairing link is reachable only over the network. A paired device is a **full-control credential** (see Security model).
15
15
  - **LAN bind toggle**: the settings card writes a managed block into the profile `cordis.patch.yml` that pins the webserver bind to `0.0.0.0` (on) or `127.0.0.1` (off) — no `--host` command-line dance; an explicit `--host`/`--port` flag still wins. It maintains the matching host firewall rule (Windows Defender via netsh; Linux firewalld/ufw/iptables; other platforms report the firewall as unmanaged) and shows the live bind, the reachable LAN URLs, and the firewall state.
16
16
  - **Live status**: the desktop badge flips to 已连接 in real time; an `/api` posture probe reports any host whose `/api` fence the SDK leaves open, and the auto-tunnel state appears on the panel while the tunnel is starting.
17
17
  - **One-click self-update**: the sidebar download trigger checks for a newer dsh-web release after load, marks the button when one exists, and runs the verified update (release notes shown in the panel).
package/README.zh.md CHANGED
@@ -11,7 +11,7 @@
11
11
  - **面板**:「远程访问」标题、「设备配对」卡片(状态区「等待设备连接」+ 状态徽标)、大二维码、带复制按钮的配对链接、停止 / 刷新二维码操作,以及已授权设备列表(按 User-Agent 推断的设备名、在线/离线、最近活动时间、逐设备取消配对)。承载凭据的设备 id 与原始 User-Agent 值绝不渲染。手机与电脑共用同一条限时链接:在令牌过期或刷新二维码之前,该链接可为多台设备完成配对,每台各自生成独立设备会话。
12
12
  - **手机侧**:扫码后以限时令牌绑定并重载进入**官方 Web GUI**——不存在会漂移的第二套界面。手机竖屏时插件在运行中的界面上注入触控适配层(见下文)。接受链路不依赖 cookie(`/pair-accept` → `/pair-app`):官方应用壳由插件直接交付,手机全程不需要 harness 浏览器认证 cookie。随后由重开 service worker(https 源)接管此后对 `/` 的导航——来自历史、书签或标签恢复的重开直接回到应用,而不是 401 死路(见安全模型)。
13
13
  - **电脑侧**:同一链接在另一台电脑的浏览器里打开完整桌面 Web GUI,流量走门控 `/remote` 通道;未配对的电脑看到引导式拦截页(含手动粘贴配对令牌输入框),其后没有任何工作区数据。
14
- - **安全**:同时只有一枚有效令牌(刷新二维码使旧链接失效;链接在其有效期窗口内可重复配对——扫码在浏览器间转手也能完成配对;令牌会过期)。停止会撤销所有配对设备与当前令牌——`/remote` 通道在下一次请求即切断它们。配对是本插件对 `/remote` 通道的访问控制;暴露到局域网后的直连 `/api` 由 harness 围栏 + 浏览器认证约束(见安全模型)。回环(127.0.0.1)继续直接使用 `/api`。配对设备是**完全控制凭据**(见安全模型)。
14
+ - **安全**:同时只有一枚有效令牌(刷新二维码使旧链接失效;链接在其有效期窗口内可重复配对——扫码在浏览器间转手也能完成配对;令牌会过期)。停止会撤销所有配对设备与当前令牌——`/remote` 通道在下一次请求即切断它们。配对是本插件对 `/remote` 通道的访问控制;暴露到局域网后的直连 `/api` 由 harness 围栏 + 浏览器认证约束(见安全模型)。回环(127.0.0.1)继续直接使用 `/api`;本机应用交付的页面(桌面外壳的 `dsh-app://app/`、`file:` 页面)同样如此——配对链接只能经网络到达。配对设备是**完全控制凭据**(见安全模型)。
15
15
  - **局域网绑定开关**:设置卡片向 profile `cordis.patch.yml` 写入受管块,将 webserver 绑定固定为 `0.0.0.0`(开)或 `127.0.0.1`(关)——无需 `--host` 命令行操作;显式 `--host`/`--port` 旗标仍然优先。同时维护对应的主机防火墙规则(Windows Defender 经 netsh;Linux firewalld/ufw/iptables;其他平台报告防火墙不受管),并展示运行中的绑定、可达局域网地址与防火墙状态。
16
16
  - **实时状态**:桌面徽标实时切换为已连接;`/api` 姿态探测报告 SDK 栅栏仍敞开的 `/api` 来源;隧道启动期间面板显示自动隧道状态。
17
17
  - **一键自更新**:侧栏下载触发器在加载后检查 dsh-web 新版本,有新版本时标记按钮并执行带校验的更新(面板展示发布说明)。
package/lib/client.js CHANGED
@@ -1231,7 +1231,7 @@ window.__ModuleLoader__.load({
1231
1231
  //#region src/client/pair-api.ts
1232
1232
  /** Read the host-authoritative desktop pairing policy. */
1233
1233
  async function readPairGatePolicy() {
1234
- const response = await fetch("/api/pair/status");
1234
+ const response = await fetch("api/pair/status");
1235
1235
  if (!response.ok) throw new Error(`remote-web-ui: status failed with ${String(response.status)}`);
1236
1236
  const value = await response.json();
1237
1237
  if (typeof value.requirePairingForLan !== "boolean") throw new Error("remote-web-ui: status omitted requirePairingForLan");
@@ -1248,7 +1248,7 @@ window.__ModuleLoader__.load({
1248
1248
  * origin — the panel is a desktop control endpoint).
1249
1249
  */
1250
1250
  async function issuePair(address) {
1251
- const response = await fetch("/api/pair/issue", {
1251
+ const response = await fetch("api/pair/issue", {
1252
1252
  method: "POST",
1253
1253
  headers: { "content-type": "application/json" },
1254
1254
  body: JSON.stringify({ ...address !== void 0 ? { address } : {} })
@@ -1277,7 +1277,7 @@ window.__ModuleLoader__.load({
1277
1277
  * @returns the wire result.
1278
1278
  */
1279
1279
  async function acceptPair(token) {
1280
- const response = await fetch("/api/pair/accept", {
1280
+ const response = await fetch("api/pair/accept", {
1281
1281
  method: "POST",
1282
1282
  headers: { "content-type": "application/json" },
1283
1283
  body: JSON.stringify({ token })
@@ -1294,7 +1294,7 @@ window.__ModuleLoader__.load({
1294
1294
  }
1295
1295
  /** Revoke mobile access (paired devices + the current token). */
1296
1296
  async function stopPair() {
1297
- const response = await fetch("/api/pair/stop", { method: "POST" });
1297
+ const response = await fetch("api/pair/stop", { method: "POST" });
1298
1298
  if (!response.ok) throw new Error(`remote-web-ui: stop failed with ${String(response.status)}`);
1299
1299
  }
1300
1300
  /**
@@ -1302,7 +1302,7 @@ window.__ModuleLoader__.load({
1302
1302
  * @param deviceId - the session id of the row to drop.
1303
1303
  */
1304
1304
  async function revokePair(deviceId) {
1305
- const response = await fetch("/api/pair/revoke", {
1305
+ const response = await fetch("api/pair/revoke", {
1306
1306
  method: "POST",
1307
1307
  headers: { "content-type": "application/json" },
1308
1308
  body: JSON.stringify({ deviceId })
@@ -1316,7 +1316,7 @@ window.__ModuleLoader__.load({
1316
1316
  * proves this page is not paired (see {@link shouldStopHeartbeat}).
1317
1317
  */
1318
1318
  async function sendHeartbeat() {
1319
- return (await fetch("/api/pair/heartbeat", { method: "POST" })).status;
1319
+ return (await fetch("api/pair/heartbeat", { method: "POST" })).status;
1320
1320
  }
1321
1321
  /**
1322
1322
  * Whether a heartbeat answer means "this page can never be accepted again" and
@@ -1349,7 +1349,7 @@ window.__ModuleLoader__.load({
1349
1349
  };
1350
1350
  /** Read the LAN-bind facts (loopback-only endpoint). */
1351
1351
  async function readLanBindStatus() {
1352
- const response = await fetch("/api/pair/lan-bind");
1352
+ const response = await fetch("api/pair/lan-bind");
1353
1353
  if (!response.ok) throw new LanBindStatusError(response.status, `remote-web-ui: lan-bind status failed with ${String(response.status)}`);
1354
1354
  return await response.json();
1355
1355
  }
@@ -1409,7 +1409,7 @@ window.__ModuleLoader__.load({
1409
1409
  }
1410
1410
  //#endregion
1411
1411
  //#region \0dsh-css:packages/dsh-remote-web-ui/src/client/remote.module.css.mjs
1412
- const css$1 = ".fThDlq_overlay{z-index:1000;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.fThDlq_mask{background:var(--dsw-alias-bg-mask-1);backdrop-filter:var(--dsw-mask-blur);position:absolute;inset:0}.fThDlq_trigger{width:36px;height:36px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;transition:background-color .12s,color .12s,box-shadow .12s;display:inline-flex;position:relative}.fThDlq_trigger[data-update-available]{color:var(--dsw-alias-brand-primary)}.fThDlq_trigger[data-update-available]:after{box-sizing:border-box;border:2px solid var(--dsw-alias-bg-layer-1);background:var(--dsw-alias-brand-primary);content:\"\";pointer-events:none;border-radius:50%;width:8px;height:8px;position:absolute;top:4px;right:4px}.fThDlq_trigger[data-wide=wide]{border-radius:999px;flex:auto;justify-content:flex-start;gap:8px;width:auto;min-width:0;padding:0 10px}.fThDlq_trigger[data-update-available][data-wide=wide]{flex:none;gap:6px}.fThDlq_trigger[data-update-available][data-wide=wide]:after{display:none}.fThDlq_updateBadgeText{white-space:nowrap;font-size:13px;font-weight:500;line-height:1}.fThDlq_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.fThDlq_trigger:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.fThDlq_trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.fThDlq_trigger:disabled{opacity:.5;cursor:default}.fThDlq_panel{z-index:1;box-sizing:border-box;background:var(--dsw-alias-bg-layer-2);width:560px;max-width:calc(100vw - 48px);max-height:calc(100vh - 48px);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);border-radius:24px;flex-direction:column;gap:14px;padding:24px;font-size:14px;line-height:22px;display:flex;position:relative;overflow:auto}.fThDlq_header{align-items:flex-start;gap:12px;display:flex}.fThDlq_heading{flex:1;min-width:0}.fThDlq_title{margin:0;font-size:18px;font-weight:600;line-height:26px}.fThDlq_subtitle{color:var(--dsw-alias-label-secondary);margin:4px 0 0;font-size:13px}.fThDlq_close{width:28px;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;transition:background-color .12s,color .12s,box-shadow .12s;display:inline-flex}.fThDlq_close:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_close:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.fThDlq_close:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.fThDlq_close:disabled{opacity:.5;cursor:default}.fThDlq_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;flex-direction:column;align-items:center;gap:12px;padding:16px;display:flex}.fThDlq_cardHeader{justify-content:space-between;align-items:center;gap:12px;width:100%;display:flex}.fThDlq_cardTitle{font-weight:500}.fThDlq_badge{white-space:nowrap;border-radius:999px;flex:none;align-items:center;gap:6px;min-width:0;padding:2px 10px;font-size:12px;line-height:18px;display:inline-flex}.fThDlq_badge:before{content:\"\";background:currentColor;border-radius:50%;width:8px;height:8px}.fThDlq_badge-waiting{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badge-connected{color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badge-disconnected{color:var(--dsw-alias-state-warn-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badge-stopped{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badgePublic{color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badges{flex:none;align-items:center;gap:6px;display:inline-flex}.fThDlq_qrWrap{background:var(--dsw-alias-bg-base);border-radius:12px;justify-content:center;align-items:center;padding:12px;display:flex}.fThDlq_qr{display:block}.fThDlq_expired{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}.fThDlq_expiry{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px}.fThDlq_hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}.fThDlq_link{text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-caption);font-family:var(--dsw-font-mono,ui-monospace, monospace);margin:0;font-size:12px;display:block;overflow:hidden}.fThDlq_pairLinks{flex-direction:column;gap:8px;display:flex}.fThDlq_pairLinkRow{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:10px;align-items:center;gap:10px;min-width:0;padding:10px 12px;display:flex}.fThDlq_pairLinkText{flex:1;min-width:0}.fThDlq_pairLinkLabel{color:var(--dsw-alias-label-secondary);margin-bottom:3px;font-size:12px;display:block}.fThDlq_copyLink{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-button-elevated-fill);min-height:30px;color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:8px;flex:none;align-items:center;gap:5px;padding:0 10px;display:inline-flex}.fThDlq_oneTimeHint{color:var(--dsw-alias-label-caption);margin:0;font-size:12px}.fThDlq_stoppedHint{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}.fThDlq_tunnelNote{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}.fThDlq_tunnelFailed{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}.fThDlq_actions{gap:8px;display:flex}.fThDlq_action{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-button-elevated-fill);height:34px;color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;border-radius:10px;justify-content:center;align-items:center;gap:6px;padding:0 14px;font-size:13px;transition:background-color .12s,border-color .12s,box-shadow .12s;display:inline-flex}.fThDlq_action:hover:not(:disabled){background:var(--dsw-alias-button-floating-hover)}.fThDlq_action:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.fThDlq_action:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.fThDlq_action:disabled{opacity:.5;cursor:default}.fThDlq_banner{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;padding:16px}.fThDlq_bannerTitle{color:var(--dsw-alias-state-warn-primary);margin:0;font-weight:500}.fThDlq_bannerHint{color:var(--dsw-alias-label-secondary);margin:6px 0 0;font-size:13px}.fThDlq_fencePage{z-index:2000;box-sizing:border-box;background:var(--dsw-alias-bg-base);text-align:center;flex-direction:column;justify-content:center;align-items:center;padding:40px 24px;display:flex;position:fixed;inset:0;overflow:auto}.fThDlq_fenceCard{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);width:min(520px,100%);box-shadow:var(--dsw-shadow-lv3);text-align:center;border-radius:20px;margin-inline:auto;padding:36px 40px}.fThDlq_fenceMark{background:var(--dsw-alias-state-error-secondary);width:44px;height:44px;color:var(--dsw-alias-state-error-primary);border-radius:50%;place-items:center;margin-inline:auto;font-size:24px;line-height:1;display:grid}.fThDlq_fenceEyebrow{color:var(--dsw-alias-state-error-primary);margin:22px 0 8px;font-size:13px;font-weight:600}.fThDlq_fenceTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:24px;line-height:1.35}.fThDlq_fenceDetail{color:var(--dsw-alias-label-secondary);margin:12px 0 0;font-size:14px;line-height:1.65}.fThDlq_fenceSteps{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);text-align:left;border-radius:12px;margin:24px auto 0;padding:20px 20px 20px 42px;font-size:14px;line-height:1.65}.fThDlq_fenceSteps li+li{margin-top:8px}.fThDlq_fenceForm{width:100%;margin-top:20px}.fThDlq_fenceInputRow{gap:8px;width:100%;display:flex}.fThDlq_fenceInput{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);min-width:0;color:var(--dsw-alias-label-primary);font:inherit;border-radius:10px;flex:1;padding:10px 14px;font-size:13px;transition:border-color .12s,box-shadow .12s}.fThDlq_fenceInput:focus{border-color:var(--dsw-alias-brand-primary);outline:none;box-shadow:0 0 0 2px #0066ff26}.fThDlq_fencePairButton{border:1px solid var(--dsw-alias-button-primary-fill);background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);font:inherit;cursor:pointer;border-radius:10px;flex-shrink:0;padding:10px 18px;font-size:13px;font-weight:500;transition:filter .12s,opacity .12s}.fThDlq_fencePairButton:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,var(--dsw-alias-button-primary-fill));border-color:var(--dsw-alias-button-primary-hover,var(--dsw-alias-button-primary-fill))}.fThDlq_fencePairButton:disabled{opacity:.55;cursor:not-allowed}.fThDlq_fencePairButton:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.fThDlq_fenceError{color:var(--dsw-alias-state-error-primary);text-align:left;margin:10px 0 0;font-size:13px;line-height:1.4}.fThDlq_fenceRetry{border:1px solid var(--dsw-alias-border-l2);width:100%;color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;border-radius:10px;margin-top:12px;padding:10px 16px;font-weight:500;transition:background-color .12s,color .12s}.fThDlq_fenceRetry:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.fThDlq_fenceRetry:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}.fThDlq_fenceFootnote{color:var(--dsw-alias-label-tertiary);margin:14px 0 0;font-size:12px;line-height:1.55}.fThDlq_addresses{border:none;margin:12px 0 0;padding:0}.fThDlq_addresses legend{color:var(--dsw-alias-label-secondary);padding:0;font-size:13px}.fThDlq_address{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;cursor:pointer;border-radius:6px;align-items:center;gap:8px;margin-top:6px;padding:4px 6px;font-size:13px;transition:background-color .12s;display:flex}.fThDlq_address:hover{background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_address input:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);border-radius:50%;outline:none}.fThDlq_addressValue{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-secondary);flex:1;font-size:12px;overflow:hidden}.fThDlq_addressHint{color:var(--dsw-alias-label-tertiary);margin:6px 0 0;font-size:12px}.fThDlq_devices{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;flex-direction:column;gap:8px;padding:12px 16px 14px;display:flex}.fThDlq_devicesTitle{margin:0;font-size:13px;font-weight:500;line-height:20px}.fThDlq_devicesEmpty{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}.fThDlq_deviceList{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}.fThDlq_deviceRow{justify-content:space-between;align-items:flex-start;gap:12px;display:flex}.fThDlq_deviceMeta{flex-direction:column;gap:2px;min-width:0;display:flex}.fThDlq_deviceName{text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:500;overflow:hidden}.fThDlq_devicePresence{font-size:12px;line-height:18px}.fThDlq_deviceOnline{color:var(--dsw-alias-state-success-primary)}.fThDlq_deviceOffline{color:var(--dsw-alias-label-secondary)}.fThDlq_deviceSeen{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:12px}.fThDlq_deviceRevoke{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:8px;flex:none;padding:6px 10px;font-size:12px;transition:background-color .12s,color .12s}.fThDlq_deviceRevoke:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.fThDlq_deviceRevoke:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}@media (prefers-reduced-motion:reduce){.fThDlq_trigger,.fThDlq_close,.fThDlq_action,.fThDlq_address,.fThDlq_deviceRevoke{transition:none}}.fThDlq_entryRow{flex:none;align-items:center;gap:6px;min-width:0;display:flex}.fThDlq_entryRow[data-rail=rail]{flex-direction:column-reverse;gap:4px}[data-dsh-frame]:not([data-sidebar-collapsed]) [class*=footArea]{flex-flow:wrap;align-items:center}[data-dsh-frame]:not([data-sidebar-collapsed]) [class*=footArea]>:not([class*=settingsArea]):not([class*=footerActions]){flex:100%;min-width:0}[data-dsh-frame]:not([data-sidebar-collapsed]) [class*=settingsArea]{flex:auto;order:1;width:auto;min-width:0}[data-dsh-frame]:not([data-sidebar-collapsed]) [class*=footerActions]{flex:none;order:2;align-items:center;width:auto;min-width:0}.fThDlq_updateStatus{margin:0;font-weight:600}.fThDlq_updateDetail{color:var(--dsw-alias-label-secondary);margin:6px 0 0;font-size:13px}.fThDlq_updateError{color:var(--dsw-alias-text-danger,var(--dsw-alias-label-primary));margin:0;font-weight:600}.fThDlq_updateOutput{background:var(--dsw-alias-bg-layer-1);max-height:180px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-all;border-radius:10px;margin:10px 0 0;padding:10px 12px;font-size:12px;line-height:18px;overflow:auto}.fThDlq_updateList{flex-direction:column;gap:6px;margin:12px 0 0;padding:0;list-style:none;display:flex}.fThDlq_updateListItem{justify-content:space-between;align-items:baseline;gap:12px;font-size:13px;display:flex}.fThDlq_updateListName{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-primary);overflow:hidden}.fThDlq_updateListVersions{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex:none}.fThDlq_updateNotes{margin-top:14px}.fThDlq_updateNotesTitle{margin:0 0 8px;font-size:14px;font-weight:600}.fThDlq_updateNoteSection{flex-direction:column;gap:4px;margin-top:10px;display:flex}.fThDlq_updateNoteHeading{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px;font-weight:600}.fThDlq_updateNoteList{flex-direction:column;gap:4px;margin:0;padding-left:18px;font-size:13px;display:flex}.fThDlq_updateNoteItem{color:var(--dsw-alias-label-primary);overflow-wrap:anywhere}.fThDlq_updateVersions{margin-top:12px}.fThDlq_updateVersionsSummary{cursor:pointer;color:var(--dsw-alias-label-secondary);user-select:none;font-size:12px}.fThDlq_updateActions{justify-content:flex-end;margin-top:16px;display:flex}.fThDlq_updateRetry{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);cursor:pointer;border:none;border-radius:10px;align-items:center;gap:6px;padding:8px 14px;font-size:13px;transition:background-color .12s,color .12s;display:inline-flex}.fThDlq_updateRetry:hover{background:var(--dsw-alias-interactive-bg-active)}@media (prefers-reduced-motion:reduce){.fThDlq_updateRetry{transition:none}}";
1412
+ const css$1 = ".fThDlq_overlay{z-index:1000;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.fThDlq_mask{background:var(--dsw-alias-bg-mask-1);backdrop-filter:var(--dsw-mask-blur);position:absolute;inset:0}.fThDlq_trigger{width:36px;height:36px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;transition:background-color .12s,color .12s,box-shadow .12s;display:inline-flex;position:relative}.fThDlq_trigger[data-update-available]{color:var(--dsw-alias-brand-primary)}.fThDlq_trigger[data-update-available]:after{box-sizing:border-box;border:2px solid var(--dsw-alias-bg-layer-1);background:var(--dsw-alias-brand-primary);content:\"\";pointer-events:none;border-radius:50%;width:8px;height:8px;position:absolute;top:4px;right:4px}.fThDlq_trigger[data-wide=wide]{border-radius:999px;flex:auto;justify-content:flex-start;gap:8px;width:auto;min-width:0;padding:0 10px}.fThDlq_trigger[data-update-available][data-wide=wide]{flex:none;gap:6px}.fThDlq_trigger[data-update-available][data-wide=wide]:after{display:none}.fThDlq_updateBadgeText{white-space:nowrap;font-size:13px;font-weight:500;line-height:1}.fThDlq_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.fThDlq_trigger:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.fThDlq_trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.fThDlq_trigger:disabled{opacity:.5;cursor:default}.fThDlq_panel{z-index:1;box-sizing:border-box;background:var(--dsw-alias-bg-layer-2);width:560px;max-width:calc(100vw - 48px);max-height:calc(100vh - 48px);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);border-radius:24px;flex-direction:column;gap:14px;padding:24px;font-size:14px;line-height:22px;display:flex;position:relative;overflow:auto}.fThDlq_header{align-items:flex-start;gap:12px;display:flex}.fThDlq_heading{flex:1;min-width:0}.fThDlq_title{margin:0;font-size:18px;font-weight:600;line-height:26px}.fThDlq_subtitle{color:var(--dsw-alias-label-secondary);margin:4px 0 0;font-size:13px}.fThDlq_close{width:28px;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;transition:background-color .12s,color .12s,box-shadow .12s;display:inline-flex}.fThDlq_close:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_close:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.fThDlq_close:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.fThDlq_close:disabled{opacity:.5;cursor:default}.fThDlq_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;flex-direction:column;align-items:center;gap:12px;padding:16px;display:flex}.fThDlq_cardHeader{justify-content:space-between;align-items:center;gap:12px;width:100%;display:flex}.fThDlq_cardTitle{font-weight:500}.fThDlq_badge{white-space:nowrap;border-radius:999px;flex:none;align-items:center;gap:6px;min-width:0;padding:2px 10px;font-size:12px;line-height:18px;display:inline-flex}.fThDlq_badge:before{content:\"\";background:currentColor;border-radius:50%;width:8px;height:8px}.fThDlq_badge-waiting{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badge-connected{color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badge-disconnected{color:var(--dsw-alias-state-warn-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badge-stopped{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badgePublic{color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badges{flex:none;align-items:center;gap:6px;display:inline-flex}.fThDlq_qrWrap{background:var(--dsw-alias-bg-base);border-radius:12px;justify-content:center;align-items:center;padding:12px;display:flex}.fThDlq_qr{display:block}.fThDlq_expired{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}.fThDlq_expiry{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px}.fThDlq_hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}.fThDlq_link{text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-caption);font-family:var(--dsw-font-mono,ui-monospace, monospace);margin:0;font-size:12px;display:block;overflow:hidden}.fThDlq_pairLinks{flex-direction:column;gap:8px;display:flex}.fThDlq_pairLinkRow{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:10px;align-items:center;gap:10px;min-width:0;padding:10px 12px;display:flex}.fThDlq_pairLinkText{flex:1;min-width:0}.fThDlq_pairLinkLabel{color:var(--dsw-alias-label-secondary);margin-bottom:3px;font-size:12px;display:block}.fThDlq_copyLink{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-button-elevated-fill);min-height:30px;color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:8px;flex:none;align-items:center;gap:5px;padding:0 10px;display:inline-flex}.fThDlq_oneTimeHint{color:var(--dsw-alias-label-caption);margin:0;font-size:12px}.fThDlq_stoppedHint{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}.fThDlq_tunnelNote{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}.fThDlq_tunnelFailed{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}.fThDlq_actions{gap:8px;display:flex}.fThDlq_action{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-button-elevated-fill);height:34px;color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;border-radius:10px;justify-content:center;align-items:center;gap:6px;padding:0 14px;font-size:13px;transition:background-color .12s,border-color .12s,box-shadow .12s;display:inline-flex}.fThDlq_action:hover:not(:disabled){background:var(--dsw-alias-button-floating-hover)}.fThDlq_action:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.fThDlq_action:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.fThDlq_action:disabled{opacity:.5;cursor:default}.fThDlq_banner{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;padding:16px}.fThDlq_bannerTitle{color:var(--dsw-alias-state-warn-primary);margin:0;font-weight:500}.fThDlq_bannerHint{color:var(--dsw-alias-label-secondary);margin:6px 0 0;font-size:13px}.fThDlq_fencePage{z-index:2000;box-sizing:border-box;background:var(--dsw-alias-bg-base);text-align:center;flex-direction:column;justify-content:center;align-items:center;padding:40px 24px;display:flex;position:fixed;inset:0;overflow:auto}.fThDlq_fenceCard{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);width:min(520px,100%);box-shadow:var(--dsw-shadow-lv3);text-align:center;border-radius:20px;margin-inline:auto;padding:36px 40px}.fThDlq_fenceMark{background:var(--dsw-alias-state-error-secondary);width:44px;height:44px;color:var(--dsw-alias-state-error-primary);border-radius:50%;place-items:center;margin-inline:auto;font-size:24px;line-height:1;display:grid}.fThDlq_fenceEyebrow{color:var(--dsw-alias-state-error-primary);margin:22px 0 8px;font-size:13px;font-weight:600}.fThDlq_fenceTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:24px;line-height:1.35}.fThDlq_fenceDetail{color:var(--dsw-alias-label-secondary);margin:12px 0 0;font-size:14px;line-height:1.65}.fThDlq_fenceSteps{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);text-align:left;border-radius:12px;margin:24px auto 0;padding:20px 20px 20px 42px;font-size:14px;line-height:1.65}.fThDlq_fenceSteps li+li{margin-top:8px}.fThDlq_fenceForm{width:100%;margin-top:20px}.fThDlq_fenceInputRow{gap:8px;width:100%;display:flex}.fThDlq_fenceInput{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);min-width:0;color:var(--dsw-alias-label-primary);font:inherit;border-radius:10px;flex:1;padding:10px 14px;font-size:13px;transition:border-color .12s,box-shadow .12s}.fThDlq_fenceInput:focus{border-color:var(--dsw-alias-brand-primary);outline:none;box-shadow:0 0 0 2px #0066ff26}.fThDlq_fencePairButton{border:1px solid var(--dsw-alias-button-primary-fill);background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);font:inherit;cursor:pointer;border-radius:10px;flex-shrink:0;padding:10px 18px;font-size:13px;font-weight:500;transition:filter .12s,opacity .12s}.fThDlq_fencePairButton:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,var(--dsw-alias-button-primary-fill));border-color:var(--dsw-alias-button-primary-hover,var(--dsw-alias-button-primary-fill))}.fThDlq_fencePairButton:disabled{opacity:.55;cursor:not-allowed}.fThDlq_fencePairButton:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.fThDlq_fenceError{color:var(--dsw-alias-state-error-primary);text-align:left;margin:10px 0 0;font-size:13px;line-height:1.4}.fThDlq_fenceRetry{border:1px solid var(--dsw-alias-border-l2);width:100%;color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;border-radius:10px;margin-top:12px;padding:10px 16px;font-weight:500;transition:background-color .12s,color .12s}.fThDlq_fenceRetry:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.fThDlq_fenceRetry:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}.fThDlq_fenceFootnote{color:var(--dsw-alias-label-tertiary);margin:14px 0 0;font-size:12px;line-height:1.55}.fThDlq_addresses{border:none;margin:12px 0 0;padding:0}.fThDlq_addresses legend{color:var(--dsw-alias-label-secondary);padding:0;font-size:13px}.fThDlq_address{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;cursor:pointer;border-radius:6px;align-items:center;gap:8px;margin-top:6px;padding:4px 6px;font-size:13px;transition:background-color .12s;display:flex}.fThDlq_address:hover{background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_address input:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);border-radius:50%;outline:none}.fThDlq_addressValue{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-secondary);flex:1;font-size:12px;overflow:hidden}.fThDlq_addressHint{color:var(--dsw-alias-label-tertiary);margin:6px 0 0;font-size:12px}.fThDlq_devices{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;flex-direction:column;gap:8px;padding:12px 16px 14px;display:flex}.fThDlq_devicesTitle{margin:0;font-size:13px;font-weight:500;line-height:20px}.fThDlq_devicesEmpty{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}.fThDlq_deviceList{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}.fThDlq_deviceRow{justify-content:space-between;align-items:flex-start;gap:12px;display:flex}.fThDlq_deviceMeta{flex-direction:column;gap:2px;min-width:0;display:flex}.fThDlq_deviceName{text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:500;overflow:hidden}.fThDlq_devicePresence{font-size:12px;line-height:18px}.fThDlq_deviceOnline{color:var(--dsw-alias-state-success-primary)}.fThDlq_deviceOffline{color:var(--dsw-alias-label-secondary)}.fThDlq_deviceSeen{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:12px}.fThDlq_deviceRevoke{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:8px;flex:none;padding:6px 10px;font-size:12px;transition:background-color .12s,color .12s}.fThDlq_deviceRevoke:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.fThDlq_deviceRevoke:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}@media (prefers-reduced-motion:reduce){.fThDlq_trigger,.fThDlq_close,.fThDlq_action,.fThDlq_address,.fThDlq_deviceRevoke{transition:none}}.fThDlq_entryRow{flex:none;align-items:center;gap:6px;min-width:0;display:flex}.fThDlq_entryRow[data-rail=rail]{flex-direction:column-reverse;gap:4px}[data-dsh-frame]:not([data-sidebar-collapsed]) [class*=footArea]{flex-flow:wrap;align-items:center}[data-dsh-frame]:not([data-sidebar-collapsed]) [class*=footArea]>:not([class*=settingsArea]):not([class*=footerActions]){flex:100%;min-width:0}[data-dsh-frame]:not([data-sidebar-collapsed]) [class*=settingsArea]{flex:auto;order:1;width:auto;min-width:0}[data-dsh-frame]:not([data-sidebar-collapsed]) [class*=footerActions]{flex:none;order:2;align-items:center;width:auto;min-width:0}[data-dsh-frame][data-sidebar-collapsed] [class*=footerActions]:has([data-rail=rail]){flex-direction:column;align-items:center;gap:4px}.fThDlq_updateStatus{margin:0;font-weight:600}.fThDlq_updateDetail{color:var(--dsw-alias-label-secondary);margin:6px 0 0;font-size:13px}.fThDlq_updateError{color:var(--dsw-alias-text-danger,var(--dsw-alias-label-primary));margin:0;font-weight:600}.fThDlq_updateOutput{background:var(--dsw-alias-bg-layer-1);max-height:180px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-all;border-radius:10px;margin:10px 0 0;padding:10px 12px;font-size:12px;line-height:18px;overflow:auto}.fThDlq_updateList{flex-direction:column;gap:6px;margin:12px 0 0;padding:0;list-style:none;display:flex}.fThDlq_updateListItem{justify-content:space-between;align-items:baseline;gap:12px;font-size:13px;display:flex}.fThDlq_updateListName{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-primary);overflow:hidden}.fThDlq_updateListVersions{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex:none}.fThDlq_updateNotes{margin-top:14px}.fThDlq_updateNotesTitle{margin:0 0 8px;font-size:14px;font-weight:600}.fThDlq_updateNoteSection{flex-direction:column;gap:4px;margin-top:10px;display:flex}.fThDlq_updateNoteHeading{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px;font-weight:600}.fThDlq_updateNoteList{flex-direction:column;gap:4px;margin:0;padding-left:18px;font-size:13px;display:flex}.fThDlq_updateNoteItem{color:var(--dsw-alias-label-primary);overflow-wrap:anywhere}.fThDlq_updateVersions{margin-top:12px}.fThDlq_updateVersionsSummary{cursor:pointer;color:var(--dsw-alias-label-secondary);user-select:none;font-size:12px}.fThDlq_updateActions{justify-content:flex-end;margin-top:16px;display:flex}.fThDlq_updateRetry{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);cursor:pointer;border:none;border-radius:10px;align-items:center;gap:6px;padding:8px 14px;font-size:13px;transition:background-color .12s,color .12s;display:inline-flex}.fThDlq_updateRetry:hover{background:var(--dsw-alias-interactive-bg-active)}@media (prefers-reduced-motion:reduce){.fThDlq_updateRetry{transition:none}}";
1413
1413
  const tagId$1 = "@linxin666/dsh-remote-web-ui/packages/dsh-remote-web-ui/src/client/remote.module.css";
1414
1414
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
1415
1415
  const tag = document.createElement("style");
@@ -1883,7 +1883,7 @@ window.__ModuleLoader__.load({
1883
1883
  async function fetchUpdateStatus() {
1884
1884
  let response;
1885
1885
  try {
1886
- response = await fetch("/api/update/status");
1886
+ response = await fetch("api/update/status");
1887
1887
  } catch {
1888
1888
  throw new UpdateStatusError(0);
1889
1889
  }
@@ -1896,7 +1896,7 @@ window.__ModuleLoader__.load({
1896
1896
  * @returns the run outcome.
1897
1897
  */
1898
1898
  async function runUpdate() {
1899
- const response = await fetch("/api/update/run", { method: "POST" });
1899
+ const response = await fetch("api/update/run", { method: "POST" });
1900
1900
  if (!response.ok) throw new Error("update run unavailable");
1901
1901
  return await response.json();
1902
1902
  }
@@ -2415,7 +2415,7 @@ window.__ModuleLoader__.load({
2415
2415
  if (seq !== openSeq.current) return;
2416
2416
  setState(next);
2417
2417
  if (next.kind !== "ready" && next.kind !== "lan-required") return;
2418
- const source = new EventSource("/api/pair/events");
2418
+ const source = new EventSource("api/pair/events");
2419
2419
  eventSource.current = source;
2420
2420
  source.onmessage = (event) => {
2421
2421
  try {
@@ -3722,7 +3722,7 @@ window.__ModuleLoader__.load({
3722
3722
  "status.disconnected": "已配对设备离线",
3723
3723
  "status.stopped": "已停止远程访问",
3724
3724
  "status.lanRequired": "此功能需要局域网绑定或公网地址才能使用",
3725
- "status.lanRequiredHint": "当前服务仅绑定在 127.0.0.1 且未配置公网地址,手机无法访问。请在「设置 → Web 插件 → 远程访问设置」卡片中打开“局域网访问”,或用 dsh web --host 0.0.0.0 重新启动,或填写内网穿透的公网地址。",
3725
+ "status.lanRequiredHint": "当前服务仅绑定在 127.0.0.1 且未配置公网地址,手机无法访问。请在本插件的设置卡片中打开“局域网访问”(装在全家桶里时在「设置 → Web 插件 → 远程访问设置」,单独安装时在设置 → 插件 → 本插件行),或用 dsh web --host 0.0.0.0 重新启动,或填写内网穿透的公网地址。",
3726
3726
  "status.loopbackRequired": "配对面板仅限本机使用",
3727
3727
  "status.loopbackRequiredHint": "请通过 http://127.0.0.1 打开此页面后重试;手机请使用配对链接访问。",
3728
3728
  "status.unreachable": "无法连接配对服务",
@@ -3886,7 +3886,7 @@ window.__ModuleLoader__.load({
3886
3886
  "status.disconnected": "Paired devices offline",
3887
3887
  "status.stopped": "Remote access stopped",
3888
3888
  "status.lanRequired": "This feature needs a LAN bind or a public address",
3889
- "status.lanRequiredHint": "The server is bound to 127.0.0.1 and no public address is configured, so a phone cannot reach it. Turn on LAN access in the settings card under Settings → Web Plugins → Remote access, or restart with dsh web --host 0.0.0.0, or set the tunneled public address.",
3889
+ "status.lanRequiredHint": "The server is bound to 127.0.0.1 and no public address is configured, so a phone cannot reach it. Turn on LAN access in this plugin settings card (under Settings → Web Plugins → Remote access when the full family is installed, or on this plugin row under Settings → Plugins for a standalone install), or restart with dsh web --host 0.0.0.0, or set the tunneled public address.",
3890
3890
  "status.loopbackRequired": "The pairing panel works on this machine only",
3891
3891
  "status.loopbackRequiredHint": "Open this page at http://127.0.0.1 to mint a QR code; phones use the paired link.",
3892
3892
  "status.unreachable": "Cannot reach the pairing service",
@@ -4104,6 +4104,102 @@ window.__ModuleLoader__.load({
4104
4104
  */
4105
4105
  /** The gated mirror prefix (must match src/remote-methods.ts). */
4106
4106
  const REMOTE_PREFIX = "/remote";
4107
+ /**
4108
+ * The schemes that can deliver a page a remote party controls: the network
4109
+ * transports, plus the documents a network page can mint. Every other scheme
4110
+ * is registered and served by an application on this machine, so its page is
4111
+ * the machine's own page whatever the application calls the scheme.
4112
+ *
4113
+ * The list is deliberately the *web* side rather than an allowlist of known
4114
+ * desktop shells. The official DSH Desktop shell serves its Web GUI from
4115
+ * `dsh-app://app/` (`location.hostname === 'app'`), which no hostname
4116
+ * predicate can recognise; a scheme allowlist fixed that instance (#1682) but
4117
+ * left the next shell — or a `file:` page — fenced behind a pairing page it
4118
+ * can never complete, because a pairing link is reachable only over the
4119
+ * network. Naming the web side instead makes an unknown application scheme
4120
+ * local by construction.
4121
+ *
4122
+ * `blob:`, `data:`, `about:` and `filesystem:` stay on the web side: a
4123
+ * network page mints those documents, so they must keep the fence.
4124
+ */
4125
+ const WEB_PAGE_PROTOCOLS = [
4126
+ "http:",
4127
+ "https:",
4128
+ "blob:",
4129
+ "data:",
4130
+ "about:",
4131
+ "filesystem:"
4132
+ ];
4133
+ /**
4134
+ * Whether one page scheme can carry a document a remote party controls.
4135
+ * @param protocol - `location.protocol` of the page (for example `https:`).
4136
+ * @returns true for the network transports and the documents they mint.
4137
+ */
4138
+ function isWebPageProtocol(protocol) {
4139
+ return WEB_PAGE_PROTOCOLS.includes(protocol);
4140
+ }
4141
+ /**
4142
+ * Hostname-only loopback classification: localhost, the IPv6 loopback literal
4143
+ * (WHATWG keeps its brackets) and any 127/8 IPv4 literal. Kept string-in,
4144
+ * boolean-out and dependency-free so the browser half and the inlined boot
4145
+ * script can both run it verbatim.
4146
+ * @param hostname - a page/cookie hostname, IPv6 literals bracketed.
4147
+ * @returns true for a loopback name or literal.
4148
+ */
4149
+ function isLoopbackHostname(hostname) {
4150
+ if (hostname === "localhost" || hostname === "::1" || hostname === "[::1]") return true;
4151
+ const parts = hostname.split(".");
4152
+ return parts.length === 4 && parts[0] === "127" && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
4153
+ }
4154
+ /**
4155
+ * Whether the current page is served from the machine running the host, so
4156
+ * its same-origin traffic must NOT be fenced behind the pairing channel.
4157
+ * Three independent facts all describe the page itself and any one is enough:
4158
+ *
4159
+ * - the page's own hostname is loopback;
4160
+ * - the page's protocol is not a {@link WEB_PAGE_PROTOCOLS} web scheme, so an
4161
+ * application on this machine delivered it — the desktop shell's
4162
+ * `dsh-app://app/` is one such page, and a scheme this build has never
4163
+ * heard of is another;
4164
+ * - the official connection transport already declared this shell the host
4165
+ * owner (`__DSH_TRANSPORT__.ownsHost === true`) AND the page is not a
4166
+ * network origin. The official client reads the same hook to derive
4167
+ * `connection.isLoopback`, and the desktop shell sets it before any boot
4168
+ * entry — without this term the plugin's predicate contradicts the host's
4169
+ * own conclusion (#1682). The network-origin guard is deliberate: this
4170
+ * plugin's own device-gated landing (a LAN or tunnel page) publishes the
4171
+ * same hook to grant a paired remote the full UI, and that page must keep
4172
+ * riding the gated channel — `ownsHost` buys the presentation, never an
4173
+ * exemption from the pairing fence.
4174
+ *
4175
+ * The order matters: loopback is checked first so a loopback page keeps its
4176
+ * exemption even on a scheme no list carries, and the scheme test runs before
4177
+ * the hook test so a web page always reaches the network-origin guard. A
4178
+ * missing or empty scheme is not evidence of a local page - the fence stays
4179
+ * up - because a real document always reports its own scheme.
4180
+ *
4181
+ * @param hostname - `location.hostname` of the page.
4182
+ * @param protocol - `location.protocol` of the page (for example `https:`).
4183
+ * @param transportOwnsHost - `__DSH_TRANSPORT__?.ownsHost` as read by the caller.
4184
+ * @returns true when the page is local and needs no remote channel.
4185
+ */
4186
+ function isLocalPage(hostname, protocol, transportOwnsHost) {
4187
+ if (isLoopbackHostname(hostname)) return true;
4188
+ if (protocol !== void 0 && protocol !== "" && !isWebPageProtocol(protocol)) return true;
4189
+ return transportOwnsHost === true && !isNetworkOrigin(hostname);
4190
+ }
4191
+ /**
4192
+ * Whether a hostname can only be reached across a network, so a page on it is
4193
+ * remote whatever transport hook it carries: any IPv4/IPv6 literal (a dot or a
4194
+ * colon) or any dotted DNS name. A hostname with no dot and no colon is a
4195
+ * scheme-local authority (the desktop shell's `app`), which is why this test,
4196
+ * not a loopback test, is what the `ownsHost` term is gated on.
4197
+ * @param hostname - the page hostname.
4198
+ * @returns true when the page cannot be the machine's own local page.
4199
+ */
4200
+ function isNetworkOrigin(hostname) {
4201
+ return hostname.includes(".") || hostname.includes(":");
4202
+ }
4107
4203
  /** The live rule set. */
4108
4204
  const REMOTE_CHANNEL_RULES = {
4109
4205
  remotePrefix: REMOTE_PREFIX,
@@ -4126,6 +4222,7 @@ window.__ModuleLoader__.load({
4126
4222
  deviceQuery: REMOTE_DEVICE_QUERY,
4127
4223
  uploadPath: "/api/session/uploadFileBinary",
4128
4224
  uploadHookGlobal: "__DSH_FILE_UPLOAD__",
4225
+ webProtocols: WEB_PAGE_PROTOCOLS,
4129
4226
  hostGrantGlobal: "__DSH_REMOTE_HOST_GRANT__"
4130
4227
  };
4131
4228
  /** The window global the boot patch publishes its seat under. */
@@ -4156,24 +4253,29 @@ window.__ModuleLoader__.load({
4156
4253
  * the given window and returns their restore.
4157
4254
  */
4158
4255
  const RULES = REMOTE_CHANNEL_RULES;
4256
+ /**
4257
+ * Read the page-origin facts from a window-like object: the hostname and
4258
+ * protocol of the page itself plus the official transport's host-owner flag
4259
+ * (`__DSH_TRANSPORT__.ownsHost`, set by the desktop shell before any boot
4260
+ * entry, and by this plugin's own paired landing).
4261
+ * @param win - the window to read (defaults to the real one).
4262
+ * @returns the facts `isLocalPage`/`remoteChannelRequired` decide from.
4263
+ */
4264
+ function pageOriginFacts(win = window) {
4265
+ const transport = win.__DSH_TRANSPORT__;
4266
+ return {
4267
+ hostname: win.location.hostname,
4268
+ protocol: win.location.protocol,
4269
+ ...transport?.ownsHost === true ? { transportOwnsHost: true } : {}
4270
+ };
4271
+ }
4159
4272
  /** Decide whether a remote desktop channel is required from local or host policy. */
4160
- function remoteChannelRequired(hostname, snapshot, hostPairingPolicy) {
4161
- if (isLoopbackHostname(hostname)) return false;
4273
+ function remoteChannelRequired(origin, snapshot, hostPairingPolicy) {
4274
+ if (isLocalPage(origin.hostname, origin.protocol, origin.transportOwnsHost)) return false;
4162
4275
  if (snapshot.status === "ready") return (snapshot.value?.enabled ?? true) && (snapshot.value?.requirePairingForLan ?? true);
4163
4276
  return hostPairingPolicy !== false;
4164
4277
  }
4165
4278
  /**
4166
- * Browser-safe loopback classification for the page origin (the SDK client
4167
- * exports its own; this copy keeps the module dependency-free).
4168
- * @param hostname - a location hostname (IPv6 without brackets).
4169
- * @returns true for localhost, IPv6 loopback, or any 127/8 literal.
4170
- */
4171
- function isLoopbackHostname(hostname) {
4172
- if (hostname === "localhost" || hostname === "::1" || hostname === "[::1]") return true;
4173
- const parts = hostname.split(".");
4174
- return parts.length === 4 && parts[0] === "127" && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
4175
- }
4176
- /**
4177
4279
  * Whether one same-origin path must ride the gated channel (fetch, EventSource,
4178
4280
  * img/script/iframe src).
4179
4281
  * @param pathname - the request URL pathname.
@@ -4553,7 +4655,7 @@ window.__ModuleLoader__.load({
4553
4655
  /** The building package's version, when the bundle carries it. */
4554
4656
  function bakedVersion() {
4555
4657
  try {
4556
- return "0.4.1";
4658
+ return "0.4.2";
4557
4659
  } catch {
4558
4660
  return;
4559
4661
  }
@@ -5860,7 +5962,7 @@ window.__ModuleLoader__.load({
5860
5962
  }
5861
5963
  showFenceNotice();
5862
5964
  };
5863
- const channelActive = () => remoteChannelRequired(window.location.hostname, settingsForm.getSnapshot(), hostPairingPolicy);
5965
+ const channelActive = () => remoteChannelRequired(pageOriginFacts(window), settingsForm.getSnapshot(), hostPairingPolicy);
5864
5966
  const bootSeat = () => window[REMOTE_CHANNEL_BOOT_GLOBAL];
5865
5967
  const syncChannel = () => {
5866
5968
  const transition = channelTransition(channelActive(), disposeChannel !== void 0);
@@ -5892,7 +5994,8 @@ window.__ModuleLoader__.load({
5892
5994
  };
5893
5995
  settingsForm.subscribe(syncChannel);
5894
5996
  syncChannel();
5895
- if (!isLoopbackHostname(window.location.hostname) && settingsForm.getSnapshot().status !== "ready") readPairGatePolicy().then((policy) => {
5997
+ const pageOrigin = pageOriginFacts(window);
5998
+ if (!isLocalPage(pageOrigin.hostname, pageOrigin.protocol, pageOrigin.transportOwnsHost) && settingsForm.getSnapshot().status !== "ready") readPairGatePolicy().then((policy) => {
5896
5999
  hostPairingPolicy = policy.requirePairingForLan;
5897
6000
  syncChannel();
5898
6001
  if (hostPairingPolicy && unpairedWhilePolicyPending) showFenceNotice();