test(plugin-auth): 证明 ADR-0069 D4 真能撤销活动会话,并把「会话的记录之处」定为 sys_session (#4785) - #5101
Merged
Conversation
…s_session as the session of record (#4785) The maintainer's ruling on #4785: the session of record is ALWAYS `sys_session` (the database); the kernel `cache` service serves auth as the rate-limit counter store only. Dual-write (`session.storeSessionInDatabase: true`) is rejected. No runtime change — this records the decision and proves the behaviour that depends on it. `session-of-record.test.ts` is the deliverable the conflict was hiding behind: ADR-0069 D4 declares three session controls (idle timeout, absolute lifetime, concurrent cap) that all revoke by writing the `sys_session` row, and nothing asserted that write actually ends a LIVE session. Every test here therefore asserts de-authentication of a real session cookie through the real better-auth pipeline, not that a column was stamped — a stamped row nobody reads is exactly the failure mode #4785 describes. Verified by mutation: neutering either enforcement path, or stamping `revoked_at` without expiring `expires_at`, turns the corresponding tests red. Two facts found while writing it, both pinned: - `AuthManager` never plumbs `storeSessionInDatabase`, so the rejected dual-write shape is unreachable through configuration; - the default composition (OIDC provider on) makes better-auth REFUSE to boot with a `secondaryStorage` rather than degrade quietly — so the standard `serve` cannot silently reach the broken architecture. ADR-0069: D2's "shared store" scoped to rate-limit counters, with a cache-backed session store named as a NEW decision requiring its own revocation-consistency requirements; D4 records `sys_session` as a precondition rather than a deployment preference; status lines made factual. `content/docs/kernel/contracts/cache-service.mdx` no longer lists session storage among the cache's uses, and says what `cacheSecondaryStorage()` costs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015W6nhsDrz6zWQc8je12a1t
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckNo hand-written docs reference the 0 changed package(s). ✅ |
os-zhuang
marked this pull request as ready for review
August 4, 2026 04:39
os-zhuang
enabled auto-merge
August 4, 2026 04:39
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4785
按维护者 2026-08-04 的裁定落地方案 A:会话的记录之处永远是
sys_session(数据库);cache 之于 auth 只是限流计数器。方案 C(双写)明确否决。运行时零改动 —— 本 PR 做的是:把决策记录下来,并证明依赖这条决策的行为真的成立。
核心交付:那条一直缺席的端到端测试
packages/plugins/plugin-auth/src/session-of-record.test.ts(新增,12 个用例)。ADR-0069 D4 声明了三个会话管控(空闲超时 / 绝对时长 / 并发上限),三者都靠写
sys_session行来撤销,而此前没有任何测试断言过「这次写真的让一个活动会话失效了」—— 顶多断言了某个列被盖上了戳。盖了戳却没人读的行,正是 #4785 描述的失效形态,所以每个用例断言的都是链路的终点:一个带真实会话 cookie 的请求不再是已认证状态,而不是某列被写了。沿用 #3585 EdDSA 测试确立的范式:跑真实的 better-auth 管线(真实
Request→AuthManager.handleRequest→ better-auth 签发的真实 cookie),不 stub 我们自己的代码。覆盖:
sys_session行;把行删掉,cookie 立刻失认证 —— 这条反向断言才真正证明「读的是数据库」。revoke_reason各不相同,且expires_at确实被写到过去。变异测试验证过它不是空转(都能变红):
enforceSessionControls/enforceConcurrentCap改成 no-op → 4 个用例失败;revoked_at而不把expires_at写到过去(即「盖戳但读路径不看」)→ 2 个用例失败。顺带查实的两个事实(都已钉住)
排查中发现,这个洞比 issue 正文估计的更难被误踩,两点都写成了测试:
AuthManager根本不透传session.storeSessionInDatabase—— 它构造 better-auth 的session块时只取expiresIn/updateAge。被否决的方案 C 在配置层面就不可达:宿主即使传了这个标志,拿到的仍是 cache-only 形态。secondaryStorage却没有storeSessionInDatabase时直接拒绝启动。加上第 1 点,标准serve组合根本无法悄悄走到出问题的架构上 —— 是响亮的启动失败,不是静默降级(符合 AGENTS.md「Absence must be loud」)。D4 本身有没有真缺陷?没有
按任务要求核过了:在裁定的架构(DB 为记录之处)下三个管控都真的生效。读路径确实认写进去的过去
expires_at(better-authgetSession第 190 行判expiresAt < now),且 ObjectStack 没有开启session.cookieCache,所以不存在「撤销了但 cookie 缓存还认」的窗口。auth-manager.ts未作任何修改。一个值得记录的语义(已写进 ADR 与测试注释):空闲/绝对两项在
customSession里执行,而它跑在本次请求的会话已被校验之后,所以发现超时的那次请求仍然成功,下一次才失认证;并发上限在登录 after-hook 里跑,立即生效。测试把这个一次请求的延迟钉住了 —— 免得有人为了「修掉延迟」把检查挪到撤销写不再发生的地方。文档侧
docs/adr/0069-*.mdrateLimit.customStorage,不是secondaryStorage(后者会连带搬走会话记录之处);并写明若将来要把会话搬进缓存,那是一条新决策,必须在同一条 ADR 里给出撤销一致性要求(撤销如何让每个节点的缓存快照失效,失效失败时怎么办)。sys_session是 D4 的前置条件,不是部署偏好;点明真正起撤销作用的是expires_at(revoked_at/revoke_reason是诊断用),只盖后两者的撤销是无效的。content/docs/kernel/contracts/cache-service.mdx(按裁定只改开头那一段):从 cache 的用途列表里去掉 "session storage",改为写实 —— 会话记录之处是sys_session;宿主可以用cacheSecondaryStorage()显式选用,但选用即停用 D4 三个会话管控,被撤销的会话会一直可用到缓存副本过期。auth-plugin.test.ts那条「cache 不绑 secondaryStorage」的钉扎测试保留,加注指向新测试 —— 原注释说的是「我们怎么做」,新测试说的是「为什么这件事重要」。验证
已合入
origin/main(合入内容只动packages/lint,与本 PR 不相交),合后重跑上述用例仍全绿。范围
packages/plugins/plugin-auth/**(纯测试)+docs/adr/0069-*.md+cache-service.mdx开头一段 + changeset。packages/spec/**与content/docs/releases/零改动。一处可选的后续(未做,因超出认领的文件面):可以给
secondary-storage.ts在scripts/adr-anchors.json里加一条 ADR-0069 锚点。目前 ADR 正文点名了测试文件、测试文件通篇点名 ADR-0069 D4,双向可发现性已经成立,check:adr-anchors也是绿的。Generated by Claude Code