| author | |
| committer | |
| log | 40d43a19670dd059f8d5705f801db7208620a2df |
| tree | 3ab2aa1c7c7f2beac5c91ce592b0e9000780ceac |
| parent | ffceb61fcbbdc7ac5268812a1cc5dfedd7a6f7b9 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>2 files changed, 64 insertions(+), 2 deletions(-)
packages/client-runtime/src/connection/registry.test.ts+42| ... | @@ -1410,6 +1410,48 @@ describe("EnvironmentRegistry", () => { | ... | @@ -1410,6 +1410,48 @@ describe("EnvironmentRegistry", () => { |
| 1410 | }), | 1410 | }), |
| 1411 | ); | 1411 | ); |
| 1412 | 1412 | ||
| 1413 | // The desktop rotates the bootstrap token, so the renderer exchanges it again | ||
| 1414 | // and the server revokes the old bearer while the socket stays open. HTTP | ||
| 1415 | // reads such as the session's scopes must move to the new bearer (#13324). | ||
| 1416 | it.effect("a replaced platform bearer reaches a connected session's HTTP requests", () => | ||
| 1417 | Effect.gen(function* () { | ||
| 1418 | const harness = yield* makeHarness([]); | ||
| 1419 | |||
| 1420 | yield* Effect.gen(function* () { | ||
| 1421 | const registry = yield* EnvironmentRegistry.EnvironmentRegistry; | ||
| 1422 | const registration = (token: string) => | ||
| 1423 | new BearerConnectionRegistration({ | ||
| 1424 | target: BEARER_TARGET, | ||
| 1425 | profile: BEARER_PROFILE, | ||
| 1426 | credential: new BearerConnectionCredential({ token }), | ||
| 1427 | }); | ||
| 1428 | const preparedToken = registry.run( | ||
| 1429 | BEARER_TARGET.environmentId, | ||
| 1430 | EnvironmentSupervisor.EnvironmentSupervisor.pipe( | ||
| 1431 | Effect.flatMap((supervisor) => SubscriptionRef.get(supervisor.prepared)), | ||
| 1432 | Effect.map((prepared) => | ||
| 1433 | Option.isSome(prepared) && prepared.value.httpAuthorization?._tag === "Bearer" | ||
| 1434 | ? prepared.value.httpAuthorization.token | ||
| 1435 | : null, | ||
| 1436 | ), | ||
| 1437 | ), | ||
| 1438 | ); | ||
| 1439 | yield* registry.reconcilePlatform([registration("old-bearer")]); | ||
| 1440 | yield* awaitConnectionState( | ||
| 1441 | registry, | ||
| 1442 | BEARER_TARGET.environmentId, | ||
| 1443 | (state) => state.phase === "connected", | ||
| 1444 | ); | ||
| 1445 | expect(yield* preparedToken).toBe("old-bearer"); | ||
| 1446 | |||
| 1447 | yield* registry.reconcilePlatform([registration("new-bearer")]); | ||
| 1448 | |||
| 1449 | expect(yield* preparedToken).toBe("new-bearer"); | ||
| 1450 | expect(yield* Ref.get(harness.sessions)).toHaveLength(1); | ||
| 1451 | }).pipe(Effect.provide(harness.layer), Effect.scoped); | ||
| 1452 | }), | ||
| 1453 | ); | ||
| 1454 | |||
| 1413 | it.effect("a bearer replaced while an attempt is in flight retries with the new one", () => | 1455 | it.effect("a bearer replaced while an attempt is in flight retries with the new one", () => |
| 1414 | Effect.gen(function* () { | 1456 | Effect.gen(function* () { |
| 1415 | const attemptStarted = yield* Deferred.make<void>(); | 1457 | const attemptStarted = yield* Deferred.make<void>(); |
packages/client-runtime/src/connection/registry.ts+22-2| ... | @@ -47,6 +47,7 @@ import { | ... | @@ -47,6 +47,7 @@ import { |
| 47 | RELAY_ROUTE_ID, | 47 | RELAY_ROUTE_ID, |
| 48 | connectionRouteId, | 48 | connectionRouteId, |
| 49 | connectionRoutes, | 49 | connectionRoutes, |
| 50 | credentialConnectionId, | ||
| 50 | entryWithRoutes, | 51 | entryWithRoutes, |
| 51 | findRouteToSameAddress, | 52 | findRouteToSameAddress, |
| 52 | mergeLearnedRoutes, | 53 | mergeLearnedRoutes, |
| ... | @@ -712,8 +713,10 @@ export const make = Effect.gen(function* () { | ... | @@ -712,8 +713,10 @@ export const make = Effect.gen(function* () { |
| 712 | // the equivalent runtime. Anything not yet connected may be using the | 713 | // the equivalent runtime. Anything not yet connected may be using the |
| 713 | // old bearer: an attempt in flight would fail with it and then wait | 714 | // old bearer: an attempt in flight would fail with it and then wait |
| 714 | // blocked for a manual retry. Retry with the new one instead. A | 715 | // blocked for a manual retry. Retry with the new one instead. A |
| 715 | // connected session keeps its socket. | 716 | // connected session keeps its socket, but the server revoked the old |
| 716 | if (bearerReplaced) { | 717 | // bearer, so its HTTP requests (such as the session's scopes) move to |
| 718 | // the new one. | ||
| 719 | if (bearerReplaced && registration._tag === "BearerConnectionRegistration") { | ||
| 717 | const scope = (yield* SubscriptionRef.get(serviceScopes)).get(target.environmentId); | 720 | const scope = (yield* SubscriptionRef.get(serviceScopes)).get(target.environmentId); |
| 718 | if (scope !== undefined) { | 721 | if (scope !== undefined) { |
| 719 | const state = yield* SubscriptionRef.get(scope.supervisor.state); | 722 | const state = yield* SubscriptionRef.get(scope.supervisor.state); |
| ... | @@ -725,6 +728,23 @@ export const make = Effect.gen(function* () { | ... | @@ -725,6 +728,23 @@ export const make = Effect.gen(function* () { |
| 725 | state.lastFailure.reason === "authentication") | 728 | state.lastFailure.reason === "authentication") |
| 726 | ) { | 729 | ) { |
| 727 | yield* scope.supervisor.retryNow; | 730 | yield* scope.supervisor.retryNow; |
| 731 | } else if (state.phase === "connected") { | ||
| 732 | const token = registration.credential.token; | ||
| 733 | yield* SubscriptionRef.updateSome(scope.supervisor.prepared, (current) => | ||
| 734 | Option.isSome(current) && | ||
| 735 | current.value.target._tag === "BearerConnectionTarget" && | ||
| 736 | credentialConnectionId(current.value.target.connectionId) === | ||
| 737 | registration.target.connectionId && | ||
| 738 | current.value.httpAuthorization?._tag === "Bearer" && | ||
| 739 | current.value.httpAuthorization.token !== token | ||
| 740 | ? Option.some( | ||
| 741 | Option.some({ | ||
| 742 | ...current.value, | ||
| 743 | httpAuthorization: { _tag: "Bearer", token }, | ||
| 744 | }), | ||
| 745 | ) | ||
| 746 | : Option.none(), | ||
| 747 | ); | ||
| 728 | } | 748 | } |
| 729 | } | 749 | } |
| 730 | } | 750 | } |