Skip to content

fix(plugin-auth)!: clearing an authentication setting, or resetting the namespace, takes effect at once (#30312) - #30447

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-nbraza
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-nbraza

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

维护者速读

  • 改了什么:Setup → 认证里清空一个设置(或"恢复默认"整组重置)后,运行值当场回到部署自己声明的值(插件选项 / auth:configure,没声明就回内置默认),不再要重启。覆盖全部 33 个认证设置:锁定阈值、每人最大会话、密码策略、会话时长、强制 MFA、API 密钥期限、限流、Google、受众策略等;IP 白名单(Allowed IP ranges: an administrator can exclude their own address unwarned, and clearing the field does not take effect until a restart #30203)并入同一机制。
  • 为什么改:清空后界面显示默认值,但运行时一直沿用上次存的值,直到重启(实测:锁定阈值 3、最大会话 2 清空后仍生效)。管理员以为已经恢复默认,其实没有。
  • 风险与回滚:收窄一处(Clause-② yes (narrowing)):部署声明"邮箱密码登录关"、在 Setup 里打开、再关掉了 Google,此时清空"邮箱密码登录"会被拒存(400 SETTINGS_VALIDATION),因为清空现在真的会把它关掉、导致无人能登录。其余清空只是开始生效。环境变量锁定的设置不受影响。回滚即撤本 PR。
  • 先看:packages/plugins/plugin-auth/src/auth-settings-declared.ts(回落表,每个键走哪一片);auth-plugin.ts 的 applySettings 新增回落段与受众回落;sign-in-method-floor.ts 清空按声明值判的一行。
  • 两分钟验证:pnpm --filter @objectstack/plugin-auth exec vitest run src/auth-settings-clear.pin.test.ts,预期 17/17 通过,其中第一组断言锁定阈值清空后当场回到声明的 7。
  • 需要你做的:无,席位落地。

Fixes #30312

Clause-②: yes (narrowing) — one save an administrator could make before is now refused at the auth settings save door with the existing SETTINGS_VALIDATION / invalid_value field error at email_password_enabled: clearing that toggle on a deployment that itself declared email and password off, when nothing else signs anyone in after the save. Before, that clear was saved and changed nothing at runtime; now a clear applies, and that one would lock everyone out. No error code, settings key or export is added or removed.
Metadata-protocol: none
Skills-docs: none — no file under skills/** teaches the auth settings namespace, its clear or the sign-in floor.

Why

PUT /api/settings/auth with a key set to null (the console's clear), or the namespace reset, answers saved, and the key resolves source: 'default'. Every if (isExplicit(key)) branch in applySettings skipped such a key, so the running value kept the last stored one until a restart. Re-measured first on dbf53efe, real AuthPlugin + SettingsService: lockout_threshold 3 and max_concurrent_sessions_per_user 2 stayed 3 / 2 after setMany(…, null); a reset left lockout_threshold 4. Only allowed_ip_ranges had a reset arm (#30203, PR #30309).

What changes

  • One mechanism, not thirty else arms. auth-settings-declared.ts holds two pieces of data. captureDeclaredAuthOptions takes, once in init() after auth:configure and before the manager is built, the deployment's own value of every option an auth.* key drives. AUTH_SETTING_RESTORE names, for each of the 33 keys of the shipped auth manifest, the piece its declared value goes back through.
  • The restore arm in applySettings. The pass keeps the set of keys the running config last took from the store. A key in that set that now resolves source: 'default' is restored in the same pass, through the same piece a save of it rides, so the manager judges a restore as it judges a save:
    • plain options (membership policy, password complexity / classes / history / expiry, MFA grace, the four API-key keys, session idle / absolute / concurrent, lockout pair, allowed_ip_ranges) ride the plain patch;
    • emailAndPassword fields (email_password_enabled, signup_enabled, require_email_verification, password_min_length, password_max_length) are each their own refusable piece; the verification flag restores as the deployment's declaration (plugin-auth: under the open audience posture, honour an explicit requireEmailVerification: false (email_domain stays forced) #20389), as at boot;
    • password_reject_breached is its own plugins piece;
    • mfa_required restores as a save of the declared value would apply: a declared on is one piece with plugins.twoFactor, a declared off clears the requirement and leaves the enrollment path standing, exactly as an explicit off does;
    • session_expiry_days / session_refresh_days merge over the declared session block (a stored field overrides its declared twin; before, a stored field replaced the whole block);
    • the rate-limit pair and the Google trio restore whole once none of their keys is stored; the audience restores whole once audience_posture is not stored, through the same merged-result validation (a sibling still stored without a posture is IGNORED, as before).
    • A key whose restore the manager refuses stays owed and is retried on the next pass. A key the store never gave a value is never touched (boot restores nothing). Env overrides resolve source: 'env' and are unaffected.
  • allowed_ip_ranges moves onto the same table; declaredAllowedIpRanges / allowedIpRangesFromSettings are gone. Its Allowed IP ranges: an administrator can exclude their own address unwarned, and clearing the field does not take effect until a restart #30203 pins pass unchanged (an undeclared list restores to undefined, which the gate reads as open, as [] was).
  • The sign-in floor (sign-in-method-floor.ts, file surface widened by the seat). The floor read a cleared email_password_enabled as on (the manifest default). With clears now applying, a deployment that declared email and password off could clear the toggle into a lockout. Measured before the floor change: declared off + Google, Setup sets email/password on, then Google off, then clears the toggle → 200, and the running config ends emailPassword.enabled: false with no provider. The floor now takes an optional declaredEmailPasswordEnabled() reader (from bindAuthSettings) and judges a cleared toggle at the declaration; the same clear is refused 400 and nothing changes. With another way in standing, the clear saves and applies.

Tests

packages/plugins/plugin-auth/src/auth-settings-clear.pin.test.ts, 17 cases, on #30203's composition (real AuthPlugin, real SettingsService with the shipped auth manifest, real settings routes for the save-door cases):

  1. The restore table names exactly the shipped manifest's keys.
  2. Plain options: the measured case (declared lockoutThreshold: 7 back to 7; undeclared max sessions back to undefined); password classes + API-key max expiry; a pass about one stored key applies only that key's option (applyConfigPatch spy).
  3. Refusable pieces: emailAndPassword fields; breached-password flag; mfa_required to a declared off (enrollment stays) and to a declared on (with twoFactor); the floor refuses the clear with no other way in (code, field, invalid_value, runtime unchanged) and lets it through with Google standing.
  4. Groups: session merge; rate-limit pair; Google; audience (all three cleared; posture alone cleared with siblings stored).
  5. Namespace reset across families; an env-pinned OS_AUTH_LOCKOUT_THRESHOLD survives a reset.

Ablations, each through scripts/ablation-replace.mjs (anchor 1 → 0, restored to the HEAD blob, git diff HEAD empty; tests import src/, no dist in the path):

Ablated Red
A1 plain-patch arm 7 (incl. all three #30203 clear pins)
A2 emailAndPassword arm 3
A3 plugins arm 1
A4 session arm 1
A5 mfa_required declared-off arm 1
A6 mfa_required declared-on arm 1
A7 rate-limit group 2
A8 Google group 1
A9 audience restore 2
A10 floor reads a clear at the declaration 1
A11 store tracking (restore every unstored key) 1 (the spy pin)

Acceptance notes

Verification

Readings under scripts/pm/os-verify-lock.sh, shared box. Landing head c22645eb: 8b9f159f plus a merge of origin/main at f506b79d (29 commits, among them PR #30449 for #30300). One hand resolution, in auth-plugin.ts: #30449's one-line registerSignInMethodFloor(…, this.signInMethodFloorDeps(…), …) taken, and this PR's declaredEmailPasswordEnabled reader moved into that shared constructor (read only by the save floor's batch fallback; the SSO-provider delete floor and the boot report judge the running config). Delta contract review PASS, comment 6659465418.

On c22645eb, one run under the verify lock (VERDICT command-exit 0, every step exit 0): closure build of dogfood^... and service-settings^...; full dogfood Test Files 419 passed, 3 skipped (422), Tests 3492 passed, 36 skipped (3528); @objectstack/plugin-auth test 225/225 files, 3991 passed, 10 skipped (4001); plugin-auth typecheck exit 0 (check:test-typecheck OK); @objectstack/service-settings test 68/68 files, 1156/1156; service-settings typecheck exit 0.

Earlier readings, kept for the record: code head f58be8f5; 8b9f159f was that plus a merge of origin/main (23 commits, none touching auth-plugin.ts, sign-in-method-floor.ts or service-settings).

Suites and typecheck
  • On 8b9f159f: pnpm --filter '@objectstack/plugin-auth^...' build && pnpm --filter @objectstack/plugin-auth test && pnpm --filter @objectstack/plugin-auth run typecheck: VERDICT command-exit 0; Test Files 225 passed (225), Tests 3971 passed, 10 skipped (3981); check:test-typecheck: OK. (A first run after the merge, before rebuilding the closure, read a stale @objectstack/spec dist and failed public-feature-registry.test.ts on main's new emailDelivery flag; the rebuilt run above is green.)
  • On f58be8f5: the same suite 225/225 files, 3966 passed; pnpm lint: exit 0.
  • The new file alone: 17/17. Ablations as in the table above.
Full dogfood battery (owed by the narrowing arm)

On f58be8f5: pnpm --filter '@objectstack/dogfood^...' build && pnpm --filter @objectstack/dogfood test (no shard, no path): VERDICT command-exit 0, held the lock 40m35s; Test Files 419 passed, 3 skipped (422); Tests 3492 passed, 36 skipped (3528); no failure.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 64 commands on f58be8f5; each run with its exit code recorded before any pipe. --ran: 64 derived, 63 run, 1 NOT-MEASURED, 0 UNRUN. NOT MEASURED: pnpm check:dual-build-cjs-loads (exit 3, PREREQUISITE NOT MET: it needs a whole-workspace build; CI runs it). check-plugin-teardown-shape --self-test first exited 3 (shallow clone, pinned fixture commit), then 0 after fetching that commit. Every other command exited 0.


Generated by Claude Code

@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth, touching 82 documentable anchor(s).

21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json f506b79d85790a8f74a762ac4031090c7fbdae25.

⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json f506b79d85790a8f74a762ac4031090c7fbdae25 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1bcbb13d7278e37b8f048d5381f01c837b5a5b85 — the merge of head c22645ebea8b55d8c6e6592c8546d731657720cb into base f506b79d85790a8f74a762ac4031090c7fbdae25, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 1bcbb13d7278e37b8f048d5381f01c837b5a5b85 && git checkout 1bcbb13d7278e37b8f048d5381f01c837b5a5b85
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f506b79d85790a8f74a762ac4031090c7fbdae25 c22645ebea8b55d8c6e6592c8546d731657720cb && git checkout -B drift-repro f506b79d85790a8f74a762ac4031090c7fbdae25 && git merge --no-ff c22645ebea8b55d8c6e6592c8546d731657720cb

node scripts/docs-audit/affected-docs.mjs --json f506b79d85790a8f74a762ac4031090c7fbdae25

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs f506b79d85790a8f74a762ac4031090c7fbdae25 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8b9f159f2d3638304f2a969fad6c395dac5ea200
Local-runs: none
Scope: packages/plugins/plugin-auth/src/auth-plugin.ts, packages/plugins/plugin-auth/src/auth-settings-declared.ts, packages/plugins/plugin-auth/src/sign-in-method-floor.ts, packages/plugins/plugin-auth/src/auth-settings-clear.pin.test.ts + packages/plugins/plugin-auth/src/auth-manager.ts (applyConfigPatch, constructor, isClientIpAllowed, isAuthGateActive), packages/plugins/plugin-auth/src/auth-manager-auth-instance.ts (emailAndPassword forwarding), packages/plugins/plugin-auth/src/audience-posture.ts (assertAudienceConfig), packages/plugins/plugin-auth/src/mfa-required-enrollment-predicate.ts, packages/plugins/plugin-auth/src/allowed-ip-ranges.pin.test.ts, packages/services/service-settings/src/settings-service.ts (resetNamespace). The changeset is the prose face: read sentence by sentence, findings under ②, the ACCEPT names them.

Face: declared narrowing (Clause-②: yes (narrowing) on the PR body and the changeset, both line-initial). Card #30312; seat rulings on the card thread: (1) a key resolving source: 'default' after a clear or a namespace reset returns to the deployment-declared value in the same pass; (2) ruling A: the sign-in floor judges a cleared email_password_enabled at the declared value, sign-in-method-floor.ts added to the surface by the seat. Read against the diff merge-base f18adce2 → head, and the cited code on the head.

① Derived judgments

  1. Every isExplicit branch is covered. 33 isExplicit keys in applySettings on the head (lines 2198–2429 of auth-plugin.ts); AUTH_SETTING_RESTORE names 33 keys, and the first pin holds the table equal to the shipped auth manifest's specifier keys. Per key the arm matches the piece the explicit branch rides: emailAndPassword ×5 (enabled, disableSignUp, requireEmailVerification, minPasswordLength, maxPasswordLength) as their own refusable pieces; password_reject_breached as a plugins piece (re-judged by assertScimAdminCoherence on the merged block, as a save is); mfa_required as its own arm; session_expiry_days / session_refresh_days as a merged session block; 20 plain options on the plain patch (allowed_ip_ranges among them); rate-limit pair, Google trio and audience trio as groups. Correct.
  2. The declared value is exactly the boot input. captureDeclaredAuthOptions(authConfig) runs in init() after applyEnvSocialProviderFallbacks, after auth:configure, and immediately before new AuthManager(authConfig) (head lines 761, 766, 809–811): the snapshot is the object the manager was constructed from, one level copied. Google restores through this.configuredSocialProviders, the same post-auth:configure snapshot the explicit branch starts from. Where the deployment declared nothing the restore writes undefined, which every consumer read resolves to the same built-in default boot used: isClientIpAllowed (!ranges || ranges.length === 0 → open, so undefined equals the [] PR fix(plugin-auth)!: clearing the allowed IP ranges applies at once, and a console save that leaves out the saving address is refused #30309 wrote), isAuthGateActive (mfaRequired === true), auth-manager-auth-instance.ts (enabled: … ?? true, length fields spread only when != null). Correct.
  3. Env-pinned keys are untouched. The settings service reports an env override as source: 'env', so isExplicit is true and restoring is false; resetNamespace nulls only global/tenant/user rows. Pinned by the reset-with-OS_AUTH_LOCKOUT_THRESHOLD case. Correct.
  4. No clear leaves a refusable piece half-applied. mfa_required: declared on restores as one piece { mfaRequired: true, plugins: { twoFactor: true } } (refused together or applied together); declared off/undefined rides the plain patch and leaves plugins.twoFactor standing, which is exactly what an explicit off does. The MFA enrollment predicate reads a null as off (readToggle → false), so a clear is never refused there; a declared-on deployment under OS_AUTH_TWO_FACTOR=false restores to its own boot state, as the acceptance note says. Google trio: restored whole only when none of the three is explicit (groupRestoring), else the explicit branch composes from the stored keys as before. Audience: restored whole through one applyConfigPatch({ audience }) after the emailAndPassword pieces, only when audience_posture is not stored, by the same merged-result validation; a sibling stored without a posture stays IGNORED. require_email_verification restores with requireEmailVerificationFrom: 'deployment', so assertAudienceConfig sees the deployment's own declaration, as at boot. A refused restore is pushed to owed and so stays in authSettingsFromStore for the next pass; the standing value keeps ruling meanwhile, as for a refused save. Correct.
  5. Floor change is confined to the lockout case. The only changed input is the fallback readToggle uses for a cleared email_password_enabled in the batch (deps.declaredEmailPasswordEnabled?.() ?? true); an env override still wins in valueAfter; the Google-key fallbacks are unchanged; the refusal still requires no other way in after the save (otherSignInMethods, googleAfter). A deployment that declared email and password on, or absent, is judged exactly as before. The floor is registered on setMany, which resetNamespace also goes through, so a "Restore defaults" that would leave no way in is refused the same way; before the PR the same reset was saved and applied nothing. Within the declared narrowing. Correct.
  6. allowed_ip_ranges behaviour unchanged. PR fix(plugin-auth)!: clearing the allowed IP ranges applies at once, and a console save that leaves out the saving address is refused #30309's two fields are gone; the key rides the plain arm with restoring replacing allowedIpRangesFromSettings; the restored value is the declared list copied, or undefined (open) when undeclared. The three Allowed IP ranges: an administrator can exclude their own address unwarned, and clearing the field does not take effect until a restart #30203 clear pins in allowed-ip-ranges.pin.test.ts are untouched and the dev's ablation A1 reports all three red when the plain arm is removed. Correct.
  7. Declaration. Clause-②: yes (narrowing): the accept set of a published HTTP door (PUT /api/settings/auth, SettingsService.setMany('auth', …)) narrows by one save; no TypeScript export moves (sign-in-method-floor.ts and auth-settings-declared.ts are not re-exported from index.ts; auth-plugin.ts adds imports and private fields only; the new optional declaredEmailPasswordEnabled? field sits on an interface outside the package's exports map). Metadata-protocol: none is right: nothing an author writes changes. Skills-docs: none — REASON present as the narrowing arm requires; verified no file under skills/** names email_password_enabled, allowed_ip_ranges or lockout_threshold. content/docs/permissions/authentication.mdx names the keys but documents neither the sign-in floor nor this clear, so no documented statement becomes false; its line "Changes take effect immediately; no restart is required" becomes true for clears. Correct.
  8. Session merge (dev deviation 3): a stored session_expiry_days / session_refresh_days now overrides its field over the declared session block instead of replacing the block. Follows ruling (1): a declared field nobody stored rules. Named in the changeset. Correct.
  9. Residual of ruling (1), pre-existing and declared: with one rate-limit key cleared while the other stays stored, the explicit branch still composes the pair from the stored key and the built-in 10 / 60, not from the declared rateLimit; unchanged from before the PR, in the PR's Acceptance notes with carrier: none. Not a defect this PR introduces; the seat decides whether it is a follow-up card (recommended: a small one, since the card's wanted line reads "every isExplicit branch").
  10. Pins are ablation-plausible. The reported red counts per ablation (A1 7, A2 3, A3 1, A4 1, A5 1, A6 1, A7 2, A8 1, A9 2, A10 1, A11 1) match the cases in the new file that each arm alone can turn red (for example A7's two are the rate-limit pair case and the reset case's rateLimit assertion; A9's two are the two audience cases). The refusal pin asserts status 400, error.code SETTINGS_VALIDATION, the field error invalid_value at email_password_enabled, and the running value unchanged; the preservation pin beside it asserts the same clear saves and applies with Google standing.

② Semver level

@objectstack/plugin-auth (published) takes patch: Metadata-protocol: none, and a non-protocol narrowing ships patch with its BREAKING carriers (AGENTS.md Post-Task Checklist step 3). Carriers present and well-formed: title fix(plugin-auth)!:, body **BREAKING** accept-set narrowing …, exactly one adr-0087: marker, not-required (no-migration-prescription) …, which readDisposition parses (category, then the why); the body carries an administrator remedy, no consumer code rewrite. Check Changeset (which runs check-adr-0087-registration --base) is success on this head. Both declaration lines and Skills-docs: are line-initial in the changeset. Changeset sentences read against the diff: the fix paragraph, the refused-clear paragraph (constraint.lastSignInMethod: email_password is what the floor returns) and the session-lifetime paragraph all hold. One wording note for the seat's ACCEPT, not a correction: the refused-clear paragraph speaks of clearing the toggle; "Restore defaults" on the namespace is the same door and is refused in the same lockout state.

③ Boundary flags

  • Dev deviations: (1) surface widened to sign-in-method-floor.ts — the seat's ruling A authorised it; (2) harness assign/review step skipped — per the dispatch order; (3) session merge — judged under ① item 8. open_questions: none. security_findings: none. out_of_scope_findings: one observation, judged under ① item 9.
  • Author: PR user.login is objectstack-fleet[bot] (relay); no out-of-relay author to name.
  • Check-runs on 8b9f159f at 2026-11-05T12:00Z: 30 success, 13 skipped (UI and opt-in lanes), 4 in_progress (Test Core 1–4 of 8), 0 failure. Not green yet; nothing red.
  • Owed before queue (the dispatching seat's gate, not this face): the full dogfood battery reading is on f58be8f5, not on the head. 8b9f159f is f58be8f5 plus a merge of origin/main that moves packages/plugins/plugin-auth/src/auth-manager.ts (12 lines, getPublicConfig().features.emailDelivery) and packages/qa/dogfood/test/record-page-timeline.dogfood.test.ts. The plugin-auth suite and typecheck were re-run on the head and are green per the body; the review checklist pins the (narrowing) dogfood reading to the head. The seat either re-runs the battery on the landing head or records why the merge-only delta carries the f58be8f5 reading forward.

④ Design judgment

  • The narrowing against the published surface on main. The published spec declares no capability to clear auth.email_password_enabled into a state with no sign-in method; the sign-in floor (PR fix(plugin-auth)!: saving the auth settings refuses to turn off the last way to sign in #30298) already refuses an explicit save into that state. Before this PR a clear passed the floor on the manifest default and then applied nothing, so the state the floor exists to refuse was reachable only on paper. With clears applying, judging the cleared toggle at the declared value makes the floor's own refusal real for the one input it mis-read. It takes no declared capability away at the consumer; it closes a gap between what was saved and what ran. Holds.
  • auth-settings-declared.ts against ruling (1): one table and one snapshot instead of thirty else arms; the table is held to the manifest by a pin, so a new manifest key without an arm fails the suite rather than silently keeping its stored value. Restores travel the same piece as saves, so the manager's verdict, not the plugin's, decides a restore. copyDeclared is one level deep: audience.allowedEmailDomains and rateLimit.customRules remain shared between snapshot and running config after a restore; no mutation site reads them in place (applyConfigPatch builds new objects, the audience replaces whole), so this is a note, not a defect.
  • auth-plugin.ts against ruling (1): authSettingsFromStore keeps "boot restores nothing" (the spy pin) and the restore arms sit after the explicit branches and before the piece application, so a key is restored in the same pass that reads it as default. The per-process set is rebuilt from isExplicit on every pass plus owed, so a restart or a second node reaches the same answer from the store alone.
  • sign-in-method-floor.ts against ruling A: the dep is optional and absent-means-on, so every other caller of the predicate keeps its reading; the change is one fallback argument. Holds.
  • Non-blocking shape note: the new pin file copies the MockHttp / boot rig of allowed-ip-ranges.pin.test.ts verbatim (about 90 lines). There is no single shared harness file for the auth settings binding today (each prior card has its own rig), so the shared-harness rule does not bind here; a later card that touches this binding again should lift the rig into one file rather than copy it a third time.

Implemented-by: session_01Utp6CZPLJKSBMtARXMHc4W
Reviewed-by: session_01Utp6Cbvcmv8YWFhFJwzjKL

VERDICT: PASS

Must-fix: none. Owed before queue (seat): dogfood battery pinned to the landing head (③). Non-blocking: ① 9 (rate-limit half-clear residual, filing recommended), ④ rig duplication note, ② wording note on the reset reading.

Co-Authored-By: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01Utp6CZPLJKSBMtARXMHc4W

# Conflicts:
#	packages/plugins/plugin-auth/src/auth-plugin.ts
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

Contract review — delta

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c22645ebea8b55d8c6e6592c8546d731657720cb
Local-runs: none
Scope: the delta 8b9f159f → c22645eb only: the merge of origin/main at f506b79d (29 commits, among them PR #30449 for #30300 at a93ef3e3) into this branch, read as it bears on this PR's declared narrowing (a cleared email_password_enabled judged at the deployment's declaration) and on #30449's two doors (the SSO-provider delete floor and the kernel:ready no-sign-in-method report). Files read on the head: packages/plugins/plugin-auth/src/auth-plugin.ts, packages/plugins/plugin-auth/src/sign-in-method-floor.ts, packages/plugins/plugin-auth/src/auth-settings-clear.pin.test.ts, packages/plugins/plugin-auth/src/sign-in-method-floor.test.ts, packages/plugins/plugin-auth/src/auth-manager.ts, .changeset/30312-auth-settings-clear-applies-default.md, .changeset/30300-keep-the-last-identity-provider.md. The full review of record on 8b9f159f (comment 6657446697) stands for everything outside this delta; nothing below re-opens it.

Shape of the delta. c22645eb is a two-parent merge (8b9f159f, f506b79d). Exactly two files differ from both parents, that is, carry a resolution by hand: auth-plugin.ts and sign-in-method-floor.ts. Every other file is byte-identical to one parent: auth-manager.ts, sign-in-method-floor.test.ts, auth-manager-before-hook.ts, register-sso-provider.test.ts, content/docs/deployment/self-hosting.mdx and .changeset/30300-keep-the-last-identity-provider.md equal main's; auth-settings-clear.pin.test.ts, auth-settings-declared.ts and .changeset/30312-auth-settings-clear-applies-default.md equal the PR's previous head. The new merge-base is f506b79d; the PR's own change set against it is the same five paths as before (two added source files, one added test, one added changeset, two modified sources).

① Derived judgments

  1. Resolution keeps both PRs' semantics. sign-in-method-floor.ts on the head, diffed against main, is exactly this PR's 14-line change (the module comment, the optional declaredEmailPasswordEnabled?(): boolean on SignInMethodFloorDeps, and the one fallback argument in signInMethodFloorPredicate), applied on top of fix(plugin-auth)!: deleting the last identity provider is refused, and a boot that serves no sign-in method says so (#30300) #30449's version of the file, which brought ssoProviderDeleteRefusal, probeOtherSsoProviders, resolveNoSignInMethodReport and reportIfNoSignInMethodAtBoot. auth-plugin.ts on the head, diffed against main, is this PR's previous patch line for line (interdiff of the two patches: the only difference is two lines re-indented, the declaredEmailPasswordEnabled reader and its comment, moved from the inline deps literal into fix(plugin-auth)!: deleting the last identity provider is refused, and a boot that serves no sign-in method says so (#30300) #30449's shared signInMethodFloorDeps() constructor); the registration is fix(plugin-auth)!: deleting the last identity provider is refused, and a boot that serves no sign-in method says so (#30300) #30449's one-liner registerSignInMethodFloor(settings, this.signInMethodFloorDeps(ctx, this.authManager), ctx.logger). auth-manager.ts carries no content from this PR and equals main (fix(plugin-auth)!: deleting the last identity provider is refused, and a boot that serves no sign-in method says so (#30300) #30449's setSsoProviderDeleteFloor present). Correct.
  2. Every reader of the new dep, on the head. Three occurrences in the tree: the interface field (sign-in-method-floor.ts line 140), the one read deps.declaredEmailPasswordEnabled?.() ?? true as the readToggle fallback inside signInMethodFloorPredicate (line 421, reached only when the save batch has own property email_password_enabled), and the constructor in auth-plugin.ts (line 2081). ssoProviderDeleteRefusal (line 525) and resolveNoSignInMethodReport (line 561) both open with const config = deps.publicConfig(); if (config.emailPassword?.enabled !== false) return … and then call otherSignInMethods and the provider probes; none of them, nor otherSignInMethods, googleAfter or valueAfter, names the dep. So the SSO delete path and the boot report cannot reach the fallback: both judge the running publicConfig, exactly as on main, and their verdicts are unchanged by this PR. The dev's claim (only the save floor's batch reading uses it) is what the code shows. Correct.
  3. The shared constructor is safe at every call site. The dep is a closure over this.declaredAuthOptions, which is declared with initializer {} (line 569) and assigned by captureDeclaredAuthOptions(authConfig) at line 816, before new AuthManager(authConfig) (818) and before the first signInMethodFloorDeps() call (824, the delete floor handed to the manager). Read before capture it would resolve undefined !== false → on, the same as absent. The other two call sites (kernel:ready report at 1552, bindAuthSettings registration at 2158) run after init. Nothing in the constructor's other fields moved. Correct.
  4. The cleared-toggle fallback behaves as reviewed. The predicate's input is unchanged from 8b9f159f: a clear in the batch reads readToggle(null, declared), env override still wins through valueAfter, the Google keys keep their true fallbacks, the refusal still needs otherSignInMethods empty and googleAfter false. The two pins on the head are byte-identical to the reviewed ones: the refusal case (declared emailAndPassword.enabled: false plus Google, Setup turns email and password on, then Google off, then clears → 400, SETTINGS_VALIDATION, field email_password_enabled with invalid_value, running enabled still true after settle) and the preservation case (same declaration, Google left standing, the clear answers 200 and the running value goes to false with Google still defined). fix(plugin-auth)!: deleting the last identity provider is refused, and a boot that serves no sign-in method says so (#30300) #30449's door 1 harness turns email and password off with an explicit false in the batch, which readToggle reads as the value regardless of fallback, so that harness is not touched by the fallback either. Correct.
  5. No test dropped or weakened. auth-settings-clear.pin.test.ts: 17 it cases, identical blob to 8b9f159f. sign-in-method-floor.test.ts (fix(plugin-auth)!: deleting the last identity provider is refused, and a boot that serves no sign-in method says so (#30300) #30449's shared harness, the PR never edits it): identical blob to main; it builds its SignInMethodFloorDeps literal without the new optional field, which the ? admits, and both Type Check · workspace and TypeScript Type Check are green on the head. register-sso-provider.test.ts and the floor's self-hosting.mdx section equal main. fix(plugin-auth)!: deleting the last identity provider is refused, and a boot that serves no sign-in method says so (#30300) #30449's changeset 30300-keep-the-last-identity-provider.md is present on the head. Correct.
  6. Carriers unchanged and still true on the merged tree. .changeset/30312-auth-settings-clear-applies-default.md is byte-identical to the reviewed one: '@objectstack/plugin-auth': patch, title with !, line-initial Clause-②: yes (narrowing) — …, Metadata-protocol: none, Skills-docs: none — …, exactly one adr-0087: HTML-comment marker reading not-required (no-migration-prescription) …, and the **BREAKING** accept-set narrowing … paragraph. Read against the merged tree: the refused-clear paragraph still describes the one changed input (item 4); the fix paragraph's "every key except allowed_ip_ranges" and the session-lifetime paragraph are untouched by the merge (no commit in the 29 touches applySettings' restore arms, interdiff item 1). fix(plugin-auth)!: deleting the last identity provider is refused, and a boot that serves no sign-in method says so (#30300) #30449's own narrowing (the 409 DELETE_RESTRICTED on the last provider) is declared by its own changeset on main, not by this one, and this PR does not alter it (item 2). The PR title keeps fix(plugin-auth)!:. Check Changeset is success on c22645eb. Correct.
  7. Boot order interaction with fix(plugin-auth)!: deleting the last identity provider is refused, and a boot that serves no sign-in method says so (#30300) #30449's kernel:ready block. fix(plugin-auth)!: deleting the last identity provider is refused, and a boot that serves no sign-in method says so (#30300) #30449 awaits ensureAuthSettingsBound(ctx) and then reports; this PR's restore arm restores only keys the running config previously took from the store in this process, and the first pass takes none (the "boot restores nothing" spy pin, ablation A11), so the report reads the same facts as on main. Not a change. Correct.

③ Boundary flags

  • Check-runs on c22645eb at 2026-11-05T13:40Z: 35 success, 13 skipped (UI and opt-in lanes), 0 failure, 0 in_progress. Green, Test Core 1 to 8, Dogfood Regression Gate, Check Changeset, Governed Surface Queue Guard and all type-check lanes among them.
  • PR body Verification section is stale. It still reads "Code head f58be8f5; head 8b9f159f is that plus a merge of origin/main (23 commits …)" and every suite and dogfood reading cites 8b9f159f or f58be8f5. The dev reports, under the verify lock on c22645eb: dogfood 419 files / 3492 passed; plugin-auth 225 files / 3991 passed; plugin-auth and service-settings typecheck 0; service-settings 68 files / 1156. Those readings discharge the "owed before queue" line of the full review (dogfood pinned to the landing head) once they are on the body; the seat updates the section to c22645eb. Not a code finding.
  • The dev's own description of the resolution matches the tree: fix(plugin-auth)!: deleting the last identity provider is refused, and a boot that serves no sign-in method says so (#30300) #30449's one-line registration taken, the dep added into the shared constructor, sign-in-method-floor.ts and auth-manager.ts auto-merged (the former carries the PR's 14 lines over fix(plugin-auth)!: deleting the last identity provider is refused, and a boot that serves no sign-in method says so (#30300) #30449's file, the latter equals main).
  • Author: PR user.login is objectstack-fleet[bot] (relay); no out-of-relay author to name.
  • Nothing in this delta changes the full review's non-blocking notes (rate-limit half-clear residual, rig duplication, reset wording); they stand as written.

Implemented-by: session_01Utp6CZPLJKSBMtARXMHc4W
Reviewed-by: session_01Utp6Cbvcmv8YWFhFJwzjKL

VERDICT: PASS

Must-fix: none. Seat action before queue: update the PR body's Verification section from 8b9f159f / f58be8f5 to the c22645eb readings (③).

@objectstack-fleet

objectstack-fleet Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

维护者速读

  • 改了什么: Setup → 认证里清除某个设置,或者「恢复默认」整个认证命名空间,现在马上生效,回到部署声明的值。以前清除后运行中的值不变,要重启才回去。
  • 为什么改: Authentication settings: clearing a key (or resetting the namespace) answers saved, but the running value keeps the last stored one until a restart (lockout threshold, max sessions per user and siblings) #30312:清除后界面显示默认值,系统却还在按旧值运行(锁定阈值、会话数上限、IP 白名单等 33 个键都受影响)。
  • 风险与回滚: BREAKING,只收窄一种保存:部署声明了关掉邮箱密码登录、又没有其他登录方式时,清除 email_password_enabled(或恢复默认)会被拒,因为那样就没人能登录了。changeset 写明,带 ADR-0087 标记。环境变量钉住的键不受影响。回滚即撤本 PR。
  • 先看: packages/plugins/plugin-auth/src/auth-settings-declared.ts(还原表),再看 auth-plugin.ts applySettings 里的还原分支。
  • 两分钟验证: pnpm --filter @objectstack/plugin-auth exec vitest run src/auth-settings-clear.pin.test.ts。
  • 需要您做的: 无。非受管面、非 Tier H,契约复审(收窄面)PASS,按 pm-dispatch 技能由席位经合并队列落地(Bill 13:47Z:技能不需要他审核的不等他)。
  • 席位意见: ACCEPT。全量复审 PASS 6657446697、合 main 后增量复审 PASS 6659465418,落地 head 上 dogfood 已重跑全绿;只收窄一种会锁死登录的清除,changeset 与 ADR-0087 标记齐全。

席位核对 — ACCEPT · domain:server#2 · head c22645ebea8b55d8c6e6592c8546d731657720cb

  • 形态: draft, author objectstack-fleet[bot] (relay pr_create), no assignee, no review request; non-governed (check-governed-merges 0 of the PR's paths), under 3000 lines. Title fix(plugin-auth)!:.
  • 契约复审: CONTRACT_REVIEW_TIER, narrowing face: full review PASS 6657446697 on 8b9f159f; delta review PASS 6659465418 on c22645eb (the main merge carrying PR fix(plugin-auth)!: deleting the last identity provider is refused, and a boot that serves no sign-in method says so (#30300) #30449: one hand resolution, the declaredEmailPasswordEnabled reader moved into fix(plugin-auth)!: deleting the last identity provider is refused, and a boot that serves no sign-in method says so (#30300) #30449's shared signInMethodFloorDeps(); only the save floor's batch fallback reads it, the SSO-provider delete floor and the boot report judge the running config, verdicts unchanged). No must-fix.
  • Seat rulings recorded: (1) a key resolving source: 'default' after a clear or namespace reset returns to the deployment-declared value in the same pass; (2) ruling A: the sign-in floor judges a cleared email_password_enabled at the declared value — sign-in-method-floor.ts added to the file surface by the seat.
  • Prose face (changeset), the review's wording note: the refused-clear paragraph names clearing the toggle; "Restore defaults" on the namespace goes through the same setMany door and is refused in the same lockout state. Accepted as written with this note.
  • Owed before queue — discharged: dogfood on the landing head. Dev readings on c22645eb under the verify lock (one run, every step exit 0): full dogfood 419 files / 3492 passed; plugin-auth 225 files / 3991 passed; service-settings 68 files / 1156 passed; both typechecks 0. PR body Verification section updated to these readings by the seat (relay issue_patch, run 42038021276).
  • 门禁 check-runs on c22645eb: 35 success, 13 skipped (UI / opt-in), 0 failure.
  • Declarations: Clause-②: yes (narrowing), Metadata-protocol: none, Skills-docs: none — REASON; patch, ! title, BREAKING banner, one ADR-0087 marker; Check Changeset success.
  • Follow-ups (non-blocking, carried by the seat): rate-limit half-clear residual (review ① 9) — a small card after this lands; the shared auth-settings test rig (review ④) — next card touching the binding lifts it.
  • 落地条款: Fixes Authentication settings: clearing a key (or resetting the namespace) answers saved, but the running value keeps the last stored one until a restart (lockout threshold, max sessions per user and siblings) #30312. check-governed-merges --pr 30447: NOT governed (0 of 5 paths), 699 lines ≤ 3000. Ordinary queue landing by this seat per pm-dispatch (BREAKING, non-governed, contract review PASS; the maintainer's 13:47Z word: what the skill does not route to him does not wait for him): pr_ready + automerge_enable on this head.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 5, 2026 21:48
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 5, 2026 21:48
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit b5765be Nov 5, 2026
53 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-nbraza branch November 5, 2026 22:14
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/l tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant