@shgroup/dsh-serenity-hooks 1.24.8 → 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 +1 -1
- package/lib/index.js +55 -15
- package/package.json +1 -1
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-serenity-hooks",
|
|
3
|
-
"version": "1.24.
|
|
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
|
-
/**
|
|
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
|
-
|
|
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 || !
|
|
8489
|
+
if (cookieCsrf === void 0 || formCsrf === null || !isCsrfValid(formCsrf) || !isCsrfValid(cookieCsrf)) {
|
|
8460
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"}`);
|
|
8461
|
-
|
|
8462
|
-
res.
|
|
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,8 +8531,12 @@ 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
|
-
|
|
8499
|
-
res.
|
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.24.
|
|
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": {
|