@dsh-plus/access-gate 0.1.4 → 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 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 规则)。
@@ -395,47 +360,39 @@ ${extra}
395
360
  /**
396
361
  * 「访问控制」配置卡片:注册进 settings.plugin.item 插槽(官方插件配置页)。
397
362
  * 外壳与基础控件走 @dsh-plus/shared/client 套件(CardChrome/TextField/CheckRow),
398
- * 本文件保留业务字段(token/白名单 textarea/诊断面板)与保存逻辑。
399
- * 配置读写走官方 settingsScope 传输:value 为 schema 解析后的脱敏视图
400
- * (token 不出现,tokenConfigured 经 describe 的 secrets 探测),
401
- * 保存经 settings.update 深合并(空 token 剔除 = 保持不变)。
402
- * 卡片顶部「当前页面诊断」读 /dsh-plus/gate/status(本页放行原因/客户端 IP)。
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 围栏。
403
371
  * @module access-gate/client/card
404
372
  */
405
373
  function draftFromValue(value) {
406
374
  return {
407
375
  enabled: value.enabled,
408
- token: "",
409
376
  allowedIpsText: value.allowedIps.join("\n"),
410
- trustForwardedFor: value.trustForwardedFor,
411
- cookieMaxAgeHours: String(value.cookieMaxAgeHours),
412
- loginFailLimit: String(value.loginFailLimit),
413
- loginCooldownMs: String(value.loginCooldownMs)
377
+ trustForwardedFor: value.trustForwardedFor
414
378
  };
415
379
  }
416
- function isPositiveInt(text) {
417
- return /^[0-9]+$/.test(text) && Number(text) > 0;
418
- }
419
380
  function toPatch(draft) {
420
381
  return {
421
382
  enabled: draft.enabled,
422
383
  allowedIps: draft.allowedIpsText.split("\n").map((s) => s.trim()).filter((s) => s.length > 0),
423
- trustForwardedFor: draft.trustForwardedFor,
424
- cookieMaxAgeHours: Number(draft.cookieMaxAgeHours),
425
- loginFailLimit: Number(draft.loginFailLimit),
426
- loginCooldownMs: Number(draft.loginCooldownMs)
384
+ trustForwardedFor: draft.trustForwardedFor
427
385
  };
428
386
  }
429
387
  /** reason 枚举 → 文案 key。 */
430
388
  const REASON_KEYS = {
431
389
  local: "diagLocal",
432
- "allowed-ip": "diagAllowedIp",
433
- token: "diagToken"
390
+ cookie: "diagCookie"
434
391
  };
435
392
  function verdictLabel(t, diag) {
436
393
  if (!diag.enabled) return t("diagOff");
437
394
  if (diag.verdict === "pass") return t("diagPass");
438
- if (diag.verdict === "login") return t("diagLogin");
395
+ if (diag.verdict === "token-page") return t("diagTokenPage");
439
396
  return t("diagBlock");
440
397
  }
441
398
  function AccessGateCard(props) {
@@ -444,7 +401,6 @@ ${extra}
444
401
  const value = snapshot.value;
445
402
  const [open, setOpen] = (0, react.useState)(false);
446
403
  const [draft, setDraft] = (0, react.useState)(null);
447
- const [tokenConfigured, setTokenConfigured] = (0, react.useState)(false);
448
404
  const [diag, setDiag] = (0, react.useState)(null);
449
405
  const [saving, setSaving] = (0, react.useState)(false);
450
406
  const [status, setStatus] = (0, react.useState)(IDLE_STATUS);
@@ -452,17 +408,6 @@ ${extra}
452
408
  if (value === void 0 || draft !== null) return;
453
409
  setDraft(draftFromValue(value));
454
410
  }, [value, draft]);
