@riceawa/dsh-lan-gateway 0.6.1 → 0.7.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 +15 -12
- package/lib/client.js +106 -148
- package/lib/client.js.map +1 -1
- package/lib/index.js +52 -2
- package/package.json +21 -23
- package/src/client/index.ts +37 -26
- package/src/client/lan-gateway-card.tsx +205 -195
- package/src/gateway.ts +31 -5
- package/src/request-policy.ts +36 -0
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.
|
|
9
|
+
<img src="https://img.shields.io/badge/version-0.7.0-2b7fff?style=flat-square" alt="version 0.7.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
|
<a href="https://awesome-dsh-plugin.com"><img src="https://awesome-dsh-plugin.com/badge.svg" alt="awesome · DSH plugin" /></a>
|
|
@@ -44,7 +44,7 @@ cd dsh-lan-gateway
|
|
|
44
44
|
pnpm install
|
|
45
45
|
pnpm build # host → lib/index.js
|
|
46
46
|
pnpm build:client # client → lib/client.js
|
|
47
|
-
pnpm test #
|
|
47
|
+
pnpm test # 216 项
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
仓库里还有一个 [lan-gateway](skills/lan-gateway.md) 技能,安装后可直接在 dsh 对话里说「设置网关密码为 …」「开启远程访问」,agent 会调用 `lan_gateway` 工具完成,密码以参数传入,不写入配置,也不回显。改密码也可以不走模型:在本机打开 **Plugins** 页的「LAN 网关」卡片,直接填两次新密码覆盖(见下「登录密码」)。安装方式见 [INSTALL.md](INSTALL.md#for-agents完整安装流程)。
|
|
@@ -70,7 +70,7 @@ lan_gateway disable # 关闭
|
|
|
70
70
|
|
|
71
71
|
## 配置
|
|
72
72
|
|
|
73
|
-
所有可调项都写在本插件自己的 profile 条目里(dsh ≥ 0.1.7 起,设置写入按**条目 id** 寻址;此前的 `lan-gateway` 用户设置命名空间已随 `settingsScope` 一起移除)。打开侧边栏的 **Plugins**
|
|
73
|
+
所有可调项都写在本插件自己的 profile 条目里(dsh ≥ 0.1.7 起,设置写入按**条目 id** 寻址;此前的 `lan-gateway` 用户设置命名空间已随 `settingsScope` 一起移除)。打开侧边栏的 **Plugins** 页 →「已安装」→ `@riceawa/dsh-lan-gateway`:本插件的配置就渲染在这个包自己的页面上——挂的是 dsh 给「bundle 自己的配置」声明的 `plugins.bundle.config` 槽(按**包名** `@riceawa/dsh-lan-gateway` 挂载,与 dsh-mnemon、dshmarket 等插件同一套机制),页面标题、图标与面包屑由 Plugins 页自己绘制。保存即生效,监听器会按新配置自动重启。宿主机上用 `127.0.0.1` / `localhost` 打开网关地址时该页面同样可用(网关只对宿主机本机浏览器放行它的管理前缀,见「安全模型」);用局域网 IP、域名或从别的机器访问时页面读不到配置,改用 `lan_gateway` 工具。注册只在底座确实 served 本插件条目时发生——没有 Loader 条目(写入必然 409)时页面不会出现。下表既是卡片字段,也是配置键;页面顶部另有一栏「登录密码」,改密码不走配置表(见下「登录密码」):
|
|
74
74
|
|
|
75
75
|
| 键 | 默认值 | 说明 |
|
|
76
76
|
| --- | --- | --- |
|
|
@@ -100,7 +100,7 @@ lan_gateway disable # 关闭
|
|
|
100
100
|
卡片顶部就是「登录密码」栏:一个「已设置 / 未设置」状态,两个密码框(新密码、再次输入),点「修改密码」直接覆盖原密码。
|
|
101
101
|
|
|
102
102
|
- **不回显旧密码。** 界面只报告密码是否存在;卡片读不到旧密码。`state.json` 里只有 scrypt 哈希与盐,`/lan-gateway/password` 也从不把哈希、盐或长度返回给浏览器。
|
|
103
|
-
- **不需要旧密码。** 该路由与配置路由共用同一道围栏:只接受本机 loopback 的同源请求(Host 必须是回环、跨站请求拒绝、写操作必须带匹配的 `Origin
|
|
103
|
+
- **不需要旧密码。** 该路由与配置路由共用同一道围栏:只接受本机 loopback 的同源请求(Host 必须是回环、跨站请求拒绝、写操作必须带匹配的 `Origin`)。经网关访问时,只有宿主机本机浏览器(回环来源 + 回环地址)能被放行到这道围栏,远程浏览器仍是一律 403。能通过这两道门的本地用户本来就能读 `~/.dsh`。
|
|
104
104
|
- **改完即生效。** 写盘后递增会话代次:旧密码立即失效,所有已登录会话与已建立的 WebSocket 全部作废,各来源需要重新登录。
|
|
105
105
|
- **只设置,不清空。** 清空密码会按设计停掉监听器(没有密码不允许监听),卡片不做这件事;要清空请用 `lan_gateway set-password` 并留空密码。
|
|
106
106
|
- **密码不是配置键。** 它不写进 profile 条目,也不会出现在 `--dump-config` 里;保存配置字段动不到它,改密码也不会碰你尚未保存的字段草稿。
|
|
@@ -183,7 +183,7 @@ lan_gateway disable # 关闭
|
|
|
183
183
|
- **共享上游会话中继**(dsh ≥ 0.1.7)。dsh 不再信任回环 Host,要求出示 HMAC 签名的 `dsh-auth-*` cookie。插件经 `connection` 服务拿到启动令牌,在回环传输上做一次浏览器等价的令牌换取,取得 cookie 后中继到每个转发请求;上游一旦 401 就丢弃这条会话并重新换取。这仍是「单密码 = 单操作者」:通过网关登录的用户共用同一条上游会话,持钥的上游才是真正的授权主体。
|
|
184
184
|
- **登录页**。`/__login` 由网关独占、不转发。密码以 scrypt 校验,每写一次重新加盐;登录尝试按来源限流(5 次 / 分钟)。
|
|
185
185
|
- **会话 cookie** 是 `payload.signature` 结构(HMAC-SHA256),带撤销 epoch 与逐会话 id,`HttpOnly; SameSite=Strict`。登出撤销的是当前这条会话:它建立的 WebSocket 一并断开,其他设备不受影响。改密、清密、`rotate-secret` 递增 epoch,作废全部已签发 cookie 并断开全部已建立的 WebSocket,客户端需要重新登录。清空密码会直接停止监听。
|
|
186
|
-
- **管理面不外泄**。`/lan-gateway
|
|
186
|
+
- **管理面不外泄**。`/lan-gateway/*`(含配置路由与改密路由)由网关独占:唯一放行的是**宿主机本机浏览器**,即「TCP 来源为 loopback **且** 浏览器地址栏写的是回环权威(`127.0.0.1` / `localhost` / `::1`)」的请求,其余一律 403 不转发,远程访问者无法借网关改写 Host 触及本机 loopback 的配置接口。两个条件缺一不可:来源一项远程无法伪造,地址一项则挡住受信 TLS 终止代理的部署——那里所有请求的 TCP 来源都是代理自己的回环地址,只有回环权威仍被拒。放行的请求照样要过会话门与 CSRF 围栏,再以改写后的 loopback Host 交给插件自己的回环同源围栏。升级请求(WebSocket)不在此列,一律拒。原生 `/lan-gateway/config` 只应答回环 Host 且同源的请求。远程管理走 `lan_gateway` 工具。
|
|
187
187
|
- **CSRF 围栏**(HTTP 与 WebSocket)。网关把 Origin 改写回 loopback,会蒙蔽 dsh 自身的 CSRF 防线,所以在改写前对每个转发请求自检:`sec-fetch-site: cross-site` 直接拒;Origin 必须匹配访问者实际使用的网关权威来源;状态变更方法与 WebSocket 升级请求必须携带同源 Origin,否则 403。
|
|
188
188
|
- **未设置密码时拒绝监听**,与来源无关。
|
|
189
189
|
|
|
@@ -204,7 +204,7 @@ client bundle 在模块级给 `Crypto` 原型补一个基于 `crypto.getRandomVa
|
|
|
204
204
|
## 开发
|
|
205
205
|
|
|
206
206
|
```bash
|
|
207
|
-
pnpm test #
|
|
207
|
+
pnpm test # 216 项
|
|
208
208
|
pnpm typecheck # tsc 双端(host + client)
|
|
209
209
|
```
|
|
210
210
|
|
|
@@ -212,17 +212,20 @@ pnpm typecheck # tsc 双端(host + client)
|
|
|
212
212
|
✓ tests/gateway.test.ts (40) 分类 / HMAC cookie / epoch / 逐会话撤销 / 密码状态 / 限流
|
|
213
213
|
✓ tests/start-guard.test.ts (19) fail-closed 启动守卫 / 配置路由回环围栏 /
|
|
214
214
|
Secure cookie 属性推断(含 null 清除路径)
|
|
215
|
-
✓ tests/request-policy.test.ts (
|
|
216
|
-
两个方向的头部变换
|
|
215
|
+
✓ tests/request-policy.test.ts (50) 判定缝纯函数:路径归一化 / 归属前缀 / 同站与登录围栏 /
|
|
216
|
+
回环权威(Host)判定 / 两个方向的头部变换
|
|
217
217
|
✓ tests/upstream-session.test.ts ( 8) 真实回环令牌换取:cookie 名匹配 / 拒绝后重换 /
|
|
218
218
|
invalidate 重获取 / 日志播报 / 保住已持有会话
|
|
219
219
|
✓ tests/x509.test.ts ( 6) 自签名证书 DER/SAN/签名/TLS 握手
|
|
220
220
|
✓ tests/tls.test.ts ( 9) 证书持久化 / 到期换发 / 重生成 / 自定义证书加载
|
|
221
221
|
✓ tests/uuid-shim.test.ts ( 3) 不安全源补丁 / 安全源 no-op / v4 正确性
|
|
222
|
-
✓ tests/settings-card.test.ts (
|
|
223
|
-
卡片注册槽与条目 id 契约 /
|
|
224
|
-
|
|
225
|
-
|
|
222
|
+
✓ tests/settings-card.test.ts (19) 设置页字段编解码(三态 auto ↔ false 不可混淆)/
|
|
223
|
+
卡片注册槽与条目 id 契约 / 密码草稿闸门(长度与确认)/
|
|
224
|
+
密码状态徽标三态(缺字段 = 未知,不是「未设置」)/
|
|
225
|
+
「网关自己拒绝」与其它失败的文案分流
|
|
226
|
+
✓ tests/integration/gateway.test.ts (30) 真实网关端到端:全来源登录 / LAN 豁免 / 跨站 403 / 升级拒绝 /
|
|
227
|
+
cookie 属性 / epoch 撤销 / 逐会话登出 / 尾斜杠 / IPv6 / 会话中继 /
|
|
228
|
+
管理面只对本机浏览器放行(来源与 Host 两项都要满足)
|
|
226
229
|
✓ tests/integration/management-plane.test.ts (24) 真实 apply():工具与卡片交替启停 / 未编辑字段与未知键保留 /
|
|
227
230
|
清空后继承 / 拒绝不可启动配置 / 卡片改密路由(覆盖原密码 /
|
|
228
231
|
递增代次 / 不返回哈希 / 不给清空 / 无 settings 也生效)
|
package/lib/client.js
CHANGED
|
@@ -151,17 +151,30 @@ window.__ModuleLoader__.load({
|
|
|
151
151
|
//#endregion
|
|
152
152
|
//#region src/client/lan-gateway-card.tsx
|
|
153
153
|
/**
|
|
154
|
-
* The lan-gateway
|
|
155
|
-
* through its `plugins.
|
|
154
|
+
* The lan-gateway configuration card, rendered by the official DSH Plugins page
|
|
155
|
+
* on this bundle's own page through its `plugins.bundle.config` slot.
|
|
156
156
|
*
|
|
157
157
|
* ModLens-style: the card carries NO injected services. It reads and writes
|
|
158
158
|
* the loopback-only `/lan-gateway/config` host route (the browser never sees
|
|
159
159
|
* the settings seam or any secret), so the only platform service it needs is
|
|
160
160
|
* the `slots` service every plugin already has.
|
|
161
161
|
*
|
|
162
|
+
* The page — not the card — draws the plugin's title, icon, and crumb, and the
|
|
163
|
+
* card is the page body: `view: 'summary'` renders nothing (bundle
|
|
164
|
+
* configuration is `page`-only).
|
|
165
|
+
*
|
|
162
166
|
* @module @riceawa/dsh-lan-gateway/client/card
|
|
163
167
|
*/
|
|
164
168
|
/**
|
|
169
|
+
* Classify the host's `passwordSet` for the badge.
|
|
170
|
+
* @param passwordSet - the snapshot's flag, or `undefined` when absent.
|
|
171
|
+
* @returns `set` / `unset` / `unknown` (absent field).
|
|
172
|
+
*/
|
|
173
|
+
function passwordStatus(passwordSet) {
|
|
174
|
+
if (passwordSet === void 0) return "unknown";
|
|
175
|
+
return passwordSet ? "set" : "unset";
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
165
178
|
* Judge a password draft the way the host's password route will, so a draft the
|
|
166
179
|
* card enables is never answered with a 400. The length bound is the shared
|
|
167
180
|
* {@link MIN_PASSWORD_LENGTH}; the confirmation is a UI concern and is checked
|
|
@@ -176,6 +189,22 @@ window.__ModuleLoader__.load({
|
|
|
176
189
|
if (password !== confirm) return "mismatch";
|
|
177
190
|
return null;
|
|
178
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Whether the config route's refusal came from the gateway rather than from dsh
|
|
194
|
+
* itself.
|
|
195
|
+
*
|
|
196
|
+
* The gateway owns the `/lan-gateway*` prefix and answers it with a bare 403
|
|
197
|
+
* `forbidden` whenever the client is not on the host; dsh, when it answers the
|
|
198
|
+
* route at all, does not produce that pair. Telling the two apart is what lets
|
|
199
|
+
* the card say "you are not on the host loopback" instead of the generic
|
|
200
|
+
* "cannot read the configuration".
|
|
201
|
+
* @param status - the HTTP status of the failed read.
|
|
202
|
+
* @param body - that response's body.
|
|
203
|
+
* @returns true when the gateway itself refused the read.
|
|
204
|
+
*/
|
|
205
|
+
function refusedByGateway(status, body) {
|
|
206
|
+
return status === 403 && body.trim() === "forbidden";
|
|
207
|
+
}
|
|
179
208
|
const LABELS = {
|
|
180
209
|
zh: {
|
|
181
210
|
title: "LAN 网关",
|
|
@@ -185,7 +214,8 @@ window.__ModuleLoader__.load({
|
|
|
185
214
|
saving: "保存中…",
|
|
186
215
|
discard: "放弃",
|
|
187
216
|
reset: "重置",
|
|
188
|
-
readOnly: "
|
|
217
|
+
readOnly: "读不到网关配置:这个接口只在宿主机本机应答——Host 必须是回环地址且同源。请在宿主机上打开 dsh web 时修改,远程请改用 lan_gateway 工具。",
|
|
218
|
+
readOnlyGateway: "网关拒绝了这次读取:/lan-gateway/* 管理面由网关独占,只放行「TCP 来源为回环 且 地址写的是 127.0.0.1 / localhost」的浏览器。你现在不是从宿主机回环地址访问的——请在宿主机上用 127.0.0.1 或 localhost 打开本页(局域网 IP、域名都不算),或远程改用 lan_gateway 工具。",
|
|
189
219
|
saveFailed: "保存未生效,请检查输入后重试。",
|
|
190
220
|
loadFailed: "无法读取网关配置",
|
|
191
221
|
emptyMeansClear: "留空 = 使用默认",
|
|
@@ -199,7 +229,9 @@ window.__ModuleLoader__.load({
|
|
|
199
229
|
passwordConfirm: "再次输入新密码",
|
|
200
230
|
passwordSet: "已设置",
|
|
201
231
|
passwordUnset: "未设置",
|
|
232
|
+
passwordUnknown: "状态未知(宿主端较旧)",
|
|
202
233
|
passwordRequired: "未设置密码时网关拒绝启动。",
|
|
234
|
+
passwordHostStale: "宿主端没有报告密码状态,说明 dsh web 还在运行旧版本:重启后这里才会显示「已设置 / 未设置」,这个改密表单也才会生效。",
|
|
203
235
|
passwordChange: "修改密码",
|
|
204
236
|
passwordChanged: "密码已更新:旧密码立即失效,所有已登录会话已作废。",
|
|
205
237
|
passwordFailed: "修改密码失败,请重试。",
|
|
@@ -248,7 +280,8 @@ window.__ModuleLoader__.load({
|
|
|
248
280
|
saving: "Saving…",
|
|
249
281
|
discard: "Discard",
|
|
250
282
|
reset: "Reset",
|
|
251
|
-
readOnly: "
|
|
283
|
+
readOnly: "Cannot read the configuration: the route answers on the host only — the Host must be a loopback address and same-origin. Change the settings where dsh web runs on the host, or use the lan_gateway tool remotely.",
|
|
284
|
+
readOnlyGateway: "The gateway refused this read: it owns the /lan-gateway/* management plane and admits only browsers that are both loopback-sourced and using a 127.0.0.1 / localhost address. You are not on the host loopback — open this page on the host via 127.0.0.1 or localhost (a LAN IP or a hostname does not qualify), or use the lan_gateway tool remotely.",
|
|
252
285
|
saveFailed: "The save did not land — check the inputs and retry.",
|
|
253
286
|
loadFailed: "Cannot read the gateway configuration",
|
|
254
287
|
emptyMeansClear: "Empty = default",
|
|
@@ -262,7 +295,9 @@ window.__ModuleLoader__.load({
|
|
|
262
295
|
passwordConfirm: "Repeat new password",
|
|
263
296
|
passwordSet: "Set",
|
|
264
297
|
passwordUnset: "Not set",
|
|
298
|
+
passwordUnknown: "Unknown (older host)",
|
|
265
299
|
passwordRequired: "The gateway refuses to start without a password.",
|
|
300
|
+
passwordHostStale: "The host did not report a password state, so dsh web is still running the previous build: restart it to get Set / Not set here and to make this form take effect.",
|
|
266
301
|
passwordChange: "Change password",
|
|
267
302
|
passwordChanged: "Password updated: the old one no longer works and every signed-in session was revoked.",
|
|
268
303
|
passwordFailed: "The password change failed — retry.",
|
|
@@ -308,28 +343,19 @@ window.__ModuleLoader__.load({
|
|
|
308
343
|
return (typeof navigator !== "undefined" ? navigator.language : "en").toLowerCase().startsWith("zh") ? LABELS.zh : LABELS.en;
|
|
309
344
|
}
|
|
310
345
|
/**
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
* @returns the localized card title.
|
|
314
|
-
*/
|
|
315
|
-
function cardTitle() {
|
|
316
|
-
return labels().title;
|
|
317
|
-
}
|
|
318
|
-
/**
|
|
319
|
-
* Render the LAN gateway card. Self-loading: fetches the config route on
|
|
320
|
-
* mount, posts the edited config on save.
|
|
346
|
+
* Render the LAN gateway configuration card. Self-loading: fetches the config
|
|
347
|
+
* route on mount, posts the edited config on save.
|
|
321
348
|
*
|
|
322
|
-
* `
|
|
323
|
-
*
|
|
324
|
-
*
|
|
349
|
+
* `summary` renders nothing: dsh asks a bundle's own configuration for its page
|
|
350
|
+
* body only, and the card's former one-liner (a list entry on the official
|
|
351
|
+
* plugin card) went away with `plugins.item`.
|
|
325
352
|
* @param props - the view the Plugins page is asking for.
|
|
326
|
-
* @returns the
|
|
353
|
+
* @returns the page body, or nothing while the view is `summary`.
|
|
327
354
|
*/
|
|
328
355
|
function LanGatewayCard(props) {
|
|
329
356
|
const t = labels();
|
|
330
|
-
const [open, setOpen] = (0, react.useState)(false);
|
|
331
357
|
const [route, setRoute] = (0, react.useState)(null);
|
|
332
|
-
const [
|
|
358
|
+
const [failure, setFailure] = (0, react.useState)(null);
|
|
333
359
|
const [drafts, setDrafts] = (0, react.useState)({});
|
|
334
360
|
const [saving, setSaving] = (0, react.useState)(false);
|
|
335
361
|
const [failed, setFailed] = (0, react.useState)(null);
|
|
@@ -342,36 +368,28 @@ window.__ModuleLoader__.load({
|
|
|
342
368
|
let cancelled = false;
|
|
343
369
|
fetch("/lan-gateway/config").then(async (response) => {
|
|
344
370
|
if (cancelled) return;
|
|
345
|
-
if (!response.ok)
|
|
371
|
+
if (!response.ok) {
|
|
372
|
+
const body = await response.text().catch(() => "");
|
|
373
|
+
if (!cancelled) setFailure(refusedByGateway(response.status, body) ? "gateway" : "other");
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
346
376
|
setRoute(await response.json());
|
|
347
377
|
}).catch(() => {
|
|
348
|
-
if (!cancelled)
|
|
378
|
+
if (!cancelled) setFailure("other");
|
|
349
379
|
});
|
|
350
380
|
return () => {
|
|
351
381
|
cancelled = true;
|
|
352
382
|
};
|
|
353
383
|
}, []);
|
|
354
|
-
if (props.view === "summary") return
|
|
355
|
-
if (
|
|
384
|
+
if (props.view === "summary") return null;
|
|
385
|
+
if (failure !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
356
386
|
style: styles.card,
|
|
357
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.
|
|
358
|
-
style: styles.
|
|
359
|
-
children:
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
children: t.title
|
|
364
|
-
})
|
|
365
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
366
|
-
style: styles.description,
|
|
367
|
-
children: t.loadFailed
|
|
368
|
-
})]
|
|
369
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
370
|
-
style: styles.body,
|
|
371
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
372
|
-
style: styles.hint,
|
|
373
|
-
children: t.readOnly
|
|
374
|
-
})
|
|
387
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
388
|
+
style: styles.status,
|
|
389
|
+
children: t.loadFailed
|
|
390
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
391
|
+
style: styles.hint,
|
|
392
|
+
children: failure === "gateway" ? t.readOnlyGateway : t.readOnly
|
|
375
393
|
})]
|
|
376
394
|
});
|
|
377
395
|
if (route === null) return null;
|
|
@@ -402,6 +420,7 @@ window.__ModuleLoader__.load({
|
|
|
402
420
|
const dirty = Object.keys(drafts).length > 0;
|
|
403
421
|
const passwordDraftProblem = passwordProblem(password, passwordConfirm);
|
|
404
422
|
const passwordTyping = password !== "" || passwordConfirm !== "";
|
|
423
|
+
const passwordBadge = passwordStatus(route.passwordSet);
|
|
405
424
|
/** Adopt the host's post-write snapshot, keeping the last known values. */
|
|
406
425
|
const adopt = (body) => {
|
|
407
426
|
if (body.config === void 0) return;
|
|
@@ -411,7 +430,7 @@ window.__ModuleLoader__.load({
|
|
|
411
430
|
port: body.port ?? 0,
|
|
412
431
|
tls: body.tls ?? "",
|
|
413
432
|
lastError: body.lastError ?? null,
|
|
414
|
-
passwordSet: body.passwordSet
|
|
433
|
+
...body.passwordSet !== void 0 ? { passwordSet: body.passwordSet } : {}
|
|
415
434
|
});
|
|
416
435
|
};
|
|
417
436
|
/**
|
|
@@ -578,46 +597,18 @@ window.__ModuleLoader__.load({
|
|
|
578
597
|
};
|
|
579
598
|
const statusLine = `${route.running ? t.running : t.stopped} · ${t.tls}: ${route.tls} · :${route.port}`;
|
|
580
599
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
581
|
-
style:
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
"
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
style: styles.headerTop,
|
|
594
|
-
children: [
|
|
595
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
596
|
-
style: styles.name,
|
|
597
|
-
children: t.title
|
|
598
|
-
}),
|
|
599
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
600
|
-
style: styles.status,
|
|
601
|
-
title: statusLine,
|
|
602
|
-
children: statusLine
|
|
603
|
-
}),
|
|
604
|
-
dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
605
|
-
style: styles.pending,
|
|
606
|
-
children: t.unsaved
|
|
607
|
-
}) : null,
|
|
608
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
609
|
-
style: open ? {
|
|
610
|
-
...styles.chevron,
|
|
611
|
-
...styles.chevronOpen
|
|
612
|
-
} : styles.chevron,
|
|
613
|
-
children: open ? "▾" : "▸"
|
|
614
|
-
})
|
|
615
|
-
]
|
|
616
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
617
|
-
style: styles.description,
|
|
618
|
-
children: t.description
|
|
619
|
-
})]
|
|
620
|
-
}), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
600
|
+
style: styles.card,
|
|
601
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
602
|
+
style: styles.statusRow,
|
|
603
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
604
|
+
style: styles.status,
|
|
605
|
+
title: statusLine,
|
|
606
|
+
children: statusLine
|
|
607
|
+
}), dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
608
|
+
style: styles.pending,
|
|
609
|
+
children: t.unsaved
|
|
610
|
+
}) : null]
|
|
611
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
621
612
|
style: styles.body,
|
|
622
613
|
children: [
|
|
623
614
|
route.lastError ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
@@ -638,18 +629,22 @@ window.__ModuleLoader__.load({
|
|
|
638
629
|
style: styles.label,
|
|
639
630
|
children: t.passwordSection
|
|
640
631
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
641
|
-
style:
|
|
642
|
-
children:
|
|
632
|
+
style: passwordBadge === "unset" ? styles.badgeAlert : styles.badge,
|
|
633
|
+
children: passwordBadge === "set" ? t.passwordSet : passwordBadge === "unset" ? t.passwordUnset : t.passwordUnknown
|
|
643
634
|
})]
|
|
644
635
|
}),
|
|
645
636
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
646
637
|
style: styles.hint,
|
|
647
638
|
children: t.passwordHint
|
|
648
639
|
}),
|
|
649
|
-
|
|
640
|
+
passwordBadge === "unset" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
650
641
|
style: styles.error,
|
|
651
642
|
children: t.passwordRequired
|
|
652
|
-
}),
|
|
643
|
+
}) : null,
|
|
644
|
+
passwordBadge === "unknown" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
645
|
+
style: styles.hint,
|
|
646
|
+
children: t.passwordHostStale
|
|
647
|
+
}) : null,
|
|
653
648
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
654
649
|
style: styles.passwordRow,
|
|
655
650
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
@@ -736,7 +731,7 @@ window.__ModuleLoader__.load({
|
|
|
736
731
|
]
|
|
737
732
|
})
|
|
738
733
|
]
|
|
739
|
-
})
|
|
734
|
+
})]
|
|
740
735
|
});
|
|
741
736
|
}
|
|
742
737
|
/** Theme token with a fallback for token-less environments. */
|
|
@@ -746,13 +741,10 @@ window.__ModuleLoader__.load({
|
|
|
746
741
|
const L = {
|
|
747
742
|
border: tk("--dsw-alias-border-l2", "rgba(127,127,127,0.35)"),
|
|
748
743
|
bg: tk("--dsw-alias-bg-layer-3", "transparent"),
|
|
749
|
-
bgOpen: tk("--dsw-alias-bg-layer-2", "transparent"),
|
|
750
744
|
labelPrimary: tk("--dsw-alias-label-primary", "inherit"),
|
|
751
745
|
labelSecondary: tk("--dsw-alias-label-secondary", "inherit"),
|
|
752
746
|
labelTertiary: tk("--dsw-alias-label-tertiary", "rgba(127,127,127,0.8)"),
|
|
753
|
-
labelDimmed: tk("--dsw-alias-label-dimmed", "rgba(127,127,127,0.6)"),
|
|
754
747
|
error: tk("--dsw-alias-label-error", "#d1242f"),
|
|
755
|
-
brand: tk("--dsw-alias-brand-primary", "#4f6ef7"),
|
|
756
748
|
badgeBg: tk("--dsw-alias-bg-module-platform", "rgba(127,127,127,0.14)")
|
|
757
749
|
};
|
|
758
750
|
const styles = {
|
|
@@ -761,45 +753,21 @@ window.__ModuleLoader__.load({
|
|
|
761
753
|
border: `1px solid ${L.border}`,
|
|
762
754
|
borderRadius: "12px",
|
|
763
755
|
background: L.bg,
|
|
764
|
-
transition: "border-color .16s, background .16s",
|
|
765
|
-
overflow: "hidden"
|
|
766
|
-
},
|
|
767
|
-
cardOpen: {
|
|
768
|
-
background: L.bgOpen,
|
|
769
|
-
borderColor: L.labelDimmed
|
|
770
|
-
},
|
|
771
|
-
header: {
|
|
772
756
|
display: "flex",
|
|
773
757
|
flexDirection: "column",
|
|
774
|
-
|
|
775
|
-
gap: "6px",
|
|
776
|
-
width: "100%",
|
|
777
|
-
padding: "14px 16px",
|
|
778
|
-
border: 0,
|
|
779
|
-
background: "none",
|
|
780
|
-
font: "inherit",
|
|
781
|
-
color: "inherit",
|
|
782
|
-
textAlign: "left",
|
|
783
|
-
cursor: "pointer"
|
|
758
|
+
overflow: "hidden"
|
|
784
759
|
},
|
|
785
|
-
|
|
760
|
+
statusRow: {
|
|
786
761
|
display: "flex",
|
|
787
762
|
alignItems: "center",
|
|
788
763
|
gap: "12px",
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
name: {
|
|
792
|
-
flex: "1 1 auto",
|
|
793
|
-
minWidth: 0,
|
|
794
|
-
fontSize: "15px",
|
|
795
|
-
fontWeight: 600,
|
|
796
|
-
lineHeight: 1.4,
|
|
797
|
-
color: L.labelPrimary
|
|
764
|
+
padding: "12px 16px",
|
|
765
|
+
borderBottom: `1px solid ${L.border}`
|
|
798
766
|
},
|
|
799
767
|
status: {
|
|
800
768
|
flex: "0 1 auto",
|
|
801
769
|
minWidth: 0,
|
|
802
|
-
maxWidth: "
|
|
770
|
+
maxWidth: "80%",
|
|
803
771
|
fontSize: "11px",
|
|
804
772
|
lineHeight: 1.4,
|
|
805
773
|
color: L.labelTertiary,
|
|
@@ -807,15 +775,6 @@ window.__ModuleLoader__.load({
|
|
|
807
775
|
overflow: "hidden",
|
|
808
776
|
textOverflow: "ellipsis"
|
|
809
777
|
},
|
|
810
|
-
description: {
|
|
811
|
-
display: "block",
|
|
812
|
-
fontSize: "13px",
|
|
813
|
-
lineHeight: 1.5,
|
|
814
|
-
color: L.labelTertiary,
|
|
815
|
-
whiteSpace: "nowrap",
|
|
816
|
-
overflow: "hidden",
|
|
817
|
-
textOverflow: "ellipsis"
|
|
818
|
-
},
|
|
819
778
|
pending: {
|
|
820
779
|
flex: "none",
|
|
821
780
|
borderRadius: "999px",
|
|
@@ -827,17 +786,8 @@ window.__ModuleLoader__.load({
|
|
|
827
786
|
background: L.badgeBg,
|
|
828
787
|
color: L.labelSecondary
|
|
829
788
|
},
|
|
830
|
-
chevron: {
|
|
831
|
-
flex: "none",
|
|
832
|
-
color: L.labelTertiary,
|
|
833
|
-
fontSize: "12px",
|
|
834
|
-
transition: "transform .16s"
|
|
835
|
-
},
|
|
836
|
-
chevronOpen: { transform: "rotate(180deg)" },
|
|
837
789
|
body: {
|
|
838
|
-
|
|
839
|
-
margin: "0 16px",
|
|
840
|
-
paddingBottom: "8px",
|
|
790
|
+
padding: "4px 16px 8px",
|
|
841
791
|
display: "flex",
|
|
842
792
|
flexDirection: "column"
|
|
843
793
|
},
|
|
@@ -997,8 +947,12 @@ window.__ModuleLoader__.load({
|
|
|
997
947
|
* module scope. With TLS enabled the origin is secure and the shim is a
|
|
998
948
|
* no-op.
|
|
999
949
|
* 2. Settings card: registers the LAN gateway card into the official Plugins
|
|
1000
|
-
* page
|
|
1001
|
-
* from the
|
|
950
|
+
* page as THIS bundle's own configuration (`plugins.bundle.config`, keyed by
|
|
951
|
+
* the package name), so port, CIDRs, auth, and TLS stay adjustable from the
|
|
952
|
+
* GUI on the plugin's own page. `plugins.item` is deliberately not used: dsh
|
|
953
|
+
* reserves it for the official settings pages, one companion package per
|
|
954
|
+
* host-plane namespace, and a bundle's configuration belongs in its own
|
|
955
|
+
* config slot instead.
|
|
1002
956
|
*/
|
|
1003
957
|
/** RFC 4122 v4 UUID from crypto.getRandomValues (available on insecure origins). */
|
|
1004
958
|
function uuidFromRandomValues() {
|
|
@@ -1042,8 +996,14 @@ window.__ModuleLoader__.load({
|
|
|
1042
996
|
installRandomUuidShim();
|
|
1043
997
|
const name = "dsh-lan-gateway";
|
|
1044
998
|
/**
|
|
1045
|
-
* The
|
|
1046
|
-
*
|
|
999
|
+
* The package name dsh keys a bundle's own configuration by in
|
|
1000
|
+
* `plugins.bundle.config`, and the name this profile's bundle row resolves to.
|
|
1001
|
+
* It is the package's npm name — not the locale namespace or the row id.
|
|
1002
|
+
*/
|
|
1003
|
+
const PACKAGE_NAME = "@riceawa/dsh-lan-gateway";
|
|
1004
|
+
/**
|
|
1005
|
+
* The profile entry id this plugin's bundle patch composes it under. dsh >=
|
|
1006
|
+
* 0.1.7 addresses both settings writes and the serve gate by entry id.
|
|
1047
1007
|
*/
|
|
1048
1008
|
const ENTRY_ID = "dsh-lan-gateway";
|
|
1049
1009
|
/** The slots service the card rides, and the settings mirror the gate reads. */
|
|
@@ -1054,11 +1014,9 @@ window.__ModuleLoader__.load({
|
|
|
1054
1014
|
*/
|
|
1055
1015
|
function apply(ctx) {
|
|
1056
1016
|
installRandomUuidShim();
|
|
1057
|
-
ctx.effect(() => ctx.configForms.whileServed([ENTRY_ID], () => ctx.slots.inject("plugins.
|
|
1058
|
-
name: "plugins.
|
|
1059
|
-
|
|
1060
|
-
order: 30,
|
|
1061
|
-
label: () => cardTitle()
|
|
1017
|
+
ctx.effect(() => ctx.configForms.whileServed([ENTRY_ID], () => ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
|
|
1018
|
+
name: "plugins.bundle.config",
|
|
1019
|
+
key: PACKAGE_NAME
|
|
1062
1020
|
}, LanGatewayCard))));
|
|
1063
1021
|
}
|
|
1064
1022
|
//#endregion
|