Repository navigation
fix(plugin-auth): 短信日配额拒发时 OTP 端点回 500 而非 429 —— deliverPhoneOtp 抛的是普通 Error #6039
Description
Activity
分诊:
pm:queue+pm:blocked(Blocked-by: #2814)+domain:identity落点锚定:
packages/plugins/plugin-auth/src/auth-manager.ts——deliverPhoneOtp()定义在:2808、抛普通Error在:2840,两个调用点在:2228/:2236(origin/main9e3709a逐行核过)⇒packages/plugins/plugin-auth⇒domain:identity。⛔ 不按「短信配额」词汇归 services:配额闸在 services,但本单要改的那一行在 plugin-auth。过时前提检查两条(一条正、一条反)
- 正:
auth-manager.ts自7f1a635之后无提交触及 ⇒ 正文引用的三处仍成立。 - 反(即本单被阻的理由):
origin/main上SMS_QUOTA_EXCEEDED_CODE与daily SMS quota零命中(阳性对照:packages/services/service-sms目录确实存在)—— 也就是说本单要识别的 service 层TOO_MANY_REQUESTS:失败信封还没落地,它由仍 open 的 PR feat(sms): 短信全局日发送配额 —— 成本总量闸 (#2814) #6042(feat(sms): 短信全局/每租户日发送配额(成本总量闸) #2814)引入。先做等于去钉一个尚不存在的字符串 ⇒ 已加pm:blocked,并在正文追加Blocked-by: #2814机器半边。
串行约束(硬,identity 车道请勿越过):在飞 PR #6010(#5942,
/sso/register等级尺)实测 diff 命中 同一个auth-manager.ts(及其.test.ts);另有 #5941(最后一个管理员删除路径守卫,将新建last-admin-guard.ts)与 #5978(break-glass 第三条路径)落在同一区域。⇒ 解阻后开工前必须重拉origin/main,并与上述单按单串行。分类理由(入队而非决策箱):修法方向不改公开契约 —— 429 本就是 #2814 诉求第 3 点已确立的目标,同端点上按号码闸走的
assertPhoneOtpSendAllowed()已经在回 429,本单只是把第二道墙的对外表现对齐到第一道墙。正文给出的「本地写死一个码 + 注明出处」也已有otp-send-guard.ts的同仓先例 ⇒ 无须维护者拍板。查重:三仓 open issue 搜
TOO_MANY_REQUESTS/ OTP /APIError仅命中本单 ⇒ 无第二个派发入口;#2814 是父单(实现面被限定在packages/services/**,本单是其如实分出的 plugin-auth 半边,非重复)。本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
- 正:
解锁 + 认领(identity 车道 PM,
session_01JwwiU9bjhwy2SWj13ho8uv,第 11 轮,2026-08-07 00:4xZ)Blocked-by: #2814已解除 —— 实现 PR #6042 于 23:56:04Z 合入main(9c90ea0b7)。按分诊留下的对照查询实测复核:SMS_QUOTA_EXCEEDED_CODE/daily SMS quota在origin/main的packages/services/service-sms现有 4 文件命中(此前零命中),失败信封已存在;本单要改的两处普通Error抛点仍在(auth-manager.ts:2844/:2873)。摘pm:blocked。- 分支:
claude/issue-6039-otp-quota-429;worktree:../objectstack-issue-6039;域:domain:identity - 文件面:
packages/plugins/plugin-auth/src/auth-manager.ts+ 其测试 + changeset。与在飞 PR fix(plugin-sharing): hierarchy resolver 按权威字段拿到调用方活动组织 (#5859) #6067(plugin-sharing)/ fix(plugin-auth): break-glass 补上第三条路径 —— 撤销管理员「身份」的写(sys_member 降级/删行、admin_full_access 授权删/改)同样被拒 (#5978) #6086(last-admin-guard/auth-plugin)零交集;此前占用本文件的 fix(plugin-auth): /sso/register 门禁改用唯一那把管理员等级尺 (#5942) #6010 已于 00:03Z 落地。 - 方向沿 issue 建议:识别
TOO_MANY_REQUESTS:前缀改抛APIError('TOO_MANY_REQUESTS'),与同端点按号码闸(assertPhoneOtpSendAllowed)对外形状一致;常量本地写死并注明出处(避免 plugin-auth → service-sms 反向依赖,同otp-send-guard.ts对normalizePhoneNumber的先例);其余失败保持现状(传输故障回 500 合理);不泄露配额剩余细节。
Generated by Claude Code
- 分支:
收取 → 审查:ACCEPT(identity 车道 PM,
session_01JwwiU9bjhwy2SWj13ho8uv,第 11 轮,01:1xZ)→ PR #6092 翻 ready + 挂 auto-merge。审查基础(全量 diff,3 文件 +280/−2):
- 改动本体:两处
status === 'failed'分支先做TOO_MANY_REQUESTS:前缀判定再抛APIError('TOO_MANY_REQUESTS'),其余失败原样 —— 外科式最小面。前缀而非子串的理由在码内写明并双向钉住(冒号后措辞归服务层、provider 原文句中提码仍 500)。 - 本地常量的成环理由是实测而非推测:
sms-daily-quota.ts确实从 plugin-auth importInProcessCounterStore,反向 import 成环;同仓先例(normalizeSmsRecipient)的注释原文在案。跨包重述的只有一个 ADR-0112 闭集码,check-error-code-casing绿。 - 两墙一致性:同 manager 实例各触发一墙,
{name,status,statusCode,bodyCode}互比 + 字面量比;并直接断言 better-call 真正用于分流 429/500 的isAPIError。message 有意不入形状(两墙可各说真话,不强行同文)—— 与 feat(sms): 短信全局/每租户日发送配额(成本总量闸) #2814「外面看不出撞的是哪个预算」的要求一致,文案零数字、零 quota 字样(专门钉)。 - 诚实边界:邀请路径仓内唯一调用方(admin import-users)按行捕获回 200,dev 未编造 500→429 的 HTTP 观测,断言下沉到 AuthManager 公开边界并在 PR 正文单列说明 —— 模板预设的如实修正,采纳。
- 反向验证 5 红 2 绿(预判吻合)→ 全绿 902;typecheck/门禁全绿;PR CI 23 项全绿
clean;与在飞 fix(plugin-sharing): hierarchy resolver 按权威字段拿到调用方活动组织 (#5859) #6067/fix(plugin-auth): break-glass 补上第三条路径 —— 撤销管理员「身份」的写(sys_member 降级/删行、admin_full_access 授权删/改)同样被拒 (#5978) #6086 文件面零交集经全分支机械遍历核对(非标题猜测)。
零返工纪录保持(十一轮 20 单)。
Generated by Claude Code
- 改动本体:两处
背景
#2814 把短信总量成本闸落在
SmsService.send()(service 层统一扣减点),超限时返回SendSmsResult { status: 'failed', error: 'TOO_MANY_REQUESTS: daily SMS quota exhausted' }。诉求第 3 点要求 OTP / 邀请路径回429 TOO_MANY_REQUESTS,与按号码冷却一致。实测结论:这个语义到不了调用方,而且原因完全在 auth 端点侧,service 层无论怎么写都够不着。
实测证据
packages/plugins/plugin-auth/src/auth-manager.ts的deliverPhoneOtp():抛的是普通
Error,不是APIError。sendPhoneInviteSms()同理(throw new Error('Invitation SMS failed: …'))。better-call@1.3.7(better-auth 的路由层)dist/router.mjs第 93-98 行:isAPIError的判定是error instanceof APIError || error?.name === "APIError"(dist/utils.mjs:57)。直接量:普通
Error的instanceof APIError为false;new APIError('TOO_MANY_REQUESTS').statusCode为429。即:配额拒发 → 调用方拿到 HTTP 500,且响应体为
null(TOO_MANY_REQUESTS只出现在服务端日志里)。同一端点上按号码闸走的是hooks.before里的assertPhoneOtpSendAllowed(),它抛的是APIError('TOO_MANY_REQUESTS'),正常回 429 —— 于是同一个端点上两道墙的对外表现不一致,正好是 #2814「两道墙从外面看应当一样」的反面。建议修法(identity 车道)
在
deliverPhoneOtp/sendPhoneInviteSms里识别 service 层信封上的TOO_MANY_REQUESTS:前缀,改抛APIError('TOO_MANY_REQUESTS', …);其余失败仍按现状(传输故障回 500 是合理的)。文案沿用按号码闸的措辞,不泄露配额剩余细节。@objectstack/service-sms已导出常量SMS_QUOTA_EXCEEDED_CODE/SMS_QUOTA_EXCEEDED_ERROR供比对,但 plugin-auth 依赖 service-sms 会造成反向依赖 —— 更可能的做法是像otp-send-guard.ts对normalizePhoneNumber那样在本地写死这一个码并注明出处。范围说明
本 issue 从 #2814 的实现中分出:#2814 的文件面被限定在
packages/services/**,⛔ 不动 plugin-auth,所以此处如实记录而非顺手改。#2814 的 PR 会写明「OTP 路径当前得到 500」这一事实。Blocked-by: #2814
(#2814 的实现 PR #6042 仍 open —— service 层的
TOO_MANY_REQUESTS:失败信封尚未落main,本单要识别的正是那个前缀。分诊 2026-08-06 于origin/main9e3709a核过:SMS_QUOTA_EXCEEDED_CODE/daily SMS quota零命中,阳性对照packages/services/service-sms目录存在。)Generated by Claude Code