web: after-sign-in sends the app's sign-in back to finish instead of handing it an anonymous session

Draft
TE
24d ago
+81-4

@austinpower1258 FYI. This is the web half of #20234 bug (b) and needs Lawrence's yes before it lands, so it is labelled needs a call. Do not merge.

Leo's repro, every time: (1) Sign in, the browser signs in, and Leo cancels the browser's "open cmux?" prompt. (2) Sign in again and accept: the app fails about 225 ms later with serverError(0, "auth_failed"). (3) Sign in a third time: it works.

Likely cause (not yet proven):

  • after-sign-in mints a fresh session from whatever the browser holds at that moment. That includes an anonymous user (anonymous-if-exists), and it hands that session to the app.
  • The app's validation uses getUser(or: .throw). That rejects an anonymous user as USER_NOT_SIGNED_IN, which the app shows as auth_failed.
  • Prod logs at 06:33Z fit, matched by timing because they carry no user ID. The failing callback (06:33:10.5) follows an after-sign-in at 06:33:03, before that browser's oauth-callback at 06:33:06. The working one (06:33:23) follows the after-sign-in at 06:33:22.
  • #20234 now logs the token's is_anonymous and is_restricted claims on that failure, so the next repro can confirm or rule this out.

Fix: when the app's own Sign in (a native return carrying cmux_auth_state) reaches after-sign-in and the browser holds only an anonymous session, the browser is sent back to /handler/sign-in with the same return, including the handoff nonce. It is no longer handed a session the app will refuse. Purchase returns carry no state, so the anonymous purchaser's session is unchanged (the existing test still passes).

Not covered: a restricted user, or no user with cookie tokens. That path still hands back cookie tokens, because many existing tests rely on it. If the next repro's log shows is_restricted=true, that path needs the same treatment.

Not proven: whether Stack's sign-in page shows the form, or redirects again straight away, when it sees an anonymous session. This needs a live check before landing.

Tests, in web/tests/after-sign-in-route.test.ts:

  • sends the app's sign-in back to finish when the browser holds only an anonymous session: red at b7c35c8 (it handed the anonymous tokens to the app), green at eff0513.
  • still hands a full account's session to the app's sign-in: a guard.
  • The full file is 27 pass. bun run lint:complexity, eslint and tsc are clean.

Changelog

Fixed: Signing in to cmux again after cancelling the browser's "open cmux?" prompt no longer fails with "temporarily unavailable".

šŸ¤– Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

When a Sign in from the app reaches after-sign-in while the browser holds only an anonymous session, after-sign-in now redirects back to /handler/sign-in to finish the browser's sign-in instead of handing the app tokens the app rejects as not signed in.

Bug Fixes

  • Fixes #20234, where signing in again after cancelling the browser's "open cmux?" prompt failed with "temporarily unavailable" about 225 ms later.
  • Purchase returns still keep the anonymous purchaser's session, since they carry no auth state.
  • A restricted user, or no user with cookie tokens, still goes down the existing path that hands back cookie tokens.

Tests

  • Adds tests covering the anonymous-session redirect and confirming a full account is still handed to the app.