@riceawa/dsh-lan-gateway 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  <p align="center">
8
8
  <img src="https://img.shields.io/badge/DeepSeek%20Harness-4d6bfe?logo=deepseek&logoColor=fff&style=flat-square" alt="DeepSeek Harness" />
9
- <img src="https://img.shields.io/badge/version-0.3.0-2b7fff?style=flat-square" alt="version 0.3.0" />
9
+ <img src="https://img.shields.io/badge/version-0.5.0-2b7fff?style=flat-square" alt="version 0.5.0" />
10
10
  <img src="https://img.shields.io/badge/TLS-8b5cf6?logo=lock&logoColor=fff&style=flat-square" alt="TLS" />
11
11
  <img src="https://img.shields.io/github/license/rice-awa/dsh-lan-gateway?style=flat-square" alt="MIT license" />
12
12
  </p>
@@ -20,26 +20,38 @@
20
20
 
21
21
  `dsh` 的 web CLI 会硬拒绝 `--host 0.0.0.0`(避免把远程代码执行暴露到网络),所以本插件
22
22
  让 dsh 继续只绑 `127.0.0.1`,自己另起一个 `0.0.0.0` 反向代理网关转发到 loopback 端口,
23
- 改写 `Host`/`Origin` 以通过 `/api` 信任围栏。**LAN 与 loopback 来源免密代理;非 LAN 来源
24
- 必须先完成登录页并出示 HMAC cookie。**
23
+ 改写 `Host`/`Origin`。**默认拒绝(fail-closed):所有来源——loopbackLAN、公网——都必须
24
+ 先通过网关的登录页并出示 HMAC 会话 cookie**;“LAN 免密”改为显式 `lanPasswordless` opt-in
25
+ 且默认关闭。对 dsh ≥ 0.1.2-rc.1(修复了 QVD-2026-57410 的浏览器会话认证底座),网关在进程内
26
+ 中继**一条共享的上游会话**,上游自身的授权仍然把关每个请求——网关只决定谁能骑上这条
27
+ 共享会话。
25
28
 
26
29
  ## 特性
27
30
 
28
- - **双端一体**:host 端是反向代理网关(登录 / HMAC cookie / 信任围栏 / TLS);client 端是
29
- 不安全源 UUID shim + **官方设置页卡片**(DSH Settings → Plugins → 可配置插件,网关的
30
- 端口 / 网段 / 认证 / TLS 全部可视化调整,保存即热生效)。
31
+ - **双端一体**:host 端是反向代理网关(登录 / HMAC cookie / 会话撤销 / Origin 围栏 /
32
+ TLS / 上游会话中继);client 端是不安全源 UUID shim + **官方设置页卡片**(DSH Settings
33
+ Plugins → 可配置插件,网关的端口 / 网段 / 认证 / TLS 全部可视化调整,保存即热生效)。
34
+ - **默认全来源登录(QVD-2026-57410 加固)**:不再“只对公网来源认证”;loopback/LAN 同样
35
+ 要求会话。唯一豁免是显式 `lanPasswordless: true`(仅豁免网关登录,上游会话中继仍生效)。
36
+ - **共享上游会话中继**:dsh 0.1.2 起不再信任 loopback Host,要求出示绑定权威来源的
37
+ `dsh-auth-*` cookie;插件在进程内用启动令牌走一遍浏览器等价换取,把这一条会话中继到
38
+ 每个转发请求上(“单密码 = 单操作者”语义不变)。底座不支持时自动降级为无中继转发。
39
+ - **fail-closed 启动守卫**:未设密码拒启;`authRequired:false`(v0.4 及更早)拒启并给迁移
40
+ 文案;明文监听需显式 `allowInsecurePlaintext:true`(或 TLS / 声明 `trustedTerminator`)。
31
41
  - **TLS 双模式**:`self-signed` 自动生成自签名证书(首次启动生成并持久化到
32
42
  `~/.dsh/lan-gateway/tls/`,重启复用;`lan_gateway tls-regenerate` 可换新证书),或
