@dsh-plus/access-gate 0.1.4 → 0.2.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 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 规则)。
@@ -385,8 +350,9 @@ ${extra}
385
350
  //#region src/ns.ts
386
351
  /**
387
352
  * settings 命名空间字面量(纯常量,零依赖,浏览器半可安全引入)。
388
- * 服务端经 settingsNamespace() 品牌化后使用(config.ts),浏览器半将其作为
389
- * settings.plugin.item keyed 槽位的 key —— 同一字面量两处共用,防止漂移。
353
+ * 服务端直接以字面量注册(0.1.2-alpha.2 起 dsh-settings 编译期校验命名空间
354
+ * 语法,见 config.ts),浏览器半将其作为 settings.plugin.item keyed 槽位的
355
+ * key —— 同一字面量两处共用,防止漂移。
390
356
  * @module access-gate/ns
391
357
  */
392
358
  const SETTINGS_NS = "dsh-plus-access-gate";
@@ -395,47 +361,39 @@ ${extra}
395
361
  /**
396
362
  * 「访问控制」配置卡片:注册进 settings.plugin.item 插槽(官方插件配置页)。
397
363
  * 外壳与基础控件走 @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)。
364
+ * 本文件保留业务字段(白名单 textarea/诊断面板)与保存逻辑。
365
+ * 配置读写走官方 settingsScope 传输:value 为 schema 解析后的视图,
366
+ * 保存经 settings.update 深合并。
367
+ * 卡片顶部「当前页面诊断」读 /dsh-plus/gate/status(本页放行原因/客户端 IP/
368
+ * 官方 cookie 状态)。
369
+ *
370
+ * 与官方认证合并后:访问凭据由官方 browser-auth 持有(30 天 cookie),
371
+ * 卡片不再管理令牌;allowedIps 仅为可选附加 IP 围栏。
403
372
  * @module access-gate/client/card
404
373
  */
405
374
  function draftFromValue(value) {
406
375
  return {
407
376
  enabled: value.enabled,
408
- token: "",
409
377
  allowedIpsText: value.allowedIps.join("\n"),
410
- trustForwardedFor: value.trustForwardedFor,
411
- cookieMaxAgeHours: String(value.cookieMaxAgeHours),
412
- loginFailLimit: String(value.loginFailLimit),
413
- loginCooldownMs: String(value.loginCooldownMs)
378
+ trustForwardedFor: value.trustForwardedFor
414
379
  };
415
380
  }
416
- function isPositiveInt(text) {
417
- return /^[0-9]+$/.test(text) && Number(text) > 0;
418
- }
419
381
  function toPatch(draft) {
420
382
  return {
421
383
  enabled: draft.enabled,
422
384
  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)
385
+ trustForwardedFor: draft.trustForwardedFor
427
386
  };
428
387
  }
429
388
  /** reason 枚举 → 文案 key。 */
430
389
  const REASON_KEYS = {
431
390
  local: "diagLocal",
432
- "allowed-ip": "diagAllowedIp",
433
- token: "diagToken"
391
+ cookie: "diagCookie"
434
392
  };
435
393
  function verdictLabel(t, diag) {
436
394
  if (!diag.enabled) return t("diagOff");
437
395
  if (diag.verdict === "pass") return t("diagPass");
438
- if (diag.verdict === "login") return t("diagLogin");
396
+ if (diag.verdict === "token-page") return t("diagTokenPage");
439
397
  return t("diagBlock");
440
398
  }
441
399
  function AccessGateCard(props) {
@@ -444,7 +402,6 @@ ${extra}
444
402
  const value = snapshot.value;
445
403
  const [open, setOpen] = (0, react.useState)(false);
446
404
  const [draft, setDraft] = (0, react.useState)(null);
447
- const [tokenConfigured, setTokenConfigured] = (0, react.useState)(false);
448
405
  const [diag, setDiag] = (0, react.useState)(null);
449
406
  const [saving, setSaving] = (0, react.useState)(false);
450
407
  const [status, setStatus] = (0, react.useState)(IDLE_STATUS);
@@ -452,17 +409,6 @@ ${extra}
452
409
  if (value === void 0 || draft !== null) return;
453
410
  setDraft(draftFromValue(value));
454
411
  }, [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
412
  (0, react.useEffect)(() => {
467
413
  let alive = true;
468
414
  fetch("/dsh-plus/gate/status").then((res) => res.ok ? res.json() : null).then((body) => {
@@ -472,9 +418,7 @@ ${extra}
472
418
  alive = false;
473
419
  };
474
420
  }, [snapshot]);
475
- const failClosed = (0, react.useMemo)(() => value?.enabled === true && value.allowedIps.length === 0 && !tokenConfigured, [value, tokenConfigured]);
476
421
  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
422
  if (value === void 0 || draft === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
479
423
  className: "dag-card",
480
424
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -492,7 +436,6 @@ ${extra}
492
436
  const onSave = () => {
493
437
  setSaving(true);
494
438
  const patch = toPatch(draft);
495
- if (draft.token !== "") patch.token = draft.token;
496
439
  const revision = scope.getSnapshot().revision;
497
440
  api.update(patch, revision).then(async () => {
498
441
  await scope.load();
@@ -534,14 +477,14 @@ ${extra}
534
477
  key: "save",
535
478
  label: t(saving ? "saving" : "save"),
536
479
  variant: "primary",
537
- disabled: !dirty || invalid || saving || disabled,
480
+ disabled: !dirty || saving || disabled,
538
481
  onClick: onSave
539
482
  }],
540
483
  children: [
541
- failClosed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
542
- className: "dag-warn",
543
- children: t("warnFailClosed")
544
- }) : null,
484
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
485
+ className: "dag-hint",
486
+ children: t("mergedHint")
487
+ }),
545
488
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
546
489
  prefix: "dag",
547
490
  id: "dag-enabled",
@@ -550,20 +493,6 @@ ${extra}
550
493
  disabled,
551
494
  onEdit: (v) => edit("enabled", v)
552
495
  }),
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
496
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
568
497
  className: "dag-field",
