From b0730e80e6637c6f9a3613247e21105a3b56101b Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Wed, 14 Oct 2026 03:01:28 -0700 Subject: [PATCH] fix(client-runtime): a new WSL bearer reaches the connected session The desktop rotates its bootstrap token, so the renderer exchanges it again and the server revokes the previous bearer. The registry stored the new bearer but a connected session kept the old one in its prepared HTTP authorization, so /api/auth/session read as unauthenticated and the WSL provider page showed Limited permissions until a full reload. Swap the new bearer into the connected session's prepared connection. The socket stays open, and the session scopes are fetched again with the new token. Fixes #13324 Co-Authored-By: Claude Opus 5.5 --- .../src/connection/registry.test.ts | 42 +++++++++++++++++++ .../client-runtime/src/connection/registry.ts | 24 ++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/packages/client-runtime/src/connection/registry.test.ts b/packages/client-runtime/src/connection/registry.test.ts index 821e244fd544..18d1f062f043 100644 --- a/packages/client-runtime/src/connection/registry.test.ts +++ b/packages/client-runtime/src/connection/registry.test.ts @@ -1410,6 +1410,48 @@ describe("EnvironmentRegistry", () => { }), ); + // The desktop rotates the bootstrap token, so the renderer exchanges it again + // and the server revokes the old bearer while the socket stays open. HTTP + // reads such as the session's scopes must move to the new bearer (#13324). + it.effect("a replaced platform bearer reaches a connected session's HTTP requests", () => + Effect.gen(function* () { + const harness = yield* makeHarness([]); + + yield* Effect.gen(function* () { + const registry = yield* EnvironmentRegistry.EnvironmentRegistry; + const registration = (token: string) => + new BearerConnectionRegistration({ + target: BEARER_TARGET, + profile: BEARER_PROFILE, + credential: new BearerConnectionCredential({ token }), + }); + const preparedToken = registry.run( + BEARER_TARGET.environmentId, + EnvironmentSupervisor.EnvironmentSupervisor.pipe( + Effect.flatMap((supervisor) => SubscriptionRef.get(supervisor.prepared)), + Effect.map((prepared) => + Option.isSome(prepared) && prepared.value.httpAuthorization?._tag === "Bearer" + ? prepared.value.httpAuthorization.token + : null, + ), + ), + ); + yield* registry.reconcilePlatform([registration("old-bearer")]); + yield* awaitConnectionState( + registry, + BEARER_TARGET.environmentId, + (state) => state.phase === "connected", + ); + expect(yield* preparedToken).toBe("old-bearer"); + + yield* registry.reconcilePlatform([registration("new-bearer")]); + + expect(yield* preparedToken).toBe("new-bearer"); + expect(yield* Ref.get(harness.sessions)).toHaveLength(1); + }).pipe(Effect.provide(harness.layer), Effect.scoped); + }), + ); + it.effect("a bearer replaced while an attempt is in flight retries with the new one", () => Effect.gen(function* () { const attemptStarted = yield* Deferred.make(); diff --git a/packages/client-runtime/src/connection/registry.ts b/packages/client-runtime/src/connection/registry.ts index caead65758df..501a19bb0b9e 100644 --- a/packages/client-runtime/src/connection/registry.ts +++ b/packages/client-runtime/src/connection/registry.ts @@ -47,6 +47,7 @@ import { RELAY_ROUTE_ID, connectionRouteId, connectionRoutes, + credentialConnectionId, entryWithRoutes, findRouteToSameAddress, mergeLearnedRoutes, @@ -712,8 +713,10 @@ export const make = Effect.gen(function* () { // the equivalent runtime. Anything not yet connected may be using the // old bearer: an attempt in flight would fail with it and then wait // blocked for a manual retry. Retry with the new one instead. A - // connected session keeps its socket. - if (bearerReplaced) { + // connected session keeps its socket, but the server revoked the old + // bearer, so its HTTP requests (such as the session's scopes) move to + // the new one. + if (bearerReplaced && registration._tag === "BearerConnectionRegistration") { const scope = (yield* SubscriptionRef.get(serviceScopes)).get(target.environmentId); if (scope !== undefined) { const state = yield* SubscriptionRef.get(scope.supervisor.state); @@ -725,6 +728,23 @@ export const make = Effect.gen(function* () { state.lastFailure.reason === "authentication") ) { yield* scope.supervisor.retryNow; + } else if (state.phase === "connected") { + const token = registration.credential.token; + yield* SubscriptionRef.updateSome(scope.supervisor.prepared, (current) => + Option.isSome(current) && + current.value.target._tag === "BearerConnectionTarget" && + credentialConnectionId(current.value.target.connectionId) === + registration.target.connectionId && + current.value.httpAuthorization?._tag === "Bearer" && + current.value.httpAuthorization.token !== token + ? Option.some( + Option.some({ + ...current.value, + httpAuthorization: { _tag: "Bearer", token }, + }), + ) + : Option.none(), + ); } } }