feat(messaging/auth): 短信基建(SMS provider + messaging sms channel)+ 手机号 OTP 首登/重置 (#2780)#2790
Merged
Merged
Conversation
…in/reset (#2780) - @objectstack/plugin-sms: ISmsService + pluggable transports (Aliyun SMS with ACS3-HMAC-SHA256 signing, Twilio, dev log fallback), bound to the new `sms` settings namespace (live rebind + send-test action). Deliberately no message persistence and no body logging - SMS bodies carry OTP codes. - spec: ISmsService/ISmsTransport contracts; phoneNumber/phoneNumberOtp feature flags in the public auth-config schema. - service-messaging: pluggable `sms` channel (recipient user id -> sys_user.phone_number, (topic,'sms',locale) template rendering), registered at kernel:ready when an `sms` service is present, so notify(channels:['sms']) delivers. - plugin-auth: phoneNumber plugin's sendOTP/sendPasswordResetOTP now deliver through the sms service, opening /phone-number/send-otp + /verify and /phone-number/request-password-reset + /reset-password; without a deliverable service the endpoints keep failing loudly (NOT_SUPPORTED). Security posture shipped with the feature: explicit allowedAttempts=3, always-on per-number cooldown (60s) + rolling-hour cap (5) via OtpSendGuard (shared secondaryStorage when clustered, fail-open), /phone-number/* added to the settings-bound per-IP rate-limit rules, and OTP codes never reach logs or error messages. - /admin/import-users: the invite policy gains an SMS variant - phone-only rows get a credential-free invitation SMS (first sign-in via phone OTP, then self-set password); mixed files validate the reachable channel per row instead of rejecting the whole request. - cli: `sms` capability added to the always-on slate (log fallback until a provider is configured; config.sms / OS_SMS_* respected). Closes #2780 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013LXUXU66dBaP3SSG4ZVtuH
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 5 package(s): 101 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
Validate Package Dependencies requires every public workspace package in .changeset/config.json's "fixed" group (lockstep versioning). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013LXUXU66dBaP3SSG4ZVtuH
7 additive exports (ISmsService/ISmsTransport + input/result types) — the check:api-surface gate requires the committed snapshot to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013LXUXU66dBaP3SSG4ZVtuH
serve-defaults.test.ts pins the first six ALWAYS_ON_CAPABILITIES in stable order; grow the slate after them — move 'sms' behind 'storage'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013LXUXU66dBaP3SSG4ZVtuH
os-zhuang
marked this pull request as ready for review
July 10, 2026 13:10
This was referenced Jul 10, 2026
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.
Closes #2780
按 issue 的四条诉求 + 安全要求逐项落地(与 #2766/PR #2771 的 phoneNumber 接入衔接)。
1. 短信 provider 抽象与对接 — 新包
@objectstack/plugin-smspackages/spec/src/contracts/sms-service.ts):ISmsService/ISmsTransport,镜像 email 契约,但有两处刻意差异:不落库(短信正文携带 OTP,持久化等于建了一个凭据库,故没有sys_sms的对应物);输入带templateId/templateParams(阿里云只允许发送已报备模板,不接受自由正文)。SendSms,ACS3-HMAC-SHA256 签名,纯fetch+node:crypto,无厂商 SDK)+ Twilio(Basic auth REST)+ 开发用LogSmsTransport兜底。sms命名空间 manifest(provider 选择 + 各自凭据字段,密钥encrypted,sms/test发测试短信动作),插件在 kernel:ready 绑定并订阅变更、热替换 transport(对齐mail的做法);OS_SMS_*环境变量经 settings 解析层天然生效(如OS_SMS_PROVIDER、OS_SMS_ALIYUN_ACCESS_KEY_ID)。sms加入 always-on capability(与email同列);未配置时是 log 兜底、不会真实发送。2. messaging
smschannelservice-messaging/src/sms-channel.ts完全参照 email channel:收件人是电话形状则直接用,否则查sys_user.phone_number;渲染(topic, 'sms', locale)的sys_notification_template(无模板回退 title/body);messaging-service-plugin.ts在 kernel:ready 检测到sms服务时注册 channel,notify(channels:['sms'])即可用,重试/死信由既有 outbox dispatcher 兜底。3. auth 侧接线
phoneNumber({ sendOTP, sendPasswordResetOTP })改为经 sms 服务发送,打开POST /phone-number/send-otp+/verify与/phone-number/request-password-reset+/reset-password;无(可投递的)短信服务时保持原样大声抛 NOT_SUPPORTED,手机号+密码登录不受影响。features.phoneNumberOtp只在「插件开启 + 短信可投递」时为 true,登录 UI 永远不会展示一个发不出验证码的入口;开发环境 log transport 打印正文,本地可端到端联调 OTP。signUpOnVerification依旧不配置——手机号账号只由管理员直建/导入产生(占位邮箱路径),OTP 不做自助注册。4. 导入联动(
/admin/import-users)invite策略新增短信邀请变体:有手机号、无邮箱的行创建后发送不含任何凭据的邀请短信(用户自己在登录页请求 OTP 首登,再自设密码/走手机号自助重置);混合文件按行校验可达通道(邮箱行要求 email 服务,纯手机行要求短信可投递),失败行标INVITE_SMS_FAILED/EMAIL_SERVICE_REQUIRED,不再整单拒绝。占位邮箱行为与邮件拦截逻辑完全对齐:placeholder 地址在任何通道上都不是投递目标。安全要求(与功能同 PR)
otp-send-guard.ts,默认 60s 冷却、5 条/小时/号码,phoneOtp配置可调):始终开启、不依赖操作员配置,集群下复用 better-authsecondaryStorage做跨节点共享,存储故障 fail-open(限流不能把登录拖下水)。send-otp上冷却违规抛TOO_MANY_REQUESTS(诚实 429);request-password-reset路径 better-auth 以 runInBackgroundOrAwait 吞错并恒定返回{status:true},冷却不会成为号码注册与否的探测信道。/phone-number*10 次/分钟的 per-IP 默认;settings 绑定的rate_limit_max/rate_limit_window_seconds现在同样收紧四个 OTP 端点。allowedAttempts: 3显式传入(不随依赖升级漂移);phoneNumberValidator在花钱发短信前先拒绝垃圾输入。测试
新增/更新 60+ 用例:plugin-sms 28(服务/两个 transport 签名与请求形状/settings 绑定)、sms channel 9、OTP guard 6、auth-manager OTP 11、import-users 短信邀请 4;受影响包全部套件绿(plugin-auth 361、spec 6684、service-messaging 140、service-settings 129、runtime 487)。
说明
features.phoneNumberOtp(展示"验证码登录/短信找回"入口)在objectui侧单独跟进。aliyun_template_code兜底模板(单变量${content}),OTP 建议报备专用模板(变量名code)。🤖 Generated with Claude Code
https://claude.ai/code/session_013LXUXU66dBaP3SSG4ZVtuH
Generated by Claude Code