v1xingyue / dsh-auth-plugin

목록에 있음

DSH 用户授权插件:密码 + 通用 OAuth 2.0(内置 GitHub/Google/Discord 模板)+ Solana/EVM 钱包登录。零依赖核心,纯配置接入

main기타 소스 보기

설치

npx -y @deepseek-ai/dsh plugin --profile web add github:v1xingyue/dsh-auth-plugin

이 설치 명령은 GitHub 저장소 주소에서 생성된 확인되지 않은 시작점입니다.

README

유지 관리자가 작성한 문서 스냅샷입니다.

GitHub에서 보기 ↗
커밋 dec45bf동기화 2026. 8. 18.

최근 디렉터리 동기화에서 이 README 스냅샷을 새로 고치지 못했습니다.

DSH Auth Plugin — DeepSeek Harness 登录认证插件

给 DeepSeek Harness Web GUI 添加登录保护的 Cordis 插件。 零第三方依赖、单文件、极简配置,同时支持:

  • 🔑 用户名 + 密码登录(scrypt 哈希存储)
  • 🦖 Solana 钱包登录(ed25519 签名验证 + 公钥白名单)
  • 🛡️ 统一会话管理(HMAC-SHA256 令牌 + HttpOnly Cookie + 登出吊销)

目录

  1. 特性总览
  2. 架构与工作原理
  3. 安装
  4. 快速开始
  5. 配置参考
  6. API 参考
  7. 前端集成
  8. 安全设计
  9. 用户与密钥管理
  10. 测试
  11. 已知问题与故障排除
  12. 升级与回滚
  13. 生产部署建议
  14. 许可证

特性总览

特性说明状态
用户名密码登录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.0package.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.jsonexports/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 即重定向到登录页。


配置参考

顶层配置

字段类型默认值说明
enabledbooleantrue总开关
secretstring每次启动随机会话签名密钥;可用环境变量 DSH_AUTH_SECRET 固定
ttlHoursnumber24会话有效期(小时)
titlestringDSH 登录登录页标题
cookiestringdsh_session会话 Cookie 名
usersdict空→admin/admin123用户表(见下)
solanafalse | objectfalseSolana 钱包登录配置(见下)
evmfalse | objectfalseEVM 钱包登录配置(见下)
oauthobject{providers:{}}通用 OAuth 配置(见下)
publicPathsstring[]见下免认证路径(精确或前缀匹配)

默认 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 配置

字段类型默认值说明
enabledbooleantrue(对象形式)开关;solana: false 或省略 = 禁用
challengeTtlMsnumber300000nonce 有效期(5 分钟)
allowliststring[]必填公钥白名单(base58),至少 1 个
rolestringuser钱包登录默认角色

⚠️ allowlist 必填:启用时缺字段或空数组都会在加载时报配置错误 (schema 层拦截)——不允许"任何钱包可登录"

evm 配置(MetaMask 等 EVM 钱包)

evm:
  enabled: true
  allowlist:                # 必填:只允许这些 0x 地址(小写或混合大小写均可)
    - "0x4e984616e2dd9dffe7f2413efc7da35ef64c4117"
  role: user
字段类型默认值说明
enabledbooleantrue(对象形式)开关;evm: false 或省略 = 禁用
challengeTtlMsnumber300000nonce 有效期(5 分钟)
allowliststring[]必填允许的 0x 地址,至少 1 个
rolestringuser登录成功角色

