npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@fonlan/dsh-web-auth

v0.2.0

Published

DSH web plugin: password-gate authentication (login page + HttpOnly signed cookie) for exposing dsh web behind a reverse proxy

Readme

dsh-web-auth

为 DSH Web GUI 增加密码访问认证的插件:所有 HTTP 请求与 WebSocket 升级都必须携带有效会话,未登录一律跳转到登录页。装上它之后,把 dsh 反代到公网(或直接暴露端口)才不至于裸奔。

特性

  • 登录页 + HttpOnly 签名 Cookie(HMAC-SHA256,7 天滑动续期),浏览器自动携带,HTTP / WebSocket / SSE / 静态资源全覆盖
  • 首次设置:未配置密码时,登录页显示「设置访问密码」表单(引导流程,见下文安全说明)
  • GUI 改密:设置 → 侧栏「访问认证」页面,需旧密码;改密后轮换签名密钥,所有已登录会话立即下线
  • 退出登录:设置卡片按钮,或访问 /logout
  • 防爆破:登录与改密接口按客户端 IP 限速(连续 5 次失败锁 1 分钟起,指数退避至 30 分钟;15 分钟内累计)
  • 反代友好:Cookie Secure 自动跟随 x-forwarded-proto;限速信任本机对端的 X-Forwarded-For
  • 监听地址切换:设置卡片可在本机网卡的具体地址之间切换(含 127.0.0.1 仅本机、以及每张网卡的 IP),热生效、无需重启进程(REST → host 半区应用 patch → HMR 重绑 webserver)。不再提供 0.0.0.0:dsh 0.2.1-alpha.2 起通配符地址会在解析配置阶段直接让启动失败,写进去即硬砖(见下文「通配符与启动失败」)
  • 远程设置解锁(可选):unlockRemoteSettings 组合配置在启动页注入 __DSH_TRANSPORT__.ownsHost 标志,让反代域名下的设置面(模型页、插件配置、常规设置等)完整可用,默认关闭(见下文安全权衡)
  • 启动令牌收口:未登录浏览器带旧 ?token= 链接访问一律重定向回登录页,只有插件自身的服务端 mint 走回环放行通道——既不需要手工引导,也不会再撞上 DSH 的 401 死页面(0.1.7 起)
  • 密码以 scrypt 加盐哈希存储($DSH_HOME/web-auth/password.hash,0600),永不回显、不落明文配置

安装

dsh plugin --profile web add @fonlan/dsh-web-auth

从 npm 安装:dsh plugin add 会把包写入 profile 的依赖与 bundle 栈(cordis.patch.yml 自动 insert 插件行,host 半区 + 浏览器半区一次挂载)。升级到新版本:

dsh plugin --profile web update @fonlan/dsh-web-auth

安装后需要重启 dsh web 进程才生效(当前会话会短暂断连重连)。

宿主兼容性:0.2.0 面向 dsh ^0.2.0-rc.1 || ^0.2.1-alpha.1 || ^0.2.1-alpha.2,含两处按宿主演进做的适配:取值域去掉通配符(宿主 0.2.1-alpha.2 起拒绝),设置页不再依赖已被移除的客户端 settingsScope 服务(改走本插件 REST)。在更早的 0.1.x 宿主上本插件同样可用,但请勿把监听地址设成 0.0.0.0——宿主一旦升级到 0.2.1-alpha.2,该值会让 dsh 无法启动(见下文「通配符与启动失败」)。

激活(首次设置密码)

重启后打开 dsh web,会被重定向到登录页:

  1. 未设置过密码时,登录页显示「设置访问密码」——输入 ≥8 位的新密码并确认
  2. 设置成功后立即签发会话并跳回原页面;此后每次访问都要输密码

改密入口在 设置 → 侧栏「访问认证」(需先登录)。

行为细节

