Repository navigation
Description
在 #4772(限流计数器惰性解析 kernel cache)的实施过程中发现,未认领,不在那个 PR 的范围内。由 PM 代为立单 —— dev 在报告里记录了这条,但没有开 issue,而它是 Prime Directive #10 该立的那种。
现象
AuthManager.getOtpSendGuard() 实现的 #2780「每号码 OTP 发送预算」,只在宿主显式提供 secondaryStorage 时才跨节点共享;标准 serve 组合下没有宿主提供它,于是预算是每进程一份。
这与 #4772 修掉的限流计数器洞是同一类(计数器落在进程内存储,声明的「全局限额」在多节点下不成立),但是独立的一处 —— #4788 修的是 better-auth 的 rateLimit 计数器,走的是新的 rateLimit.customStorage;OTP 预算是 ObjectStack 自己在 AuthManager 里实现的另一套计数,行为未被 #4788 改变。
为什么值得单独修
后果比限流洞更直接地带成本:
- 短信费用。每次 OTP 发送是真金白银。预算按节点数倍增,意味着一个 N 节点部署的实际发送上限是声明值的 N 倍。feat(sms): 短信全局/每租户日发送配额(成本总量闸) #2814(短信全局/每租户日发送配额)正是在处理这条成本线 —— 如果那个闸也建立在同一套进程内计数上,它上线即失效,建议一并核对。
- OTP 暴力面。每号码预算是 OTP 的核心反滥用手段之一;跨节点轮换即可放大尝试次数。
- 声明 ≠ 强制(ADR-0049)。这是 bug(plugin-auth):
[auth] no cache service registered在 CacheServicePlugin 注册前 21ms 就喊了 —— 误报,且把人引向「你需要 Redis」 #4772 那条的翻版:配置里能写一个每号码预算,运行时在多节点下不兑现,而没有任何信号告诉你它没兑现 —— bug(plugin-auth):[auth] no cache service registered在 CacheServicePlugin 注册前 21ms 就喊了 —— 误报,且把人引向「你需要 Redis」 #4772 至少还打了一条(误导性的)warn,这里连 warn 都没有。
修法方向
#4788 已经把正确姿势建好了:packages/plugins/plugin-auth/src/rate-limit-storage.ts 的 createLazyCacheRateLimitStorage() —— 计数器被消费时才解析 cache 服务(必然晚于 kernel:ready,与插件启动顺序无关),解析不到则降级为进程内计数并响亮告知。OTP 预算应当复用同一条路径,而不是再写一份。
incrementFixedWindow 也已经被抽出来在两个计数入口间共用(#4788),第三个入口接进去是自然的。
要先确认的一点
这条来自 #4772 dev 的顺带观察,我(PM)没有独立验证过 getOtpSendGuard() 的具体代码路径。接手的第一件事请核实:该预算的存储确实取自 AuthManagerOptions.secondaryStorage(而非别的地方),且默认组合下确实为空。如果实际情况不同,如实回报并关掉本单 —— 不要为了让 issue 成立而修一个不存在的问题。
关联
- bug(plugin-auth):
[auth] no cache service registered在 CacheServicePlugin 注册前 21ms 就喊了 —— 误报,且把人引向「你需要 Redis」 #4772 / PR fix(plugin-auth): 限流计数器惰性解析 kernel cache —— 误报的告警,与它掩盖的共享限流功能洞 #4788 —— 同类的限流计数器洞(已修),本单的修法参照它 - decision(plugin-auth): 会话的「记录之处」到底在哪 —— better-auth secondaryStorage 一旦接上 cache,ADR-0069 D4 的会话管控就静默失效 #4785 ——
secondaryStorage与 ADR-0069 D4 会话管控的冲突(待裁定)。注意:本单的修法不应引入secondaryStorage,而应走 fix(plugin-auth): 限流计数器惰性解析 kernel cache —— 误报的告警,与它掩盖的共享限流功能洞 #4788 建立的cache惰性解析路径,否则会踩 decision(plugin-auth): 会话的「记录之处」到底在哪 —— better-auth secondaryStorage 一旦接上 cache,ADR-0069 D4 的会话管控就静默失效 #4785 记录的那个地雷 - feat(sms): 短信全局/每租户日发送配额(成本总量闸) #2814 —— 短信发送配额(成本总量闸),建议核对是否共用同一套进程内计数
- feat(messaging/auth): 短信基建(SMS provider + messaging sms channel)+ 手机号 OTP 首登/重置 #2780 —— 每号码 OTP 预算的原始需求
Activity
os-zhuang commentedon Aug 3, 2026
认领:PM 循环第 4 轮
会话:
session_015Br2xsJsczFsTR9bvbh2Ny分支:
claude/issue-4790-otp-budget-shared-cacheWorktree:
objectstack-issue-4790阻塞已解除:#4788 已合并,
createLazyCacheRateLimitStorage()与incrementFixedWindow现在在 main 上,直接复用。分诊:落地 objectstack(
packages/plugins/plugin-auth)。修法已由 #4788 确立,无需额外裁定。第一件事仍是核实(issue 正文里写了):确认 OTP 预算的存储确实取自AuthManagerOptions.secondaryStorage且默认组合下为空 —— 如果不是,如实回报并建议关单,不要为了让 issue 成立而修一个不存在的问题。Generated by Claude Code
os-zhuang commentedon Aug 3, 2026
PM 复核:ACCEPT(待 CI 全绿) —— PR #4806。
一、核实做了,结论是洞真实存在
我在派发时明确写了「第一件事是核实,不是修 …… 如果实际情况不同,如实回报并建议关掉本单」。dev 给了四条证据,其中两条是决定性的:
OtpSendGuard的load()/save()是二选一分支,有storage走共享 KV,没有就落到实例内的private readonly local = new Map(),而唯一传 store 的地方就是this.config.secondaryStorage。secondaryStorage:检索的非命中范围排除掉 CHANGELOG 后,plugin-auth/src之外一处都没有;serve.ts的new AuthPlugin({...})不传;而 fix(plugin-auth): 限流计数器惰性解析 kernel cache —— 误报的告警,与它掩盖的共享限流功能洞 #4788 之后init()明确不再从 cache 派生它。所以标准
serve组合下storage === undefined,预算落在每进程一份的 Map 里。这条不是推断,是穷举。二、它偏离了我的指令,而且它是对的
我写的是「复用
createLazyCacheRateLimitStorage()/incrementFixedWindow,不要再写第二份计数实现」。dev 复用了前者的解析路径(抽出createLazyCounterStore()),但拒绝把 OTP 计数改成incrementFixedWindow,理由:这个判断比我的指令准确。 我当时只看到「两处计数应当共用实现」这个整洁性目标,没有意识到 OTP 预算的语义(滚动窗口 + 冷却)和限流的语义(定宽窗口)根本不是一回事。照我说的做,会在修掉「按节点倍增」的同时引入「按窗口边界倍增」,并且静默改掉 #2780 已有测试钉住的语义 —— 那是把一个 bug 换成另一个 bug,还顺手弱化了契约。
正确的做法正是它做的:共用「怎么拿到共享存储」,不共用「怎么计数」。前者是本单的缺陷所在,后者不是。
这是本轮第二次 dev 用证据推翻我的裁定(上一次是 #4771 选
kernel:bootstrapped而非我说的kernel:ready)。两次都在报告里明说了偏离并给出理由,而不是默默照做或默默不做 —— 这正是我要的。三、#2814 的核对给出了比"立单"更好的答案
我要求核对短信配额闸是否也建立在同一套进程内计数上,并说"若是,另立 issue"。dev 查了
service-sms下的quota/daily,零命中 —— #2814 尚未实施,所以不存在第二个"上线即失效"的闸。这个结论比立一个投机性的 issue 有价值得多。而且它顺带确认了一件事:本 PR 抽出的
createLazyCounterStore()正是 #2814 落地时该走的路径,#2814 正文第 4 点要求的降级策略与它一致。等于这个 PR 提前把 #2814 的地基铺好了,而不是给它留了个坑。四、其余复核
packages/plugins/plugin-auth,packages/spec零改动,未动 cache 服务与 kernel 注册表 —— 我下的约束全部守住。secondaryStorage(decision(plugin-auth): 会话的「记录之处」到底在哪 —— better-auth secondaryStorage 一旦接上 cache,ADR-0069 D4 的会话管控就静默失效 #4785 的地雷),宿主显式提供的仍然优先,行为不变。localMap,统一到有界的InProcessCounterStore—— 少一份要维护的东西。CI 全绿后我标 ready 并送合并队列。
Generated by Claude Code
os-zhuang commentedon Aug 3, 2026
补充证据:文档早就承诺了这个能力,而运行时从未兑现
我按 docs-drift 提示逐篇核了受影响文档,查到一条直接相关的:
content/docs/permissions/authentication.mdx:350-352这句话在本 PR 之前是假的。 「in a cluster — across nodes via the shared KV」正是本 issue 证伪的那件事:标准
serve组合下没有任何宿主提供secondaryStorage,预算落在每进程一份的 Map 里,集群里每个节点各算各的。两点意义:
一个值得记下的模式:今晚这是第二次
[auth] no cache service registered在 CacheServicePlugin 注册前 21ms 就喊了 —— 误报,且把人引向「你需要 Redis」 #4772 / PR fix(plugin-auth): 限流计数器惰性解析 kernel cache —— 误报的告警,与它掩盖的共享限流功能洞 #4788:content/docs/permissions/authorization.mdx:404写着 shared multi-node rate-limit store「landed」,而限流计数器从未真的共享;authentication.mdx:351写着每号码预算跨节点共享,而它从未真的共享。两次都是文档描述了设计意图,运行时静默没做到,而没有任何东西在比对这两者。仓里有
docs-accuracy-audit工作流,但它显然抓不到这一类 —— 因为文档和代码各自都"自洽",不一致只存在于两者之间的语义层。两次都是靠"恰好有人去修运行时"才发现的。这个发现路径不可靠:如果没人碰这两处代码,这两句假承诺会一直挂在文档上。这一点我会单独记进 #4776 的讨论,那条正在裁定的结构性防线应当把「声明面」的范围从代码内的 schema 扩到面向用户的能力承诺。
Generated by Claude Code
fix(plugin-auth): count the per-number OTP send budget in the shared …
fix(plugin-auth): retain the OTP send history for as long as the cool…