Context
The create-user phone bug (fixed in objectui #2406 + framework #2871) was one instance of a broader class:
The UI advertises a capability the runtime doesn't have because the backing plugin is off.
The create_user form offered a phoneNumber field, but the default backend rejects phone numbers unless the phoneNumber auth plugin is loaded (auth.plugins.phoneNumber). The fix gated the param with the established visible: 'features.phoneNumber == true' predicate.
The problem is that nothing guarantees coverage — which is exactly why phoneNumber slipped through while its neighbours were correctly gated (create_user itself is visible: 'features.admin == true', org fields are visible: 'features.organization != false'). Gating is hand-authored per call site, so gaps are silent until a user hits the error.
The class of bug
getPublicConfig().features exposes 8 capability flags (packages/plugins/plugin-auth/src/auth-manager.ts):
twoFactor · passkeys · magicLink · phoneNumber · phoneNumberOtp · sso · organization · admin
Any form field / action param / object field whose backend path requires one of these must be gated with a matching predicate. Today that discipline is unenforced.
Proposed work
- Audit the remaining high-risk flags —
twoFactor, passkeys, magicLink, sso — across action params, object fields, and forms for capability-dependent inputs that ship ungated. (phoneNumber is done.)
- Guard — add a CI/test check (spec-liveness style) that regresses when a capability-dependent input has no feature gate.
- (Optional, more durable) introduce a declarative
requiresFeature: 'phoneNumber' on param/field schemas; the renderer auto-hides when the feature is off. This turns "remember to gate" from a per-site discipline into a mechanism, replacing hand-written visible: features.X CEL.
Acceptance criteria
References
Surfaced during go-live UAT while confirming the phone-field fix.
Context
The create-user phone bug (fixed in objectui #2406 + framework #2871) was one instance of a broader class:
The
create_userform offered aphoneNumberfield, but the default backend rejects phone numbers unless thephoneNumberauth plugin is loaded (auth.plugins.phoneNumber). The fix gated the param with the establishedvisible: 'features.phoneNumber == true'predicate.The problem is that nothing guarantees coverage — which is exactly why
phoneNumberslipped through while its neighbours were correctly gated (create_useritself isvisible: 'features.admin == true', org fields arevisible: 'features.organization != false'). Gating is hand-authored per call site, so gaps are silent until a user hits the error.The class of bug
getPublicConfig().featuresexposes 8 capability flags (packages/plugins/plugin-auth/src/auth-manager.ts):twoFactor · passkeys · magicLink · phoneNumber · phoneNumberOtp · sso · organization · adminAny form field / action param / object field whose backend path requires one of these must be gated with a matching predicate. Today that discipline is unenforced.
Proposed work
twoFactor,passkeys,magicLink,sso— across action params, object fields, and forms for capability-dependent inputs that ship ungated. (phoneNumberis done.)requiresFeature: 'phoneNumber'on param/field schemas; the renderer auto-hides when the feature is off. This turns "remember to gate" from a per-site discipline into a mechanism, replacing hand-writtenvisible: features.XCEL.Acceptance criteria
features.*-dependent input is gated, or the exception is documented.requiresFeaturesupported in spec and honored by the objectui renderer.References
create_usergate), objectui fix(security): platform admins see all rows of better-auth-managed identity objects (ADR-0024 / cloud#551) #2406 (filterVisibleParamsrenderer)packages/platform-objects/src/identity/sys-user.object.ts(create_userparams); features assembled inpackages/plugins/plugin-auth/src/auth-manager.tspackages/app-shell/src/views/ActionParamDialog.tsx→filterVisibleParamsSurfaced during go-live UAT while confirming the phone-field fix.