| 场景 | 行为 | |---|---| | 未登录访问页面(GET/HEAD) | 302 → /login?next=原路径,登录后跳回 | | 未登录访问 API(POST 等) | 401 JSON | | DSH 启动令牌交换(GET /?token=…) | 仅插件服务端的 mint 重入(直连回环对端 + 内部标记头)或已持有插件会话的请求放行并改写为回环来源转发:DSH 自行校验令牌、签发自己的 dsh-auth cookie(303 → 干净 /);令牌无效则收到 DSH 自带的 401 | | 浏览器直接打开打印的令牌 URL(未登录) | 302 → /login?next=/,丢弃 token 参数:令牌每次启动重新生成,旧书签/旧链接本就无效,放行只会把用户丢到 DSH 的 401 页面;登录后服务端 mint 会自动补发有效的 dsh-auth cookie | | DSH 会话 cookie 自动签发 | 插件在放行响应上服务端代理 DSH 的令牌交换(走本机回环 /?token=),把 dsh-auth Set-Cookie 随响应转发给浏览器——新浏览器无需再手工打开打印的 token URL;每分钟最多一次(结果会持续续期 DSH 的 30 天 cookie) | | 反代域名下已登录访问任意路径(含 /api、插件前缀) | 改写为回环来源放行,网关特权方法与插件围栏不再 403 | | 反代域名下的设置页(模型页等) | 默认报 settings are unavailable in this browser——dsh 客户端按页面 hostname 判定"非本机浏览器",设置镜像保持进程本地。开启 unlockRemoteSettings 后启动页注入 ownsHost,设置面完整可用(读写仍过密码门 + 回环围栏) | | 跨站请求(即使带 Cookie) | 仍被围栏 403 拒绝 | | 未登录 WebSocket 升级 | 握手直接 401 拒绝 | | 登录失败 | 跳回登录页显示「密码错误」(统一文案) | | 连续失败 | 按 IP 限速,提示「尝试过于频繁」 | | 会话过期 | 跳回登录页;滑动续期:剩余不足 24h 自动刷新 | | 修改密码 | 校验旧密码 → 轮换签名密钥 → 全员下线,跳转登录页 | | 切换监听地址(设置卡片) | 校验取值(必须是具体 IPv4/IPv6 字面量,通配符被拒绝)→ POST /api/web-auth/listen → host 半区应用 patch(profile 优先,home 兜底)→ HMR 热重载 webserver 重新绑定;WebSocket 短暂断连后自动重连 | | /logout | 清除 Cookie,跳转登录页 |

