Skip to content

fix(cli): keep browser sign-in open through checkout - #6879

Merged
vanceingalls merged 4 commits into
mainfrom
fix/cli-login-checkout-timeout
Nov 1, 2026
Merged

vanceingalls merged 4 commits into
mainfrom
fix/cli-login-checkout-timeout

Conversation

@vanceingalls

@vanceingalls vanceingalls commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

hyperframes auth login waits 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 printing Sign-in failed: OAuth callback timed out after 300000ms. The browser's Authorize step then redirected to a dead port and showed ERR_CONNECTION_REFUSED. Seen in production on 2026-10-31 on CLI 0.8.223.

Change

  • Listener default 5 min → 60 min (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.
  • The waiting line states the listener's own limit and Ctrl+C, and the timeout error reads in minutes. After a timed-out browser login only, the CLI prints Run `hyperframes auth login` again.
  • Ctrl+C during the wait is recorded as auth_login_failed / aborted and exits 130. Events flush on exit, which a default SIGINT skips, so these logins used to vanish from telemetry.
  • The listener now closes on any throw, so a failure before the wait can't hold the process for the full window.
  • Stale 120s comments corrected; docs/packages/cli.mdx mentions 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_timeout to fall and arrive later, with aborted rising, 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

  • --device flow: its page has no checkout code, so no paywall wait.
  • The loopback rejects the flow on ?error= before checking state. Pre-existing, but the window is now 12 times longer. Needs its own PR because it changes tested behaviour.
  • Whether a purchase made during a timed-out login shows on the account: unverified, so the CLI makes no claim about it.

Tests

  • New fake-timer tests: callback just before 60 min succeeds and leaves no timer behind (fails if the cleanup in startLoopback is removed); timeout fires at exactly 60 min, not before.
  • Login copy: recovery line appears for timeouts only.
  • src/auth + src/commands/auth + telemetry events: 322 pass. Full packages/cli run: 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.
  • Manually: built CLI shows the new waiting line. A real SIGINT exits 130 and frees the port.

🤖 Generated with Claude Code

vanceingalls and others added 3 commits October 31, 2026 00:46
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]>
@mintlify

mintlify Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
hyperframes 🟢 Ready View Preview Oct 31, 2026, 7:16 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

@somanshreddy somanshreddy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. startLoopback has exactly one caller, startAuthorizationCodeFlow (oauth.ts:148). Nothing passes timeoutMs in production code, so the new default only affects hyperframes auth login (browser). --device and --api-key don't touch it.
  • Server side. Nothing server-side expires sooner than the new window. /v1/oauth/authorize carries state and code_challenge in the URL, and the auth code is minted at grant time, which is after checkout. So no server TTL runs during the wait. Pacific's planCheckoutHandoff.ts keeps its sessionStorage handoff for MAX_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_000 points 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 to 127.0.0.1 on an ephemeral port, rejects non-GET requests and unknown paths, and checks state. A longer window only means a stale listener sits on a local port longer, and PKCE plus state still protect it.
  • The abandoned login blocking the terminal. The setTimeout keeps the process alive for up to 60 min, but Ctrl+C exits cleanly, and the new waiting line says so. The media-use skill 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() === 0 after the result resolves and before close(). 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 (-1ms not settled, +1ms rejects) is also good.
  • Recovery line. The timedOut regex is unchanged from the telemetry split, so flow_timeout and flow_error behave as before. The negative test (CSRF mismatch → no recovery line) is a good touch.

Nits

  1. The waiting line hardcodes the window (oauth.ts:166-168). It prints "up to 60 minutes" even when opts.timeoutMs is overridden. No production caller overrides it today, but deriving the text from the effective timeout, or exporting DEFAULT_TIMEOUT_MS and formatting from it, would keep the message and the timer from drifting apart the next time someone changes one of them.
  2. The docs line says "If it times out, run it again." It could also mention --device for 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]>
@github-actions

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 122 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

Unstable (1)

  • seqplainkeys-none-px-r0-root-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true

@somanshreddy somanshreddy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. startLoopback now returns timeoutLabel, derived from the effective timeoutMs, and both the waiting line and the timeout error use it. oauth.test.ts pins it with a "7 minutes" stub. The /timed out/i check in login.ts still matches the new timed out after 60 minutes text, so the flow_timeout telemetry split is unchanged. No other code parses the old …ms form (I grepped packages/cli/src).
  • N2 (docs --device): fixed.
  • Ctrl+C → aborted, exit 130. process.once("SIGINT", recordCancel) calls requestCliExit(130). In the real CLI, cli.ts:369 registers the root requester, which runs finalizeCli and then process.exit, so the hour-long loopback timer can't keep the process alive. Using once is the right call. If finalizeCli ever 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 in finally, so a Ctrl+C during the later token exchange or reportIdentity falls back to the default, with no aborted event. 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 from openBrowser (or buildAuthorizeUrl) can no longer leave a 60-minute timer holding the process. On success, close() is idempotent with the existing result.finally(close).
  • ?error= before the state check (deferred): agreed, a separate PR is fine. With the 12x window, the impact is that anything able to reach 127.0.0.1:<ephemeral port> can abort a pending login. That's an availability problem, not a credential one, and PKCE plus state still 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

@vanceingalls
vanceingalls merged commit 34ddced into main Nov 1, 2026
85 checks passed
@vanceingalls
vanceingalls deleted the fix/cli-login-checkout-timeout branch November 1, 2026 20:29

This branch was successfully deployed

1 active deployment
staging - docs — 386e68b2 Deployed Oct 31, 2026 by mintlify[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants