fix(auth): clear OAuth callback timer and reorder EMA scope persist

Clear the loopback callback timeout in runner-interactive-oauth finally
so already_authorized and error paths do not hang the CLI/TUI. Check EMA
step-up satisfaction against minted token scope before persisting the
requested union, matching the standard OAuth path.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Bob Dickinson 2026-07-02 17:55:34 -07:00
commit 52023ff1c3
2 changed files with 92 additions and 10 deletions

View file

@ -58,6 +58,8 @@ export async function runRunnerInteractiveOAuth(
flowReject = reject;
});
let timeoutId: ReturnType<typeof setTimeout> | undefined;
try {
const { redirectUrl } = await server.start({
hostname: options.callbackListen.hostname,
@ -85,7 +87,6 @@ export async function runRunnerInteractiveOAuth(
const timeoutMs =
options.callbackTimeoutMs ?? DEFAULT_RUNNER_INTERACTIVE_OAUTH_TIMEOUT_MS;
let timeoutId: ReturnType<typeof setTimeout> | undefined;
const waitForCallback = Promise.race([
flowDone.finally(() => {
if (timeoutId !== undefined) {
@ -131,6 +132,9 @@ export async function runRunnerInteractiveOAuth(
return { kind: "success" };
} finally {
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
}
await server.stop().catch(() => {});
}
}

View file

@ -602,13 +602,13 @@ export class OAuthManager {
if (enriched.reason === "insufficient_scope") {
const silent = await trySilentEmaAuth(config);
if (silent.status === "success") {
if (enriched.authorizationScopes?.length) {
await this.oauthConfig.storage!.saveScope(
this.getServerUrl(),
enriched.authorizationScopes.join(" "),
);
}
if (await this.checkAuthChallengeSatisfied(enriched)) {
if (enriched.authorizationScopes?.length) {
await this.oauthConfig.storage!.saveScope(
this.getServerUrl(),
enriched.authorizationScopes.join(" "),
);
}
return { kind: "satisfied" };
}
}