From 8ea3018415de38b7cc35b0ecb556fabdeaa865a0 Mon Sep 17 00:00:00 2001 From: Federico Jaramillo Martinez Date: Sat, 18 Jul 2026 21:54:26 +0200 Subject: [PATCH] fix(auth): guard post-login status refresh Skip opportunistic status requests when the flow's originating machine is no longer selected, and discard in-flight status results after the machine or session selection changes.\n\nRefs #74 --- .../src/controllers/authController.test.ts | 73 +++++++++++++++++-- src/client/src/controllers/authController.ts | 13 +++- 2 files changed, 75 insertions(+), 11 deletions(-) diff --git a/src/client/src/controllers/authController.test.ts b/src/client/src/controllers/authController.test.ts index 8906f87..009c389 100644 --- a/src/client/src/controllers/authController.test.ts +++ b/src/client/src/controllers/authController.test.ts @@ -121,11 +121,7 @@ describe("AuthController", () => { const statusCalls: { session: Parameters[0]; machineId: string | undefined }[] = []; const appliedStatuses: SessionStatus[] = []; const { controller, getState } = createController( - { - selectedMachine: remoteMachine("remote-2"), - selectedSession: session, - authDialog: { step: "oauth", flow, machineId: "remote-1", inputValue: "https://callback" }, - }, + { selectedSession: session, authDialog: { step: "oauth", flow, machineId: "local", inputValue: "https://callback" } }, { respondOAuthFlow: (flowId, requestId, value, machineId) => { respondCalls.push({ flowId, requestId, value, machineId }); @@ -142,12 +138,75 @@ describe("AuthController", () => { await controller.respondOAuth(); await flushMicrotasks(); - expect(respondCalls).toEqual([{ flowId: "flow-1", requestId: "request-1", value: "https://callback", machineId: "remote-1" }]); + expect(respondCalls).toEqual([{ flowId: "flow-1", requestId: "request-1", value: "https://callback", machineId: "local" }]); expect(getState().authDialog).toBeUndefined(); - expect(statusCalls).toEqual([{ session, machineId: "remote-1" }]); + expect(statusCalls).toEqual([{ session, machineId: "local" }]); expect(appliedStatuses).toEqual([refreshedStatus]); }); + it("does not refresh a session from another selected machine when a flow completes", async () => { + const flow = oauthFlow({ prompt: { requestId: "request-1", message: "Enter secret", kind: "prompt", promptType: "secret" } }); + const respondMachines: (string | undefined)[] = []; + const statusMachines: (string | undefined)[] = []; + const appliedStatuses: SessionStatus[] = []; + const { controller } = createController( + { + selectedMachine: remoteMachine("remote-2"), + selectedSession: sessionInfo("session-2"), + authDialog: { step: "oauth", flow, machineId: "remote-1", inputValue: "secret-value" }, + }, + { + respondOAuthFlow: (_flowId, _requestId, _value, machineId) => { + respondMachines.push(machineId); + return Promise.resolve(oauthFlow({ status: "complete" })); + }, + status: (_session, machineId) => { + statusMachines.push(machineId); + return Promise.resolve(sessionStatus("session-2")); + }, + }, + (status) => { appliedStatuses.push(status); }, + ); + + await controller.respondOAuth(); + await flushMicrotasks(); + + expect(respondMachines).toEqual(["remote-1"]); + expect(statusMachines).toEqual([]); + expect(appliedStatuses).toEqual([]); + }); + + it("does not apply an auth status refresh after the selected session changes", async () => { + const flow = oauthFlow({ prompt: { requestId: "request-1", message: "Enter secret", kind: "prompt", promptType: "secret" } }); + const originalSession = sessionInfo("session-1"); + const statusResponse = deferred(); + const statusCalls: { session: Parameters[0]; machineId: string | undefined }[] = []; + const appliedStatuses: SessionStatus[] = []; + const { controller, setState } = createController( + { + selectedMachine: remoteMachine("remote-1"), + selectedSession: originalSession, + authDialog: { step: "oauth", flow, machineId: "remote-1", inputValue: "secret-value" }, + }, + { + respondOAuthFlow: () => Promise.resolve(oauthFlow({ status: "complete" })), + status: (session, machineId) => { + statusCalls.push({ session, machineId }); + return statusResponse.promise; + }, + }, + (status) => { appliedStatuses.push(status); }, + ); + + await controller.respondOAuth(); + setState({ selectedMachine: remoteMachine("remote-2"), selectedSession: sessionInfo("session-2") }); + statusResponse.resolve(sessionStatus(originalSession.id)); + await flushMicrotasks(); + + expect(statusCalls).toEqual([{ session: originalSession, machineId: "remote-1" }]); + expect(appliedStatuses).toEqual([]); + }); + it("leaves the OAuth dialog ready to retry if responding fails", async () => { const flow = oauthFlow({ prompt: { requestId: "request-1", message: "Paste callback", kind: "manual" } }); const { controller, getState } = createController( diff --git a/src/client/src/controllers/authController.ts b/src/client/src/controllers/authController.ts index b96839d..6c2990b 100644 --- a/src/client/src/controllers/authController.ts +++ b/src/client/src/controllers/authController.ts @@ -295,17 +295,22 @@ export class AuthController { } private async refreshStatus(machineId = selectedMachineId(this.getState())): Promise { - const session = this.session(); + const session = this.selectedSessionForMachine(machineId); if (session === undefined) return; try { - this.applyStatus(await this.api.status(session, machineId)); + const status = await this.api.status(session, machineId); + const current = this.selectedSessionForMachine(machineId); + if (current?.id !== session.id || current.cwd !== session.cwd) return; + this.applyStatus(status); } catch { // Status refresh is opportunistic after login completes. } } - private session() { - const session = this.getState().selectedSession; + private selectedSessionForMachine(machineId: string) { + const state = this.getState(); + if (selectedMachineId(state) !== machineId) return undefined; + const session = state.selectedSession; if (session === undefined || session.archived === true) return undefined; return session; }