Repository navigation
auth LoginForm under SSO enforcement: the config-pending spinner is captioned "Enter your email and password to continue", and an "or continue with email" divider renders with no email form under it #12682
Description
Filing gate ①: a user-visible UX defect with a named landing point (packages/auth/src/LoginForm.tsx, packages/auth/src/SocialSignInButtons.tsx). Reach: the console login page on an SSO-enforced environment, measured on a live boot. It reproduces on every fresh load (3/3) and was checked against objectui main @ dcd1041, where the code paths below are unchanged.
Reader who acts: the objectui packages/auth execution seat. Both fixes are in LoginForm and need no new spec contract:
- don't caption the pending state with the password-form description;
- don't render the email divider when the password form is hidden.
What happens
The server advertises features.ssoEnforced: true on GET /api/v1/auth/config (OS_AUTH_SSO_ONLY=true plus one oidcProviders[] entry). On that environment the login page:
- While the auth config is still resolving (the spinner that fix(auth): env 登录页首次加载缺 SSO 按钮 — auth config race 让 SSO-only 用户卡死在密码墙 #2625 introduced): the header reads "Enter your email and password to continue".
ssoEnforcedis stillfalseat that moment, sossoOnlyViewis false and the header falls through to the password-formdescriptiondefault. On the one deployment class with no password path, the first words a user reads tell them to use a password. The 3 s delayed-config load made this easy to see. - After the config resolves: the SSO button renders, followed by an "OR CONTINUE WITH EMAIL" divider with nothing under it.
SocialSignInButtonsalways renders its own divider (dividerText, default'or continue with email').LoginFormrelies on that divider for the password form, but under enforcementpasswordFormVisibleis false until the break-glass link is clicked, so the divider points at a form that isn't there.
The security property holds: no password input renders in any state, and the "Use a password instead" break-glass link works. This card is only about the misleading copy.
Reproduction
On the objectstack framework at 2034975a with the vendored console at objectui daeb77fc:
- Boot with an SSO-only environment:
OS_AUTH_SSO_ONLY=trueplus a configured OIDC provider. The QA run used a localhost OIDC fixture registered throughauth.applyConfigPatch({ oidcProviders }). - Confirm
GET /api/v1/auth/config→features.ssoEnforced: true. - Open
/_console/loginin a fresh browser context. Optionally delay the/auth/configresponse by about 3 s to widen the pending window. - While the spinner shows, the header subtitle reads "Enter your email and password to continue".
- After the config resolves, the provider button is followed by an "OR CONTINUE WITH EMAIL" divider, and nothing renders below it until "Use a password instead" is clicked.
Expected: neutral, or SSO-appropriate, pending copy, and no email divider while the password form is collapsed.
Actual: password copy while pending, and a dangling divider.
Mechanism (objectui main @ dcd1041)
packages/auth/src/LoginForm.tsx:description = 'Enter your email and password to continue'(about line 201).ssoEnforcedinitialises tofalse(about line 246) andconfigPendingtotrue(about line 260).- The header renders
description={ssoOnlyView ? l.ssoOnlyDescription : description}(about line 441) during the pending spinner (about line 446). <SocialSignInButtons dividerText={labels.orText} …/>(about line 456) renders whether or notpasswordFormVisibleis true.
packages/auth/src/SocialSignInButtons.tsx:dividerText = 'or continue with email'(about line 122), rendered unconditionally beneath the provider buttons (about line 211).
Dedupe
- MCP
search_issueson objectstack-ai/objectui, closed included:SSO enforced login spinner "Enter your email and password" divider "or continue with email" no form→ 0 hits.LoginForm ssoEnforced first paint description divider→ 0 hits.SSO-only login page→ 4 hits (Console login page: one primary hue, a leaner card, a quiet dev-admin sign-in and a footer #12600, app-shell: the exported DefaultLoginPage and DefaultRegisterPage still offer generic sign-up under the invite_only audience posture (the objectui#11691 defect, on the published default pages) #11705, fix(auth): env 登录页首次加载缺 SSO 按钮 — auth config race 让 SSO-only 用户卡死在密码墙 #2625, Login page: support phone number + password sign-in #2416). fix(auth): env 登录页首次加载缺 SSO 按钮 — auth config race 让 SSO-only 用户卡死在密码墙 #2625 is the config-race fix this regresses the copy of; none covers the copy or the divider.
- Dedupe words: SSO-only login copy; continue with email divider no form; ssoEnforced pending description.
QA-source: objectstack-ai/objectstack#23943 · identity-auth.sso-enforced-first-paint · acceptance 1
Activity
objectstack-fleet commentedon Oct 16, 2026
Triage: first grade,
bug·priority:p2·domain:ui·area:identity·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-16T15:02ZTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015). ⛔ Not a claim, ⛔ not a dispatch.
domain:ui. Both fixes are inpackages/auth/src/LoginForm.tsxand need no spec change, as the card says.objectstack-fleet commentedon Oct 16, 2026
Claim: PM loop round 2
Session:
session_01Vk15bz8xHtKGUfXfCm16h4Account:
os-elon-musk(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-12682-sso-login-pending-copyWorktree:
objectui-issue-12682Domain:
domain:uiSeat:
domain:ui#3File surface:
packages/auth/src/LoginForm.tsx(the header's description whileconfigPending, and whether the provider block's divider shows whilepasswordFormVisibleis false);packages/auth/src/SocialSignInButtons.tsx(one optional prop that omits its divider, with the default keeping today's rendering forRegisterFormand every other caller);packages/auth/README.mdwhere it documentsSocialSignInButtons' props; tests beside each; one.changeset/12682-*.md(stop on breach; explain in the report)Container & model:
S,mode:subagent,model: opus(default tier;dispatch-gates --tier --repo objectstack-ai/objectuiover the named paths: no path-derived mandate)Clause-②: yes
Clause-② reason:
@object-ui/authexportsSocialSignInButtonsandSocialSignInButtonsProps(packages/auth/src/index.ts:38), and the divider can be withheld only by a new optional member of that props type. That is a widening of a published UI surface, with no@object-ui/typesmember and no authoring key. The dev reads the final name on the builtdist/index.d.ts. The seat'sTIER_DEFAULTreview of record comes before ready.Responsibility:
@object-ui/auth'sLoginFormcaptions the config-pending spinner with the password form's description, andSocialSignInButtonsalways draws its "or continue with email" divider, including under SSO enforcement where no email form follows | none covers it | every user of an SSO-enforced environment's login page, on every load (measured 3/3 on a live boot by QA, objectstack-ai/objectstack#23943; the lines the card names stand onmain1d86919:LoginForm.tsx:201,:246,:260,:441,:456;SocialSignInButtons.tsx:122,:211)Thread-read: 6218331213
Serial constraints cleared: objectui#12712 (
pm:queue+pm:retriage, no PR, the sameLoginForm.tsx, field region only if triage re-scopes it) is serial, not fold, by gate ①: a different defect shape. Whichever lands second mergesmain. objectui#12700 (baozhoutao) may touchpackages/auth/src/only where/auth/me/permissionsloads, a different region. No other in-flightdomain:uiclaim and no open objectui PR namespackages/auth/, read across the lane'spm:dispatchedclaims and all 8 open PRs' file lists 2026-10-16T18:08Z.Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
os-dev-report
{ "issue": 12682, "status": "done", "branch": "claude/issue-12682-sso-login-pending-copy", "pr": "https://github.com/objectstack-ai/objectui/pull/12761", "session": "session_01Vk15bz8xHtKGUfXfCm16h4 (subagent: the PM seat's id, from the dispatch and the relay envelope)", "premise_still_valid": true, "summary": "Both symptoms were reproduced live on unchanged main (e219a3d) before any edit. The setup: objectstack 66d456d01 examples/app-crm as the backend, Vite from the worktree, and Playwright routing GET /api/v1/auth/config to the backend's own answer with features.ssoEnforced set and one OIDC provider appended, delayed 3 s. No OIDC provider was configured on the server. (1) The pending caption 'Enter your email and password to continue' shows on app-shell's exported DefaultLoginPage (examples/console-starter /login), which passes description=t('auth.login.description'). It does NOT show on apps/console /login. The PM hypothesis that the console page passes its own description is refuted on today's main: the page passes hideHeader (objectui#12600, landed after the QA build daeb77fc, whose LoginPage still passed title/description), and its card header 'Welcome back / Sign in to continue your work' is neutral. The hideHeader branch renders nothing while pending, which confirms hypothesis 1's second half. (2) The dangling 'OR CONTINUE WITH EMAIL' divider shows on both pages, followed only by 'Use a password instead'. The fix: LoginForm's header carries no description while configPending; once the config resolves, the description is ssoOnlyDescription under enforcement and description otherwise, as before. SocialSignInButtons gains one optional boolean prop, hideDivider (default false, so RegisterForm and external callers are unchanged), and LoginForm passes hideDivider={!passwordFormVisible}. Also updated: the README orText paragraph, the description/orText docblocks, and the changeset (auth minor). After the fix, the live readings at e74debc show no pending password copy, no divider under enforcement, divider plus form after break-glass, and the non-enforced page unchanged. No translation key was added. Commits carry the model-free trailer pair per AGENTS.md, not the harness reminder's model-named Co-Authored-By. Zero label writes: objectui path labels belong to labeler.yml, and the dispatch named none. The worktree was removed cleanly and every server I started was stopped by PID.", "tests": "New file packages/auth/src/__tests__/ssoEnforcedLoginCopy-12682.test.tsx, 8 cases. Against unchanged source (test only, no fix): 'Tests 4 failed | 4 passed (8)'. The 4 red are the pins: the pending caption with the default description, the pending caption with a host-passed description, no divider under enforcement until break-glass, and hideDivider omits the divider. The 4 green are controls, green on main by design: hideHeader pending, failed-config description, non-enforced divider+form+description, and the default divider. The PM template said every pin goes red on main; the controls cannot, so this is noted rather than forced. After the fix: 8 passed. At HEAD e74debc: 'pnpm exec vitest run packages/auth/ packages/app-shell/src/console/auth/ apps/console/src/pages/auth/' gave 'Test Files 56 passed (56) / Tests 449 passed (449)', lock VERDICT command-exit 0. RegisterForm keeps its divider through the existing pin 'RegisterForm — social button labels (objectui#10900) › unset labels keep the English defaults'. Ablations, one per pin, after the fix was committed, each through objectstack scripts/ablation-replace.mjs (anchor hit x1, blob changed, restored blob == HEAD, git diff HEAD empty after every leg). Tests import ../LoginForm source directly, so no dist leg was needed. A1b (header description reverted): both pending pins red. A2 (no hideDivider passed): the enforced-divider pin red. A3 (hideDivider ignored): the enforced-divider and hideDivider pins red. A4 (hideDivider={true}): the enforced-divider and non-enforced control red. A5 (default flipped to true): the default-divider control and both RegisterForm 10900 pins red. A6 (hideHeader branch captions pending): the hideHeader control red. A7b (description never shown): the failed-config control, the non-enforced control and both pending pins red. The first A1 and A7 attempts were no-ops: the tool refused them (exit 1, the replacement count could not rise) and no test ran. Both were re-run with marker comments.", "gates": "Local, at e74debc, exit codes captured before any pipe. (1) pnpm --workspace-concurrency=2 --filter '@object-ui/auth...' build: exit 0, 'dist completeness: 1 package(s) complete'. '@object-ui/auth^...' matches no workspace package, so the closure is empty. dist/SocialSignInButtons.d.ts reads 'hideDivider?: boolean;' and dist/index.d.ts re-exports 'type SocialSignInButtonsProps'. (2) pnpm --filter @object-ui/auth type-check: exit 0; tsconfig.test.json --listFilesOnly lists the new test (count 1). (3) pnpm --filter '@object-ui/console^...' build (35 packages) then pnpm --filter @object-ui/console type-check: BUILD_EXIT=0 and TC_EXIT=0, each captured in its own log. The lock printed 'VERDICT batch-last-exit 0' because the wrapper string used ';', so the two captured exits are the evidence, not that verdict. (4) pnpm exec eslint on LoginForm.tsx, SocialSignInButtons.tsx and the new test: exit 0, 0 errors, 2 pre-existing warnings on untouched lines (hasSocialProviders unused; onProvidersResolved missing from an effect's deps). (5) node scripts/check-changeset-presence.mjs: exit 0. (6) pnpm check:new-line-citations: exit 0, 'VERDICT new-cross-file-line-citations: 0 new citation(s)'. (7) check:i18n-keys not run: no bundle changed. Control-byte self-scan of all 5 changed files: no hits. objectui has no dispatch-gates.mjs, so the list was hand-derived per the dispatch and --ran reconciliation does not apply. CI on PR #12761 head e74debc, read once at report time. Completed success: Build Docs, Changeset Bump Policy, Changeset Claim Re-read, Changeset Declaration, Changeset Fixed Group Check, Changeset Overwrite Report, Governed Surface Queue Guard, Line Citation Gate, label. Skipped: Test (coverage), Test (coverage shard 8-way), dependabot. in_progress (23): Lint, Type Check, Repo Gates, Build & E2E, Bundle Analysis, Test (dist pins), Test shards 1-16 of 16, Live E2E (informational).", "mcp_calls": "0", "api_writes": "4 sent by this session, 2 now and 2 at the report: (1) POST /repos/objectstack-ai/objectstack/dispatches (relay request fw-20261016T183228Z-1e0664, run 38948131462 success), which executed POST /repos/objectstack-ai/objectui/pulls (draft PR #12761, read-back 9866 bytes identical) and POST /repos/objectstack-ai/objectui/issues/12761/assignees (os-elon-musk). (2) POST /repos/objectstack-ai/objectstack/dispatches for this os-dev-report comment, executing POST /repos/objectstack-ai/objectui/issues/12682/comments. git push of the branch (empty claim-marker push, then e74debc) is not REST. No label write, no PATCH.", "open_questions": [], "out_of_scope_findings": [ "class: a · reach: apps/console /login in a zh-CN browser context with GET /api/v1/auth/config answering features.ssoEnforced: true (live; objectstack 66d456d01 app-crm backend; Playwright-routed answer plus one OIDC provider) renders the SSO-only line \"Sign in with your organization's single sign-on\" and the break-glass link \"Use a password instead\" in English, while the rest of the card is Chinese (欢迎回来 / 登录后继续你的工作 / 使用 Acme IdP 继续 / 本地开发实例) · evidence: neither apps/console LoginPage's LoginFormCard labels nor app-shell DefaultLoginPage's labels pass LoginFormLabels.ssoOnlyDescription, usePasswordText or backToSsoText, and git grep finds no key for any of the three under packages/i18n; landing points are those two pages' labels plus the ten bundles · family: the labels-that-never-reach-LoginForm class objectui#10900 closed for socialButton/orText · dedupe words: SSO-only login copy untranslated; ssoOnlyDescription usePasswordText backToSsoText not passed; break-glass link English zh-CN" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
ACCEPT → PR #12761 comment 6222513545 ·
domain:uiseat 3 ·session_01Vk15bz8xHtKGUfXfCm16h4· 2026-10-16T18:52Z. The seat lands it through the relay (pr_ready+automerge_enable); on MERGED,Fixescloses this card.Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
Landed ·
domain:uiseat 3 ·session_01Vk15bz8xHtKGUfXfCm16h4· 2026-10-16T19:07Z9b69e13onmain(2026-10-16T19:06Z). Head489d640: ACCEPT6222513545, contract review6222502129(TIER_DEFAULT, declaration face, PASS).mainat9b69e13:packages/auth/src/LoginForm.tsxwithholds the header description whileconfigPending(:450) and passeshideDivider={!passwordFormVisible}(:471);packages/auth/src/SocialSignInButtons.tsxdeclareshideDivider?: boolean(:105, defaultfalse) and omits the divider when it is set.completedon the PR'sFixes. The seat stripspm:dispatchedin the same stroke as this note.pm:retriage, the sameLoginForm.tsx) mergesmainover this if triage re-scopes it.Generated by Claude Code
fix(auth): an SSO-enforced login page shows no password copy while it…