Repository navigation
fix(cli): keep browser sign-in open through checkout - #6879
Conversation
The loopback listener timed out after 5 minutes, but plan checkout runs inside the OAuth flow. A slower purchase ended on ERR_CONNECTION_REFUSED. Raise the default to 60 minutes, matching pacific's plan-checkout handoff. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The waiting line now states the 60-minute limit and Ctrl+C. After a timed-out browser login the CLI says to run auth login again. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… limit Review fixes: the timeout hint no longer says purchases stay on the account (the CLI cannot observe billing); the no-timer-left test now asserts before any explicit close() and uses node:http so undici timers cannot pollute the count; docs mention the 60-minute window and Ctrl+C. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
somanshreddy
left a comment
There was a problem hiding this comment.
Review at 664e8061d: no blockers. The change does what it says. I have two nits, plus a few notes on the "hardest look" points.
What I checked
- Callers.
startLoopbackhas exactly one caller,startAuthorizationCodeFlow(oauth.ts:148). Nothing passestimeoutMsin production code, so the new default only affectshyperframes auth login(browser).--deviceand--api-keydon't touch it. - Server side. Nothing server-side expires sooner than the new window.
/v1/oauth/authorizecarriesstateandcode_challengein the URL, and the auth code is minted at grant time, which is after checkout. So no server TTL runs during the wait. Pacific'splanCheckoutHandoff.tskeeps its sessionStorage handoff forMAX_AGE_MS = 60 * 60 * 1000, so 60 min on the CLI side is the right match. One caveat: pacific's clock starts when checkout is marked, while the CLI's clock starts when the command runs. So the CLI is always the shorter of the two, by however long sign-in and choosing a plan took. That's fine; the CLI won't keep listening after pacific has given up. - Where the 5 min came from.
git log -S300_000points at #6754 (cb2389cf3). It raised the value from 120s to 300s as part of the reverse-any-video and billing work, with no stated reason. The JSDoc still said "120s" until this PR. I don't see a security reason to keep it short. The listener binds to127.0.0.1on an ephemeral port, rejects non-GET requests and unknown paths, and checksstate. A longer window only means a stale listener sits on a local port longer, and PKCE plusstatestill protect it. - The abandoned login blocking the terminal. The
setTimeoutkeeps the process alive for up to 60 min, but Ctrl+C exits cleanly, and the new waiting line says so. Themedia-useskill tells agents to have the person run! npx hyperframes auth login, so a human is at the keyboard. An agent calling it through a Bash tool is capped by that tool's own timeout. Acceptable. - Cleanup on success. The new fake-timer test asserts
vi.getTimerCount() === 0after the result resolves and beforeclose(). That pins the behaviour that matters most now: with an hour-long timer, a leaked timer would keep the CLI alive for the full hour after "Signed in".result.finally(close)→clearTimeout(timer)makes that hold. The boundary test (-1msnot settled,+1msrejects) is also good. - Recovery line. The
timedOutregex is unchanged from the telemetry split, soflow_timeoutandflow_errorbehave as before. The negative test (CSRF mismatch → no recovery line) is a good touch.
Nits
- The waiting line hardcodes the window (
oauth.ts:166-168). It prints "up to 60 minutes" even whenopts.timeoutMsis overridden. No production caller overrides it today, but deriving the text from the effective timeout, or exportingDEFAULT_TIMEOUT_MSand formatting from it, would keep the message and the timer from drifting apart the next time someone changes one of them. - The docs line says "If it times out, run it again." It could also mention
--devicefor SSH or headless setups where the browser can't reach the loopback port, since that's the other common "never called back" case. Optional.
Not verified
- A real checkout taking more than 5 minutes end to end. Same gap as the PR states.
- Whether a purchase made during a timed-out login is visible on the account. The PR doesn't claim this either, and I agree with not printing anything about it.
- CI was still running when I reviewed. I'll check it before anyone asks for an approve.
— Somu
Ctrl+C is now the advertised exit, but events only flush on exit, which a default SIGINT skips, so an abandoned login vanished from telemetry. Record it as aborted and exit 130 through requestCliExit. Close the listener on any throw, so a failure before the wait can't hold the process for the full window. Derive the waiting line and timeout message from the listener's own limit, and name the constant for why it is long, which clears the comment ratchet. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Edit accuracy: accurate 271 (base branch 271), smooth 122 of thoseThe gate passes. Quarantined, measured but not gated (0) Unstable (1)
|
somanshreddy
left a comment
There was a problem hiding this comment.
Re-review at 386e68b2d, covering the delta from 664e8061d (my last review), one commit: no blockers. Both nits are taken, and the new Ctrl+C and cleanup changes hold up.
- N1 (hardcoded window): fixed.
startLoopbacknow returnstimeoutLabel, derived from the effectivetimeoutMs, and both the waiting line and the timeout error use it.oauth.test.tspins it with a "7 minutes" stub. The/timed out/icheck inlogin.tsstill matches the newtimed out after 60 minutestext, so theflow_timeouttelemetry split is unchanged. No other code parses the old…msform (I greppedpackages/cli/src). - N2 (docs
--device): fixed. - Ctrl+C →
aborted, exit 130.process.once("SIGINT", recordCancel)callsrequestCliExit(130). In the real CLI,cli.ts:369registers the root requester, which runsfinalizeCliand thenprocess.exit, so the hour-long loopback timer can't keep the process alive. Usingonceis the right call. IffinalizeCliever stalls (say a telemetry flush on a dead network), a second Ctrl+C gets Node's default SIGINT and still kills the process. The listener is removed infinally, so a Ctrl+C during the later token exchange orreportIdentityfalls back to the default, with noabortedevent. That's acceptable. The test checks the listener set is restored. - Listener cleanup: correct. The authorize-URL build, the browser open and the wait now sit inside
try { … } finally { close() }. A throw fromopenBrowser(orbuildAuthorizeUrl) can no longer leave a 60-minute timer holding the process. On success,close()is idempotent with the existingresult.finally(close). ?error=before thestatecheck (deferred): agreed, a separate PR is fine. With the 12x window, the impact is that anything able to reach127.0.0.1:<ephemeral port>can abort a pending login. That's an availability problem, not a credential one, and PKCE plusstatestill protect the code path.
Nit (cosmetic, non-blocking): ${timeoutMs / 60_000} minutes produces "0.5 minutes" or long decimals for sub-minute or odd overrides. No production caller overrides the timeout, so it only matters if one appears.
CI is green at this head with no failing checks, and there are no open CHANGES_REQUESTED. I read the delta at diff level and checked the root exit path in cli.ts and commandResult.ts. I didn't re-run the suite locally or run a real checkout past 5 minutes.
— Somu
Problem
hyperframes auth loginwaits for the browser on a loopback listener that gave up after 5 minutes. The HyperFrames paywall now runs inside that OAuth flow (plan choice, Stripe checkout, return trip), so a slower purchase ended with the CLI printingSign-in failed: OAuth callback timed out after 300000ms. The browser's Authorize step then redirected to a dead port and showedERR_CONNECTION_REFUSED. Seen in production on 2026-10-31 on CLI 0.8.223.Change
loopback.ts). Pacific keeps the OAuth plan-checkout handoff for about the same window (MAX_AGE_MS,planCheckoutHandoff.ts), so a shorter limit leaves the same failure for slower checkouts. The CLI's clock starts first, so a user who spends 15 minutes before checkout gets 45 minutes of it.Run `hyperframes auth login` again.auth_login_failed/abortedand exits 130. Events flush onexit, which a default SIGINT skips, so these logins used to vanish from telemetry.120scomments corrected;docs/packages/cli.mdxmentions the window.The 300s value came in with #6754 (re-landed as #6833); neither commit message gives a reason for it. Flagging in case its author knows one.
Cost
An abandoned login now blocks its terminal for up to an hour (Ctrl+C ends it). Expect
flow_timeoutto fall and arrive later, withabortedrising, on any login funnel. An agent's foreground shell command still dies at its tool timeout, so that path keeps the old failure; background runs get the full window.Not in this PR
--deviceflow: its page has no checkout code, so no paywall wait.?error=before checkingstate. Pre-existing, but the window is now 12 times longer. Needs its own PR because it changes tested behaviour.Tests
startLoopbackis removed); timeout fires at exactly 60 min, not before.src/auth+src/commands/auth+ telemetry events: 322 pass. Fullpackages/clirun: 8 failures (checkSweep WebGL ×2, fileWatcher.atomic, studioServer.stampOnOpen, whisper manager.install, sherpa ×3), the same 8 fail with the original source and none import auth.🤖 Generated with Claude Code