Skip to content

fix(plugin-auth): Create User with no email names Email, and a wrong password on the password-change screen reads in the user's language - #28288

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-9wtlmh
Oct 31, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-9wtlmh

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

domain:server#1 · os-elon-musk · session_016WV4cwXFwjU2YZW2dRuZZt

Requested by Elon · project thread

Fixes #27857
Part of #27869

Clause-②: no
Metadata-protocol: none
Skills-docs: none — git grep of skills/** and content/docs/** for "Either email or phoneNumber is required", "Invalid password" and INVALID_PASSWORD has zero hits.

维护者速读

  • 改了什么:设置 → 创建用户:不填邮箱时,报错从「Either email or phoneNumber is required」改为「Email is required」(开启手机号登录时为「Email or phone number is required」);强制改密页面输错当前/临时密码时,按用户语言显示提示,不再显示英文「Invalid password」。
  • 为什么改:HotCRM 18.2.0 设置走查:报错点名了界面上不存在的 phoneNumber;法语用户改密输错时看到英文。邮箱必填标记另立卡(见下文)。
  • 风险与回滚:错误码 INVALID_REQUEST 与状态 400 不变,只改措辞;createAuthClient 的 changePassword 抛错时保留 better-auth 的错误码(原先丢失,映射永不触发);界面只多一个翻译键;元数据未改。回滚即还原本 PR。
  • 先看:packages/plugins/plugin-auth/src/admin-user-endpoints.ts(报错措辞)、ui/packages/auth/src/createAuthClient.ts(保留错误码)、ui/packages/app-shell/src/console/RemediationOverlay.tsx(错误码映射)、ui/packages/i18n/src/locales/fr.ts。
  • 两分钟验证:设置 → 用户 → 创建用户,邮箱留空提交:提示「Email is required」;以法语用户登录强制改密页,临时密码输错:显示法语提示。
  • 需要你做的:无,席位落地。

What changes

One commit per card, plus a revert commit, a merge of origin/main, and the seat-review fix for #27869 (a):

  1. Create User with Email empty says "Either email or phoneNumber is required" and the dialog marks nothing required (#27521 row 10) #27857 — Create User with no email. plugin-auth admin-user-endpoints.ts: with neither an email nor a phone number, the refusal reads "Email is required", or "Email or phone number is required" when phone sign-in is on (deps.phoneNumberEnabled()). It used to read "Either email or phoneNumber is required", naming a body key. Status 400 and code INVALID_REQUEST are unchanged (dispatch: wording only).
  2. French user sees English: "Invalid password", untranslated sys_user labels in Home feed, misleading Locale help (#27521 row 22) #27869 (a) — "Invalid password" on the password-change screen. better-auth's /change-password refuses a wrong current or temporary password with APIError.from('BAD_REQUEST', BASE_ERROR_CODES.INVALID_PASSWORD), so the body is { message: "Invalid password", code: "INVALID_PASSWORD" } (better-auth 1.7.12, api/routes/update-user.mjs). Two links were broken:
    • createAuthClient's changePassword threw new Error(error.message) and dropped the code. It now throws through toAuthError, as sign-in and the organization methods already do, so the thrown error carries code. The message is unchanged.
    • ChangePasswordForm in RemediationOverlay.tsx showed err.message as it came. It now maps INVALID_PASSWORD to auth.remediation.password.wrongPassword, a new key in all ten console packs (ar, de, en, es, fr, ja, ko, pt, ru, zh). Any other refusal keeps the server's message.

What this PR does not carry

Tests

  • New: changePassword-code-27869.test.ts (drives the real better-auth client through a mocked fetch; a 400 { message: 'Invalid password', code: 'INVALID_PASSWORD' } from /change-password rejects with .code === 'INVALID_PASSWORD' and the message unchanged). New: RemediationOverlay.wrongPassword-27869.test.tsx (the fr and en sentences, plus a control: another code keeps the server message; it mocks useAuth, so the client pin above covers the first link). Extended: admin-user-endpoints.test.ts (the refusal wording with phone sign-in off and on, code and status pinned). Updated: request-json-body-refusal.test.ts (the empty-body control reads the new wording).
  • pnpm --filter @objectstack/plugin-auth test: Test Files 205 passed (205), Tests 3917 passed, 10 skipped. typecheck: exit 0. ui vitest over packages/auth/, packages/app-shell/src/console/ and packages/i18n/: Test Files 354 passed (354), Tests 3299 passed, 13 skipped. @object-ui/auth type-check: exit 0.
  • Ablations (each through scripts/ablation-replace.mjs, restored to the HEAD blob, git status clean after):
    • Old refusal wording put back → admin-user-endpoints.test.ts 2 failed / 93 passed.
    • The INVALID_PASSWORD mapping disabled → RemediationOverlay.wrongPassword-27869.test.tsx 2 failed / 1 passed (the control stays green).
    • changePassword back to throw new Error(error.message ?? …) → changePassword-code-27869.test.ts 1 failed (expected undefined to be 'INVALID_PASSWORD').

Acceptance notes

Verification

The test and ablation readings above are on head d5b0a9e4. The gate readings below are on 9248a860; d5b0a9e4 adds only the ui/packages/auth commit, dispatch-gates --commands derives the same 66 families on it, and their rerun on d5b0a9e4 is recorded in the card report on #27857.

Derived gate families: 66 of 66 exit 0
  • node scripts/pm/dispatch-gates.mjs --commands derived 66 families. They ran after builds of @objectstack/cli + platform-objects (with the i18n-bundle packages check:i18n names), the @objectstack/dogfood^... and @objectstack/runtime^... closures, and @objectstack/plugin-auth... (all exit 0).
  • All 66 exit 0, including check:i18n, check:dual-build-cjs-loads, check:closing-keyword-parity, check:nul-bytes, check:published-files and pnpm --dir ui exec vitest run scripts/.
  • dispatch-gates --ran: "66 derived famil(ies) accounted for — 66 run, 0 NOT-MEASURED", exit 0.
  • pnpm lint (eslint . --no-inline-config): exit 0.
  • Not run locally: ui type-check and the full ui test shards. CI's Type Check UI and Test UI own them.

CI check-runs on this PR are the authority for every family CI runs.

Human Minimum

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4cwXFwjU2YZW2dRuZZt


Generated by Claude Code

…og marks Email required when phone sign-in is off

The refusal read "Either email or phoneNumber is required", naming a body
key the dialog does not show. It now reads "Email is required", or "Email
or phone number is required" where phone sign-in is on; the code and status
are unchanged. The create_user dialog backs Email with two params, a
required one shown while phone sign-in is off and an optional one behind
the phoneNumber feature, so the required mark appears exactly when Email
is required.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cwXFwjU2YZW2dRuZZt
… or temporary password

The screen showed the server's English "Invalid password" for the
INVALID_PASSWORD refusal in every language. The code now maps to a
translated sentence, one new key in every console language pack; any
other refusal keeps the server's message.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cwXFwjU2YZW2dRuZZt
The two visibility-gated Email params trip the feature-gate registry
guard: the negated phoneNumber gate is not a shape PUBLIC_AUTH_FEATURES
books, and booking it means editing the spec registry. The required
mark is carried separately; this change keeps the refusal wording.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cwXFwjU2YZW2dRuZZt
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 31, 2026
@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 59 of 211 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 53749673ba7a0005d09e8a79544da10d5faa10bc → packageMentionDocs.

Which tree this was computed on

This run read content/docs from c61303dd6a1c9b8ffe38fbdc72c9fc4d67e2a78d — the merge of head d5b0a9e4beee79dbe7b26b3b98632aff0ac5e521 into base 53749673ba7a0005d09e8a79544da10d5faa10bc, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c61303dd6a1c9b8ffe38fbdc72c9fc4d67e2a78d && git checkout c61303dd6a1c9b8ffe38fbdc72c9fc4d67e2a78d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 53749673ba7a0005d09e8a79544da10d5faa10bc d5b0a9e4beee79dbe7b26b3b98632aff0ac5e521 && git checkout -B drift-repro 53749673ba7a0005d09e8a79544da10d5faa10bc && git merge --no-ff d5b0a9e4beee79dbe7b26b3b98632aff0ac5e521

node scripts/docs-audit/affected-docs.mjs --json 53749673ba7a0005d09e8a79544da10d5faa10bc

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

changePassword threw a bare Error built from error.message, so the
INVALID_PASSWORD code a wrong current password carries never reached
ChangePasswordForm and its mapping to the translated sentence never fired.
Route the refusal through toAuthError, as sign-in and the organization
methods already do, and pin the code at the client boundary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cwXFwjU2YZW2dRuZZt
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-elon-musk · session_016WV4czVnUdQQCVKSW5J2uk

Seat ACCEPT · PR #28288 · head d5b0a9e4beee79dbe7b26b3b98632aff0ac5e521 · 2026-10-31T04:05Z

维护者速读

席位核对

ACCEPT

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 31, 2026 04:06
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 31, 2026
Merged via the queue into main with commit b339465 Oct 31, 2026
67 of 68 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-9wtlmh branch October 31, 2026 04:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create User with Email empty says "Either email or phoneNumber is required" and the dialog marks nothing required (#27521 row 10)

1 participant