feat: allow signup via the cli-auth browser flow - #5686
Conversation
boxel-cli's browser authorization page was login-only, so the only way a brand-new user could complete `boxel profile add` was Google SSO auto-provisioning — email/password signup was unreachable from the CLI. Add a register mode to the /cli-auth page that reuses the web app's <RegisterUser> flow (email verification, invite token, and the full post-signup bootstrap that gives the user a personal realm). Registration mints exactly one device; that device is POSTed to the CLI's loopback listener and then forgotten from this browser's persisted session, so the CLI is its sole owner (a later browser-side logout can't revoke it). The account-level bootstrap side effects persist regardless of device. The registration entry points now await the SDK load, since the standalone /cli-auth route can reach registration before the SDK has finished loading (operator mode always boots first). The CLI's loopback timeout is extended to cover the interactive email round-trip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The cli-auth page offers registration to a browser that already holds a session — it prefills that account's username on the password form — but registration bootstraps a new account onto the same page, and the session already there belongs to someone else. The realm-server token is what carries the old identity across: it persists in localStorage apart from the Matrix session and its payload names a session room, and the realm-auth handshake adopts that room. The new account was never invited to it, so the join is forbidden and the bootstrap fails there — before the personal realm exists, and before the device can be handed to the CLI, which then waits on a hand-off that never comes. The failing bootstrap also logs out, revoking the device it just minted. So forgetting a persisted session now forgets all three of its keys rather than the Matrix one alone, and entering register mode does that and reloads. The reload sheds the in-memory half — requests already in flight under the old identity otherwise land afterwards and persist a token again — and clearing once more on the far side sweeps anything that did. The port and nonce stay in the URL, so the CLI is still waiting on the other side of it. The account itself keeps every device: this is local to the browser, and the page says so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
[Claude Code 🤖] Pushed 8a05d29, which fixes a defect manual testing turned up: registering from a browser that already holds a session never reached the hand-off, so the CLI waited forever. The page offers registration to a signed-in browser — it prefills that account's username on the password form — but registration bootstraps a new account onto the same page. What carries the old identity across is the realm-server token: it persists in localStorage apart from the Matrix session, its payload names a session room, and the realm-auth handshake adopts that room. The new account was never invited to it, so the join is forbidden and the bootstrap dies there — before the personal realm exists and before
Also in this push:
Verified by hand against the local stack, both paths end to end: a never-signed-in browser and a browser signed in as another account. Each delivered a working session to the loopback, with exactly one device on the new account, the personal realm bootstrapped, and One caveat on reviewing this branch: signup cannot be exercised in a browser until #5691 lands (CS-12436) — an unrelated pre-existing bug blocks the email step everywhere, including production. |
There was a problem hiding this comment.
Pull request overview
This PR extends the browser-based CLI authorization page (/cli-auth) to support account creation in addition to login, so the CLI can complete boxel profile add for brand-new users via the same registration flow the web app uses.
Changes:
- Add a register mode to the
/cli-authUI and hand the newly minted registration session back to the CLI loopback listener. - Add an optional
@onComplete(session)hook to<RegisterUser>so callers can consume the bootstrapped session without forcing an operator-mode transition. - Increase the CLI loopback wait window and add a new end-to-end Matrix Playwright test covering signup → email verification → session handoff → personal realm bootstrap.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/matrix/tests/cli-signup.spec.ts | Adds an end-to-end Playwright test that signs up via /cli-auth, verifies email, receives the session via loopback, and asserts bootstrap results. |
| packages/host/app/utils/local-storage-keys.ts | Introduces a shared constant for the realm-server session localStorage key. |
| packages/host/app/services/realm-server.ts | Uses the shared realm-server session localStorage key constant instead of an inline string. |
| packages/host/app/services/matrix-service.ts | Awaits ready for registration entry points and adds forgetPersistedSession() for local-only session clearing used by the CLI auth flow. |
| packages/host/app/components/matrix/register-user.gts | Adds optional @onComplete(session) callback invoked after bootstrap completes. |
| packages/host/app/components/matrix/cli-auth.gts | Adds registration UI/mode, hands off registration session to CLI, and clears persisted browser session locally to avoid device ownership conflicts. |
| packages/boxel-cli/tests/lib/sso-login.test.ts | Updates unit expectations to match the new, longer browser wait timeout. |
| packages/boxel-cli/src/lib/sso-login.ts | Extends loopback wait timeout and updates user messaging to include account creation. |
Suppressed comments (1)
packages/matrix/tests/cli-signup.spec.ts:199
- [Claude Code 🤖] This comment also uses temporal phrasing (“used to prevent”) rather than stating the contract the test asserts. Consider rewriting to describe the invariant: the bootstrap completes and a personal realm exists even when starting from a browser with persisted realm-server tokens.
// The bootstrap ran to completion — this is what the stale token used to
// prevent.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The authorization page tells the person how long the CLI keeps its listener open, and the CLI enforces that window. Two copies of the number drift: the page quoted a quarter of an hour while the listener waited half of one. Both sides now read CLI_AUTH_TIMEOUT_MS from runtime-common, which both packages already depend on, and the page renders it through the same describeDuration the CLI prints — so the sentence a person reads and the deadline they are racing cannot disagree. Also states what the page does to the browser's own session accurately, now that one of its three paths ends that session locally. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
I’m getting a CORS error trying to exercise this locally, will keep at it ETA: it’s #5691 of course |
backspace
left a comment
There was a problem hiding this comment.
This worked well for me; nothing blocking but some suggestions:
I’d guess it wouldn’t be too much to make this match the rest of the site design?
I have a baroque development browser setup so I had to copy this URL to have it run in the right place, Claude Code in the terminal has a press c shortcut that copies the URL for you, could be nice to steal
I got an error after creating the account but it’s surely an environment mode thing
The pages the loopback listener serves after sign-in finishes were bare system-font HTML. They now carry the same dark shell, Boxel mark, and type treatment as the host app's auth screens, inlined because nothing else is served from that address. The error page also HTML-escapes the message it interpolates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
While the CLI waits on the browser, pressing c copies the authorization URL through the platform's clipboard tool — for anyone whose default browser isn't the one they want to finish the flow in. Raw mode is what makes the keypress visible and it also swallows Ctrl-C, so Ctrl-C is re-implemented; piped stdin gets no watcher and the wait message only promises the keys that work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
[Claude Code 🤖] The pages the CLI's loopback listener serves at the end of the auth flow now match the host app's auth screens (previously bare white system-font HTML). Also added a Success page Error page |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
heh what happened here
I tried it out again, looks good!


Extends the browser-based CLI auth flow (CS-12370, #5661) so a brand-new user can sign up, not just log in. Today
/cli-authis login-only, so the only way a new user completesboxel profile addis Google SSO auto-provisioning — email/password signup is unreachable from the CLI.Resolves CS-12426.
What this does
packages/host/app/components/matrix/cli-auth.gts): adds aregistermode plus a "Don't have an account? Create a new Boxel account" affordance (and a path back to login), rendering the reused<RegisterUser>flow.port/statepersist across the mode switch (they stay in the URL, same as the forgot-password resume).@onCompletehook (register-user.gts):<RegisterUser>gains an optional@onComplete(session). AfterinitializeNewUserruns the full web-parity bootstrap (personal realm + realm auth), the minted session is handed toonComplete. The default operator-mode path is unchanged when the arg is absent — one registration flow, no bespoke CLI form.onRegisterCompletePOSTs it to the CLI's loopback listener, then calls the newMatrixService.forgetPersistedSession()— a local-only session clear (no server-side logout, which would revoke the device) — so the CLI is the device's sole owner. Account-level bootstrap side effects (personal realm, realm auth) persist regardless.requestRegisterEmailToken,registerRequest,isUsernameAvailable) nowawait this.ready. The lightweight/cli-authroute can reach registration before the Matrix SDK has finished loading (operator mode always boots first, so the web register form is only reached once the SDK is ready). The e2e below surfaced this.boxel-cli/src/lib/sso-login.ts): the session loopback branch already accepts a freshly-minted session generically, so no protocol change. The loopback timeout is extended 15→30 min to cover the interactive email round-trip, and the browser prompts now say "sign in or create an account".Testing
New matrix e2e
packages/matrix/tests/cli-signup.spec.ts: real browser register → email verify (smtp4dev) → loopback receives the session →whoamion the delivered device, and asserts the personal realm was bootstrapped. Passing locally — this exercises the standalone bootstrap end-to-end (the main verification risk).Also: host
ember-tscclean, boxel-clitscclean,boxel-clisso-loginunit tests updated and passing (23/23), eslint + prettier clean on all touched files.Follow-up / not in this PR
reCAPTCHA is an unimplemented TODO in
register-user.gts. Invite-only is handled — the reused form already collects the registration token — but if prod Synapse enforces reCAPTCHA, cli-auth signup hits the same wall as the web app and needs an infra decision (parallels CS-12370's infra PR for the SSO whitelist). Overlaps with CS-12427 (defaultboxel profile addenv to production).🤖 Generated with Claude Code