Repository navigation
fix(plugin-auth): store sys_user.locale in its BCP-47 case (zh-cn → zh-CN) on every write path - #30049
Conversation
zh-cn and ZH-CN are stored as zh-CN on every ObjectQL write of the column (data door, bulk, admin import, IdP locale sync), through the LocaleSchema fold translation locales already use. A value the schema does not accept is left as sent, so the column rule still refuses zh_CN and an empty value still clears. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfsqbTMmPhapjVEeoo
📓 Docs Drift CheckThis PR changes 1 package(s): 37 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 22 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 fec757d045e7e4b61e4cc68f4ee65a74b1def205 && git checkout fec757d045e7e4b61e4cc68f4ee65a74b1def205
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 318bc76faf9ffec6437c0842a476216aca001fec 1c72a05faee0465e2b869b65858cbfcd2c4410b2 && git checkout -B drift-repro 318bc76faf9ffec6437c0842a476216aca001fec && git merge --no-ff 1c72a05faee0465e2b869b65858cbfcd2c4410b2
node scripts/docs-audit/affected-docs.mjs --json 318bc76faf9ffec6437c0842a476216aca001fec
|
|
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5 席位核对Verdict: ACCEPT at head
|
Requested by Yibo Wang · project thread
Fixes #29895
维护者速读
sys_user.locale每次写入都按 BCP-47 大小写存(zh-cn、ZH-CN存成zh-CN),覆盖数据接口(单条与批量)、管理员导入用户、身份源登录同步三条写入路径;归一用的就是 feat(spec)!: a locale tag written in another case is corrected to its BCP-47 case #29833 给翻译语言做归一的LocaleSchema,没有另写一份。zh-cn被原样存下,而控制台按精确匹配挑界面语言,于是这个用户看到英文界面且没有任何报错。zh_CN仍答 400,空值仍是清空;已存的错大小写旧行不迁移,下次写这一列时才归一。回滚即 revert 本 PR。packages/plugins/plugin-auth/src/sys-user-locale-case.ts(钩子与归一),auth-plugin.ts的注册处(优先级 5,在身份写守卫之前),idp-locale-claim.ts的resolveIdpLocale(记录与列同一拼写)。pnpm --filter @objectstack/plugin-auth exec vitest run src/sys-user-locale-case.test.ts,预期全部通过;真实路由见 dogfooduser-locale-case-fold.dogfood.test.ts(6 个用例)。Clause-②: no
Metadata-protocol: none
What changes
sys-user-locale-case.ts(plugin-auth).registerUserLocaleCaseFoldregistersbeforeInsert/beforeUpdatehooks onsys_userat priority 5, for every context (isSystemincluded). It foldsdata.localethroughcanonicalUserLocale, which isLocaleSchema.safeParse(value)from@objectstack/spec/system: the parse's output on success, the value as sent otherwise.canonicalLocaleCaseitself is package-internal in spec by design (its docblock: an exported fold would invite a second fold in a consumer), so the parse is the reuse, and no second fold exists.auth-plugin.tsatkernel:ready, besideregisterMemberRoleCanonicalization(the same write-path canonicalisation shape and priority slot, ahead of the ADR-0092 identity write guard at 10).resolveIdpLocale(idp-locale-claim.ts) now answers the supported locale in the same case. Without that, a supported list spelled off-case (zh-cn) would get the column folded tozh-CNwhile theidp-locale:record keptzh-cn, and the next sign-in would read the IdP's own value as a local choice (decideIdpLocaleWritecompares stored with recorded). With the stock locales this changes nothing (zh-cn→zh-CNwas already the answer).@objectstack/plugin-authpatch, with the two declaration lines.packages/metadata-protocol/src/protocol.tsandpackages/objectql/src/engine.tsare not touched.Write paths measured (origin/main
c618e890)PATCH /api/v1/data/sys_user/:id, and the bulk form (apiMethods: ['get','list','update','bulk'])update→ identity write guard whitelist (SYS_USER_PROFILE_EDIT_FIELDShaslocale)admin-import-users.ts:750,SYS_USER_IMPORT_UPDATE_FIELDS⊇locale)updateidp-locale-claim.tsIdpLocaleSync.apply)updatelocaleis not on its user model (identity-write-guard.tsSESSION_SNAPSHOT_MIRRORED_FIELDSnote)All of them are ObjectQL writes, so the one hook pair covers them. Generic data import on
sys_useris not open (importis not inapiMethods). The format rule runs after the before-hooks (enginevalidateRecordfollowstriggerHooks('beforeUpdate')), and the fold is case only, so the rule's verdict on any value is unchanged.sys-user-locale-case.test.tspins that for every case in its table.Tests
sys-user-locale-case.test.ts(new, plugin-auth). The fold table:zh-cn,ZH-CN→zh-CN,EN→en,ZH-HANS-cn→zh-Hans-CN,xx-yy→xx-YY;zh_CN,Chinese,zh CN, a trailing space,''and non-strings come back as sent. The fold composed with the realSysUserrules throughevaluateValidationRules: refused after exactly when refused before, andzh_CNstillVALIDATION_FAILEDonlocale/invalid_format. The hooks:sys_useronly, priority 5, fold in place on insert and update, for user, system and no context; canonical, refused and cleared values untouched; no key invented; the shared predicate-update payload folded.idp-locale-claim.test.ts: one new case, an off-case supported list answers the canonical tag.user-locale-case-fold.dogfood.test.ts(new, the real door on a booted showcase). A signed-up member's own-rowPATCH /api/v1/data/sys_user/:id:zh-cnandZH-CN→ 200, storedzh-CN;GET /auth/me/localizationserveszh-CN;zh-CNstored unchanged (control);zh_CN→ 400VALIDATION_FAILED, stored value unchanged; a system-contextupdatewithJA-jpstoresja-JP.Acceptance notes
storedSignedInUserLocale(plugin-hono-servercurrent-user-endpoints.ts) trims it and checks the column's shape rule only, so/auth/me/localizationstill serveszh-cnand the console still renders English for that user. That lasts until the column is written again, and the next write is folded.domain:ui, not touched). Once stored values are canonical it matters for two populations only: rows stored before this release, and canonical tags that are not spelled exactly like a console locale (zh,zh-Hans-CN,en-GB). The server's own matcher (resolveLocale: exact → case → base language → variant) would answer those, and the console does not.xx-YY, now storedxx-YY) and''with 200, as the card notes. Refusing either would narrow the door.localeor29895(REST search, 0 hits).Verification
Local runs at head
1c72a05f(one commit on origin/mainc618e890):Build, suites, type-check, lint
pnpm --filter '@objectstack/plugin-auth...' build(underos-verify-lock): VERDICT command-exit 0 (5m03s).pnpm --filter @objectstack/plugin-auth typecheck: VERDICT command-exit 0;check:test-typecheck: OK(test layer, 23 pinned signatures held).pnpm --filter @objectstack/plugin-auth test: Test Files 220 passed (220), Tests 3815 passed, 10 skipped (3825), VERDICT command-exit 0.pnpm --filter '@objectstack/dogfood^...' build: VERDICT command-exit 0 (7m00s); thenpnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/user-locale-case-fold.dogfood.test.ts: Test Files 1 passed (1), Tests 6 passed (6), VERDICT command-exit 0.pnpm lint: exit 0.!in the title, no(narrowing)arm).Ablation (fix committed first, mutation through scripts/ablation-replace.mjs, plugin-auth dist rebuilt both legs)
sys-user-locale-case.ts,const after = canonicalUserLocale(before);→const after = (globalThis as any).ABLATION_29895_NO_FOLD === undefined ? before : canonicalUserLocale(before);(the fold off, which is the old behaviour).ablation-replaceexit 0,ablation-dist-preflight @objectstack/plugin-auth ABLATION_29895_NO_FOLDexit 0 after the rebuild, so the dogfood boot read the mutated dist.expected 'zh-cn' to be 'zh-CN',expected 'ZH-CN' to be 'zh-CN'), the localization door, the system-context write. Green as expected: thezh-CNcontrol and thezh_CN400 case.ablation-replace --restorereports blob == HEAD (e3fd65676c23) andgit diff HEADempty; rebuild;ablation-dist-preflight … --absentexit 0 (working tree clean against HEAD); dogfood file 6/6 again, VERDICT command-exit 0.dispatch-gates --commands (68 derived) and --ran
check-plugin-teardown-shape --self-test(shallow clone: fetched the pinned fixture621a4876, then 48 cases pass) andcheck:dual-build-cjs-loads(seven packages had nodist/; built them under the lock, then exit 0).node scripts/pm/dispatch-gates.mjs --ran:68 derived famil(ies) accounted for — 68 run, 0 NOT-MEASURED, exit 0.check-platform-checklist: not run (no exported symbol renamed or removed).🤖 Generated with Claude Code
https://claude.ai/code/session_01WGufzfsqbTMmPhapjVEeoo
Generated by Claude Code