569
498
  children: [
@@ -597,42 +526,6 @@ ${extra}
597
526
  disabled,
598
527
  onEdit: (v) => edit("trustForwardedFor", v)
599
528
  }),
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
529
  diag !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
637
530
  className: `dag-diag${diag.invalidEntries.length > 0 ? " dag-diagWarn" : ""}`,
638
531
  children: [
@@ -670,6 +563,16 @@ ${extra}
670
563
  children: diag.reason !== null ? t(REASON_KEYS[diag.reason] ?? diag.reason) : "—"
671
564
  })]
672
565
  }),
566
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
567
+ className: "dag-diagRow",
568
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
569
+ className: "dag-diagKey",
570
+ children: t("diagOfficial")
571
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
572
+ className: "dag-diagVal",
573
+ children: diag.officialAuthed ? t("diagOfficialYes") : t("diagOfficialNo")
574
+ })]
575
+ }),
673
576
  diag.invalidEntries.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
674
577
  className: "dag-diagRow",
675
578
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -695,69 +598,53 @@ ${extra}
695
598
  const NS = "dsh-plus-access-gate";
696
599
  const ownZh = {
697
600
  title: "访问控制",
698
- description: "Web 访问围栏:本机直连放行;远程按白名单 IP 或访问令牌放行,其余拦截到登录页。",
601
+ description: "Web 访问围栏:与官方登录合并——官方 cookie 为唯一凭据;未认证导航显示启动令牌输入页(PWA 内可用);可选 IP 附加围栏。",
699
602
  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。",
603
+ enabledHint: "关闭即完全旁路(等价插件缺席);启用后未认证流量被拦截到令牌输入页或拒绝。",
604
+ mergedHint: "登录凭据由官方认证持有(启动令牌换 30 天 cookie)。获取当前令牌:服务器执行 dshctl url,或本机访问 /dsh-plus/gate/launch-url。",
605
+ allowedIps: "附加 IP 围栏白名单",
606
+ allowedIpsHint: "每行一个:精确 IP 或 CIDR(IPv4/IPv6 皆可)。非空时仅白名单来源可通行与登录;留空 = 不限制来源 IP(仅官方登录保护)。",
707
607
  trustForwardedFor: "信任 x-forwarded-for 还原客户端 IP",
708
608
  trustForwardedForHint: "仅当入口代理强制覆盖该头时开启(tailscale serve 满足);直连暴露场景必须关闭。",
709
- cookieMaxAgeHours: "登录有效期(小时)",
710
- cookieMaxAgeHoursHint: "令牌登录后 cookie 的有效期。",
711
- loginFailLimit: "登录失败阈值",
712
- loginFailLimitHint: "每个 IP 在冷却窗口内允许的失败次数,超出进入冷却。",
713
- loginCooldownMs: "登录冷却(毫秒)",
714
- loginCooldownMsHint: "失败达到阈值后的冷却时长。",
715
609
  diagTitle: "当前页面诊断",
716
610
  diagVerdict: "本页判定",
717
611
  diagClientIp: "客户端 IP",
718
612
  diagReason: "放行原因",
719
613
  diagLocal: "本机直连",
720
- diagAllowedIp: "白名单 IP",
721
- diagToken: "令牌",
614
+ diagCookie: "官方 cookie",
615
+ diagOfficial: "官方登录状态",
616
+ diagOfficialYes: "已登录",
617
+ diagOfficialNo: "未登录",
722
618
  diagPass: "放行",
723
619
  diagBlock: "拦截",
724
- diagLogin: "登录页",
620
+ diagTokenPage: "令牌输入页",
725
621
  diagOff: "围栏未启用",
726
- diagInvalid: "白名单存在无法解析的条目:",
727
- warnFailClosed: "警告:已启用但令牌与白名单均为空——所有远程访问将被拒绝(含本页之外的设备)。"
622
+ diagInvalid: "白名单存在无法解析的条目:"
728
623
  };
729
624
  const ownEn = {
730
625
  title: "Access Control",
731
- description: "Web access gate: loopback passes; remote traffic needs an allowlisted IP or the token, others hit the login page.",
626
+ 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
627
  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.",
628
+ enabledHint: "Off means full bypass (as if absent); on, unauthenticated traffic hits the token page or is refused.",
629
+ 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.",
630
+ allowedIps: "Additional IP fence allowlist",
631
+ 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
632
  trustForwardedFor: "Trust x-forwarded-for for client IP",
741
633
  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
634
  diagTitle: "This page",
749
635
  diagVerdict: "Verdict",
750
636
  diagClientIp: "Client IP",
751
637
  diagReason: "Reason",
752
638
  diagLocal: "loopback",
753
- diagAllowedIp: "allowlisted IP",
754
- diagToken: "token",
639
+ diagCookie: "official cookie",
640
+ diagOfficial: "Official login",
641
+ diagOfficialYes: "authenticated",
642
+ diagOfficialNo: "not authenticated",
755
643
  diagPass: "pass",
756
644
  diagBlock: "blocked",
757
- diagLogin: "login page",
645
+ diagTokenPage: "token page",
758
646
  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."
647
+ diagInvalid: "Allowlist entries that failed to parse:"
761
648
  };
762
649
  const zh = mergeDict(commonZh, ownZh);
763
650
  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;