1import {
2 EnvironmentId,
3 ORCHESTRATION_PROTOCOL_VERSION,
4 type DesktopSshEnvironmentTarget,
5} from "@t3tools/contracts";
6import { describe, expect, it } from "@effect/vitest";
7import * as Effect from "effect/Effect";
8import * as Layer from "effect/Layer";
9import * as Option from "effect/Option";
10import * as Ref from "effect/Ref";
11
12import * as ConnectionResolver from "./resolver.ts";
13import * as ClientCapabilities from "../platform/capabilities.ts";
14import * as RemoteEnvironmentAuthorization from "../authorization/service.ts";
15import {
16 BearerConnectionCredential,
17 type ConnectionCatalogEntry,
18 SshConnectionProfile,
19 type ConnectionCredential,
20 type ConnectionProfile,
21} from "./catalog.ts";
22import * as ConnectionCredentialStore from "./credentialStore.ts";
23import {
24 ConnectionTransientError,
25 PrimaryConnectionTarget,
26 SshConnectionTarget,
27 type ConnectionTarget,
28} from "./model.ts";
29import * as ConnectionProfileStore from "./profileStore.ts";
30import * as RpcHttp from "../rpc/http.ts";
31import {
32 PullRequestRoutingPermissions,
33 pullRequestRoutingConnectionKey,
34 makePullRequestRoutingPermissions,
35} from "./pullRequestRoutingPermissions.ts";
36
37const ENVIRONMENT_ID = EnvironmentId.make("environment-1");
38const ENDPOINT = {
39 httpBaseUrl: "https://environment.example.test",
40 wsBaseUrl: "wss://environment.example.test",
41};
42const SSH_TARGET: DesktopSshEnvironmentTarget = {
43 alias: "development",
44 hostname: "development.example.test",
45 username: "developer",
46 port: 22,
47};
48
49function catalogEntry(
50 target: ConnectionTarget,
51 profile: Option.Option<ConnectionProfile> = Option.none(),
52): ConnectionCatalogEntry {
53 return { target, profile, enabled: true };
54}
55
56const makeDependencies = Effect.fn("TestConnectionResolver.makeDependencies")((options?: {
57 readonly profiles?: ReadonlyArray<ConnectionProfile>;
58 readonly profileStore?: ConnectionProfileStore.ConnectionProfileStore["Service"];
59 readonly credentials?: ReadonlyArray<readonly [string, ConnectionCredential]>;
60 readonly authorizeBearer?: RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization["Service"]["authorizeBearer"];
61 readonly authorizeDpop?: RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization["Service"]["authorizeDpop"];
62 readonly primaryBearerToken?: string;
63 readonly prepareSsh?: ClientCapabilities.SshEnvironmentGateway["Service"]["prepare"];
64 readonly descriptorProtocolVersion?: number | null | undefined;
65}) => {
66 const profiles = new Map(
67 (options?.profiles ?? []).map((profile) => [profile.connectionId, profile]),
68 );
69 const credentials = new Map(options?.credentials ?? []);
70
71 const profileStore = ConnectionProfileStore.ConnectionProfileStore.of({
72 get: (connectionId) => Effect.succeed(Option.fromNullishOr(profiles.get(connectionId))),
73 put: (profile) => Effect.sync(() => void profiles.set(profile.connectionId, profile)),
74 remove: (connectionId) => Effect.sync(() => void profiles.delete(connectionId)),
75 });
76 const credentialStore = ConnectionCredentialStore.ConnectionCredentialStore.of({
77 get: (connectionId) => Effect.succeed(Option.fromNullishOr(credentials.get(connectionId))),
78 put: (connectionId, credential) =>
79 Effect.sync(() => void credentials.set(connectionId, credential)),
80 remove: (connectionId) => Effect.sync(() => void credentials.delete(connectionId)),
81 });
82 const remote = RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization.of({
83 authorizeBearer:
84 options?.authorizeBearer ??
85 ((input) =>
86 Effect.succeed({
87 environmentId: input.expectedEnvironmentId,
88 label: "Authorized bearer environment",
89 httpBaseUrl: input.httpBaseUrl,
90 socketUrl: "wss://authorized.example.test/ws?wsTicket=bearer",
91 httpAuthorization: {
92 _tag: "Bearer" as const,
93 token: input.bearerToken,
94 },
95 })),
96 authorizeDpop:
97 options?.authorizeDpop ??
98 ((input) =>
99 Effect.succeed({
100 environmentId: input.expectedEnvironmentId,
101 label: "Authorized relay environment",
102 httpBaseUrl: ENDPOINT.httpBaseUrl,
103 socketUrl: "wss://authorized.example.test/ws?wsTicket=dpop",
104 httpAuthorization: {
105 _tag: "Dpop" as const,
106 accessToken: "dpop-access-token",
107 expiresAtEpochMs: Number.MAX_SAFE_INTEGER,
108 },
109 })),
110 authorizeDpopHttp: () => Effect.die("unused"),
111 rememberCheckedDescriptor: () => Effect.void,
112 renewDpopBeforeExpiry: () => Effect.die("unused"),
113 });
114 const ssh = ClientCapabilities.SshEnvironmentGateway.of({
115 provision: () => Effect.die("unused"),
116 prepare:
117 options?.prepareSsh ??
118 (() =>
119 Effect.succeed({
120 bootstrap: {
121 target: SSH_TARGET,
122 httpBaseUrl: "http://127.0.0.1:4010",
123 wsBaseUrl: "ws://127.0.0.1:4010",
124 pairingToken: null,
125 },
126 bearerToken: "ssh-bearer",
127 })),
128 disconnect: () => Effect.void,
129 });
130
131 const layerDependencies = Layer.mergeAll(
132 RpcHttp.layerRemoteHttpClient((() =>
133 Promise.resolve(
134 Response.json({
135 environmentId: ENVIRONMENT_ID,
136 label: "Compatible environment",
137 platform: { os: "linux", arch: "x64" },
138 serverVersion: "0.0.0-test",
139 ...(options?.descriptorProtocolVersion === undefined
140 ? { orchestrationProtocolVersion: ORCHESTRATION_PROTOCOL_VERSION }
141 : options.descriptorProtocolVersion === null
142 ? {}
143 : { orchestrationProtocolVersion: options.descriptorProtocolVersion }),
144 capabilities: { repositoryIdentity: true },
145 }),
146 )) satisfies typeof fetch),
147 Layer.succeed(
148 ConnectionProfileStore.ConnectionProfileStore,
149 options?.profileStore ?? profileStore,
150 ),
151 Layer.succeed(ConnectionCredentialStore.ConnectionCredentialStore, credentialStore),
152 Layer.succeed(
153 ClientCapabilities.PrimaryEnvironmentAuth,
154 ClientCapabilities.PrimaryEnvironmentAuth.of({
155 bearerToken: Effect.succeed(Option.fromNullishOr(options?.primaryBearerToken)),
156 }),
157 ),
158 Layer.succeed(
159 ClientCapabilities.ClientPresentation,
160 ClientCapabilities.ClientPresentation.of({
161 metadata: { label: "Test Client", deviceType: "desktop", surface: "web" },
162 }),
163 ),
164 Layer.succeed(RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization, remote),
165 Layer.succeed(ClientCapabilities.SshEnvironmentGateway, ssh),
166 );
167
168 return Effect.succeed(ConnectionResolver.layer.pipe(Layer.provide(layerDependencies)));
169});
170
171describe("ConnectionResolver", () => {
172 it.effect("blocks an old host during discovery before opening orchestration RPC", () =>
173 Effect.gen(function* () {
174 const layerBroker = yield* makeDependencies({ descriptorProtocolVersion: null });
175 const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(layerBroker));
176 const target = new PrimaryConnectionTarget({
177 environmentId: ENVIRONMENT_ID,
178 label: "Primary",
179 httpBaseUrl: "http://127.0.0.1:3777",
180 wsBaseUrl: "ws://127.0.0.1:3777",
181 });
182
183 const error = yield* Effect.flip(broker.prepare(catalogEntry(target)));
184
185 expect(error).toMatchObject({ reason: "unsupported" });
186 expect(error.message).toContain("Update T3 Code on Compatible environment");
187 }),
188 );
189
190 it.effect("prepares a primary environment without remote capabilities", () =>
191 Effect.gen(function* () {
192 const layerBroker = yield* makeDependencies();
193 const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(layerBroker));
194 const target = new PrimaryConnectionTarget({
195 environmentId: ENVIRONMENT_ID,
196 label: "Primary",
197 httpBaseUrl: "http://127.0.0.1:3777",
198 wsBaseUrl: "ws://127.0.0.1:3777",
199 });
200
201 expect(yield* broker.prepare(catalogEntry(target))).toEqual({
202 environmentId: ENVIRONMENT_ID,
203 label: "Primary",
204 httpBaseUrl: "http://127.0.0.1:3777",
205 socketUrl:
206 "ws://127.0.0.1:3777/ws?clientSurface=web&clientDeviceType=desktop&connectionMethod=direct&orchestrationProtocol=2&clientCapabilities=settingsSecretReferences",
207 httpAuthorization: null,
208 target,
209 // The shell reads the server's build and shell flags here before the socket opens.
210 descriptor: {
211 environmentId: ENVIRONMENT_ID,
212 label: "Compatible environment",
213 platform: { os: "linux", arch: "x64" },
214 serverVersion: "0.0.0-test",
215 orchestrationProtocolVersion: ORCHESTRATION_PROTOCOL_VERSION,
216 capabilities: { repositoryIdentity: true },
217 },
218 });
219 }),
220 );
221
222 it.effect("offers the saved SSH token on each connect and saves the one it gets back", () =>
223 Effect.gen(function* () {
224 const offeredTokens = yield* Ref.make<ReadonlyArray<string | undefined>>([]);
225 const target = new SshConnectionTarget({
226 environmentId: ENVIRONMENT_ID,
227 label: "SSH",
228 connectionId: "ssh-1",
229 });
230 const profile = new SshConnectionProfile({
231 connectionId: "ssh-1",
232 environmentId: ENVIRONMENT_ID,
233 label: "SSH",
234 target: SSH_TARGET,
235 });
236 const layerBroker = yield* makeDependencies({
237 credentials: [["ssh-1", new BearerConnectionCredential({ token: "revoked-token" })]],
238 // The host accepts only the token it paired last; anything else pairs again.
239 prepareSsh: (input) =>
240 Ref.update(offeredTokens, (tokens) => [...tokens, input.bearerToken]).pipe(
241 Effect.as({
242 bootstrap: {
243 target: input.target,
244 httpBaseUrl: "http://127.0.0.1:4010",
245 wsBaseUrl: "ws://127.0.0.1:4010",
246 pairingToken: null,
247 },
248 bearerToken: "paired-token",
249 }),
250 ),
251 });
252 const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(layerBroker));
253 const entry = catalogEntry(target, Option.some(profile));
254
255 const first = yield* broker.prepare(entry);
256 const second = yield* broker.prepare(entry);
257
258 expect(yield* Ref.get(offeredTokens)).toEqual(["revoked-token", "paired-token"]);
259 expect(first.httpAuthorization).toMatchObject({ token: "paired-token" });
260 expect(second.httpAuthorization).toMatchObject({ token: "paired-token" });
261 }),
262 );
263
264 it.effect.each(["unchanged", "changed", "revocation-failed"] as const)(
265 "handles %s SSH routing consent before saving or authorizing",
266 (scenario) =>
267 Effect.gen(function* () {
268 const calls: string[] = [];
269 const target = new SshConnectionTarget({
270 environmentId: ENVIRONMENT_ID,
271 label: "SSH",
272 connectionId: "ssh-1",
273 });
274 const profile = new SshConnectionProfile({
275 connectionId: target.connectionId,
276 environmentId: ENVIRONMENT_ID,
277 label: "SSH",
278 target: SSH_TARGET,
279 });
280 const entry = catalogEntry(target, Option.some(profile));
281 const preparedTarget =
282 scenario === "unchanged"
283 ? SSH_TARGET
284 : { ...SSH_TARGET, hostname: "replacement.example.test" };
285 const failure = new ConnectionTransientError({
286 reason: "remote-unavailable",
287 detail: "Could not persist routing consent.",
288 });
289 const permissions = yield* makePullRequestRoutingPermissions({
290 read: Effect.succeed([
291 {
292 environmentId: ENVIRONMENT_ID,
293 connectionKey: pullRequestRoutingConnectionKey(entry)!,
294 permission: "read-write",
295 },
296 ]),
297 write: () =>
298 Effect.sync(() => {
299 calls.push("revoke");
300 }).pipe(
301 Effect.andThen(scenario === "revocation-failed" ? Effect.fail(failure) : Effect.void),
302 ),
303 });
304 let savedProfile: ConnectionProfile = profile;
305 const layerBroker = yield* makeDependencies({
306 profileStore: {
307 get: () => Effect.sync(() => Option.some(savedProfile)),
308 put: (value) =>
309 Effect.sync(() => {
310 calls.push("profile");
311 savedProfile = value;
312 }),
313 remove: () => Effect.die("unused"),
314 },
315 prepareSsh: () =>
316 Effect.succeed({
317 bootstrap: {
318 target: preparedTarget,
319 httpBaseUrl: "http://127.0.0.1:4010",
320 wsBaseUrl: "ws://127.0.0.1:4010",
321 pairingToken: null,
322 },
323 bearerToken: "ssh-bearer",
324 }),
325 authorizeBearer: (input) =>
326 Effect.sync(() => {
327 calls.push("authorize");
328 return {
329 environmentId: input.expectedEnvironmentId,
330 label: "SSH",
331 httpBaseUrl: input.httpBaseUrl,
332 socketUrl: "ws://127.0.0.1:4010/ws?wsTicket=ssh",
333 httpAuthorization: { _tag: "Bearer" as const, token: input.bearerToken },
334 };
335 }),
336 });
337 const broker = yield* ConnectionResolver.ConnectionResolver.pipe(
338 Effect.provide(layerBroker),
339 );
340 const prepare = broker
341 .prepare(entry)
342 .pipe(Effect.provideService(PullRequestRoutingPermissions, permissions));
343 if (scenario === "revocation-failed") {
344 expect(yield* Effect.flip(prepare)).toBe(failure);
345 expect(savedProfile).toBe(profile);
346 expect(calls).toEqual(["revoke"]);
347 } else {
348 expect((yield* prepare).socketUrl).toContain("wsTicket=ssh");
349 expect(savedProfile).toMatchObject({ target: preparedTarget });
350 expect(calls).toEqual(
351 scenario === "unchanged"
352 ? ["profile", "authorize"]
353 : ["revoke", "profile", "authorize"],
354 );
355 }
356 expect(yield* permissions.get(entry)).toBe(scenario === "changed" ? "off" : "read-write");
357 }),
358 );
359});