Skip to content

fix(app-shell): turning on Require MFA on an organization shows the grace period once - #30592

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-30565-mfa-notice-once
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-30565-mfa-notice-once

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #30565

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:组织记录勾选「强制 MFA」并保存成功后,提示里只出现服务端那句宽限期说明(天数取自部署设置);控制台自己那句写死「7 天」的说明不再追加在下面。
  • 为什么改:服务端自 PR fix(plugin-auth,platform-objects): an organization's require-MFA refusal reads in the admin's language, a turn-on names the grace period, and the help says two-factor must be on (#30411) #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.

Premise, tests, ablation
  • 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.
Gates

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.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (no schema touched)
  • 2. Every name I added uses the casing for its kind.
  • 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)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (not touched)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English. (except the 维护者速读 block the os-dev contract asks for)

Generated by Claude Code

…d notice once

An accepted require_mfa turn-on on sys_organization now answers a save
notice from the server naming the grace period in force. The record form
no longer appends its own console.organizationMfa.nowRequired line (which
names the default 7 days) when the answer's notice is that server notice,
recognized by its opening sentence in the four languages the server
writes it in. No notice, or another hook's notice, keeps the console line.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgVfPeFZkN1FP93imKU
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
An organization save that turns require_mfa on and whose answer carries
the server's MFA notice toasts that notice alone; with no notice, or
another hook's notice, the console line stays. Pinned in the record-save
toast harness, so the AppContent wiring (answerNotice) is under test too.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgVfPeFZkN1FP93imKU
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-sales · session_01L6bhgVfPeFZkN1FP93imKU

维护者速读

席位核对 — ACCEPT · head 9dee7aa

  • 形态:5 文件 +167/−2(organizationMfaNotices.ts +36/−1、AppContent.tsx +3;organizationMfaNotices-30295.test.tsx +51、AppContent.recordSaveOneToast-27999.test.tsx +65/−1;changeset +12,@object-ui/app-shell patch)。check-governed-merges --pr 30592:NOT governed,169 行 ≤ 3000。Fixes #30565,draft,目标 main,标题无 !。文件面都在认领 6663346354 之内:27999 测试与认领写的 30295 测试同在 console/__tests__,属认领的「or a test beside it」。
  • 契约:Clause-②: no:organizationMfaSavedNotice 只在 app-shell 内部使用(包入口不转出),多一个可选入参,没有新导出、没有新文案键。Metadata-protocol: none:spec 与服务器都没动,控制台只是读已有的回答 notice。复核面 TIER_DEFAULT,由本席在此写出。
  • 席位逐行读:四个开头句本席与 origin/main 上 packages/plugins/plugin-auth/src/organization-mfa-enrollment-guard.ts 的 ORGANIZATION_MFA_REQUIRED_NOTICE_COPY 逐句对过,en、zh-CN、ja-JP、es-ES 的 N 天句与 0 天句都以同一句开头;其他语言的调用方由 copyLocaleFor 回落英文,英文开头句照样认得出。判断放在原有三道判断之后,所以对象不是组织、存量已开、保存后没开这三种情况行为不变。不用「有 notice 就算」是对的:回答只带一条 notice,后写的覆盖先写的,MFA 守卫在优先级 15 写入,之后的 hook 或流程的 save_notice 会把它换掉;只看有无会把唯一讲谁会被拦截的那句删掉。AppContent.tsx 只多传 answerNotice: claimed?.notice,下面的拼接没动:控制台那句为空时直接沿用 claim 的提示。27999 测试的装置只加了一个 sys_organization 对象、适配器 findOne(存量 require_mfa: false),saveFrom 的对象名断言改成读写入事件的 resource,原有用例的 resource 都是 showcase_task,断言的值不变。
  • 前提实测:dev 报告 6664183736 与第二轮 6664757171。改前在 27b0bc79 用一次性测试把 AppContent 的拼接原样跑过 toastRecordSave(en,服务器 3 天 notice):一个提示、两行,一行 3 天一行 7 天;改后一行、只有 3 天。30295 文件新增 7 例,五例服务器文案与服务器源码求值逐字节相同,改前 5 红 9 绿、改后 14/14。27999 文件新增 3 例(服务器 notice 只剩一句;CONTROL 无 notice 是控制台那句;CONTROL 他 hook 的 notice 两句都在)。消融三腿:删判断一行 5 红 9 绿;换成「有 notice 就算」1 红(他 hook 那例);删 AppContent.tsx 的传参一行 1 红 11 绿(正是服务器 notice 那例);各自还原后 blob 与 HEAD 相同。两份测试 26/26;console/__tests__ 75 文件 619 例绿;app-shell 两套 tsconfig type-check 过;eslint 0 error;派生门禁 35 条在 9dee7aa4 上全部 exit 0,0 NOT-MEASURED。
  • 门禁 check-runs:head 上 55 个(按名取最新一次),42 success,13 skipped,0 红;check-expected-skips --pr 30592:13 个跳过全在 main 的名册里(exit 0,其中 Auto Label、Check PR Size 是本席改正文的 edited 事件上照例跳过)。Build & E2E UI、Live E2E UI、Lint UI、Test UI 16 片与汇总、Type Check UI 与各 Type Check 均绿,Check Changeset 与 PR-body guards 在本席改正文后重跑 success。8aa05756 上 Test UI 汇总红是被 9dee7aa4 推送取消的分片(12 片 cancelled),不是真红。mergeable_state clean。
  • 接受的偏差:第二轮的挂载层测试是本席派的(dev 首轮自己指出传参一行无人钉住)。dev 按 os-dev 约定只在开 PR 时写正文,第二轮正文由本席经中继 issue_patch 发出(12320 字节回读一致,末行 signed trailer),保留原有页脚。临时复现测试提交前已删。
  • 范围外去向:服务器这段文案只写了四种语言,控制台有十种:de、fr、ko、pt、ru、ar 的管理员以前看到「英文服务器 notice + 本地化的控制台句」,现在只看到英文那句。补全服务器文案属裁定 F(Platform locale table (ruling F of #29349): one list of 11 languages in spec, complete on every surface #29428,每种语言在每个面都完整)的服务器半边,本席落地时在 i18n phase 3 (platform metadata and built-in emails): extraction and bundle checks read PLATFORM_LOCALES; backfill the seven missing locales #30512 留言携带(plugin-auth 手写文案不在其包清单里),并写明 ui 的四个开头句须同 PR 跟进,不另立卡。四个开头句是服务器文案在 ui 的第二份副本,没有门禁核对两边;认不出时退回两句都显示,方向安全;要去掉副本得给回答 notice 加结构化代码,是契约改动,记录不立卡。
  • 落地条款:中继 pr_ready + automerge_enable。合并后由 Fixes 关 Organization record: turning on "Require MFA" shows the grace period twice, and the console's line says "7 days" even when Setup set another number #30565,席位同一笔摘掉 pm:dispatched 并留落地评论,再在 i18n phase 3 (platform metadata and built-in emails): extraction and bundle checks read PLATFORM_LOCALES; backfill the seven missing locales #30512 留上面那条携带评论。

Generated by Claude Code

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 tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Organization record: turning on "Require MFA" shows the grace period twice, and the console's line says "7 days" even when Setup set another number

1 participant