33
43
  `custom` 直接挂载你自己的 PEM 证书与私钥(如 Let's Encrypt / 自建 CA 签发)。
44
+ - **会话撤销**:cookie 携带撤销 epoch;改密 / 清密 / `rotate-secret` 都会递增 epoch,使
45
+ 所有已签发 cookie **与已建立的 WebSocket** 立即失效。清空密码会直接停止监听。
34
46
  - **默认关闭(安全)**:bundle patch 里 `enabled: false`,只有运行 `lan_gateway enable`
35
47
  后才监听网络端口。
36
48
  - **密钥不进配置**:密码哈希、cookie secret 存 `~/.dsh/lan-gateway/state.json`。
37
49
 
38
50
  ## 快速安装(推荐)
39
51
 
40
- 请把下面这段话发送给你的 agent:
52
+ 已发布到 npm(预构建,安装无需 `allowBuilds` 授权)。请把下面这段话发送给你的 agent:
41
53
 
42
- > 帮我从 `https://github.com/rice-awa/dsh-lan-gateway` 安装这个 dsh 插件,遵循
54
+ > 帮我安装 dsh 插件 `@riceawa/dsh-lan-gateway`,遵循
43
55
  > `https://github.com/rice-awa/dsh-lan-gateway/blob/main/INSTALL.md`
44
56
 
45
57
  ## 配套 skill
@@ -62,18 +74,19 @@ dsh plugin --profile web add github:mexiaosqwq/dsh-web-mobile
62
74
 
63
75
  ## 手动安装
64
76
 
65
- ### 方式 A:使用官方 CLI 安装
77
+ ### 方式 A:npm 包(推荐,免 allowBuilds)
78
+
79
+ 已发布预构建产物到 npm,安装时**不需要**批准构建脚本:
66
80
 
67
81
  ```bash
68
82
  # 官方装配(重启后由 bundles 列表接管,生产态)
69
- dsh plugin --profile web add github:rice-awa/dsh-lan-gateway
83
+ dsh plugin --profile web add @riceawa/dsh-lan-gateway
70
84
  ```
71
85
 
72
- > **注意**:`dsh plugin ... add` 把剩余参数转发给 profile 目录里的 pnpm。安装本插件时
73
- > pnpm 会要求先在其构建脚本白名单(`allowBuilds`)中批准本包,否则报
74
- > `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`。把报错提示中的条目(或 `pnpm approve-builds`
75
- > 的选项)写进 `~/.dsh/profiles/web/pnpm-workspace.yaml` 再重试即可。完整步骤见
76
- > [INSTALL.md](INSTALL.md#for-agents完整安装流程)。
86
+ > `dsh plugin ... add` 把剩余参数转发给 profile 目录里的 pnpm,npm 包自带预构建
87
+ > `lib/`,不会触发 `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`。若仍提示,把报错
88
+ > 条目写进 `~/.dsh/profiles/web/pnpm-workspace.yaml` `allowBuilds` 再重试,
89
+ > 完整步骤见 [INSTALL.md](INSTALL.md#for-agents完整安装流程)。
77
90
 
78
91
  ### 方式 B:从源码构建
79
92
 
@@ -83,21 +96,24 @@ cd dsh-lan-gateway
83
96
  pnpm install
84
97
  pnpm build # host(lib/index.js)
85
98
  pnpm build:client # client(lib/client.js,window.__ModuleLoader__ 格式)
86
- pnpm test # 38 项(网关 23 + UUID shim 3 + x509 4 + TLS 6
99
+ pnpm test # 72 项(网关单元 27 + start-guard 12 + 集成 17 + UUID shim 3 + x509 6 + TLS 7
87
100
  ```
88
101
 
89
102
  ## 使用
90
103
 
91
104
  ```bash
92
- # 开启网关(监听 0.0.0.0:3081,LAN 免密 / 非 LAN 需登录)
105
+ # 开启网关。先满足启动条件(已设密码 + 加密入口),否则 enable 会给出迁移文案
93
106
  lan_gateway enable
94
107
 
95
- # 查看状态
108
+ # 查看状态(端口 / 目标 / 密码 / 会话 epoch / 中继状态 / 入口加密方式 / 上次错误)
96
109
  lan_gateway status
97
110
 
98
- # 设置非 LAN 访问密码(≥8 位)
111
+ # 设置登录密码(≥8 位;改动会让所有已签发会话立即失效)
99
112
  lan_gateway set-password
100
113
 
114
+ # 轮换会话密钥(作废全部登录 cookie 与已建立的 WebSocket)
115
+ lan_gateway rotate-secret
116
+
101
117
  # 换发自签名 TLS 证书(tlsMode=self-signed 时;换新密钥并热重启监听器)
102
118
  lan_gateway tls-regenerate
103
119
 
@@ -121,8 +137,8 @@ lan_gateway disable
121
137
  | `enabled` | `false` | 是否在启动时监听网络端口 |
122
138
  | `gatewayPort` | `3081` | 网关监听端口(`0.0.0.0`) |
123
139
  | `dshTargetPort` | 跟随 `ctx.webServer.port` | 转发到的 dsh loopback 端口 |
124
- | `lanCidrs` | RFC1918 + link-local(见下) | 免密的受信 LAN 网段(逗号分隔) |
125
- | `authRequired` | `true` | LAN 来源是否需要登录 |
140
+ | `lanCidrs` | RFC1918 + link-local(见下) | 视为 LAN 的网段;仅在 `lanPasswordless` 开启时用作豁免匹配集 |
141
+ | `lanPasswordless` | `false` | 显式 opt-in:LAN/loopback 来源跳过网关登录页(上游会话中继仍把关) |
126
142
  | `cookieMaxAgeDays` | `7` | 会话 cookie 有效期(天) |
127
143
  | `cookieName` | `dsh_gw_auth` | 会话 cookie 名(不进卡片) |
128
144
  | `tlsEnabled` | `false` | 是否以 HTTPS(TLS)提供服务 |
@@ -131,21 +147,37 @@ lan_gateway disable
131
147
  | `tlsCertPath` | — | `custom` 模式:PEM 证书(或证书链)绝对路径 |
132
148
  | `tlsKeyPath` | — | `custom` 模式:PEM 私钥绝对路径 |
133
149
  | `tlsCertMaxAgeDays` | `825` | 自签名证书有效期(天) |
150
+ | `allowInsecurePlaintext` | `false` | 显式 opt-in:允许明文 HTTP 监听(见下「入口加密」) |
151
+ | `trustedTerminator` | — | 声明一个受信 TLS 终止代理标识,视为加密入口(如 `nginx`) |
152
+
153
+ > v0.5.0 起 `authRequired` 被移除:认证恒为必需。若配置里残留 `authRequired: false`
154
+ > (v0.4 及更早的写法),启停守卫会拒绝并提示迁移——不会静默降级回“免密”。
155
+
156
+ ### 入口加密(防明文)
157
+
158
+ 网关默认**拒绝纯明文监听**,三种方式任选其一即可启动:
159
+
160
+ 1. 启用 TLS:`tlsEnabled: true`(推荐,自签名或 custom 证书均可);
161
+ 2. 声明由可信反向代理(nginx 等)终止 TLS:
162
+ ```yaml
163
+ - id: dsh-lan-gateway
164
+ config:
165
+ enabled: true
166
+ gatewayPort: 8080
167
+ trustedTerminator: nginx # 由 nginx 以 HTTPS 对外,再转发回本端口
168
+ ```
169
+ 3. 显式接受明文(风险自担,密码与会话将明文在网内传输):
170
+ ```yaml
171
+ - id: dsh-lan-gateway
172
+ config:
173
+ enabled: true
174
+ gatewayPort: 3081
175
+ allowInsecurePlaintext: true
176
+ ```
134
177
 
135
178
  自签名证书在**首次启用 TLS 时生成一次**,持久化于 `~/.dsh/lan-gateway/tls/`
136
179
  (`selfsigned.crt` / `selfsigned.key`,0600),之后重启复用同一张证书;
137
180
  `lan_gateway tls-regenerate` 可随时换发新证书(新密钥)并热重启监听器。
138
- 配置示例(`--patch`):
139
-
140
- ```yaml
141
- - id: dsh-lan-gateway
142
- config:
143
- enabled: true
144
- gatewayPort: 8443
145
- tlsEnabled: true
146
- tlsMode: self-signed
147
- tlsSelfSignedHosts: localhost, 192.168.1.5
148
- ```
149
181
 
150
182
  或用自己的证书(例如 `/etc/letsencrypt/live/example.com/` 下签发的 PEM):
151
183
 
@@ -158,35 +190,79 @@ lan_gateway disable
158
190
  tlsKeyPath: /etc/letsencrypt/live/example.com/privkey.pem
159
191
  ```
160
192
 
161
- 启用 TLS 后访问 `https://<主机>:<端口>/`;登录 cookie 自动带 `Secure`,网关自身响应
162
- (登录页 / 重定向 / 拒绝)带 HSTS。自签名证书首次访问会看到浏览器警告,属预期行为。
193
+ 启用 TLS(或声明受信终止代理)后,登录 cookie 自动带 `Secure`;监听器自身是 HTTPS 时,
194
+ 网关响应(登录页 / 重定向 / 拒绝)带 HSTS。自签名证书首次访问会看到浏览器警告,属预期行为。
163
195
 
164
- 默认 `lanCidrs`:`10.0.0.0/8`、`172.16.0.0/12`、`192.168.0.0/16`、`169.254.0.0/16`,
165
- IPv6 的 `fe80::/10`(link-local)与回环地址始终免密。
196
+ 默认 `lanCidrs`:`10.0.0.0/8`、`172.16.0.0/12`、`192.168.0.0/16`、`169.254.0.0/16`;
197
+ IPv6 的 `fe80::/10`(link-local)与 `127.0.0.0/8` / `::1` 归类为 LAN/loopback。
166
198
 
167
199
  ## 安全模型
168
200
 
169
201
  - **来源分级**:仅依据 `socket.remoteAddress`(IPv4-mapped IPv6 会先解包)把请求分为
170
- loopback / lan / internet 三档,绝不信任 `X-Forwarded-For`。
171
- - **免密**:LAN loopback 来源直接代理;`internet` 来源(`authRequired: true` 时)
172
- 必须携带有效 HMAC 会话 cookie,否则 302 `/__login` 登录页。
202
+ loopback / lan / internet 三档,绝不信任 `X-Forwarded-For`。**分级本身不授予任何访问**
203
+ ——默认每一档都必须出示有效网关会话,否则 302 `/__login`。
204
+ - **LAN 免密是显式 opt-in**:`lanPasswordless: true` 只让命中 `lanCidrs`(或 loopback)的
205
+ 来源跳过**网关自己的登录页**;上游 dsh 若 ≥ 0.1.2-rc.1,插件在进程内中继一条共享上游
206
+ 会话(见下),上游授权仍把关每个请求。底座没有浏览器会话认证时该开关**拒绝启用**——
207
+ 否则就是把 QVD-2026-57410 原样装回去。
208
+ - **共享上游会话中继(dsh ≥ 0.1.2-rc.1)**:dsh 已不再信任“回环 Host”,要求请求出示
209
+ HMAC 签名的 `dsh-auth-*` cookie。插件经 `connection` 服务拿到启动令牌,在回环传输上
210
+ 做浏览器等价的令牌换取,取得 cookie 后中继到每个转发请求;上游一旦以 401 拒绝就丢弃
211
+ 这条会话并重新换取。**这仍是“单密码 = 单操作者”**:通过网关登录的人都骑同一条上游
212
+ 会话,上游持钥,才是真正的授权主体。
173
213
  - **登录页**:`/__login` 由网关独占、不转发;密码以 scrypt(每写一次重新加盐)校验,
174
214
  登录尝试按来源限流(5 次 / 分钟)。
175
- - **会话 cookie**:`payload.signature` 结构(HMAC-SHA256),`HttpOnly; SameSite=Lax`,
176
- 过期后(`cookieMaxAgeDays`)即失效;`lan_gateway rotate-secret` 可作废全部会话。
177
- 启用 TLS 后自动附加 `Secure`。
178
- - **TLS**:监听器为 HTTPS 时,登录成功签发 `Secure` cookie,网关自身响应带 HSTS
179
- (`max-age=15552000`);转发到 dsh loopback 连接仍为明文 HTTP(不出本机)。
180
- - **CSRF 围栏**:因为网关把 Origin 改写回 loopback、会蒙蔽 dsh 自身的 CSRF 防线,网关在
181
- 转发前会对 `/api*` 请求自检 `sec-fetch-site` Origin 是否匹配网关的权威来源,
182
- 跨站请求直接 403。
183
- - **密码未设置时拒启**:`authRequired: true` 且未设密码时,`enable` 会拒绝监听——避免
184
- 把远程代码执行的门户开放给非 LAN 来源。
185
- - **WebSocket**:`/api` 升级请求同样过登录校验,再原样拼接转发给 dsh。
215
+ - **会话 cookie**:`payload.signature` 结构(HMAC-SHA256),携带**撤销 epoch**;
216
+ `HttpOnly; SameSite=Strict`,过期即失效。改密 / 清密 / `rotate-secret` 递增 epoch,
217
+ 作废全部已签发 cookie **并断开已建立的 WebSocket**,客户端重新登录。清空密码会停止
218
+ 监听(网关必须有密码才能运行)。
219
+ - **管理面不外泄**:`/lan-gateway/*`(含配置路由)由网关独占、一律 403 不转发——远程
220
+ 访问者无法借网关改写 Host 触及本机 loopback 的配置接口;原生 `/lan-gateway/config`
221
+ 仅对回环 Host 且同源的请求应答(本地用户 / 本机能读 `~/.dsh` 的进程)。远程管理走
222
+ `lan_gateway` 工具。
223
+ - **CSRF / 跨站围栏(HTTP 与 WebSocket)**:因为网关把 Origin 改写回 loopback、会蒙蔽
224
+ dsh 自身的 CSRF 防线,网关在改写前对每个转发请求自检:`sec-fetch-site: cross-site`
225
+ 直接拒绝;Origin 必须匹配访问者实际使用的网关权威来源;状态变更方法与 WebSocket
226
+ 升级请求**必须携带同源 Origin**,否则 403。
227
+ - **密码未设置时拒启**:无论来源如何,未设密码一律拒绝监听——杜绝把远程代码执行门户
228
+ 开放给任何非本机来源。
229
+ - **WebSocket**:`/api` 升级请求同样过登录校验、同源 Origin 校验,再拼接转发给 dsh,
230
+ 并纳入会话撤销(epoch 变化即断开)。
231
+
232
+ ## 从 v0.4(及更早)升级
233
+
234
+ 1. **把底座升级到 dsh ≥ 0.1.2-rc.1**(含 QVD-2026-57410 上游修复)。低于该版本时本插件
235
+ 仍可运行,但 `lanPasswordless` 会拒绝启用(fail-closed)。
236
+ 2. 若配置里写过 `authRequired: false`:删除它。现在认证恒为必需;LAN 想免密改为显式
237
+ `lanPasswordless: true`。
238
+ 3. 若以明文(无 TLS)运行且从未设置过密码:升级后 `enable` 会拒绝。请启用 TLS /
239
+ 声明 `trustedTerminator` / 显式 `allowInsecurePlaintext: true` 之一,并先
240
+ `lan_gateway set-password`。
241
+ 4. 曾以“免密 + 伪造 Host”形态暴露过受影响的 dsh 实例:按“可能已失陷”处置——轮换模型 /
242
+ 系统凭据,并检查是否有未知登录会话。
243
+
244
+ 一步到位迁移示例(HTTPS 自签名 + LAN 免密,本机已跑通):
245
+
246
+ ```yaml
247
+ - id: dsh-lan-gateway
248
+ config:
249
+ enabled: true
250
+ tlsEnabled: true
251
+ tlsMode: self-signed
252
+ # 证书 SAN 覆盖所有接入方式:回环 / 主机名 / LAN IP / Tailscale IP。
253
+ # Tailscale 走 100.64.0.0/10(CGNAT),按默认 lanCidrs 归为 internet,
254
+ # 仍需密码登录,不会因 lanPasswordless 而豁免。
255
+ tlsSelfSignedHosts: localhost,my-host,192.168.1.20,100.99.1.2
256
+ lanPasswordless: true # LAN/loopback 免登录;需 dsh ≥ 0.1.2-rc.1 上游会话底座
257
+ ```
258
+
259
+ 首次以 `https://<主机名|LAN-IP|Tailscale-IP>:3081` 访问会看到自签名证书警告(预期)。
260
+ 证书在首次启用 TLS 时生成一次并持久化到 `~/.dsh/lan-gateway/tls/`;若已有一张旧证书,
261
+ **必须用 `lan_gateway tls-regenerate` 重新签发**,新的 `tlsSelfSignedHosts` 才会进入 SAN。
186
262
 
187
263
  ## 登录页截图(预期)
188
264
 
189
- LAN 来源打开 `http://<主机>:3081/` 时,先看到网关自带的登录表单(`/__login`),
265
+ 远程来源打开 `http://<主机>:3081/` 时,先看到网关自带的登录表单(`/__login`),
190
266
  输入正确密码后签发会话 cookie 并跳回 `/`。
191
267
 
192
268
  <p align="center">
@@ -210,12 +286,26 @@ IPv6 的 `fe80::/10`(link-local)与回环地址始终免密。
210
286
 
211
287
  ```bash
212
288
  pnpm test
213
- # ✓ tests/gateway.test.ts (23) 网关代理 / 登录 / HMAC / 信任围栏
214
- # ✓ tests/uuid-shim.test.ts ( 3) 不安全源补丁 / 安全源 no-op / v4 正确性
215
- # ✓ tests/x509.test.ts ( 4) 自签名证书 DER/SAN/签名/TLS 握手
216
- # tests/tls.test.ts ( 6) 证书持久化 / 重生成 / 自定义证书加载
289
+ # ✓ tests/gateway.test.ts (27) 分类 / HMAC cookie / epoch / 密码状态 / 限流
290
+ # ✓ tests/start-guard.test.ts (12) fail-closed 启动守卫 / 配置路由回环围栏
291
+ # ✓ tests/integration/gateway.test.ts (17) 真实网关端到端:全来源登录 / LAN 豁免 /
292
+ # 跨站 403 / 升级拒绝 / cookie 属性 / epoch 撤销 / 会话中继
293
+ # ✓ tests/uuid-shim.test.ts ( 3) 不安全源补丁 / 安全源 no-op / v4 正确性
294
+ # ✓ tests/x509.test.ts ( 6) 自签名证书 DER/SAN/签名/TLS 握手
295
+ # ✓ tests/tls.test.ts ( 7) 证书持久化 / 重生成 / 自定义证书加载
217
296
  ```
218
297
 
298
+ ## 安全评估与修复记录
299
+
300
+ 0.5.0 的默认拒绝模型源自针对 QVD-2026-57410(DSH Web API 的 Host 信任缺陷)的加固,
301
+ 相关文档收在 [docs/security/](docs/security/):
302
+
303
+ - [LAN 网关安全评估](docs/security/SECURITY-AUDIT.md)——0.4.0 时代 F1–F5 审计快照与 13 个
304
+ 隔离观察(顶部标注 0.5.0 的修复状态)。
305
+ - [QVD-2026-57410 修复方案](docs/security/qvd-2026-57410-fix-plan.md)——方案全文 + §15
306
+ 实施状态(0.5.0 落地差异)。
307
+ - [上游研究](docs/security/qvd-2026-57410-research.md)——公开通告 / 上游提交与版本核对。
308
+
219
309
  ## 许可
220
310
 
221
311
  [MIT](./LICENSE)
package/cordis.patch.yml CHANGED
@@ -1,7 +1,10 @@
1
1
  # dsh-lan-gateway bundle patch: inserts the gateway plugin into a profile's layer stack.
2
2
  # Disabled by default (safe): the gateway only opens a network port once `lan_gateway
3
- # enable` is run through the tool. Composition-level settings live here; secrets
4
- # (password hash, cookie secret) live in ~/.dsh/lan-gateway/state.json, never in config.
3
+ # enable` is run through the tool (or `enabled: true` is set here). Composition-level
4
+ # settings live here; secrets (password hash, cookie secret) live in ~/.dsh/lan-gateway/
5
+ # state.json, never in config. Secure defaults inherited from the plugin schema:
6
+ # auth is required for every source (lanPasswordless: false), the listener refuses
7
+ # plaintext (allowInsecurePlaintext: false) unless TLS or trustedTerminator is set.
5
8
  - insert:
6
9
  - id: dsh-lan-gateway
7
10
  name: '@riceawa/dsh-lan-gateway'
package/lib/client.js CHANGED
@@ -44,8 +44,12 @@ window.__ModuleLoader__.load({
44
44
  "hint.dshTargetPort": "留空则自动跟随 dsh web 端口(默认 3080)",
45
45
  "field.lanCidrs": "免密 LAN 网段",
46
46
  "hint.lanCidrs": "逗号分隔的 CIDR,如 10.0.0.0/8, 192.168.0.0/16",
47
- "field.authRequired": "LAN 访问需要密码",
48
- "hint.authRequired": "公网来源必须登录后才能访问",
47
+ "field.lanPasswordless": "LAN 免登录",
48
+ "hint.lanPasswordless": "LAN/回环来源跳过网关登录页,但仍共用同一上游会话(需 dsh ≥ 0.1.2)",
49
+ "field.allowInsecurePlaintext": "允许明文 HTTP",
50
+ "hint.allowInsecurePlaintext": "危险:关闭 TLS 或受信终止代理时仍启动监听,密码与会话将以明文传输",
51
+ "field.trustedTerminator": "受信 TLS 终止代理",
52
+ "hint.trustedTerminator": "可选:声明前置代理标识,视为加密入口(如 nginx)。留空 = 未声明",
49
53
  "field.cookieMaxAgeDays": "会话有效期(天)",
50
54
  "hint.cookieMaxAgeDays": "登录 cookie 的存活天数(默认 7)",
51
55
  "field.tlsEnabled": "启用 TLS(HTTPS)",
@@ -86,8 +90,12 @@ window.__ModuleLoader__.load({
86
90
  "hint.dshTargetPort": "Leave empty to follow the dsh web port (default 3080)",
87
91
  "field.lanCidrs": "Password-free LAN CIDRs",
88
92
  "hint.lanCidrs": "Comma separated CIDRs, e.g. 10.0.0.0/8, 192.168.0.0/16",
89
- "field.authRequired": "Password required for non-LAN",
90
- "hint.authRequired": "Internet sources must sign in before reaching the GUI",
93
+ "field.lanPasswordless": "LAN skip login",
94
+ "hint.lanPasswordless": "LAN/loopback sources skip the gateway login page but still ride one shared upstream session (needs dsh >= 0.1.2)",
95
+ "field.allowInsecurePlaintext": "Allow plaintext HTTP",
96
+ "hint.allowInsecurePlaintext": "Dangerous: start the listener even without TLS or a trusted terminator; passwords and sessions travel in clear",
97
+ "field.trustedTerminator": "Trusted TLS terminator",
98
+ "hint.trustedTerminator": "Optional identifier for a front proxy (e.g. nginx) treated as the encrypted ingress. Empty = none declared",
91
99
  "field.cookieMaxAgeDays": "Session lifetime (days)",
92
100
  "hint.cookieMaxAgeDays": "Login cookie lifetime (default 7)",
93
101
  "field.tlsEnabled": "Enable TLS (HTTPS)",
@@ -126,7 +134,7 @@ window.__ModuleLoader__.load({
126
134
  kind: "cidrs"
127
135
  },
128
136
  {
129
- field: "authRequired",
137
+ field: "lanPasswordless",
130
138
  kind: "boolean"
131
139
  },
132
140
  {
@@ -159,6 +167,15 @@ window.__ModuleLoader__.load({
159
167
  {
160
168
  field: "tlsCertMaxAgeDays",
161
169
  kind: "number"
170
+ },
171
+ {
172
+ field: "allowInsecurePlaintext",
173
+ kind: "boolean"
174
+ },
175
+ {
176
+ field: "trustedTerminator",
177
+ kind: "text",
178
+ optional: true
162
179
  }
163
180
  ];
164
181
  function formatValue(def, value) {
@@ -401,33 +418,34 @@ window.__ModuleLoader__.load({
401
418
  onClick: () => {
402
419
  setOpen(!open);
403
420
  },
404
- children: [
405
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
406
- style: styles.headText,
407
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
421
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
422
+ style: styles.headerTop,
423
+ children: [
424
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
408
425
  style: styles.name,
409
426
  children: t.title
410
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
411
- style: styles.description,
412
- children: t.description
413
- })]
414
- }),
415
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
416
- style: styles.status,
417
- children: statusLine
418
- }),
419
- dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
420
- style: styles.pending,
421
- children: t.unsaved
422
- }) : null,
423
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
424
- style: open ? {
425
- ...styles.chevron,
426
- ...styles.chevronOpen
427
- } : styles.chevron,
428
- children: open ? "▾" : ""
429
- })
430
- ]
427
+ }),
428
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
429
+ style: styles.status,
430
+ title: statusLine,
431
+ children: statusLine
432
+ }),
433
+ dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
434
+ style: styles.pending,
435
+ children: t.unsaved
436
+ }) : null,
437
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
438
+ style: open ? {
439
+ ...styles.chevron,
440
+ ...styles.chevronOpen
441
+ } : styles.chevron,
442
+ children: open ? "▾" : "▸"
443
+ })
444
+ ]
445
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
446
+ style: styles.description,
447
+ children: t.description
448
+ })]
431
449
  }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
