Repository navigation
plugin-auth: under the open audience posture, honour an explicit requireEmailVerification: false (email_domain stays forced) #20389
Description
Activity
- addedenhancementNew feature or requestNew feature or requestarea:identityLogin and identity — sign-up, sessions, organization membership, SSOLogin and identity — sign-up, sessions, organization membership, SSO
on Sep 28, 2026 Claim: PM loop round 1 (maintainer direct dispatch)
Session:session_d8bf7d34-4f31-407d-ac85-6cb849eec4bf
Account:hotlong(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-20389-open-posture-verification-optout
Worktree:objectstack-issue-20389
Domain:domain:spec
Seat: domain:spec#9 (an unregistered number for a maintainer-commanded direct-dispatch session anchored inobjectstack-ai/cloud; seats 1–5 have their own posts and this claim takes none of them)
File surface:packages/plugins/plugin-auth/src/**(audience-posture, auth-manager and their tests),packages/spec/src/system/auth-config.zod.ts(theaudiencedocblock only),content/docs/**pages that state the forced-verification invariant, one.changeset/20389-*.md(stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus(default tier;dispatch-gates.mjs --tieratab946560fd: no path-derived mandate, Clause ② judged from content; the contract review is owed atCONTRACT_REVIEW_TIERbefore enqueue)
Clause-②: yes
Thread-read: none
Serial constraints cleared: none — at 2026-09-28T05:51Z, none of the 13 open PRs touchesaudience-posture.ts,auth-manager.tsorauth-config.zod.ts; the in-flight spec/services cards (#20374 plugin-email, #20321 RLS tags, others) do not declare these filesClause-② note: the two protocol lines read in opposite directions for this card — the negative boundary says a runtime security-behaviour change is the human floor rather than Clause ②, while removing a deliberate security refusal reads as
yes. Resolved to the stricter reading (yes, contract review owed). The human floor is satisfied by the maintainer instruction quoted verbatim in the card body.
Ruling-ref: the card body (maintainer instruction quoted verbatim; no separate ruling comment exists)os-dev-report
{
"issue": 20389,
"status": "done",
"branch": "claude/issue-20389-open-posture-verification-optout",
"pr": "#20406",
"session": "session_d8bf7d34-4f31-407d-ac85-6cb849eec4bf — subagent, the parent PM session's id (mode:subagent)",
"premise_still_valid": true,
"summary": "Under posture open, an explicit emailAndPassword.requireEmailVerification false declared by the DEPLOYMENT is now honoured. The deployment declares it through the AuthManager constructor or stack config, host code calling applyConfigPatch (the default declarant), or the OS_AUTH_REQUIRE_EMAIL_VERIFICATION env override (settings source env). A value stored only through the settings console is still refused, and email_domain refuses an explicit false from every source with a byte-identical message. The wiring and getPublicConfig read one resolver (resolveEmailVerificationRequirement). AuthPlugin logs one boot warn naming the posture and the consequence. The spec docblocks, the posture describe (reference doc regenerated), self-hosting.mdx and environment-variables.mdx are amended. Zone-2 item 3 was FALSIFIED as stated, see deviations[0]: the maintainer's env variable reaches the manager through the settings door (applyConfigPatch), not the constructor, so refusing that door would have left the variable inert. Measured on main db74b16: it also drops every sibling auth setting in the pass. The switch is therefore keyed on who declared the value, not on which method received it. Premise of the card itself (open has no working opt-out) holds.",
"tests": "FINAL HEAD e2d1823. plugin-auth: 'pnpm --filter @objectstack/plugin-auth test' → Test Files 114 passed, Tests 2454 passed (VERDICT command-exit 0); typecheck exit 0 incl. check:test-typecheck ('10 file(s) / 94 error(s) / 23 pinned signature(s) held'); build exit 0 (plus full turbo build ./packages/* ./packages// → 71/71 successful at this head). spec: build exit 0, typecheck exit 0; full suite 'vitest run --maxWorkers=2' → 17097 passed / 4 failed in 3 files under heavy contention; rerun alone: scoped-context.test.ts green (its 2 were 60s timeouts); the other 2 (build-schemas-check-mode 'writes the generation stamp…' expected mismatch to be match; check-liveness 'drill ceiling' JSON truncated at position 65478) STILL FAIL with packages/spec/src/system/auth-config.zod.ts restored to BASE bytes (trap-guarded restore, blob verified back to HEAD 8f0d17fed9, git diff HEAD empty) → not this diff. Pins flipped (not deleted): audience-posture.test.ts open refusal → honoured-from-deployment + refused-from-console (message first sentence asserted); applyConfigPatch tail → console declarant; constructor boots open+false; email_domain+false refused for all three declarants on the message family regex '^[audience] invalid audience configuration: posture email_domain opens self-registration, which FORCES email verification on — …colleague impersonation'. New end-of-chain: open + deployment false → sign-up returns a session token and user.emailVerified false, public config false; control open undeclared → token null. Settings channel: env false under open honoured with exactly one warning (and none on a second pass); stack-config + env both doors agree and sibling session_expiry_days applies; env false under email_domain refused; console-stored false under open stays refused (deliberately NOT flipped: it is the console door). Ablations via scripts/ablation-replace.mjs (anchor hit x1→x0, blob moved, restored to HEAD blob, git diff HEAD empty; src imported directly, no dist in path): A1 console-door refusal → if(false): 5 red / 66 green; A2 resolver open branch → return true: 5 red / 66 green; A3 plugin env→deployment mapping → constant console: 1 red (env-only door) / 70 green — the dual-door test stays green by design (constructor opt-out already stands, the console false is agreement). First A1 attempt ran but its vitest output was not captured (stdio inherit); first A3 attempt was refused by the tool (replacement text already present, count unchanged) and never ran — both re-run as above. Lint (measured narrowing): 'pnpm exec eslint --no-inline-config --format json' over the 7 changed .ts files → 7 files, 0 errors, 0 warnings; population read from eslint.config.mjs ('/.{ts,…}' blocks; md/mdx not linted); invariance: the config never enables type-aware linting (its own comment, lines 327-328).",
"mcp_calls": "0",
"api_writes": "3 REST writes as hotlong (GH_TOKEN from gh auth token): POST /repos/objectstack-ai/objectstack/pulls (draft PR 20406); POST /repos//issues/20406/assignees via scripts/pm/label-write.mjs (--assign hotlong, read-back matched); POST /repos//issues/20389/comments (this os-dev-report). Plus 6 git pushes of the branch (empty-branch probe + 5 commits; not REST).",
"gates": {
"derivation": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths: merge-base change set) at e2d1823 → 113 commands, identical to the first derivation at a20cb57",
"ran_verdict": "node scripts/pm/dispatch-gates.mjs --ran 20389-ran.list → '113 derived famil(ies) accounted for — 112 run, 1 NOT-MEASURED (0 DERIVED from a recorded exit 3, 1 claimed)', exit 0",
"at_head": "e2d18233c9 (gates 1-43 re-run at this head after the last commit; 44-113 run at this head)",
"exit_0": 111,
"red": ["pnpm check:merge-driver :: exit 1 — inputs (scripts/, .githooks/, .gitattributes) byte-identical to BASE; control: 'node scripts/git-merge-regen.mjs --self-test' on a detached BASE db74b16 worktree exits 1 at the same 'FIRING CONTROL: with the artifact CURRENT the same commit is ACCEPTED' leg → host fact, not this diff"],
"not_measured": ["pnpm --filter @objectstack/spec run check:generated — host pnpm 10.31.0 refuses the wrapper's inner 'pnpm -s SCRIPT' ('error: unexpected argument -s found') for all 15 member gates, so the wrapper never reached a gate body; each member gate run directly as 'pnpm run check:NAME' in packages/spec exited 0 (check:docs after gen:schema + gen:docs regenerated content/docs/references/system/auth-config.mdx)"],
"recovered_after_prerequisite": ["check:skill-examples (exit 3 → built client-react closure → exit 0)", "check:dual-build-cjs-loads (exit 3 → full build → exit 0)", "check:type-check-debt (exit 3 → full build → exit 0)"],
"changeset_level_axis": "node scripts/check-changeset-no-major.mjs --base origin/main --event (simulated PR payload with 'Clause-②: yes') → 'LEVEL AXIS: this PR declares clause-② yes, and it grades a package … at minor or above'",
"verify_lock": "UNLOCKED (declared) for every build/test — no usable flock on this macOS host; declared in the PR body",
"ci_not_covered_locally": "53 artifact-roster families, 10 wide-population families, 6 path-scheduled CI jobs and the type-check lanes are outside the derived total (tool's own listing) — CI's",
"logs": "copied to the session scratchpad issue-20389/rig-logs/ (20389-gates-results.tsv, 20389-gate-NNN.log, 20389-ran.list, 20389-ran-verdict.txt)"
},
"line_budget": { "additions": 537, "deletions": 72, "files": 11, "source": "GET pulls/20406 additions/deletions/changed_files, equal to git diff --numstat BASE..HEAD" },
"files_changed": [
".changeset/20389-open-posture-verification-optout.md",
"content/docs/deployment/environment-variables.mdx",
"content/docs/deployment/self-hosting.mdx",
"content/docs/references/system/auth-config.mdx (generated: gen:schema + gen:docs)",
"packages/plugins/plugin-auth/src/audience-posture-setting.test.ts",
"packages/plugins/plugin-auth/src/audience-posture.test.ts",
"packages/plugins/plugin-auth/src/audience-posture.ts",
"packages/plugins/plugin-auth/src/auth-manager.ts",
"packages/plugins/plugin-auth/src/auth-plugin.ts",
"packages/plugins/plugin-auth/src/boot-sign-in-reachability.ts",
"packages/spec/src/system/auth-config.zod.ts"
],
"deviations": [
"ZONE-2 ITEM 3 FALSIFIED, route changed per ruling intent (not a quiet widening). In this framework OS_AUTH_REQUIRE_EMAIL_VERIFICATION is the settings env override for auth.require_email_verification (content/docs/deployment/environment-variables.mdx; service-settings reports source 'env', locked), so it reaches the manager ONLY through bindAuthSettings → applyConfigPatch. On main db74b16 (probe: stack-config open + env false + sibling session_expiry_days 3), the env value was refused, and the sibling was dropped (session.expiresIn undefined) with only a warn 'Auth: failed to apply auth settings'. The same boot's error prescribed 'disable verification (OS_AUTH_REQUIRE_EMAIL_VERIFICATION=false)', the very value refused. The cloud control plane mounts SettingsServicePlugin (cloud origin/main packages/service-cloud/src/control-plane-preset.ts:343), so a host mapping the variable into the constructor also receives it via this door, and refusing the door would drop every auth settings pass there. Built: the two paths are told apart by DECLARANT, not by method. Constructor, host applyConfigPatch and settings source env count as the deployment and are honoured under open; a console-stored value is refused under open, as today. Fork recorded in open_questions[0].",
"Card acceptance says every existing test pinning the open refusal is flipped: all were flipped EXCEPT audience-posture-setting.test.ts 'open with verification OFF stored only through the CONSOLE is refused' (source 'global'), which keeps asserting the refusal (plus the new message anchor) because under the built design it is the console door.",
"In-place fix under the bounded exemption: the no-mail-transport boot error in auth-plugin.ts no longer prescribes OS_AUTH_REQUIRE_EMAIL_VERIFICATION=false under email_domain, where it is refused. Same defect class, same claimed file, same gate family; pinned; named with evidence in the PR body.",
"Spec edit beyond the literal 'audience docblock': the posture .describe() string in AudienceConfigSchema was amended (it states the invariant and generates content/docs/references/system/auth-config.mdx), regenerated with gen:schema + gen:docs as zone-2 item 4 anticipated; boot-sign-in-reachability.ts runtime recovery text gained a one-clause qualifier to keep agreeing with self-hosting.mdx (its docblock requires that). Both inside the claim's declared surfaces (auth-config.zod.ts audience schema; plugin-auth/src/).",
"Worktree: the dispatch's 'worktree add' command omitted --no-track (os-dev.md/AGENTS.md require it); upstream was unset before the first edit and the empty-branch push re-tracked the branch to its own remote.",
"Gate derivation ran with NO path arguments (merge-base change set, per os-dev.md) instead of 'each changed path as a separate argument' (dispatch); the change set also included untracked .rig-logs files, a superset — the derived list was identical before and after.",
"Temp files went to WORKTREE/.rig-logs/ (dispatch) and were copied to the session scratchpad issue-20389/ subdirectory (os-dev.md) before the worktree was removed.",
"GitHub writes went as hotlong via gh api / label-write with GH_TOKEN (dispatch-authorized); the AGENTS.md fleet route (with-fleet.sh as objectstack-fleet[bot]) is unavailable on this host (0 OS_FLEET_* variables).",
"main was not merged before opening (AGENTS.md §10 asks for it): declared narrowing — the 5 commits origin/main gained since BASE touch none of this diff's paths (git diff --stat over them empty; local merge-tree clean); PR merge ref and queue validate the joint result.",
"Changeset first carried 'Clause-②: yes (widening)'; aligned to the claim's verbatim 'Clause-②: yes' (PR body carries the same line); changeset bumps plugin-auth minor, spec patch (fixed group).",
"Report JSON carries the PM-requested fields (gates, line_budget, deviations, files_changed) in addition to the os-dev.md template fields.",
"Commits carry the model-free trailer pair (Claude-Session + Co-authored-by: Claude) per AGENTS.md, not the harness-suggested trailer with a model name; no card relation in any commit."
],
"open_questions": [
{
"question": "Should a value STORED through the auth settings console (require_email_verification false, any non-env source) be able to turn verification off under posture open? The ruling covers the deployment's configuration; the console is the one runtime-mutable door.",
"options": [
"A — provenance-blind: honour an explicit false under open from every door (drop the declarant bit and the applyConfigPatch option; assertAudienceConfig becomes a pure email_domain check). Real need: no measured consumer of the console door. Long-term: one value rule, and the #11768 settings contract ('the settings channel can never reach a posture the boot-config channel could not') stays symmetric. AI-error: one rule, but a console/settings-API write silently opens it (warned only). Startup scope: a new capability on the console with no pull. Cost: roughly -15 lines.",
"B (BUILT) — deployment-declared only: honour the constructor, host applyConfigPatch and the OS_AUTH_* env override (source env), and refuse a console-stored false under open; the console may only agree with a standing deployment opt-out. Real need: covers every measured consumer (cloud control plane through both doors; self-hosters through the documented env variable). Long-term: rides the settings layer's existing env-vs-stored distinction (env values are already locked 'Set via env'), at the price of one declarant bit in AuthManager. AI-error: an agent writing the console or settings API is refused loudly under open. Startup scope: no capability beyond the ruling.",
"C (zone-2 as stated) — constructor only; refuse every applyConfigPatch false under open. FALSIFIED: it leaves the framework's documented variable inert under open, and on a host whose settings service carries the same env value, every auth settings pass is refused wholesale."
],
"recommendation": "B, as built. On real business need it is the only option that serves all measured consumers and adds nothing unpulled. On AI error it keeps the one runtime-mutable, agent-writable door refused loudly rather than silently permissive. On startup scope it implements exactly the ruling. A differs by a small deletion if the maintainer later wants the console toggle to be a real switch too."
},
{
"question": "With verification off under open, does the invitation consequence (requireEmailVerificationOnInvitation false: an account holding an address can accept an invitation addressed to it) deserve more than a sentence?",
"options": [
"A — a sentence (done): named in the boot warning, self-hosting.mdx and the changeset",
"B — change invitation acceptance (e.g. require a verified email for accept-invitation when verification is off)"
],
"recommendation": "A. The exposure is not new in kind. requireEmailVerificationOnInvitation false is set unconditionally (auth-manager.ts), and under the default invite_only posture, where verification is off by default, the invitation carve-out already lets whoever registers an invited address accept it. The open opt-out only widens the window to addresses registered before an invitation exists. B would dead-end every invitee on exactly the no-mail deployments the opt-out exists for. It is outside the ruling and was not touched. Related, pre-existing and unchanged: accountLinking requireLocalEmailVerified false (better-auth link-account.mjs:79,128) lets a later trusted-provider sign-in link to a pre-registered unverified local account under open even with verification on."
}
],
"out_of_scope_findings": [
"class: a · reach: OS_AUTH_* settings env override or console save → sibling auth settings silently not applied (measured on main db74b16: stack-config open + OS_AUTH_REQUIRE_EMAIL_VERIFICATION=false + session_expiry_days 3 → session.expiresIn undefined, only warn 'Auth: failed to apply auth settings: …') · evidence: bindAuthSettings (auth-plugin.ts applySettings) runs the main applyConfigPatch inside one try whose catch logs at warn, so ONE refused key (verification contradiction, scim/admin coherence) drops password policy, MFA, rate limits, session expiry and social providers of that pass while the console shows them saved; the sibling audience block already logs such refusals at error for exactly that reason (#5152 rationale). This PR removes the open+env trigger; still reachable via email_domain + env false, a console-stored false under open, and a plugins scim/admin refusal. Cloud staging (open + env false, SettingsServicePlugin mounted) is in this state until the pin moves past this PR · dedupe words: 'failed to apply auth settings', 'applySettings single try drops sibling', 'bindAuthSettings applyConfigPatch throws', 'auth settings pass dropped warn'",
"class: a · reach: local 'pnpm --filter @objectstack/spec test' on a macOS host (darwin 25.5, node 26.7, pnpm 10.31.0), reproduced at BASE db74b16 · evidence: scripts/liveness/check-liveness.test.ts 'reports every key below the drill ceiling…' fails with SyntaxError 'Unterminated string in JSON at position 65478' (child stdout cut near a 64 KiB pipe boundary), and scripts/build-schemas-check-mode.test.ts 'writes the generation stamp as its last step, in --check mode too' reads schemaStamp state 'mismatch'; packages/spec check:generated's inner 'pnpm -s SCRIPT' is refused by this pnpm ('unexpected argument -s found'); git-merge-regen --self-test FIRING CONTROL leg fails at BASE · dedupe words: 'drill ceiling unterminated JSON 65478', 'generation stamp mismatch check mode', 'pnpm -s unexpected argument', 'merge-regen FIRING CONTROL darwin'",
"carrier: 承接者:无 · noted, not filed — packages/services/service-settings/src/manifests/auth.manifest.ts audience group description and audience_posture help (and the en / zh-CN / ja-JP / es-ES bundles) still say every non-invite_only posture forces verification; true from the console's side under the built design, but beside a locked env false it reads as a wall that is lifted. Outside the claim's file surface (stop-on-breach), so it is in the PR's Acceptance notes; a one-line follow-up if the PM widens the surface",
"carrier: 承接者:无 · noted, not filed — docs/qa/platform-checklist/areas/identity-auth.json knownGaps states 'on a permitting posture, requireEmailVerification is FORCED on' and 'audience is config-only (no env knob)'; now true only by default for open, and OS_AUTH_AUDIENCE_POSTURE already exists as a settings env override. Outside the file surface; in the PR's Acceptance notes"
]
}
Generated by Claude Code
ACCEPT (CI-conditional) — PR #20406 @
e2d18233c9. Contract review owed before enqueue.Seat
domain:spec#9(direct dispatch), sessionsession_d8bf7d34-4f31-407d-ac85-6cb849eec4bf, reviewed 2026-09-28T08:04Z against GitHub, not against the report.check reading PR shape draft · base main· body first lineFixes #20389· line 3Clause-②: yes· assigneehotlongfile surface 11 files, +537/−72 — all inside the claim: plugin-auth/src/**(5 src + 2 tests),spec/src/system/auth-config.zod.ts, 3content/docs/**pages (one generated),.changeset/20389-*.mdruling, item 1 resolveEmailVerificationRequirement:open⇒declared !== false; both the wiring andgetPublicConfig()read it, so the advertisement cannot disagreeruling, item 2 email_domain⇒ alwaystrue; the explicit-falserefusal keeps its message byte for byteruling, item 3 one ctx.logger.warn(OPEN_POSTURE_VERIFICATION_OFF_WARNING)after settings apply; the text names the posture and the invitation consequencescope of the ruling the dev falsified the PM's zone-2 item 3 with a measurement: the framework's OS_AUTH_REQUIRE_EMAIL_VERIFICATIONarrives throughapplyConfigPatch(settings sourceenv), so "constructor only" would have left the variable inert. Built instead: the declarant decides. Constructor, hostapplyConfigPatchand settings sourceenvare the deployment; a console-storedfalseunderopenstays refused. That is the ruling's scope (the deployment's configuration) implemented faithfully, and no wider — accepted.CI 14 success · 2 skipped · 16 in progress at 08:04Z — this verdict is conditional on the rest converging green Dev open questions
- Q1 (console door) — B as built, accepted. It is the ruling's literal scope; A would widen a runtime, agent-writable door the ruling did not name. Not escalated: no option beyond the ruling is being taken.
- Q2 (invitation consequence) — A, accepted: a sentence in the warning, the docs and the changeset. Changing invitation acceptance is outside the ruling.
Out-of-scope findings
bindAuthSettings: one refused key drops every sibling auth setting in the pass, logged only atwarn(class a, reach measured onmain) — not filed this fire. This host cannot mint the fleet identity, so every write lands as aUseraccount, and the filing gate forbids new cards from aUserwriter. Carried to the maintainer.- macOS-host-only failures in
@objectstack/spectests, andpnpm -srefused by pnpm 10.31 (class a, reach: a local darwin host only) — Acceptance notes, not filed; CI's Linux runners are the record. - and 4.
service-settingsauth manifest help text, and the identity-auth platform checklistknownGaps— Acceptance notes, as the dev placed them (carrier: none).
Findings carrier update, 2026-09-28T09:01Z: my ACCEPT above said findings 1, 3 and 4 were "not filed this fire" (User-identity filing gate). The maintainer has since instructed filing, verbatim: 「直接开卡吧」.
- finding 1 (
applySettingsdrops sibling settings,warnonly) → filed plugin-auth: one refused key in the auth settings pass drops every sibling setting, and says so only atwarn#20412 (pm:queue) - findings 3 + 4 (stale invariant text: settings console in 4 locales, identity-auth checklist) → folded into one family card, filed Stale "every non-invite_only posture forces email verification" statements after #20389: settings console text (4 locales) and the identity-auth platform checklist #20413 (
pm:blocked,Blocked-by: #20389) - finding 2 (macOS-host-only local test failures) → stays in Acceptance notes, not filed
- finding 1 (
LANDED 2026-09-28T09:15Z: PR #20406 went through the merge queue (
added_to_merge_queue08:50:09Z →merged09:13:25Z), squashed intomainas65352b7d6d(1 parent). The contract-review PASS on the head is PR comment5866574059. This card was closedcompletedby the PR'sFixes;pm:dispatchedwas removed in the same act. The unblocked family card #20413 moves topm:queue.- added a commit that references this issue
on Sep 28, 2026 - added a commit that references this issue
on Sep 29, 2026
Filing-gate: ③ maintainer-directed task (direct dispatch from a maintainer-commanded PM session)
Maintainer instruction (verbatim, not translated)
Given 2026-09-28 in Claude Code session
d8bf7d34-4f31-407d-ac85-6cb849eec4bf(a PM session anchored inobjectstack-ai/cloud), after the seat showed that the cloud deployment'sOS_AUTH_REQUIRE_EMAIL_VERIFICATION=falseis inert:Reader
This PM seat claims and dispatches this card to an
os-devin the same session. The consumer isobjectstack-ai/cloud#2249, which is blocked on this card and then on a cloud pin move.Dedupe
Search (open + closed) on 2026-09-28:
requireEmailVerification open posture→ 9 hits,email verification audience posture→ 77,requireEmailVerification false refused→ 7. None asks for this. The closest are the closed #11739 / #11767, which introduced the forcing this card narrows.What is true on
maintoday (read atab946560fd)packages/plugins/plugin-auth/src/audience-posture.ts,assertAudienceConfig: under any posture that permits self-registration (email_domainoropen), an explicitemailAndPassword.requireEmailVerification: falseis refused at config entry. The message anchor isFORCES email verification on.packages/plugins/plugin-auth/src/auth-manager.tsforcesrequireEmailVerification: truewheneveraudiencePermitsSelfRegistration(posture), in both the wired better-auth options andgetPublicConfig().packages/spec/src/system/auth-config.zod.ts, theaudiencedocblock, states that invariant as protocol: bothemail_domainandopenforce verification on.⇒ A deployment on the
openposture has no way to run self-registration without mailbox verification. A pre-production deployment with no mail transport becomes a dead end at the verify page.Ruling (from the instruction above)
Make it a real switch, for the
openposture only:open, an explicitemailAndPassword.requireEmailVerification: falseis honoured, not refused. Absent ortruekeeps today's forced-on default, so nothing changes for anyone who does not opt out.email_domain, nothing changes: an explicitfalsestays refused. There the domain allowlist is the only gate, so an unverified signup is colleague impersonation, which is the reason the invariant exists.getPublicConfig()reports the value actually wired, so the advertisement cannot disagree with the behaviour.Acceptance
open+ explicitfalse: boots, and a sign-up without verification yields a session.getPublicConfig()reportsrequireEmailVerification: false, and the warning is emitted once.open+ absent ortrue: forced on, exactly as today.email_domain+ explicitfalse: still refused at config entry, with the same code and message family.invite_only: unchanged.openrefusal is flipped to assert the new behaviour, not deleted.Out of scope
Wiring the variable in the cloud deployment (
objectstack-ai/cloud#2249), and any change toemail_domainorinvite_onlysemantics.