@linxin666/dsh-remote-web-ui 0.2.8 → 0.3.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.
Files changed (86) hide show
  1. package/LICENSE +202 -28
  2. package/README.i18n.yaml +2 -2
  3. package/README.md +51 -15
  4. package/README.zh.md +9 -4
  5. package/lib/client.js +65 -34
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +667 -77
  8. package/lib/mobile.js +874 -64
  9. package/lib/mobile.js.map +1 -1
  10. package/lib/types/client/index.d.ts.map +1 -1
  11. package/lib/types/client/index.js +35 -5
  12. package/lib/types/client/remote-channel.d.ts +4 -5
  13. package/lib/types/client/remote-channel.d.ts.map +1 -1
  14. package/lib/types/client/remote-channel.js +13 -28
  15. package/lib/types/http.d.ts +37 -8
  16. package/lib/types/http.d.ts.map +1 -1
  17. package/lib/types/http.js +76 -11
  18. package/lib/types/index.d.ts.map +1 -1
  19. package/lib/types/index.js +40 -5
  20. package/lib/types/mobile/api.d.ts +57 -0
  21. package/lib/types/mobile/api.d.ts.map +1 -1
  22. package/lib/types/mobile/api.js +29 -0
  23. package/lib/types/mobile/markdown.d.ts +7 -1
  24. package/lib/types/mobile/markdown.d.ts.map +1 -1
  25. package/lib/types/mobile/markdown.js +16 -7
  26. package/lib/types/mobile/markdown.test.js +8 -0
  27. package/lib/types/mobile/mobile-styles.d.ts +1 -1
  28. package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
  29. package/lib/types/mobile/mobile-styles.js +325 -6
  30. package/lib/types/mobile/views/ChatView.d.ts +10 -0
  31. package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
  32. package/lib/types/mobile/views/ChatView.js +205 -8
  33. package/lib/types/mobile/views/ChatView.stream.test.d.ts +2 -0
  34. package/lib/types/mobile/views/ChatView.stream.test.d.ts.map +1 -0
  35. package/lib/types/mobile/views/ChatView.stream.test.js +205 -0
  36. package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -1
  37. package/lib/types/mobile/views/WorkspaceView.js +46 -5
  38. package/lib/types/mobile/views/WorkspaceView.test.js +129 -2
  39. package/lib/types/mobile-api.d.ts +9 -3
  40. package/lib/types/mobile-api.d.ts.map +1 -1
  41. package/lib/types/mobile-api.js +61 -16
  42. package/lib/types/mobile-pending.d.ts +38 -0
  43. package/lib/types/mobile-pending.d.ts.map +1 -0
  44. package/lib/types/mobile-pending.js +59 -0
  45. package/lib/types/paired-model-catalog.d.ts +17 -0
  46. package/lib/types/paired-model-catalog.d.ts.map +1 -0
  47. package/lib/types/paired-model-catalog.js +252 -0
  48. package/lib/types/pairing.d.ts +13 -4
  49. package/lib/types/pairing.d.ts.map +1 -1
  50. package/lib/types/pairing.js +13 -5
  51. package/lib/types/remote-api.d.ts +13 -4
  52. package/lib/types/remote-api.d.ts.map +1 -1
  53. package/lib/types/remote-api.js +27 -17
  54. package/lib/types/remote-channel-boot.d.ts +26 -0
  55. package/lib/types/remote-channel-boot.d.ts.map +1 -0
  56. package/lib/types/remote-channel-boot.js +128 -0
  57. package/lib/types/remote-channel-rules.d.ts +40 -0
  58. package/lib/types/remote-channel-rules.d.ts.map +1 -0
  59. package/lib/types/remote-channel-rules.js +32 -0
  60. package/lib/types/routes.d.ts.map +1 -1
  61. package/lib/types/routes.js +12 -23
  62. package/lib/types/uuid-polyfill.d.ts +10 -0
  63. package/lib/types/uuid-polyfill.d.ts.map +1 -0
  64. package/lib/types/uuid-polyfill.js +27 -0
  65. package/package.json +1 -1
  66. package/src/client/index.ts +41 -5
  67. package/src/client/remote-channel.ts +18 -28
  68. package/src/http.ts +87 -12
  69. package/src/index.ts +42 -6
  70. package/src/mobile/api.ts +91 -0
  71. package/src/mobile/markdown.test.ts +9 -0
  72. package/src/mobile/markdown.ts +17 -7
  73. package/src/mobile/mobile-styles.ts +326 -6
  74. package/src/mobile/views/ChatView.stream.test.tsx +231 -0
  75. package/src/mobile/views/ChatView.tsx +328 -10
  76. package/src/mobile/views/WorkspaceView.test.tsx +163 -2
  77. package/src/mobile/views/WorkspaceView.tsx +152 -26
  78. package/src/mobile-api.ts +59 -17
  79. package/src/mobile-pending.ts +88 -0
  80. package/src/paired-model-catalog.ts +224 -0
  81. package/src/pairing.ts +13 -5
  82. package/src/remote-api.ts +34 -18
  83. package/src/remote-channel-boot.ts +131 -0
  84. package/src/remote-channel-rules.ts +62 -0
  85. package/src/routes.ts +13 -24
  86. package/src/uuid-polyfill.ts +28 -0
