@riceawa/dsh-lan-gateway 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +221 -0
- package/cordis.patch.yml +10 -0
- package/lib/client.js +733 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +71 -0
- package/lib/index.js +1310 -0
- package/package.json +86 -0
- package/skills/lan-gateway.md +56 -0
- package/src/auth.ts +182 -0
- package/src/client/index.ts +89 -0
- package/src/client/lan-gateway-card.tsx +603 -0
- package/src/gateway.ts +343 -0
- package/src/index.ts +498 -0
- package/src/login.ts +133 -0
- package/src/state.ts +93 -0
- package/src/tls.ts +164 -0
- package/src/tool.ts +82 -0
- package/src/x509.ts +314 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@riceawa/dsh-lan-gateway",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
//#region src/client/lan-gateway-card.tsx
|
|
10
|
+
/**
|
|
11
|
+
* The lan-gateway settings card shown in the official DSH Settings → Plugins
|
|
12
|
+
* page (the `settings.plugin.item` slot).
|
|
13
|
+
*
|
|
14
|
+
* ModLens-style: the card carries NO injected services. It reads and writes
|
|
15
|
+
* the loopback-only `/lan-gateway/config` host route (the browser never sees
|
|
16
|
+
* the settings seam or any secret), so the client bundle's only dependency is
|
|
17
|
+
* the `slots` service that every plugin already has.
|
|
18
|
+
*
|
|
19
|
+
* @module @riceawa/dsh-lan-gateway/client/card
|
|
20
|
+
*/
|
|
21
|
+
const LABELS = {
|
|
22
|
+
zh: {
|
|
23
|
+
title: "LAN 网关",
|
|
24
|
+
description: "远程访问开关、端口、TLS 证书、受信网段等网关设置",
|
|
25
|
+
unsaved: "未保存",
|
|
26
|
+
save: "保存",
|
|
27
|
+
saving: "保存中…",
|
|
28
|
+
discard: "放弃",
|
|
29
|
+
reset: "重置",
|
|
30
|
+
overridden: "已覆盖",
|
|
31
|
+
readOnly: "网关设置当前不可用(读不到配置路由)。",
|
|
32
|
+
saveFailed: "保存未生效,请检查输入后重试。",
|
|
33
|
+
loadFailed: "加载网关配置失败。",
|
|
34
|
+
emptyMeansClear: "留空 = 使用默认",
|
|
35
|
+
running: "运行中",
|
|
36
|
+
stopped: "已停止",
|
|
37
|
+
tls: "TLS",
|
|
38
|
+
lastError: "上次错误",
|
|
39
|
+
"field.enabled": "启用网关",
|
|
40
|
+
"hint.enabled": "启动时监听 0.0.0.0 网关端口",
|
|
41
|
+
"field.gatewayPort": "网关端口",
|
|
42
|
+
"hint.gatewayPort": "绑定到 0.0.0.0 的监听端口(默认 3081)",
|
|
43
|
+
"field.dshTargetPort": "dsh 目标端口",
|
|
44
|
+
"hint.dshTargetPort": "留空则自动跟随 dsh web 端口(默认 3080)",
|
|
45
|
+
"field.lanCidrs": "免密 LAN 网段",
|
|
46
|
+
"hint.lanCidrs": "逗号分隔的 CIDR,如 10.0.0.0/8, 192.168.0.0/16",
|
|
47
|
+
"field.authRequired": "非 LAN 访问需要密码",
|
|
48
|
+
"hint.authRequired": "公网来源必须登录后才能访问",
|
|
49
|
+
"field.cookieMaxAgeDays": "会话有效期(天)",
|
|
50
|
+
"hint.cookieMaxAgeDays": "登录 cookie 的存活天数(默认 7)",
|
|
51
|
+
"field.tlsEnabled": "启用 TLS(HTTPS)",
|
|
52
|
+
"hint.tlsEnabled": "以 HTTPS 提供网关服务",
|
|
53
|
+
"field.tlsMode": "证书来源",
|
|
54
|
+
"hint.tlsMode": "self-signed = 自动生成自签名证书;custom = 使用自己的证书",
|
|
55
|
+
"field.tlsSelfSignedHosts": "自签名证书域名/IP",
|
|
56
|
+
"hint.tlsSelfSignedHosts": "逗号分隔,写入证书 SAN,如 localhost, 192.168.1.5",
|
|
57
|
+
"field.tlsCertPath": "证书文件路径(custom)",
|
|
58
|
+
"hint.tlsCertPath": "PEM 格式证书(或证书链)的绝对路径",
|
|
59
|
+
"field.tlsKeyPath": "私钥文件路径(custom)",
|
|
60
|
+
"hint.tlsKeyPath": "与证书配套的 PEM 私钥绝对路径",
|
|
61
|
+
"field.tlsCertMaxAgeDays": "自签名证书有效期(天)",
|
|
62
|
+
"hint.tlsCertMaxAgeDays": "默认 825(约 27 个月)"
|
|
63
|
+
},
|
|
64
|
+
en: {
|
|
65
|
+
title: "LAN Gateway",
|
|
66
|
+
description: "Remote-access switch, port, TLS certificate, trusted CIDRs and more",
|
|
67
|
+
unsaved: "Unsaved",
|
|
68
|
+
save: "Save",
|
|
69
|
+
saving: "Saving…",
|
|
70
|
+
discard: "Discard",
|
|
71
|
+
reset: "Reset",
|
|
72
|
+
overridden: "overridden",
|
|
73
|
+
readOnly: "Gateway settings unavailable (config route unreachable).",
|
|
74
|
+
saveFailed: "The save did not land — check the inputs and retry.",
|
|
75
|
+
loadFailed: "Failed to load gateway configuration.",
|
|
76
|
+
emptyMeansClear: "Empty = default",
|
|
77
|
+
running: "Running",
|
|
78
|
+
stopped: "Stopped",
|
|
79
|
+
tls: "TLS",
|
|
80
|
+
lastError: "Last error",
|
|
81
|
+
"field.enabled": "Enable gateway",
|
|
82
|
+
"hint.enabled": "Listen on the gateway port at boot",
|
|
83
|
+
"field.gatewayPort": "Gateway port",
|
|
84
|
+
"hint.gatewayPort": "Port bound on 0.0.0.0 (default 3081)",
|
|
85
|
+
"field.dshTargetPort": "dsh target port",
|
|
86
|
+
"hint.dshTargetPort": "Leave empty to follow the dsh web port (default 3080)",
|
|
87
|
+
"field.lanCidrs": "Password-free LAN CIDRs",
|
|
88
|
+
"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",
|
|
91
|
+
"field.cookieMaxAgeDays": "Session lifetime (days)",
|
|
92
|
+
"hint.cookieMaxAgeDays": "Login cookie lifetime (default 7)",
|
|
93
|
+
"field.tlsEnabled": "Enable TLS (HTTPS)",
|
|
94
|
+
"hint.tlsEnabled": "Serve the gateway over HTTPS",
|
|
95
|
+
"field.tlsMode": "Certificate source",
|
|
96
|
+
"hint.tlsMode": "self-signed = auto-generated certificate; custom = your own files",
|
|
97
|
+
"field.tlsSelfSignedHosts": "Self-signed hosts (SANs)",
|
|
98
|
+
"hint.tlsSelfSignedHosts": "Comma separated DNS/IP names, e.g. localhost, 192.168.1.5",
|
|
99
|
+
"field.tlsCertPath": "Certificate path (custom)",
|
|
100
|
+
"hint.tlsCertPath": "Absolute path to a PEM certificate (or chain)",
|
|
101
|
+
"field.tlsKeyPath": "Private key path (custom)",
|
|
102
|
+
"hint.tlsKeyPath": "Absolute path to the matching PEM private key",
|
|
103
|
+
"field.tlsCertMaxAgeDays": "Self-signed validity (days)",
|
|
104
|
+
"hint.tlsCertMaxAgeDays": "Default 825 (about 27 months)"
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
function labels() {
|
|
108
|
+
return (typeof navigator !== "undefined" ? navigator.language : "en").toLowerCase().startsWith("zh") ? LABELS.zh : LABELS.en;
|
|
109
|
+
}
|
|
110
|
+
const FIELDS = [
|
|
111
|
+
{
|
|
112
|
+
field: "enabled",
|
|
113
|
+
kind: "boolean"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
field: "gatewayPort",
|
|
117
|
+
kind: "number"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
field: "dshTargetPort",
|
|
121
|
+
kind: "number",
|
|
122
|
+
optional: true
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
field: "lanCidrs",
|
|
126
|
+
kind: "cidrs"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
field: "authRequired",
|
|
130
|
+
kind: "boolean"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
field: "cookieMaxAgeDays",
|
|
134
|
+
kind: "number"
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
field: "tlsEnabled",
|
|
138
|
+
kind: "boolean"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
field: "tlsMode",
|
|
142
|
+
kind: "select",
|
|
143
|
+
options: ["self-signed", "custom"]
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
field: "tlsSelfSignedHosts",
|
|
147
|
+
kind: "text"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
field: "tlsCertPath",
|
|
151
|
+
kind: "text",
|
|
152
|
+
optional: true
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
field: "tlsKeyPath",
|
|
156
|
+
kind: "text",
|
|
157
|
+
optional: true
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
field: "tlsCertMaxAgeDays",
|
|
161
|
+
kind: "number"
|
|
162
|
+
}
|
|
163
|
+
];
|
|
164
|
+
function formatValue(def, value) {
|
|
165
|
+
switch (def.kind) {
|
|
166
|
+
case "boolean": return value === true ? "true" : "false";
|
|
167
|
+
case "number": return typeof value === "number" ? String(value) : "";
|
|
168
|
+
case "cidrs": return Array.isArray(value) ? value.join(", ") : "";
|
|
169
|
+
case "select": return typeof value === "string" ? value : def.options?.[0] ?? "";
|
|
170
|
+
case "text": return typeof value === "string" ? value : "";
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** Parse draft text into a value for the POST body; undefined blocks saving. */
|
|
174
|
+
function parseValue(def, text) {
|
|
175
|
+
const trimmed = text.trim();
|
|
176
|
+
switch (def.kind) {
|
|
177
|
+
case "boolean":
|
|
178
|
+
if (trimmed === "true") return {
|
|
179
|
+
kind: "set",
|
|
180
|
+
value: true
|
|
181
|
+
};
|
|
182
|
+
if (trimmed === "false") return {
|
|
183
|
+
kind: "set",
|
|
184
|
+
value: false
|
|
185
|
+
};
|
|
186
|
+
return;
|
|
187
|
+
case "number":
|
|
188
|
+
if (trimmed === "") return def.optional ? { kind: "clear" } : void 0;
|
|
189
|
+
if (!/^\d+$/.test(trimmed)) return void 0;
|
|
190
|
+
return {
|
|
191
|
+
kind: "set",
|
|
192
|
+
value: Number(trimmed)
|
|
193
|
+
};
|
|
194
|
+
case "cidrs": {
|
|
195
|
+
const cidrs = trimmed.split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
196
|
+
return cidrs.length === 0 ? { kind: "clear" } : {
|
|
197
|
+
kind: "set",
|
|
198
|
+
value: cidrs
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
case "select": return def.options?.includes(trimmed) ? {
|
|
202
|
+
kind: "set",
|
|
203
|
+
value: trimmed
|
|
204
|
+
} : void 0;
|
|
205
|
+
case "text": return trimmed === "" ? def.optional ? { kind: "clear" } : void 0 : {
|
|
206
|
+
kind: "set",
|
|
207
|
+
value: trimmed
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Render the LAN gateway card. Self-loading: fetches the config route on
|
|
213
|
+
* mount, posts the edited config on save.
|
|
214
|
+
* @param _props - unused; the card needs no injected face.
|
|
215
|
+
* @returns the card, or nothing while the route is unreachable.
|
|
216
|
+
*/
|
|
217
|
+
function LanGatewayCard(_props) {
|
|
218
|
+
const t = labels();
|
|
219
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
220
|
+
const [route, setRoute] = (0, react.useState)(null);
|
|
221
|
+
const [loadFailed, setLoadFailed] = (0, react.useState)(false);
|
|
222
|
+
const [drafts, setDrafts] = (0, react.useState)({});
|
|
223
|
+
const [saving, setSaving] = (0, react.useState)(false);
|
|
224
|
+
const [failed, setFailed] = (0, react.useState)(null);
|
|
225
|
+
(0, react.useEffect)(() => {
|
|
226
|
+
let cancelled = false;
|
|
227
|
+
fetch("/lan-gateway/config").then(async (response) => {
|
|
228
|
+
if (cancelled) return;
|
|
229
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
230
|
+
setRoute(await response.json());
|
|
231
|
+
}).catch(() => {
|
|
232
|
+
if (!cancelled) setLoadFailed(true);
|
|
233
|
+
});
|
|
234
|
+
return () => {
|
|
235
|
+
cancelled = true;
|
|
236
|
+
};
|
|
237
|
+
}, []);
|
|
238
|
+
if (loadFailed) return null;
|
|
239
|
+
if (route === null) return null;
|
|
240
|
+
const { config } = route;
|
|
241
|
+
const draftOf = (field) => drafts[field] ?? formatValue(FIELDS.find((f) => f.field === field), config[field]);
|
|
242
|
+
const stage = (field, text) => {
|
|
243
|
+
setDrafts((prev) => ({
|
|
244
|
+
...prev,
|
|
245
|
+
[field]: text
|
|
246
|
+
}));
|
|
247
|
+
setFailed(null);
|
|
248
|
+
};
|
|
249
|
+
const resetField = (def) => {
|
|
250
|
+
setDrafts((prev) => {
|
|
251
|
+
const next = { ...prev };
|
|
252
|
+
delete next[def.field];
|
|
253
|
+
return next;
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
const discard = () => {
|
|
257
|
+
setDrafts({});
|
|
258
|
+
setFailed(null);
|
|
259
|
+
};
|
|
260
|
+
const invalid = () => Object.entries(drafts).some(([field, text]) => {
|
|
261
|
+
const def = FIELDS.find((f) => f.field === field);
|
|
262
|
+
return def === void 0 || parseValue(def, text ?? "") === void 0;
|
|
263
|
+
});
|
|
264
|
+
const dirty = Object.keys(drafts).length > 0;
|
|
265
|
+
const save = async () => {
|
|
266
|
+
if (!dirty || saving || invalid()) return;
|
|
267
|
+
setSaving(true);
|
|
268
|
+
setFailed(null);
|
|
269
|
+
try {
|
|
270
|
+
const next = {};
|
|
271
|
+
for (const def of FIELDS) {
|
|
272
|
+
const write = parseValue(def, drafts[def.field] ?? formatValue(def, config[def.field]));
|
|
273
|
+
if (write === void 0) continue;
|
|
274
|
+
next[def.field] = write.kind === "clear" ? null : write.value;
|
|
275
|
+
}
|
|
276
|
+
const response = await fetch("/lan-gateway/config", {
|
|
277
|
+
method: "POST",
|
|
278
|
+
headers: { "content-type": "application/json" },
|
|
279
|
+
body: JSON.stringify(next)
|
|
280
|
+
});
|
|
281
|
+
const body = await response.json().catch(() => ({}));
|
|
282
|
+
if (!response.ok) {
|
|
283
|
+
setFailed(body.error ?? `HTTP ${response.status}`);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (body.config !== void 0) setRoute({
|
|
287
|
+
config: body.config,
|
|
288
|
+
running: body.running ?? false,
|
|
289
|
+
port: body.port ?? 0,
|
|
290
|
+
tls: body.tls ?? "",
|
|
291
|
+
lastError: body.lastError ?? null
|
|
292
|
+
});
|
|
293
|
+
setDrafts({});
|
|
294
|
+
} catch {
|
|
295
|
+
setFailed(t.saveFailed);
|
|
296
|
+
} finally {
|
|
297
|
+
setSaving(false);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
const renderControl = (def) => {
|
|
301
|
+
const field = def.field;
|
|
302
|
+
const label = t[`field.${field}`];
|
|
303
|
+
const hint = t[`hint.${field}`];
|
|
304
|
+
const text = draftOf(field);
|
|
305
|
+
switch (def.kind) {
|
|
306
|
+
case "boolean": return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
307
|
+
style: styles.field,
|
|
308
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
309
|
+
style: styles.checkRow,
|
|
310
|
+
children: [
|
|
311
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
312
|
+
type: "checkbox",
|
|
313
|
+
checked: text === "true",
|
|
314
|
+
disabled: saving,
|
|
315
|
+
onChange: (e) => stage(field, e.target.checked ? "true" : "false")
|
|
316
|
+
}),
|
|
317
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
318
|
+
style: styles.label,
|
|
319
|
+
children: label
|
|
320
|
+
}),
|
|
321
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
322
|
+
type: "button",
|
|
323
|
+
style: styles.reset,
|
|
324
|
+
disabled: saving || !drafts[field],
|
|
325
|
+
onClick: () => resetField(def),
|
|
326
|
+
children: t.reset
|
|
327
|
+
})
|
|
328
|
+
]
|
|
329
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
330
|
+
style: styles.hint,
|
|
331
|
+
children: hint
|
|
332
|
+
})]
|
|
333
|
+
});
|
|
334
|
+
case "select": return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
335
|
+
style: styles.field,
|
|
336
|
+
children: [
|
|
337
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
338
|
+
style: styles.label,
|
|
339
|
+
htmlFor: `lan-gw-${field}`,
|
|
340
|
+
children: label
|
|
341
|
+
}),
|
|
342
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
343
|
+
id: `lan-gw-${field}`,
|
|
344
|
+
style: styles.input,
|
|
345
|
+
value: text,
|
|
346
|
+
disabled: saving,
|
|
347
|
+
onChange: (e) => stage(field, e.target.value),
|
|
348
|
+
children: def.options?.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
349
|
+
value: option,
|
|
350
|
+
children: option
|
|
351
|
+
}, option))
|
|
352
|
+
}),
|
|
353
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
354
|
+
style: styles.hint,
|
|
355
|
+
children: hint
|
|
356
|
+
}),
|
|
357
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
358
|
+
type: "button",
|
|
359
|
+
style: styles.reset,
|
|
360
|
+
disabled: saving || !drafts[field],
|
|
361
|
+
onClick: () => resetField(def),
|
|
362
|
+
children: t.reset
|
|
363
|
+
})
|
|
364
|
+
]
|
|
365
|
+
});
|
|
366
|
+
default: return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
367
|
+
style: styles.field,
|
|
368
|
+
children: [
|
|
369
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
370
|
+
style: styles.label,
|
|
371
|
+
htmlFor: `lan-gw-${field}`,
|
|
372
|
+
children: label
|
|
373
|
+
}),
|
|
374
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
375
|
+
id: `lan-gw-${field}`,
|
|
376
|
+
style: styles.input,
|
|
377
|
+
type: def.kind === "number" ? "number" : "text",
|
|
378
|
+
value: text,
|
|
379
|
+
disabled: saving,
|
|
380
|
+
placeholder: def.optional ? t.emptyMeansClear : void 0,
|
|
381
|
+
onChange: (e) => stage(field, e.target.value)
|
|
382
|
+
}),
|
|
383
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
384
|
+
style: styles.hint,
|
|
385
|
+
children: hint
|
|
386
|
+
})
|
|
387
|
+
]
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
const statusLine = `${route.running ? t.running : t.stopped} · ${t.tls}: ${route.tls} · :${route.port}`;
|
|
392
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
393
|
+
style: open ? {
|
|
394
|
+
...styles.card,
|
|
395
|
+
...styles.cardOpen
|
|
396
|
+
} : styles.card,
|
|
397
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
398
|
+
type: "button",
|
|
399
|
+
style: styles.header,
|
|
400
|
+
"aria-expanded": open,
|
|
401
|
+
onClick: () => {
|
|
402
|
+
setOpen(!open);
|
|
403
|
+
},
|
|
404
|
+
children: [
|
|
405
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
406
|
+
style: styles.headText,
|
|
407
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
408
|
+
style: styles.name,
|
|
409
|
+
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
|
+
]
|
|
431
|
+
}), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
432
|
+
style: styles.body,
|
|
433
|
+
children: [
|
|
434
|
+
route.lastError ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
435
|
+
style: styles.error,
|
|
436
|
+
role: "status",
|
|
437
|
+
children: [
|
|
438
|
+
t.lastError,
|
|
439
|
+
": ",
|
|
440
|
+
route.lastError
|
|
441
|
+
]
|
|
442
|
+
}) : null,
|
|
443
|
+
FIELDS.map((def) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: renderControl(def) }, def.field)),
|
|
444
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
445
|
+
style: styles.footer,
|
|
446
|
+
children: [
|
|
447
|
+
failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
448
|
+
style: styles.error,
|
|
449
|
+
role: "status",
|
|
450
|
+
children: failed
|
|
451
|
+
}) : null,
|
|
452
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
453
|
+
type: "button",
|
|
454
|
+
style: styles.discard,
|
|
455
|
+
disabled: !dirty || saving,
|
|
456
|
+
onClick: discard,
|
|
457
|
+
children: t.discard
|
|
458
|
+
}),
|
|
459
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
460
|
+
type: "button",
|
|
461
|
+
style: styles.save,
|
|
462
|
+
disabled: !dirty || invalid() || saving,
|
|
463
|
+
onClick: () => {
|
|
464
|
+
save();
|
|
465
|
+
},
|
|
466
|
+
children: saving ? t.saving : t.save
|
|
467
|
+
})
|
|
468
|
+
]
|
|
469
|
+
})
|
|
470
|
+
]
|
|
471
|
+
}) : null]
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
/** Theme token with a fallback for token-less environments. */
|
|
475
|
+
function tk(token, fallback) {
|
|
476
|
+
return `var(${token}, ${fallback})`;
|
|
477
|
+
}
|
|
478
|
+
const L = {
|
|
479
|
+
border: tk("--dsw-alias-border-l2", "rgba(127,127,127,0.35)"),
|
|
480
|
+
bg: tk("--dsw-alias-bg-layer-3", "transparent"),
|
|
481
|
+
bgOpen: tk("--dsw-alias-bg-layer-2", "transparent"),
|
|
482
|
+
labelPrimary: tk("--dsw-alias-label-primary", "inherit"),
|
|
483
|
+
labelSecondary: tk("--dsw-alias-label-secondary", "inherit"),
|
|
484
|
+
labelTertiary: tk("--dsw-alias-label-tertiary", "rgba(127,127,127,0.8)"),
|
|
485
|
+
labelDimmed: tk("--dsw-alias-label-dimmed", "rgba(127,127,127,0.6)"),
|
|
486
|
+
error: tk("--dsw-alias-label-error", "#d1242f"),
|
|
487
|
+
brand: tk("--dsw-alias-brand-primary", "#4f6ef7"),
|
|
488
|
+
badgeBg: tk("--dsw-alias-bg-module-platform", "rgba(127,127,127,0.14)")
|
|
489
|
+
};
|
|
490
|
+
const styles = {
|
|
491
|
+
card: {
|
|
492
|
+
listStyle: "none",
|
|
493
|
+
border: `1px solid ${L.border}`,
|
|
494
|
+
borderRadius: "12px",
|
|
495
|
+
background: L.bg,
|
|
496
|
+
transition: "border-color .16s, background .16s",
|
|
497
|
+
overflow: "hidden"
|
|
498
|
+
},
|
|
499
|
+
cardOpen: {
|
|
500
|
+
background: L.bgOpen,
|
|
501
|
+
borderColor: L.labelDimmed
|
|
502
|
+
},
|
|
503
|
+
header: {
|
|
504
|
+
display: "flex",
|
|
505
|
+
alignItems: "center",
|
|
506
|
+
gap: "12px",
|
|
507
|
+
width: "100%",
|
|
508
|
+
padding: "14px 16px",
|
|
509
|
+
border: 0,
|
|
510
|
+
background: "none",
|
|
511
|
+
font: "inherit",
|
|
512
|
+
color: "inherit",
|
|
513
|
+
textAlign: "left",
|
|
514
|
+
cursor: "pointer"
|
|
515
|
+
},
|
|
516
|
+
headText: {
|
|
517
|
+
display: "flex",
|
|
518
|
+
flexDirection: "column",
|
|
519
|
+
gap: "4px",
|
|
520
|
+
flex: 1,
|
|
521
|
+
minWidth: 0
|
|
522
|
+
},
|
|
523
|
+
name: {
|
|
524
|
+
fontSize: "15px",
|
|
525
|
+
fontWeight: 600,
|
|
526
|
+
lineHeight: 1.4,
|
|
527
|
+
color: L.labelPrimary
|
|
528
|
+
},
|
|
529
|
+
description: {
|
|
530
|
+
fontSize: "13px",
|
|
531
|
+
lineHeight: 1.5,
|
|
532
|
+
color: L.labelTertiary
|
|
533
|
+
},
|
|
534
|
+
status: {
|
|
535
|
+
fontSize: "11px",
|
|
536
|
+
color: L.labelTertiary,
|
|
537
|
+
whiteSpace: "nowrap"
|
|
538
|
+
},
|
|
539
|
+
pending: {
|
|
540
|
+
flex: "none",
|
|
541
|
+
borderRadius: "999px",
|
|
542
|
+
padding: "1px 8px",
|
|
543
|
+
fontSize: "11px",
|
|
544
|
+
lineHeight: "17px",
|
|
545
|
+
fontWeight: 500,
|
|
546
|
+
whiteSpace: "nowrap",
|
|
547
|
+
background: L.badgeBg,
|
|
548
|
+
color: L.labelSecondary
|
|
549
|
+
},
|
|
550
|
+
chevron: {
|
|
551
|
+
flex: "none",
|
|
552
|
+
color: L.labelTertiary,
|
|
553
|
+
fontSize: "12px",
|
|
554
|
+
transition: "transform .16s"
|
|
555
|
+
},
|
|
556
|
+
chevronOpen: { transform: "rotate(180deg)" },
|
|
557
|
+
body: {
|
|
558
|
+
borderTop: `1px solid ${L.border}`,
|
|
559
|
+
margin: "0 16px",
|
|
560
|
+
paddingBottom: "8px",
|
|
561
|
+
display: "flex",
|
|
562
|
+
flexDirection: "column"
|
|
563
|
+
},
|
|
564
|
+
field: {
|
|
565
|
+
display: "flex",
|
|
566
|
+
flexDirection: "column",
|
|
567
|
+
gap: "6px",
|
|
568
|
+
padding: "12px 0"
|
|
569
|
+
},
|
|
570
|
+
label: {
|
|
571
|
+
fontSize: "13px",
|
|
572
|
+
fontWeight: 500,
|
|
573
|
+
lineHeight: 1.5,
|
|
574
|
+
color: L.labelPrimary
|
|
575
|
+
},
|
|
576
|
+
hint: {
|
|
577
|
+
margin: 0,
|
|
578
|
+
fontSize: "12px",
|
|
579
|
+
lineHeight: 1.5,
|
|
580
|
+
color: L.labelTertiary
|
|
581
|
+
},
|
|
582
|
+
checkRow: {
|
|
583
|
+
display: "flex",
|
|
584
|
+
alignItems: "center",
|
|
585
|
+
gap: "8px",
|
|
586
|
+
cursor: "pointer"
|
|
587
|
+
},
|
|
588
|
+
reset: {
|
|
589
|
+
border: "none",
|
|
590
|
+
background: "none",
|
|
591
|
+
padding: 0,
|
|
592
|
+
font: "inherit",
|
|
593
|
+
fontSize: "12px",
|
|
594
|
+
lineHeight: 1.5,
|
|
595
|
+
color: L.labelSecondary,
|
|
596
|
+
cursor: "pointer",
|
|
597
|
+
alignSelf: "flex-start"
|
|
598
|
+
},
|
|
599
|
+
input: {
|
|
600
|
+
height: "34px",
|
|
601
|
+
padding: "0 12px",
|
|
602
|
+
border: `1px solid ${L.border}`,
|
|
603
|
+
borderRadius: "8px",
|
|
604
|
+
background: L.bg,
|
|
605
|
+
font: "inherit",
|
|
606
|
+
fontSize: "13px",
|
|
607
|
+
lineHeight: 1.5,
|
|
608
|
+
color: L.labelPrimary
|
|
609
|
+
},
|
|
610
|
+
footer: {
|
|
611
|
+
display: "flex",
|
|
612
|
+
alignItems: "center",
|
|
613
|
+
justifyContent: "flex-end",
|
|
614
|
+
gap: "8px",
|
|
615
|
+
padding: "12px 0 4px",
|
|
616
|
+
borderTop: `1px solid ${L.border}`
|
|
617
|
+
},
|
|
618
|
+
error: {
|
|
619
|
+
flex: 1,
|
|
620
|
+
minWidth: 0,
|
|
621
|
+
margin: 0,
|
|
622
|
+
fontSize: "12px",
|
|
623
|
+
lineHeight: 1.5,
|
|
624
|
+
color: L.error
|
|
625
|
+
},
|
|
626
|
+
discard: {
|
|
627
|
+
appearance: "none",
|
|
628
|
+
border: `1px solid ${L.border}`,
|
|
629
|
+
borderRadius: "8px",
|
|
630
|
+
padding: "5px 14px",
|
|
631
|
+
font: "inherit",
|
|
632
|
+
fontSize: "13px",
|
|
633
|
+
lineHeight: 1.5,
|
|
634
|
+
background: "none",
|
|
635
|
+
color: L.labelSecondary,
|
|
636
|
+
cursor: "pointer"
|
|
637
|
+
},
|
|
638
|
+
save: {
|
|
639
|
+
appearance: "none",
|
|
640
|
+
border: "1px solid transparent",
|
|
641
|
+
borderRadius: "8px",
|
|
642
|
+
padding: "5px 14px",
|
|
643
|
+
font: "inherit",
|
|
644
|
+
fontSize: "13px",
|
|
645
|
+
lineHeight: 1.5,
|
|
646
|
+
background: L.labelPrimary,
|
|
647
|
+
color: L.bg,
|
|
648
|
+
cursor: "pointer"
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
//#endregion
|
|
652
|
+
//#region src/client/index.ts
|
|
653
|
+
/**
|
|
654
|
+
* @riceawa/dsh-lan-gateway — browser half.
|
|
655
|
+
*
|
|
656
|
+
* Two jobs:
|
|
657
|
+
* 1. Insecure-origin UUID shim: the gateway can serve the GUI over plain HTTP
|
|
658
|
+
* on LAN addresses, where browsers lack `crypto.randomUUID()`. This bundle
|
|
659
|
+
* installs a getRandomValues-backed `randomUUID` on the Crypto prototype at
|
|
660
|
+
* module scope. With TLS enabled the origin is secure and the shim is a
|
|
661
|
+
* no-op.
|
|
662
|
+
* 2. Settings card: registers the LAN gateway card into the official
|
|
663
|
+
* Settings → Plugins page (`settings.plugin.item` slot), editing the
|
|
664
|
+
* `lan-gateway` settings namespace so port, CIDRs, auth, and TLS are
|
|
665
|
+
* adjustable from the GUI.
|
|
666
|
+
*/
|
|
667
|
+
/** RFC 4122 v4 UUID from crypto.getRandomValues (available on insecure origins). */
|
|
668
|
+
function uuidFromRandomValues() {
|
|
669
|
+
const bytes = globalThis.crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
|
|
670
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
671
|
+
view.setUint8(6, view.getUint8(6) & 15 | 64);
|
|
672
|
+
view.setUint8(8, view.getUint8(8) & 63 | 128);
|
|
673
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
674
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Install `randomUUID` on the browser Crypto prototype when the platform
|
|
678
|
+
* lacks it. Idempotent; re-checks every call.
|
|
679
|
+
* @returns `true` when the shim was installed by this call.
|
|
680
|
+
*/
|
|
681
|
+
function installRandomUuidShim() {
|
|
682
|
+
const cryptoObj = globalThis.crypto;
|
|
683
|
+
if (cryptoObj === void 0) return false;
|
|
684
|
+
if (typeof cryptoObj.randomUUID === "function") return false;
|
|
685
|
+
if (typeof cryptoObj.getRandomValues !== "function") return false;
|
|
686
|
+
try {
|
|
687
|
+
const proto = Object.getPrototypeOf(cryptoObj);
|
|
688
|
+
if (proto !== null && typeof proto.randomUUID !== "function") {
|
|
689
|
+
Object.defineProperty(proto, "randomUUID", {
|
|
690
|
+
value: uuidFromRandomValues,
|
|
691
|
+
writable: true,
|
|
692
|
+
configurable: true
|
|
693
|
+
});
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
696
|
+
Object.defineProperty(cryptoObj, "randomUUID", {
|
|
697
|
+
value: uuidFromRandomValues,
|
|
698
|
+
writable: true,
|
|
699
|
+
configurable: true
|
|
700
|
+
});
|
|
701
|
+
return true;
|
|
702
|
+
} catch {
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
installRandomUuidShim();
|
|
707
|
+
const name = "dsh-lan-gateway";
|
|
708
|
+
/** Only the slots service: the card itself is self-loading (ModLens-style). */
|
|
709
|
+
const inject = ["slots"];
|
|
710
|
+
/**
|
|
711
|
+
* Mount the settings card and the UUID shim.
|
|
712
|
+
* @param ctx - the browser plugin context.
|
|
713
|
+
*/
|
|
714
|
+
function apply(ctx) {
|
|
715
|
+
installRandomUuidShim();
|
|
716
|
+
ctx.slots.inject("settings.plugin.item", function* () {
|
|
717
|
+
yield ctx.slots.register({
|
|
718
|
+
name: "settings.plugin.item",
|
|
719
|
+
id: "lan-gateway",
|
|
720
|
+
order: 30
|
|
721
|
+
}, LanGatewayCard);
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
//#endregion
|
|
725
|
+
exports.apply = apply;
|
|
726
|
+
exports.inject = inject;
|
|
727
|
+
exports.installRandomUuidShim = installRandomUuidShim;
|
|
728
|
+
exports.name = name;
|
|
729
|
+
return module.exports;
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
//# sourceMappingURL=client.js.map
|