455
- (0, react.useEffect)(() => {
456
- let alive = true;
457
- api.describe().then((view) => {
458
- if (!alive) return;
459
- const ns = view.namespaces.find((candidate) => candidate.ns === SETTINGS_NS);
460
- setTokenConfigured(ns?.secrets.some((secret) => secret.path.join(".") === "token" && secret.set) ?? false);
461
- }).catch(() => {});
462
- return () => {
463
- alive = false;
464
- };
465
- }, [api, snapshot]);
466
411
  (0, react.useEffect)(() => {
467
412
  let alive = true;
468
413
  fetch("/dsh-plus/gate/status").then((res) => res.ok ? res.json() : null).then((body) => {
@@ -472,9 +417,7 @@ ${extra}
472
417
  alive = false;
473
418
  };
474
419
  }, [snapshot]);
475
- const failClosed = (0, react.useMemo)(() => value?.enabled === true && value.allowedIps.length === 0 && !tokenConfigured, [value, tokenConfigured]);
476
420
  const dirty = (0, react.useMemo)(() => value !== void 0 && draft !== null && JSON.stringify(toPatch(draft)) !== JSON.stringify(toPatch(draftFromValue(value))), [value, draft]);
477
- const invalid = (0, react.useMemo)(() => draft !== null && (!isPositiveInt(draft.cookieMaxAgeHours) || !isPositiveInt(draft.loginFailLimit) || !isPositiveInt(draft.loginCooldownMs)), [draft]);
478
421
  if (value === void 0 || draft === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
479
422
  className: "dag-card",
480
423
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -492,7 +435,6 @@ ${extra}
492
435
  const onSave = () => {
493
436
  setSaving(true);
494
437
  const patch = toPatch(draft);
495
- if (draft.token !== "") patch.token = draft.token;
496
438
  const revision = scope.getSnapshot().revision;
497
439
  api.update(patch, revision).then(async () => {
498
440
  await scope.load();
@@ -534,14 +476,14 @@ ${extra}
534
476
  key: "save",
535
477
  label: t(saving ? "saving" : "save"),
536
478
  variant: "primary",
537
- disabled: !dirty || invalid || saving || disabled,
479
+ disabled: !dirty || saving || disabled,
538
480
  onClick: onSave
539
481
  }],
540
482
  children: [
541
- failClosed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
542
- className: "dag-warn",
543
- children: t("warnFailClosed")
544
- }) : null,
483
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
484
+ className: "dag-hint",
485
+ children: t("mergedHint")
486
+ }),
545
487
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
546
488
  prefix: "dag",
547
489
  id: "dag-enabled",
@@ -550,20 +492,6 @@ ${extra}
550
492
  disabled,
551
493
  onEdit: (v) => edit("enabled", v)
552
494
  }),
553
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
554
- prefix: "dag",
555
- id: "dag-token",
556
- label: t("token"),
557
- hint: t("tokenHint"),
558
- value: draft.token,
559
- password: true,
560
- disabled,
561
- badge: {
562
- text: t(tokenConfigured ? "tokenSet" : "tokenUnset"),
563
- set: tokenConfigured
564
- },
565
- onEdit: (v) => edit("token", v)
566
- }),
567
495
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
568
496
  className: "dag-field",
569
497
  children: [
@@ -597,42 +525,6 @@ ${extra}
597
525
  disabled,
598
526
  onEdit: (v) => edit("trustForwardedFor", v)
599
527
  }),
600
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
601
- prefix: "dag",
602
- id: "dag-cookie",
603
- label: t("cookieMaxAgeHours"),
604
- hint: t("cookieMaxAgeHoursHint"),
605
- value: draft.cookieMaxAgeHours,
606
- numeric: true,
607
- disabled,
608
- invalid: !isPositiveInt(draft.cookieMaxAgeHours),
609
- invalidLabel: t("invalidNumber"),
610
- onEdit: (v) => edit("cookieMaxAgeHours", v)
611
- }),
612
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
613
- prefix: "dag",
614
- id: "dag-faillimit",
615
- label: t("loginFailLimit"),
616
- hint: t("loginFailLimitHint"),
617
- value: draft.loginFailLimit,
618
- numeric: true,
619
- disabled,
620
- invalid: !isPositiveInt(draft.loginFailLimit),
621
- invalidLabel: t("invalidNumber"),
622
- onEdit: (v) => edit("loginFailLimit", v)
623
- }),
624
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
625
- prefix: "dag",
626
- id: "dag-cooldown",
627
- label: t("loginCooldownMs"),
628
- hint: t("loginCooldownMsHint"),
629
- value: draft.loginCooldownMs,
630
- numeric: true,
631
- disabled,
632
- invalid: !isPositiveInt(draft.loginCooldownMs),
633
- invalidLabel: t("invalidNumber"),
634
- onEdit: (v) => edit("loginCooldownMs", v)
635
- }),
636
528
  diag !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
637
529
  className: `dag-diag${diag.invalidEntries.length > 0 ? " dag-diagWarn" : ""}`,
638
530
  children: [
@@ -670,6 +562,16 @@ ${extra}
670
562
  children: diag.reason !== null ? t(REASON_KEYS[diag.reason] ?? diag.reason) : "—"
671
563
  })]
672
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
+ }),
673
575
  diag.invalidEntries.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
674
576
  className: "dag-diagRow",
675
577
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -695,69 +597,53 @@ ${extra}
695
597
  const NS = "dsh-plus-access-gate";
