背景
create-user phone bug(objectui #2406 + framework #2871 已修)是一个更广 bug 类的实例:
UI 宣传了一个 runtime 没有的能力,因为背后的插件没开。
create_user 表单提供 phoneNumber 字段,但默认后端在 phoneNumber auth 插件未加载时拒绝它。修复用既有谓词模式 gated:visible: 'features.phoneNumber == true'(sys-user.object.ts ~L160)。邻居字段本来就 gated 对了(create_user 本身 features.admin == true ~L143,org 字段 features.organization != false ~L66),唯独 phone 漏掉——说明 per-site 手工 gating 纪律必然出静默缺口 。
v2(2026-07-15)重写说明 :原版把「CI 守卫」列为必做、把声明式 requiresFeature 列为可选——依赖方向反了 :没有声明载体,「input 依赖哪个 feature」这一知识只存在于插件运行时行为里,静态检查没有 ground truth,原验收「fails if a capability-dependent input ships ungated」不可实现。另外原版的 flag 清单(8 个)在开出当天就已过时(实际 13 个,见下)——恰好自证了手工清单必漂移的论点。本次重写:修正清单、按消费面拆分审计范围、把工作重排为 P0 注册表 → P1 声明式标注 → P2 审计,验收改为可机械判定 的两条守卫。标签去掉 security(后端本来就 fail-closed 拒绝,这是可用性/诚实宣传问题,不是权限提升;反向类「UI 隐藏但后端接受」才是安全面,不在本 issue 范围)。
已核实现状(2026-07-15,落点级)
getPublicConfig().features(auth-manager.ts ~L2663-2701)实际暴露 13 个布尔 flag (另有 termsUrl/privacyUrl 两个非布尔,豁免),按消费面 分两类:
① Admin/CRUD 面 (action params / object fields,经 objectui filterVisibleParams 渲染链)——本 issue 的主审计面:
flag
默认
现有 gating
admin
false(SCIM 强制开,ADR-0071/#2766 )
sys-user.object.ts:143
organization
true
sys-user/team/member/invitation 多处 != false
multiOrgEnabled
false(OS_MULTI_ORG_ENABLED)
sys-organization.object.ts ×6 处 != false
phoneNumber
false
sys-user.object.ts:160(本次修复)
② 登录面 (objectui 登录 UI 直接消费 /auth/config,不走 param 渲染链)——twoFactor / passkeys / magicLink / sso / ssoEnforced / phoneNumberOtp / oidcProvider / deviceAuthorization。这条链已有刻意设计:phoneNumberOtp 仅在 SMS 可投递时 advertised(~L2698,#2780 ),sso 经 isSsoUsable() 细化以隐藏死按钮(~L2678-2682)。审计预期以核对 为主而非补漏。
③ 状态旗 ——degradedTenancy(ADR-0093 D5 降级横幅):不是输入门,豁免并注明。
原版清单漏掉的 5 个:multiOrgEnabled(已被 6 处谓词消费!)、degradedTenancy、oidcProvider、ssoEnforced、deviceAuthorization。
任务(按依赖排序)
P0 — flag 分类注册表 + 清单漂移守卫 (~1 PD,立即可做,抓最大漂移类)
spec 侧建单一注册表:每个 flag 记录 {消费面: crud|login|status, 默认语义: == true | != false, 映射的 spec inputs 或豁免理由}。测试断言 getPublicConfig().features 的 key 集合 ≡ 注册表 key 集合——新增 flag 未分类即 CI 红 (正是本 issue 自己漏 5 个 flag 的那类漂移)。
P1 — 声明式 requiresFeature 语法糖 (~1-2 PD,依赖 P0 的语义表)
param/field schema 支持 requiresFeature: 'phoneNumber',spec 加载期 查 P0 语义表 lower 成 visible: 'features.phoneNumber == true'(或 != false)——objectui 渲染链(filterVisibleParams)零改动 。既有手写 CEL 门迁移为标注(行为等价,矩阵锁定)。
P2 — 双面审计 + 注册表完整性守卫 (~1-2 PD,依赖 P0)
① CRUD 面:枚举 platform-objects 全部 action params / object fields,凡后端路径依赖注册表 flag 者补 requiresFeature;② 登录面:核对 objectui 登录 UI 对 8 个登录 flag 的消费,缺口记录或修复;③ 守卫:注册表中「需 gating」flag 映射的每个 input 必带匹配谓词,CI 断言。
显式非目标
「任意新 input 忘了标注必被 CI 抓住」不承诺 ——「这个 input 依赖 feature X」的 ground truth 只在插件运行时行为里,静态不可判定。声明式标注把纪律成本降到最低,注册表守卫抓 flag 级漂移;运行时兜底属于 dogfood 矩阵(插件关闭 → advertised 表单逐项探测),如需要另立 issue。
反向安全类(UI 隐藏但后端接受)不在范围。
验收
参考
Surfaced during go-live UAT while confirming the phone-field fix. Rewritten v2 after site-level verification.
背景
create-user phone bug(objectui #2406 + framework #2871 已修)是一个更广 bug 类的实例:
create_user表单提供phoneNumber字段,但默认后端在phoneNumberauth 插件未加载时拒绝它。修复用既有谓词模式 gated:visible: 'features.phoneNumber == true'(sys-user.object.ts~L160)。邻居字段本来就 gated 对了(create_user本身features.admin == true~L143,org 字段features.organization != false~L66),唯独 phone 漏掉——说明 per-site 手工 gating 纪律必然出静默缺口。已核实现状(2026-07-15,落点级)
getPublicConfig().features(auth-manager.ts~L2663-2701)实际暴露 13 个布尔 flag(另有termsUrl/privacyUrl两个非布尔,豁免),按消费面分两类:① Admin/CRUD 面(action params / object fields,经 objectui
filterVisibleParams渲染链)——本 issue 的主审计面:adminsys-user.object.ts:143organizationsys-user/team/member/invitation多处!= falsemultiOrgEnabledOS_MULTI_ORG_ENABLED)sys-organization.object.ts×6 处!= falsephoneNumbersys-user.object.ts:160(本次修复)② 登录面(objectui 登录 UI 直接消费
/auth/config,不走 param 渲染链)——twoFactor / passkeys / magicLink / sso / ssoEnforced / phoneNumberOtp / oidcProvider / deviceAuthorization。这条链已有刻意设计:phoneNumberOtp仅在 SMS 可投递时 advertised(~L2698,#2780),sso经isSsoUsable()细化以隐藏死按钮(~L2678-2682)。审计预期以核对为主而非补漏。③ 状态旗——
degradedTenancy(ADR-0093 D5 降级横幅):不是输入门,豁免并注明。原版清单漏掉的 5 个:
multiOrgEnabled(已被 6 处谓词消费!)、degradedTenancy、oidcProvider、ssoEnforced、deviceAuthorization。任务(按依赖排序)
spec 侧建单一注册表:每个 flag 记录
{消费面: crud|login|status, 默认语义: == true | != false, 映射的 spec inputs 或豁免理由}。测试断言getPublicConfig().features的 key 集合 ≡ 注册表 key 集合——新增 flag 未分类即 CI 红(正是本 issue 自己漏 5 个 flag 的那类漂移)。requiresFeature语法糖(~1-2 PD,依赖 P0 的语义表)param/field schema 支持
requiresFeature: 'phoneNumber',spec 加载期查 P0 语义表 lower 成visible: 'features.phoneNumber == true'(或!= false)——objectui 渲染链(filterVisibleParams)零改动。既有手写 CEL 门迁移为标注(行为等价,矩阵锁定)。① CRUD 面:枚举
platform-objects全部 action params / object fields,凡后端路径依赖注册表 flag 者补requiresFeature;② 登录面:核对 objectui 登录 UI 对 8 个登录 flag 的消费,缺口记录或修复;③ 守卫:注册表中「需 gating」flag 映射的每个 input 必带匹配谓词,CI 断言。显式非目标
验收
requiresFeature在 spec 支持并 lower 正确(两种默认语义各有测试);objectui 无改动即生效;既有手写门迁移后行为矩阵无 delta。参考
create_user门)、objectui fix(security): platform admins see all rows of better-auth-managed identity objects (ADR-0024 / cloud#551) #2406(filterVisibleParams)packages/platform-objects/src/identity/sys-user.object.ts(create_user params);features 组装:packages/plugins/plugin-auth/src/auth-manager.ts~L2663-2701packages/app-shell/src/views/ActionParamDialog.tsx→filterVisibleParamsSurfaced during go-live UAT while confirming the phone-field fix. Rewritten v2 after site-level verification.