@shgroup/dsh-serenity-hooks 1.24.7 → 1.24.9

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/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.24.7",
3
+ "version": "1.24.9",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):真实 DSH 工具 cc_fs/session/acc_msm/eap/neat/cce/handyman/session_rebuild + 拦截缝机械约束(safe-mode/路径守卫)+ 高级设定面板(双端口网关/账号管理)。适配 DSH 公开版(0.1.0-rc,deepseek-ai/deepseek-harness)。",
6
6
  "engines": {
package/lib/index.js CHANGED
@@ -8018,9 +8018,45 @@ function accountLockRemaining(user) {
8018
8018
  const st = getFailState(user);
8019
8019
  return st.lockedUntil > 0 && Date.now() < st.lockedUntil ? st.lockedUntil - Date.now() : 0;
8020
8020
  }
8021
- /** 生成 CSRF cookie 值(随机 32B hex) */
8021
+ /** CSRF token 有效期(毫秒):10 分钟窗口(扫码/多标签场景留足时间) */
8022
+ const CSRF_TTL_MS = 6e5;
8023
+ /**
8024
+ * 服务端 CSRF token 集合(v1.24.9 修复:多标签/刷新竞争导致登录死循环)。
8025
+ * 背景:每次 GET 登录页都生成新 token 覆盖 cookie——多标签页场景下,早先打开的
8026
+ * 标签携带旧 form token 提交时与最新 cookie 不匹配 → "会话校验失败"永远失败
8027
+ * (S142 用户实测:cookieCsrf=present formCsrf=present 但不匹配)。
8028
+ * 修复:每次 GET 生成的 token 存入集合(TTL 10min),提交时 form/cookie token
8029
+ * 只要 ∈ 集合即有效——多标签各自 token 都接受。
8030
+ */
8031
+ const csrfTokens = /* @__PURE__ */ new Map();
8032
+ /** 生成 CSRF token(随机 32B hex)并入集合 */
8022
8033
  function newCsrfToken() {
8023
- return randomBytes(32).toString("hex");
8034
+ const token = randomBytes(32).toString("hex");
8035
+ csrfTokens.set(token, Date.now() + CSRF_TTL_MS);
8036
+ if (csrfTokens.size > 50) {
8037
+ const now = Date.now();
8038
+ for (const [t, expiry] of csrfTokens) if (expiry < now) csrfTokens.delete(t);
8039
+ if (csrfTokens.size > 50) {
8040
+ let oldest = null;
8041
+ let oldestExpiry = Infinity;
8042
+ for (const [t, expiry] of csrfTokens) if (expiry < oldestExpiry) {
8043
+ oldest = t;
8044
+ oldestExpiry = expiry;
8045
+ }
8046
+ if (oldest !== null) csrfTokens.delete(oldest);
8047
+ }
8048
+ }
8049
+ return token;
8050
+ }
8051
+ /** token 是否有效(在集合且未过期;过期自动清理) */
8052
+ function isCsrfValid(token) {
8053
+ const expiry = csrfTokens.get(token);
8054
+ if (expiry === void 0) return false;
8055
+ if (expiry < Date.now()) {
8056
+ csrfTokens.delete(token);
8057
+ return false;
8058
+ }
8059
+ return true;
8024
8060
  }
8025
8061
  /** 从请求头取 CSRF token(X-CSRF-Token 或表单字段) */
8026
8062
  function csrfFromRequest(req, body) {
@@ -8029,12 +8065,6 @@ function csrfFromRequest(req, body) {
8029
8065
  const form = body.get("csrf");
8030
8066
  return form && form !== "" ? form : null;
8031
8067
  }
8032
- /** 常量时间比较(token 校验用) */
8033
- function safeEqual(a, b) {
8034
- const ab = Buffer.from(a);
8035
- const bb = Buffer.from(b);
8036
- return ab.length === bb.length && timingSafeEqual(ab, bb);
8037
- }
8038
8068
  /**
8039
8069
  * Origin 校验(S3):跨站 POST 被浏览器强制带 Origin(同源 GET/导航除外)。
8040
8070
  * 允许:① 同源(Origin.host === 网关自身 host)② 主端口 loopback(127.0.0.1:mainPort)。
@@ -8456,20 +8486,26 @@ function startGateway(config, getAccounts) {
8456
8486
  const remote = req.socket.remoteAddress ?? "unknown";
8457
8487
  const cookieCsrf = cookieValue(req.headers.cookie, "serenity_csrf");
8458
8488
  const formCsrf = csrfFromRequest(req, params);
8459
- if (cookieCsrf === void 0 || formCsrf === null || !safeEqual(cookieCsrf, formCsrf)) {
8460
- console.warn(`[serenity-hooks] 登录拒绝(CSRF 校验失败):user=${user} ip=${remote}`);
8461
- res.writeHead(403, { "content-type": "text/html; charset=utf-8" });
8462
- res.end(loginPageHtml("会话校验失败,请刷新页面重试"));
8489
+ if (cookieCsrf === void 0 || formCsrf === null || !isCsrfValid(formCsrf) || !isCsrfValid(cookieCsrf)) {
8490
+ console.warn(`[serenity-hooks] 登录拒绝(CSRF 校验失败):user=${user} ip=${remote} cookieSecure=${cookieSecure ? "on" : "off"} cookieCsrf=${cookieCsrf === void 0 ? "missing" : "present"} formCsrf=${formCsrf === null ? "missing" : "present"}`);
8491
+ const retryCsrf = newCsrfToken();
8492
+ res.writeHead(403, {
8493
+ "content-type": "text/html; charset=utf-8",
8494
+ "set-cookie": `serenity_csrf=${retryCsrf}; HttpOnly; SameSite=Strict; Path=/`
8495
+ });
8496
+ res.end(loginPageHtml("会话校验失败,请刷新页面重试" + (cookieSecure ? "(若仍失败:设置面板关闭 Secure Cookie——明文 HTTP 下必须关闭)" : ""), retryCsrf));
8463
8497
  return;
8464
8498
  }
8465
8499
  const lockedRemaining = accountLockRemaining(user);
8466
8500
  if (lockedRemaining > 0) {
8467
8501
  console.warn(`[serenity-hooks] 登录拒绝(账号锁定中):user=${user} ip=${remote} 剩余=${Math.ceil(lockedRemaining / 6e4)}min`);
8502
+ const retryCsrf = newCsrfToken();
8468
8503
  res.writeHead(429, {
8469
8504
  "content-type": "text/html; charset=utf-8",
8470
- "retry-after": String(Math.ceil(lockedRemaining / 1e3))
8505
+ "retry-after": String(Math.ceil(lockedRemaining / 1e3)),
8506
+ "set-cookie": `serenity_csrf=${retryCsrf}; HttpOnly; SameSite=Strict; Path=/`
8471
8507
  });
8472
- res.end(loginPageHtml(`账号已锁定,请 ${Math.ceil(lockedRemaining / 6e4)} 分钟后再试`));
8508
+ res.end(loginPageHtml(`账号已锁定,请 ${Math.ceil(lockedRemaining / 6e4)} 分钟后再试`, retryCsrf));
8473
8509
  return;
8474
8510
  }
8475
8511
  const account = getAccounts().find((a) => a.user === user);
@@ -8495,15 +8531,19 @@ function startGateway(config, getAccounts) {
8495
8531
  const lockMs = recordLoginFailure(user);
8496
8532
  console.warn(`[serenity-hooks] 登录失败: user=${user} ip=${remote}${lockMs > 0 ? ` 锁定=${Math.ceil(lockMs / 6e4)}min` : ""}`);
8497
8533
  const msg = lockMs > 0 ? `尝试过多,账号已锁定 ${Math.ceil(lockMs / 6e4)} 分钟` : "用户名、密码或验证码错误";
8498
- res.writeHead(401, { "content-type": "text/html; charset=utf-8" });
8499
- res.end(loginPageHtml(msg));
8534
+ const retryCsrf = newCsrfToken();
8535
+ res.writeHead(401, {
8536
+ "content-type": "text/html; charset=utf-8",
8537
+ "set-cookie": `serenity_csrf=${retryCsrf}; HttpOnly; SameSite=Strict; Path=/`
8538
+ });
8539
+ res.end(loginPageHtml(msg, retryCsrf));
8500
8540
  });
8501
8541
  return;
8502
8542
  }
8503
8543
  const csrf = newCsrfToken();
8504
8544
  res.writeHead(200, {
8505
8545
  "content-type": "text/html; charset=utf-8",
8506
- "set-cookie": `serenity_csrf=${csrf}; HttpOnly; SameSite=Strict; Path=/${cookieSecure ? "; Secure" : ""}`
8546
+ "set-cookie": `serenity_csrf=${csrf}; HttpOnly; SameSite=Strict; Path=/`
8507
8547
  });
8508
8548
  res.end(loginPageHtml("", csrf));
8509
8549
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.24.7",
3
+ "version": "1.24.9",
4
4
  "description": "宁静号 ACC harness — Native Cordis 插件(DeepSeek Harness 运行时)。真实 DSH 工具注册(cc_fs/session/acc_msm 等 9 工具)+ 拦截缝机械约束(safe-mode/路径守卫/会话落盘)+ 系统提示词注入(ACC/CCE/Constraints/SKILL/Session 五块)。适配 DSH 公开版(deepseek-ai/deepseek-harness 0.1.0-rc)。",
5
5
  "license": "MIT",
6
6
  "repository": {