Wire real password reset into the Builder auth screen (Refs #7698) - #798
Merged
Merged
Conversation
Builder's "Forgot password?" button was a stub that never called anything:
if (mode === 'forgot' || mode === 'reset') { setError('Password reset is
coming soon — contact support.'); return }
so a founder clicking it hit a dead end (live: 2026-09-18).
Builder has no AINative password store of its own — core owns the users
table, the reset token and the reset email — so this is a proxy, not a
local implementation:
- app/api/auth/forgot-password/route.ts proxies to core
POST /api/v1/auth/forgot-password with app:'builder' (core's new branding
hook), and POST /api/v1/auth/reset-password for the final token exchange,
following the existing core-proxy pattern in app/api/build/register.
- Auth.tsx's forgot/reset modes now call it for real, with neutral
"check your email" and "password updated" confirmations matching the
existing verify-email panel. The forgot confirmation stays conditional
("if an account exists") so it cannot become the account-enumeration
oracle core carefully avoids being.
- app/reset-password/page.tsx is the landing page for the emailed link,
which core builds as {frontend_url}/reset-password?token=… — without it
every Builder reset link 404s. Thin redirect into the SPA's existing
'reset' screen, same shape as /refer. Allowlisted anonymously in
middleware.ts: a founder resetting a password is by definition logged out.
Tests: 20 new (red before the fix, green after) across the route and the
component. Full suite 5660 passed; tsc --noEmit clean.
Refs #7698
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Reported live 2026-09-18: a founder clicked "Forgot password?" in Builder and hit a dead end. The handler in
components/build/screens/Auth.tsxwas a stub that never called anything:Change
Builder has no AINative password store of its own — core owns the users table, the reset token, and the reset email. So this is a proxy, not a local implementation (unlike
app/api/auth/login/route.ts, which checks Builder's own DB).app/api/auth/forgot-password/route.ts(new) — proxies to core, following the existing core-proxy pattern inapp/api/build/register/route.ts(AINATIVE_API_URLbase, 25s timeout,{ok}response shape):POST /api/v1/auth/forgot-passwordwith{email, app: 'builder'}(core's new #7698 branding hook, so the email is Builder-branded and its link returns here)action:'reset'→POST /api/v1/auth/reset-passwordwith core's real{token, new_password}contractcomponents/build/screens/Auth.tsx— forgot/reset modes call it for real. Confirmation panels mirror the existing verify-email panel's shape and styling rather than inventing new patterns. The forgot confirmation stays deliberately conditional ("if an account exists…") so this screen cannot become the account-enumeration oracle core carefully avoids being.app/reset-password/page.tsx(new) +middleware.ts— see below.The landing page: it did NOT already exist
Worth calling out explicitly, since it was flagged as possible follow-up scope.
Auth.tsxhas a fullmode==='reset'branch rendering a "New password" field, and'reset'was already an allowed deep-link screen (added by Auth screens have no navigation back to the landing page #651).{frontend_url}/reset-password?token=…, which for Builder is nowhttps://builder.ainative.studio/reset-password?token=…— a hard 404 on Builder today. Every reset link would have died there.Because the SPA screen already existed, closing this was small and obvious from an existing precedent (
app/refer/page.tsx): a thin redirect forwarding the token into/build?screen=reset&token=…. Built, not deferred. It's allowlisted anonymously inmiddleware.ts— a founder resetting a password is by definition logged out, so gating it behind auth would make every reset link dead.Test evidence
20 new tests, genuine red → green (10 failed with the source stashed, 20 passed with it):
Route tests cover:
app:'builder'actually being sent, email normalization, invalid email rejected without calling core, no account-existence leak, 429 surfaced plainly, 502 on core unreachable, thenew_password(notpassword) field name, and expired-token errors surfaced rather than swallowed. Component tests cover: the stub message being gone, the neutral confirmation, local validation, token read from the URL, and error states not claiming false success.No regressions in the 3 pre-existing Auth suites:
Full suite and typecheck:
One unrelated flake,
__tests__/lib/generation-persist.test.ts(a background-retry timing test), fails only under full-suite parallel load — it passes in isolation both with and without this change, and this PR touches none of that code path.Companion PR
Core-side
appbranding parameter: AINative-Studio/core#7699. That must merge and deploy first — until it does, core ignores the unknownappfield and falls back to AINative branding, so this PR degrades safely rather than breaking.Refs #7698