Repository navigation
Commit 5b748df
fix(app-shell): turning on Require MFA on an organization shows the grace period once (#30592)
Fixes #30565
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:组织记录勾选「强制 MFA」并保存成功后,提示里只出现服务端那句宽限期说明(天数取自部署设置);控制台自己那句写死「7
天」的说明不再追加在下面。
- 为什么改:服务端自 PR #30529 起会在保存回答里带上真实宽限期的说明,控制台再加一句就重复两遍,Setup 改成 3
天时两句还互相矛盾(在 main `27b0bc79` 上用单元复现:提示两行,一行 3 天、一行 7 天)。
- 风险与回滚:只看回答里的 notice 是否以服务端那句开头(四种语言);认不出时退回原样显示两句,不会两句都没有。没有
notice(旧服务端)照旧显示控制台那句。无数据迁移,回滚即还原本 PR。
- 先看:`ui/packages/app-shell/src/console/organizationMfaNotices.ts` 的
`SERVER_MFA_NOTICE_OPENINGS` 与 `isServerMfaNotice`;`AppContent.tsx` 的
`handleRecordFormSuccess` 里多传的 `answerNotice: claimed?.notice`。
- 两分钟验证:`pnpm --dir ui exec vitest run
packages/app-shell/src/console/__tests__/organizationMfaNotices-30295.test.tsx
packages/app-shell/src/console/__tests__/AppContent.recordSaveOneToast-27999.test.tsx`,预期
26 个用例全绿。
- 需要你做的:无,席位落地。
## What changes
- **`@object-ui/app-shell`, `organizationMfaSavedNotice`**
(`console/organizationMfaNotices.ts`) takes one more optional input,
`answerNotice`: the save answer's `notice` as the record form's claim
took it. When that notice is the server's own MFA notice, the function
returns `undefined`, so the console adds no
`console.organizationMfa.nowRequired` line. Everything else it judged
before (object, stored value, saved value) is unchanged.
- **`AppContent.tsx`, `handleRecordFormSuccess`** passes `answerNotice:
claimed?.notice` to it. The join below (`claimed.notice` + the console
line) is unchanged: with no console line it hands the claim's feedback
through, so the toast carries the server's notice once.
- **The rule that tells the server's notice apart:** the notice contains
the server copy's opening sentence in one of the four languages it is
written in (`en`, `zh-CN`, `ja-JP`, `es-ES`; plugin-auth's
`ORGANIZATION_MFA_REQUIRED_NOTICE_COPY`). The N-day and 0-day sentences
open with the same one, so four constants cover all of them. A bare "a
notice is there" check is not enough: the answer carries ONE notice, the
last one a hook or a flow's `save_notice` node on the write set
(`WRITE_ANSWER_NOTICE_DESCRIPTION` in the protocol, "when more than one
call sets it, the LAST one wins"). The MFA guard sets its notice at
`beforeInsert`/`beforeUpdate` priority 15, so any later hook replaces
it. On that save a presence check would drop the only sentence that
tells the administrator who will be blocked. The wrong direction for
this rule is also the safe one: if the server's wording ever changes,
the console shows both lines again, never neither.
- No i18n change: the console sentence still serves an answer that
carries no server notice.
- **Changeset:** `.changeset/30565-organization-mfa-notice-once.md`,
`@object-ui/app-shell` patch.
## Tests
A new top-level `describe` in
`ui/packages/app-shell/src/console/__tests__/organizationMfaNotices-30295.test.tsx`
(this module's existing harness):
- The answer carries the server notice: no console line. Five cases, the
server's text verbatim: `en` 3 days, `en` 0 days, `zh-CN` 3 days,
`ja-JP` 0 days, `es-ES` 3 days. Each fixture was checked byte-identical
to `ORGANIZATION_MFA_REQUIRED_NOTICE_COPY[locale](days)` evaluated from
the server source at this head.
- No notice on the answer: the console line, as before.
- Another hook's notice on the same save (`Sent to your manager for
approval`): the console line too.
A new top-level `describe` in
`ui/packages/app-shell/src/console/__tests__/AppContent.recordSaveOneToast-27999.test.tsx`
(the record-save toast harness, a real `AppContent` mount; the harness
gains a `sys_organization` object, a `findOne` on its adapter stub
answering the stored row with `require_mfa: false`, and `saveFrom` now
checks the overlay edits the object the write names). An edit of
organization `org_1` saves `require_mfa: true`:
- The answer's `notice` is the server's English 3-day notice (the 30295
fixture): ONE success toast, `Organization "Acme" updated`, whose
description is the server notice alone; the stored row was read
(`findOne('sys_organization', 'org_1')`).
- CONTROL: no notice on the answer: the description is the console line
(`console.organizationMfa.nowRequired` in `en`, which names 7 days).
- CONTROL: another hook's notice on the answer: the description is that
notice, then the console line.
## Acceptance notes
- **The two copies, compared.** The console line says: members without
an authenticator app must enroll; they are blocked from data once the
grace period ends; "7 days unless changed in Setup → Authentication";
counted from first use after the change; members who already use one are
not affected. The server notice says all of that, with the real number
of days and where it is set. Nothing the console line says is lost.
- **Languages.** The server copy is written in four languages, and other
caller locales get the English one (`copyLocaleFor`). The console ships
ten locales. A console in `de`, `fr`, `ko`, `pt`, `ru` or `ar` used to
show the English server notice followed by its own localized line. It
now shows the English server notice only. Adding those six languages to
the server copy belongs to the server lane (out of scope here).
- **Mount-level pin.** The decision is pinned on the function, and the
one wiring line in `AppContent.tsx` (`answerNotice: claimed?.notice`) is
pinned on a mounted `AppContent` in the shared record-save toast harness
(see Tests). Deleting that line reds the server-notice case (ablation
3).
- The four opening sentences are a second copy of server text in the
console, and no gate checks the two against each other. The test
fixtures are the server's text at this head. A protocol-level code on
the answer's notice would remove the copy, but that is a contract
change, out of scope.
## Verification
Base `27b0bc79`, head `9dee7aa4`. The fix is `8aa05756`; `9dee7aa4` adds
only the mount-level pin.
<details><summary>Premise, tests, ablation</summary>
- **Premise, on the base** (a throwaway test, deleted before the commit,
that runs `AppContent`'s join verbatim through `toastRecordSave` with
the `en` catalogue and the server's 3-day notice): one success toast
whose description has **2 lines**, containing both "3 days" and "7
days". The same probe on the fix: 1 line, "3 days" only.
- New pins on the base: 5 failed (the five server-notice cases), 9
passed.
- `pnpm --dir ui exec vitest run --maxWorkers=2
packages/app-shell/src/console/__tests__/organizationMfaNotices-30295.test.tsx`
on the head: Test Files 1 passed (1), Tests 14 passed (14).
- `pnpm --dir ui exec vitest run --maxWorkers=2
packages/app-shell/src/console/__tests__/AppContent.recordSaveOneToast-27999.test.tsx
packages/app-shell/src/console/__tests__/organizationMfaNotices-30295.test.tsx`
on `9dee7aa4`: Test Files 2 passed (2), Tests 26 passed (26).
- `pnpm --dir ui exec vitest run --maxWorkers=2
packages/app-shell/src/console/__tests__/` under the verify lock on
`9dee7aa4`: Test Files 75 passed (75), Tests 619 passed (619), VERDICT
command-exit 0 (616 on `8aa05756`).
- **Ablation 1** (`scripts/ablation-replace.mjs`, delete `if
(isServerMfaNotice(input.answerNotice)) return undefined;`, anchor x1 to
x0): 5 failed / 9 passed, the five server-notice cases red, the
no-notice and other-notice controls green. Restored: blob `63b87c8c`
equals HEAD, `git diff HEAD` empty.
- **Ablation 2** (the same line replaced by a presence check, `if
(input.answerNotice) return undefined;`): 1 failed / 13 passed, the
other-hook-notice case red. Restored: blob equals HEAD, `git diff HEAD`
empty.
- **Ablation 3** (on `9dee7aa4`, delete `answerNotice: claimed?.notice,`
in `AppContent.tsx`, anchor x1 to x0, blob `b8669035` to `adc800b5`): 1
failed / 11 passed in the 27999 harness, the server-notice case red,
both CONTROLs and the nine earlier cases green. Restored: blob
`b8669035` equals HEAD, `git diff HEAD` empty.
</details>
<details><summary>Gates</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` on head `9dee7aa4` (5 paths) derived the
same 35 commands as on `8aa05756`; each was re-run on `9dee7aa4` and its
exit code recorded before any pipe:
- `node scripts/check-adr-0087-registration.mjs --base origin/main` ::
exit 0
- `node scripts/check-adr-0087-registration.mjs --self-test` :: exit 0
- `node scripts/check-changeset-no-major.mjs --base origin/main` :: exit
0
- `node scripts/check-closing-keyword-parity.mjs` :: exit 0
- `node scripts/check-closing-keyword-parity.mjs --self-test` :: exit 0
- `node scripts/check-comment-mask-corpus.mjs` :: exit 0
- `node scripts/check-empty-changeset.mjs --base origin/main` :: exit 0
- `node scripts/check-empty-changeset.mjs --self-test` :: exit 0
- `node scripts/check-sdui-manifest.mjs` :: exit 0
- `node scripts/check-sdui-manifest.mjs --self-test` :: exit 0
- `node scripts/pm/release-rehearsal-clone.mjs --self-test` :: exit 0
- `node scripts/release-pending-publish.mjs --self-test` :: exit 0
- `pnpm --dir ui exec vitest run scripts/` :: exit 0
- `pnpm --filter @objectstack/spec run check:duration-unit-keys` :: exit
0
- `pnpm check:changeset-gate-self-tests` :: exit 0
- `pnpm check:closed-ceremony-text` :: exit 0
- `pnpm check:cross-package-test-inputs` :: exit 0
- `pnpm check:dispatcher-error-vocabulary` :: exit 0
- `pnpm check:driver-memory-census` :: exit 0
- `pnpm check:engine-double-contract` :: exit 0
- `pnpm check:gitlink-declared` :: exit 0
- `pnpm check:nul-bytes` :: exit 0
- `pnpm check:published-files` :: exit 0
- `pnpm check:query-options-erasure` :: exit 0
- `pnpm check:refd-timer-probe` :: exit 0
- `pnpm check:registry-log-declared` :: exit 0
- `pnpm check:rest-log-spy-declared` :: exit 0
- `pnpm check:skill-identifier-liveness` :: exit 0
- `pnpm check:test-source-alias` :: exit 0
- `pnpm check:tier-file-adoption` :: exit 0
- `pnpm check:type-check-coverage` :: exit 0
- `pnpm check:type-check-debt` :: exit 0
- `pnpm check:undeclared-dep-imports` :: exit 0
- `pnpm check:watch-hint-literal` :: exit 0
- `pnpm check:where-matcher` :: exit 0
`node scripts/pm/dispatch-gates.mjs --ran ran.list --repo
objectstack-ai/objectstack` on `9dee7aa4` :: exit 0, "35 derived, 35
run, 0 NOT-MEASURED, 0 UNRUN".
Also:
- `pnpm --dir ui run check:spec-symbols` :: exit 0
- `node scripts/check-new-cross-file-line-citations.mjs` (ui) :: exit 0,
0 new citations
- `node scripts/check-control-bytes.mjs` (ui) :: exit 0
- `pnpm exec turbo run type-check --filter=@object-ui/app-shell` under
the verify lock on `9dee7aa4` (cache miss on
`@object-ui/app-shell:type-check`, both `tsc --noEmit` and `tsc -p
tsconfig.test.json`, so the test files are compiled) :: VERDICT
command-exit 0, 68 tasks successful
- `eslint` on the changed source and test files :: exit 0, 0 errors (the
27999 harness: 0 problems)
CI runs the rest.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(no schema touched)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes. (eslint on the changed
source and test files: 0 errors; its warnings are on `AppContent.tsx`
lines this PR does not change)
- [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: its generated artifacts are
regenerated and committed. (not touched)
- [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. (except the `维护者速读`
block the os-dev contract asks for)
---
_Generated by [Claude
Code](https://claude.ai/code/session_01L6bhgVfPeFZkN1FP93imKU)_
<!-- signed: domain:ui#4 · os-sales · session_01L6bhgVfPeFZkN1FP93imKU
-->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 4831d50 commit 5b748df
5 files changed
Lines changed: 167 additions & 2 deletions
File tree
- .changeset
- ui/packages/app-shell/src/console
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1058 | 1058 | | |
1059 | 1059 | | |
1060 | 1060 | | |
| 1061 | + | |
| 1062 | + | |
1061 | 1063 | | |
1062 | 1064 | | |
1063 | 1065 | | |
1064 | 1066 | | |
| 1067 | + | |
1065 | 1068 | | |
1066 | 1069 | | |
1067 | 1070 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
97 | 97 | | |
98 | 98 | | |
99 | 99 | | |
| 100 | + | |
| 101 | + | |
100 | 102 | | |
101 | 103 | | |
102 | 104 | | |
| |||
113 | 115 | | |
114 | 116 | | |
115 | 117 | | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
116 | 125 | | |
117 | 126 | | |
118 | 127 | | |
| |||
211 | 220 | | |
212 | 221 | | |
213 | 222 | | |
214 | | - | |
| 223 | + | |
| 224 | + | |
215 | 225 | | |
216 | 226 | | |
217 | 227 | | |
| |||
326 | 336 | | |
327 | 337 | | |
328 | 338 | | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
60 | 60 | | |
61 | 61 | | |
62 | 62 | | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
63 | 114 | | |
64 | 115 | | |
65 | 116 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | | - | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
29 | 32 | | |
30 | 33 | | |
31 | 34 | | |
| |||
57 | 60 | | |
58 | 61 | | |
59 | 62 | | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
60 | 88 | | |
61 | 89 | | |
62 | 90 | | |
63 | 91 | | |
64 | 92 | | |
65 | 93 | | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
66 | 98 | | |
67 | 99 | | |
68 | 100 | | |
69 | 101 | | |
70 | 102 | | |
| 103 | + | |
| 104 | + | |
71 | 105 | | |
72 | 106 | | |
73 | 107 | | |
74 | 108 | | |
75 | 109 | | |
76 | 110 | | |
| 111 | + | |
77 | 112 | | |
78 | 113 | | |
79 | 114 | | |
| |||
0 commit comments