Skip to content

Commit e8e924f

Browse files
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)
<!-- 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
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/plugin-auth': patch
3+
'@objectstack/platform-objects': patch
4+
---
5+
6+
fix(plugin-auth,platform-objects): an organization's require-MFA refusal reads in the caller's language, an accepted save that turns it on names the grace period in force, and the field help says two-factor must be on (#30411)
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
- **Before.** Saving `sys_organization.require_mfa` on while two-factor enrollment was off was refused with an English sentence, whatever language the administrator's console used. An accepted save that turned it on answered nothing about when members would be blocked, and the configured grace period (`auth.mfa_grace_period_days`) was readable only through the `auth` settings, which need `manage_platform_settings`, a capability an organization administrator does not hold. The field help did not say the requirement needs two-factor on.
12+
- **Now.** The refusal is in the request's language (English, Simplified Chinese, Japanese, Spanish; matched from `Accept-Language` exactly, then by language, else English), with the same shape as before: `400 VALIDATION_FAILED`, one `require_mfa` / `invalid_value` field entry carrying the same sentence as the top-level message. A save that is admitted and turns the requirement on (two-factor on, the stored value off, or an insert) answers a save `notice` on the create or update answer, in the same language: every member without an authenticator app must enroll, within the grace period in force, in days, read per write from the same setting the MFA gate's clock holds to. A save that turns nothing on (clearing it, re-saving a stored `true`, writing other columns) answers no notice. The field help, in all four languages, says the requirement needs two-factor authentication on for the deployment (`OS_AUTH_TWO_FACTOR=true`) and that saving it on is refused while it is off.
13+
- **Unchanged.** Which writes are refused and which are admitted, the refusal's code, status and field entry, the MFA gate and its grace clock, and every `auth` setting and its read permission. No member is added to any answer: `notice` is the create and update answers' existing optional member.

‎packages/platform-objects/src/apps/translations/en.objects.generated.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
557557
},
558558
require_mfa: {
559559
label: "Require Multi-Factor Auth",
560-
help: "When true, every member of this organization must enroll an authenticator app to access data."
560+
help: "When true, every member of this organization must enroll an authenticator app to access data. Needs two-factor authentication on for this deployment (OS_AUTH_TWO_FACTOR=true); while it is off, saving this as true is refused."
561561
},
562562
timezone: {
563563
label: "Timezone",

‎packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
557557
},
558558
require_mfa: {
559559
label: "Exigir autenticación multifactor",
560-
help: "Si es true, todos los miembros de esta organización deben registrar una aplicación de autenticación para acceder a los datos."
560+
help: "Si es true, todos los miembros de esta organización deben registrar una aplicación de autenticación para acceder a los datos. Requiere que la autenticación de dos factores esté activada en esta implementación (OS_AUTH_TWO_FACTOR=true); mientras esté desactivada, guardarlo como true se rechaza."
561561
},
562562
timezone: {
563563
label: "Zona horaria",

‎packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
557557
},
558558
require_mfa: {
559559
label: "多要素認証を必須化",
560-
help: "true の場合、この組織のすべてのメンバーはデータにアクセスするために認証アプリの登録が必要になります。"
560+
help: "true の場合、この組織のすべてのメンバーはデータにアクセスするために認証アプリの登録が必要になります。このデプロイで二要素認証がオンになっている必要があります(OS_AUTH_TWO_FACTOR=true)。オフの間は true で保存すると拒否されます。"
561561
},
562562
timezone: {
563563
label: "タイムゾーン",

‎packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
557557
},
558558
require_mfa: {
559559
label: "强制多因素认证",
560-
help: "为 true 时,该组织的每位成员都必须注册身份验证器 App 才能访问数据。"
560+
help: "为 true 时,该组织的每位成员都必须注册身份验证器 App 才能访问数据。需要本部署开启双重验证(OS_AUTH_TWO_FACTOR=true);双重验证关闭时,保存为 true 会被拒绝。"
561561
},
562562
timezone: {
563563
label: "时区",

‎packages/platform-objects/src/identity/sys-organization-update-door.test.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@
4343
import { describe, it, expect } from 'vitest';
4444
import { checkManagedApiMethodAffordances } from '@objectstack/spec/data';
4545
import { SysOrganization } from './sys-organization.object';
46+
import { enObjects } from '../apps/translations/en.objects.generated.js';
47+
import { esESObjects } from '../apps/translations/es-ES.objects.generated.js';
48+
import { jaJPObjects } from '../apps/translations/ja-JP.objects.generated.js';
49+
import { zhCNObjects } from '../apps/translations/zh-CN.objects.generated.js';
4650

4751
describe('#15873 — sys_organization.enable.apiMethods admits `update`, and only `update`', () => {
4852
it('declares exactly get / list / update — no create, no delete, no bulk', () => {
@@ -89,3 +93,23 @@ describe('#15873 — better-auth keeps its own door for its own columns', () =>
8993
expect(create.target).toBe('/api/v1/auth/organization/create');
9094
});
9195
});
96+
97+
describe('#30411 — require_mfa\'s help says it needs two-factor on, in the source and in every locale', () => {
98+
const bundles = { en: enObjects, 'zh-CN': zhCNObjects, 'ja-JP': jaJPObjects, 'es-ES': esESObjects };
99+
100+
it('the source description names the deployment switch the save is refused without', () => {
101+
const description = SysOrganization.fields.require_mfa?.description;
102+
expect(description).toMatch(/two-factor authentication on for this deployment/);
103+
expect(description).toMatch(/OS_AUTH_TWO_FACTOR=true/);
104+
});
105+
106+
it.each(Object.entries(bundles))('%s: the generated help carries the condition', (_locale, bundle) => {
107+
const help = (bundle as any).sys_organization?.fields?.require_mfa?.help;
108+
expect(help).toEqual(expect.any(String));
109+
expect(help).toMatch(/OS_AUTH_TWO_FACTOR=true/);
110+
});
111+
112+
it('the en bundle is the source, verbatim (the generator writes it)', () => {
113+
expect((enObjects as any).sys_organization.fields.require_mfa.help).toBe(SysOrganization.fields.require_mfa?.description);
114+
});
115+
});

‎packages/platform-objects/src/identity/sys-organization.object.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,13 +395,18 @@ export const SysOrganization = ObjectSchema.create({
395395
// ADR-0069 D3 — per-org MFA tightening above the global floor. When true,
396396
// members of this org must enrol TOTP to access data (enforced at the
397397
// session-validation gate). An org can only tighten, never loosen, the
398-
// global `mfa_required` setting.
398+
// global `mfa_required` setting. Saving it on is refused while two-factor
399+
// enrollment is off for the deployment (plugin-auth's
400+
// `organization-mfa-enrollment-guard.ts`), which the help says (#30411).
399401
require_mfa: Field.boolean({
400402
label: 'Require Multi-Factor Auth',
401403
required: false,
402404
defaultValue: false,
403405
group: 'Configuration',
404-
description: 'When true, every member of this organization must enroll an authenticator app to access data.',
406+
description:
407+
'When true, every member of this organization must enroll an authenticator app to access data. ' +
408+
'Needs two-factor authentication on for this deployment (OS_AUTH_TWO_FACTOR=true); while it is off, ' +
409+
'saving this as true is refused.',
405410
}),
406411

407412
// #14238 — maintainer ruling 2026-09-02, option A (verbatim 「同意」): the

‎packages/plugins/plugin-auth/src/auth-manager-password-session-policy.ts‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,17 @@ export interface PasswordSessionPolicyHost {
7171
resolveActor: AuthManager['resolveActor'];
7272
}
7373

74+
/**
75+
* The MFA grace period in force, in whole days (ADR-0069 D3): the configured
76+
* `mfaGracePeriodDays` (the `auth.mfa_grace_period_days` setting), 7 when
77+
* unset, never below 0. The one reading of it: the gate's clock below and the
78+
* organization MFA guard's save notice (`AuthManager#getMfaGracePeriodDays`)
79+
* both ask here, so the number a saver is told is the number the gate holds to.
80+
*/
81+
export function resolveMfaGracePeriodDays(config: { mfaGracePeriodDays?: number }): number {
82+
return Math.max(0, Math.floor(Number(config.mfaGracePeriodDays ?? 7)));
83+
}
84+
7485
/** The body of `AuthManager#assertPasswordComplexity`; the method's docblock there is its contract. */
7586
export async function assertPasswordComplexity(this: Pick<PasswordSessionPolicyHost, 'config'>, password: string): Promise<void> {
7687
if (!this.config.passwordRequireComplexity) return;
@@ -222,7 +233,7 @@ export async function resolveUserAuthGates(
222233
if (mfaRequired && unenrolled && !this.isTwoFactorEnabled()) {
223234
this.noteMfaRequirementWithoutEnrollment();
224235
} else if (mfaRequired && unenrolled) {
225-
const graceDays = Math.max(0, Math.floor(Number(this.config.mfaGracePeriodDays ?? 7)));
236+
const graceDays = resolveMfaGracePeriodDays(this.config);
226237
let requiredAt = u?.mfa_required_at;
227238
if (!requiredAt) {
228239
requiredAt = new Date();

‎packages/plugins/plugin-auth/src/auth-manager.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ import {
125125
recordPasswordHistory,
126126
refreshMustChangeCacheIfStale,
127127
refreshOrgMfaCacheIfStale,
128+
resolveMfaGracePeriodDays,
128129
resolvePasswordChangeUserId,
129130
resolveUserAuthGates,
130131
stampPasswordChangedAt,
@@ -5360,6 +5361,20 @@ export class AuthManager {
53605361
return resolveTwoFactorEnabled(this.config.plugins);
53615362
}
53625363

5364+
/**
5365+
* @internal Package-internal: read by the organization MFA enrollment guard
5366+
* `AuthPlugin` registers; not a supported entry point.
5367+
*
5368+
* [#30411] The MFA grace period in force, in whole days — the
5369+
* `auth.mfa_grace_period_days` setting as the settings pass applied it,
5370+
* {@link resolveMfaGracePeriodDays} over the live config, the same answer
5371+
* the MFA gate's clock reads. Read per call, so a settings change is seen at
5372+
* once.
5373+
*/
5374+
public getMfaGracePeriodDays(): number {
5375+
return resolveMfaGracePeriodDays(this.config);
5376+
}
5377+
53635378
/**
53645379
* [#30201] Called by the MFA gate when a user is under an MFA requirement
53655380
* (the global `mfaRequired` or their organization's `require_mfa`) that it

‎packages/plugins/plugin-auth/src/auth-plugin.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1944,6 +1944,9 @@ export class AuthPlugin implements Plugin {
19441944
registerOrganizationMfaEnrollmentGuard(engine, {
19451945
packageId: 'com.objectstack.plugin-auth.organization-mfa-enrollment-guard',
19461946
isTwoFactorEnabled: () => this.authManager?.isTwoFactorEnabled() ?? false,
1947+
// [#30411] The grace period an admitted turn-on's save notice names.
1948+
// No manager means two-factor reads off above, so nothing is admitted.
1949+
mfaGracePeriodDays: () => this.authManager?.getMfaGracePeriodDays() ?? Number.NaN,
19471950
});
19481951
// A reader's own named list views: the body is judged on every save as
19491952
// a ListView for the row's own object. See personal-view-hooks.ts.

0 commit comments

Comments
 (0)