Repository navigation
fix(platform-objects): Setup Create User marks Email required while phone sign-in is off - #28707
Conversation
…n-in (red first) Co-Authored-By: Claude <noreply@anthropic.com>
…ign-in is off Two params on the one email field: the optional, phoneNumber-gated one first, then a required one shown while the served config says phone sign-in is off. Book the path in phoneNumber.gatedInputs. Co-Authored-By: Claude <noreply@anthropic.com>
…hile phone sign-in is off Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 168 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0fb8c97c40e475b1024f94a88db414be819336ad && git checkout 0fb8c97c40e475b1024f94a88db414be819336ad
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6cbdef1e4418d1ba62e3f93e9299fe8db953aaa8 9684fb3ba196ca4e8fd9a31ed3bffe40cac30ba8 && git checkout -B drift-repro 6cbdef1e4418d1ba62e3f93e9299fe8db953aaa8 && git merge --no-ff 9684fb3ba196ca4e8fd9a31ed3bffe40cac30ba8
node scripts/docs-audit/affected-docs.mjs --json 6cbdef1e4418d1ba62e3f93e9299fe8db953aaa8
|
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH Contract reviewServed-tier: Inputs: card #28280 (body, triage 6538087552, claim 6558617122, os-dev-report 6559354696), PR #28707 body, file list and net diff at the head above, the head's check-runs, and the live tree at ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: FAIL |
…ly when every declaration is sys_user.create_user declares email twice (optional while phone sign-in is on, required while it is off). validateActionParams keyed params by name with the last declaration winning, so the action doors refused a phone-only create. A name is now required only when every declaration of it is; the value shape is the first declaration's. Co-Authored-By: Claude <noreply@anthropic.com>
…ng, semantic, spec minor) Co-Authored-By: Claude <noreply@anthropic.com>
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH Contract reviewServed-tier: Delta record extending the base record on Check-runs on ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: PASS |
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH 席位核对 · ACCEPT · PR #28707 · 2026-11-01T15:58ZHead-sha: 维护者速读(席位终稿)
核对六项
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH
Requested by Leehom · project thread
Fixes #28280
Clause-②: yes (widening)
Metadata-protocol: semantic
维护者速读
/auth/config的应答时(加载中或请求失败),邮箱不带标记,和 main 上一样。sys_user.create_user的邮箱参数拆成两个(同一字段,按features.phoneNumber各自显隐,对话框只画并只提交显示的那个)。为此动作门的参数校验validateActionParams改了一处语义:同名参数声明多次时,只有每一处都必填才算必填,值形状按第一处(以前是最后一处说了算,会让 REST 动作门把只填手机号的创建拒成 400)。仓库、示例和 hotcrm 里没有别的动作重复声明参数名,所以别处行为不变。外加public-auth-features.ts登记一条路径。纯 revert 即可回滚。packages/spec/src/ui/action-params.zod.ts(同名合并那一行);packages/platform-objects/src/identity/sys-user.object.ts(两个邮箱参数的顺序与visible);packages/runtime/src/domains/api-action-create-user-email-param.test.ts(经真实 REST 动作门的三条路径)。pnpm --filter @objectstack/runtime exec vitest run src/domains/api-action-create-user-email-param.test.ts,预期 5 例全绿(只填手机号的创建能到达接口;空邮箱由接口自己答 400 "Email is required")。packages/spec路径的 Contract review)。What changes
sys_user.create_userdeclares two params on the oneemailfield (packages/platform-objects/src/identity/sys-user.object.ts). A param'srequiredis a static boolean, and the endpoint requires the email only while phone sign-in is off, so:{ field: 'email', required: false, requiresFeature: 'phoneNumber' }, which lowers tofeatures.phoneNumber == true;{ field: 'email', required: true, visible: 'has(features.phoneNumber) && features.phoneNumber != true' }.The console collects and submits only the params whose gate says yes (
filterVisibleParams), so exactly one Email input renders. The rendered order is unchanged: Email, then Phone Number when it shows, then Name.packages/spec/src/ui/action-params.zod.ts, under the one-PR exception the seat extended to this file).validateActionParams, whichenforceActionParamsruns on the REST action route and the MCPrun_actionbridge before the handler, keyed params by name with the LAST declaration winning, so the required second Email param made the REST door refuse a phone-only create (400 VALIDATION_ERROR"Action param "email" is required"). A name declared more than once is now required only when EVERY declaration is, and its value shape is the FIRST declaration's; unknown-key handling is unchanged. The door reads novisibleorfeatures, so the endpoint stays the judge of a blank email.packages/spec/src/kernel/public-auth-features.ts):PUBLIC_AUTH_FEATURES.phoneNumber.gatedInputsbookssys_user.actions.create_user.params.email. The row's notes say the path stands for two params, the second negatively gated, and the module andgatedInputsdocblocks say a param path resolves to the first param on its field and the gate is checked as a conjunct. No registry model change and no required-when predicate, as ruled.packages/platform-objects/src/feature-gate-guard.test.ts) states that this one booked path stands for two params, one of them negatively gated. The guard's logic is untouched.features; a runtime test that drives the real REST action route with the realSysUserdeclaration; twovalidateActionParamscases inaction-params.test.ts..changeset/28280-create-user-email-required-mark.md:@objectstack/specminor (the semantic change to a twice-declared param'srequiredat the doors) and@objectstack/platform-objectspatch. Both packages shipdist, and the builtdistof each carries the change.Tests
Red first on origin/main 49a6a5c, green at head
pnpm --filter @objectstack/platform-objects exec vitest run src/identity/sys-user-create-user-email-required.test.ts, with only the test file on top oforigin/main:At head, with the guard file (after
pnpm --filter @objectstack/spec build):Whole package:
pnpm --filter @objectstack/platform-objects test→Test Files 107 passed (107) · Tests 1496 passed (1496).The Create User dialog (app-shell)
useConsoleActionRuntime.createUserEmailRequiredMark-28280.test.tsxboots the dialog the way #28649's test does (realActionRunnerwith the console runtime's api and param handlers, realActionParamDialog, real ChineseI18nProvider), inside the console's ownExpressionProviderfed the servedfeatures, withsys_user's fields so the field-backed params resolve throughresolveActionParams:aria-required="true", no Phone Number; a blank Confirm shows邮箱 为必填项, marks the input invalid, and sends no request;featuresis{}): one Email input, not required;cd ui && pnpm exec vitest run packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.createUserEmailRequiredMark-28280.test.tsx packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.createUserRequiredField-28289.test.tsx→Test Files 2 passed (2) · Tests 10 passed (10)(#28649's six server-refusal marks stay green).The REST action door (runtime), red first and the merge ablated
pnpm --filter @objectstack/runtime exec vitest run src/domains/api-action-create-user-email-param.test.ts, drivingHttpDispatcher.handleActions('/sys_user/create_user', 'POST', …)over theapi-action-forward.test.tsrecording target, with the realSysUserfrom@objectstack/platform-objects/identity:[false, true];{ phoneNumber, name }passes the door and is forwarded to/api/v1/auth/admin/create-user;400 VALIDATION_FAILED"Email is required" with itsfields, relayed whole (the door does not refuse it);Red first, with
packages/spec/src/ui/action-params.zod.tsat 2310b30 and spec rebuilt:× phone-only … reaches the endpointwith{"code":"VALIDATION_ERROR","message":"Invalid action params: Action param \"email\" is required","httpStatus":400},× blank email …(the seat's finding). That run was on a six-case draft: 3 failed / 3 passed, the third an MCP case that failed on the system-object exposure refusal instead and was dropped (see below). Spec unit cases: 2 failed / 33 passed.Merge ablated through
scripts/ablation-replace.mjs(declared.set(p.name, p), last wins), spec rebuilt,disthits of the merge 0: door test 2 failed / 3 passed, spec cases 2 failed / 33 passed. Restored to the HEAD blob (git diff HEADempty), rebuilt,disthits 1: 5/5 and 35/35.The MCP bridge is not driven for this action:
create_userdeclares noai.exposed, sorun_actionrefuses it as a system-object action before any param check (measured). Both doors call the sameenforceActionParams.Ablations (each through scripts/ablation-replace.mjs, restored to the HEAD blob, git diff HEAD empty)
required: false× phone sign-in off: one Email input, and it is required· 1 failed / 115 passedhas(features.phoneNumber) &&dropped (barefeatures.phoneNumber != true)× no served flag …: one Email input, optional· 1 failed / 114 passed× the first Email param carries the canonical phoneNumber gate …and the guard's forward row× sys_user.actions.create_user.params.email is gated on phoneNumber· 2 failed / 114 passed× phone sign-in off: … required mark …· 1 failed / 3 passedfeatures.phoneNumber != true× no served flag …· 1 failed / 3 passedThe platform-objects pin imports the object from
src/(relative import), and ui vitest aliases@object-ui/*tosrc/, so no leg needed a rebuild. A3 confirms the order matters: the forward guard resolves the path to the first param on the field.Acceptance notes
features.phoneNumber != true. Measured (A2, U2): while the console holds no served flag (itsfeaturesis{}until/auth/configanswers, and stays{}if that fetch fails) both gates fault on the absent key, and the dialog shows a param whose gate faults, so two Email inputs render. Thehas(features.phoneNumber) &&term keeps the required param hidden then, and Email stays optional as it was. The precedent spelling issys-user.page.ts'shas(features.twoFactor) && features.twoFactor == true. The first, optional param keeps the canonical gate the guard wants.params/actionParamsarray literal in this repository (430 arrays,ui/andexamples/included) and in hotcrmorigin/main(7 arrays), plus the 2 JSON params arrays, finds no name declared twice exceptcreate_user'semail. No existing action relied on last-wins.emailrows (one perfeatures.phoneNumberreference across the two params), all booked by the one path.Verification
Local gates at head
Local gate run at head 9684fb3, derived by dispatch-gates --commands (87 families), each exit captured before any pipe.
Re-run: pnpm --dir ui exec vitest run scripts/ first exit 1 (unit-registry-absence-collision could not import app-shell modules while @objectstack/client dist was still being built in this fresh worktree), then exit 0 (169 files) once it was built.
dispatch-gates --ran: 87 derived, 86 run, 1 NOT-MEASURED (check:dual-build-cjs-loads, PREREQUISITE NOT MET: needs every package dist), 0 UNRUN.
Generated by Claude Code