⚠️ 同 Solana:allowlist 必填;且需要可选依赖 @noble/curves + @noble/hashesdsh 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 端点默认 scopeidField
githubgithub.com/login/oauth/authorize…/access_tokenapi.github.com/userread:userid
googleaccounts.google.com/o/oauth2/v2/authoauth2.googleapis.com/token…/oauth2/v3/userinfoopenid email profilesub
discorddiscord.com/oauth2/authorizediscord.com/api/oauth2/tokendiscord.com/api/users/@meidentify emailid
gitlabgitlab.com/oauth/authorizegitlab.com/oauth/tokengitlab.com/api/v4/userread_userid
microsoftlogin.microsoftonline.com/common/oauth2/v2.0/authorize…/tokengraph.microsoft.com/v1.0/meUser.Readid
bitbucketbitbucket.org/site/oauth2/authorize…/access_tokenapi.bitbucket.org/2.0/useraccountuuid
完整方式:显式配置(任意标准 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://..." # 可选:显式回调地址
字段类型默认值说明
builtinstring""内置模板名(见上表);填了则端点/字段/scope 用模板
labelstringprovider id / 模板登录页按钮文案
clientId / clientSecretstring必填OAuth 应用凭据
authorizeUrl / tokenUrl / userInfoUrlstring模板值三个端点
responseTypestringcode授权响应类型(授权码模式)
scopestring模板值/空请求的 scope(空格分隔)
redirectUristring自动显式回调地址;留空 = http://<Host>/api/auth/oauth/<id>/callback
idField / nameField / emailFieldstringid / 模板userinfo 字段映射
emailDomainsstring[][]邮箱域名白名单;非空时邮箱域名必须命中,否则拒绝(oauth_email_not_allowed
rolestringuser登录成功角色
userInfoHeadersdict{}取 userinfo 附加请求头
tokenParamsdict{}token 请求附加参数
stateTtlMsnumber600000state 有效期(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
未知 provider302/login?error=oauth_unknown_provider

会话身份:oauth:<providerId>:<id>(如 oauth:github:user-42)。


前端集成

密码表单

内置登录页为自包含 HTML(无外部资源),POST 到 /api/auth/login, 成功即设置 Cookie 并跳转 /

Solana 钱包登录

登录页内联脚本(零外部依赖):

  1. 检测钱包:window.solana.isPhantomwindow.phantom.solana
  2. 点击按钮 → provider.connect() 取公钥
  3. 请求 challenge → provider.signMessage(TextEncoder.encode(message), "utf8")
  4. 签名 base58 编码后 POST verify → 成功 location.href = "/"

支持 Phantom、Solflare 等注入 window.solana 的钱包;未安装钱包时 按钮下方提示"未检测到钱包"。

OAuth 第三方登录

登录页按配置为每个 provider 渲染一个按钮(<a href="/api/auth/oauth/<id>/start">), 点击即进入标准 OAuth 授权码流程,无需前端脚本。

EVM 钱包登录

登录页「⬡ 使用 EVM 钱包登录」按钮,内联脚本检测 window.ethereumeth_requestAccounts 取地址 → challenge → personal_sign → 提交 verify。


安全设计

实现
密码存储node:crypto scrypt(随机盐),支持明文仅限开发
令牌完整性HMAC-SHA256 签名,timingSafeEqual 比较
CookieHttpOnly; 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.ymlusers 表后重启即可。支持多用户、多角色 (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
钱包签名后 401nonce 过期(5 分钟)或已被使用,重新点击登录
OAuth 按钮不显示确认 oauth.providers 至少配置了一个 provider
OAuth 回调 oauth_bad_state刷新/重放旧回调,或 state 过期(10 分钟);重新从登录页发起
OAuth 回调 oauth_token_failed检查 clientId/clientSecret 与回调地址是否与 provider 后台一致
OAuth 回调 oauth_no_iduserinfo 响应里没有 idField 指定字段,按 provider 响应调整映射
EVM 按钮不显示确认 evm.enabled: true 且 allowlist 非空,且已安装 @noble(缺失时启动日志有警告)
EVM 登录报 403钱包地址不在 allowlist
EVM 签名后 401nonce 过期(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。)


生产部署建议

  1. 固定 secretsecret: !!js process.env.DSH_AUTH_SECRET(环境变量注入)
  2. HTTPS 反向代理:Nginx/Caddy 终结 TLS,转发 127.0.0.1:3080 (注意 WebSocket upgrade 头:Upgrade / Connection: upgrade
  3. 强密码:全部用户使用 scrypt 哈希,避免明文
  4. 钱包白名单:Solana 登录务必维护 allowlist,公钥即身份
  5. OS 边界:防火墙限制端口暴露;/api 仍由 DSH 自带篱笆保护
  6. 日志审计:如需登录审计日志,可 fork 插件在 loginHandler / verifyHandler 成功后打点

许可证

MIT

프로젝트 파일 및 신호

표시된 항목은 디렉터리 스냅샷에서 감지된 공개 저장소 신호입니다.

테스트감지됨
문서감지됨

저장소 정보

언어
JavaScript
라이선스
MIT
마지막 업데이트
2026. 8. 18. AM 7:47

신중하게 설치하기

소스 코드, 권한, 수명 주기 스크립트, 의존성 및 네트워크 접근을 검토하고 신뢰하지 않는 플러그인은 격리 환경에서 테스트하세요.