package/LICENSE CHANGED
@@ -1,28 +1,202 @@
1
- BSD 3-Clause License
2
-
3
- Copyright (c) 2026, zhu1090093659
4
-
5
- Redistribution and use in source and binary forms, with or without
6
- modification, are permitted provided that the following conditions are met:
7
-
8
- 1. Redistributions of source code must retain the above copyright notice, this
9
- list of conditions and the following disclaimer.
10
-
11
- 2. Redistributions in binary form must reproduce the above copyright notice,
12
- this list of conditions and the following disclaimer in the documentation
13
- and/or other materials provided with the distribution.
14
-
15
- 3. Neither the name of the copyright holder nor the names of its
16
- contributors may be used to endorse or promote products derived from
17
- this software without specific prior written permission.
18
-
19
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
- SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
- CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
- OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
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: dc31fab1b49b32fe325ead616d52846435fa1180
6
- README.zh.md: e5ccaf0f52d01e9b8f92e265947c22dff68e2a97
5
+ README.md: 2007aecc144d421951a3aebd9d94708746e1f549
6
+ README.zh.md: 85a76a5df45f3cb2d4ec0a477e65affd78c98795
package/README.md CHANGED
@@ -44,7 +44,8 @@ that probes and runs the update.
44
44
  their next request. Pairing is this plugin's access control for its own
45
45
  remote channels: `/m/api` for the phone and `/remote` for a desktop
46
46
  browser opened at a non-loopback origin. Unpaired `/remote` callers
47
- are refused before the request body is read. Loopback (127.0.0.1) keeps using
47
+ are refused before the request body is read while `requirePairingForLan` is
48
+ on (default). Loopback (127.0.0.1) keeps using
48
49
  `/api` directly. The default remote-desktop path does not use
49
50
  `--trusted-host`: the connection plugin's `/api` fence stays closed for
50
51
  public and LAN hosts, and the paired PC rides `/remote` instead.
@@ -52,18 +53,40 @@ that probes and runs the update.
52
53
  itself — pairing does not gate `/api` (no plugin can; the fence is the
53
54
  SDK's own seam). The posture probe below reports that stance when `/api`
54
55
  is reachable without pairing.
56
+ - **Paired model catalog**: authenticated paired devices can use `GET
57
+ /api/pair/model-catalog`, `POST /api/pair/model-catalog/discover`, and
58
+ `POST /api/pair/model-catalog/upsert` to inspect and adopt models for an
59
+ existing active `llm-pi-ai` provider only. The capability fixes the
60
+ provider settings address internally and cannot create providers or read or
61
+ change credentials, general settings, endpoints, headers, or arbitrary
62
+ configuration. The exact routes are served ahead of the connection
63
+ plugin's `/api` prefix, so a paired LAN or tunnel client may call them
64
+ directly, and they carry the same Host/Origin trust fence as the sibling
65
+ `/api/pair` endpoints: loopback, the advertised LAN literals, or the
66
+ configured public base URL. Through the `/remote` desktop channel all
67
+ `/api/pair/*` paths stay loopback-only. Adoption is refused while the
68
+ provider's live model catalog is unavailable or unknown. Stop or device
69
+ revocation disables it immediately; the
70
+ generic `settings.*`, `credentials.*`, and `llm.discoverModels` RPC methods
71
+ remain loopback-only.
55
72
  - **Remote desktop channel**: with `requirePairingForLan` on (default), a
56
73
  desktop Web GUI opened at the LAN URL or through the tunnel transparently
57
- rides `/remote` — the same UI, gated by the same pairing cookie. Browser
74
+ rides `/remote` — the same UI, gated by the same pairing cookie. The
75
+ rewrite installs twice-removed from the race it fixes: the host inlines a
76
+ small classic script right after the opening `<head>` tag
77
+ (`webserver/index-inject`), so fetch/WebSocket/EventSource and resource
78
+ `src` rewrites are active before any boot entry (the connection plugin
79
+ opens its event streams first); the browser half then adopts the installed
80
+ seat instead of patching twice. Loopback origins skip the patch entirely. Browser
58
81
  requests under `/api`, `/sidebar`, `/git`, and `/pet`, including the known
59
82
  event, terminal, and SSH WebSockets, are re-issued to the local web server
60
83
  without forwarding the remote Origin or pairing cookie; the authenticated
61
84
  proxy supplies its own same-origin browser marker for sibling route fences. The
62
85
  SDK's loopback-only privileged methods (native dialogs, the settings and
63
- credentials plane) stay unreachable from a paired remote desktop, and the
64
- `/api/pair/*`, `/api/update/*`, `/api/plugin-manager/*`,
65
- `/api/dsh-desktop-launcher/*` and `/api/dsh-web-ui-settings/*` control
66
- endpoints stay loopback-only. Unpaired desktop browsers get a persistent
86
+ credentials plane) stay unreachable from a paired remote desktop;
87
+ `/api/pair/*` (including the paired model-catalog routes),
88
+ `/api/update/*`, `/api/plugin-manager/*`, `/api/dsh-desktop-launcher/*`
89
+ and `/api/dsh-web-ui-settings/*` control endpoints stay loopback-only. Unpaired desktop browsers get a persistent
67
90
  full-page block instead of data (the page keys off the `unpaired` error
68
91
  code, not every 403). The block retires once a gated call succeeds or the
