Skip to content

fix(service-settings): Google login saves ON when its credentials come from the server - #24874

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-24844-google-env-credentials-save
Oct 20, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-24844-google-env-credentials-save

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24844

Clause-②: no

What changes

auth.google_client_id and auth.google_client_secret in packages/services/service-settings/src/manifests/auth.manifest.ts are now required: false (they were required: true). Nothing else moves: no default, visible rule, option, label, help text or translation bundle. The google_enabled help already states the conjunction (Google is offered once a client ID and a client secret both come through, saved below or set as GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET on the server, which win over saved values). It still reads true, so it is unchanged. A code comment above the toggle now says why the two keys are not required.

One changeset: @objectstack/service-settings patch.

Why

  • SettingsService.setMany judges required against the STORED value. It reads only OS_* env names (envKeyOf), so the GOOGLE_* pair is invisible to it.
  • plugin-auth offers Google when the toggle is on and both credentials are available, saved or from the env pair, with the env pair winning (applyEnvSocialProviderFallbacks at boot, the Google block of bindAuthSettings). It needs the pair to be available. It never needs it stored.
  • So on an env-credential deployment, the declaration refused a save that the runtime would honour. setMany('auth', { google_enabled: true }) answered SETTINGS_VALIDATION with google_client_id/required and google_client_secret/required. The TOUCH gate judges both, because their visible reads google_enabled. An admin who switched Google off on the page could not switch it back on there.

Route C (the resolver reports the env pair as the source) needs settings-service.ts or a spec-level env-name declaration, and both are outside this card. See the decision record below.

The offer matrix, before and after

This was measured with a one-off probe (not a repo file). It uses a real SettingsService, the real auth manifest from builtinSettingsManifests, a real AuthPlugin booted with that service as settings, and AuthManager.getPublicConfig().socialProviders. The credential values are fake. Before is the base build at df8bb40a03. After is this branch's build at 6c1589e41c. "Offered" is the providers the login page lists; the source in brackets is whose client ID the registered provider carries.

deployment admin action before: save before: offered after: save after: offered
env pair none (toggle unset) n/a google (env) n/a google (env)
env pair save ON REFUSED (required x2) google (env), toggle stays unset saved google (env)
env pair save OFF saved none saved none
env pair save OFF, then ON ON REFUSED none (stuck off) saved google (env)
stored pair none n/a google (stored) n/a google (stored)
stored pair save ON saved google (stored) saved google (stored)
stored pair save OFF saved none saved none
stored pair save OFF, then ON saved google (stored) saved google (stored)
env + stored none n/a google (env) n/a google (env)
env + stored save ON saved google (env) saved google (env)
env + stored save OFF saved none saved none
env + stored save OFF, then ON saved google (env) saved google (env)
no credentials none n/a none n/a none
no credentials save ON REFUSED (required x2) none saved none
no credentials save OFF saved none saved none
no credentials save OFF, then ON ON REFUSED none saved none

Four more rows check what a cleared credential does. Their "before" column comes from an emulated base: this branch's build with required: true put back on the two keys in memory, which is the only behavioural difference in service-settings. The emulation's 16 shared rows were checked against the real base build's reading above, and they match exactly.

deployment admin action before: save before: offered after: save after: offered
stored pair client ID saved as an empty string REFUSED (google_client_id/required) google (stored) saved none
stored pair client ID reset (null) saved none saved none
env + stored client ID saved as an empty string REFUSED google (env) saved google (env)
env + stored client ID reset (null) saved google (env) saved google (env)

Reading. For every stored state, the providers offered are the same before and after. Only the saves that the base refused change. Three states can now be saved:

  • the toggle ON on an env deployment, which offers Google, the same as the default;
  • the toggle ON with nothing available, which offers no Google, the same as the default;
  • a credential stored as an empty string. plugin-auth reads that as unset (asTrimmedString). The base already reached the same offered state with a null reset, as the rows above show.

Who may sign in, and which providers the login page offers, do not change for any deployment shape.

Pins

packages/services/service-settings/src/manifests/auth.manifest.test.ts, on a real SettingsService with the real auth manifest:

  • declares neither credential key required.
  • saves Google ON with both credential fields empty, from unset and after an OFF. The service's env map carries fake GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET values. getNamespace hands plugin-auth google_enabled: true from the global source and both credentials from default, so plugin-auth falls back to the env pair.
  • control: a stored credential pair still saves and reads back.

The plugin-auth half, which maps stored or env values to offered providers, is already pinned in packages/plugins/plugin-auth/src/auth-plugin.test.ts:

  • "enables Google from env credentials when google_enabled is explicit true";
  • "enables Google from settings-managed credentials";
  • "keeps deployment Google credentials ahead of settings-managed credentials";
  • "disables env-backed Google when google_enabled is explicit false".

This PR does not touch plugin-auth.

Ablation (one-off, no permanent file)

required: true was put back on both keys through scripts/ablation-replace.mjs: the anchor hit once, and the blob changed 146f1574445f to 04ddcdc314d2. Then pnpm --filter @objectstack/service-settings exec vitest run --maxWorkers=2 src/manifests/auth.manifest.test.ts ran:

  • FAIL "declares neither credential key required": google_client_id: expected true to be false.
  • FAIL "saves Google ON with both credential fields empty, from unset and after an OFF": the promise rejected with code: 'SETTINGS_VALIDATION' and fields google_client_id/required and google_client_secret/required.
  • Tests 2 failed | 16 passed (18). The stored-credential control is among the passes.

The restore ran git checkout HEAD -- PATH on the absolute path. Afterwards the blob was 146f1574445f, equal to the HEAD blob; git diff HEAD was 0 bytes; git status --porcelain showed 0 lines. The file then ran 18 of 18 green. The test imports the manifest from source, so no rebuild was needed for this ablation.

