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

vercel Bot commented Jul 10, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 10, 2026 12:51pm

Request Review

@github-actions github-actions Bot 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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation protocol:system size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants