@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/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