696
598
  const ownZh = {
697
599
  title: "访问控制",
698
- description: "Web 访问围栏:本机直连放行;远程按白名单 IP 或访问令牌放行,其余拦截到登录页。",
600
+ description: "Web 访问围栏:与官方登录合并——官方 cookie 为唯一凭据;未认证导航显示启动令牌输入页(PWA 内可用);可选 IP 附加围栏。",
699
601
  enabled: "启用访问围栏",
700
- enabledHint: "关闭即完全旁路(等价插件缺席);启用后远程流量需命中白名单或令牌。",
701
- token: "访问令牌",
702
- tokenHint: "留空表示保持已存令牌不变;清空并保存需先输入既有令牌确认。",
703
- tokenSet: "已配置",
704
- tokenUnset: "未配置",
705
- allowedIps: "放行 IP 白名单",
706
- 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(仅官方登录保护)。",
707
606
  trustForwardedFor: "信任 x-forwarded-for 还原客户端 IP",
708
607
  trustForwardedForHint: "仅当入口代理强制覆盖该头时开启(tailscale serve 满足);直连暴露场景必须关闭。",
709
- cookieMaxAgeHours: "登录有效期(小时)",
710
- cookieMaxAgeHoursHint: "令牌登录后 cookie 的有效期。",
711
- loginFailLimit: "登录失败阈值",
712
- loginFailLimitHint: "每个 IP 在冷却窗口内允许的失败次数,超出进入冷却。",
713
- loginCooldownMs: "登录冷却(毫秒)",
714
- loginCooldownMsHint: "失败达到阈值后的冷却时长。",
715
608
  diagTitle: "当前页面诊断",
716
609
  diagVerdict: "本页判定",
717
610
  diagClientIp: "客户端 IP",
718
611
  diagReason: "放行原因",
719
612
  diagLocal: "本机直连",
720
- diagAllowedIp: "白名单 IP",
721
- diagToken: "令牌",
613
+ diagCookie: "官方 cookie",
614
+ diagOfficial: "官方登录状态",
615
+ diagOfficialYes: "已登录",
616
+ diagOfficialNo: "未登录",
722
617
  diagPass: "放行",
723
618
  diagBlock: "拦截",
724
- diagLogin: "登录页",
619
+ diagTokenPage: "令牌输入页",
725
620
  diagOff: "围栏未启用",
726
- diagInvalid: "白名单存在无法解析的条目:",
727
- warnFailClosed: "警告:已启用但令牌与白名单均为空——所有远程访问将被拒绝(含本页之外的设备)。"
621
+ diagInvalid: "白名单存在无法解析的条目:"
728
622
  };
729
623
  const ownEn = {
730
624
  title: "Access Control",
731
- description: "Web access gate: loopback passes; remote traffic needs an allowlisted IP or the token, others hit the login page.",
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.",
732
626
  enabled: "Enable access gate",
733
- enabledHint: "Off means full bypass (as if absent); on, remote traffic must match the allowlist or the token.",
734
- token: "Access token",
735
- tokenHint: "Leave blank to keep the stored token; clearing requires typing the current one.",
736
- tokenSet: "Set",
737
- tokenUnset: "Not set",
738
- allowedIps: "Allowed IP allowlist",
739
- 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).",
740
631
  trustForwardedFor: "Trust x-forwarded-for for client IP",
741
632
  trustForwardedForHint: "Enable only when the entry proxy overwrites that header (tailscale serve does); must be off for direct exposure.",
742
- cookieMaxAgeHours: "Login validity (hours)",
743
- cookieMaxAgeHoursHint: "Cookie lifetime after a token login.",
744
- loginFailLimit: "Login failure limit",
745
- loginFailLimitHint: "Failures per IP within the cooldown window before throttling.",
746
- loginCooldownMs: "Login cooldown (ms)",
747
- loginCooldownMsHint: "Cooldown duration after reaching the limit.",
748
633
  diagTitle: "This page",
749
634
  diagVerdict: "Verdict",
750
635
  diagClientIp: "Client IP",
751
636
  diagReason: "Reason",
752
637
  diagLocal: "loopback",
753
- diagAllowedIp: "allowlisted IP",
754
- diagToken: "token",
638
+ diagCookie: "official cookie",
639
+ diagOfficial: "Official login",
640
+ diagOfficialYes: "authenticated",
641
+ diagOfficialNo: "not authenticated",
755
642
  diagPass: "pass",
756
643
  diagBlock: "blocked",
757
- diagLogin: "login page",
644
+ diagTokenPage: "token page",
758
645
  diagOff: "gate disabled",
759
- diagInvalid: "Allowlist entries that failed to parse:",
760
- warnFailClosed: "Warning: enabled with no token and an empty allowlist — all remote access will be refused."
646
+ diagInvalid: "Allowlist entries that failed to parse:"
761
647
  };
762
648
  const zh = mergeDict(commonZh, ownZh);
763
649
  const en = mergeDict(commonEn, ownEn);
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;