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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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: d3195d3b8a3dc58cf077538b0e18455b7d1706a6
6
+ README.zh.md: c0a06bf7ae0b49460e6f4aa250b42f2c227b4e0a
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.
@@ -244,12 +245,14 @@ over Server-Sent Events on `/m/api/events.mux`. The canonical `/m/` page owns a
244
245
  - 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
246
  - Installing this plugin routes fenced non-loopback desktop traffic onto the
246
247
  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
248
+ While the flag is on (default), a desktop browser opened via the LAN URL or
249
+ the tunnel must pair like any remote device — the unpaired state shows a
250
+ persistent blocking page instead of data; loopback (127.0.0.1) is unaffected
251
+ and keeps `/api`. Set `requirePairingForLan: false` in the profile patch to
252
+ keep the desktop on plain `/api` and have a leftover `/remote` rewrite
253
+ proxy unpaired calls (only useful with the open-LAN stance; loopback-only
254
+ paths stay denied) while keeping tokens/status/revocation. Note the
255
+ underlying `/api` route itself is the
253
256
  SDK's: on a `--host 0.0.0.0` bind the SDK auto-trusts LAN literals, so a
254
257
  LAN client bypassing the UI can still reach `/api` directly — the posture
255
258
  probe reports that stance on the panel.
package/README.zh.md CHANGED
@@ -12,7 +12,7 @@
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` 无需配对即可到达时,下方姿态探测会报告该姿态。
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
16
  - **远程桌面通道**:`requirePairingForLan` 开启(默认)时,经局域网 URL 或隧道打开的桌面 Web GUI 透明地改走 `/remote`——同一套 UI,同一枚配对 cookie 门控。浏览器中 `/api`、`/sidebar`、`/git`、`/pet` 下的请求,以及已知的事件、终端与 SSH WebSocket,会在不转发远程 Origin 或配对 cookie 的前提下重新发给本机 Web Server;认证反代会为兄弟路由围栏提供自己生成的同源浏览器标记。SDK 的 loopback 专属特权方法(原生对话框、settings 与 credentials 面)对已配对远程桌面保持不可达,`/api/pair/*`、`/api/update/*`、`/api/plugin-manager/*`、`/api/dsh-desktop-launcher/*` 与 `/api/dsh-web-ui-settings/*` 控制端点保持仅 loopback。未配对的桌面浏览器看到持久的全页阻断界面而不是数据(阻断页按错误码 `unpaired` 触发,不是任意 403)。阻断页在任一门控调用成功后,或通道本身被拆除时退出——把 `requirePairingForLan` 关掉(或关闭插件)也会清掉通道短暂激活期间弹出的提示。
17
17
  - **姿态探测**:插件用伪造 Host 头(公网 base 与每个 LAN base)探测 SDK 的 `/api` 围栏。403 是默认姿态(围栏关闭,远程访问走配对)。任何非 403 结果——`--trusted-host`,或 `--host 0.0.0.0` 下 SDK 的局域网自动信任——都会以 CRITICAL 日志与配对面板红色横幅呈现,因此 SDK 的 `/api` 信任姿态是被看见的,而不是被假设的。探测失败会丢掉进行中的目标缓存,相同 origin 会在下次触发时重试。
18
18
  - **实时状态**:桌面面板经 SSE 流实时镜像配对状态(等待 → 已连接 → 已断开),并展示已授权设备名单。
@@ -77,7 +77,7 @@ dsh plugin --profile web add link:$(pwd)/packages/dsh-remote-web-ui
77
77
 
78
78
  - 移动端输入框默认 Enter 发送(Shift+Enter 换行)。在插件设置卡片(或 profile patch)把 `mobileEnterToSend` 设为 false 后,普通 Enter 改为插入换行,只有「发送」按钮会发送;手机打开聊天时经自己的 `/m/api` 偏好方法读取该开关。在支持 `field-sizing: content` 的浏览器上,输入框随草稿自动增高,最高 120px 封顶(两种模式一致)。
79
79
  - `/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`——姿态探测会在面板上报告该姿态。
80
+ - 安装本插件后,非 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
81
  - `/api` 之外的兄弟 host 路由(`/pet/*`、`/git/*`、右侧面板的 `/aionui-panel/*`)可查询本插件的 `remoteWebUiPairing` 服务:有效的已配对设备 cookie 是放行路径,`stop()` 仍会切断它们;未安装本插件时该服务不存在。
82
82
  - 二维码链接基于机器的非内部 IPv4 字面量构建;多宿主主机(Wi-Fi + 有线,或代理/VPN 虚拟适配器)会显示单选器供你发布手机实际可达的网络。第一个字面量是默认值。设 `publicBaseUrl` 后,单选器在顶部额外加一项 公网地址——默认二维码改用公网 base,选中局域网字面量会重新铸一枚网内链接。
83
83
  - 配置的 `publicBaseUrl` 本身满足可达绑定需求:`dsh web` 绑定 `127.0.0.1`(不带 `--host 0.0.0.0`)仍能经隧道铸出可用的公网二维码链接。
package/lib/index.js CHANGED
@@ -1989,17 +1989,19 @@ function loopbackOnlyDenial(innerPath) {
1989
1989
  }
1990
1990
  /**
1991
1991
  * Build the remote desktop channel HTTP routes.
1992
- * @param deps - pairing service + local port.
1992
+ * @param deps - pairing service + local port + live pairing policy.
1993
1993
  * @returns the routes to register on webServer.
1994
1994
  */
