DSH Auth Plugin — DeepSeek Harness 登录认证插件
给 DeepSeek Harness Web GUI 添加登录保护的 Cordis 插件。 零第三方依赖、单文件、极简配置,同时支持:
- 🔑 用户名 + 密码登录(scrypt 哈希存储)
- 🦖 Solana 钱包登录(ed25519 签名验证 + 公钥白名单)
- 🛡️ 统一会话管理(HMAC-SHA256 令牌 + HttpOnly Cookie + 登出吊销)
目录
特性总览
| 特性 | 说明 | 状态 |
|---|---|---|
| 用户名密码登录 | scrypt 哈希存储,支持明文(开发环境)与哈希 | ✅ |
| 通用 OAuth 2.0 登录 | 任意授权码 provider(GitHub/Google/Discord…),纯配置接入 | ✅ |
| Solana 钱包登录 | Phantom/Solflare,ed25519 challenge-response | ✅ |
| EVM 钱包登录 | MetaMask 等,EIP-191 personal_sign + ecrecover | ✅ 新增 |
| 白名单强制 | 钱包登录 allowlist/allowlist 必填,禁止任意钱包 |
✅ |
| 会话令牌 | HMAC-SHA256 自包含 token(JWT 风格) | ✅ |
| 防重放 | 一次性 nonce/state,消费即删 | ✅ |
| 登出吊销 | 服务端内存黑名单,防被窃 cookie 复用 | ✅ |
| 统一门卫 | 包装 webserver fallback,保护全部 SPA 页面与静态资源 | ✅ |
| 核心零依赖 | node:crypto(scrypt/ed25519/HMAC)+ Node 全局 fetch + 手写 base58 |
✅ |
| 可选增强 | EVM 需要纯 JS 库 @noble/curves+@noble/hashes(缺失时自动禁用) |
⚙️ |
| 极简安装 | 复制 1 个文件 + 2 行配置,无需 pnpm/npm | ✅ |
| npm 发布就绪 | exports/files/engines/prepack 校验,npm pack 验证通过 |
✅ |
| bundle 化 | dsh.bundle 声明,dsh plugin add 一行安装即生效 |
✅ |
版本:1.8.0(package.json)· 测试:60 项(34 单元 + 26 端到端)
架构与工作原理
模块解析基础
DSH 的 Loader 以 profile 目录(~/.dsh/profiles/web/)为 baseUrl,
插件行的 name 直接传给 Node import(),因此:
name: "./dsh-auth-plugin.js"→ 相对 profile 目录加载文件name: "@scope/pkg"→ 从 profile 的node_modules解析name: "file:///abs/path"→ 绝对路径加载
$DSH_HOME/profiles/node_modules 是 dsh 全依赖闭包的扁平回退目录
(含全部 195+ 个 @deepseek-ai/* 包),所以插件里
import "@deepseek-ai/schemastery" 等依赖无需安装即可解析——
这是"复制即用"机制的根基。
请求流(门卫架构)
浏览器请求
│
▼
webserver.match(pathname)
│
├─ 精确路由表命中(exact) → 直接处理
│ /login → 登录页
│ /api/auth/login → 密码登录 API
│ /api/auth/logout → 登出 API
│ /api/auth/solana/challenge → Solana 钱包 challenge
│ /api/auth/solana/verify → Solana 钱包 verify
│ /api/auth/evm/challenge → EVM 钱包 challenge
│ /api/auth/evm/verify → EVM 钱包 verify
│
├─ 最长前缀路由命中(prefix)
│ /api/auth/oauth/<id>/start → OAuth 授权跳转(本插件)
│ /api/auth/oauth/<id>/callback → OAuth 回调(本插件)
│ /api/*(client-connection 拥有)→ 保持 DSH 自带 loopback/
│ trustedHosts 篱笆(刻意不拦截)
│
└─ 未命中 → fallback 座位(本插件包装)
├─ 公开路径? → 放行
├─ 有有效会话 Cookie? → 放行(用户信息挂 req.authUser)
├─ 浏览器请求(Accept: html)→ 302 → /login
└─ API 调用 → 401 JSON { code: "auth_required" }
│
▼
frontend-static(原始 dist 服务,SPA 页面与静态资源)
关键设计:插件注册为唯一 fallback 座位持有者(替换 frontend-static
的占座,认证通过后转交原始 handler)。这比注册 prefix "/" 路由可靠——
webserver 的 prefix 匹配 pathname.startsWith("/" + "/") 对非根路径恒为
false,prefix "/" 实际拦不住任何路径(早期版本的 bug,已修复)。
会话令牌
- 结构:
<base64url(payload)>.<HMAC-SHA256(base64url(payload))> - Payload:
{ u: 用户名, r: 角色, exp: 过期时间戳 } - 传输:HttpOnly + SameSite=Strict + Max-Age 的 Cookie
- 吊销:登出时 token 进内存黑名单(重启清空)
Solana 登录流程
登录页点击「使用 Solana 钱包登录」
│
├─ 1. POST /api/auth/solana/challenge { publicKey }
│ 校验 base58(32B) → 白名单检查(403) → 签发一次性 nonce
│ 返回 { nonce, message: "DSH Login <nonce>", expiresAt }
│
├─ 2. 钱包 signMessage(message)(用户在钱包中确认)
│
└─ 3. POST /api/auth/solana/verify { publicKey, signature, nonce }
白名单检查(403) → 消费 nonce(401 防重放) → ed25519 验签(401)
→ 通过则签发会话 Cookie
消息编码兼容两种:Phantom signMessage 直接 UTF-8 字节,以及 SIWS v0
官方格式(\xff + 小写 "solana offchain message" + preamble,见
Agave 规范)。
OAuth 授权码流程
登录页点击「使用 GitHub 登录」
│
├─ 1. GET /api/auth/oauth/<id>/start
│ 签发一次性 state(绑定 provider,防 CSRF)
│ 302 → provider 授权页 ?client_id&redirect_uri&state[&scope]
│
├─ 2. 用户在 provider 页面授权
│
├─ 3. provider 302 → /api/auth/oauth/<id>/callback?code=&state=
│ 消费 state(一次性+过期+绑定)→ 失败 302 /login?error=oauth_bad_state
│ 服务端 code 换 token(client_secret 不下发浏览器)
│ 带 Bearer 取 userinfo → 映射 id/name/email
│ 签发会话 Cookie → 302 /
零依赖:token 交换与 userinfo 用 Node 全局 fetch(Node 18+)。
EVM 钱包流程(MetaMask 等)
登录页点击「⬡ 使用 EVM 钱包登录」
│
├─ 1. POST /api/auth/evm/challenge { address }
│ 校验 0x 地址 → 白名单检查(403) → 签发一次性 nonce
│ 返回 { nonce, message: "DSH Login <nonce>", expiresAt }
│
├─ 2. 钱包 personal_sign(message, address)(用户在钱包中确认)
│ 返回 65 字节 r||s||v 十六进制签名
│
└─ 3. POST /api/auth/evm/verify { address, signature, nonce }
白名单检查(403) → 消费 nonce(401 防重放)
→ EIP-191 消息哈希 → ecrecover 恢复地址(secp256k1)
→ 恢复地址 === 声明地址 → 签发会话 Cookie
依赖可选的 @noble/curves + @noble/hashes(纯 JS、无 native);
缺失时 EVM 登录自动禁用并打印警告,其他登录方式不受影响。
安装(仅启用 EVM 时需要):
dsh plugin --profile web add @noble/curves @noble/hashes
安装
方式 A:直接复制(推荐,已验证)
# 1. 复制单文件到 web profile 目录
cp lib/index.js ~/.dsh/profiles/web/dsh-auth-plugin.js
# 2. ~/.dsh/profiles/web/cordis.patch.yml 追加
- insert:
- id: auth
name: "./dsh-auth-plugin.js"
config:
users:
admin: admin123
重启 dsh web 生效。不需要改 package.json、跑 pnpm 或安装依赖。
方式 B:npm 包安装(发布到 registry 后)
# 从 npm registry 安装到 web profile
dsh plugin --profile web add dsh-auth-plugin
然后同样在 cordis.patch.yml insert 一行(name 用包名,依赖由
dsh plugin/pnpm 解析):
- insert:
- id: auth
name: "dsh-auth-plugin"
config:
users:
admin: admin123
dsh plugin是 pnpm 转发器;本插件为普通插件(无dsh.bundle), 安装后需 insert 一行声明(bundle 形态见方式 C)。
发布到 npm(维护者)
插件已做好发布就绪(package.json 含 exports/files/engines/
prepack 校验):
npm pack # 本地验证产物(prepack 会先跑语法检查)
npm login
npm publish --access public
# 本地测试 tarball 安装
npm pack
dsh plugin --profile web add file:/path/to/your-org-dsh-auth-plugin-1.6.0.tgz
方式 C:bundle 化(推荐给发布后的 npm 包)✅ 已实现
本插件已声明 "dsh": { "bundle": { "patch": "./cordis.patch.yml" } },
安装即自动成为配置层,连 cordis.patch.yml 的 insert 都省了:
dsh plugin --profile web add dsh-auth-plugin
# → pnpm 安装 → 自动加入 bundles 层 → auth 插件行自动生效
# (默认 admin/admin123,启动警告,请立即覆盖配置)
安装后在 profile 的 cordis.patch.yml 按 id 覆盖 config 即可:
- id: auth
config:
users:
admin: { password: "scrypt$...", role: admin }
oauth:
providers:
github: { builtin: github, clientId: "...", clientSecret: "..." }
已在隔离 DSH_HOME 完整验证:
dsh plugin add <tarball>→ bundles 列表 自动追加 →--dump-config出现# == dsh-auth-plugin段。
安装后文件布局
~/.dsh/profiles/web/
├── cordis.patch.yml # 认证配置(insert auth 行)
├── dsh-auth-plugin.js # 插件本体(单文件)
├── package.json # profile manifest(可选加 "type": "module")
└── pnpm-workspace.yaml
快速开始
# cordis.patch.yml —— 最简配置:用户名密码登录
- insert:
- id: auth
name: "./dsh-auth-plugin.js"
config:
users:
admin: admin123
# 用户名密码 + Solana 钱包登录(allowlist 必填)
- insert:
- id: auth
name: "./dsh-auth-plugin.js"
config:
users:
admin:
password: "scrypt$<salt>$<hash>"
role: "admin"
solana:
enabled: true
allowlist:
- "Dy6mBH4YeqJCRZohd39iSFaf4jyLaxPeBakbZwt1jToL"
重启 dsh web 后访问 http://127.0.0.1:3080 即重定向到登录页。
配置参考
顶层配置
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
enabled |
boolean | true |
总开关 |
secret |
string | 每次启动随机 | 会话签名密钥;可用环境变量 DSH_AUTH_SECRET 固定 |
ttlHours |
number | 24 |
会话有效期(小时) |
title |
string | DSH 登录 |
登录页标题 |
cookie |
string | dsh_session |
会话 Cookie 名 |
users |
dict | 空→admin/admin123 |
用户表(见下) |
solana |
false | object |
false |
Solana 钱包登录配置(见下) |
evm |
false | object |
false |
EVM 钱包登录配置(见下) |
oauth |
object | {providers:{}} |
通用 OAuth 配置(见下) |
publicPaths |
string[] | 见下 | 免认证路径(精确或前缀匹配) |
默认 publicPaths:/login、/api/auth/login、/api/auth/logout、
/favicon.ico
⚠️
secret为空时每次启动随机——重启后所有会话失效需重新登录。 这是保守设计(无需持久化);生产环境建议固定:secret: !!js process.env.DSH_AUTH_SECRET。
users 三种写法
# 1. 极简:用户名: 明文密码(开发环境)
users:
admin: admin123
# 2. 带角色
users:
admin: { password: admin123, role: admin }
# 3. 推荐:scrypt 哈希
users:
admin: { password: "scrypt$<salt>$<hash>", role: admin }
明文密码启动时会打印警告(仅限开发环境)。
solana 配置
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
enabled |
boolean | true(对象形式) |
开关;solana: false 或省略 = 禁用 |
challengeTtlMs |
number | 300000 |
nonce 有效期(5 分钟) |
allowlist |
string[] | 必填 | 公钥白名单(base58),至少 1 个 |
role |
string | user |
钱包登录默认角色 |
⚠️
allowlist必填:启用时缺字段或空数组都会在加载时报配置错误 (schema 层拦截)——不允许"任何钱包可登录"。
evm 配置(MetaMask 等 EVM 钱包)
evm:
enabled: true
allowlist: # 必填:只允许这些 0x 地址(小写或混合大小写均可)
- "0x4e984616e2dd9dffe7f2413efc7da35ef64c4117"
role: user
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
enabled |
boolean | true(对象形式) |
开关;evm: false 或省略 = 禁用 |
challengeTtlMs |
number | 300000 |
nonce 有效期(5 分钟) |
allowlist |
string[] | 必填 | 允许的 0x 地址,至少 1 个 |
role |
string | user |
登录成功角色 |
⚠️ 同 Solana:
allowlist必填;且需要可选依赖@noble/curves+@noble/hashes(dsh plugin --profile web add @noble/curves @noble/hashes), 缺失时该功能自动禁用(其余登录不受影响)。
oauth 通用 OAuth 2.0 配置
每个 provider 一条,纯配置接入任意标准授权码 OAuth 服务。
支持内置模板:填 builtin + 凭据即可,端点/字段映射/scope 自动填充。
最简方式:内置模板(推荐)
oauth:
providers:
github: # provider id
builtin: "github" # ← 内置模板:端点/字段/scope 自动填充
clientId: "Ov23li..."
clientSecret: "ghp_..." # 仅服务端使用,绝不下发浏览器
内置模板一览(BUILTIN_OAUTH,显式字段可覆盖模板):
| builtin | 授权端点 | token 端点 | userinfo 端点 | 默认 scope | idField |
|---|---|---|---|---|---|
github |
github.com/login/oauth/authorize | …/access_token | api.github.com/user | read:user |
id |
google |
accounts.google.com/o/oauth2/v2/auth | oauth2.googleapis.com/token | …/oauth2/v3/userinfo | openid email profile |
sub |
discord |
discord.com/oauth2/authorize | discord.com/api/oauth2/token | discord.com/api/users/@me | identify email |
id |
gitlab |
gitlab.com/oauth/authorize | gitlab.com/oauth/token | gitlab.com/api/v4/user | read_user |
id |
microsoft |
login.microsoftonline.com/common/oauth2/v2.0/authorize | …/token | graph.microsoft.com/v1.0/me | User.Read |
id |
bitbucket |
bitbucket.org/site/oauth2/authorize | …/access_token | api.bitbucket.org/2.0/user | account |
uuid |
完整方式:显式配置(任意标准 OAuth 服务)
oauth:
providers:
custom:
label: "我的服务"
clientId: "..."
clientSecret: "..."
authorizeUrl: "https://.../authorize"
tokenUrl: "https://.../token"
userInfoUrl: "https://.../userinfo"
scope: "read" # 可选
idField: "id" # 可选,默认 id
nameField: "name" # 可选
emailField: "email" # 可选
role: "user" # 可选
# redirectUri: "https://..." # 可选:显式回调地址
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
builtin |
string | "" |
内置模板名(见上表);填了则端点/字段/scope 用模板 |
label |
string | provider id / 模板 | 登录页按钮文案 |
clientId / clientSecret |
string | 必填 | OAuth 应用凭据 |
authorizeUrl / tokenUrl / userInfoUrl |
string | 模板值 | 三个端点 |
responseType |
string | code |
授权响应类型(授权码模式) |
scope |
string | 模板值/空 | 请求的 scope(空格分隔) |
redirectUri |
string | 自动 | 显式回调地址;留空 = http://<Host>/api/auth/oauth/<id>/callback |
idField / nameField / emailField |
string | id / 模板 |
userinfo 字段映射 |
emailDomains |
string[] | [] |
邮箱域名白名单;非空时邮箱域名必须命中,否则拒绝(oauth_email_not_allowed) |
role |
string | user |
登录成功角色 |
userInfoHeaders |
dict | {} |
取 userinfo 附加请求头 |
tokenParams |
dict | {} |
token 请求附加参数 |
stateTtlMs |
number | 600000 |
state 有效期(10 分钟) |
回调地址(OAuth 应用后台填写):
http(s)://<你的地址>/api/auth/oauth/<id>/callback例如:http://127.0.0.1:3080/api/auth/oauth/github/callback未知builtin名或合并后缺必需字段会在启动时报配置错误。
API 参考
GET /login
返回内置登录页(含密码表单 + 可选 Solana 钱包区块)。状态码:200。
POST /api/auth/login
用户名密码登录。支持 JSON 与 application/x-www-form-urlencoded。
curl -X POST http://127.0.0.1:3080/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"secret"}'
| 结果 | 状态码 | 说明 |
|---|---|---|
| 成功(JSON 调用) | 200 |
{ ok, username, role } + Set-Cookie |
| 成功(浏览器表单) | 302 |
重定向 / + Set-Cookie |
| 凭据错误(JSON) | 401 |
{ error: "invalid credentials" } |
| 凭据错误(表单) | 401 |
登录页 + 错误提示 |
| 缺参 | 400 |
{ error: "username and password required" } |
| 方法错误 | 405 |
非 GET/POST |
POST /api/auth/logout
登出。服务端将当前 token 加入吊销黑名单并清除 Cookie。
成功:200 { ok: true }(JSON)或 302 → /login(浏览器)。
POST /api/auth/solana/challenge
签发一次性 nonce(绑定公钥)。
curl -X POST http://127.0.0.1:3080/api/auth/solana/challenge \
-H 'Content-Type: application/json' \
-d '{"publicKey":"<32字节base58>"}'
| 结果 | 状态码 | 说明 |
|---|---|---|
| 成功 | 200 |
{ nonce, message, expiresAt } |
| 公钥非法 | 400 |
{ error: "invalid public key" } |
| 不在白名单 | 403 |
{ error: "public key not allowed" } |
POST /api/auth/solana/verify
提交钱包签名换取会话。
curl -X POST http://127.0.0.1:3080/api/auth/solana/verify \
-H 'Content-Type: application/json' \
-d '{"publicKey":"<base58>","signature":"<base58 64B>","nonce":"<challenge 返回>"}'
| 结果 | 状态码 | 说明 |
|---|---|---|
| 成功 | 200 |
{ ok, username: "solana:<pubkey>", role, publicKey } + Set-Cookie |
| 不在白名单 | 403 |
拒绝 |
| nonce 无效/过期/重放 | 401 |
{ error: "invalid or expired challenge" } |
| 签名验证失败 | 401 |
{ error: "signature verification failed" } |
| 缺参 | 400 |
{ error: "publicKey, signature and nonce required" } |
POST /api/auth/evm/challenge
EVM 钱包登录第一步:签发一次性 nonce(绑定 0x 地址)。
curl -X POST http://127.0.0.1:3080/api/auth/evm/challenge \
-H 'Content-Type: application/json' \
-d '{"address":"0x4e984616e2dd9dffe7f2413efc7da35ef64c4117"}'
| 结果 | 状态码 | 说明 |
|---|---|---|
| 成功 | 200 |
{ nonce, message: "DSH Login <nonce>", expiresAt } |
| 地址非法 | 400 |
{ error: "invalid address" } |
| 不在白名单 | 403 |
{ error: "address not allowed" } |
POST /api/auth/evm/verify
提交 personal_sign 签名换取会话(需要可选依赖 @noble)。
curl -X POST http://127.0.0.1:3080/api/auth/evm/verify \
-H 'Content-Type: application/json' \
-d '{"address":"0x...","signature":"0x<65字节r||s||v>","nonce":"<challenge 返回>"}'
| 结果 | 状态码 | 说明 |
|---|---|---|
| 成功 | 200 |
{ ok, username: "evm:<address>", role, address } + Set-Cookie |
| 不在白名单 | 403 |
拒绝 |
| nonce 无效/过期/重放 | 401 |
{ error: "invalid or expired challenge" } |
| ecrecover 不匹配 | 401 |
{ error: "signature verification failed" } |
| 缺参/地址非法 | 400 |
{ error: "address, signature and nonce required" } |
GET /api/auth/oauth/<id>/start
发起 OAuth 登录。签发一次性 state 后 302 到 provider 授权页
(authorizeUrl?client_id&redirect_uri&state[&scope])。
未知 provider → 404。
GET /api/auth/oauth/<id>/callback
OAuth 回调(用户从 provider 授权页跳回)。浏览器直接访问即可,无需手动调用。
# 模拟(真实流程由浏览器跳转完成)
curl -i "http://127.0.0.1:3080/api/auth/oauth/github/callback?code=xxx&state=<start 返回的 state>"
| 结果 | 状态码 | 行为 |
|---|---|---|
| 成功 | 302 |
重定向 / + Set-Cookie(会话已建立) |
| state 无效/过期/重放 | 302 |
重定向 /login?error=oauth_bad_state(防 CSRF) |
| token 交换失败 | 302 |
/login?error=oauth_token_failed |
| userinfo 获取失败 | 302 |
/login?error=oauth_userinfo_failed |
| userinfo 缺 id 字段 | 302 |
/login?error=oauth_no_id |
| 未知 provider | 302 |
/login?error=oauth_unknown_provider |
会话身份:oauth:<providerId>:<id>(如 oauth:github:user-42)。
前端集成
密码表单
内置登录页为自包含 HTML(无外部资源),POST 到 /api/auth/login,
成功即设置 Cookie 并跳转 /。
Solana 钱包登录
登录页内联脚本(零外部依赖):
- 检测钱包:
window.solana.isPhantom→window.phantom.solana - 点击按钮 →
provider.connect()取公钥 - 请求 challenge →
provider.signMessage(TextEncoder.encode(message), "utf8") - 签名 base58 编码后 POST verify → 成功
location.href = "/"
支持 Phantom、Solflare 等注入 window.solana 的钱包;未安装钱包时
按钮下方提示"未检测到钱包"。
OAuth 第三方登录
登录页按配置为每个 provider 渲染一个按钮(<a href="/api/auth/oauth/<id>/start">),
点击即进入标准 OAuth 授权码流程,无需前端脚本。
EVM 钱包登录
登录页「⬡ 使用 EVM 钱包登录」按钮,内联脚本检测 window.ethereum:
eth_requestAccounts 取地址 → challenge → personal_sign → 提交 verify。
安全设计
| 项 | 实现 |
|---|---|
| 密码存储 | node:crypto scrypt(随机盐),支持明文仅限开发 |
| 令牌完整性 | HMAC-SHA256 签名,timingSafeEqual 比较 |
| Cookie | HttpOnly; SameSite=Strict; Path=/; Max-Age |
| 登出吊销 | 服务端内存黑名单(revoked Set) |
| nonce 防重放 | 一次性消费,consume 即删;绑定公钥/地址;TTL 5 分钟 + 惰性 sweep |
| OAuth state 防 CSRF | 一次性 + 绑定 provider + 10 分钟 TTL;伪造/重用 state → oauth_bad_state |
| client_secret 保密 | 仅服务端持有,token 交换在服务端完成,绝不下发浏览器 |
| userinfo 服务端获取 | Bearer token 不出服务端 |
| EVM 验签 | EIP-191 personal_sign 消息哈希 + secp256k1 ecrecover 恢复地址,与声明地址比对 |
| 登录审计 | 每次登录成功 info / 失败 warn 打点([dsh-auth] login ok/failed: <method> <身份> [原因]) |
| 白名单强制 | 钱包登录 allowlist/allowlist 必填,challenge 与 verify 双重 403 |
| 消息重建 | 服务端 DSH Login <nonce> 重建,不信任客户端回传 message |
| 定时器依赖 | inject: ["webServer", "timer"] 显式声明(Cordis 强制) |
/api 边界 |
保持 DSH 自带 loopback/trustedHosts 篱笆,插件不越权 |
用户与密钥管理
生成 scrypt 哈希
# 方式一:内置脚本
node scripts/manage-users.js hash 你的密码
# 方式二:一行命令
node --input-type=module -e "
import { scrypt, randomBytes } from 'node:crypto';
const salt = randomBytes(16).toString('hex');
scrypt(process.argv[1], salt, 32, (e,k)=>{ if(e) throw e; console.log('scrypt\$'+salt+'\$'+k.toString('hex')); });
" 你的密码
添加/修改/删除用户
直接编辑 cordis.patch.yml 的 users 表后重启即可。支持多用户、多角色
(admin / user,角色目前仅作标识,可由其他插件消费 req.authUser)。
Solana 公钥获取
钱包连接后从 publicKey.toString() 得到 base58 地址,填入 allowlist。
验证合法性:node -e "import('./dsh-auth-plugin.js').then(m => console.log(m.base58Decode('...').length === 32))"
测试
# 单元测试(核心零依赖,直接可跑;EVM 组需要 @noble,缺失时自动跳过)
node --test test/auth.test.js
# → 34 项:scrypt、HMAC 令牌、base58、ed25519 验签(含 SIWS v0)、
# nonce/state 管理、EVM ecrecover、OAuth 模板合并
# 端到端测试(需要 DSH 的 node_modules 环境解析 schematery)
node --test test/e2e.test.js
# → 26 项:密码登录、Cookie、登出吊销、Solana/EVM 钱包、白名单 403、
# fake OAuth provider 授权码流程(含内置模板)、邮箱域名白名单、防重放
已知问题与故障排除
已修复:cannot get property "timer" without inject(v1.1.0 → v1.2.0)
启用 Solana 后启动失败:
Error: dsh: plugin tree failed to load:
failed to apply loader entry auth (./dsh-auth-plugin.js):
cannot get property "timer" without inject
根因:ctx.setInterval() 由 Cordis 的 timer 服务提供,新版 Cordis
要求显式声明所有经 ctx 使用的服务;原 inject = ["webServer"] 遗漏了
timer。
修复:const inject = ["webServer", "timer"]。
完整排查记录见 docs/repair-timer-inject.md。
常见问题
| 现象 | 处理 |
|---|---|
启动报 failed to import loader entry auth |
确认 ./dsh-auth-plugin.js 相对 profile 目录路径正确、文件存在 |
报找不到 @deepseek-ai/schemastery |
确认 ~/.dsh/profiles/node_modules 存在(dsh 首次启动自动生成) |
| 登录后又被弹回登录页 | secret 未固定 → 重启后旧会话失效,属正常;或系统时间异常 |
cannot get property "timer" |
确认 inject 含 "timer"(v1.2.0 起已内置) |
| Solana 按钮不显示 | 确认 solana.enabled: true 且 allowlist 非空 |
| 钱包登录报 403 | 钱包公钥不在 allowlist |
| 钱包签名后 401 | nonce 过期(5 分钟)或已被使用,重新点击登录 |
| OAuth 按钮不显示 | 确认 oauth.providers 至少配置了一个 provider |
OAuth 回调 oauth_bad_state |
刷新/重放旧回调,或 state 过期(10 分钟);重新从登录页发起 |
OAuth 回调 oauth_token_failed |
检查 clientId/clientSecret 与回调地址是否与 provider 后台一致 |
OAuth 回调 oauth_no_id |
userinfo 响应里没有 idField 指定字段,按 provider 响应调整映射 |
| EVM 按钮不显示 | 确认 evm.enabled: true 且 allowlist 非空,且已安装 @noble(缺失时启动日志有警告) |
| EVM 登录报 403 | 钱包地址不在 allowlist |
| EVM 签名后 401 | nonce 过期(5 分钟)或已被使用,重新点击登录;或签名格式非 0x + 65 字节 |
启动时的 MODULE_TYPELESS 警告 |
无碍;在 profile 的 package.json 加 "type": "module" 消除 |
升级与回滚
升级
cp <新版本>/lib/index.js ~/.dsh/profiles/web/dsh-auth-plugin.js
# 重启 dsh web
回滚
cp ~/.dsh/profiles/web/cordis.patch.yml.bak ~/.dsh/profiles/web/cordis.patch.yml
rm -f ~/.dsh/profiles/web/dsh-auth-plugin.js
(安装时自动备份了 cordis.patch.yml.bak;再次改动前有 .bak2。)
生产部署建议
- 固定 secret:
secret: !!js process.env.DSH_AUTH_SECRET(环境变量注入) - HTTPS 反向代理:Nginx/Caddy 终结 TLS,转发
127.0.0.1:3080(注意 WebSocket upgrade 头:Upgrade/Connection: upgrade) - 强密码:全部用户使用 scrypt 哈希,避免明文
- 钱包白名单:Solana 登录务必维护
allowlist,公钥即身份 - OS 边界:防火墙限制端口暴露;
/api仍由 DSH 自带篱笆保护 - 日志审计:如需登录审计日志,可 fork 插件在
loginHandler/verifyHandler成功后打点
许可证
MIT
还没有评论,来写第一条。