Verification (head 6c1589e41c)

  • Closure: pnpm --workspace-concurrency=2 --filter '@objectstack/service-settings^...' --filter '@objectstack/plugin-auth...' build, exit 0. This adds plugin-auth and service-settings themselves to the dispatch's closure, for the probe. Both were rebuilt at the final head.
  • Tests: pnpm --filter @objectstack/service-settings test gave Test Files 54 passed (54) and Tests 906 passed (906).
  • Typecheck: pnpm --filter @objectstack/service-settings typecheck, exit 0. tsc --listFiles includes src/manifests/auth.manifest.test.ts.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 66 commands. 65 exited 0. pnpm check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET): it reads every package's dist/ and needs a whole-tree build, so it was NOT MEASURED locally; CI's Lint & Repo Gates runs it.
  • Reconciliation: --ran printed ✓ dispatch-gates --ran: 66 derived famil(ies) accounted for — 65 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3).

Decision record (options A / B / C from the card), on the four axes

  • Real business need.
    • The writer is the Setup → Authentication form (PUT /api/settings/auth, which calls setMany). The reader is plugin-auth's bindAuthSettings.
    • content/docs/deployment/environment-variables.mdx documents the env pair as a supported deployment shape.
    • The refusal is measured above, including the stuck-off row.
    • B and C both serve this need. A leaves the stuck-off state. C's extra, showing "set via env" on the page, has no measured pull.
  • Long-term soundness.
    • B makes the declaration say what the runtime needs: a credential that is available, not one that is stored.
    • C is also truthful. It teaches the resolver a non-OS_ env name, which is new resolver surface in the 18.x C8 territory.
    • A keeps a declaration that is wrong on a documented deployment shape.
  • Making AI mistakes harder.
    • B removes a constraint that is broader than the runtime's need, and adds no consumer-side tolerance. The manifest stays strict, and the help carries the conjunction.
    • Under A, an author who reads required: true would conclude that the credentials must be stored. That invites bootstrap code that copies the env secrets into settings.
    • C adds a mapping declaration that is one more thing to get wrong.
  • Startup focus.
    • B adds no surface and removes one declared constraint.
    • C is a capability expansion with no named pull.
    • A costs nothing and fixes nothing.

Recommendation and route taken: B.

One forward note, a reading and not measured here: objectstack-ai/objectui#13239 makes the console refuse Save while a visible required field is empty. With required: true kept, that check would block every Save on the Authentication page of an env-credential deployment while Google is on, which is its default. With required: false, the console and the server agree.

Related

Acceptance notes


Generated by Claude Code

…e from the server

The two Google credential keys in the auth settings manifest are no longer
required. The runtime needs the pair to resolve, saved on the page or from
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET, and the settings service cannot see
that pair, so the required flag refused google_enabled: true on every
env-credential deployment.

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 3fbef41b4a573f751fda81a875a6dd3bc4dd5009 → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

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

6c1589e41c8fa8c1ea9b242f6804491147cb1fa8
  1. Shape.

    • Draft, base main, first line Fixes #24844.
    • Line-start Clause-②: no in the body and the changeset.
    • 3 files, +104/−2, none on a governed surface (governedPathsIn answers []).
    • The trial merge onto origin/main is clean.
  2. Route B, read in the diff.

  3. The floor, measured on a real stack.

    • The stack was a real SettingsService, the real auth manifest and a real AuthPlugin, read through getPublicConfig().socialProviders with fake credentials.
    • The matrix covered 4 deployment shapes × unset / ON / OFF / OFF-then-ON, plus clearing and resetting a stored id.
    • The providers offered for every stored state are unchanged. Only saves the base refused now succeed:
      • env + ON now saves, and Google is offered from the env pair;
      • env + OFF-then-ON is no longer stuck withdrawn;
      • no credentials + ON saves, and no Google button shows, the same runtime state as the default.
    • One edge, accepted: clearing a stored id to "" used to be refused and is now saved, and Google is then withdrawn. That is an admin's explicit act, and its resulting state was already reachable by resetting the id to null.
  4. Pins and ablation.

    • auth.manifest.test.ts, on a real SettingsService, covers:
      • neither key declared required;
      • ON saving from unset and after an OFF, with both fields empty;
      • the stored-credential control.
    • Ablation (required: true back on both keys): 2 failed | 16 passed, the control green. The restore was proven by blob equality.
  5. Changeset, @objectstack/service-settings patch, read sentence by sentence:

    • the refusal and why the settings service cannot see the env pair;
    • the two keys no longer required;
    • "who may sign in, and which providers the login page offers, do not change";
    • saving ON with nothing resolving.

    Each sentence 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. The other, check:dual-build-cjs-loads, is NOT MEASURED (exit 3, prerequisite); CI's lint lane measures it.
    • service-settings: 906 passed.
  7. Accepted deviations.

    • The closure build was widened so the probe could import the two packages' dist.
    • The full-stack matrix is a one-off probe. plugin-auth is read-only for this card, and service-settings cannot import plugin-auth (that would be a workspace cycle). The plugin-auth half is pinned by four existing auth-plugin.test.ts cases, named in the PR body.
    • The clear and reset "before" rows come from an in-memory emulation of the base, checked against the real base build on the 16 rows they share.
    • One count read and the gate derivation reads ran outside the lock.
    • One merge of main.
    • Model-free trailers.
  8. Out-of-scope dispositions. The settings page cannot show that the server's Google pair is in force; that is route C, settings-service.ts, C8 territory. Recorded in Acceptance notes, with no measured pull.

  9. 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

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 20, 2026 12:53
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 20, 2026 12:53
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit fcb40ad Oct 20, 2026
42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24844-google-env-credentials-save branch October 20, 2026 13:24
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