Repository navigation
Commit e8e924f
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgVdbeFpuoCy4uKFMsDahtKa)_
domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL
## 维护者速读
-
改了什么:两步验证关着时,组织表单勾上「强制多因素认证」被拒的那句话,改为按管理员的界面语言(中、英、日、西)返回,错误码、状态和字段位置不变;两步验证开着、保存成功把它打开时,保存应答多带一句提示(写入应答已有的
`notice` 字段):没绑定身份验证器的成员要在几天内绑定,天数就是当前生效的宽限期;`require_mfa`
的字段说明四种语言都补上「需要本部署开启双重验证(OS_AUTH_TWO_FACTOR=true),关闭时保存为开会被拒」。
- 为什么改:中文管理员在表单上方看到中文提示,被拒后字段下却是一段英文;宽限期存在 auth 设置里,读它要
`manage_platform_settings`,组织管理员(organization_admin)没有这个权限,所以界面只能写「默认 7
天」;字段说明也没说要先开双重验证。
-
风险与回滚:只改文案和保存成功时的一句提示:哪些写入被拒、哪些放行完全不变,拒绝的错误码、状态、字段条目不变,门禁和宽限期计时不变(宽限期的读取抽成一个函数,门禁和提示读同一个值),不新增任何读口、设置项或应答字段。回滚即撤本
PR。
-
先看:`packages/plugins/plugin-auth/src/organization-mfa-enrollment-guard.ts`(四语拒绝文案、保存提示及何时发出);`auth-manager-password-session-policy.ts`
的
`resolveMfaGracePeriodDays`(门禁读数不变);`packages/platform-objects/src/identity/sys-organization.object.ts`
的 `require_mfa` 说明。
- 两分钟验证:`pnpm --filter @objectstack/plugin-auth exec vitest run
src/organization-mfa-enrollment-guard.test.ts`,预期全部通过,其中真实引擎用例断言 zh-CN
管理员被拒时读到中文、保存成功时 `onNotice` 收到写着生效天数的中文提示。
- 需要你做的:无,席位落地。
Fixes #30411
Clause-②: no
Metadata-protocol: none
## What changes
The three Wanted items of the card, all on the organization write path
(`sys_organization` insert and update, every data door):
1. **The refusal is localized.** `organization-mfa-enrollment-guard.ts`
refuses a write that turns `require_mfa` on while two-factor enrollment
is off, as before. The sentence now reads in the caller's language: the
request's `Accept-Language` tag, which the engine hands every hook as
`ctx.session.locale`, matched by core's `resolveLocale` against the four
languages the copy carries (exact, then by language, so `zh` reaches
`zh-CN`, else English). The copy lives beside the guard, the same home
the global switch's refusal (`mfa-required-enrollment-predicate.ts`)
uses. The field-level shape is unchanged in every language: `400
VALIDATION_FAILED`, one `require_mfa` / `invalid_value` entry whose
`message` is the top-level one. The English sentence is byte-identical
to before.
2. **The grace period is answered by the save.** Measured first: the
`auth` settings namespace that holds `mfa_grace_period_days` is read
through `GET /api/settings/auth`, whose `readPermission` is
`manage_platform_settings`
(`service-settings/src/manifests/auth.manifest.ts`, enforced by
`SettingsService.assertPermitted` for every HTTP caller). The
organization administrator who can edit the organization holds
`organization_admin` (or its wall-less variant), whose system
permissions are `manage_org_users`, `setup.access`, `setup.write`,
`manage_mailboxes` and `manage_connector_connections`
(`plugin-security/src/objects/default-permission-sets.ts`), so that read
answers 403 for them. No read is added and `/api/v1/auth/config` is not
touched. Instead, the card's own alternative: a save the guard admits
that turns the requirement on (two-factor on, the stored value off, or
an insert) calls `ctx.notice(message)`, which the create and update
answers already carry as their optional `notice` member
(`WRITE_ANSWER_NOTICE_DESCRIPTION`, `@objectstack/spec/api`). The
message says every member without an authenticator app must enroll,
within the grace period in force in days (0 and 1 have their own
sentences), in the caller's language. The number is read per write from
`AuthManager.getMfaGracePeriodDays()`, which asks the same
`resolveMfaGracePeriodDays` the MFA gate's clock now asks, so the saver
is told the number the gate holds to. A write that turns nothing on (a
clear, a re-save of a stored `true`, another column) answers no notice;
a refused write answers its error.
3. **The field help.** `sys_organization.require_mfa`'s description says
it needs two-factor authentication on for the deployment
(`OS_AUTH_TWO_FACTOR=true`) and that saving it on is refused while it is
off. `en.objects.generated.ts` is regenerated from the source with `pnpm
i18n:extract`; the three translated values (`zh-CN`, `ja-JP`, `es-ES`)
are re-translated by hand, which AGENTS.md's Documentation Guardrails
allow for translated-locale values (keys unchanged, so no drift).
**The read the ui half should use for the grace period:** the `notice`
member of the update answer of `PATCH
/api/v1/data/sys_organization/{id}` (and of the create answer), which
the console already receives as `RecordSaveFeedback.notice` through
`answerWarningClaim` in
`ui/packages/app-shell/src/console/AppContent.tsx`. It is present only
on a save that turned the requirement on, and is already in the
console's language.
Files: `organization-mfa-enrollment-guard.ts` and its test;
`auth-manager-password-session-policy.ts` (the grace-period reading
extracted, gate unchanged); `auth-manager.ts` (one `@internal`
accessor); `auth-plugin.ts` (one option at the guard's registration,
outside `applySettings`); `sys-organization.object.ts` and the four
`*.objects.generated.ts`; one changeset.
## Why
- **Item 1.** On `main` the guard's
`organizationMfaWithoutEnrollmentRefusal()` (#30293) took no locale and
threw one English sentence whatever the caller's language, so a zh-CN
administrator read a Chinese form and an English refusal under the
field.
- **Item 2, measured** on a real boot (`pnpm dev -- --fresh`, showcase,
single posture; the settings read path is not touched by this diff): an
organization administrator (`organization_admin_no_bypass`, made through
the admin create-user and member-role doors) asking `GET
/api/settings/auth` got `403 SETTINGS_FORBIDDEN` ("requires the
'manage_platform_settings' capability"); the platform administrator got
`200`. No read the organization administrator holds carries
`mfa_grace_period_days`, so the console can only write "7 days unless
changed".
- **Item 3.** `require_mfa`'s description did not say that two-factor
must be on.
## Tests
All in `organization-mfa-enrollment-guard.test.ts`, as new top-level
`describe` blocks beside the existing ones:
- the refusal in zh-CN and en (first sentence, the env var and the
setting key named, the field entry's message equal to the top-level
one), the locale matching table (`zh`, `zh-cn`, `zh-Hans-CN`, `ja`,
`es-MX`, `fr-FR`, empty), the same envelope in all four languages, and
the hook answering in `ctx.session.locale` (English without one);
- the notice: once per admitted turn-on (insert, update from stored off,
update with no `previous`), its number, its zh-CN text, the 0-day and
1-day sentences in every language, silence for a clear, another column
and a re-save of a stored `true`, and an admitted write with no notice
member;
- on a real ObjectQL engine over the real `sys_organization` behind the
D2 guard: a zh-CN administrator's refused save reads Chinese and the row
keeps its value (control: no locale reads English); an admitted save
hands the write's `onNotice` listener the zh-CN notice with the
configured days, a changed grace period is read on the next write, and a
re-save answers none;
- through `AuthPlugin`'s real `kernel:ready` registration: the live
manager's grace period reaches the notice, and a settings patch changes
it on the next write;
- the gate's reading unchanged: `resolveMfaGracePeriodDays` over
`undefined`, `0`, `14`, `2.9`, `-3`, `120`, and, end to end on
better-auth, a member whose clock started 30 days ago is held under a
20-day and the default period and not under a 40-day one, while
`getMfaGracePeriodDays()` answers the same number.
<details><summary>Ablations (each through
<code>scripts/ablation-replace.mjs</code> in wrap mode, restored, blob
equal to HEAD after)</summary>
Each mutation landed (anchor 1 → 0, blob changed), the guard test file
went red as below, and the file was restored to its HEAD blob (`git diff
HEAD` empty). The file is 56/56 green before and after.
- **A1**, the refusal ignores the caller's locale
(`organizationMfaWithoutEnrollmentRefusal()` without
`callerLocale(ctx)`): the real-engine zh-CN refusal and the hook-level
locale case go red. 2 failed, 54 passed.
- **A2b**, the notice is also sent on a re-save of a stored `true`: five
cases go red, among them "says nothing for a write that turns nothing
on", the real-engine notice case and the `AuthPlugin` registration case.
5 failed, 51 passed.
- **A3**, the grace period is read once and cached: the real-engine
"changed grace period is read on the next write" case and the
`AuthPlugin` settings-patch case go red. 2 failed, 54 passed.
- **A4**, the notice call removed: the four notice cases go red. 4
failed, 52 passed.
- **A5b**, the gate's default moved from 7 to 8 days in
`resolveMfaGracePeriodDays`: the `undefined` row, the end-to-end unset
case and the registration case go red. 3 failed, 53 passed. This is the
pin that the gate's reading did not move.
A first A2 attempt used an anchor that the replacement contained, so the
anchor count did not drop and `ablation-replace` refused it before
running anything (not a measurement). A2b used a different anchor. A1 to
A4 ran before the commit that made the unset grace-period case configure
none (`f2c7a1fa`, test file only); A5b and the green runs ran after it.
The guard and policy blobs are unchanged since (`862fa2ec`, `f7160fdb`).
</details>
## Acceptance notes
- The console shows a save's `notice` and then adds its own after-save
line for this field (`organizationMfaSavedNotice`,
`console.organizationMfa.nowRequired`, which names "7 days unless
changed"). Once this lands, an accepted turn-on shows both. The ui
follow-up is to show the server's notice instead of that line when the
answer carries one; it is outside this diff (`ui/**`) and handed to the
seat.
- `@objectstack/client` has no settings read either; not needed for this
shape, and not touched.
- The pins assert the first sentence and the named subjects (env var,
setting key, day count), not whole sentences, per the repository's rule
that copy is pinned only where its wording is the contract; the language
is the contract here, so the first sentence carries it.
## Verification
All readings are on head `f3b41646`: this branch merged with
`origin/main` `f506b79d`, which carries #30449. Heavy runs were under
`scripts/pm/os-verify-lock.sh` on a shared box.
<details><summary>Real boot (pnpm dev, showcase, fresh
database)</summary>
Two-factor off, signed in as the platform administrator:
- `PATCH /api/v1/data/sys_organization/{id} {"require_mfa":true}` with
`Accept-Language: zh-CN` → `400
{"error":"本组织暂时无法强制多因素认证:本部署未开启双重验证的绑定,…","code":"VALIDATION_FAILED","fields":[{"field":"require_mfa","code":"invalid_value","message":"本组织暂时无法强制多因素认证:…"}],"object":"sys_organization"}`.
- The same with `Accept-Language: en-US` → `400` with the English
sentence, byte-identical to before.
- The `require_mfa` field help is served in zh-CN and en with the new
sentence.
Restarted with `OS_AUTH_TWO_FACTOR=true`:
- The same PATCH with `Accept-Language: zh-CN` → `200`, the record with
`"require_mfa":true`, and `"notice":"本组织现已强制多因素认证。尚未绑定身份验证器应用的成员有 7
天时间完成绑定,…"`.
- `PUT /api/settings/auth {"values":{"mfa_grace_period_days":3}}`, the
column set back to `false`, then on again → the notice reads `有 3 天`,
with no restart.
- A re-save of the stored `true` → `200` with no `notice` key.
</details>
<details><summary>Suites, typecheck, lint</summary>
- `pnpm build --concurrency=2` → `VERDICT command-exit 0`, 119/119
tasks.
- `pnpm --filter @objectstack/plugin-auth test` → Test Files 224 passed
(224), Tests 4001 passed, 10 skipped (4011).
- `pnpm --filter @objectstack/platform-objects test` → Test Files 111
passed (111), Tests 1532 passed (1532).
- `pnpm --filter @objectstack/plugin-auth typecheck` and `pnpm --filter
@objectstack/platform-objects typecheck` → `VERDICT command-exit 0`.
- `src/organization-mfa-enrollment-guard.test.ts` alone → 56/56.
- `node scripts/check-i18n-bundles.mjs` → OK.
- `pnpm lint` → `VERDICT command-exit 0`.
</details>
<details><summary>Full dogfood battery</summary>
`pnpm --filter @objectstack/dogfood test` (no shard, no path), after the
whole-workspace build above, on `f3b41646` → `VERDICT command-exit 0`,
held the lock 29m31s. Test Files 419 passed, 3 skipped (422). Tests 3492
passed, 36 skipped (3528). No failure. The same battery on the pre-merge
head `e950155e` gave the same counts.
</details>
<details><summary>Gates</summary>
`node scripts/pm/dispatch-gates.mjs --commands` derived 66 commands on
`f3b41646`. Each was run with its exit code recorded before any pipe,
and all 66 exited 0. `--ran`: 66 derived, 66 run, 0 NOT-MEASURED, 0
UNRUN.
On the pre-merge head `e950155e`, two commands were non-zero, neither a
finding:
- `check-plugin-teardown-shape --self-test`: exit 3, its pinned fixture
commit missing from the shallow clone. It passed after `git fetch
--depth=1` of that commit.
- `check:type-check-debt`: exit 3, NOT MEASURED, because its closure
build never got a turn on the verify lock while this branch's own
dogfood held it. It exited 0 on the final head.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(No spec change.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: n/a.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English (the 维护者速读 block is the
maintainers' Chinese summary).
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01Utp6Cai3TgWyncLtR5EwF7
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 898145b commit e8e924f
12 files changed
Lines changed: 464 additions & 20 deletions
File tree
- .changeset
- packages
- platform-objects/src
- apps/translations
- identity
- plugins/plugin-auth/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
557 | 557 | | |
558 | 558 | | |
559 | 559 | | |
560 | | - | |
| 560 | + | |
561 | 561 | | |
562 | 562 | | |
563 | 563 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
557 | 557 | | |
558 | 558 | | |
559 | 559 | | |
560 | | - | |
| 560 | + | |
561 | 561 | | |
562 | 562 | | |
563 | 563 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
557 | 557 | | |
558 | 558 | | |
559 | 559 | | |
560 | | - | |
| 560 | + | |
561 | 561 | | |
562 | 562 | | |
563 | 563 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
557 | 557 | | |
558 | 558 | | |
559 | 559 | | |
560 | | - | |
| 560 | + | |
561 | 561 | | |
562 | 562 | | |
563 | 563 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
46 | 50 | | |
47 | 51 | | |
48 | 52 | | |
| |||
89 | 93 | | |
90 | 94 | | |
91 | 95 | | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
395 | 395 | | |
396 | 396 | | |
397 | 397 | | |
398 | | - | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
399 | 401 | | |
400 | 402 | | |
401 | 403 | | |
402 | 404 | | |
403 | 405 | | |
404 | | - | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
405 | 410 | | |
406 | 411 | | |
407 | 412 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
71 | 71 | | |
72 | 72 | | |
73 | 73 | | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
74 | 85 | | |
75 | 86 | | |
76 | 87 | | |
| |||
222 | 233 | | |
223 | 234 | | |
224 | 235 | | |
225 | | - | |
| 236 | + | |
226 | 237 | | |
227 | 238 | | |
228 | 239 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
125 | 125 | | |
126 | 126 | | |
127 | 127 | | |
| 128 | + | |
128 | 129 | | |
129 | 130 | | |
130 | 131 | | |
| |||
5360 | 5361 | | |
5361 | 5362 | | |
5362 | 5363 | | |
| 5364 | + | |
| 5365 | + | |
| 5366 | + | |
| 5367 | + | |
| 5368 | + | |
| 5369 | + | |
| 5370 | + | |
| 5371 | + | |
| 5372 | + | |
| 5373 | + | |
| 5374 | + | |
| 5375 | + | |
| 5376 | + | |
| 5377 | + | |
5363 | 5378 | | |
5364 | 5379 | | |
5365 | 5380 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1944 | 1944 | | |
1945 | 1945 | | |
1946 | 1946 | | |
| 1947 | + | |
| 1948 | + | |
| 1949 | + | |
1947 | 1950 | | |
1948 | 1951 | | |
1949 | 1952 | | |
| |||
0 commit comments