@dsh-plus/access-gate 0.1.3 → 0.2.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 +83 -186
- package/lib/index.d.ts +1 -9
- package/lib/index.js +183 -258
- package/package.json +10 -10
- package/src/client/card.tsx +25 -123
- package/src/client/client.ts +13 -13
- package/src/client/i18n.ts +23 -37
- package/src/config.ts +9 -23
- package/src/decision.ts +42 -50
- package/src/index.ts +26 -7
- package/src/interceptor.ts +33 -11
- package/src/routes.ts +47 -109
- package/src/token-page.ts +88 -0
- package/src/login-page.ts +0 -77
package/lib/client.js
CHANGED
|
@@ -91,41 +91,6 @@ window.__ModuleLoader__.load({
|
|
|
91
91
|
}
|
|
92
92
|
//#endregion
|
|
93
93
|
//#region ../shared/src/client/fields.tsx
|
|
94
|
-
function TextField(props) {
|
|
95
|
-
const p = props.prefix ?? "dshp";
|
|
96
|
-
const invalid = props.invalid === true;
|
|
97
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
98
|
-
className: `${p}-field`,
|
|
99
|
-
children: [
|
|
100
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
101
|
-
className: `${p}-head`,
|
|
102
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
103
|
-
className: `${p}-label`,
|
|
104
|
-
htmlFor: props.id,
|
|
105
|
-
children: props.label
|
|
106
|
-
}), props.badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
107
|
-
className: `${p}-badge ${props.badge.set ? `${p}-badgeSet` : `${p}-badgeUnset`}`,
|
|
108
|
-
children: props.badge.text
|
|
109
|
-
}) : null]
|
|
110
|
-
}),
|
|
111
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
112
|
-
id: props.id,
|
|
113
|
-
className: `${p}-input${invalid ? ` ${p}-inputInvalid` : ""}`,
|
|
114
|
-
type: props.password === true ? "password" : "text",
|
|
115
|
-
inputMode: props.numeric === true ? "numeric" : void 0,
|
|
116
|
-
autoComplete: props.password === true ? "off" : void 0,
|
|
117
|
-
"aria-invalid": invalid || void 0,
|
|
118
|
-
value: props.value,
|
|
119
|
-
disabled: props.disabled === true,
|
|
120
|
-
onChange: (event) => props.onEdit(event.target.value)
|
|
121
|
-
}),
|
|
122
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
123
|
-
className: invalid ? `${p}-invalid` : `${p}-hint`,
|
|
124
|
-
children: invalid ? props.invalidLabel ?? "" : props.hint ?? ""
|
|
125
|
-
})
|
|
126
|
-
]
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
94
|
/**
|
|
130
95
|
* 勾选行:自绘圆角扁平开关(switch),对齐官方 WebUI 无原生 checkbox 的
|
|
131
96
|
* 设计语言(官方选择类控件全走胶囊/菜单,见 cardCss 的 -switch 规则)。
|
|
@@ -196,14 +161,15 @@ window.__ModuleLoader__.load({
|
|
|
196
161
|
//#endregion
|
|
197
162
|
//#region ../shared/src/client/scope.ts
|
|
198
163
|
/**
|
|
199
|
-
*
|
|
164
|
+
* remote.settings 直连实现的命名空间 scope(官方配置页 tab 同款模式)。
|
|
200
165
|
* 读:describe 取命名空间视图(脱敏 value/revision + 顶层 writable);
|
|
201
166
|
* 刷新:remote `settings/document-updated`(按命名空间过滤)与
|
|
202
167
|
* `connection/reset`;generation 防旧读覆盖新发布。任何页面 origin
|
|
203
168
|
* (loopback 或 tailnet 信任域名)下行为一致。
|
|
204
169
|
* effect 归属调用方 fiber(label 需含插件名以便诊断)。
|
|
205
170
|
*/
|
|
206
|
-
function
|
|
171
|
+
function createSettingsScope(c, ns, label) {
|
|
172
|
+
const remote = c.get("remote");
|
|
207
173
|
let state = {
|
|
208
174
|
status: "loading",
|
|
209
175
|
value: void 0,
|
|
@@ -218,36 +184,37 @@ window.__ModuleLoader__.load({
|
|
|
218
184
|
};
|
|
219
185
|
const load = async () => {
|
|
220
186
|
const gen = ++generation;
|
|
221
|
-
let
|
|
187
|
+
let view;
|
|
222
188
|
try {
|
|
223
|
-
response = await
|
|
189
|
+
const response = await remote.settings.describe();
|
|
190
|
+
if (!response.ok) return;
|
|
191
|
+
view = response.value;
|
|
224
192
|
} catch {
|
|
225
193
|
return;
|
|
226
194
|
}
|
|
227
|
-
if (gen !== generation
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
if (view === void 0) {
|
|
195
|
+
if (gen !== generation) return;
|
|
196
|
+
const namespace = view.namespaces.find((candidate) => candidate.ns === ns);
|
|
197
|
+
if (namespace === void 0) {
|
|
231
198
|
publish({
|
|
232
199
|
status: "unavailable",
|
|
233
200
|
value: void 0,
|
|
234
201
|
revision: void 0,
|
|
235
|
-
writable
|
|
202
|
+
writable: view.writable
|
|
236
203
|
});
|
|
237
204
|
return;
|
|
238
205
|
}
|
|
239
206
|
publish({
|
|
240
207
|
status: "ready",
|
|
241
|
-
value:
|
|
242
|
-
revision:
|
|
243
|
-
writable
|
|
208
|
+
value: namespace.value,
|
|
209
|
+
revision: namespace.revision,
|
|
210
|
+
writable: view.writable
|
|
244
211
|
});
|
|
245
212
|
};
|
|
246
213
|
const refresh = (namespace) => {
|
|
247
214
|
if (namespace !== void 0 && namespace !== ns) return;
|
|
248
215
|
load();
|
|
249
216
|
};
|
|
250
|
-
const disposers = [
|
|
217
|
+
const disposers = [remote.$on("settings/document-updated", refresh), c.on("connection/reset", () => {
|
|
251
218
|
load();
|
|
252
219
|
})];
|
|
253
220
|
c.effect(() => () => {
|
|
@@ -265,6 +232,20 @@ window.__ModuleLoader__.load({
|
|
|
265
232
|
load
|
|
266
233
|
};
|
|
267
234
|
}
|
|
235
|
+
/** 以 remote.settings 构造命名空间绑定的 api 面。 */
|
|
236
|
+
function createNamespaceApi(remote, ns) {
|
|
237
|
+
return {
|
|
238
|
+
update: async (patch, expectedRevision) => {
|
|
239
|
+
const response = await remote.update(ns, patch, expectedRevision);
|
|
240
|
+
if (!response.ok) throw new Error(response.error.message ?? "settings update failed");
|
|
241
|
+
},
|
|
242
|
+
describe: async () => {
|
|
243
|
+
const response = await remote.describe();
|
|
244
|
+
if (!response.ok) throw new Error(response.error.message ?? "settings describe failed");
|
|
245
|
+
return response.value;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
268
249
|
//#endregion
|
|
269
250
|
//#region ../shared/src/client/styles.ts
|
|
270
251
|
/**
|
|
@@ -379,47 +360,39 @@ ${extra}
|
|
|
379
360
|
/**
|
|
380
361
|
* 「访问控制」配置卡片:注册进 settings.plugin.item 插槽(官方插件配置页)。
|
|
381
362
|
* 外壳与基础控件走 @dsh-plus/shared/client 套件(CardChrome/TextField/CheckRow),
|
|
382
|
-
*
|
|
383
|
-
* 配置读写走官方 settingsScope 传输:value 为 schema
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
363
|
+
* 本文件保留业务字段(白名单 textarea/诊断面板)与保存逻辑。
|
|
364
|
+
* 配置读写走官方 settingsScope 传输:value 为 schema 解析后的视图,
|
|
365
|
+
* 保存经 settings.update 深合并。
|
|
366
|
+
* 卡片顶部「当前页面诊断」读 /dsh-plus/gate/status(本页放行原因/客户端 IP/
|
|
367
|
+
* 官方 cookie 状态)。
|
|
368
|
+
*
|
|
369
|
+
* 与官方认证合并后:访问凭据由官方 browser-auth 持有(30 天 cookie),
|
|
370
|
+
* 卡片不再管理令牌;allowedIps 仅为可选附加 IP 围栏。
|
|
387
371
|
* @module access-gate/client/card
|
|
388
372
|
*/
|
|
389
373
|
function draftFromValue(value) {
|
|
390
374
|
return {
|
|
391
375
|
enabled: value.enabled,
|
|
392
|
-
token: "",
|
|
393
376
|
allowedIpsText: value.allowedIps.join("\n"),
|
|
394
|
-
trustForwardedFor: value.trustForwardedFor
|
|
395
|
-
cookieMaxAgeHours: String(value.cookieMaxAgeHours),
|
|
396
|
-
loginFailLimit: String(value.loginFailLimit),
|
|
397
|
-
loginCooldownMs: String(value.loginCooldownMs)
|
|
377
|
+
trustForwardedFor: value.trustForwardedFor
|
|
398
378
|
};
|
|
399
379
|
}
|
|
400
|
-
function isPositiveInt(text) {
|
|
401
|
-
return /^[0-9]+$/.test(text) && Number(text) > 0;
|
|
402
|
-
}
|
|
403
380
|
function toPatch(draft) {
|
|
404
381
|
return {
|
|
405
382
|
enabled: draft.enabled,
|
|
406
383
|
allowedIps: draft.allowedIpsText.split("\n").map((s) => s.trim()).filter((s) => s.length > 0),
|
|
407
|
-
trustForwardedFor: draft.trustForwardedFor
|
|
408
|
-
cookieMaxAgeHours: Number(draft.cookieMaxAgeHours),
|
|
409
|
-
loginFailLimit: Number(draft.loginFailLimit),
|
|
410
|
-
loginCooldownMs: Number(draft.loginCooldownMs)
|
|
384
|
+
trustForwardedFor: draft.trustForwardedFor
|
|
411
385
|
};
|
|
412
386
|
}
|
|
413
387
|
/** reason 枚举 → 文案 key。 */
|
|
414
388
|
const REASON_KEYS = {
|
|
415
389
|
local: "diagLocal",
|
|
416
|
-
|
|
417
|
-
token: "diagToken"
|
|
390
|
+
cookie: "diagCookie"
|
|
418
391
|
};
|
|
419
392
|
function verdictLabel(t, diag) {
|
|
420
393
|
if (!diag.enabled) return t("diagOff");
|
|
421
394
|
if (diag.verdict === "pass") return t("diagPass");
|
|
422
|
-
if (diag.verdict === "
|
|
395
|
+
if (diag.verdict === "token-page") return t("diagTokenPage");
|
|
423
396
|
return t("diagBlock");
|
|
424
397
|
}
|
|
425
398
|
function AccessGateCard(props) {
|
|
@@ -428,7 +401,6 @@ ${extra}
|
|
|
428
401
|
const value = snapshot.value;
|
|
429
402
|
const [open, setOpen] = (0, react.useState)(false);
|
|
430
403
|
const [draft, setDraft] = (0, react.useState)(null);
|
|
431
|
-
const [tokenConfigured, setTokenConfigured] = (0, react.useState)(false);
|
|
432
404
|
const [diag, setDiag] = (0, react.useState)(null);
|
|
433
405
|
const [saving, setSaving] = (0, react.useState)(false);
|
|
434
406
|
const [status, setStatus] = (0, react.useState)(IDLE_STATUS);
|
|
@@ -436,17 +408,6 @@ ${extra}
|
|
|
436
408
|
if (value === void 0 || draft !== null) return;
|
|
437
409
|
setDraft(draftFromValue(value));
|
|
438
410
|
}, [value, draft]);
|
|
439
|
-
(0, react.useEffect)(() => {
|
|
440
|
-
let alive = true;
|
|
441
|
-
api.describe({}).then((response) => {
|
|
442
|
-
if (!alive || !response.result.ok) return;
|
|
443
|
-
const view = response.result.value?.namespaces.find((ns) => ns.ns === SETTINGS_NS);
|
|
444
|
-
setTokenConfigured(view?.secrets.some((secret) => secret.path.join(".") === "token" && secret.set) ?? false);
|
|
445
|
-
}).catch(() => {});
|
|
446
|
-
return () => {
|
|
447
|
-
alive = false;
|
|
448
|
-
};
|
|
449
|
-
}, [api, snapshot]);
|
|
450
411
|
(0, react.useEffect)(() => {
|
|
451
412
|
let alive = true;
|
|
452
413
|
fetch("/dsh-plus/gate/status").then((res) => res.ok ? res.json() : null).then((body) => {
|
|
@@ -456,9 +417,7 @@ ${extra}
|
|
|
456
417
|
alive = false;
|
|
457
418
|
};
|
|
458
419
|
}, [snapshot]);
|
|
459
|
-
const failClosed = (0, react.useMemo)(() => value?.enabled === true && value.allowedIps.length === 0 && !tokenConfigured, [value, tokenConfigured]);
|
|
460
420
|
const dirty = (0, react.useMemo)(() => value !== void 0 && draft !== null && JSON.stringify(toPatch(draft)) !== JSON.stringify(toPatch(draftFromValue(value))), [value, draft]);
|
|
461
|
-
const invalid = (0, react.useMemo)(() => draft !== null && (!isPositiveInt(draft.cookieMaxAgeHours) || !isPositiveInt(draft.loginFailLimit) || !isPositiveInt(draft.loginCooldownMs)), [draft]);
|
|
462
421
|
if (value === void 0 || draft === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
463
422
|
className: "dag-card",
|
|
464
423
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -476,14 +435,8 @@ ${extra}
|
|
|
476
435
|
const onSave = () => {
|
|
477
436
|
setSaving(true);
|
|
478
437
|
const patch = toPatch(draft);
|
|
479
|
-
if (draft.token !== "") patch.token = draft.token;
|
|
480
438
|
const revision = scope.getSnapshot().revision;
|
|
481
|
-
api.update({
|
|
482
|
-
ns: SETTINGS_NS,
|
|
483
|
-
patch,
|
|
484
|
-
...revision !== void 0 ? { expectedRevision: revision } : {}
|
|
485
|
-
}).then(async (response) => {
|
|
486
|
-
if (!response.result.ok) throw new Error(response.result.error?.message ?? t("saveFailed"));
|
|
439
|
+
api.update(patch, revision).then(async () => {
|
|
487
440
|
await scope.load();
|
|
488
441
|
const next = scope.getSnapshot().value;
|
|
489
442
|
if (next !== void 0) setDraft(draftFromValue(next));
|
|
@@ -523,14 +476,14 @@ ${extra}
|
|
|
523
476
|
key: "save",
|
|
524
477
|
label: t(saving ? "saving" : "save"),
|
|
525
478
|
variant: "primary",
|
|
526
|
-
disabled: !dirty ||
|
|
479
|
+
disabled: !dirty || saving || disabled,
|
|
527
480
|
onClick: onSave
|
|
528
481
|
}],
|
|
529
482
|
children: [
|
|
530
|
-
|
|
531
|
-
className: "dag-
|
|
532
|
-
children: t("
|
|
533
|
-
})
|
|
483
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
484
|
+
className: "dag-hint",
|
|
485
|
+
children: t("mergedHint")
|
|
486
|
+
}),
|
|
534
487
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
|
|
535
488
|
prefix: "dag",
|
|
536
489
|
id: "dag-enabled",
|
|
@@ -539,20 +492,6 @@ ${extra}
|
|
|
539
492
|
disabled,
|
|
540
493
|
onEdit: (v) => edit("enabled", v)
|
|
541
494
|
}),
|
|
542
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
543
|
-
prefix: "dag",
|
|
544
|
-
id: "dag-token",
|
|
545
|
-
label: t("token"),
|
|
546
|
-
hint: t("tokenHint"),
|
|
547
|
-
value: draft.token,
|
|
548
|
-
password: true,
|
|
549
|
-
disabled,
|
|
550
|
-
badge: {
|
|
551
|
-
text: t(tokenConfigured ? "tokenSet" : "tokenUnset"),
|
|
552
|
-
set: tokenConfigured
|
|
553
|
-
},
|
|
554
|
-
onEdit: (v) => edit("token", v)
|
|
555
|
-
}),
|
|
556
495
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
557
496
|
className: "dag-field",
|
|
558
497
|
children: [
|
|
@@ -586,42 +525,6 @@ ${extra}
|
|
|
586
525
|
disabled,
|
|
587
526
|
onEdit: (v) => edit("trustForwardedFor", v)
|
|
588
527
|
}),
|
|
589
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
590
|
-
prefix: "dag",
|
|
591
|
-
id: "dag-cookie",
|
|
592
|
-
label: t("cookieMaxAgeHours"),
|
|
593
|
-
hint: t("cookieMaxAgeHoursHint"),
|
|
594
|
-
value: draft.cookieMaxAgeHours,
|
|
595
|
-
numeric: true,
|
|
596
|
-
disabled,
|
|
597
|
-
invalid: !isPositiveInt(draft.cookieMaxAgeHours),
|
|
598
|
-
invalidLabel: t("invalidNumber"),
|
|
599
|
-
onEdit: (v) => edit("cookieMaxAgeHours", v)
|
|
600
|
-
}),
|
|
601
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
602
|
-
prefix: "dag",
|
|
603
|
-
id: "dag-faillimit",
|
|
604
|
-
label: t("loginFailLimit"),
|
|
605
|
-
hint: t("loginFailLimitHint"),
|
|
606
|
-
value: draft.loginFailLimit,
|
|
607
|
-
numeric: true,
|
|
608
|
-
disabled,
|
|
609
|
-
invalid: !isPositiveInt(draft.loginFailLimit),
|
|
610
|
-
invalidLabel: t("invalidNumber"),
|
|
611
|
-
onEdit: (v) => edit("loginFailLimit", v)
|
|
612
|
-
}),
|
|
613
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
614
|
-
prefix: "dag",
|
|
615
|
-
id: "dag-cooldown",
|
|
616
|
-
label: t("loginCooldownMs"),
|
|
617
|
-
hint: t("loginCooldownMsHint"),
|
|
618
|
-
value: draft.loginCooldownMs,
|
|
619
|
-
numeric: true,
|
|
620
|
-
disabled,
|
|
621
|
-
invalid: !isPositiveInt(draft.loginCooldownMs),
|
|
622
|
-
invalidLabel: t("invalidNumber"),
|
|
623
|
-
onEdit: (v) => edit("loginCooldownMs", v)
|
|
624
|
-
}),
|
|
625
528
|
diag !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
626
529
|
className: `dag-diag${diag.invalidEntries.length > 0 ? " dag-diagWarn" : ""}`,
|
|
627
530
|
children: [
|
|
@@ -659,6 +562,16 @@ ${extra}
|
|
|
659
562
|
children: diag.reason !== null ? t(REASON_KEYS[diag.reason] ?? diag.reason) : "—"
|
|
660
563
|
})]
|
|
661
564
|
}),
|
|
565
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
566
|
+
className: "dag-diagRow",
|
|
567
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
568
|
+
className: "dag-diagKey",
|
|
569
|
+
children: t("diagOfficial")
|
|
570
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
571
|
+
className: "dag-diagVal",
|
|
572
|
+
children: diag.officialAuthed ? t("diagOfficialYes") : t("diagOfficialNo")
|
|
573
|
+
})]
|
|
574
|
+
}),
|
|
662
575
|
diag.invalidEntries.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
663
576
|
className: "dag-diagRow",
|
|
664
577
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -684,69 +597,53 @@ ${extra}
|
|
|
684
597
|
const NS = "dsh-plus-access-gate";
|
|
685
598
|
const ownZh = {
|
|
686
599
|
title: "访问控制",
|
|
687
|
-
description: "Web
|
|
600
|
+
description: "Web 访问围栏:与官方登录合并——官方 cookie 为唯一凭据;未认证导航显示启动令牌输入页(PWA 内可用);可选 IP 附加围栏。",
|
|
688
601
|
enabled: "启用访问围栏",
|
|
689
|
-
enabledHint: "
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
tokenUnset: "未配置",
|
|
694
|
-
allowedIps: "放行 IP 白名单",
|
|
695
|
-
allowedIpsHint: "每行一个:精确 IP 或 CIDR(IPv4/IPv6 皆可),如 100.86.108.55 或 fd7a:115c:a1e0::/48。",
|
|
602
|
+
enabledHint: "关闭即完全旁路(等价插件缺席);启用后未认证流量被拦截到令牌输入页或拒绝。",
|
|
603
|
+
mergedHint: "登录凭据由官方认证持有(启动令牌换 30 天 cookie)。获取当前令牌:服务器执行 dshctl url,或本机访问 /dsh-plus/gate/launch-url。",
|
|
604
|
+
allowedIps: "附加 IP 围栏白名单",
|
|
605
|
+
allowedIpsHint: "每行一个:精确 IP 或 CIDR(IPv4/IPv6 皆可)。非空时仅白名单来源可通行与登录;留空 = 不限制来源 IP(仅官方登录保护)。",
|
|
696
606
|
trustForwardedFor: "信任 x-forwarded-for 还原客户端 IP",
|
|
697
607
|
trustForwardedForHint: "仅当入口代理强制覆盖该头时开启(tailscale serve 满足);直连暴露场景必须关闭。",
|
|
698
|
-
cookieMaxAgeHours: "登录有效期(小时)",
|
|
699
|
-
cookieMaxAgeHoursHint: "令牌登录后 cookie 的有效期。",
|
|
700
|
-
loginFailLimit: "登录失败阈值",
|
|
701
|
-
loginFailLimitHint: "每个 IP 在冷却窗口内允许的失败次数,超出进入冷却。",
|
|
702
|
-
loginCooldownMs: "登录冷却(毫秒)",
|
|
703
|
-
loginCooldownMsHint: "失败达到阈值后的冷却时长。",
|
|
704
608
|
diagTitle: "当前页面诊断",
|
|
705
609
|
diagVerdict: "本页判定",
|
|
706
610
|
diagClientIp: "客户端 IP",
|
|
707
611
|
diagReason: "放行原因",
|
|
708
612
|
diagLocal: "本机直连",
|
|
709
|
-
|
|
710
|
-
|
|
613
|
+
diagCookie: "官方 cookie",
|
|
614
|
+
diagOfficial: "官方登录状态",
|
|
615
|
+
diagOfficialYes: "已登录",
|
|
616
|
+
diagOfficialNo: "未登录",
|
|
711
617
|
diagPass: "放行",
|
|
712
618
|
diagBlock: "拦截",
|
|
713
|
-
|
|
619
|
+
diagTokenPage: "令牌输入页",
|
|
714
620
|
diagOff: "围栏未启用",
|
|
715
|
-
diagInvalid: "白名单存在无法解析的条目:"
|
|
716
|
-
warnFailClosed: "警告:已启用但令牌与白名单均为空——所有远程访问将被拒绝(含本页之外的设备)。"
|
|
621
|
+
diagInvalid: "白名单存在无法解析的条目:"
|
|
717
622
|
};
|
|
718
623
|
const ownEn = {
|
|
719
624
|
title: "Access Control",
|
|
720
|
-
description: "Web access gate:
|
|
625
|
+
description: "Web access gate merged with official auth: the official cookie is the sole credential; unauthenticated navigations get a launch-token page (works inside the PWA); optional IP fence.",
|
|
721
626
|
enabled: "Enable access gate",
|
|
722
|
-
enabledHint: "Off means full bypass (as if absent); on,
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
tokenUnset: "Not set",
|
|
727
|
-
allowedIps: "Allowed IP allowlist",
|
|
728
|
-
allowedIpsHint: "One per line: exact IP or CIDR (IPv4/IPv6), e.g. 100.86.108.55 or fd7a:115c:a1e0::/48.",
|
|
627
|
+
enabledHint: "Off means full bypass (as if absent); on, unauthenticated traffic hits the token page or is refused.",
|
|
628
|
+
mergedHint: "Credentials are owned by official auth (launch token exchange, 30-day cookie). Get the current token: run dshctl url on the server, or visit /dsh-plus/gate/launch-url locally.",
|
|
629
|
+
allowedIps: "Additional IP fence allowlist",
|
|
630
|
+
allowedIpsHint: "One per line: exact IP or CIDR (IPv4/IPv6). When non-empty only listed sources may pass or log in; empty = no IP restriction (official login only).",
|
|
729
631
|
trustForwardedFor: "Trust x-forwarded-for for client IP",
|
|
730
632
|
trustForwardedForHint: "Enable only when the entry proxy overwrites that header (tailscale serve does); must be off for direct exposure.",
|
|
731
|
-
cookieMaxAgeHours: "Login validity (hours)",
|
|
732
|
-
cookieMaxAgeHoursHint: "Cookie lifetime after a token login.",
|
|
733
|
-
loginFailLimit: "Login failure limit",
|
|
734
|
-
loginFailLimitHint: "Failures per IP within the cooldown window before throttling.",
|
|
735
|
-
loginCooldownMs: "Login cooldown (ms)",
|
|
736
|
-
loginCooldownMsHint: "Cooldown duration after reaching the limit.",
|
|
737
633
|
diagTitle: "This page",
|
|
738
634
|
diagVerdict: "Verdict",
|
|
739
635
|
diagClientIp: "Client IP",
|
|
740
636
|
diagReason: "Reason",
|
|
741
637
|
diagLocal: "loopback",
|
|
742
|
-
|
|
743
|
-
|
|
638
|
+
diagCookie: "official cookie",
|
|
639
|
+
diagOfficial: "Official login",
|
|
640
|
+
diagOfficialYes: "authenticated",
|
|
641
|
+
diagOfficialNo: "not authenticated",
|
|
744
642
|
diagPass: "pass",
|
|
745
643
|
diagBlock: "blocked",
|
|
746
|
-
|
|
644
|
+
diagTokenPage: "token page",
|
|
747
645
|
diagOff: "gate disabled",
|
|
748
|
-
diagInvalid: "Allowlist entries that failed to parse:"
|
|
749
|
-
warnFailClosed: "Warning: enabled with no token and an empty allowlist — all remote access will be refused."
|
|
646
|
+
diagInvalid: "Allowlist entries that failed to parse:"
|
|
750
647
|
};
|
|
751
648
|
const zh = mergeDict(commonZh, ownZh);
|
|
752
649
|
const en = mergeDict(commonEn, ownEn);
|
|
@@ -787,8 +684,8 @@ ${extra}
|
|
|
787
684
|
const inject = [
|
|
788
685
|
"slots",
|
|
789
686
|
"locale",
|
|
790
|
-
"
|
|
791
|
-
"remote"
|
|
687
|
+
"remote",
|
|
688
|
+
"remote.settings"
|
|
792
689
|
];
|
|
793
690
|
function apply(ctx) {
|
|
794
691
|
const c = ctx;
|
|
@@ -800,8 +697,8 @@ ${extra}
|
|
|
800
697
|
zh,
|
|
801
698
|
en
|
|
802
699
|
}), "access-gate: locale");
|
|
803
|
-
const
|
|
804
|
-
const
|
|
700
|
+
const scope = createSettingsScope(c, SETTINGS_NS, "access-gate: settings scope");
|
|
701
|
+
const api = createNamespaceApi(c.get("remote").settings, SETTINGS_NS);
|
|
805
702
|
c.slots.inject("settings.plugin.item", () => c.slots.register({
|
|
806
703
|
name: "settings.plugin.item",
|
|
807
704
|
key: SETTINGS_NS,
|
package/lib/index.d.ts
CHANGED
|
@@ -3,26 +3,18 @@ import z from "@deepseek-ai/schemastery";
|
|
|
3
3
|
//#region src/config.d.ts
|
|
4
4
|
declare const Config: z<Schemastery.ObjectS<{
|
|
5
5
|
enabled: z<boolean, boolean>;
|
|
6
|
-
token: z<string, string>;
|
|
7
6
|
allowedIps: z<string[], string[]>;
|
|
8
7
|
trustForwardedFor: z<boolean, boolean>;
|
|
9
|
-
cookieMaxAgeHours: z<number, number>;
|
|
10
|
-
loginFailLimit: z<number, number>;
|
|
11
|
-
loginCooldownMs: z<number, number>;
|
|
12
8
|
}>, Schemastery.ObjectT<{
|
|
13
9
|
enabled: z<boolean, boolean>;
|
|
14
|
-
token: z<string, string>;
|
|
15
10
|
allowedIps: z<string[], string[]>;
|
|
16
11
|
trustForwardedFor: z<boolean, boolean>;
|
|
17
|
-
cookieMaxAgeHours: z<number, number>;
|
|
18
|
-
loginFailLimit: z<number, number>;
|
|
19
|
-
loginCooldownMs: z<number, number>;
|
|
20
12
|
}>>;
|
|
21
13
|
type AccessGateConfig = Schemastery.TypeT<typeof Config>;
|
|
22
14
|
//#endregion
|
|
23
15
|
//#region src/index.d.ts
|
|
24
16
|
declare const name = "dsh-plus-access-gate";
|
|
25
|
-
declare const inject: readonly ["webServer"];
|
|
17
|
+
declare const inject: readonly ["webServer", "connection"];
|
|
26
18
|
declare module '@deepseek-ai/cordis' {
|
|
27
19
|
interface Context {
|
|
28
20
|
accessGate: AccessGateService;
|