Skip to content

fix(service-settings): Setup → Authentication says what the sign-up and Google toggles do - #24842

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-24776-auth-settings-contradictions
Oct 20, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-24776-auth-settings-contradictions

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24776

Clause-②: no

What changes

Setup → Authentication showed two controls that read as contradicting the system on a fresh install. Both are still truthful about what the runtime does; what they lacked is the second condition. This PR states it. Only wording changes. No default, visible rule, required flag, option or stored key moves. The admission matrix below is byte-identical before and after.

  • auth.signup_enabled: relabelled Allow email sign-up (was Allow self-service registration), and it now has help text. The help says the toggle opens the email-and-password sign-up form, and the Self-registration audience decides who that form admits:

    • invite_only: only an address holding a pending invitation;
    • email_domain: the allowlisted domains, plus invited addresses;
    • open: anyone.

    Off closes the form under every audience, invitations included.

  • auth.google_enabled: the help says Google sign-in is offered only once a client ID and a client secret both resolve, either saved on the page or from GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET on the server (the server values win over saved ones). Until both resolve, the login page shows no Google button, even while the toggle is on. Off withdraws Google even when the server sets both.

  • The en, zh-CN, ja-JP and es-ES bundles carry the new label and help. en matters too: the console resolves settings.auth.keys.KEY.label / .help from the served bundle before the manifest literal, so if en lagged, the English page would keep showing the old text.

Files: packages/services/service-settings/src/manifests/auth.manifest.ts, auth.manifest.test.ts, src/translations/{en,zh-CN,ja-JP,es-ES}.ts, .changeset/24776-auth-settings-signup-google-help.md (patch, @objectstack/service-settings). Nothing under plugin-auth/src, settings-service.ts, settings-service-plugin.ts, packages/spec or objectui is written.

