Repository navigation
fix(service-settings): audience help and the identity-auth checklist state the email-verification rule as shipped - #20421
Conversation
…shipped email_domain always forces email verification on; open forces it on unless the deployment turns it off (OS_AUTH_REQUIRE_EMAIL_VERIFICATION=false, or emailAndPassword.requireEmailVerification: false in the stack config); a false stored through the console is refused under open. The audience group description and the audience_posture help said every non-invite_only posture forces it, in the manifest and all four locales. Claude-Session: https://claude.ai/code/session_017B6YKCGu8CTY2KBWgwaHAs Co-authored-by: Claude <noreply@anthropic.com>
…e audience env door as shipped self-signup-gate and email-verification-loop said every permitting posture forces email verification, and that the audience is config-only with no env knob. email_domain always forces it; open forces it unless the deployment declares it off, and a console-stored false is refused under open. The audience is also declared through the auth settings namespace (console and the OS_AUTH_AUDIENCE_* env overrides). Both items bump to revision 2 with a history entry; steps, oracles and the unaffected clauses are unchanged. Claude-Session: https://claude.ai/code/session_017B6YKCGu8CTY2KBWgwaHAs Co-authored-by: Claude <noreply@anthropic.com>
…n text Claude-Session: https://claude.ai/code/session_017B6YKCGu8CTY2KBWgwaHAs Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 8 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
Contract reviewServed-tier: Inputs read: card #20413 (body + all 4 comments: unlock ① Derived judgmentsThe diff is 7 files, +40/−23, string literals and JSON prose only. Every accept-set and public-surface implication, each judged:
② Semver levelThe declaration is
③ Boundary flags
Implemented-by: VERDICT: PASS Rendered by an isolated contract-review subagent and adopted by the Generated by Claude Code |
…-verification rule as shipped (objectstack-ai#20434) Fixes objectstack-ai#20413 Clause-②: no Round 2 of the card, location 4: the plugin-auth boot report. Round 1 (PR objectstack-ai#20421, landed as `24b70859`) covered locations 1–3. This round completes the card. It changes text, comments and tests only. No admission decision, verification default or accepted value moves. ## What changed The `no_sign_in_account_at_boot` report tells an operator to recover a locked-out deployment by inviting one address. Its advice said an `'open'` or `'email_domain'` posture "forces email verification on the INVITED login too". Later, in remedy (b), the same message said verification is forced "unless an 'open' deployment has itself declared it off". The first sentence stopped being true when `open` began honouring a deployment's opt-out (`65352b7d`, objectstack-ai#20406), so the message contradicted itself. **The rule, re-derived from `packages/plugins/plugin-auth/src` at `24b70859`:** - `resolveEmailVerificationRequirement` (audience-posture.ts), the one resolver the better-auth wiring and `getPublicConfig()` both read: - `email_domain` is always on; - `open` is on unless the declared value is `false`; - `invite_only` uses the declared value, off by default. - `assertAudienceConfig` refuses an explicit `false` under `email_domain` from any source. Under `open` it refuses a `false` whose declarant is `console`. - `applyConfigPatch` (auth-manager.ts) marks a patch as the deployment's unless its caller passes `requireEmailVerificationFrom: 'console'`. - The invitation carve-out in `decideAudienceAdmission` returns only `admit` and `grantPermissionSet`, so it cannot exempt anyone from verification. The only row born `emailVerified: true` is the walled-owner operator stamp, and an invitation-admitted creation never gets it. **The message now:** - states the rule once, in the invitation paragraph: - the INVITED login meets the same email-verification rule as any sign-up; - `email_domain` is always ON; - `open` is ON unless the DEPLOYMENT declared it off (`emailAndPassword.requireEmailVerification: false` or `OS_AUTH_REQUIRE_EMAIL_VERIFICATION=false`), and a `false` stored only through the settings console is refused there; - `invite_only` follows that declaration and is OFF by default. - keeps the scoped no-mail-transport rider and the advice order ("close the posture back to 'invite_only' BEFORE that person registers"). It adds "and leave verification at its default OFF", because under `invite_only` a declared `true` would still apply. - in remedy (b), refers back to that rule ("wherever it is ON") instead of restating it. The advice still matches `content/docs/deployment/self-hosting.mdx` ("Email verification under each posture"), which the module docblock names as this message's long form. That page already stated the rule correctly. **Files:** - `packages/plugins/plugin-auth/src/boot-sign-in-reachability.ts`: the remedy string, plus the file docblock's invitation bullet (the old "third bullet below applies to the INVITED login too" sentence). - `packages/plugins/plugin-auth/src/boot-sign-in-reachability.test.ts`: - the pin at the old line 164 is flipped. It now asserts the new text's substance: the invited-login rule, `email_domain` always ON, the `open` conditional, the console refusal, and the rider's new wording. - two negative pins are added, one per old unconditional spelling. - a new `objectstack-ai#20389` describe drives each clause against the manager: `open` + the deployment's `false` wires OFF; the same `false` from the console is refused and stays ON, with the deployment patch as control; `email_domain` + `false` refuses the boot, with a control; `invite_only` is OFF by default and ON when declared. - three titles and comments that repeated the old quantifier now say "with nothing declared". - `managerWith` takes an optional deployment `emailAndPassword` block. - `packages/plugins/plugin-auth/src/auth-manager.ts`: one comment near the old line 2295 (the objectstack-ai#15587 uniqueness-refusal block), restated as "on by default". - `packages/plugins/plugin-auth/src/signup-existing-address-refusal.test.ts`: one docblock sentence (old line 30), restated the same way. - `.changeset/20413-boot-report-verification-text.md`: `@objectstack/plugin-auth` `patch`. The boot report prose ships. Stale-phrase counts across `packages/plugins/plugin-auth/src`, BASE `24b70859` → HEAD `7898a47d`: "INVITED login too" 2 → 0; "every posture other than 'invite_only' ('open'," 1 → 0; "self-registration FORCES `requireEmailVerification` on (see" 1 → 0; "self-registration-permitting posture FORCES `requireEmailVerification`" 1 → 0; "third bullet below applies to the INVITED login too" 1 → 0. ## Verification (all at HEAD `7898a47d`) - **Build:** `pnpm --workspace-concurrency=2 --filter '@objectstack/plugin-auth^...' build`, run through the verify lock: `VERDICT command-exit 0`. Then `pnpm --filter @objectstack/plugin-auth build`: check-dts-emitted 2/2. - **Targeted tests:** `pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 src/boot-sign-in-reachability.test.ts src/signup-existing-address-refusal.test.ts` gave `Test Files 2 passed (2)` / `Tests 59 passed (59)`. - **Package tests:** `pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2` gave `Test Files 114 passed (114)` / `Tests 2458 passed (2458)`. - **Typecheck:** `pnpm --filter @objectstack/plugin-auth run typecheck` exited 0. It runs `tsc --noEmit`, the examples project, and `check:test-typecheck`, which reported "OK … 10 file(s) / 94 error(s) … held in test-typecheck-debt.json". - **Reverse verification.** The fix was committed first. Each leg ran through `scripts/ablation-replace.mjs` in WRAP mode, with a driver trap that restores from HEAD by absolute path. The test imports the module by relative path, so it reads `src/` and no rebuild was needed between legs. - Leg A put the old sentence back in place of the new one. The anchor went 1 → 0 and the blob went `b0be40fa` → `d1b521c9`. Result: `Tests 1 failed | 50 passed (51)`, failing `[objectstack-ai#15588/F1] the no-mail-transport rider is SCOPED to the default posture` with "expected '[auth] no_sign_in_account_at_boot: th…' to contain 'under the default \'invite_only\' pos…'". - Leg B added the old sentence beside the new text. Result: the same single test red, on the negative pin "not to match /an 'open' or 'email_domain' posture …/i". - Leg C added the old remedy-(b) quantifier. Result: the same single test red, on "not to match /every posture other than 'invite_onl…/i". - After all three legs, the blob equals the HEAD blob (`b0be40fa`) and `git diff HEAD` is empty. The direction observed was red, as expected. - **Lint, narrowed to the 4 touched `.ts` files:** `eslint --no-inline-config --format json` linted 4 files with 0 errors and 0 warnings. `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`, no typed rules), so this diff cannot change a verdict on an untouched file. The repo-wide run belongs to CI. - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 64 commands over these 5 paths. `--ran` reconciled "64 derived, 62 run, 2 NOT-MEASURED, 0 UNRUN". All 62 that ran exited 0. - NOT MEASURED: `pnpm check:dual-build-cjs-loads` exited 3 with PREREQUISITE NOT MET, because it needs every package's `dist/` and this tree built only the plugin-auth closure. Declared narrowing, applied to the one touched package: `node --check dist/index.js` exits 0, and `require('./dist/index.js')` loads 143 exports. The new text is present in `dist/index.js` and `dist/index.mjs` (1 each), and the old phrase in neither (0 each). - NOT MEASURED: `pnpm check:type-check-debt` exited 3 with PREREQUISITE NOT MET, because `@objectstack/runtime`, `service-cluster` and `service-job` have no built types here. Declared narrowing: the ledger's DEBT keys are `cloud-connection`, `hono`, `observability` and `spec-monorepo`, none of which depends on plugin-auth, and TEST_DEBT is empty. The only non-comment source change is the content of one string literal, so no type moves. - `check:nul-bytes`: OK. Control-byte self-scan over the 5 files: no hits. ## Acceptance notes Sweep, at `24b70859`: lines naming email verification (`email.?verification|requireEmailVerification|EMAIL_NOT_VERIFIED|REQUIRE_EMAIL_VERIFICATION`) that also carry a forcing or quantifier word (`forc|other than invite_only|every posture|permitting posture|widen`). That gave 51 lines in 19 files, and every hit outside round 1's surface was read in context. In the files this round owns, all hits are fixed. **Stale or incomplete outside this round's surface. All are comments or frozen history; none is edited here.** - `packages/plugins/plugin-auth/src/audience-gate-test-support.ts:17`: "`open` (and `email_domain`) force `requireEmailVerification` on". This is a docblock explaining why fixtures use the invitation lane. It is true for the undeclared fixture config but omits `open`'s opt-out. No carrier. - `packages/plugins/plugin-auth/src/admin-remove-user-gate-ordering.test.ts:135`: "`open` would force email verification on". The same shape, in a test comment. No carrier. - `packages/verify/src/harness.ts:778`: "`open` and `email_domain` force `requireEmailVerification` on". The same shape, in a comment. No carrier. - `packages/spec/src/migrations/entries/semantic/18.audience-posture-default-invite-only.ts:19`, and its generated mirror `packages/spec/src/migrations/registry.ts:6270`: "accepts forced email verification". This is the v18 migration's reason text, accurate for that release. It is `packages/spec` history, not live advice. **Read and left, because each is correct:** `audience-posture.ts:221` (it says "unless noted", then notes the `open` exception); `auth-manager.ts:1639` (it carries the objectstack-ai#20389 exception); the runtime log at `auth-manager.ts:4818` ("… when email verification is forced on" is a sufficient condition, not a rule statement); `packages/spec/src/system/auth-config.zod.ts` lines 408–419 and 588; `content/docs/deployment/self-hosting.mdx:580` and its posture section; `content/docs/deployment/environment-variables.mdx:84`; and the `audience-posture*.test.ts` assertions, which pin the refusal messages as they stand. `auth-plugin.ts` has no hit, so nothing reaches the file objectstack-ai#20412 holds. --- _Generated by [Claude Code](https://claude.ai/code/session_017B6YKCGu8CTY2KBWgwaHAs)_ Co-authored-by: Claude <noreply@anthropic.com>
Part of #20413 — the settings console text (4 locales) and the identity-auth checklist. The plugin-auth boot-diagnostic location of the same stale statement follows in a second round on the card (seat append below).
Clause-②: no
What changed
The auth settings console and the identity-auth platform checklist both said every audience posture other than
invite_onlyforces email verification on. Commit65352b7dchanged that rule. Each location now states the rule the code enforces.The rule, re-derived from
packages/plugins/plugin-auth/srcat base40b315b0:email_domainalways forces verification on (resolveEmailVerificationRequirement, audience-posture.ts). An explicitfalseis refused at entry from any source (assertAudienceConfig).openforces it on unless the deployment declares an explicitfalse, in either of two ways:emailAndPassword.requireEmailVerification: falsein the stack config;OS_AUTH_REQUIRE_EMAIL_VERIFICATION=false.bindAuthSettingsin auth-plugin.ts treats an env-sourced value as the deployment's own declaration.open, afalsestored through the settings console is refused. This is theverificationDeclaredBy: 'console'branch ofassertAudienceConfig, and it plays out two ways:falsesaved in one pass: the posture change is refused.falsesaved whileopenalready stands: the storedfalseis refused.invite_onlyuses whatever value is declared, and defaults to off.The code agrees with the card's statement of the rule.
Landing points (strings re-located on this tree by key and by each locale's wording)
service-settings/src/manifests/auth.manifest.tsaudiencegroupdescription;audience_posturedescriptionservice-settings/src/translations/en.tsgroups.audience.description;keys.audience_posture.helpservice-settings/src/translations/zh-CN.tsservice-settings/src/translations/es-ES.tsservice-settings/src/translations/ja-JP.tsdocs/qa/platform-checklist/areas/identity-auth.jsonThe card calls the posture string
help. In the manifest it is actually the specifier'sdescription; only the bundles call ithelp.A repo-wide search for each locale's wording found these files and nothing else that repeats the old rule, with one exception listed under Acceptance notes. Three other hits already state the new rule:
describe;content/docs/deployment/self-hosting.mdx.No
*.generated.tsbundle holds these strings."No env knob" was measured, and it is false
The checklist said the audience is config-only, with no env knob. In fact, the settings service derives an
OS_AUTH_*override for every auth manifest key (envKeyOf, settings-service.types.ts). So the three audience keys have these overrides:OS_AUTH_AUDIENCE_POSTUREOS_AUTH_AUDIENCE_ALLOWED_EMAIL_DOMAINSOS_AUTH_AUDIENCE_SELF_REGISTRATION_PERMISSION_SETProbe. A one-off probe, not committed, ran the real
SettingsServicewith the shippedauthSettingsManifest, set the env as shown, and calledgetNamespace('auth'):The plugin-auth half is already pinned. In
audience-posture-setting.test.ts, the case "open with the OS_AUTH_REQUIRE_EMAIL_VERIFICATION env override OFF is honoured" drivesbindAuthSettingswith env-sourced audience keys and asserts postureopen.Composition.
settingsis in the always-on capability slate for everyservepreset exceptminimal.Not measured: a live stock boot with those variables set. The checklist text says this.
Checklist: two items, beyond the two quoted sentences
The card quotes two
knownGapssentences. The same file states the same old rule in seven more fields across two items. These are fixed here as a bounded in-place fix, which all four conditions allow:check:platform-checklist.Left alone, those fields would contradict the rewritten gaps.
identity-auth.self-signup-gate, revision 1 to 2:knownGaps[0]now names both doors: stack config, and the settings namespace through the console or env. It says the env door is measured in halves, not yet end to end.knownGaps[1](the BOUNDARY note).variants[2].source[2].identity-auth.email-verification-loop, revision 1 to 2:fixtures.requires[0].acceptance[5].negative[2]. As written, it would have scored anopenboot with the deployment opt-out as a FAIL. That boot correctly advertisesfalse.source[0]andsource[1].Each item gets a new
historyentry with ref#20413. The earlier entries keep their text. I searched the file for nine old-rule phrases, such asconfig-only,no env knobandpermitting-posture boot advertising. On base40b315b0they occur 11 times; at head they occur 0 times outside the two new history entries.Verification
Every run below is at head
a22b90fc.Build.
pnpm --filter '@objectstack/service-settings^...' buildfinished with VERDICT command-exit 0, then the package itself was built.Tests.
pnpm --filter @objectstack/service-settings exec vitest run --maxWorkers=2: Test Files 33 passed (33), Tests 584 passed (584). This includes:settings-translation-coverage.test.ts, which checks that zh-CN, ja-JP and es-ES coversettings.auth;auth.manifest.test.ts.Typecheck.
pnpm --filter @objectstack/service-settings run typecheck(tsc --noEmit) exits 0.Derived gates.
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 57 commands, and all 57 were run. Reconciled with--ranand exit codes: 57 derived, 56 run, 1 NOT-MEASURED.pnpm check:dual-build-cjs-loads. It exited 3 (PREREQUISITE NOT MET) because it needs every package'sdist/. Declared narrowing: I applied the gate's two properties to the one package the diff touches.node --check dist/index.cjsexits 0.require('./dist/index.cjs')loads (46 exports, new text present).check-plugin-teardown-shape.mjs --self-testfirst exited 3, because its pinned fixture commit was outside the shallow clone. After I fetched that one commit at depth 1, it passed 48 cases.pnpm check:platform-checklist: OK, 15 areas, 266 items.pnpm check:nul-bytes: OK.Roster gates. The derivation said the rosters of three gates live under
packages/, so their silence is not evidence either way. I ran them anyway, and all three exit 0:check:authz-resolver,check:error-code-casing,check:filter-alias-parity.Translation gates. No gate script reads these translation files. A search of
scripts/and.github/forservice-settings/src/translationsandsettingsBuiltinTranslationsfinds 0 hits. As a control, the same search surface does findservice-automation. The only gate on these files is the package's coverage test above.Acceptance notes
packages/plugins/plugin-auth/src/boot-sign-in-reachability.tsline 478 still says anopenoremail_domainposture "forces email verification on the INVITED login too". Line 492 of the same message already names theopenopt-out. plugin-auth source is outside this claim, so this goes to the PM as a finding. The phrase is pinned inboot-sign-in-reachability.test.ts.auth-manager.ts, near line 2295, still says a self-registration posture forces verification on. It is a comment only.Changeset
.changeset/20413-verification-posture-statements.md:@objectstack/service-settingspatch, because the strings ship in that package. Thedocs/qafile is not published.Seat append (
domain:servicesseat #6021,session_017B6YKCGu8CTY2KBWgwaHAs)Part of #20413. The dev's report5867903998found one more location of the same stale statement, outside this PR's claim surface: theno_sign_in_account_at_bootremedy prose inpackages/plugins/plugin-auth/src/boot-sign-in-reachability.ts, pinned by its test. By the family rule it joins Stale "every non-invite_only posture forces email verification" statements after #20389: settings console text (4 locales) and the identity-auth platform checklist #20413 as a second round instead of a new card, so this PR no longer closes the card.Generated by Claude Code