fix(auth): LoginForm's registerUrl has no default, so an absent URL renders no sign-up link (objectui#11634) - #11652
Conversation
…enders no sign-up link (objectui#11634)
Both console login pages pass `registerUrl={undefined}` when the server
reports `emailPassword.disableSignUp`. LoginForm destructured
`registerUrl = '/register'`, so that undefined brought the default back and
a sign-up-disabled deployment still showed "Don't have an account? Sign up".
The default is dropped: the link renders only for a passed URL. The prop's
type is unchanged and no second "off" value is added (triage ruling).
Tests: LoginForm pins absent/undefined -> no link and a passed URL -> that
href; the enforced-mode pin now passes registerUrl so it still measures the
enforced guard; a console LoginPage pin covers disableSignUp true/false.
Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL
Co-authored-by: Claude <noreply@anthropic.com>
…osing its '/register' default (objectui#11634) States the behaviour change for a caller that left `registerUrl` out and relied on the default, and what it now passes to keep the link. Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
ACCEPT: PR objectui#11652, head
|
Fixes #11634
Clause-②: no
What changed
LoginForm(packages/auth/src/LoginForm.tsx) no longer defaultsregisterUrlto'/register'. The "Don't have an account? Sign up" row renders only when the caller passes a URL, and the prop's doc comment now says so. The prop's type is unchanged (registerUrl?: string). Nonullarm is added, per the triage ruling on the card (comment 5986895176)..changeset/11634-loginform-register-default.md:'@object-ui/auth': minor. It states the behaviour change: a caller that left the prop out and relied on the default now passesregisterUrl="/register"to keep the link.packages/auth/src/__tests__/LoginForm.test.tsxand a newapps/console/src/pages/auth/__tests__/LoginPage.sign-up-gate-11634.test.tsx.Why
When
/auth/configreportsemailPassword.disableSignUp: true, both console login pages passregisterUrlasundefined. The destructuring default turned thatundefinedback into'/register', so a deployment with sign-up turned off still offered "Sign up".Callers and examples (measured on
origin/main)LoginPage(itsLoginFormCard) passessignUpDisabled ? undefined : registerUrl, whereregisterUrlis'/register'or'/register?redirect=…'. It already passes the URL whenever sign-up is on, so it is unchanged.@object-ui/app-shell'sDefaultLoginPage(mounted byexamples/console-starter) passessignUpDisabled ? undefined : '/register'. It is unchanged for the same reason.LoginFormwith noregisterUrl:packages/auth/README.md(theAuthGuardfallback and the forms example), theAuthShelldoc comment example, andskills/objectui/guides/auth-permissions.md. None of them states or expects a sign-up link; the skills guide lists "email/password fields, social login buttons, forgot password link". They now render no sign-up link, and none needs the URL to stay correct, so none is edited. The skills guide is on the governed surface in any case.apps/sitepage and nocontent/docs/**page rendersLoginFormor states the'/register'default, so no docs page changes.LoginForm's own@examplealready passesregisterUrl="/register", so it is still correct.socialButtonLabels-10900.test.tsxrendersLoginFormwith noregisterUrl. It asserts only the social-button labels, so it is unaffected.One existing pin was re-judged (not deleted).
LoginForm — SSO-only (enforced) mode› "hides the password form + sign-up and shows a break-glass link when features.ssoEnforced" rendered with noregisterUrland asserted no "Sign up". Without a default, that assertion holds whatever the enforced guard does. It now passesregisterUrl: '/register', so it measures the!ssoEnforcedguard. Ablation leg B below shows it goes red when that guard is removed.New pins
LoginForm:registerUrlleft out, or passed asundefined, renders no link and no "Don't have an account?" text.registerUrl="/x"renders the link with href/x.LoginPage:disableSignUp: truerenders no "Sign up" link.disableSignUp: falserenders it with href/register. The test uses the mockAuthClientthatLoginPage.dev-admin-hint.test.tsxalready uses, so it adds no production seam. The dev-admin hint comes from the same config read that sets the sign-up gate, so the test waits for it before it judges the link.Verification at
b906a13(branch head, after one merge oforigin/mainate398a54)Each command was run from the repo root, with its exit code captured before any pipe.
pnpm exec vitest run packages/auth/pnpm exec vitest run apps/console/src/pages/auth/pnpm exec vitest run packages/app-shell/src/console/auth/pnpm --filter @object-ui/auth type-checktsc --noEmit && tsc -p tsconfig.test.json;--listFileson the test project includesLoginForm.test.tsx.@object-ui/authhas no workspace dependencies, so its build closure is empty.pnpm --filter @object-ui/auth lintLoginForm.tsx(hasSocialProvidersunused) was already onmain.--format jsonshows 3 files, 0 errorscheck-changeset-presence/check-changeset-no-major/check-changeset-fixed/check-changeset-overwritecheck:changeset-claims/check:pending-changeset-literalscheck:new-line-citationscheck:control-bytes/check:test-path-rootscheck:vi-mock-specifiers/-inherit/-override-shapeNarrowed lint. The repo-wide
pnpm lintbelongs to CI. Locally I linted the 3 changed.ts/.tsxfiles, whicheslint.config.jsputs in the**/*.{ts,tsx}population (the 2 tests are also in the**/*.test.{ts,tsx}/**/__tests__/**blocks).--format jsonreported 3 files.eslint.config.jssets noparserOptions.projectand noprojectService, so linting is not type-aware and this diff cannot change the verdict on any untouched file.NOT MEASURED: console
type-check(tsc --noEmitoverapps/console, the only program that compiles the new console test). Reason: that program resolves every@object-ui/*import through the package's builtdisttypes, so it needs the console's whole workspace build closure, and that build was not run here. CI runs it. The new file follows the pattern of the existingLoginPage.dev-admin-hint.test.tsxand is lint-clean.Ablation (from the committed head,
traprestore,ablation-replace.mjs: the anchor must hit, and the restore is checked against the HEAD blob)'/register'default put back (on-disk count of the default line: 0 → 1 during the mutation → 0 after). 3 tests failed and 25 passed:renders no sign-up link when registerUrl is left out,… is passed as undefined(both "expected a href=/register … to be null"), and the consolerenders no sign-up link when the server reports disableSignUp: true. The console red also shows the console test reaches thesrcof@object-ui/auth(vitest alias), not a staledist.!ssoEnforceddropped from the sign-up row guard (guard count 1 → 0 → 1). 1 test failed and 25 passed: the re-judged enforced-mode pin.c62cd03ca3a4) andgit diff HEADwas empty.Live before/after
Backend: objectstack
mainat27991556,examples/app-showcase,objectstack dev --seed-admin --fresh, from a separate objectstack worktree (port 4634). Console: this worktree's Vite dev server (port 5634) proxied to that backend. Route/login(the dev server's basename is/; it is the sameLoginPageroute the card reached at/_console/login). Each context was a fresh Chromium context: wait for network idle and the identifier field, wait 4 s more, then counta[href$="/register"].GET /api/v1/auth/configemailPassworda[href$="/register"]per contextOS_DISABLE_SIGNUP=true{"enabled":true,"disableSignUp":true,…}'/register'default put back (the base line)[1,1,1], each/register "Sign up"OS_DISABLE_SIGNUP=true[0,0,0], no "Don't have an account?"OS_DISABLE_SIGNUPunset){"enabled":true,"disableSignUp":false,…}[1,1,1], each/register "Sign up"In every context the dev-admin banner rendered and the page's own
/auth/configresponse carried thedisableSignUpvalue above, so the config was applied before the link was counted.Acceptance notes
forgotPasswordUrlstill defaults to'/forgot-password', soundefinedbrings that link back too. No caller passesundefinedto switch it off: both login pages pass"/forgot-password", andAuthPublicConfighas no flag that turns password reset off. Nothing pulls on it, so it is noted here and not filed. Out of scope for this card.LoginForm's config-loading gate. The console app's page startssignUpDisabledatfalse, andDefaultLoginPagestarts it atundefined, which falls to'/register'. So on a sign-up-off boot both pages show the link until their config read resolves, and then drop it. The steady state is what this card judged.DefaultLoginPage's comment says itsundefinedstart avoids this flicker, butundefinedfalls to the URL arm. This is polish, not filed.packages/auth/README.mddoes not documentregisterUrlbefore or after this change. The behaviour change is stated in the changeset.Session:
https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsALGenerated by Claude Code