What was measured (the card's premise holds)

The composition: the real SettingsService with the real auth manifest, the real AuthPlugin booted init → start → kernel:ready, and the real better-auth pipeline over an in-memory engine. bindAuthSettings reads that service. Each cell boots fresh with one existing human (so the bootstrap bypass is off), a pending invitation for invitee@elsewhere.org, and member_default as the declared self-registration set. The probes:

  • POST /api/v1/auth/sign-up/email through manager.handleRequest;
  • getPublicConfig() (what /auth/config serves the login page);
  • SettingsService.get (what the page shows).

The instrument was a one-off test file and is not committed.

H2 — signup_enabled × audience_posture. "unset" is the manifest default, read with source default. Every cell has zero bind refusals.

signup_enabled audience_posture stranger@acme.com stranger@elsewhere.org invitee (pending invitation)
unset / true unset / invite_only 403 SELF_REGISTRATION_CLOSED 403 SELF_REGISTRATION_CLOSED admitted
unset / true email_domain (acme.com) admitted 403 EMAIL_DOMAIN_NOT_ALLOWED admitted
unset / true open admitted admitted admitted
false unset / invite_only 403 SELF_REGISTRATION_CLOSED 403 SELF_REGISTRATION_CLOSED 400 EMAIL_PASSWORD_SIGN_UP_DISABLED
false email_domain 400 EMAIL_PASSWORD_SIGN_UP_DISABLED 403 EMAIL_DOMAIN_NOT_ALLOWED 400 EMAIL_PASSWORD_SIGN_UP_DISABLED
false open 400 EMAIL_PASSWORD_SIGN_UP_DISABLED 400 EMAIL_PASSWORD_SIGN_UP_DISABLED 400 EMAIL_PASSWORD_SIGN_UP_DISABLED
env OS_AUTH_SIGNUP_ENABLED=false open 400 400 400
env OS_DISABLE_SIGNUP=true open 400 400 400
unset env OS_AUTH_AUDIENCE_POSTURE=open admitted admitted admitted

(The table folds pairs of cells that read the same. The record has 15 separate rows: 12 stored-or-default, plus 3 env.)

So signup_enabled is a door of its own, in series with the posture. false closes what the posture alone leaves open, and under invite_only it still decides whether an invited address can register through the form. Unset reads as true and an unset posture reads as invite_only, so the page's defaults already show the runtime's effective state. The contradiction was only in the wording.

H3 — Google. No GOOGLE_* value is printed anywhere; the instrument set fake values in-process.

shape save page shows login offers Google client id wired from
no credentials anywhere, google keys unset (the tester's shape) — ON, both fields empty no —
env credentials, google_enabled unset — ON, both fields empty yes env
env credentials + save google_enabled: true refused google_client_id/required, google_client_secret/required ON, empty yes env
env credentials + stored google_enabled: false ok OFF no —
env credentials + OS_AUTH_GOOGLE_ENABLED=false — OFF (env) no —
no credentials + save google_enabled: true refused (required) ON, empty no —
stored credentials, google_enabled unset ok ON, both SET yes stored
stored credentials + stored google_enabled: false ok OFF no —
env AND stored credentials, google_enabled unset ok ON, both SET yes env

The env-credential deployment with an unset toggle signs in with Google, both before and after this PR. No default moves.

Before / after. Base 87c09ebb7a vs head fbf586d971: the 26-line record is byte-identical (diff exit 0; sha256 74e45ec11dd103de… on both sides). A control line from the after-run reads the edited manifest (signup_enabled.label=Allow email sign-up), which proves that run loaded the change it claims to compare.

Why this route (decision frame: business need · long-term · AI-proofing · startup scope)

signup_enabled. Chosen: relabel plus help text stating the conjunction.

  • Hide it under invite_only (visible): rejected. The matrix shows the toggle decides the invitee column there. A stored false hidden under invite_only would keep invited people out with the cause off-screen.
  • Default false: rejected. The runtime's default is open (disableSignUp unset). The page would read OFF over an open form, and the first save that carried the shown value would close the invitee door, which is a narrowing this card may not make.
  • Retire the key: not applicable. It has a reader and an effect of its own (the whole false row).
  • On the four axes: the chosen route serves a measured need (the tester's reading); it adds no surface (one description, one label); and its pin ties the help to the posture option table, so a posture added later reds until the help covers it.

google_enabled. Chosen: help text stating the conjunction.

  • Default false: rejected on measurement. It reads OFF on the two shapes that do offer Google (env credentials with the toggle unset; stored credentials with the toggle unset), and its visible rule would hide the stored credentials there. The runtime reads explicit values only, so its behaviour would not move; the page would simply stop telling the truth on those shapes.
  • A conditional info_banner: rejected. It is one more specifier and eight more strings, and it cannot see env credentials either, so its text would hedge exactly as the help does.
  • Have the settings resolver report GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET as an env source for the two keys: this would let the page show "Set via env", but it lives in settings-service.ts or the spec. Both are outside this claim. It is raised in the report as an open question, with the save refusal below.

Pins and ablation

auth.manifest.test.ts gains three cases:

  1. signup_enabled keeps default true and its email_password_enabled-only visible rule, and its help names every audience_posture option value (read from the option table) and the select's label.
  2. google_enabled keeps default true, and its help names GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.
  3. The en bundle carries the manifest label and help verbatim for both keys.

Ablation (scripts/ablation-replace.mjs, one leg per changed declaration, run on the committed head fbf586d971). Each leg was mutated, run red, then restored, with the restore proven by blob == HEAD and an empty git diff HEAD:

  • delete under invite_only, …; from the signup_enabled help → 2 failed / 13 passed (cases 1 and 3); restored to blob 1fc310c429e5.
  • replace GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET on the server with credentials on the server → 2 failed / 13 passed (cases 2 and 3); restored to blob 1fc310c429e5.
  • revert the en label to Allow self-service registration → 1 failed / 14 passed (case 3); restored to blob 8784d7d80de3.

The test imports the manifest and bundle by relative path, so it reads src/: no build or dist/ leg is involved.

Verification (head fbf586d971)

  • pnpm --filter @objectstack/service-settings exec vitest run --maxWorkers=2 → 53 files, 897 tests passed.
  • pnpm --filter @objectstack/service-settings run typecheck → exit 0. tsc --listFiles counts auth.manifest.test.ts in the program once.
  • Derived gates (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, 66 commands): 65 exit 0. --ran reports 66 accounted, 65 run, 1 NOT-MEASURED, 0 UNRUN.
  • NOT MEASURED: pnpm check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET (exit 3). It reads every package's dist/, and this worktree built only the service-settings / plugin-auth closure. CI runs it.
  • Not run locally, declared to CI: test:repo; plugin-auth's suite, because this diff writes nothing there and changes only manifest strings.
  • main moved four commits past the base while this ran; none of them touches service-settings or plugin-auth, so the branch is not re-merged.

Acceptance notes

  • Save refusal on env-credential deployments (handed to the seat as a finding). On a deployment whose Google credentials come from GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET, saving google_enabled: true from the console is refused (google_client_id/required, google_client_secret/required) while the login page offers Google. So an admin who switches Google off on the page cannot switch it back on there without re-typing credentials the server already holds. This PR leaves required alone, because that is save-validation territory.
  • The old label overclaimed (code reading only, not measured end to end). signup_enabled binds emailAndPassword.disableSignUp, which better-auth consults only on the email sign-up route (dist/api/routes/sign-up.mjs). Social sign-up reads the provider's own flag (dist/api/routes/callback.mjs), so Allow self-service registration overstated what the switch closes. The new label names what it actually controls.
  • Group descriptions not touched. The email_password group description ("…and self-service registration") and the membership group description ("Pairs with self-service registration above") still use the old phrase. They are outside this claim's specifiers. Carrier: none.

Generated by Claude Code

…le toggles do

signup_enabled is relabelled "Allow email sign-up" and its help states the
conjunction with the audience posture: the form admits only what the posture
admits, and off closes it under every posture, invitations included.
google_enabled's help states that Google is offered only once a client ID and
secret resolve, saved here or from GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET.
Defaults, visible rules, required flags and options are unchanged; the en, zh-CN,
ja-JP and es-ES bundles carry the new label and help.

Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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 — 9 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 7697ddcf9421955642f3878ea990421497b99f83 → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:services seat 1 · 2026-10-20T10:05Z

os-justin · session_011TdZ8GjixNZFLz1xJD9zzK (seat post #6021). Reviewed against GitHub and origin/main 5cb6e6e0aa, not against the report (os-dev-report on #24776). Head under review:

fbf586d9712813bd46a10660d16fd520c8dd1d26
  1. Shape. Draft, base main, first line Fixes #24776, line-start Clause-②: no in the body and the changeset. The body names no other issue. 7 files, +111/−10, none on a governed surface (governedPathsIn answers []). The trial merge onto origin/main is clean.
  2. Wording only, read in the diff.
    • signup_enabled is relabelled Allow email sign-up and gains help text stating the conjunction with Self-registration audience, per posture. Its default stays true and its visible rule is unchanged.
    • google_enabled's help now says Google is offered only once a client id and secret resolve, saved or from GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET, which win. Its default stays true.
    • No default, visible rule, required flag, option or stored key moves. The en, zh-CN, ja-JP and es-ES bundles carry the same label and help. The zh-CN text was read: it states the same per-posture rule.
  3. The premise, measured on a real stack (the dev's readings, checked against the code).
    • signup_enabled is the email sign-up route's own door (auth-plugin.ts:1875–1877, emailAndPassword.disableSignUp), in series with audience_posture. Measured:
      • unset or true × invite_only: strangers get 403 SELF_REGISTRATION_CLOSED and an invitee is admitted;
      • false × invite_only: the invitee gets 400;
      • false × open: everyone gets 400.
        So the toggle ON beside Invitation only is not a contradiction. It is the form that invited addresses use, and the old label overclaimed.
    • google_enabled unset + env credentials: the login page offers Google. Keeping the default true preserves that shape.
    • The 15-cell admission matrix (including 3 env rows) and the 9 Google shapes are byte-identical before (87c09ebb7a) and after (fbf586d971): record sha256 74e45ec11dd103de on both sides. The after-run control line proves the edit was loaded. This honours the dispatch's floor: no change to who may self-register or sign in.
  4. Pins and ablations. auth.manifest.test.ts pins both toggles' defaults, the sign-up visible rule, each posture and env name in the help, and the en bundle's label and help verbatim. Three ablation legs (the invite clause removed, the env names removed, the en label reverted) each turn red. The restores were proven by blob equality through ablation-replace.mjs.
  5. Changeset, @objectstack/service-settings patch, read sentence by sentence: "no default, visible rule, required flag, option or stored key moves, and no deployment admits or signs in anyone it did not before", the two bullets, and the four bundles. Each matches 2 and 3.
  6. Gates. At ACCEPT, every check-run on this head is success or an expected skip, among them TypeScript Type Check, Lint & Repo Gates, Test Core 1–6/6, Build Core and Governed Surface Queue Guard. Locally, 65 of 66 derived families exited 0. 1 was NOT MEASURED (check:dual-build-cjs-loads, exit 3, prerequisite), and CI's lint lane measures it. service-settings: 897 of 897 tests passed.
  7. The dev's open questions, answered by the seat.
  8. Accepted deviations.
    • A one-off measurement instrument, never committed.
    • The branch is 4 commits behind main, none of them in service-settings or plugin-auth. The merge group builds the combination, and the trial merge is clean.
    • One lock queue-timeout (exit 99) was re-run; it is not a measurement.
    • Commit trailers are model-free, per AGENTS.md.
  9. Out-of-scope dispositions.
  10. Landing clause. Tier S: not governed, under the human-merge threshold. Next step: ready + auto-merge through the relay once every check on the current head is green or an expected skip. A push resets this reading.

Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants