fix: harden auth middleware against transient db errors
This commit is contained in:
@@ -7,8 +7,8 @@
|
||||
- 生产运行目录: `/Users/john/Project/Memind`
|
||||
- 干净同步分支: `/Users/john/Project/memind-clean-main-20260702`
|
||||
- 分支: `memind-streaming-runtime-20260702`
|
||||
- 最新提交: `212e163 feat: add code run rollout policy gates`
|
||||
- 最新 release: `20260702-104734-212e163`
|
||||
- 最新提交: 本文档所在分支 HEAD,提交信息 `fix: harden auth middleware against transient db errors`
|
||||
- 最新 release: `20260702-104734-212e163` 后续改造以生产备份和分支提交方式推进。
|
||||
- 当前公网入口: `https://mm.tkmind.cn`
|
||||
|
||||
## 总体结论
|
||||
@@ -19,12 +19,12 @@
|
||||
- Stream Controller 已具备 SSE headers、abort propagation、backpressure pipeline。
|
||||
- Redis Router 已启用,承担 worker runtime state。
|
||||
- Goose Worker Pool 已从固定单点走向四 worker 可观测调度。
|
||||
- Aider/OpenHands 已从普通聊天默认能力中剥离,进入 code mode 和后端灰度门禁;P6.3 已把 code run 从 goosed session extension 外移到 `agent-run-v1` Tool Gateway 协议,P6.4 已完成 Aider 真实执行 canary,P6.5 已完成 OpenHands 真实执行 canary,P6.6 已完成 external worker 精确接管 code-run canary,P6.7 已加入 Tool Gateway 产物校验与输出审计,P6.8 已安装 external worker LaunchAgent,P6.9 已完成带 validation 的 external worker 灰度 canary,P6.10 已加入 external worker 只读观测脚本,P6.11 已加入放量策略门禁,P6.12 已开启全用户长期灰度并通过普通测试用户真实路径,P6.13 已安装自动暂停 guard,P6.15 已让 H5 code-run 自动补 receipt validation 并恢复后端 required validation,P6.17/P6.18 已把 external worker 并发 2 通过 canary 并固化为当前 all-user gray 策略,P8.1 已加入 stale running recovery。
|
||||
- Aider/OpenHands 已从普通聊天默认能力中剥离,进入 code mode 和后端灰度门禁;P6.3 已把 code run 从 goosed session extension 外移到 `agent-run-v1` Tool Gateway 协议,P6.4 已完成 Aider 真实执行 canary,P6.5 已完成 OpenHands 真实执行 canary,P6.6 已完成 external worker 精确接管 code-run canary,P6.7 已加入 Tool Gateway 产物校验与输出审计,P6.8 已安装 external worker LaunchAgent,P6.9 已完成带 validation 的 external worker 灰度 canary,P6.10 已加入 external worker 只读观测脚本,P6.11 已加入放量策略门禁,P6.12 已开启全用户长期灰度并通过普通测试用户真实路径,P6.13 已安装自动暂停 guard,P6.15 已让 H5 code-run 自动补 receipt validation 并恢复后端 required validation,P6.17/P6.18 已把 external worker 并发 2 通过 canary 并固化为当前 all-user gray 策略,P8.1 已加入 stale running recovery,P8.2 已完成 Portal DB/Auth 瞬时错误兜底。
|
||||
- PG 和 MindSpace 仍保持生产数据边界,SLO 报告只做统计读取;P6.3-P6.15 不新增 schema migration,不删除或修改既有用户数据。
|
||||
|
||||
整体执行评分: 9.95 / 10。
|
||||
|
||||
可以支撑当前 H5 streaming 稳定性改造的基础目标。自动采样、worker sidecar heartbeat、SLO 只读快照、SLO 日报定时器、SLO 日报保留策略、first-token EWMA、first-token p50/p95 窗口趋势、Tool Gateway Queue v0、外部 worker 接管入口、真实 worker canary、后端 code-mode canary、code-run 用户级灰度 gate、H5 页面编辑 UI canary、P6.3 Tool Gateway 协议化、P6.4 Aider 真实 canary、P6.5 OpenHands 真实 canary、P6.6 external worker code-run canary、P6.7 Tool Gateway guardrails、P6.8 worker LaunchAgent、P6.9 validated external worker canary、P6.10 worker observability、P6.11 rollout policy gates、P6.12 all-user gray、P6.13 auto-pause guard、P6.15 H5 validation metadata、P6.17/P6.18 worker concurrency 2 和 P8.1 stale running recovery 已经落地。主要剩余差距转为 DB/session auth 瞬时错误收敛、更细粒度的任务级产物规范和用户可见进度/失败说明。
|
||||
可以支撑当前 H5 streaming 稳定性改造的基础目标。自动采样、worker sidecar heartbeat、SLO 只读快照、SLO 日报定时器、SLO 日报保留策略、first-token EWMA、first-token p50/p95 窗口趋势、Tool Gateway Queue v0、外部 worker 接管入口、真实 worker canary、后端 code-mode canary、code-run 用户级灰度 gate、H5 页面编辑 UI canary、P6.3 Tool Gateway 协议化、P6.4 Aider 真实 canary、P6.5 OpenHands 真实 canary、P6.6 external worker code-run canary、P6.7 Tool Gateway guardrails、P6.8 worker LaunchAgent、P6.9 validated external worker canary、P6.10 worker observability、P6.11 rollout policy gates、P6.12 all-user gray、P6.13 auto-pause guard、P6.15 H5 validation metadata、P6.17/P6.18 worker concurrency 2、P8.1 stale running recovery 和 P8.2 Portal DB/Auth transient hardening 已经落地。主要剩余差距转为更细粒度的任务级产物规范和用户可见进度/失败说明。
|
||||
|
||||
## 实测结果
|
||||
|
||||
@@ -916,6 +916,31 @@ Data boundary:
|
||||
- guard `shouldPause=false`
|
||||
- SLO `ok=true`, `failures=[]`
|
||||
|
||||
### P8.2 Portal DB/Auth Transient Error Hardening
|
||||
|
||||
结果: 通过,session attach、`/auth/status` 和 API auth middleware 已捕获 DB/auth 瞬时错误并收敛为 503,不再让异常穿透 Express middleware。
|
||||
|
||||
- `attachUserSession()` 捕获 `userAuth.verify()` 异常,记录 `req.userSessionError`。
|
||||
- `/auth/status` 捕获用户态查询/能力解析异常,返回 `503 mode=unavailable`。
|
||||
- API auth middleware:
|
||||
- protected route 遇到 session verify transient error 返回 503。
|
||||
- plaza public read 在 session attach 失败时继续放行。
|
||||
- 后续 `getMe()` transient error 返回 503。
|
||||
- 测试:
|
||||
- `node --check server.mjs`
|
||||
- `node --test auth.test.mjs user-auth.test.mjs admin-guard.test.mjs`,23 tests pass。
|
||||
- 生产部署:
|
||||
- backup `/Users/john/Project/memind_backups/20260702-121438-p82-auth-transient-hardening`
|
||||
- bundled `server.mjs`
|
||||
- restarted `cn.tkmind.memind-portal`
|
||||
- live 验证:
|
||||
- `/api/status` 返回 `ok`
|
||||
- `/auth/status` 未登录返回 `200 {"authenticated":false,"mode":"user"}`
|
||||
- runtime/status 仍为 all-user code-run gray、`requireValidation=true`、`maxConcurrentRuns=2`
|
||||
- worker running `--limit 2`
|
||||
- guard `shouldPause=false`
|
||||
- SLO `ok=true`, `failures=[]`
|
||||
|
||||
## 下一步执行建议
|
||||
|
||||
### P6.19 Task-level Artifact Validation
|
||||
@@ -924,12 +949,6 @@ Data boundary:
|
||||
- 页面编辑任务追加目标 `public/*.html` 校验。
|
||||
- 文件/仓库任务根据任务参数声明目标相对路径。
|
||||
|
||||
### P8.2 Portal DB/Auth Transient Error Hardening
|
||||
|
||||
- `read EADDRNOTAVAIL` 不应作为 uncaught exception 杀掉 Portal。
|
||||
- session attach / auth verify 的 DB 瞬时错误应返回 503/401 或跳过认证态。
|
||||
- 增加回归测试,证明 DB read transient error 不会触发进程级 unhandled rejection。
|
||||
|
||||
### P6.22 Task Artifact UX and Failure Messages
|
||||
|
||||
- 用户侧显示 queued/running/validation/succeeded/failed。
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
- P6.17 Controlled Worker Concurrency Canary: 已完成,external worker 短窗口提升到并发 2,两条真实 code-run 同时 running 并全部通过 receipt validation。
|
||||
- P6.18 Concurrency Rollout Policy: 已完成,生产 worker 和 Portal runtime/status 已对齐到并发 2,worker KeepAlive 保持开启,保留回滚备份。
|
||||
- P8.1 Queue Lease / Stuck Run Recovery: 已完成第一步,worker dispatch 前自动回收超时 running run,新增 `--recover-stale` dry-run/apply 运维入口,生产 synthetic stale run 验证通过。
|
||||
- P8.2 Portal DB/Auth Transient Error Hardening: 已完成第一步,session attach、`/auth/status` 和 API auth middleware 已捕获 DB/auth 瞬时错误,生产部署后 live health、auth/status、runtime/status、guard 和 SLO 验证通过。
|
||||
- P5.15 Active Stream TTL Reconcile: 已按用户要求跳过,暂不做报表/定时 reconcile。
|
||||
- P5 Worker Pool 运维化: 已完成第一步,Redis Router 支持 worker drain。
|
||||
- P5.9 First-token Latency EWMA: 已完成,StreamController 会把首个 SSE chunk 延迟写入 Redis,SLO 报告已展示。
|
||||
@@ -2805,6 +2806,63 @@ runtime/status:
|
||||
- guard `shouldPause=false`
|
||||
- SLO `ok=true`, `failures=[]`
|
||||
|
||||
### 2026-07-02 P8.2 Portal DB/Auth Transient Error Hardening
|
||||
|
||||
目标:
|
||||
|
||||
- 防止 session verify / auth status / API auth 中的 MySQL 或 socket 瞬时错误扩大为进程级异常。
|
||||
- 将认证依赖临时不可用时的用户可见结果收敛为 503,而不是 uncaught rejection 或 Portal 退出。
|
||||
- 保持 plaza public read 路径在 session attach 瞬时失败时仍可继续读取。
|
||||
|
||||
改动:
|
||||
|
||||
- `attachUserSession()`:
|
||||
- 捕获 `userAuth.verify(token)` 异常。
|
||||
- 写入 `req.userSessionError`,记录 `[Auth] session verify failed`。
|
||||
- 不让异常穿透 Express middleware。
|
||||
- `/auth/status`:
|
||||
- 捕获 `getMe()`、`getUserById()`、`resolveUserCapabilities()` 异常。
|
||||
- 返回 `503 { authenticated:false, mode:'unavailable' }`。
|
||||
- API auth middleware:
|
||||
- 对 `req.userSessionError` 返回 503。
|
||||
- 对 public plaza read 继续放行。
|
||||
- 捕获后续 `getMe()` 异常并返回 503。
|
||||
|
||||
测试:
|
||||
|
||||
- `node --check server.mjs` 通过。
|
||||
- `node --test auth.test.mjs user-auth.test.mjs admin-guard.test.mjs` 通过,23 tests pass。
|
||||
|
||||
生产部署:
|
||||
|
||||
- 备份:
|
||||
- `/Users/john/Project/memind_backups/20260702-121438-p82-auth-transient-hardening`
|
||||
- 已部署:
|
||||
- bundled `server.mjs`
|
||||
- 已重启:
|
||||
- `cn.tkmind.memind-portal`
|
||||
|
||||
生产验证:
|
||||
|
||||
- `https://mm.tkmind.cn/api/status` 返回 `ok`。
|
||||
- `https://mm.tkmind.cn/auth/status` 未登录返回 `200 {"authenticated":false,"mode":"user"}`。
|
||||
- `/api/runtime/status.toolRuntime`:
|
||||
- `codeRunsEnabled=true`
|
||||
- `requireValidation=true`
|
||||
- `maxConcurrentRuns=2`
|
||||
- `oldestRunningStartedAt=null`
|
||||
- `oldestRunningAgeMs=0`
|
||||
- worker LaunchAgent running:
|
||||
- `--limit 2`
|
||||
- `MEMIND_TOOL_GATEWAY_ENABLED=1`
|
||||
- `MEMIND_AGENT_RUN_QUEUE_CONCURRENCY=2`
|
||||
- guard dry-run:
|
||||
- `ok=true`
|
||||
- `shouldPause=false`
|
||||
- SLO:
|
||||
- `ok=true`
|
||||
- `failures=[]`
|
||||
|
||||
## 回滚策略
|
||||
|
||||
- P0: 修改前保留 `server.mjs` 备份;如启动失败,恢复备份并 `launchctl kickstart` Portal。
|
||||
|
||||
+48
-23
@@ -689,7 +689,14 @@ async function attachUserSession(req, _res, next) {
|
||||
if (!userAuth) return next();
|
||||
const token = userToken(req);
|
||||
req.userToken = token;
|
||||
req.userSession = token ? await userAuth.verify(token) : null;
|
||||
try {
|
||||
req.userSession = token ? await userAuth.verify(token) : null;
|
||||
req.userSessionError = null;
|
||||
} catch (err) {
|
||||
req.userSession = null;
|
||||
req.userSessionError = err;
|
||||
console.error('[Auth] session verify failed:', err instanceof Error ? err.message : err);
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -700,18 +707,27 @@ app.use(attachUserSession);
|
||||
app.get('/auth/status', async (req, res) => {
|
||||
await userAuthReady;
|
||||
if (userAuth) {
|
||||
const me = await userAuth.getMe(userToken(req));
|
||||
if (!me) return res.json({ authenticated: false, mode: 'user' });
|
||||
const row = await userAuth.getUserById(me.id);
|
||||
const capabilityState = await userAuth.resolveUserCapabilities(row);
|
||||
return res.json({
|
||||
authenticated: true,
|
||||
user: me,
|
||||
mode: 'user',
|
||||
capabilities: capabilityState.capabilities,
|
||||
grantedSkills: capabilityState.grantedSkills ?? [],
|
||||
unrestricted: capabilityState.unrestricted,
|
||||
});
|
||||
try {
|
||||
const me = await userAuth.getMe(userToken(req));
|
||||
if (!me) return res.json({ authenticated: false, mode: 'user' });
|
||||
const row = await userAuth.getUserById(me.id);
|
||||
const capabilityState = await userAuth.resolveUserCapabilities(row);
|
||||
return res.json({
|
||||
authenticated: true,
|
||||
user: me,
|
||||
mode: 'user',
|
||||
capabilities: capabilityState.capabilities,
|
||||
grantedSkills: capabilityState.grantedSkills ?? [],
|
||||
unrestricted: capabilityState.unrestricted,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[Auth] status failed:', err instanceof Error ? err.message : err);
|
||||
return res.status(503).json({
|
||||
authenticated: false,
|
||||
mode: 'unavailable',
|
||||
message: '用户认证服务不可用,请稍后重试',
|
||||
});
|
||||
}
|
||||
}
|
||||
if (isDatabaseConfigured()) {
|
||||
return res.status(503).json({
|
||||
@@ -1742,18 +1758,27 @@ api.use(async (req, res, next) => {
|
||||
const plazaPublic = isPlazaPublicRead(req.path, req.method);
|
||||
|
||||
if (userAuth && tkmindProxy) {
|
||||
if (req.userSession) {
|
||||
if (req.userSessionError) {
|
||||
if (plazaPublic) return next();
|
||||
return res.status(503).json({ message: '用户认证服务不可用,请稍后重试' });
|
||||
}
|
||||
try {
|
||||
if (req.userSession) {
|
||||
const me = await userAuth.getMe(req.userToken);
|
||||
if (me) req.currentUser = me;
|
||||
}
|
||||
if (plazaPublic) return next();
|
||||
if (!req.userSession) {
|
||||
return res.status(401).json({ message: '未授权,请重新登录' });
|
||||
}
|
||||
const me = await userAuth.getMe(req.userToken);
|
||||
if (me) req.currentUser = me;
|
||||
if (!me) return res.status(401).json({ message: '登录已过期' });
|
||||
req.currentUser = me;
|
||||
return next();
|
||||
} catch (err) {
|
||||
console.error('[Auth] API auth failed:', err instanceof Error ? err.message : err);
|
||||
return res.status(503).json({ message: '用户认证服务不可用,请稍后重试' });
|
||||
}
|
||||
if (plazaPublic) return next();
|
||||
if (!req.userSession) {
|
||||
return res.status(401).json({ message: '未授权,请重新登录' });
|
||||
}
|
||||
const me = await userAuth.getMe(req.userToken);
|
||||
if (!me) return res.status(401).json({ message: '登录已过期' });
|
||||
req.currentUser = me;
|
||||
return next();
|
||||
}
|
||||
|
||||
if (legacyAuth?.verify(legacySessionToken(req))) return next();
|
||||
|
||||
Reference in New Issue
Block a user