@lvxianchao/dsh-web-guard 2.1.1 → 3.0.1

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,1071 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@lvxianchao/dsh-web-guard",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ "use strict";
7
+ var __defProp = Object.defineProperty;
8
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
9
+ var __getOwnPropNames = Object.getOwnPropertyNames;
10
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
24
+
25
+ // src/client/module.tsx
26
+ var module_exports = {};
27
+ __export(module_exports, {
28
+ apply: () => apply,
29
+ inject: () => inject
30
+ });
31
+ module.exports = __toCommonJS(module_exports);
32
+
33
+ // src/client/GuardSettings.tsx
34
+ var import_react = require("react");
35
+
36
+ // src/shared/api.ts
37
+ var GUARD_API = {
38
+ state: "/__guard/api/state",
39
+ settings: "/__guard/api/settings",
40
+ password: "/__guard/api/password",
41
+ kick: "/__guard/api/kick",
42
+ logoutAll: "/__guard/api/logout-all"
43
+ };
44
+
45
+ // src/client/api.ts
46
+ var ERROR_CODES = /* @__PURE__ */ new Set([
47
+ "unauthorized",
48
+ "forbidden",
49
+ "invalid_body",
50
+ "invalid_field",
51
+ "wrong_current",
52
+ "too_short",
53
+ "mismatch",
54
+ "not_found",
55
+ "method_not_allowed",
56
+ "kick_missing",
57
+ "restart_required"
58
+ ]);
59
+ function isRecord(value) {
60
+ return typeof value === "object" && value !== null && !Array.isArray(value);
61
+ }
62
+ function isErrorCode(value) {
63
+ return typeof value === "string" && ERROR_CODES.has(value);
64
+ }
65
+ async function request(path, init) {
66
+ let response;
67
+ try {
68
+ response = await fetch(path, { ...init, credentials: "same-origin" });
69
+ } catch {
70
+ return { ok: false, error: "network_error" };
71
+ }
72
+ let payload;
73
+ try {
74
+ payload = await response.json();
75
+ } catch {
76
+ payload = void 0;
77
+ }
78
+ if (!isRecord(payload) || payload.ok !== true) {
79
+ const error = isRecord(payload) && isErrorCode(payload.error) ? payload.error : "unknown_error";
80
+ const field = isRecord(payload) && typeof payload.field === "string" ? payload.field : void 0;
81
+ return field === void 0 ? { ok: false, error } : { ok: false, error, field };
82
+ }
83
+ return { ok: true, data: payload };
84
+ }
85
+ function post(path, body) {
86
+ let serialized;
87
+ try {
88
+ serialized = JSON.stringify(body);
89
+ } catch {
90
+ return Promise.resolve({ ok: false, error: "invalid_body" });
91
+ }
92
+ return request(path, {
93
+ method: "POST",
94
+ headers: { accept: "application/json", "content-type": "application/json" },
95
+ body: serialized
96
+ });
97
+ }
98
+ function fetchGuardState() {
99
+ return request(GUARD_API.state, {
100
+ method: "GET",
101
+ headers: { accept: "application/json" }
102
+ });
103
+ }
104
+ function updateGuardSettings(update) {
105
+ return post(GUARD_API.settings, update);
106
+ }
107
+ function changeGuardPassword(change) {
108
+ return post(GUARD_API.password, change);
109
+ }
110
+ function kickGuardSession(kick) {
111
+ return post(GUARD_API.kick, kick);
112
+ }
113
+ function signOutAllDevices() {
114
+ return post(GUARD_API.logoutAll, {});
115
+ }
116
+
117
+ // src/client/GuardSettings.tsx
118
+ var import_jsx_runtime = require("react/jsx-runtime");
119
+ var LIMITS = {
120
+ sessionDays: { min: 0, max: 365 },
121
+ maxAttempts: { min: 1, max: 50 },
122
+ lockMinutes: { min: 1, max: 1440 },
123
+ listenPort: { min: 1, max: 65535 }
124
+ };
125
+ var NOTICE_MS = 4e3;
126
+ var ERROR_KEYS = {
127
+ unauthorized: "error.unauthorized",
128
+ forbidden: "error.forbidden",
129
+ invalid_body: "error.invalid_body",
130
+ invalid_field: "error.invalid_field",
131
+ wrong_current: "error.wrong_current",
132
+ too_short: "error.too_short",
133
+ mismatch: "error.mismatch",
134
+ not_found: "error.not_found",
135
+ method_not_allowed: "error.method_not_allowed",
136
+ kick_missing: "error.kick_missing",
137
+ restart_required: "error.restart_required",
138
+ network_error: "error.network",
139
+ unknown_error: "error.unknown"
140
+ };
141
+ var FIELD_KEYS = {
142
+ sessionDays: "field.sessionDays",
143
+ maxAttempts: "field.maxAttempts",
144
+ lockMinutes: "field.lockMinutes",
145
+ listenHost: "field.listenHost",
146
+ listenPort: "field.listenPort"
147
+ };
148
+ var EMPTY_DRAFT = {
149
+ sessionDays: "",
150
+ maxAttempts: "",
151
+ lockMinutes: "",
152
+ listenHost: "",
153
+ listenPort: ""
154
+ };
155
+ function textValue(value) {
156
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
157
+ if (typeof value === "string") return value;
158
+ return "";
159
+ }
160
+ function draftOf(view) {
161
+ return {
162
+ sessionDays: textValue(view?.sessionDays),
163
+ maxAttempts: textValue(view?.maxAttempts),
164
+ lockMinutes: textValue(view?.lockMinutes),
165
+ listenHost: textValue(view?.listenHost),
166
+ listenPort: textValue(view?.listenPort)
167
+ };
168
+ }
169
+ function withDraftField(current, key, value) {
170
+ const next = { ...current };
171
+ next[key] = value;
172
+ return next;
173
+ }
174
+ function parseInteger(value) {
175
+ const trimmed = value.trim();
176
+ if (trimmed === "" || !/^\d+$/u.test(trimmed)) return void 0;
177
+ const parsed = Number(trimmed);
178
+ return Number.isSafeInteger(parsed) ? parsed : void 0;
179
+ }
180
+ function inRange(value, min, max) {
181
+ return value !== void 0 && Number.isFinite(value) && value >= min && value <= max;
182
+ }
183
+ function makeTranslate(t) {
184
+ if (typeof t === "function") return t;
185
+ return (key) => key;
186
+ }
187
+ function invalidField(t, fieldKey) {
188
+ return t("error.invalid_field", { field: t(fieldKey) });
189
+ }
190
+ function describeError(t, failure) {
191
+ if (failure.error === "invalid_field" && failure.field !== void 0) {
192
+ const key2 = FIELD_KEYS[failure.field];
193
+ return t("error.invalid_field", { field: key2 === void 0 ? failure.field : t(key2) });
194
+ }
195
+ const key = ERROR_KEYS[failure.error];
196
+ if (key !== void 0) return t(key);
197
+ return t("error.generic", { error: failure.error });
198
+ }
199
+ function formatDateTime(value) {
200
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return "\u2014";
201
+ try {
202
+ return new Date(value).toLocaleString();
203
+ } catch {
204
+ return "\u2014";
205
+ }
206
+ }
207
+ function userAgentText(ua) {
208
+ const trimmed = typeof ua === "string" ? ua.trim() : "";
209
+ return trimmed === "" ? "\u2014" : trimmed;
210
+ }
211
+ function useTransientNotice(notice, setNotice) {
212
+ (0, import_react.useEffect)(() => {
213
+ if (notice === void 0 || notice.kind !== "ok") return void 0;
214
+ const timer = window.setTimeout(() => {
215
+ setNotice(void 0);
216
+ }, NOTICE_MS);
217
+ return () => {
218
+ window.clearTimeout(timer);
219
+ };
220
+ }, [notice, setNotice]);
221
+ }
222
+ function Field(props) {
223
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "wg-field", children: [
224
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "wg-label", htmlFor: props.id, children: props.label }),
225
+ props.children,
226
+ props.hint === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "wg-hint", children: props.hint })
227
+ ] });
228
+ }
229
+ function NoticeLine(props) {
230
+ if (props.notice === void 0) return null;
231
+ const failed = props.notice.kind === "error";
232
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: failed ? "wg-notice wg-noticeError" : "wg-notice wg-noticeOk", role: failed ? "alert" : "status", children: props.notice.text });
233
+ }
234
+ function GuardSettings({ t }) {
235
+ const translate = makeTranslate(t);
236
+ const translateRef = (0, import_react.useRef)(translate);
237
+ translateRef.current = translate;
238
+ const [state, setState] = (0, import_react.useState)(void 0);
239
+ const [draft, setDraft] = (0, import_react.useState)(EMPTY_DRAFT);
240
+ const [loading, setLoading] = (0, import_react.useState)(true);
241
+ const [loadError, setLoadError] = (0, import_react.useState)(void 0);
242
+ const [reloadTick, setReloadTick] = (0, import_react.useState)(0);
243
+ const [policyBusy, setPolicyBusy] = (0, import_react.useState)(false);
244
+ const [policyNotice, setPolicyNotice] = (0, import_react.useState)(void 0);
245
+ const [listenerBusy, setListenerBusy] = (0, import_react.useState)(false);
246
+ const [listenerNotice, setListenerNotice] = (0, import_react.useState)(void 0);
247
+ const [listenerRestart, setListenerRestart] = (0, import_react.useState)(false);
248
+ const [currentPassword, setCurrentPassword] = (0, import_react.useState)("");
249
+ const [nextPassword, setNextPassword] = (0, import_react.useState)("");
250
+ const [confirmPassword, setConfirmPassword] = (0, import_react.useState)("");
251
+ const [passwordBusy, setPasswordBusy] = (0, import_react.useState)(false);
252
+ const [passwordNotice, setPasswordNotice] = (0, import_react.useState)(void 0);
253
+ const [kicking, setKicking] = (0, import_react.useState)("");
254
+ const [sessionsNotice, setSessionsNotice] = (0, import_react.useState)(void 0);
255
+ const [signOutBusy, setSignOutBusy] = (0, import_react.useState)(false);
256
+ const [signOutNotice, setSignOutNotice] = (0, import_react.useState)(void 0);
257
+ const [signedOut, setSignedOut] = (0, import_react.useState)(false);
258
+ useTransientNotice(policyNotice, setPolicyNotice);
259
+ useTransientNotice(listenerNotice, setListenerNotice);
260
+ useTransientNotice(passwordNotice, setPasswordNotice);
261
+ useTransientNotice(sessionsNotice, setSessionsNotice);
262
+ useTransientNotice(signOutNotice, setSignOutNotice);
263
+ (0, import_react.useEffect)(() => {
264
+ let alive = true;
265
+ setLoading(true);
266
+ fetchGuardState().then((result) => {
267
+ if (!alive) return;
268
+ if (result.ok) {
269
+ setState(result.data);
270
+ setDraft(draftOf(result.data.settings));
271
+ setListenerRestart(result.data.restartRequired === true);
272
+ setLoadError(void 0);
273
+ } else {
274
+ setLoadError({
275
+ text: describeError(translateRef.current, result),
276
+ unauthorized: result.error === "unauthorized"
277
+ });
278
+ }
279
+ setLoading(false);
280
+ }).catch((error) => {
281
+ if (!alive) return;
282
+ setLoadError({
283
+ text: translateRef.current("error.generic", { error: String(error) }),
284
+ unauthorized: false
285
+ });
286
+ setLoading(false);
287
+ });
288
+ return () => {
289
+ alive = false;
290
+ };
291
+ }, [reloadTick]);
292
+ const acceptSettings = (view) => {
293
+ setState((current) => current === void 0 ? current : { ...current, settings: view });
294
+ };
295
+ const onDraft = (key) => (event) => {
296
+ const value = event.target.value;
297
+ setDraft((current) => withDraftField(current, key, value));
298
+ };
299
+ const applySettingsResult = (result, onSuccess, successText, setNotice) => {
300
+ if (!result.ok) {
301
+ setNotice({ kind: "error", text: describeError(translateRef.current, result) });
302
+ return;
303
+ }
304
+ acceptSettings(result.data.settings);
305
+ onSuccess(result.data);
306
+ setNotice({ kind: "ok", text: successText(result.data) });
307
+ };
308
+ const savePolicy = async (event) => {
309
+ event.preventDefault();
310
+ const t0 = translateRef.current;
311
+ const sessionDays = parseInteger(draft.sessionDays);
312
+ const maxAttempts = parseInteger(draft.maxAttempts);
313
+ const lockMinutes = parseInteger(draft.lockMinutes);
314
+ if (!inRange(sessionDays, LIMITS.sessionDays.min, LIMITS.sessionDays.max)) {
315
+ setPolicyNotice({ kind: "error", text: invalidField(t0, "field.sessionDays") });
316
+ return;
317
+ }
318
+ if (!inRange(maxAttempts, LIMITS.maxAttempts.min, LIMITS.maxAttempts.max)) {
319
+ setPolicyNotice({ kind: "error", text: invalidField(t0, "field.maxAttempts") });
320
+ return;
321
+ }
322
+ if (!inRange(lockMinutes, LIMITS.lockMinutes.min, LIMITS.lockMinutes.max)) {
323
+ setPolicyNotice({ kind: "error", text: invalidField(t0, "field.lockMinutes") });
324
+ return;
325
+ }
326
+ setPolicyBusy(true);
327
+ setPolicyNotice(void 0);
328
+ try {
329
+ const result = await updateGuardSettings({ sessionDays, maxAttempts, lockMinutes });
330
+ applySettingsResult(
331
+ result,
332
+ (data) => {
333
+ setDraft((current) => ({
334
+ ...current,
335
+ sessionDays: textValue(data.settings.sessionDays),
336
+ maxAttempts: textValue(data.settings.maxAttempts),
337
+ lockMinutes: textValue(data.settings.lockMinutes)
338
+ }));
339
+ },
340
+ () => t0("policy.saved"),
341
+ setPolicyNotice
342
+ );
343
+ } finally {
344
+ setPolicyBusy(false);
345
+ }
346
+ };
347
+ const saveListener = async (event) => {
348
+ event.preventDefault();
349
+ const t0 = translateRef.current;
350
+ const listenHost = draft.listenHost.trim();
351
+ const listenPort = parseInteger(draft.listenPort);
352
+ if (listenHost === "" || listenHost.length > 64) {
353
+ setListenerNotice({ kind: "error", text: invalidField(t0, "field.listenHost") });
354
+ return;
355
+ }
356
+ if (!inRange(listenPort, LIMITS.listenPort.min, LIMITS.listenPort.max)) {
357
+ setListenerNotice({ kind: "error", text: invalidField(t0, "field.listenPort") });
358
+ return;
359
+ }
360
+ setListenerBusy(true);
361
+ setListenerNotice(void 0);
362
+ setListenerRestart(false);
363
+ try {
364
+ const result = await updateGuardSettings({ listenHost, listenPort });
365
+ applySettingsResult(
366
+ result,
367
+ (data) => {
368
+ setDraft((current) => ({
369
+ ...current,
370
+ listenHost: textValue(data.settings.listenHost),
371
+ listenPort: textValue(data.settings.listenPort)
372
+ }));
373
+ setListenerRestart(data.restartRequired === true);
374
+ },
375
+ (data) => data.restartRequired ? t0("listener.restartRequired") : t0("listener.saved"),
376
+ setListenerNotice
377
+ );
378
+ } finally {
379
+ setListenerBusy(false);
380
+ }
381
+ };
382
+ const savePassword = async (event) => {
383
+ event.preventDefault();
384
+ const t0 = translateRef.current;
385
+ if (currentPassword === "") {
386
+ setPasswordNotice({ kind: "error", text: t0("password.requiredCurrent") });
387
+ return;
388
+ }
389
+ if (nextPassword === "") {
390
+ setPasswordNotice({ kind: "error", text: t0("password.requiredNext") });
391
+ return;
392
+ }
393
+ if (nextPassword.length < 6) {
394
+ setPasswordNotice({ kind: "error", text: t0("error.too_short") });
395
+ return;
396
+ }
397
+ if (nextPassword !== confirmPassword) {
398
+ setPasswordNotice({ kind: "error", text: t0("error.mismatch") });
399
+ return;
400
+ }
401
+ setPasswordBusy(true);
402
+ setPasswordNotice(void 0);
403
+ try {
404
+ const result = await changeGuardPassword({
405
+ current: currentPassword,
406
+ next: nextPassword,
407
+ confirm: confirmPassword
408
+ });
409
+ if (!result.ok) {
410
+ setPasswordNotice({ kind: "error", text: describeError(t0, result) });
411
+ return;
412
+ }
413
+ setCurrentPassword("");
414
+ setNextPassword("");
415
+ setConfirmPassword("");
416
+ setPasswordNotice({ kind: "ok", text: t0("password.saved") });
417
+ } finally {
418
+ setPasswordBusy(false);
419
+ }
420
+ };
421
+ const refreshSessions = () => {
422
+ setSessionsNotice(void 0);
423
+ setReloadTick((value) => value + 1);
424
+ };
425
+ const kickSession = async (id) => {
426
+ const t0 = translateRef.current;
427
+ setKicking(id);
428
+ setSessionsNotice(void 0);
429
+ try {
430
+ const result = await kickGuardSession({ id });
431
+ if (!result.ok) {
432
+ setSessionsNotice({ kind: "error", text: describeError(t0, result) });
433
+ return;
434
+ }
435
+ setState(
436
+ (current) => current === void 0 ? current : { ...current, sessions: current.sessions.filter((session) => session.id !== id) }
437
+ );
438
+ setSessionsNotice({ kind: "ok", text: t0("sessions.kicked") });
439
+ } finally {
440
+ setKicking("");
441
+ }
442
+ };
443
+ const signOutEverywhere = async () => {
444
+ const t0 = translateRef.current;
445
+ let confirmed = false;
446
+ try {
447
+ confirmed = window.confirm(t0("signout.confirm"));
448
+ } catch {
449
+ confirmed = false;
450
+ }
451
+ if (!confirmed) return;
452
+ setSignOutBusy(true);
453
+ setSignOutNotice(void 0);
454
+ try {
455
+ const result = await signOutAllDevices();
456
+ if (!result.ok) {
457
+ setSignOutNotice({ kind: "error", text: describeError(t0, result) });
458
+ return;
459
+ }
460
+ setSignedOut(true);
461
+ setState((current) => current === void 0 ? current : { ...current, sessions: [] });
462
+ setSignOutNotice({ kind: "ok", text: t0("signout.done") });
463
+ } finally {
464
+ setSignOutBusy(false);
465
+ }
466
+ };
467
+ const sessions = state !== void 0 && Array.isArray(state.sessions) ? state.sessions.filter((session) => session !== null && typeof session === "object") : [];
468
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "wg-root", children: [
469
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", { className: "wg-head", children: [
470
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "wg-title", children: translate("settings.title") }),
471
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "wg-lead", children: translate("settings.lead") })
472
+ ] }),
473
+ loadError === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "wg-card", children: [
474
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "wg-notice wg-noticeError", role: "alert", children: loadError.text }),
475
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "wg-actions", children: [
476
+ loadError.unauthorized ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { className: "wg-btn", href: "/login", children: translate("signout.goLogin") }) : null,
477
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
478
+ "button",
479
+ {
480
+ type: "button",
481
+ className: "wg-btn",
482
+ onClick: () => {
483
+ setReloadTick((value) => value + 1);
484
+ },
485
+ children: translate("common.retry")
486
+ }
487
+ )
488
+ ] })
489
+ ] }),
490
+ loading && state === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "wg-hint", role: "status", children: translate("common.loading") }) : null,
491
+ state === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
492
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "wg-card", "aria-labelledby": "wg-policy-title", children: [
493
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "wg-cardTitle", id: "wg-policy-title", children: translate("policy.section") }),
494
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "wg-cardHint", children: translate("policy.description") }),
495
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
496
+ "form",
497
+ {
498
+ onSubmit: (event) => {
499
+ void savePolicy(event);
500
+ },
501
+ children: [
502
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "wg-grid", children: [
503
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
504
+ Field,
505
+ {
506
+ id: "wg-session-days",
507
+ label: translate("policy.sessionDays"),
508
+ hint: translate("policy.sessionDaysHint"),
509
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
510
+ "input",
511
+ {
512
+ id: "wg-session-days",
513
+ className: "wg-input",
514
+ type: "number",
515
+ inputMode: "numeric",
516
+ min: LIMITS.sessionDays.min,
517
+ max: LIMITS.sessionDays.max,
518
+ step: 1,
519
+ value: draft.sessionDays,
520
+ disabled: policyBusy,
521
+ onChange: onDraft("sessionDays")
522
+ }
523
+ )
524
+ }
525
+ ),
526
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
527
+ Field,
528
+ {
529
+ id: "wg-max-attempts",
530
+ label: translate("policy.maxAttempts"),
531
+ hint: translate("policy.maxAttemptsHint"),
532
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
533
+ "input",
534
+ {
535
+ id: "wg-max-attempts",
536
+ className: "wg-input",
537
+ type: "number",
538
+ inputMode: "numeric",
539
+ min: LIMITS.maxAttempts.min,
540
+ max: LIMITS.maxAttempts.max,
541
+ step: 1,
542
+ value: draft.maxAttempts,
543
+ disabled: policyBusy,
544
+ onChange: onDraft("maxAttempts")
545
+ }
546
+ )
547
+ }
548
+ ),
549
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
550
+ Field,
551
+ {
552
+ id: "wg-lock-minutes",
553
+ label: translate("policy.lockMinutes"),
554
+ hint: translate("policy.lockMinutesHint"),
555
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
556
+ "input",
557
+ {
558
+ id: "wg-lock-minutes",
559
+ className: "wg-input",
560
+ type: "number",
561
+ inputMode: "numeric",
562
+ min: LIMITS.lockMinutes.min,
563
+ max: LIMITS.lockMinutes.max,
564
+ step: 1,
565
+ value: draft.lockMinutes,
566
+ disabled: policyBusy,
567
+ onChange: onDraft("lockMinutes")
568
+ }
569
+ )
570
+ }
571
+ )
572
+ ] }),
573
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NoticeLine, { notice: policyNotice }),
574
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "wg-actions", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", className: "wg-btn wg-btnPrimary", disabled: policyBusy, children: policyBusy ? translate("common.saving") : translate("policy.save") }) })
575
+ ]
576
+ }
577
+ )
578
+ ] }),
579
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "wg-card", "aria-labelledby": "wg-password-title", children: [
580
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "wg-cardTitle", id: "wg-password-title", children: translate("password.section") }),
581
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "wg-cardHint", children: translate("password.description") }),
582
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
583
+ "form",
584
+ {
585
+ onSubmit: (event) => {
586
+ void savePassword(event);
587
+ },
588
+ children: [
589
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "wg-grid", children: [
590
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { id: "wg-password-current", label: translate("password.current"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
591
+ "input",
592
+ {
593
+ id: "wg-password-current",
594
+ className: "wg-input",
595
+ type: "password",
596
+ autoComplete: "current-password",
597
+ value: currentPassword,
598
+ disabled: passwordBusy,
599
+ onChange: (event) => {
600
+ setCurrentPassword(event.target.value);
601
+ }
602
+ }
603
+ ) }),
604
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { id: "wg-password-next", label: translate("password.next"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
605
+ "input",
606
+ {
607
+ id: "wg-password-next",
608
+ className: "wg-input",
609
+ type: "password",
610
+ autoComplete: "new-password",
611
+ value: nextPassword,
612
+ disabled: passwordBusy,
613
+ onChange: (event) => {
614
+ setNextPassword(event.target.value);
615
+ }
616
+ }
617
+ ) }),
618
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { id: "wg-password-confirm", label: translate("password.confirm"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
619
+ "input",
620
+ {
621
+ id: "wg-password-confirm",
622
+ className: "wg-input",
623
+ type: "password",
624
+ autoComplete: "new-password",
625
+ value: confirmPassword,
626
+ disabled: passwordBusy,
627
+ onChange: (event) => {
628
+ setConfirmPassword(event.target.value);
629
+ }
630
+ }
631
+ ) })
632
+ ] }),
633
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NoticeLine, { notice: passwordNotice }),
634
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "wg-actions", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", className: "wg-btn wg-btnPrimary", disabled: passwordBusy, children: passwordBusy ? translate("common.saving") : translate("password.submit") }) })
635
+ ]
636
+ }
637
+ )
638
+ ] }),
639
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "wg-card", "aria-labelledby": "wg-listener-title", children: [
640
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("h3", { className: "wg-cardTitle", id: "wg-listener-title", children: [
641
+ translate("listener.section"),
642
+ listenerRestart ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "wg-badge", children: translate("listener.restartBadge") }) : null
643
+ ] }),
644
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "wg-cardHint", children: translate("listener.description") }),
645
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
646
+ "form",
647
+ {
648
+ onSubmit: (event) => {
649
+ void saveListener(event);
650
+ },
651
+ children: [
652
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "wg-grid wg-gridTwo", children: [
653
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { id: "wg-listen-host", label: translate("listener.host"), hint: translate("listener.hostHint"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
654
+ "input",
655
+ {
656
+ id: "wg-listen-host",
657
+ className: "wg-input",
658
+ type: "text",
659
+ maxLength: 64,
660
+ spellCheck: false,
661
+ autoComplete: "off",
662
+ value: draft.listenHost,
663
+ disabled: listenerBusy,
664
+ onChange: onDraft("listenHost")
665
+ }
666
+ ) }),
667
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { id: "wg-listen-port", label: translate("listener.port"), hint: translate("listener.portHint"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
668
+ "input",
669
+ {
670
+ id: "wg-listen-port",
671
+ className: "wg-input",
672
+ type: "number",
673
+ inputMode: "numeric",
674
+ min: LIMITS.listenPort.min,
675
+ max: LIMITS.listenPort.max,
676
+ step: 1,
677
+ value: draft.listenPort,
678
+ disabled: listenerBusy,
679
+ onChange: onDraft("listenPort")
680
+ }
681
+ ) })
682
+ ] }),
683
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NoticeLine, { notice: listenerNotice }),
684
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "wg-actions", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", className: "wg-btn wg-btnPrimary", disabled: listenerBusy, children: listenerBusy ? translate("common.saving") : translate("listener.save") }) })
685
+ ]
686
+ }
687
+ )
688
+ ] }),
689
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "wg-card", "aria-labelledby": "wg-sessions-title", children: [
690
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "wg-sectionHead", children: [
691
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
692
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "wg-cardTitle", id: "wg-sessions-title", children: translate("sessions.section") }),
693
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "wg-cardHint", children: translate("sessions.description") })
694
+ ] }),
695
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "wg-btn", disabled: loading, onClick: refreshSessions, children: translate("sessions.refresh") })
696
+ ] }),
697
+ sessions.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "wg-empty", children: translate("sessions.empty") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "wg-tableWrap", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", { className: "wg-table", "aria-label": translate("sessions.section"), children: [
698
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", { children: [
699
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { scope: "col", children: translate("sessions.ip") }),
700
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { scope: "col", children: translate("sessions.ua") }),
701
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { scope: "col", children: translate("sessions.lastSeen") }),
702
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { scope: "col", "aria-label": translate("sessions.kick") })
703
+ ] }) }),
704
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: sessions.map((session) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", { children: [
705
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("td", { className: "wg-cellIp", children: [
706
+ typeof session.ip === "string" && session.ip !== "" ? session.ip : "\u2014",
707
+ session.current === true ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "wg-badge", children: translate("sessions.current") }) : null
708
+ ] }),
709
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { className: "wg-cellUa", title: userAgentText(session.ua), children: userAgentText(session.ua) }),
710
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("td", { className: "wg-cellTime", children: [
711
+ formatDateTime(session.lastSeen),
712
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "wg-cellTimeDim", children: translate("sessions.signedIn", { time: formatDateTime(session.createdAt) }) })
713
+ ] }),
714
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { className: "wg-cellAction", children: session.current ? (
715
+ // The current device cannot be "kicked" through the API;
716
+ // its own action is the ordinary per-device sign-out.
717
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { className: "wg-btn", href: "/logout", children: translate("sessions.signOut") })
718
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
719
+ "button",
720
+ {
721
+ type: "button",
722
+ className: "wg-btn wg-btnDanger",
723
+ disabled: kicking !== "",
724
+ onClick: () => {
725
+ void kickSession(session.id);
726
+ },
727
+ children: kicking === session.id ? translate("sessions.kicking") : translate("sessions.kick")
728
+ }
729
+ ) })
730
+ ] }, session.id)) })
731
+ ] }) }),
732
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NoticeLine, { notice: sessionsNotice })
733
+ ] }),
734
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "wg-card", "aria-labelledby": "wg-signout-title", children: [
735
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "wg-cardTitle", id: "wg-signout-title", children: translate("signout.section") }),
736
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "wg-cardHint", children: translate("signout.description") }),
737
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NoticeLine, { notice: signOutNotice }),
738
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "wg-actions", children: [
739
+ signedOut ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { className: "wg-btn wg-btnPrimary", href: "/login", children: translate("signout.goLogin") }) : (
740
+ // The heavier action leads (it also drops every other device);
741
+ // the ordinary per-device sign-out follows as the calmer option.
742
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
743
+ "button",
744
+ {
745
+ type: "button",
746
+ className: "wg-btn wg-btnDanger",
747
+ disabled: signOutBusy,
748
+ onClick: () => {
749
+ void signOutEverywhere();
750
+ },
751
+ children: signOutBusy ? translate("common.saving") : translate("signout.action")
752
+ }
753
+ )
754
+ ),
755
+ signedOut ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { className: "wg-btn", href: "/logout", children: translate("signout.self") })
756
+ ] })
757
+ ] })
758
+ ] })
759
+ ] });
760
+ }
761
+
762
+ // src/client/locales.ts
763
+ var zh = {
764
+ "settings.navLabel": "DSH Web Guard",
765
+ "settings.title": "\u5B89\u5168",
766
+ "settings.lead": "\u767B\u5F55\u5BC6\u7801\u4E0E\u8BBF\u95EE\u63A7\u5236\u7531 dsh-web-guard \u7BA1\u7406\u3002",
767
+ "common.loading": "\u52A0\u8F7D\u4E2D\u2026",
768
+ "common.retry": "\u91CD\u8BD5",
769
+ "common.saving": "\u4FDD\u5B58\u4E2D\u2026",
770
+ "policy.section": "\u5B89\u5168\u7B56\u7565",
771
+ "policy.description": "\u63A7\u5236\u767B\u5F55\u4F1A\u8BDD\u7684\u6709\u6548\u671F\uFF0C\u4EE5\u53CA\u8FDE\u7EED\u767B\u5F55\u5931\u8D25\u540E\u7684\u9501\u5B9A\u7B56\u7565\u3002",
772
+ "policy.sessionDays": "\u4F1A\u8BDD\u6709\u6548\u671F\uFF08\u5929\uFF09",
773
+ "policy.sessionDaysHint": "0 \u8868\u793A\u4EC5\u5728\u6D4F\u89C8\u5668\u4F1A\u8BDD\u5185\u6709\u6548\uFF1B\u8303\u56F4 0\u2013365\u3002",
774
+ "policy.maxAttempts": "\u6700\u5927\u5931\u8D25\u5C1D\u8BD5\u6B21\u6570",
775
+ "policy.maxAttemptsHint": "\u8FDE\u7EED\u5931\u8D25\u8FBE\u5230\u8BE5\u6B21\u6570\u540E\u9501\u5B9A\u6765\u6E90\u5730\u5740\uFF1B\u8303\u56F4 1\u201350\u3002",
776
+ "policy.lockMinutes": "\u9501\u5B9A\u65F6\u957F\uFF08\u5206\u949F\uFF09",
777
+ "policy.lockMinutesHint": "\u8303\u56F4 1\u20131440\u3002",
778
+ "policy.save": "\u4FDD\u5B58\u7B56\u7565",
779
+ "policy.saved": "\u5B89\u5168\u7B56\u7565\u5DF2\u4FDD\u5B58\u3002",
780
+ "password.section": "\u4FEE\u6539\u5BC6\u7801",
781
+ "password.description": "\u65B0\u5BC6\u7801\u81F3\u5C11 6 \u4E2A\u5B57\u7B26\u3002\u4FEE\u6539\u540E\uFF0C\u65B0\u7684\u767B\u5F55\u9700\u8981\u4F7F\u7528\u65B0\u5BC6\u7801\u3002",
782
+ "password.current": "\u5F53\u524D\u5BC6\u7801",
783
+ "password.next": "\u65B0\u5BC6\u7801",
784
+ "password.confirm": "\u786E\u8BA4\u65B0\u5BC6\u7801",
785
+ "password.submit": "\u66F4\u65B0\u5BC6\u7801",
786
+ "password.saved": "\u5BC6\u7801\u5DF2\u66F4\u65B0\u3002",
787
+ "password.requiredCurrent": "\u8BF7\u8F93\u5165\u5F53\u524D\u5BC6\u7801\u3002",
788
+ "password.requiredNext": "\u8BF7\u8F93\u5165\u65B0\u5BC6\u7801\u3002",
789
+ "listener.section": "\u76D1\u542C\u5730\u5740",
790
+ "listener.description": "DSH \u7F51\u5173\u76D1\u542C\u7684\u5730\u5740\u4E0E\u7AEF\u53E3\u3002\u4FEE\u6539\u540E\u9700\u8981\u91CD\u542F DSH \u624D\u80FD\u751F\u6548\u3002",
791
+ "listener.host": "\u76D1\u542C\u5730\u5740",
792
+ "listener.hostHint": "\u4F8B\u5982 0.0.0.0 \u6216 127.0.0.1\uFF1B\u6700\u591A 64 \u4E2A\u5B57\u7B26\u3002",
793
+ "listener.port": "\u7AEF\u53E3",
794
+ "listener.portHint": "\u8303\u56F4 1\u201365535\u3002",
795
+ "listener.save": "\u4FDD\u5B58\u76D1\u542C\u8BBE\u7F6E",
796
+ "listener.saved": "\u76D1\u542C\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\u3002",
797
+ "listener.restartRequired": "\u76D1\u542C\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u91CD\u542F DSH \u540E\u751F\u6548\u3002",
798
+ "listener.restartBadge": "\u91CD\u542F\u751F\u6548",
799
+ "sessions.section": "\u6D3B\u8DC3\u4F1A\u8BDD",
800
+ "sessions.description": "\u5F53\u524D\u4ECD\u6709\u6548\u7684\u767B\u5F55\u4F1A\u8BDD\u3002\u8E22\u51FA\u540E\u8BE5\u8BBE\u5907\u9700\u8981\u91CD\u65B0\u767B\u5F55\u3002",
801
+ "sessions.empty": "\u6682\u65E0\u6D3B\u8DC3\u4F1A\u8BDD\u3002",
802
+ "sessions.ip": "IP",
803
+ "sessions.ua": "\u6D4F\u89C8\u5668",
804
+ "sessions.lastSeen": "\u6700\u540E\u6D3B\u52A8",
805
+ "sessions.signedIn": "\u767B\u5F55\u4E8E {time}",
806
+ "sessions.current": "\u672C\u673A",
807
+ "sessions.kick": "\u8E22\u51FA",
808
+ "sessions.kicking": "\u8E22\u51FA\u4E2D\u2026",
809
+ "sessions.kicked": "\u5DF2\u8E22\u51FA\u8BE5\u4F1A\u8BDD\u3002",
810
+ "sessions.refresh": "\u5237\u65B0",
811
+ "sessions.signOut": "\u9000\u51FA\u767B\u5F55",
812
+ "signout.section": "\u9000\u51FA\u6240\u6709\u8BBE\u5907",
813
+ "signout.description": "\u7ACB\u5373\u4EE4\u6240\u6709\u8BBE\u5907\u4E0A\u7684\u767B\u5F55\u4F1A\u8BDD\u5931\u6548\uFF0C\u5305\u62EC\u5F53\u524D\u8BBE\u5907\u3002",
814
+ "signout.action": "\u9000\u51FA\u6240\u6709\u8BBE\u5907",
815
+ "signout.confirm": "\u786E\u5B9A\u8981\u9000\u51FA\u6240\u6709\u8BBE\u5907\u5417\uFF1F\u5F53\u524D\u9875\u9762\u4E5F\u9700\u8981\u91CD\u65B0\u767B\u5F55\u3002",
816
+ "signout.done": "\u5DF2\u9000\u51FA\u6240\u6709\u8BBE\u5907\u3002",
817
+ "signout.goLogin": "\u524D\u5F80\u767B\u5F55\u9875",
818
+ "signout.self": "\u9000\u51FA\u767B\u5F55\uFF08\u672C\u8BBE\u5907\uFF09",
819
+ "field.sessionDays": "\u4F1A\u8BDD\u6709\u6548\u671F",
820
+ "field.maxAttempts": "\u6700\u5927\u5931\u8D25\u5C1D\u8BD5\u6B21\u6570",
821
+ "field.lockMinutes": "\u9501\u5B9A\u65F6\u957F",
822
+ "field.listenHost": "\u76D1\u542C\u5730\u5740",
823
+ "field.listenPort": "\u76D1\u542C\u7AEF\u53E3",
824
+ "error.unauthorized": "\u767B\u5F55\u72B6\u6001\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u3002",
825
+ "error.forbidden": "\u8BF7\u6C42\u88AB\u62D2\u7EDD\uFF08\u8DE8\u7AD9\u8BF7\u6C42\u6216\u6743\u9650\u4E0D\u8DB3\uFF09\u3002",
826
+ "error.invalid_body": "\u8BF7\u6C42\u5185\u5BB9\u65E0\u6548\u3002",
827
+ "error.invalid_field": "\u201C{field}\u201D\u7684\u503C\u65E0\u6548\uFF0C\u8BF7\u68C0\u67E5\u5141\u8BB8\u8303\u56F4\u3002",
828
+ "error.wrong_current": "\u5F53\u524D\u5BC6\u7801\u4E0D\u6B63\u786E\u3002",
829
+ "error.too_short": "\u65B0\u5BC6\u7801\u81F3\u5C11\u9700\u8981 6 \u4E2A\u5B57\u7B26\u3002",
830
+ "error.mismatch": "\u4E24\u6B21\u8F93\u5165\u7684\u65B0\u5BC6\u7801\u4E0D\u4E00\u81F4\u3002",
831
+ "error.not_found": "\u63A5\u53E3\u4E0D\u5B58\u5728\uFF0C\u8BF7\u786E\u8BA4\u670D\u52A1\u7AEF\u7248\u672C\u3002",
832
+ "error.method_not_allowed": "\u8BE5\u63A5\u53E3\u4E0D\u652F\u6301\u8FD9\u79CD\u8BF7\u6C42\u65B9\u5F0F\u3002",
833
+ "error.kick_missing": "\u8BE5\u4F1A\u8BDD\u5DF2\u4E0D\u5B58\u5728\uFF0C\u5217\u8868\u53EF\u80FD\u5DF2\u8FC7\u671F\u3002",
834
+ "error.restart_required": "\u9700\u8981\u91CD\u542F DSH \u624D\u80FD\u5E94\u7528\u8BE5\u6539\u52A8\u3002",
835
+ "error.network": "\u65E0\u6CD5\u8FDE\u63A5\u5230\u670D\u52A1\u5668\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8BD5\u3002",
836
+ "error.unknown": "\u670D\u52A1\u5668\u8FD4\u56DE\u4E86\u65E0\u6CD5\u8BC6\u522B\u7684\u54CD\u5E94\u3002",
837
+ "error.generic": "\u64CD\u4F5C\u5931\u8D25\uFF1A{error}"
838
+ };
839
+ var en = {
840
+ "settings.navLabel": "DSH Web Guard",
841
+ "settings.title": "Security",
842
+ "settings.lead": "Sign-in password and access control are managed by dsh-web-guard.",
843
+ "common.loading": "Loading\u2026",
844
+ "common.retry": "Retry",
845
+ "common.saving": "Saving\u2026",
846
+ "policy.section": "Security policy",
847
+ "policy.description": "Control how long signed-in sessions last and how repeated failures lock the source address.",
848
+ "policy.sessionDays": "Session lifetime (days)",
849
+ "policy.sessionDaysHint": "0 keeps the session for the browser session only; range 0\u2013365.",
850
+ "policy.maxAttempts": "Maximum failed attempts",
851
+ "policy.maxAttemptsHint": "The source address is locked after this many consecutive failures; range 1\u201350.",
852
+ "policy.lockMinutes": "Lockout duration (minutes)",
853
+ "policy.lockMinutesHint": "Range 1\u20131440.",
854
+ "policy.save": "Save policy",
855
+ "policy.saved": "Security policy saved.",
856
+ "password.section": "Change password",
857
+ "password.description": "At least 6 characters. After the change, new sign-ins must use the new password.",
858
+ "password.current": "Current password",
859
+ "password.next": "New password",
860
+ "password.confirm": "Confirm new password",
861
+ "password.submit": "Update password",
862
+ "password.saved": "Password updated.",
863
+ "password.requiredCurrent": "Enter the current password.",
864
+ "password.requiredNext": "Enter the new password.",
865
+ "listener.section": "Listener",
866
+ "listener.description": "Host and port the DSH gate listens on. Changes apply only after DSH restarts.",
867
+ "listener.host": "Host",
868
+ "listener.hostHint": "For example 0.0.0.0 or 127.0.0.1; at most 64 characters.",
869
+ "listener.port": "Port",
870
+ "listener.portHint": "Range 1\u201365535.",
871
+ "listener.save": "Save listener",
872
+ "listener.saved": "Listener settings saved.",
873
+ "listener.restartRequired": "Listener settings saved; they apply after DSH restarts.",
874
+ "listener.restartBadge": "Restart required",
875
+ "sessions.section": "Active sessions",
876
+ "sessions.description": "Sessions that are still signed in. Kicking one forces that device to sign in again.",
877
+ "sessions.empty": "No active sessions.",
878
+ "sessions.ip": "IP",
879
+ "sessions.ua": "Browser",
880
+ "sessions.lastSeen": "Last active",
881
+ "sessions.signedIn": "Signed in {time}",
882
+ "sessions.current": "This device",
883
+ "sessions.kick": "Kick",
884
+ "sessions.kicking": "Kicking\u2026",
885
+ "sessions.kicked": "Session kicked.",
886
+ "sessions.refresh": "Refresh",
887
+ "sessions.signOut": "Sign out",
888
+ "signout.section": "Sign out everywhere",
889
+ "signout.description": "Immediately invalidate every signed-in session, including this device.",
890
+ "signout.action": "Sign out everywhere",
891
+ "signout.confirm": "Sign out all devices? This page will need to sign in again.",
892
+ "signout.done": "Signed out everywhere.",
893
+ "signout.goLogin": "Go to sign-in page",
894
+ "signout.self": "Sign out on this device",
895
+ "field.sessionDays": "Session lifetime",
896
+ "field.maxAttempts": "Maximum failed attempts",
897
+ "field.lockMinutes": "Lockout duration",
898
+ "field.listenHost": "Listener host",
899
+ "field.listenPort": "Listener port",
900
+ "error.unauthorized": "Your session has expired. Sign in again.",
901
+ "error.forbidden": "Request rejected (cross-site request or insufficient permission).",
902
+ "error.invalid_body": "The request body is invalid.",
903
+ "error.invalid_field": '"{field}" is invalid; check the allowed range.',
904
+ "error.wrong_current": "The current password is incorrect.",
905
+ "error.too_short": "The new password must be at least 6 characters.",
906
+ "error.mismatch": "The new passwords do not match.",
907
+ "error.not_found": "This endpoint does not exist; check the server version.",
908
+ "error.method_not_allowed": "This endpoint does not support that request method.",
909
+ "error.kick_missing": "That session no longer exists; the list may be stale.",
910
+ "error.restart_required": "Restart DSH to apply this change.",
911
+ "error.network": "Could not reach the server. Check the connection and retry.",
912
+ "error.unknown": "The server returned an unrecognized response.",
913
+ "error.generic": "Request failed: {error}"
914
+ };
915
+
916
+ // src/client/styles.ts
917
+ var GUARD_CSS = `
918
+ .wg-root{display:flex;flex-direction:column;gap:16px;padding:8px 0 24px;color:var(--dsw-alias-label-primary,#0f1115);font-size:14px;line-height:22px}
919
+ .wg-head{display:flex;flex-direction:column;gap:4px}
920
+ .wg-title{margin:0;font-size:16px;font-weight:500;line-height:24px}
921
+ .wg-lead{margin:0;color:var(--dsw-alias-label-secondary,#61666b);font-size:13px;line-height:20px}
922
+ .wg-card{box-sizing:border-box;border:.5px solid var(--dsw-alias-settings-card-stroke,rgba(15,17,21,.12));border-radius:12px;background:var(--dsw-alias-settings-card-fill,transparent);padding:16px}
923
+ .wg-cardTitle{margin:0 0 4px;font-size:14px;font-weight:500;line-height:22px}
924
+ .wg-cardHint{margin:0 0 12px;color:var(--dsw-alias-label-tertiary,#8b929c);font-size:12px;line-height:18px}
925
+ .wg-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}
926
+ .wg-gridTwo{grid-template-columns:repeat(2,minmax(0,1fr))}
927
+ .wg-field{display:flex;flex-direction:column;gap:6px;min-width:0;box-sizing:border-box}
928
+ .wg-label{color:var(--dsw-alias-label-secondary,#61666b);font-size:12px;line-height:18px}
929
+ .wg-hint{color:var(--dsw-alias-label-tertiary,#8b929c);font-size:11px;line-height:16px}
930
+ .wg-input{box-sizing:border-box;width:100%;height:34px;padding:0 10px;border-radius:8px;border:.5px solid var(--dsw-alias-border-l2,rgba(15,17,21,.18));background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#0f1115);font:inherit;font-size:14px;line-height:32px}
931
+ .wg-input::placeholder{color:var(--dsw-alias-label-quaternary,#a8adb3)}
932
+ .wg-input:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4176e6);outline-offset:1px}
933
+ .wg-sectionHead{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}
934
+ .wg-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:8px;margin-top:12px}
935
+ .wg-btn{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;min-height:32px;padding:0 14px;border-radius:8px;border:.5px solid var(--dsw-alias-border-l3,rgba(15,17,21,.22));background:transparent;color:var(--dsw-alias-label-primary,#0f1115);font:inherit;font-size:13px;line-height:20px;cursor:pointer;text-decoration:none;white-space:nowrap}
936
+ .wg-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(15,17,21,.06))}
937
+ .wg-btn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4176e6);outline-offset:1px}
938
+ .wg-btn:disabled{opacity:.55;cursor:default}
939
+ .wg-btnPrimary{border-color:transparent;background:var(--dsw-alias-button-primary-fill,#4176e6);color:var(--dsw-alias-label-primary-foreground,#fff)}
940
+ .wg-btnPrimary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,#2f63d4)}
941
+ .wg-btnDanger{border-color:transparent;background:transparent;color:var(--dsw-alias-state-error-primary,#c2402f)}
942
+ .wg-btnDanger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger,rgba(194,64,47,.08))}
943
+ .wg-notice{margin:10px 0 0;font-size:12px;line-height:18px}
944
+ .wg-noticeOk{color:var(--dsw-alias-state-success-primary,#1f7a46)}
945
+ .wg-noticeError{color:var(--dsw-alias-state-error-primary,#c2402f)}
946
+ .wg-badge{display:inline-block;margin-left:6px;padding:0 6px;border:.5px solid var(--dsw-alias-border-l3,rgba(15,17,21,.22));border-radius:999px;color:var(--dsw-alias-label-tertiary,#8b929c);font-size:11px;line-height:16px;white-space:nowrap}
947
+ .wg-tableWrap{overflow-x:auto;border:.5px solid var(--dsw-alias-border-l2,rgba(15,17,21,.12));border-radius:10px}
948
+ .wg-table{width:100%;min-width:520px;border-collapse:collapse;font-size:13px;line-height:20px}
949
+ .wg-table th{text-align:left;padding:8px 12px;color:var(--dsw-alias-label-tertiary,#8b929c);font-weight:400;font-size:12px;border-bottom:.5px solid var(--dsw-alias-border-l2,rgba(15,17,21,.12));white-space:nowrap}
950
+ .wg-table td{padding:9px 12px;vertical-align:middle;border-bottom:.5px solid var(--dsw-alias-border-l2,rgba(15,17,21,.12))}
951
+ .wg-table tr:last-child td{border-bottom:none}
952
+ .wg-cellIp{white-space:nowrap;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}
953
+ .wg-cellUa{max-width:340px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-secondary,#61666b)}
954
+ .wg-cellTime{white-space:nowrap;color:var(--dsw-alias-label-secondary,#61666b);font-size:12px}
955
+ .wg-cellTimeDim{display:block;color:var(--dsw-alias-label-tertiary,#8b929c);font-size:11px;line-height:16px}
956
+ .wg-cellAction{text-align:right;white-space:nowrap}
957
+ .wg-empty{padding:16px 12px;color:var(--dsw-alias-label-tertiary,#8b929c);text-align:center}
958
+ .wg-inlineActions{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px;margin-top:12px}
959
+ .wg-link{color:var(--dsw-alias-link,#4176e6);text-decoration:none}
960
+ .wg-link:hover{text-decoration:underline}
961
+ @media (max-width:640px){
962
+ .wg-root{gap:12px;padding-bottom:16px}
963
+ .wg-grid,.wg-gridTwo{grid-template-columns:minmax(0,1fr)}
964
+ .wg-card{padding:12px}
965
+ .wg-actions{flex-direction:column;align-items:stretch}
966
+ .wg-actions .wg-btn{width:100%}
967
+ .wg-inlineActions{flex-direction:column;align-items:stretch}
968
+ }
969
+ `;
970
+ function installGuardStyles() {
971
+ if (typeof document === "undefined" || document.head === null) return () => {
972
+ };
973
+ const tag = document.createElement("style");
974
+ tag.setAttribute("data-dsh-web-guard", "");
975
+ tag.textContent = GUARD_CSS;
976
+ document.head.appendChild(tag);
977
+ return () => tag.remove();
978
+ }
979
+
980
+ // src/client/module.tsx
981
+ var import_jsx_runtime2 = require("react/jsx-runtime");
982
+ var LOCALE_NS = "webGuard";
983
+ var SECTION_ID = "web-guard";
984
+ var SECTION_ORDER = 500;
985
+ function GuardSectionIcon({ size, active }) {
986
+ const edge = typeof size === "number" && Number.isFinite(size) && size > 0 ? Math.min(Math.max(size, 12), 32) : 18;
987
+ const selected = active === true;
988
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
989
+ "svg",
990
+ {
991
+ width: edge,
992
+ height: edge,
993
+ viewBox: "0 0 24 24",
994
+ role: "presentation",
995
+ focusable: "false",
996
+ "aria-hidden": "true",
997
+ style: { display: "block", opacity: selected ? 1 : 0.78 },
998
+ children: [
999
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1000
+ "path",
1001
+ {
1002
+ d: "M12 2.8 4.6 5.9v5.3c0 4.6 3.1 8.3 7.4 9.9 4.3-1.6 7.4-5.3 7.4-9.9V5.9L12 2.8Z",
1003
+ fill: selected ? "currentColor" : "none",
1004
+ fillOpacity: selected ? 0.12 : 0,
1005
+ stroke: "currentColor",
1006
+ strokeWidth: "1.5",
1007
+ strokeLinejoin: "round"
1008
+ }
1009
+ ),
1010
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1011
+ "path",
1012
+ {
1013
+ d: "m8.9 11.7 2.2 2.2 4-4.3",
1014
+ fill: "none",
1015
+ stroke: "currentColor",
1016
+ strokeWidth: "1.5",
1017
+ strokeLinecap: "round",
1018
+ strokeLinejoin: "round"
1019
+ }
1020
+ )
1021
+ ]
1022
+ }
1023
+ );
1024
+ }
1025
+ var inject = ["slots", "locale"];
1026
+ function apply(ctx) {
1027
+ try {
1028
+ const t = ctx.locale.bind(LOCALE_NS);
1029
+ ctx.effect(() => ctx.locale.register(LOCALE_NS, { zh, en }), "web-guard: dictionaries");
1030
+ ctx.effect(
1031
+ () => ctx.slots.inject(
1032
+ "settings.section",
1033
+ () => ctx.slots.register(
1034
+ {
1035
+ name: "settings.section",
1036
+ id: SECTION_ID,
1037
+ order: SECTION_ORDER,
1038
+ label: () => t("settings.navLabel"),
1039
+ locale: LOCALE_NS,
1040
+ inject: () => ({ t })
1041
+ },
1042
+ GuardSettings
1043
+ )
1044
+ ),
1045
+ "web-guard: settings section"
1046
+ );
1047
+ ctx.effect(
1048
+ () => ctx.slots.inject(
1049
+ "settings.section.icon",
1050
+ () => ctx.slots.register(
1051
+ {
1052
+ name: "settings.section.icon",
1053
+ id: SECTION_ID,
1054
+ order: SECTION_ORDER,
1055
+ label: () => t("settings.navLabel"),
1056
+ locale: LOCALE_NS
1057
+ },
1058
+ GuardSectionIcon
1059
+ )
1060
+ ),
1061
+ "web-guard: settings icon"
1062
+ );
1063
+ ctx.effect(() => installGuardStyles(), "web-guard: styles");
1064
+ } catch (error) {
1065
+ console.warn("[dsh-web-guard] client module failed to activate:", error);
1066
+ }
1067
+ }
1068
+
1069
+ return module.exports;
1070
+ },
1071
+ });