1995
1995
  function makeRemoteApiRoutes(deps) {
1996
- const { service, port } = deps;
1996
+ const { service, port, requirePairingForLan = true } = deps;
1997
1997
  const handler = (req, res) => {
1998
- const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
1999
- if (!(deviceId !== void 0 && service.touchDevice(deviceId))) {
2000
- req.resume();
2001
- envelopeError(res, 403, "invalid-request", "unpaired", "this device is not paired with the desktop");
2002
- return;
1998
+ if (typeof requirePairingForLan === "function" ? requirePairingForLan() : requirePairingForLan) {
1999
+ const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
2000
+ if (!(deviceId !== void 0 && service.touchDevice(deviceId))) {
2001
+ req.resume();
2002
+ envelopeError(res, 403, "invalid-request", "unpaired", "this device is not paired with the desktop");
2003
+ return;
2004
+ }
2003
2005
  }
2004
2006
  const method = req.method ?? "GET";
2005
2007
  if (!ALLOWED_METHODS.has(method)) {
@@ -2046,17 +2048,19 @@ function upgradeInnerPath(reqUrl, fallbackPath) {
2046
2048
  /**
2047
2049
  * Build the WebSocket upgrade routes for the event streams and known plugin
2048
2050
  * sockets. webServer matches upgrades by exact path.
2049
- * @param deps - pairing service + local port.
2051
+ * @param deps - pairing service + local port + live pairing policy.
2050
2052
  * @returns the upgrade routes to register on webServer.
2051
2053
  */
2052
2054
  function makeRemoteApiUpgradeRoutes(deps) {
2053
- const { service, port } = deps;
2055
+ const { service, port, requirePairingForLan = true } = deps;
2054
2056
  const handlerFor = (fallbackPath) => (req, socket, head) => {
2055
- const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
2056
- if (deviceId === void 0 || !service.touchDevice(deviceId)) {
2057
- socket.write("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
2058
- socket.destroy();
2059
- return;
2057
+ if (typeof requirePairingForLan === "function" ? requirePairingForLan() : requirePairingForLan) {
2058
+ const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
2059
+ if (deviceId === void 0 || !service.touchDevice(deviceId)) {
2060
+ socket.write("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
2061
+ socket.destroy();
2062
+ return;
2063
+ }
2060
2064
  }
2061
2065
  const inner = upgradeInnerPath(req.url, fallbackPath);
2062
2066
  if (loopbackOnlyDenial(inner.split("?")[0] ?? inner) !== void 0) {
@@ -3198,13 +3202,15 @@ function applyImpl(ctx, config) {
3198
3202
  }) : [],
3199
3203
  ...makeRemoteApiRoutes({
3200
3204
  service,
3201
- port: ctx.webServer.port
3205
+ port: ctx.webServer.port,
3206
+ requirePairingForLan: () => resolve().requirePairingForLan
3202
3207
  }),
3203
3208
  ...updateRoutes
3204
3209
  ];
3205
3210
  const upgrades = makeRemoteApiUpgradeRoutes({
3206
3211
  service,
3207
- port: ctx.webServer.port
3212
+ port: ctx.webServer.port,
3213
+ requirePairingForLan: () => resolve().requirePairingForLan
3208
3214
  });
3209
3215
  const gate = makeGateListener(service, () => resolve().requirePairingForLan, () => resolve().enabled);
3210
3216
  ctx.effect(() => ctx.on("api/gate", gate), "remote-web-ui: api gate");
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,OAAO,CAAC,MAAM,aAAa,CAAA;AAE3B,OAAO,EAA0C,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AAsBzF,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,MAAM;QACd;;;;;WAKG;QACH,UAAU,CACR,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,IAAI,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GACrC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;KAC9B;CACF;AAED,iCAAiC;AACjC,eAAO,MAAM,IAAI,kBAAkB,CAAA;AAEnC,+DAA+D;AAC/D,eAAO,MAAM,MAAM,UAA4B,CAAA;AAE/C;;;;GAIG;AACH,eAAO,MAAM,gCAAgC,uDAAqC,CAAA;AAElF,qEAAqE;AACrE,MAAM,WAAW,MAAM;IACrB,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;;;OAMG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,2EAA2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,eAAO,MAAM,MAAM,EAAE,CAAC,CAAC,MAAM,CAY3B,CAAA;AAKF;;;;GAIG;AACH,KAAK,cAAc,GAAG,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,eAAe,GAAG,aAAa,CAAC,CAAC,GAAG;IAC9E,aAAa,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,IAAI,CAC5C,cAAc,EACd,YAAY,GAAG,gBAAgB,GAAG,YAAY,GAAG,cAAc,GAAG,YAAY,GAAG,aAAa,CAC/F,GAAG,aAAa,CAShB;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,IAAI,GAAE,MAAkB,GAAG,MAAM,CAEnE;AAiBD;;;;GAIG;AACH,eAAO,MAAM,KAAK,kBAAuD,CAAA;AAEzE,iBAAS,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAuRtD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,OAAO,CAAC,MAAM,aAAa,CAAA;AAE3B,OAAO,EAA0C,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AAsBzF,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,MAAM;QACd;;;;;WAKG;QACH,UAAU,CACR,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,IAAI,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GACrC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;KAC9B;CACF;AAED,iCAAiC;AACjC,eAAO,MAAM,IAAI,kBAAkB,CAAA;AAEnC,+DAA+D;AAC/D,eAAO,MAAM,MAAM,UAA4B,CAAA;AAE/C;;;;GAIG;AACH,eAAO,MAAM,gCAAgC,uDAAqC,CAAA;AAElF,qEAAqE;AACrE,MAAM,WAAW,MAAM;IACrB,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;;;OAMG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,2EAA2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,eAAO,MAAM,MAAM,EAAE,CAAC,CAAC,MAAM,CAY3B,CAAA;AAKF;;;;GAIG;AACH,KAAK,cAAc,GAAG,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,eAAe,GAAG,aAAa,CAAC,CAAC,GAAG;IAC9E,aAAa,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,IAAI,CAC5C,cAAc,EACd,YAAY,GAAG,gBAAgB,GAAG,YAAY,GAAG,cAAc,GAAG,YAAY,GAAG,aAAa,CAC/F,GAAG,aAAa,CAShB;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,IAAI,GAAE,MAAkB,GAAG,MAAM,CAEnE;AAiBD;;;;GAIG;AACH,eAAO,MAAM,KAAK,kBAAuD,CAAA;AAEzE,iBAAS,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAkStD"}
@@ -253,12 +253,23 @@ function applyImpl(ctx, config) {
253
253
  ...(apiProxy !== undefined
254
254
  ? makeMobileApiRoutes({ service, apiProxy, mobileEnterToSend: () => resolve().mobileEnterToSend })
255
255
  : []),
256
- // The remote desktop channel: paired-cookie-gated `/remote` prefix that
257
- // re-issues fenced paths to loopback (see remote-api.ts).
258
- ...makeRemoteApiRoutes({ service, port: ctx.webServer.port }),
256
+ // The remote desktop channel: policy-gated `/remote` prefix that
257
+ // re-issues fenced paths to loopback (see remote-api.ts). The live
258
+ // requirePairingForLan is re-read per request, same as the gate listener
259
+ // and routes above, so a stale client rewrite on an open-LAN deployment
260
+ // proxies instead of 403ing.
261
+ ...makeRemoteApiRoutes({
262
+ service,
263
+ port: ctx.webServer.port,
264
+ requirePairingForLan: () => resolve().requirePairingForLan,
265
+ }),
259
266
  ...updateRoutes,
260
267
  ];
261
- const upgrades = makeRemoteApiUpgradeRoutes({ service, port: ctx.webServer.port });
268
+ const upgrades = makeRemoteApiUpgradeRoutes({
269
+ service,
270
+ port: ctx.webServer.port,
271
+ requirePairingForLan: () => resolve().requirePairingForLan,
272
+ });
262
273
  const gate = makeGateListener(service, () => resolve().requirePairingForLan, () => resolve().enabled);
263
274
  ctx.effect(() => ctx.on('api/gate', gate), 'remote-web-ui: api gate');
264
275
  // ── posture probe ─────────────────────────────────────────────────────────
@@ -7,8 +7,10 @@
7
7
  * and no per-plugin pairing consult.
8
8
  *
9
9
  * Security model:
10
- * - Every request must carry a live paired-device cookie, enforced before
11
- * any bytes are forwarded and before any host call.
10
+ * - While `requirePairingForLan` is on (default), every request must carry a
11
+ * live paired-device cookie, enforced before any bytes are forwarded and
12
+ * before any host call. With the policy off, the cookie gate is skipped
13
+ * (the loopback-only denials below still apply).
12
14
  * - The SDK's loopback-only privileged methods (native dialogs, the settings
13
15
  * plane, credentials — the `PRIVILEGED_METHODS` set of client-connection)
14
16
  * are denied here. The set is pinned by tests/remote-contract.spec.ts.
@@ -29,6 +31,13 @@ export interface RemoteApiDeps {
29
31
  service: PairingService;
30
32
  /** The local webServer port the loopback proxy connects to. */
31
33
  port: number;
34
+ /**
35
+ * Live policy: whether the paired-device cookie gates the /remote channel.
36
+ * When false, requests are proxied without a cookie (the loopback-only
37
+ * denials still apply). A function is re-read per request, so a settings
38
+ * edit takes effect without a restart. Defaults to true.
39
+ */
40
+ requirePairingForLan?: boolean | (() => boolean);
32
41
  }
33
42
  /**
34
43
  * Map `/remote/...` to the inner path, or undefined when the outer path is
@@ -42,7 +51,7 @@ export declare function innerPathOf(pathname: string): string | undefined;
42
51
  export declare function loopbackOnlyDenial(innerPath: string): string | undefined;
43
52
  /**
44
53
  * Build the remote desktop channel HTTP routes.
45
- * @param deps - pairing service + local port.
54
+ * @param deps - pairing service + local port + live pairing policy.
46
55
  * @returns the routes to register on webServer.
47
56
  */
48
57
  export declare function makeRemoteApiRoutes(deps: RemoteApiDeps): WebRoute[];
@@ -53,7 +62,7 @@ export declare function upgradeInnerPath(reqUrl: string | undefined, fallbackPat
53
62
  /**
54
63
  * Build the WebSocket upgrade routes for the event streams and known plugin
55
64
  * sockets. webServer matches upgrades by exact path.
56
- * @param deps - pairing service + local port.
65
+ * @param deps - pairing service + local port + live pairing policy.
57
66
  * @returns the upgrade routes to register on webServer.
58
67
  */
59
68
  export declare function makeRemoteApiUpgradeRoutes(deps: RemoteApiDeps): WebUpgradeRoute[];
@@ -1 +1 @@
1
- {"version":3,"file":"remote-api.d.ts","sourceRoot":"","sources":["../../src/remote-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAA;AAChF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAclD,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,mBAAmB,EACnB,gBAAgB,EAChB,aAAa,EACb,oBAAoB,EACpB,2BAA2B,GAC5B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAgBvD,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,cAAc,CAAA;IACvB,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAA;CACb;AAWD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUhE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAsBxE;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,CAsCnE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAWzF;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,aAAa,GAAG,eAAe,EAAE,CAwBjF"}
1
+ {"version":3,"file":"remote-api.d.ts","sourceRoot":"","sources":["../../src/remote-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAA;AAChF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAclD,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,mBAAmB,EACnB,gBAAgB,EAChB,aAAa,EACb,oBAAoB,EACpB,2BAA2B,GAC5B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAgBvD,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,cAAc,CAAA;IACvB,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,CAAA;CACjD;AAWD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUhE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAsBxE;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,CA2CnE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAWzF;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,aAAa,GAAG,eAAe,EAAE,CA2BjF"}
@@ -7,8 +7,10 @@
7
7
  * and no per-plugin pairing consult.
8
8
  *
9
9
  * Security model:
10
- * - Every request must carry a live paired-device cookie, enforced before
11
- * any bytes are forwarded and before any host call.
10
+ * - While `requirePairingForLan` is on (default), every request must carry a
11
+ * live paired-device cookie, enforced before any bytes are forwarded and
12
+ * before any host call. With the policy off, the cookie gate is skipped
13
+ * (the loopback-only denials below still apply).
12
14
  * - The SDK's loopback-only privileged methods (native dialogs, the settings
13
15
  * plane, credentials — the `PRIVILEGED_METHODS` set of client-connection)
14
16
  * are denied here. The set is pinned by tests/remote-contract.spec.ts.
@@ -95,19 +97,24 @@ export function loopbackOnlyDenial(innerPath) {
95
97
  }
96
98
  /**
97
99
  * Build the remote desktop channel HTTP routes.
98
- * @param deps - pairing service + local port.
100
+ * @param deps - pairing service + local port + live pairing policy.
99
101
  * @returns the routes to register on webServer.
100
102
  */
101
103
  export function makeRemoteApiRoutes(deps) {
102
- const { service, port } = deps;
104
+ const { service, port, requirePairingForLan = true } = deps;
103
105
  const handler = (req, res) => {
104
106
  // Cookie gate first — same order as /m/api. Do not buffer an unpaired body.
105
- const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
106
- const paired = deviceId !== undefined && service.touchDevice(deviceId);
107
- if (!paired) {
108
- req.resume();
109
- envelopeError(res, 403, 'invalid-request', 'unpaired', 'this device is not paired with the desktop');
110
- return;
107
+ // With the live policy off, untrusted-but-policy-open callers are proxied
108
+ // (a stale client rewrite must not 403); loopback-only denials stay below.
109
+ const require = typeof requirePairingForLan === 'function' ? requirePairingForLan() : requirePairingForLan;
110
+ if (require) {
111
+ const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
112
+ const paired = deviceId !== undefined && service.touchDevice(deviceId);
113
+ if (!paired) {
114
+ req.resume();
115
+ envelopeError(res, 403, 'invalid-request', 'unpaired', 'this device is not paired with the desktop');
116
+ return;
117
+ }
111
118
  }
112
119
  const method = req.method ?? 'GET';
113
120
  if (!ALLOWED_METHODS.has(method)) {
@@ -153,17 +160,20 @@ export function upgradeInnerPath(reqUrl, fallbackPath) {
153
160
  /**
154
161
  * Build the WebSocket upgrade routes for the event streams and known plugin
155
162
  * sockets. webServer matches upgrades by exact path.
156
- * @param deps - pairing service + local port.
163
+ * @param deps - pairing service + local port + live pairing policy.
157
164
  * @returns the upgrade routes to register on webServer.
158
165
  */
159
166
  export function makeRemoteApiUpgradeRoutes(deps) {
160
- const { service, port } = deps;
167
+ const { service, port, requirePairingForLan = true } = deps;
161
168
  const handlerFor = (fallbackPath) => (req, socket, head) => {
162
- const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
163
- if (deviceId === undefined || !service.touchDevice(deviceId)) {
164
- socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
165
- socket.destroy();
166
- return;
169
+ const require = typeof requirePairingForLan === 'function' ? requirePairingForLan() : requirePairingForLan;
170
+ if (require) {
171
+ const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
172
+ if (deviceId === undefined || !service.touchDevice(deviceId)) {
173
+ socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
174
+ socket.destroy();
175
+ return;
176
+ }
167
177
  }
168
178
  const inner = upgradeInnerPath(req.url, fallbackPath);
169
179
  const denied = loopbackOnlyDenial(inner.split('?')[0] ?? inner);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-remote-web-ui",
3
3
  "description": "Scan-to-pair mobile and PC remote control for the dsh web GUI: QR entry beside the settings button, one-time pairing tokens, a gated /remote/api desktop channel, live device status, and revocable sessions",
4
- "version": "0.2.8",
4
+ "version": "0.2.9",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
package/src/index.ts CHANGED
@@ -358,12 +358,23 @@ function applyImpl(ctx: Context, config?: Config): void {
358
358
  ...(apiProxy !== undefined
359
359
  ? makeMobileApiRoutes({ service, apiProxy, mobileEnterToSend: () => resolve().mobileEnterToSend })
360
360
  : []),
361
- // The remote desktop channel: paired-cookie-gated `/remote` prefix that
362
- // re-issues fenced paths to loopback (see remote-api.ts).
363
- ...makeRemoteApiRoutes({ service, port: ctx.webServer.port }),
361
+ // The remote desktop channel: policy-gated `/remote` prefix that
362
+ // re-issues fenced paths to loopback (see remote-api.ts). The live
363
+ // requirePairingForLan is re-read per request, same as the gate listener
364
+ // and routes above, so a stale client rewrite on an open-LAN deployment
365
+ // proxies instead of 403ing.
366
+ ...makeRemoteApiRoutes({
367
+ service,
368
+ port: ctx.webServer.port,
369
+ requirePairingForLan: () => resolve().requirePairingForLan,
370
+ }),
364
371
  ...updateRoutes,
365
372
  ]
366
- const upgrades = makeRemoteApiUpgradeRoutes({ service, port: ctx.webServer.port })
373
+ const upgrades = makeRemoteApiUpgradeRoutes({
374
+ service,
375
+ port: ctx.webServer.port,
376
+ requirePairingForLan: () => resolve().requirePairingForLan,
377
+ })
367
378
  const gate = makeGateListener(service, () => resolve().requirePairingForLan, () => resolve().enabled)
368
379
  ctx.effect(() => ctx.on('api/gate', gate), 'remote-web-ui: api gate')
369
380
 
package/src/remote-api.ts CHANGED
@@ -7,8 +7,10 @@
7
7
  * and no per-plugin pairing consult.
8
8
  *
9
9
  * Security model:
10
- * - Every request must carry a live paired-device cookie, enforced before
11
- * any bytes are forwarded and before any host call.
10
+ * - While `requirePairingForLan` is on (default), every request must carry a
11
+ * live paired-device cookie, enforced before any bytes are forwarded and
12
+ * before any host call. With the policy off, the cookie gate is skipped
13
+ * (the loopback-only denials below still apply).
12
14
  * - The SDK's loopback-only privileged methods (native dialogs, the settings
13
15
  * plane, credentials — the `PRIVILEGED_METHODS` set of client-connection)
14
16
  * are denied here. The set is pinned by tests/remote-contract.spec.ts.
@@ -68,6 +70,13 @@ export interface RemoteApiDeps {
68
70
  service: PairingService
69
71
  /** The local webServer port the loopback proxy connects to. */
70
72
  port: number
73
+ /**
74
+ * Live policy: whether the paired-device cookie gates the /remote channel.
75
+ * When false, requests are proxied without a cookie (the loopback-only
76
+ * denials still apply). A function is re-read per request, so a settings
77
+ * edit takes effect without a restart. Defaults to true.
78
+ */
79
+ requirePairingForLan?: boolean | (() => boolean)
71
80
  }
72
81
 
73
82
  /** One SDK-shaped error envelope (keeps the desktop client's parse path intact). */
@@ -125,20 +134,25 @@ export function loopbackOnlyDenial(innerPath: string): string | undefined {
125
134
 
126
135
  /**
127
136
  * Build the remote desktop channel HTTP routes.
128
- * @param deps - pairing service + local port.
137
+ * @param deps - pairing service + local port + live pairing policy.
129
138
  * @returns the routes to register on webServer.
130
139
  */
131
140
  export function makeRemoteApiRoutes(deps: RemoteApiDeps): WebRoute[] {
132
- const { service, port } = deps
141
+ const { service, port, requirePairingForLan = true } = deps
133
142
 
134
143
  const handler = (req: IncomingMessage, res: ServerResponse): void => {
135
144
  // Cookie gate first — same order as /m/api. Do not buffer an unpaired body.
136
- const deviceId = readCookie(req.headers.cookie, service.config.cookieName)
137
- const paired = deviceId !== undefined && service.touchDevice(deviceId)
138
- if (!paired) {
139
- req.resume()
140
- envelopeError(res, 403, 'invalid-request', 'unpaired', 'this device is not paired with the desktop')
141
- return
145
+ // With the live policy off, untrusted-but-policy-open callers are proxied
146
+ // (a stale client rewrite must not 403); loopback-only denials stay below.
147
+ const require = typeof requirePairingForLan === 'function' ? requirePairingForLan() : requirePairingForLan
148
+ if (require) {
149
+ const deviceId = readCookie(req.headers.cookie, service.config.cookieName)
150
+ const paired = deviceId !== undefined && service.touchDevice(deviceId)
151
+ if (!paired) {
152
+ req.resume()
153
+ envelopeError(res, 403, 'invalid-request', 'unpaired', 'this device is not paired with the desktop')
154
+ return
155
+ }
142
156
  }
143
157
 
144
158
  const method = req.method ?? 'GET'
@@ -187,18 +201,21 @@ export function upgradeInnerPath(reqUrl: string | undefined, fallbackPath: strin
187
201
  /**
188
202
  * Build the WebSocket upgrade routes for the event streams and known plugin
189
203
  * sockets. webServer matches upgrades by exact path.
190
- * @param deps - pairing service + local port.
204
+ * @param deps - pairing service + local port + live pairing policy.
191
205
  * @returns the upgrade routes to register on webServer.
192
206
  */
193
207
  export function makeRemoteApiUpgradeRoutes(deps: RemoteApiDeps): WebUpgradeRoute[] {
194
- const { service, port } = deps
208
+ const { service, port, requirePairingForLan = true } = deps
195
209
 
196
210
  const handlerFor = (fallbackPath: string) => (req: IncomingMessage, socket: Duplex, head: Buffer): void => {
197
- const deviceId = readCookie(req.headers.cookie, service.config.cookieName)
198
- if (deviceId === undefined || !service.touchDevice(deviceId)) {
199
- socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n')
200
- socket.destroy()
201
- return
211
+ const require = typeof requirePairingForLan === 'function' ? requirePairingForLan() : requirePairingForLan
212
+ if (require) {
213
+ const deviceId = readCookie(req.headers.cookie, service.config.cookieName)
214
+ if (deviceId === undefined || !service.touchDevice(deviceId)) {
215
+ socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n')
216
+ socket.destroy()
217
+ return
218
+ }
202
219
  }
203
220
  const inner = upgradeInnerPath(req.url, fallbackPath)
204
221
  const denied = loopbackOnlyDenial(inner.split('?')[0] ?? inner)