Repository navigation
fix(login): resume the login in flight instead of restarting it (CL-8857) - #14
Merged
Merged
Conversation
…857) Every attempt for a provider contends for the one registered redirect_uri, and the authorize page already open in the browser is bound to the state and PKCE verifier of the attempt that opened it. Starting over therefore either fails to bind the port or leaves that page redirecting into a listener that no longer recognises its state, refused as a mismatch. createLoginRegistry holds one live login per key and hands the same handle back to a second start; an entry is released when the login settles. The hub mount uses it, so a colliding start answers 200 with the pending login's id rather than 409. A login is resumed only for the tag that started it. A refused redirect is also now reported as a code -- state_mismatch, provider_error, no_code -- rather than prose, so each product maps it onto its own copy.
close() stopped the listener but left waitForCode pending, so a caller that gave up waited on a redirect that could no longer be received -- and anything tracking the login, such as the new registry, never learned it had ended and would resume a dead attempt. CL-8857
The loopback listener is on a fixed port anything on the machine can reach. Ending the wait on a mismatched state let any stray or crafted request -- a stale tab, a page firing a link at localhost -- cancel a legitimate sign-in. The redirect is still refused with 400 and its code never trusted; the login keeps waiting for its own. CL-8857
TheGreatAxios
force-pushed
the
cl-8857-resume-login-in-flight
branch
from
October 20, 2026 02:31
cd67cd5 to
3a720e6
Compare
A login leaves the registry as soon as it settles, while its credential is still being stored. A newer login for the same provider, possibly another principal's, can be live by then: the TTL sweep reaching the older entry cancelled the newer one, and its completion deleted the newer login's id, so a retry could no longer be resumed. Cancel now targets the login's own handle, and the id is released only if it is still that login's. CL-8857
TheGreatAxios
force-pushed
the
cl-8857-resume-login-in-flight
branch
from
October 20, 2026 02:32
3a720e6 to
16e1d79
Compare
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.
The bug
Every attempt for a provider contends for the one registered
redirect_uri, and the authorize page already open in the browser is bound to the state and PKCE verifier of the attempt that opened it. A second start therefore has two ways to fail, and both are reachable from an ordinary retry:mountOAuthLoginstarts a fresh login without ending the old one, so the callback server cannot bind the fixed port and the retry answers409.state mismatch, with the sign-in lost. That is how this was found:A well-formed state from
generateState(), just not the one the listener then expected.The change
createLoginRegistry(root export, nohono/@intx/*): one live login per key.startOrResume(key, begin, { tag })returns the handle already in flight, or starts one.resumedsays which.tagthat started it — one principal's authorization is not another's to continue. A different tag falls through tobeginand contends for the port exactly as today.completedthere also stops a rejection landing unhandled when a caller only readsauthorizeUrl.cancel(key)ends the login in flight and frees the port.mountOAuthLoginuses it: a start that collides with the same principal's pending login now answers200with that login's id and authorize URL instead of409.Also: a refused redirect is reported as a code —
state_mismatch,provider_error(carrying the server's ownerror),no_code— instead of prose. What a person should be told differs by product and brand; what the redirect was does not.OAuthCallbackErrorcarries the code too.Breaking
CallbackServerConfig.failedHtmlnow takesCallbackFailurerather than astring. Both known consumers are ours; Solutions Builder's pin bump lands with the matching change.Verification
bun run check— typecheck, lint, format, 58 tests pass (4 new for the registry: resume, tag isolation, release on settle, cancel). Not yet driven through a live ChatGPT sign-in; that happens on the Solutions Builder pin bump.Hardening from security review
mountOAuthLogincleans up by login, not by provider. A settled login leaves the registry while its credential is still being stored; a newer login for the same provider (possibly another principal's) could then be cancelled by the TTL sweep reaching the older entry, or lose its id so a retry could not resume. Cancel now targets the login's own handle; the id is released only if it is still that login's.59 pass. The mismatch change is covered by a test (a crafted redirect is refused, then the genuine one completes). The mount race has no test:
mountOAuthLoginhas no harness, since it needs a DB and tenant context. The fix comes from reading the code.