Skip to content

feat(messaging/auth): 短信基建(SMS provider + messaging sms channel)+ 手机号 OTP 首登/重置 (#2780) - #2790

Merged
os-zhuang merged 4 commits into
mainfrom
claude/sms-infrastructure-phone-otp-9tisdu
Jul 10, 2026
Merged

feat(messaging/auth): 短信基建(SMS provider + messaging sms channel)+ 手机号 OTP 首登/重置 (#2780)#2790
os-zhuang merged 4 commits into
mainfrom
claude/sms-infrastructure-phone-otp-9tisdu

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes#2780

按 issue 的四条诉求 + 安全要求逐项落地(与 #2766/PR #2771 的 phoneNumber 接入衔接)。

1. 短信 provider 抽象与对接 — 新包 @objectstack/plugin-sms

  • 契约packages/spec/src/contracts/sms-service.ts):ISmsService / ISmsTransport,镜像 email 契约,但有两处刻意差异:不落库(短信正文携带 OTP,持久化等于建了一个凭据库,故没有 sys_sms 的对应物);输入带 templateId/templateParams(阿里云只允许发送已报备模板,不接受自由正文)。
  • Provider 实现:阿里云短信(SendSms,ACS3-HMAC-SHA256 签名,纯 fetch + node:crypto,无厂商 SDK)+ Twilio(Basic auth REST)+ 开发用 LogSmsTransport 兜底。
  • 配置走 service-settings 模式:新 sms 命名空间 manifest(provider 选择 + 各自凭据字段,密钥 encryptedsms/test 发测试短信动作),插件在 kernel:ready 绑定并订阅变更、热替换 transport(对齐 mail 的做法);OS_SMS_* 环境变量经 settings 解析层天然生效(如 OS_SMS_PROVIDEROS_SMS_ALIYUN_ACCESS_KEY_ID)。
  • CLI:sms 加入 always-on capability(与 email 同列);未配置时是 log 兜底、不会真实发送。

2. messaging sms channel

service-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,手机号+密码登录不受影响。
  • 生产环境下 log-only(未配置 provider)不算可投递: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-auth secondaryStorage 做跨节点共享,存储故障 fail-open(限流不能把登录拖下水)。send-otp 上冷却违规抛 TOO_MANY_REQUESTS(诚实 429);request-password-reset 路径 better-auth 以 runInBackgroundOrAwait 吞错并恒定返回 {status:true},冷却不会成为号码注册与否的探测信道。
  • 端点级限流:better-auth phone-number 插件自带 /phone-number* 10 次/分钟的 per-IP 默认;settings 绑定的 rate_limit_max/rate_limit_window_seconds 现在同样收紧四个 OTP 端点。
  • allowedAttempts: 3 显式传入(不随依赖升级漂移);phoneNumberValidator 在花钱发短信前先拒绝垃圾输入。
  • OTP 绝不落日志:SmsService 只记 masked 号码 + 状态,正文永不入日志;LogSmsTransport 生产环境抑制正文;投递失败的错误信息只含 transport 详情、不含验证码。

测试

新增/更新 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)。

说明

  • 登录 UI 消费 features.phoneNumberOtp(展示"验证码登录/短信找回"入口)在 objectui 侧单独跟进。
  • 阿里云是模板制短信:通用通知走 aliyun_template_code 兜底模板(单变量 ${content}),OTP 建议报备专用模板(变量名 code)。

🤖 Generated with Claude Code

https://claude.ai/code/session_013LXUXU66dBaP3SSG4ZVtuH


Generated by Claude Code

…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
@vercel

vercelBot commented Jul 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 10, 2026 12:51pm

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file protocol:system tests tooling size/xl labels Jul 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/cli, @objectstack/plugin-auth, @objectstack/plugin-sms, packages/services, @objectstack/spec.

101 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via packages/cli, @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/cli, @objectstack/spec)
  • content/docs/api/data-flow.mdx(via @objectstack/cli)
  • content/docs/api/environment-routing.mdx(via @objectstack/cli, @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/cli, @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via packages/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/cli, packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via packages/services, @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via packages/spec)
  • content/docs/concepts/north-star.mdx(via packages/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/production-readiness.mdx(via @objectstack/plugin-auth)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/cli.mdx(via @objectstack/cli, @objectstack/plugin-auth, @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via packages/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/audit-service.mdx(via packages/services)
  • content/docs/kernel/runtime-services/data-service.mdx(via packages/cli)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/cli, packages/services, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/settings-service.mdx(via packages/services)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via packages/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/plugin-auth, @objectstack/spec)
  • content/docs/permissions/authentication.mdx(via @objectstack/cli, @objectstack/plugin-auth)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/permissions/sso.mdx(via @objectstack/plugin-auth)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/plugin-auth, @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/cli, @objectstack/plugin-auth, @objectstack/plugin-sms, packages/services, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectos/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/objectos/i18n-standard.mdx(via packages/services, @objectstack/spec)
  • content/docs/protocol/objectos/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/objectos/plugin-spec.mdx(via @objectstack/cli, @objectstack/spec)
  • content/docs/protocol/objectos/realtime-protocol.mdx(via @objectstack/cli)
  • content/docs/protocol/objectos/runtime-capabilities.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via packages/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx(via @objectstack/cli, @objectstack/plugin-auth, @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/plugin-auth, @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

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
claude added 2 commits July 10, 2026 12:44
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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationprotocol:systemsize/xlteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(messaging/auth): 短信基建(SMS provider + messaging sms channel)+ 手机号 OTP 首登/重置

2 participants

@os-zhuang@claude