无需再处理 DSH 自己的启动令牌:DSH 的浏览器会话认证(dsh web 启动时打印的 http://127.0.0.1:3080/?token=…)用 cookie 绑定 Host。插件会把所有已认证请求的 Host 改写为回环地址,而 DSH 的 cookie 必须与「DSH 看到的 Host」一致——0.1.5 起插件会在登录/会话期间自动在服务端完成 DSH 令牌交换并把 dsh-auth cookie 转交给浏览器(含持续续期),所以外部设备、新浏览器都不需要再手工访问带 token 的 URL。0.1.7 起这条路径进一步收紧:未登录浏览器带 token 访问会被重定向到 /login 并丢弃该参数(旧令牌在 dsh 重启后即失效,放行只会展示 DSH 自己的 401 页面),登录本身就已经完成 mint,不再需要任何手工引导步骤。

切换监听地址

dsh 默认只监听 127.0.0.1。在 设置 → 侧栏「访问认证」 页面底部可以把它切换为本机任意网卡上的具体地址(列表由 host 半区枚举 os.networkInterfaces() 生成),或切回 127.0.0.1 仅本机——无需重启 dsh 进程:

  • 页面先 GET /api/web-auth/status 取当前地址与候选列表(allowed),提交时 POST /api/web-auth/listen;host 半区把取值写进当前部署的 patch 层:profile 的 cordis.patch.yml;若 home 层($DSH_HOME/cordis.patch.yml)已声明 webserver 行则写 home 层(home 优先级更高,改在低层会被覆盖)。文件里其余内容(注释、!!js 端口表达式)原样保留。
  • 写入前统一过 isLocalListenHost:必须是 IPv4/IPv6 字面量、不是通配符、并且确实是本机网卡拥有的地址,否则直接拒绝(400 invalid-host,响应里附上合法候选列表),不会写坏文件。下拉列表即由同一枚举生成,页面不可能提供一个写不进去的值。
  • dsh 的 HMR 会监听到 patch 文件变化并热重载 webserver 行:关闭旧监听、按新地址重新绑定,全程无需重启进程。重启进程后该设置依然生效(patch 文件在)。
  • 切换瞬间所有连接(包括当前页面与 WebSocket)会断开重连,属正常现象;卡片会轮询状态确认新地址生效。
  • 绑定具体网卡地址后 127.0.0.1 不再监听:本机也必须改用该地址访问(http://<网卡地址>:3080)。想同时保留本机与局域网入口,请让 dsh 继续监听 127.0.0.1,另在本机放一层反代监听局域网/公网并指向它——这也是 dsh 现在推荐的暴露方式。
  • 注意:若你正通过局域网 IP 访问并切回 127.0.0.1,切回后只能在本机访问,请改用 127.0.0.1 重新打开页面。

该能力依赖 webserver 行位于 profile/home patch 层(默认安装即如此)。若你的 webserver 配置来自自定义 --patch overlay,请直接编辑 overlay 文件。

写入值导致启动失败(三类,及恢复方式)

监听地址是组合配置,写在 patch 文件里、由 loader 在启动时读取。因此一个写坏的取值不是「绑定失败」而已——它会让下一次启动整体失败,而本插件自己也在 waiting、无法自救。0.2.0 把所有已知的三类都堵在写入之前:

| 类别 | 写坏后会看到 | 插件现在如何拦截 | |---|---|---| | 通配符 0.0.0.0 / :: | 解析配置阶段报 host "0.0.0.0" is an unspecified (wildcard) address, which is not supported … | 通配符一律拒绝(400 invalid-host / 写盘前抛错) | | 本机没有的地址(如 macOS 上的 127.0.0.2、过期 DHCP 地址) | listen EADDRNOTAVAIL: address not available … | 只接受本机网卡实际拥有的地址(os.networkInterfaces() 枚举,127.0.0.1 恒定可用) | | 把行追加到模板的空序列 [] 之后 | failed to parse overlay …: end of the stream or a document separator is expected | 识别模板 [] 并替换它(非空 flow 文档直接拒绝编辑) |

三类症状长这样(webserver 未激活,所有依赖它的插件排队等 webServer):

dsh: startup failed: 2 required plugins did not activate
  webserver (required)  @deepseek-ai/dsh-host-webserver  → 上面某一类报错
Plugins waiting for services:
  web-runtime / web-auth / better-sidebar / task-kanban / … (missing webServer)

恢复方式都是手工改 patch 文件(插件此时不会加载):

# 1) 看当前写死的 host
grep -n -A3 '^- id: webserver' ~/.dsh/profiles/web/cordis.patch.yml
# 2) 改成 127.0.0.1(或本机某张网卡的实际地址)后保存

若第一行是 [] 之类的空序列而下面又跟了 - id: 行,把那个 [] 行整行删掉即为合法文档。

DHCP 提醒:绑定具体网卡地址后,地址由 DHCP 变化(或网卡消失)时,下一次启动同样会 EADDRNOTAVAIL。长期部署建议让 dsh 监听 127.0.0.1,把对外暴露交给本机反代(反代再指向回环),这样地址变化不影响 dsh 启动。

0.2.0 起插件的所有写路径都会在改动任何字节之前校验,因此这三类砖不会再被本插件写出来。(若你仍在用 0.1.10 及更早版本并已中招,同样按上面方式手工修复,再升级到 0.2.0。)

远程设置解锁(unlockRemoteSettings,默认关闭)

现象:通过域名反代访问 dsh 时,设置 → 模型页报「加载提供方目录失败: settings are unavailable in this browser」,常规设置、插件配置等设置面同样不可用。这不是故障,是 dsh 的有意设计:设置文档(含 provider 凭据引用等敏感配置)只对"本机浏览器"开放——客户端在页面加载时按 location.hostname 判定是否回环(localhost / 127.x),反代域名永远不算,设置镜像因此保持进程本地、根本不发起读取。注意这与服务端无关:本插件已把已认证请求改写为回环来源,/api 通道完全正常,只有客户端这道门在拦。

开启:在 profile 的 cordis.patch.yml(~/.dsh/profiles/<profile>/cordis.patch.yml)加一条 config 覆盖:

- id: web-auth
  config:
    unlockRemoteSettings: true

HMR 会监听到 patch 变化并热重载 web-auth 行(无需重启进程)。生效后,域名下已登录页面的启动 HTML 会携带一段内联脚本,设置 window.__DSH_TRANSPORT__.ownsHost = true——dsh 客户端把它读作"该页面由宿主直接提供",设置镜像随之正常加载。只在成功渲染的 index 响应上注入(未登录请求在闸门处就被 302/401,根本到不了渲染);已存在的 __DSH_TRANSPORT__ 载体只会合并、不会被覆盖。不需要时删掉该配置即回退(同样热生效)。

安全权衡(请务必理解后再开启):ownsHost 是 dsh 画的"设置仅本机可改"边界;开启等于把这道边界改由本插件的密码门承担——每个通过密码认证的浏览器都能读写 host 设置文档(包括 provider API key 的引用、监听地址等)。若域名暴露在公网,请确保:强密码、反代全程 HTTPS、最好叠加反代层限流/IP 白名单。只在本机/内网使用或设置面不受影响时,保持默认关闭即可。

安全模型与风险(请务必阅读)

  • 认证是访问控制,不是安全边界。dsh 的 agent 拥有 bash 执行能力(远程代码执行级别),即使有密码保护,也不要把它当成不可攻破的堡垒:密码强度、反代层 WAF/限流、IP 白名单仍然值得做。
  • 首密窗口期:未设置密码时,任何人都能访问登录页并抢注密码(设计如此,作为 bootstrap 通道)。启动日志会打印警告;请装好插件后第一时间设置密码。生产环境建议先用 127.0.0.1 + SSH 端口转发完成首密设置,再开放公网。
  • 会话是无状态的(HMAC 签名),服务端无法单独吊销某一个会话;/logout 只清浏览器 Cookie。密码泄露时请修改密码——这会轮换密钥并让所有会话失效。
  • 密码哈希与签名密钥存放于 $DSH_HOME/web-auth/(默认 ~/.dsh/web-auth/),权限 0600;删除 password.hash 即回到未配置状态(认证关闭)。
  • 限速为进程内存级,多实例部署时各自独立,重启清零;如需更强防护请依赖反代层限流。

开发

pnpm install
npm run build      # host: tsc → lib/;client: esbuild → lib/client.js(ModuleLoader 格式)
npm run typecheck
npm run test       # 单元 + 集成测试(node:test,真实 HTTP/WS 全流程)

仓库结构:

  • src/auth-core.ts — 纯逻辑:scrypt 哈希、Cookie 签名/校验、限速器、next 校验
  • src/gate.ts — 把认证闸门包到 node:http 服务器的 request/upgrade 监听器外层
  • src/handlers.ts — 登录页/登录/登出/改密/状态/监听切换 路由处理
  • src/profile-patch.ts — 监听地址切换:webserver 行的 patch 层文本改写(保留注释与 !!js 表达式),并在写入前拒绝通配符
  • src/listen-addresses.ts — 监听地址取值域:字面量/通配符判定、本机网卡枚举(候选列表)、本机对端判定(mint 重入与 XFF 信任)
  • src/remote-unlock.ts — 远程设置解锁:unlockRemoteSettings 开启时经 webServer.tapIndex 注入 ownsHost 启动页脚本
  • src/index.ts — cordis 插件装配(host 半区)
  • src/client/index.tsx — 设置卡片(浏览器半区,settings.section 插槽,id web-auth;读写走本插件 REST)
  • cordis.patch.yml — bundle 补丁层(insert 插件行)

License

MIT