432
450
  style: styles.body,
433
451
  children: [
@@ -502,8 +520,9 @@ window.__ModuleLoader__.load({
502
520
  },
503
521
  header: {
504
522
  display: "flex",
505
- alignItems: "center",
506
- gap: "12px",
523
+ flexDirection: "column",
524
+ alignItems: "stretch",
525
+ gap: "6px",
507
526
  width: "100%",
508
527
  padding: "14px 16px",
509
528
  border: 0,
@@ -513,28 +532,39 @@ window.__ModuleLoader__.load({
513
532
  textAlign: "left",
514
533
  cursor: "pointer"
515
534
  },
516
- headText: {
535
+ headerTop: {
517
536
  display: "flex",
518
- flexDirection: "column",
519
- gap: "4px",
520
- flex: 1,
521
- minWidth: 0
537
+ alignItems: "center",
538
+ gap: "12px",
539
+ width: "100%"
522
540
  },
523
541
  name: {
542
+ flex: "1 1 auto",
543
+ minWidth: 0,
524
544
  fontSize: "15px",
525
545
  fontWeight: 600,
526
546
  lineHeight: 1.4,
527
547
  color: L.labelPrimary
528
548
  },
549
+ status: {
550
+ flex: "0 1 auto",
551
+ minWidth: 0,
552
+ maxWidth: "60%",
553
+ fontSize: "11px",
554
+ lineHeight: 1.4,
555
+ color: L.labelTertiary,
556
+ whiteSpace: "nowrap",
557
+ overflow: "hidden",
558
+ textOverflow: "ellipsis"
559
+ },
529
560
  description: {
561
+ display: "block",
530
562
  fontSize: "13px",
531
563
  lineHeight: 1.5,
532
- color: L.labelTertiary
533
- },
534
- status: {
535
- fontSize: "11px",
536
564
  color: L.labelTertiary,
537
- whiteSpace: "nowrap"
565
+ whiteSpace: "nowrap",
566
+ overflow: "hidden",
567
+ textOverflow: "ellipsis"
538
568
  },
539
569
  pending: {
540
570
  flex: "none",
@@ -713,11 +743,15 @@ window.__ModuleLoader__.load({
713
743
  */
714
744
  function apply(ctx) {
715
745
  installRandomUuidShim();
746
+ const legacyListOptions = {
747
+ id: "lan-gateway",
748
+ order: 30
749
+ };
716
750
  ctx.slots.inject("settings.plugin.item", function* () {
717
751
  yield ctx.slots.register({
718
752
  name: "settings.plugin.item",
719
- id: "lan-gateway",
720
- order: 30
753
+ key: "lan-gateway",
754
+ ...legacyListOptions
721
755
  }, LanGatewayCard);
722
756
  });
723
757
  }