# dsh-auth-gate [English](README.md) | **简体中文** [![npm version](https://img.shields.io/npm/v/dsh-auth-gate.svg)](https://www.npmjs.com/package/dsh-auth-gate) [![npm downloads](https://img.shields.io/npm/dt/dsh-auth-gate.svg)](https://www.npmjs.com/package/dsh-auth-gate) [![npm monthly downloads](https://img.shields.io/npm/dm/dsh-auth-gate.svg)](https://www.npmjs.com/package/dsh-auth-gate) [![node](https://img.shields.io/node/v/dsh-auth-gate.svg)](https://www.npmjs.com/package/dsh-auth-gate) [![types](https://img.shields.io/npm/types/dsh-auth-gate.svg)](https://www.npmjs.com/package/dsh-auth-gate) [![CI](https://github.com/TecFancy/dsh-auth-gate/actions/workflows/ci.yml/badge.svg)](https://github.com/TecFancy/dsh-auth-gate/actions/workflows/ci.yml) [![license](https://img.shields.io/npm/l/dsh-auth-gate.svg)](LICENSE) 给 [DeepSeek Harness](https://github.com/deepseek-ai/dsh)(dsh)网页版加一道登录门。部署到 公网 dsh 实例前面之后,不登录就没人能碰到你的 agent、聊天会话和 LLM 凭证。 ## 基于 dsh-plugin-framework 构建 本插件建立在 [dsh-plugin-framework](https://github.com/TecFancy/dsh-plugin-framework) (dsh 生态的参考插件框架)的工程约定之上:`src/` 分层(features/shared,跨 slice 只能走 barrel)、工程门禁(`npm run verify` 全链、bundle/slice/no-emdash 校验)和决策记录纪律 全部对齐该框架——这些约定在 dsh 官方代码库中久经考验。好的工程实践,值得站在上面。 ## 它能做什么 - **所有访问都要先登录。** 每个页面、每个 API 调用、每条 WebSocket 连接都会检查; 没有有效会话的访客会被带到简单的登录页(API/脚本请求则返回 `401`)。唯一例外是 `GET /manifest.webmanifest`:浏览器抓 Web App Manifest 时不带凭证,所以这条精确 路径公开(只含应用名 / 图标 / 显示模式)。 - **两种登录方式**(配置里二选一): - **密码**(推荐):每个管理员一个用户名和密码。 - **令牌**:整个实例共用一个秘密令牌。 - **浏览器和脚本都能用。** 浏览器走登录页;脚本和 curl 直接带 `Authorization: Bearer ` 就能跳过登录页。 - **可选两步验证(TOTP)。** 密码模式下,账号绑定了 TOTP 密钥的用户登录时需要 密码**加**验证器 App 的 6 位动态码(RFC 6238;配置 off/optional/required 三态)。 - **默认就安全。** 密码只存哈希、登录有限速(反复输错会临时锁定该地址)、会话 cookie 带安全属性,而且配置缺失或损坏时**拒绝访问而不是悄悄开门**。 - **一个管理用户的小命令行工具**: ```sh dsh-auth user add admin --password-stdin # 添加用户 dsh-auth user list # 查看用户 dsh-auth user disable admin # 禁止某用户今后登录,并吊销其已发会话 dsh-auth user totp enable admin # 生成 TOTP 密钥(打印 otpauth:// URI) dsh-auth user totp disable admin # 移除 TOTP 密钥 ``` 全局安装时 `dsh-auth` 直接在你的 PATH 上;`dsh plugin add` 安装后二进制在 profile 里,需要经由 profile 调用——见[快速开始](#快速开始)。 ## 快速开始 ```sh # 1. 从 npm 装进你的 dsh profile。 # 0.4.1 起包声明了 dsh.bundle manifest,`dsh plugin add` 会同时自动注册挂载 # (dsh.profile.bundles),无需手动写挂载行: dsh plugin --profile web add dsh-auth-gate # 2. 创建管理员账号。 # `dsh plugin add` 把插件装进 profile 的 node_modules # ($DSH_HOME/profiles/web,默认 ~/.dsh/...),CLI **不会**进你的 PATH, # 所以要经由 profile 调用。`dsh plugin` 本来就要求有 pnpm: printf '%s\n' '选一个强密码' | \ pnpm --dir "$DSH_HOME/profiles/web" exec dsh-auth user add admin --password-stdin # 3. 开启密码登录:在 $DSH_HOME/cordis.patch.yml 里覆盖插件配置 # (仓库自带现成配置覆盖模板 deploy/cordis.patch.yml,见下方"配置"——挂载本身 # 不需要手动 patch 行) # 4. 重启 dsh,打开你的站点——会先要求登录。 ``` ## 效果预览 未登录的访客会被带到登录页: ![登录页](docs/demo/login-page.png) 账号启用了两步验证(TOTP)时,登录还会继续第二步——输入验证器 App(1Password、 Google Authenticator 等)里的 6 位验证码(先密码、后验证码): ![两步验证码页](docs/demo/totp-code.png) 登录后进入你的实例: ![dsh 实例](docs/demo/dashboard.png) 在 dsh 0.1.2-alpha 及更高版本(页面有 launch token 门)上,登录会自动桥接这道门: 登录跳转会先经过一次相对 `/?token=…` 的短跳、mint 好 dsh cookie,再落到 `/` (详见 `docs/implemented/impl-launch-token-bridge_zh.md`)。 设置面板里有一个醒目的**「退出登录 / Sign out」**按钮——在 **设置 → 通用设置** 页的最下方(最后一条设置项之后)。它是居中排布的填充式危险按钮(16px 门形图标 + 本地化文字,配色用主题 token、深浅色自适应);文案跟随界面语言(复用「设置」里 语言切换的同一套 locale 机制);点击走原有的原生 `POST /auth/logout?next=/` 登出流程。 ## 配置 bundle 挂载行(id `dsh-auth-gate`,由 `dsh plugin add` 自动插入)使用默认配置: `mode: "token"`,由 `DSH_AUTH_TOKEN` 环境变量提供共享秘密。要改配置,在 `$DSH_HOME/cordis.patch.yml`(或 profile 的 `cordis.patch.yml`)里按 id 覆盖—— 仓库自带现成覆盖模板 `deploy/cordis.patch.yml`。注意:覆盖条目**不要带 `insert`** (否则会二次挂载插件),只覆盖 config: ```yaml - id: dsh-auth-gate config: mode: "password" # "password"(推荐)或 "token" totp: "optional" # "off"(默认)、"optional" 或 "required" cookieSecure: true # 使用 https 时保持 true ``` | 选项 | 默认值 | 作用 | | --------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `mode` | `"token"` | `"password"` = 用户名密码登录;`"token"` = 一个共享秘密 | | `totp` | `"off"` | 仅密码模式。`"optional"`:绑定了 TOTP 密钥的用户登录需密码+动态码;`"required"`:所有用户都必须有密钥(无密钥/未知用户在密码阶段即统一 401,与错密同响应体,防枚举) | | `sessionTtl` | `604800` | 一次登录持续多久(秒),到期需重新登录 | | `cookieName` | `dsh_auth` | 会话 cookie 的名字(很少需要改) | | `tokenRef` | `"DSH_AUTH_TOKEN"` | 仅令牌模式:共享秘密存在哪个环境变量里 | | `cookieSecure` | `true` | 只在纯 http 测试环境设为 `false` | | `usersFile` | `""` | 密码模式:用户列表文件位置。默认 `$DSH_HOME/auth/users.yaml` | | `revokeSweepMs` | `5000` | 密码模式:被 `dsh-auth user disable` 禁用的用户,其**已发**会话多久内(毫秒)被吊销。`0` = 不扫描(禁用只拦新登录) | | `logoutOrder` | `1000` | 「退出登录」按钮在 设置 → 通用设置 页的槽位顺序(越大越靠底)。若有其他插件注册了更大的 order,可调大此值 | 给用户开启 TOTP:运行 `dsh-auth user totp enable `,把打印出的密钥(或 `otpauth://` URI 二维码)录入验证器 App(Google Authenticator、1Password 等)。 动态码每 30 秒变化一次;前后一个窗口内的码也接受(容忍时钟漂移)。 ## 内置配置技能 本包随附一份配置速查技能(`.agents/skills/dsh-auth-gate-config/`,即本页内容)。 把它安装到用户级技能目录后,部署侧的 dsh agent 就能直接回答「auth-gate 支持哪些配置」: ```sh pnpm --dir "${DSH_HOME:-$HOME/.dsh}/profiles/" exec dsh-auth skill install [--force] ``` 该命令把技能复制到 `$DSH_HOME/skills/dsh-auth-gate-config/`,dsh 技能发现机制会自动 加载。重复执行不带 `--force` 会保留你对技能的本地修改;`--force` 从包内刷新。 该技能是**仅用户可用技能**(frontmatter 里 `disable-model-invocation: true`): 它不会出现在模型的可自动调用技能目录中(不常驻每一轮 agent 上下文),需要查配置时 在技能面板显式打开即可(输入框 `/` 菜单里标记 `仅用户`)。若希望 agent 自动回答配置 问题,安装后移除该 frontmatter 字段即可。 ## 故障排查 ### `dsh-auth: command not found` `dsh plugin --profile web add dsh-auth-gate` 把包装进 profile 的 `node_modules` (`$DSH_HOME/profiles/web/node_modules/dsh-auth-gate`,默认 `~/.dsh/...`), 但不会往你的 shell `PATH` 里加任何东西,所以 CLI 二进制不能直接用名字调用。 这只影响 CLI——插件本身运行正常。任选其一: 1. **经由 profile 调用(推荐)。** `dsh plugin` 本来就要求有 pnpm,让 CLI 从插件所在的同一位置解析: ```sh pnpm --dir "${DSH_HOME:-$HOME/.dsh}/profiles/web" exec dsh-auth user add admin --password-stdin pnpm --dir "${DSH_HOME:-$HOME/.dsh}/profiles/web" exec dsh-auth user list ``` 可选,每个 shell 会话加一次: ```sh alias dsh-auth='pnpm --dir "${DSH_HOME:-$HOME/.dsh}/profiles/web" exec dsh-auth' ``` 2. **直接用 node 调用**(运行时不依赖 pnpm): ```sh node "$DSH_HOME/profiles/web/node_modules/dsh-auth-gate/lib/cli.js" user add admin --password-stdin ``` 3. **全局安装**,`dsh-auth` 就会在你的 PATH 上: ```sh npm install -g dsh-auth-gate dsh-auth user add admin --password-stdin ``` 无论哪种调用方式,CLI 读写的是同一份共享用户列表 (`$DSH_HOME/auth/users.yaml`,兜底 `~/.dsh/auth/users.yaml`,即插件读取的 那份)——全局安装的包只是启动器。 ## 部署 - [反代部署指南](docs/deployed/reverse-proxy_zh.md) —— Caddy/nginx 配置、浏览器信任栅栏的坑 (反代后设置页 `403`,以及为什么只加认证修不了它)、推荐的半外壳拓扑。 - [docs/deployed/deployment_zh.md](docs/deployed/deployment_zh.md) —— 运维清单、验收步骤(A–I)与故障诊断。 ## 认证本地代理(可选,dsh-auth-proxy) > ⚠️ **已知限制(重要,任何 auth-gate 版本都不改变)**:dsh 的设置页("设置 → 模型"等) > 只允许在**页面 origin 为回环**(`localhost`/`127.x`)时编辑。这是 dsh 客户端 > (`isLoopback` 检查)的设计边界,与认证正交——**域名页面打开设置弹框会显示 > "settings are unavailable in this browser",无法编辑提供方/凭据,升级 dsh-auth-gate > 也无法改变**。要编辑配置,请用本节的本地代理,或直接在服务器上访问 > `http://127.0.0.1:3080`。域名页面的聊天与模型选择不受影响。 > 半外壳解决服务端 `/api` 栅栏后,dsh **客户端**还要求"页面 origin 必须回环":域名页面下 > 设置页报 "settings are unavailable in this browser"(与认证无关)。`dsh-auth-proxy` > 在用户本机提供回环页面入口,配合 auth-gate 实现"远程编辑配置 + 全程认证", > 不修改 dsh 源码。详细设计见 [docs/deployed/local-proxy_zh.md](docs/deployed/local-proxy_zh.md)。 - 零依赖 Node bin(`dsh-auth-proxy`):严格绑定 `127.0.0.1`、无状态透传页面/API、 `events.mux`/`events.host` WebSocket 隧道、`Set-Cookie` 去 `Secure` 适配(Safari 兜底)。 - 认证复用 auth-gate(密码/令牌模式均可):登录页与会话 cookie 原样透传。 - **安全边界(deny-list,Phase 2.1)**:配合 `--mark-proxy`,服务端 guard 对标记请求中的 `host.pickDirectory`/`host.openPath`/`settings.openDocument`/`llm.discoverModels` 返回 403, 防止远程认证用户触发宿主原生能力;未开启标记时行为与未部署代理完全一致。 ```sh dsh-auth-proxy --listen 127.0.0.1:8443 --target https://your-domain.example --mark-proxy # 浏览器打开 http://127.0.0.1:8443 → 登录 →「设置 → 模型」即可编辑 ``` systemd 示例:`deploy/systemd/dsh-auth-proxy.service.example`。 ## 环境要求 - 服务器上需要 Node ≥ 22.19 和 pnpm。 - dsh 的 `web` profile 正常运行(`dsh --profile web`)。 - 如果 `cookieSecure` 是 `true`,站点必须走 https(浏览器在纯 http 下会拒绝安全 cookie)。 ## 许可证 [MIT](./LICENSE) ## 注意事项与局限 - 禁用用户会立即阻止**新**登录;**已发**会话由插件周期扫描吊销(`revokeSweepMs`,默认 5 秒内生效)。 - 登录限速在服务器重启后清零;TOTP 防重放记录同样重启清零(同一 30 秒窗口内用过的 码在重启后重新可用——需要「重启 + 同窗口窃码」同时发生才能利用)。 - TOTP 挑战态(「密码已过、等验证码」)最长 5 分钟。挑战 cookie 带 **HMAC 签名** (进程级随机密钥,ADR D10):无法伪造以跳过密码阶段。重启服务(或重载插件)后 在途挑战失效——验证码页上的用户需重新输入密码(窗口 ≤ 5 分钟);提交时按 用户当前配置的密钥验证。 - 反代部署时,限速按反代出口地址统计。 - 设置面板里有「退出登录」按钮:在 设置 → 通用设置 页最下方,文案随语言在 「退出登录」/ "Sign out" 间切换;`/auth/logout?next=/` 始终可作为兜底。 - 本插件只保护 dsh 的网页入口,不能替代服务器层面的安全:请保持服务器系统用户最小权限、 配置文件私密(`.credentials.yaml` 和 `auth/users.yaml` 创建时即为 `0600` 权限)。