Repository navigation
Commit 4be7281
fix(service-settings): Setup → Authentication says what the sign-up and Google toggles do (#24842)
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](https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 096ac40 commit 4be7281
7 files changed
Lines changed: 111 additions & 10 deletions
File tree
- .changeset
- packages/services/service-settings/src
- manifests
- translations
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| 6 | + | |
6 | 7 | | |
7 | 8 | | |
8 | 9 | | |
| |||
221 | 222 | | |
222 | 223 | | |
223 | 224 | | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
224 | 283 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
33 | 41 | | |
34 | 42 | | |
35 | 43 | | |
36 | | - | |
| 44 | + | |
37 | 45 | | |
38 | 46 | | |
| 47 | + | |
| 48 | + | |
39 | 49 | | |
40 | 50 | | |
41 | 51 | | |
| |||
426 | 436 | | |
427 | 437 | | |
428 | 438 | | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
429 | 445 | | |
430 | 446 | | |
431 | 447 | | |
432 | 448 | | |
433 | 449 | | |
434 | 450 | | |
435 | 451 | | |
436 | | - | |
| 452 | + | |
437 | 453 | | |
438 | 454 | | |
439 | 455 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
154 | 154 | | |
155 | 155 | | |
156 | 156 | | |
157 | | - | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
158 | 161 | | |
159 | 162 | | |
160 | 163 | | |
| |||
187 | 190 | | |
188 | 191 | | |
189 | 192 | | |
190 | | - | |
| 193 | + | |
191 | 194 | | |
192 | 195 | | |
193 | 196 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
125 | 125 | | |
126 | 126 | | |
127 | 127 | | |
128 | | - | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
129 | 132 | | |
130 | 133 | | |
131 | 134 | | |
| |||
234 | 237 | | |
235 | 238 | | |
236 | 239 | | |
237 | | - | |
| 240 | + | |
238 | 241 | | |
239 | 242 | | |
240 | 243 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
125 | 125 | | |
126 | 126 | | |
127 | 127 | | |
128 | | - | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
129 | 132 | | |
130 | 133 | | |
131 | 134 | | |
| |||
234 | 237 | | |
235 | 238 | | |
236 | 239 | | |
237 | | - | |
| 240 | + | |
238 | 241 | | |
239 | 242 | | |
240 | 243 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
267 | 267 | | |
268 | 268 | | |
269 | 269 | | |
270 | | - | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
271 | 274 | | |
272 | 275 | | |
273 | 276 | | |
| |||
376 | 379 | | |
377 | 380 | | |
378 | 381 | | |
379 | | - | |
| 382 | + | |
380 | 383 | | |
381 | 384 | | |
382 | 385 | | |
| |||
0 commit comments