69
92
  channel itself is torn down — turning `requirePairingForLan` off (or the
@@ -222,9 +245,16 @@ need **no harness source changes** — the phone's RPC calls ride the
222
245
  plugin's `/m/api` proxy (which delegates to the host ApiProxy service and
223
246
  pages `session.list` itself), so the tunneled Host never has to enter the
224
247
  connection plugin's trust fence. The phone is gated by its paired-device
225
- cookie and an explicit method allowlist (settings/credentials/host-action
226
- domains are never reachable from the phone; model reads/writes are limited
227
- to the advisory `session.models` / `session.selectModel` pair, agent preset
248
+ cookie and an explicit method allowlist. The allowlist constrains the
249
+ `/m/api` proxy alone: the paired-device cookie itself also passes the global
250
+ api/gate, so a paired device is a full-control credential for the host `/api`
251
+ surface — settings/credentials/host-action domains stay unreachable only
252
+ because the SDK pins those privileged methods to loopback. Pairing is full
253
+ device trust; the `/m/api` proxy's model
254
+ reads/writes are limited to the advisory `session.models` /
255
+ `session.selectModel` pair. Separately, the exact paired-only model-catalog
256
+ routes may adopt models for an existing eligible `llm-pi-ai` provider; they
257
+ cannot create providers or access credentials or general settings. Agent preset
228
258
  access to read-only `agentPreset.list`, creation to `session.create`
229
259
  (workspace id plus an optional id from that roster — the phone never names a
230
260
  working directory of its own), and the permission picker only ever sends the
@@ -244,12 +274,14 @@ over Server-Sent Events on `/m/api/events.mux`. The canonical `/m/` page owns a
244
274
  - The `/m/` worker uses network-first static-shell fallback and waits for current pages to close before an updated worker activates. It bypasses `/m/api`, `/api`, SSE, and every write request.
245
275
  - Installing this plugin routes fenced non-loopback desktop traffic onto the
246
276
  gated `/remote` channel (see `requirePairingForLan` in `src/index.ts`).
247
- A desktop browser opened via the LAN URL or the tunnel must pair like any
248
- remote device — the unpaired state shows a persistent blocking page instead of
249
- data; loopback (127.0.0.1) is unaffected and keeps `/api`. Set
250
- `requirePairingForLan: false` in the profile patch to keep the desktop on
251
- plain `/api` (only useful with the open-LAN stance) while keeping
252
- tokens/status/revocation. Note the underlying `/api` route itself is the
277
+ While the flag is on (default), a desktop browser opened via the LAN URL or
278
+ the tunnel must pair like any remote device — the unpaired state shows a
279
+ persistent blocking page instead of data; loopback (127.0.0.1) is unaffected
280
+ and keeps `/api`. Set `requirePairingForLan: false` in the profile patch to
281
+ keep the desktop on plain `/api` and have a leftover `/remote` rewrite
282
+ proxy unpaired calls (only useful with the open-LAN stance; loopback-only
283
+ paths stay denied) while keeping tokens/status/revocation. Note the
284
+ underlying `/api` route itself is the
253
285
  SDK's: on a `--host 0.0.0.0` bind the SDK auto-trusts LAN literals, so a
254
286
  LAN client bypassing the UI can still reach `/api` directly — the posture
255
287
  probe reports that stance on the panel.
@@ -455,6 +487,10 @@ accept → UI. Only `publicBaseUrl` (plugin config) names the tunneled host;
455
487
  `--trusted-host` is not part of this pairing flow. The desktop panel still
456
488
  opens at `http://127.0.0.1`.
457
489
 
490
+ ## Security model
491
+
492
+ - Mobile unary routes and the mux event stream require a live paired-device session. A missing or revoked session receives HTTP 403 with a JSON rejection carrying `error.code: "unpaired"`; the browser's `EventSource` API exposes only the stream failure, not that response body.
493
+
458
494
  ## Known Limitations and Deferred Work
459
495
 
460
496
  - **Revocation is per-request**: a paired phone whose request is already in
package/README.zh.md CHANGED
@@ -12,8 +12,9 @@
12
12
  - **面板**:「远程访问」标题、「通过手机或另一台电脑配对,远程访问当前工作区」副标题、「设备配对」卡片(含状态区「等待设备连接」+ 状态徽标)、大号二维码、各带复制按钮的手机链接(`/m/?pair=...`)与电脑链接(`/?pair=...`)、停止 / 刷新二维码操作,以及已授权设备列表(根据 User-Agent 推断的设备名称、在线/离线、最近活动时间、单台取消配对)。界面不渲染作为会话凭据的设备 id 或原始 User-Agent。两条链接共用一枚一次性令牌,任意设备配对成功后另一条立即失效。
13
13
  - **手机侧**:扫码将手机与一次性、限时令牌绑定,并落地到 **`/m/` 独立移动端界面**——一款专为小屏设计的轻客户端(见[截图](#截图)),而不是把桌面 UI 塞进手机。该页面可安装为 PWA;每个已安装应用使用自己的已配对设备 cookie,存储隔离的移动端 Web App 需要在该上下文打开新的二维码链接,或粘贴桌面端新复制的链接完成配对。链接携带 `workspace` 参数,手机落地到桌面正在查看的同一工作区。
14
14
  - **PC 侧(远程桌面配对)**:同一份二维码链接和配对令牌也能配对 PC 浏览器——把手机侧配对流扩展到桌面 Web GUI。从面板复制链接,在另一台电脑的浏览器打开(局域网 URL 或隧道 URL 均可);接受往返后,完整 Web GUI 在那台设备上运行——工作区、会话、聊天、切模型——受围栏保护的同源流量走门控的 `/remote` 通道,而不是直接调用仅 loopback 的 host 路由。手机进 `/m/`,PC 进桌面 UI:一枚令牌、一套配对流、两种界面。未配对的 PC 只看到不渲染底层数据的完整阻断页,其中提供电脑配对步骤与「重新检测」操作。
15
- - **安全**:一个有效的一次性令牌(刷新会使旧链接失效;已接受的令牌不可复用;令牌会过期)。停止会撤销每一台已配对设备与当前令牌——已配对设备在下一次请求时被切断。配对是本插件对自己远程通道的访问控制:手机走 `/m/api`,非 loopback origin 打开的桌面浏览器走 `/remote`。未配对调用方在读取请求体之前就被 `/remote` 拒绝。loopback(127.0.0.1)继续直接使用 `/api`。默认的远程桌面路径不使用 `--trusted-host`:连接插件的 `/api` 围栏对公网和局域网主机保持关闭,已配对 PC 改走 `/remote`。`--trusted-host` 是 SDK 的另一种用法,会让该主机被信任访问 `/api` 本身——配对不门控 `/api`(任何插件都无法做到;围栏是 SDK 自己的接缝)。当 `/api` 无需配对即可到达时,下方姿态探测会报告该姿态。
16
- - **远程桌面通道**:`requirePairingForLan` 开启(默认)时,经局域网 URL 或隧道打开的桌面 Web GUI 透明地改走 `/remote`——同一套 UI,同一枚配对 cookie 门控。浏览器中 `/api`、`/sidebar`、`/git`、`/pet` 下的请求,以及已知的事件、终端与 SSH WebSocket,会在不转发远程 Origin 或配对 cookie 的前提下重新发给本机 Web Server;认证反代会为兄弟路由围栏提供自己生成的同源浏览器标记。SDK 的 loopback 专属特权方法(原生对话框、settings 与 credentials 面)对已配对远程桌面保持不可达,`/api/pair/*`、`/api/update/*`、`/api/plugin-manager/*`、`/api/dsh-desktop-launcher/*` 与 `/api/dsh-web-ui-settings/*` 控制端点保持仅 loopback。未配对的桌面浏览器看到持久的全页阻断界面而不是数据(阻断页按错误码 `unpaired` 触发,不是任意 403)。阻断页在任一门控调用成功后,或通道本身被拆除时退出——把 `requirePairingForLan` 关掉(或关闭插件)也会清掉通道短暂激活期间弹出的提示。
15
+ - **安全**:一个有效的一次性令牌(刷新会使旧链接失效;已接受的令牌不可复用;令牌会过期)。停止会撤销每一台已配对设备与当前令牌——已配对设备在下一次请求时被切断。配对是本插件对自己远程通道的访问控制:手机走 `/m/api`,非 loopback origin 打开的桌面浏览器走 `/remote`。未配对调用方在 `requirePairingForLan` 开启(默认)时会在读取请求体之前就被 `/remote` 拒绝。loopback(127.0.0.1)继续直接使用 `/api`。默认的远程桌面路径不使用 `--trusted-host`:连接插件的 `/api` 围栏对公网和局域网主机保持关闭,已配对 PC 改走 `/remote`。`--trusted-host` 是 SDK 的另一种用法,会让该主机被信任访问 `/api` 本身——配对不门控 `/api`(任何插件都无法做到;围栏是 SDK 自己的接缝)。当 `/api` 无需配对即可到达时,下方姿态探测会报告该姿态。
16
+ - **已配对模型目录**:已认证的已配对设备可使用 `GET /api/pair/model-catalog`、`POST /api/pair/model-catalog/discover` 和 `POST /api/pair/model-catalog/upsert`,仅查看并采纳一个现有、活跃的 `llm-pi-ai` provider 的模型。该能力在内部固定 provider 的 settings 地址,不能创建 provider,也不能读取或修改凭据、通用 settings、端点、header 或任意配置。这三条精确路由在 connection 插件的 `/api` 前缀之前匹配,已配对的局域网或隧道客户端可直接调用,并套用与 `/api/pair` 兄弟端点相同的 Host/Origin 信任围栏:仅接受 loopback、广告的局域网字面量或配置的公网 base URL。经 `/remote` 桌面通道访问时,所有 `/api/pair/*` 路径仍保持仅 loopback。当 provider 的实时模型目录不可用或未知时,采纳会被拒绝。停止或撤销设备会立即禁用它;通用 `settings.*`、`credentials.*` 和 `llm.discoverModels` RPC 方法仍然仅限 loopback。
17
+ - **远程桌面通道**:`requirePairingForLan` 开启(默认)时,经局域网 URL 或隧道打开的桌面 Web GUI 透明地改走 `/remote`——同一套 UI,同一枚配对 cookie 门控。改写补丁在解析期即生效:宿主把一段内联 classic script 注入到 `<head>` 开标签之后(`webserver/index-inject`),使 fetch/WebSocket/EventSource 与资源 `src` 改写在任何启动条目执行前就绪(连接插件最先打开事件流);浏览器半边随后接管已安装的 seat 而非重复打补丁。loopback 来源完全跳过该补丁。浏览器中 `/api`、`/sidebar`、`/git`、`/pet` 下的请求,以及已知的事件、终端与 SSH WebSocket,会在不转发远程 Origin 或配对 cookie 的前提下重新发给本机 Web Server;认证反代会为兄弟路由围栏提供自己生成的同源浏览器标记。SDK 的 loopback 专属特权方法(原生对话框、settings 与 credentials 面)对已配对远程桌面保持不可达;`/api/pair/*`(包括三条模型目录路由)、`/api/update/*`、`/api/plugin-manager/*`、`/api/dsh-desktop-launcher/*` 与 `/api/dsh-web-ui-settings/*` 控制端点保持仅 loopback。未配对的桌面浏览器看到持久的全页阻断界面而不是数据(阻断页按错误码 `unpaired` 触发,不是任意 403)。阻断页在任一门控调用成功后,或通道本身被拆除时退出——把 `requirePairingForLan` 关掉(或关闭插件)也会清掉通道短暂激活期间弹出的提示。
17
18
  - **姿态探测**:插件用伪造 Host 头(公网 base 与每个 LAN base)探测 SDK 的 `/api` 围栏。403 是默认姿态(围栏关闭,远程访问走配对)。任何非 403 结果——`--trusted-host`,或 `--host 0.0.0.0` 下 SDK 的局域网自动信任——都会以 CRITICAL 日志与配对面板红色横幅呈现,因此 SDK 的 `/api` 信任姿态是被看见的,而不是被假设的。探测失败会丢掉进行中的目标缓存,相同 origin 会在下次触发时重试。
18
19
  - **实时状态**:桌面面板经 SSE 流实时镜像配对状态(等待 → 已连接 → 已断开),并展示已授权设备名单。
19
20
  - **远程更新**:侧边栏加载后,底部的下载触发按钮(手机图标左侧)会静默探测 npm registry 上已安装的 `@linxin666/dsh-*` 全家桶版本;发现可自动更新的新版本时,按钮显示圆点并提供“发现新版本,检查更新”提示。点击按钮打开更新面板;未安装聚合包时,检查和更新覆盖 profile 中 registry 管理的全部 `@linxin666/*` 直接依赖,本地 link / file 开发依赖会被跳过。当存在较新版本时,面板展示版本对比并等待确认——点击「开始更新」后执行更新(在所属 dsh profile 内 `pnpm update --latest`;pnpm 缺失时依次回退 `corepack pnpm`、`npx --yes pnpm`,Windows 上经 `cmd.exe` 执行以解析 npm 安装的 `.cmd` shim;由仅 loopback 的 `/api/update/status` + `/api/update/run` 端点驱动)并请求重启 dsh web 以生效。pnpm 绿色退出后还会对照 registry 复核已装版本:绿色退出但版本纹丝不动(例如 pnpm 的 `minimumReleaseAge` 门禁静默跳过同日发布的新版本)会报告为「未更新成功」并附配置指引,而不是误报成功。锚点自身是本地 link 安装(开发模式)时,只报告 npm 状态而不更新。
@@ -71,13 +72,13 @@ dsh plugin --profile web add link:$(pwd)/packages/dsh-remote-web-ui
71
72
  6. 桌面徽标实时翻到 已连接;手机离开时回落到离线/断开。
72
73
  7. 刷新二维码 使旧链接失效并铸一枚新的。停止 撤销移动端访问:已配对设备下一次请求 403,包括其实时流。
73
74
 
74
- 该移动端界面完全自包含在本插件内:`/m/` 页面及其数据通道(`/m/api`)由插件自己的路由伺服,**无需任何 harness 源码改动**——手机的 RPC 调用走插件的 `/m/api` 代理(它委托给 host 的 ApiProxy 服务并自己分页 `session.list`),因此被隧道化的 Host 永远不必进入连接插件的信任围栏。手机受其已配对设备 cookie 与显式方法白名单门控(settings/credentials/host-action 域手机永远不可达;模型读写限制于建议性的 `session.models` / `session.selectModel` 对,Agent 预设访问限制于只读 `agentPreset.list`,创建限制于 `session.create`(工作区 id 加清单中的可选 id——手机绝不自命名工作目录),权限选择器只通过已放行的 `session.prompt` 发送模式无关的 `/permission` 命令);实时流在 `/m/api/events.mux` 上经 Server-Sent Events 送达。规范的 `/m/` 页面拥有同 scope 的 manifest 与 Service Worker;其缓存仅包含静态壳和离线页,绝不包含移动端 API 响应、会话数据或命令。
75
+ 该移动端界面完全自包含在本插件内:`/m/` 页面及其数据通道(`/m/api`)由插件自己的路由伺服,**无需任何 harness 源码改动**——手机的 RPC 调用走插件的 `/m/api` 代理(它委托给 host 的 ApiProxy 服务并自己分页 `session.list`),因此被隧道化的 Host 永远不必进入连接插件的信任围栏。手机受其已配对设备 cookie 与 `/m/api` 显式方法白名单门控(白名单只约束 `/m/api` 代理本身:该 cookie 同时通过全局 api/gate,对宿主 `/api` 面是全量控制凭证——settings/credentials/host-action 域仅因 SDK 把这些特权方法钉为仅 loopback 才不可达。配对即对设备的完全信任;`/m/api` 代理的模型读写限制于建议性的 `session.models` / `session.selectModel` 对;另外,精确的已配对模型目录路由可为一个现有、合格的 `llm-pi-ai` provider 采纳模型,但不能创建 provider,也不能访问凭据或通用 settings;Agent 预设访问限制于只读 `agentPreset.list`,创建限制于 `session.create`(工作区 id 加清单中的可选 id——手机绝不自命名工作目录),权限选择器只通过已放行的 `session.prompt` 发送模式无关的 `/permission` 命令);实时流在 `/m/api/events.mux` 上经 Server-Sent Events 送达。规范的 `/m/` 页面拥有同 scope 的 manifest 与 Service Worker;其缓存仅包含静态壳和离线页,绝不包含移动端 API 响应、会话数据或命令。
75
76
 
76
77
  ### 行为说明
77
78
 
78
79
  - 移动端输入框默认 Enter 发送(Shift+Enter 换行)。在插件设置卡片(或 profile patch)把 `mobileEnterToSend` 设为 false 后,普通 Enter 改为插入换行,只有「发送」按钮会发送;手机打开聊天时经自己的 `/m/api` 偏好方法读取该开关。在支持 `field-sizing: content` 的浏览器上,输入框随草稿自动增高,最高 120px 封顶(两种模式一致)。
79
80
  - `/m/` Worker 对静态壳使用 network-first 回退,并等待当前页面关闭后才激活更新。它旁路 `/m/api`、`/api`、SSE 与所有写请求。
80
- - 安装本插件后,非 loopback 桌面中受围栏保护的流量会改走门控的 `/remote` 通道(见 `src/index.ts` 的 `requirePairingForLan`)。经局域网 URL 或隧道打开的桌面浏览器必须像任何远程设备一样配对——未配对状态显示完整阻断页而不是数据;loopback(127.0.0.1)不受影响,继续使用原始路径。把 profile patch 里 `requirePairingForLan` 设为 false 可让桌面继续走普通 `/api`(仅在开放局域网姿态下有意义),同时保留令牌/状态/撤销。注意 `/api` 路由本体属于 SDK:`--host 0.0.0.0` 绑定时 SDK 自动信任局域网字面量,绕过 UI 的局域网客户端仍能直接访问 `/api`——姿态探测会在面板上报告该姿态。
81
+ - 安装本插件后,非 loopback 桌面中受围栏保护的流量会改走门控的 `/remote` 通道(见 `src/index.ts` 的 `requirePairingForLan`)。在该开关开启(默认)时,经局域网 URL 或隧道打开的桌面浏览器必须像任何远程设备一样配对——未配对状态显示完整阻断页而不是数据;loopback(127.0.0.1)不受影响,继续使用原始路径。把 profile patch 里 `requirePairingForLan` 设为 false 可让桌面继续走普通 `/api`,残留的 `/remote` 改写也会放行未配对调用(仅在开放局域网姿态下有意义;loopback-only 路径仍被拒绝),同时保留令牌/状态/撤销。注意 `/api` 路由本体属于 SDK:`--host 0.0.0.0` 绑定时 SDK 自动信任局域网字面量,绕过 UI 的局域网客户端仍能直接访问 `/api`——姿态探测会在面板上报告该姿态。
81
82
  - `/api` 之外的兄弟 host 路由(`/pet/*`、`/git/*`、右侧面板的 `/aionui-panel/*`)可查询本插件的 `remoteWebUiPairing` 服务:有效的已配对设备 cookie 是放行路径,`stop()` 仍会切断它们;未安装本插件时该服务不存在。
82
83
  - 二维码链接基于机器的非内部 IPv4 字面量构建;多宿主主机(Wi-Fi + 有线,或代理/VPN 虚拟适配器)会显示单选器供你发布手机实际可达的网络。第一个字面量是默认值。设 `publicBaseUrl` 后,单选器在顶部额外加一项 公网地址——默认二维码改用公网 base,选中局域网字面量会重新铸一枚网内链接。
83
84
  - 配置的 `publicBaseUrl` 本身满足可达绑定需求:`dsh web` 绑定 `127.0.0.1`(不带 `--host 0.0.0.0`)仍能经隧道铸出可用的公网二维码链接。
@@ -168,6 +169,10 @@ pnpm run build
168
169
 
169
170
  公网路径是经隧道的同一往返(见「通过互联网远程访问」):loopback mint → 手机或 PC 打开公网二维码 URL → accept → UI。只有 `publicBaseUrl`(插件配置)命名隧道主机;`--trusted-host` 不属于这条配对流。桌面面板仍在 `http://127.0.0.1` 打开。
170
171
 
172
+ ## 安全模型
173
+
174
+ - 移动端普通接口与 mux 事件流都要求有效的已配对设备会话。会话缺失或已撤销时返回 HTTP 403,并使用带 `error.code: "unpaired"` 的 JSON 拒绝信封;浏览器 `EventSource` API 只暴露事件流失败,不暴露该响应体。
175
+
171
176
  ## 已知限制与待办
172
177
 
173
178
  - **撤销是逐请求的**:已配对手机请求已在 停止 落地时在途,完成该请求;下一个 403。
package/lib/client.js CHANGED
@@ -3781,6 +3781,38 @@ window.__ModuleLoader__.load({
3781
3781
  page.replaceState(`${url.pathname}${url.search}${url.hash}`);
3782
3782
  }
3783
3783
  //#endregion
3784
+ //#region src/remote-channel-rules.ts
3785
+ /**
3786
+ * The remote-channel rewrite contract as pure data (issue #987): both the
3787
+ * browser patch (client/remote-channel.ts) and the parse-time boot patch
3788
+ * (remote-channel-boot.ts, inlined into index.html by the host) decide from
3789
+ * these tables, so the two can never drift apart.
3790
+ * @module @linxin666/dsh-remote-web-ui/remote-channel-rules
3791
+ */
3792
+ /** The gated mirror prefix (must match src/remote-methods.ts). */
3793
+ const REMOTE_PREFIX = "/remote";
3794
+ /** The live rule set. */
3795
+ const REMOTE_CHANNEL_RULES = {
3796
+ remotePrefix: REMOTE_PREFIX,
3797
+ apiPrefix: "/api/",
3798
+ pairPrefix: "/api/pair/",
3799
+ updatePrefix: "/api/update/",
3800
+ desktopLauncherPrefix: "/api/dsh-desktop-launcher",
3801
+ settingsBridgePrefix: "/api/dsh-web-ui-settings",
3802
+ sidebarPrefix: "/sidebar/",
3803
+ gitPrefix: "/git/",
3804
+ petPrefix: "/pet/",
3805
+ wsPaths: [
3806
+ "/api/events.mux",
3807
+ "/api/events.host",
3808
+ "/sidebar/ws/terminal",
3809
+ "/sidebar/ws/agent-terminals",
3810
+ "/api/dsh-ssh/terminal"
3811
+ ]
3812
+ };
3813
+ /** The window global the boot patch publishes its seat under. */
3814
+ const REMOTE_CHANNEL_BOOT_GLOBAL = "__DSH_REMOTE_CHANNEL_BOOT__";
3815
+ //#endregion
3784
3816
  //#region src/client/remote-channel.ts
3785
3817
  /**
3786
3818
  * The remote desktop channel — browser half. On a non-loopback origin (LAN
@@ -3807,23 +3839,7 @@ window.__ModuleLoader__.load({
3807
3839
  * Pure helpers are exported for unit tests; `installRemoteChannel` patches
3808
3840
  * the given window and returns their restore.
3809
3841
  */
3810
- /** The gated mirror prefix (must match src/remote-methods.ts). */
3811
- const REMOTE_PREFIX = "/remote";
3812
- const API_PREFIX = "/api/";
3813
- const PAIR_PREFIX = "/api/pair/";
3814
- const UPDATE_PREFIX = "/api/update/";
3815
- const DESKTOP_LAUNCHER_PREFIX = "/api/dsh-desktop-launcher";
3816
- const SETTINGS_BRIDGE_PREFIX = "/api/dsh-web-ui-settings";
3817
- const SIDEBAR_PREFIX = "/sidebar/";
3818
- const GIT_PREFIX = "/git/";
3819
- const PET_PREFIX = "/pet/";
3820
- const WS_PATHS = /* @__PURE__ */ new Set([
3821
- "/api/events.mux",
3822
- "/api/events.host",
3823
- "/sidebar/ws/terminal",
3824
- "/sidebar/ws/agent-terminals",
3825
- "/api/dsh-ssh/terminal"
3826
- ]);
3842
+ const RULES = REMOTE_CHANNEL_RULES;
3827
3843
  /** Decide whether a remote desktop channel is required from local or host policy. */
3828
3844
  function remoteChannelRequired(hostname, snapshot, hostPairingPolicy) {
3829
3845
  if (isLoopbackHostname(hostname)) return false;
@@ -3847,14 +3863,14 @@ window.__ModuleLoader__.load({
3847
3863
  * @param pathname - the request URL pathname.
3848
3864
  */
3849
3865
  function shouldRewriteFetchPath(pathname) {
3850
- if (pathname.startsWith(PAIR_PREFIX)) return false;
3851
- if (pathname.startsWith(UPDATE_PREFIX)) return false;
3852
- if (pathname === DESKTOP_LAUNCHER_PREFIX || pathname.startsWith(`${DESKTOP_LAUNCHER_PREFIX}/`)) return false;
3853
- if (pathname === SETTINGS_BRIDGE_PREFIX || pathname.startsWith(`${SETTINGS_BRIDGE_PREFIX}/`)) return false;
3854
- if (pathname.startsWith(API_PREFIX)) return true;
3855
- if (pathname.startsWith(SIDEBAR_PREFIX) || pathname === "/sidebar") return true;
3856
- if (pathname.startsWith(GIT_PREFIX) || pathname === "/git") return true;
3857
- if (pathname.startsWith(PET_PREFIX) || pathname === "/pet") return true;
3866
+ if (pathname.startsWith(RULES.pairPrefix)) return false;
3867
+ if (pathname.startsWith(RULES.updatePrefix)) return false;
3868
+ if (pathname === RULES.desktopLauncherPrefix || pathname.startsWith(`${RULES.desktopLauncherPrefix}/`)) return false;
3869
+ if (pathname === RULES.settingsBridgePrefix || pathname.startsWith(`${RULES.settingsBridgePrefix}/`)) return false;
3870
+ if (pathname.startsWith(RULES.apiPrefix)) return true;
3871
+ if (pathname.startsWith(RULES.sidebarPrefix) || pathname === "/sidebar") return true;
3872
+ if (pathname.startsWith(RULES.gitPrefix) || pathname === "/git") return true;
3873
+ if (pathname.startsWith(RULES.petPrefix) || pathname === "/pet") return true;
3858
3874
  return false;
3859
3875
  }
3860
3876
  /**
@@ -3862,7 +3878,7 @@ window.__ModuleLoader__.load({
3862
3878
  * @param pathname - the WebSocket URL pathname.
3863
3879
  */
3864
3880
  function shouldRewriteWsPath(pathname) {
3865
- return WS_PATHS.has(pathname);
3881
+ return RULES.wsPaths.includes(pathname);
3866
3882
  }
3867
3883
  /** The gated twin of one fenced path (`/remote` + original pathname). */
3868
3884
  function rewritePath(pathname) {
@@ -4216,19 +4232,34 @@ window.__ModuleLoader__.load({
4216
4232
  showFenceNotice();
4217
4233
  };
4218
4234
  const channelActive = () => remoteChannelRequired(window.location.hostname, settingsScope.getSnapshot(), hostPairingPolicy);
4235
+ const bootSeat = () => window[REMOTE_CHANNEL_BOOT_GLOBAL];
4219
4236
  const syncChannel = () => {
4220
4237
  const transition = channelTransition(channelActive(), disposeChannel !== void 0);
4221
- if (transition === "install") disposeChannel = ctx.effect(() => {
4222
- return installRemoteChannel(window, {
4223
- onUnpaired: handleUnpaired,
4224
- onPaired: hideFenceNotice
4225
- });
4226
- }, "remote-web-ui: remote desktop channel");
4227
- else if (transition === "retire" && disposeChannel !== void 0) {
4238
+ if (transition === "install") {
4239
+ const seat = bootSeat();
4240
+ if (seat !== void 0) {
4241
+ seat.onUnpaired = handleUnpaired;
4242
+ seat.onPaired = hideFenceNotice;
4243
+ if (seat.pendingUnpaired) {
4244
+ seat.pendingUnpaired = false;
4245
+ handleUnpaired();
4246
+ }
4247
+ disposeChannel = ctx.effect(() => () => {
4248
+ seat.onUnpaired = null;
4249
+ seat.onPaired = null;
4250
+ }, "remote-web-ui: remote desktop channel (boot patch)");
4251
+ } else disposeChannel = ctx.effect(() => {
4252
+ return installRemoteChannel(window, {
4253
+ onUnpaired: handleUnpaired,
4254
+ onPaired: hideFenceNotice
4255
+ });
4256
+ }, "remote-web-ui: remote desktop channel");
4257
+ } else if (transition === "retire" && disposeChannel !== void 0) {
4228
4258
  disposeChannel();
4229
4259
  disposeChannel = void 0;
4260
+ bootSeat()?.restore();
4230
4261
  hideFenceNotice();
4231
- }
4262
+ } else if (transition === "none" && !channelActive()) bootSeat()?.restore();
4232
4263
  };
4233
4264
  settingsScope.subscribe(syncChannel);
4234
4265
  syncChannel();