1import {
2 AuthAdministrativeScopes,
3 AuthStandardClientScopes,
4 EnvironmentId,
5 type AuthEnvironmentScope,
6 ORCHESTRATION_PROTOCOL_VERSION,
7} from "@t3tools/contracts";
8import { describe, expect, it } from "@effect/vitest";
9import * as Effect from "effect/Effect";
10import * as Layer from "effect/Layer";
11import * as Option from "effect/Option";
12import * as Stream from "effect/Stream";
13import * as SubscriptionRef from "effect/SubscriptionRef";
14
15import * as RpcHttp from "../rpc/http.ts";
16import * as ClientCapabilities from "../platform/capabilities.ts";
17import * as Persistence from "../platform/persistence.ts";
18import { fetchRemoteSessionState } from "../authorization/remote.ts";
19import {
20 BearerConnectionCredential,
21 BearerConnectionProfile,
22 BearerConnectionRegistration,
23 PrimaryConnectionRegistration,
24} from "./catalog.ts";
25import * as Connectivity from "./connectivity.ts";
26import * as ConnectionCredentialStore from "./credentialStore.ts";
27import * as ConnectionDriver from "./driver.ts";
28import {
29 BearerConnectionTarget,
30 ConnectionBlockedError,
31 PrimaryConnectionTarget,
32} from "./model.ts";
33import * as Onboarding from "./onboarding.ts";
34import {
35 prepareBearerConnectionUpdate,
36 preparePairingRegistration,
37 prepareSshRegistration,
38} from "./onboarding.ts";
39import * as EnvironmentRegistry from "./registry.ts";
40import * as ConnectionProfileStore from "./profileStore.ts";
41import * as ConnectionWakeups from "./wakeups.ts";
42
43const layerClientPresentation = Layer.succeed(
44 ClientCapabilities.ClientPresentation,
45 ClientCapabilities.ClientPresentation.of({
46 metadata: {
47 label: "T3 Code Test",
48 deviceType: "desktop",
49 os: "Test OS",
50 },
51 }),
52);
53
54function layerPairingHttp(
55 calls: Array<{ readonly url: string; readonly init: RequestInit }>,
56 options?: {
57 readonly failDescriptor?: boolean;
58 readonly failExchange?: boolean;
59 readonly protocolVersion?: number;
60 readonly selfUpdate?: boolean;
61 readonly grantScopes?: ReadonlyArray<AuthEnvironmentScope>;
62 },
63) {
64 const grantScopes = options?.grantScopes ?? AuthStandardClientScopes;
65 let sessionScopes: ReadonlyArray<string> = [];
66 const fetchFn = ((input, init = {}) => {
67 const url = String(input);
68 calls.push({ url, init });
69
70 if (url.endsWith("/.well-known/t3/environment")) {
71 if (options?.failDescriptor === true) {
72 return Promise.resolve(
73 Response.json({ message: "descriptor unavailable" }, { status: 503 }),
74 );
75 }
76 return Promise.resolve(
77 Response.json({
78 environmentId: "environment-paired",
79 label: "Paired environment",
80 platform: {
81 os: "linux",
82 arch: "x64",
83 },
84 serverVersion: "0.0.0-test",
85 orchestrationProtocolVersion: options?.protocolVersion ?? ORCHESTRATION_PROTOCOL_VERSION,
86 capabilities: {
87 repositoryIdentity: true,
88 ...(options?.selfUpdate === true ? { serverSelfUpdate: "boot-service" } : {}),
89 },
90 }),
91 );
92 }
93
94 if (url.endsWith("/oauth/token")) {
95 if (options?.failExchange === true) {
96 return Promise.resolve(
97 Response.json(
98 {
99 _tag: "EnvironmentAuthInvalidError",
100 code: "auth_invalid",
101 reason: "invalid_credential",
102 traceId: "trace-pairing-test",
103 },
104 { status: 401 },
105 ),
106 );
107 }
108 const body =
109 init.body instanceof Uint8Array ? new TextDecoder().decode(init.body) : String(init.body);
110 const requestedScope = new URLSearchParams(body).get("scope");
111 sessionScopes = requestedScope === null ? grantScopes : requestedScope.split(" ");
112 if (!sessionScopes.every((scope) => grantScopes.some((granted) => granted === scope))) {
113 return Promise.resolve(
114 Response.json(
115 {
116 _tag: "EnvironmentRequestInvalidError",
117 code: "invalid_request",
118 reason: "scope_not_granted",
119 traceId: "pairing-scope-test",
120 },
121 { status: 400 },
122 ),
123 );
124 }
125 return Promise.resolve(
126 Response.json({
127 access_token: "bearer-token",
128 issued_token_type: "urn:ietf:params:oauth:token-type:access_token",
129 token_type: "Bearer",
130 expires_in: 3600,
131 scope: sessionScopes.join(" "),
132 }),
133 );
134 }
135
136 if (url.endsWith("/api/auth/session")) {
137 return Promise.resolve(
138 Response.json({
139 authenticated: true,
140 auth: {
141 policy: "remote-reachable",
142 bootstrapMethods: ["one-time-token"],
143 sessionMethods: ["bearer-access-token"],
144 sessionCookieName: "t3_session",
145 },
146 scopes: sessionScopes,
147 sessionMethod: "bearer-access-token",
148 }),
149 );
150 }
151
152 return Promise.reject(new Error(`Unexpected request: ${url}`));
153 }) satisfies typeof fetch;
154
155 return RpcHttp.layerRemoteHttpClient(fetchFn);
156}
157
158function layerPairingOnboarding(options?: Parameters<typeof layerPairingHttp>[1]) {
159 const dependencies = Layer.mergeAll(
160 layerClientPresentation,
161 layerPairingHttp([], options),
162 Layer.mock(Persistence.ConnectionTargetStore)({
163 list: Effect.succeed([]),
164 listDisabled: Effect.succeed([]),
165 }),
166 Layer.mock(Persistence.ConnectionRegistrationStore)({
167 register: () => Effect.void,
168 setEnabled: () => Effect.void,
169 }),
170 Layer.mock(Persistence.EnvironmentCacheStore)({}),
171 Layer.mock(ConnectionProfileStore.ConnectionProfileStore)({}),
172 Layer.mock(ConnectionCredentialStore.ConnectionCredentialStore)({}),
173 Layer.mock(ClientCapabilities.SshEnvironmentGateway)({}),
174 Layer.succeed(
175 Connectivity.Connectivity,
176 Connectivity.Connectivity.of({ status: Effect.succeed("online"), changes: Stream.never }),
177 ),
178 Layer.mock(ConnectionDriver.ConnectionDriver)({ connect: () => Effect.never }),
179 Layer.succeed(
180 ConnectionWakeups.ConnectionWakeups,
181 ConnectionWakeups.ConnectionWakeups.of({ changes: Stream.never }),
182 ),
183 );
184 return Onboarding.layer.pipe(
185 Layer.provideMerge(EnvironmentRegistry.layer.pipe(Layer.provideMerge(dependencies))),
186 );
187}
188
189const PAIRED_ENVIRONMENT_ID = EnvironmentId.make("environment-paired");
190const PAIRING_INPUT = { host: "remote.example.test", pairingCode: "pairing-token" };
191const PAIRED_PROFILE = new BearerConnectionProfile({
192 connectionId: "bearer:environment-paired:https://remote.example.test",
193 environmentId: PAIRED_ENVIRONMENT_ID,
194 label: "Paired environment",
195 httpBaseUrl: "https://remote.example.test/",
196 wsBaseUrl: "wss://remote.example.test/",
197});
198const UNSUPPORTED_ERROR = new ConnectionBlockedError({
199 reason: "unsupported",
200 detail: "The saved server used an incompatible orchestration protocol.",
201});
202
203const registerSwitchedOffEnvironment = Effect.gen(function* () {
204 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
205 const registration = new BearerConnectionRegistration({
206 target: new BearerConnectionTarget({
207 environmentId: PAIRED_ENVIRONMENT_ID,
208 connectionId: PAIRED_PROFILE.connectionId,
209 label: "Old label",
210 }),
211 // Keep the endpoint unchanged: registration alone must retain the stale reason.
212 profile: new BearerConnectionProfile({ ...PAIRED_PROFILE, label: "Old label" }),
213 credential: new BearerConnectionCredential({ token: "old-token" }),
214 });
215 yield* registry.register(registration);
216 yield* registry.setEnabled(PAIRED_ENVIRONMENT_ID, false);
217 yield* registry.setCompatibility(PAIRED_ENVIRONMENT_ID, UNSUPPORTED_ERROR);
218 const entries = yield* SubscriptionRef.get(registry.entries);
219 expect(entries.get(PAIRED_ENVIRONMENT_ID)).toEqual({
220 target: registration.target,
221 profile: Option.some(registration.profile),
222 enabled: false,
223 unsupportedReason: UNSUPPORTED_ERROR.message,
224 });
225 return entries;
226});
227
228describe("connection onboarding", () => {
229 it.effect("prepares a persisted bearer registration from pairing details", () =>
230 Effect.gen(function* () {
231 const calls: Array<{ readonly url: string; readonly init: RequestInit }> = [];
232 const registration = yield* preparePairingRegistration({
233 host: "remote.example.test",
234 pairingCode: "pairing-token",
235 }).pipe(Effect.provide(Layer.mergeAll(layerClientPresentation, layerPairingHttp(calls))));
236
237 expect(registration).toMatchObject({
238 _tag: "BearerConnectionRegistration",
239 target: {
240 environmentId: "environment-paired",
241 label: "Paired environment",
242 connectionId: "bearer:environment-paired:https://remote.example.test",
243 },
244 profile: {
245 environmentId: "environment-paired",
246 label: "Paired environment",
247 connectionId: "bearer:environment-paired:https://remote.example.test",
248 httpBaseUrl: "https://remote.example.test/",
249 wsBaseUrl: "wss://remote.example.test/",
250 },
251 credential: {
252 token: "bearer-token",
253 },
254 });
255 expect(calls.map((call) => call.url)).toEqual([
256 "https://remote.example.test/.well-known/t3/environment",
257 "https://remote.example.test/oauth/token",
258 ]);
259
260 const tokenRequest = calls.find((call) => call.url.endsWith("/oauth/token"));
261 const tokenBody =
262 tokenRequest?.init.body instanceof Uint8Array
263 ? new TextDecoder().decode(tokenRequest.init.body)
264 : String(tokenRequest?.init.body);
265 const tokenParams = new URLSearchParams(tokenBody);
266 expect(tokenParams.get("subject_token")).toBe("pairing-token");
267 expect(tokenParams.has("scope")).toBe(false);
268 expect(tokenParams.get("client_label")).toBe("T3 Code Test");
269 expect(tokenParams.get("client_device_type")).toBe("desktop");
270 expect(tokenParams.get("client_os")).toBe("Test OS");
271 }),
272 );
273
274 it.effect("keeps routes for different prefixes on the same hub distinct", () =>
275 Effect.gen(function* () {
276 const calls: Array<{ readonly url: string; readonly init: RequestInit }> = [];
277 const registrations = [];
278 for (const name of ["one", "two"]) {
279 registrations.push(
280 yield* preparePairingRegistration({
281 host: `https://hub.example/prefix/environments/${name}/`,
282 pairingCode: "pairing-token",
283 }).pipe(Effect.provide(Layer.mergeAll(layerClientPresentation, layerPairingHttp(calls)))),
284 );
285 }
286 expect(registrations.map((registration) => registration.target.connectionId)).toEqual([
287 "bearer:environment-paired:https://hub.example/prefix/environments/one",
288 "bearer:environment-paired:https://hub.example/prefix/environments/two",
289 ]);
290 expect(registrations[0]?.profile).toMatchObject({
291 httpBaseUrl: "https://hub.example/prefix/environments/one/",
292 wsBaseUrl: "wss://hub.example/prefix/environments/one/",
293 });
294 expect(calls.map((call) => call.url)).toEqual([
295 "https://hub.example/prefix/environments/one/.well-known/t3/environment",
296 "https://hub.example/prefix/environments/one/oauth/token",
297 "https://hub.example/prefix/environments/two/.well-known/t3/environment",
298 "https://hub.example/prefix/environments/two/oauth/token",
299 ]);
300 }),
301 );
302
303 it.effect("turns a switched-off environment back on when it is paired again", () =>
304 Effect.gen(function* () {
305 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
306 const onboarding = yield* Onboarding.ConnectionOnboarding;
307 yield* registerSwitchedOffEnvironment;
308
309 const environmentId = yield* onboarding.registerPairing(PAIRING_INPUT);
310
311 expect(environmentId).toBe(PAIRED_ENVIRONMENT_ID);
312 const entries = yield* SubscriptionRef.get(registry.entries);
313 const entry = entries.get(environmentId);
314 expect(entries.size).toBe(1);
315 expect(entry?.enabled).toBe(true);
316 expect(entry?.unsupportedReason).toBeUndefined();
317 expect(entry?.profile).toEqual(Option.some(PAIRED_PROFILE));
318 expect(entry?.target).toEqual(
319 new BearerConnectionTarget({
320 environmentId,
321 connectionId: PAIRED_PROFILE.connectionId,
322 label: PAIRED_PROFILE.label,
323 }),
324 );
325 }).pipe(Effect.provide(layerPairingOnboarding())),
326 );
327
328 it.effect(
329 "re-pairs an outdated self-updatable environment so its connection can be rechecked",
330 () =>
331 Effect.gen(function* () {
332 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
333 const onboarding = yield* Onboarding.ConnectionOnboarding;
334 yield* registerSwitchedOffEnvironment;
335 yield* registry.setCompatibility(
336 PAIRED_ENVIRONMENT_ID,
337 new ConnectionBlockedError({
338 reason: "unsupported",
339 detail: "The saved server needs an update.",
340 serverUpdateRequired: true,
341 }),
342 );
343 expect(
344 (yield* SubscriptionRef.get(registry.entries)).get(PAIRED_ENVIRONMENT_ID),
345 ).toMatchObject({
346 enabled: false,
347 serverUpdateRequired: true,
348 });
349
350 const environmentId = yield* onboarding.registerPairing(PAIRING_INPUT);
351
352 expect(environmentId).toBe(PAIRED_ENVIRONMENT_ID);
353 const entry = (yield* SubscriptionRef.get(registry.entries)).get(environmentId);
354 expect(entry?.enabled).toBe(true);
355 expect(entry?.unsupportedReason).toBeUndefined();
356 expect(entry?.serverUpdateRequired).toBeUndefined();
357 expect(entry?.profile).toEqual(Option.some(PAIRED_PROFILE));
358 }).pipe(
359 Effect.provide(
360 layerPairingOnboarding({
361 protocolVersion: ORCHESTRATION_PROTOCOL_VERSION - 1,
362 selfUpdate: true,
363 }),
364 ),
365 ),
366 );
367
368 it.effect("registers an enabled environment on first-time pairing", () =>
369 Effect.gen(function* () {
370 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
371 const onboarding = yield* Onboarding.ConnectionOnboarding;
372 expect((yield* SubscriptionRef.get(registry.entries)).size).toBe(0);
373
374 const environmentId = yield* onboarding.registerPairing(PAIRING_INPUT);
375
376 expect(environmentId).toBe(PAIRED_ENVIRONMENT_ID);
377 const entries = yield* SubscriptionRef.get(registry.entries);
378 const entry = entries.get(environmentId);
379 expect(entries.size).toBe(1);
380 expect(entry?.enabled).toBe(true);
381 expect(entry?.unsupportedReason).toBeUndefined();
382 expect(entry?.profile).toEqual(Option.some(PAIRED_PROFILE));
383 }).pipe(Effect.provide(layerPairingOnboarding())),
384 );
385
386 it.effect("leaves a platform-managed environment untouched when its id is paired", () =>
387 Effect.gen(function* () {
388 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
389 const onboarding = yield* Onboarding.ConnectionOnboarding;
390 yield* registry.registerPlatform(
391 new PrimaryConnectionRegistration({
392 target: new PrimaryConnectionTarget({
393 environmentId: PAIRED_ENVIRONMENT_ID,
394 label: "Host environment",
395 httpBaseUrl: "http://127.0.0.1:3773",
396 wsBaseUrl: "ws://127.0.0.1:3773",
397 }),
398 }),
399 );
400 yield* registry.setCompatibility(PAIRED_ENVIRONMENT_ID, UNSUPPORTED_ERROR);
401 const before = yield* SubscriptionRef.get(registry.entries);
402 expect(before.get(PAIRED_ENVIRONMENT_ID)?.enabled).toBe(false);
403
404 const environmentId = yield* onboarding.registerPairing(PAIRING_INPUT);
405
406 expect(environmentId).toBe(PAIRED_ENVIRONMENT_ID);
407 const after = yield* SubscriptionRef.get(registry.entries);
408 expect(after).toEqual(before);
409 expect(after.get(PAIRED_ENVIRONMENT_ID)?.unsupportedReason).toBe(UNSUPPORTED_ERROR.message);
410 }).pipe(Effect.provide(layerPairingOnboarding())),
411 );
412
413 it.effect("leaves a switched-off environment unchanged when the pairing exchange fails", () =>
414 Effect.gen(function* () {
415 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
416 const onboarding = yield* Onboarding.ConnectionOnboarding;
417 const before = yield* registerSwitchedOffEnvironment;
418
419 const error = yield* onboarding.registerPairing(PAIRING_INPUT).pipe(Effect.flip);
420
421 expect(error).toMatchObject({
422 _tag: "ConnectionBlockedError",
423 reason: "authentication",
424 traceId: "trace-pairing-test",
425 });
426 const after = yield* SubscriptionRef.get(registry.entries);
427 expect(after).toEqual(before);
428 expect(after.get(PAIRED_ENVIRONMENT_ID)?.enabled).toBe(false);
429 expect(after.get(PAIRED_ENVIRONMENT_ID)?.unsupportedReason).toBe(UNSUPPORTED_ERROR.message);
430 }).pipe(Effect.provide(layerPairingOnboarding({ failExchange: true }))),
431 );
432
433 it.effect("rejects an incompatible server without consuming the pairing credential", () =>
434 Effect.gen(function* () {
435 const calls: Array<{ readonly url: string; readonly init: RequestInit }> = [];
436 const error = yield* preparePairingRegistration({
437 host: "remote.example.test",
438 pairingCode: "pairing-token",
439 }).pipe(
440 Effect.provide(
441 Layer.mergeAll(
442 layerClientPresentation,
443 layerPairingHttp(calls, { protocolVersion: ORCHESTRATION_PROTOCOL_VERSION + 1 }),
444 ),
445 ),
446 Effect.flip,
447 );
448 expect(error).toMatchObject({ reason: "unsupported" });
449 expect(calls.map((call) => call.url)).toEqual([
450 "https://remote.example.test/.well-known/t3/environment",
451 ]);
452 }),
453 );
454 it.effect.each([
455 { label: "read-only", scopes: ["orchestration:read"] },
456 { label: "administrative", scopes: AuthAdministrativeScopes },
457 ] as const)("preserves the $label grant when pairing a remote environment", ({ scopes }) =>
458 Effect.gen(function* () {
459 const calls: Array<{ readonly url: string; readonly init: RequestInit }> = [];
460 const httpLayer = layerPairingHttp(calls, { grantScopes: scopes });
461 const registration = yield* preparePairingRegistration({
462 host: "remote.example.test",
463 pairingCode: "pairing-token",
464 }).pipe(Effect.provide(Layer.mergeAll(layerClientPresentation, httpLayer)));
465
466 if (registration.credential._tag !== "BearerConnectionCredential") {
467 throw new Error("Pairing must issue a bearer credential.");
468 }
469 const session = yield* fetchRemoteSessionState({
470 httpBaseUrl: registration.profile.httpBaseUrl,
471 bearerToken: registration.credential.token,
472 }).pipe(Effect.provide(httpLayer));
473
474 expect(session.authenticated).toBe(true);
475 expect(session.scopes).toEqual(scopes);
476 }),
477 );
478
479 it.effect("refuses to add a route that reaches a different machine, keeping the code", () =>
480 Effect.gen(function* () {
481 const calls: Array<{ readonly url: string; readonly init: RequestInit }> = [];
482 const error = yield* preparePairingRegistration({
483 host: "remote.example.test",
484 pairingCode: "pairing-token",
485 expectedEnvironmentId: EnvironmentId.make("some-other-machine"),
486 }).pipe(
487 Effect.provide(Layer.mergeAll(layerClientPresentation, layerPairingHttp(calls))),
488 Effect.flip,
489 );
490 expect(error).toMatchObject({ reason: "configuration" });
491 expect(error.message).toContain("different machine");
492 expect(calls.map((call) => call.url)).toEqual([
493 "https://remote.example.test/.well-known/t3/environment",
494 ]);
495 }),
496 );
497
498 it.effect("pairs an outdated server so it can be updated from this client", () =>
499 Effect.gen(function* () {
500 const calls: Array<{ readonly url: string; readonly init: RequestInit }> = [];
501 const registration = yield* preparePairingRegistration({
502 host: "remote.example.test",
503 pairingCode: "pairing-token",
504 }).pipe(
505 Effect.provide(
506 Layer.mergeAll(
507 layerClientPresentation,
508 layerPairingHttp(calls, {
509 protocolVersion: ORCHESTRATION_PROTOCOL_VERSION - 1,
510 selfUpdate: true,
511 }),
512 ),
513 ),
514 );
515 expect(registration.target.environmentId).toBe("environment-paired");
516 expect(calls.map((call) => call.url)).toContain("https://remote.example.test/oauth/token");
517 }),
518 );
519
520 it.effect("refuses an outdated server that cannot update itself", () =>
521 Effect.gen(function* () {
522 const calls: Array<{ readonly url: string; readonly init: RequestInit }> = [];
523 const error = yield* preparePairingRegistration({
524 host: "remote.example.test",
525 pairingCode: "pairing-token",
526 }).pipe(
527 Effect.provide(
528 Layer.mergeAll(
529 layerClientPresentation,
530 layerPairingHttp(calls, { protocolVersion: ORCHESTRATION_PROTOCOL_VERSION - 1 }),
531 ),
532 ),
533 Effect.flip,
534 );
535 expect(error).toMatchObject({ reason: "unsupported" });
536 expect(error).not.toHaveProperty("serverUpdateRequired");
537 expect(calls.map((call) => call.url)).toEqual([
538 "https://remote.example.test/.well-known/t3/environment",
539 ]);
540 }),
541 );
542
543 it.effect("does not consume a pairing credential when descriptor discovery fails", () =>
544 Effect.gen(function* () {
545 const calls: Array<{ readonly url: string; readonly init: RequestInit }> = [];
546
547 yield* preparePairingRegistration({
548 host: "remote.example.test",
549 pairingCode: "pairing-token",
550 }).pipe(
551 Effect.provide(
552 Layer.mergeAll(
553 layerClientPresentation,
554 layerPairingHttp(calls, { failDescriptor: true }),
555 ),
556 ),
557 Effect.flip,
558 );
559
560 expect(calls.map((call) => call.url)).toEqual([
561 "https://remote.example.test/.well-known/t3/environment",
562 ]);
563 }),
564 );
565
566 it.effect("rejects invalid pairing details before making a request", () =>
567 Effect.gen(function* () {
568 const calls: Array<{ readonly url: string; readonly init: RequestInit }> = [];
569 const error = yield* preparePairingRegistration({
570 host: "",
571 pairingCode: "",
572 }).pipe(
573 Effect.provide(Layer.mergeAll(layerClientPresentation, layerPairingHttp(calls))),
574 Effect.flip,
575 );
576
577 expect(error).toMatchObject({
578 _tag: "ConnectionBlockedError",
579 reason: "configuration",
580 message: "Enter a backend URL.",
581 });
582 expect(calls).toEqual([]);
583 }),
584 );
585
586 it.effect("updates bearer metadata while preserving the credential and identity", () =>
587 Effect.gen(function* () {
588 const environmentId = EnvironmentId.make("environment-paired");
589 const registration = yield* prepareBearerConnectionUpdate({
590 input: {
591 environmentId,
592 label: " Renamed environment ",
593 httpBaseUrl: "http://100.65.180.100:3773/path",
594 },
595 entry: Option.some({
596 target: new BearerConnectionTarget({
597 environmentId,
598 label: "Old label",
599 connectionId: "bearer:environment-paired",
600 }),
601 profile: Option.some(
602 new BearerConnectionProfile({
603 connectionId: "bearer:environment-paired",
604 environmentId,
605 label: "Old label",
606 httpBaseUrl: "http://old.example.test/",
607 wsBaseUrl: "ws://old.example.test/",
608 }),
609 ),
610 enabled: true,
611 }),
612 credential: Option.some(new BearerConnectionCredential({ token: "bearer-token" })),
613 });
614
615 expect(registration).toMatchObject({
616 target: {
617 environmentId,
618 label: "Renamed environment",
619 connectionId: "bearer:environment-paired",
620 },
621 profile: {
622 environmentId,
623 label: "Renamed environment",
624 httpBaseUrl: "http://100.65.180.100:3773/path/",
625 wsBaseUrl: "ws://100.65.180.100:3773/path/",
626 },
627 credential: { token: "bearer-token" },
628 });
629 }),
630 );
631
632 it.effect("prepares an SSH registration from the provisioned platform environment", () =>
633 Effect.gen(function* () {
634 const target = {
635 alias: "devbox",
636 hostname: "devbox.example.test",
637 username: "developer",
638 port: 22,
639 };
640 const registration = yield* prepareSshRegistration({
641 target,
642 }).pipe(
643 Effect.provideService(
644 ClientCapabilities.SshEnvironmentGateway,
645 ClientCapabilities.SshEnvironmentGateway.of({
646 provision: () =>
647 Effect.succeed({
648 environmentId: EnvironmentId.make("environment-ssh"),
649 label: "Remote development box",
650 bootstrap: {
651 target,
652 httpBaseUrl: "http://127.0.0.1:3201",
653 wsBaseUrl: "ws://127.0.0.1:3201",
654 pairingToken: "pairing-token",
655 },
656 bearerToken: "bearer-token",
657 }),
658 prepare: () => Effect.die("unused"),
659 disconnect: () => Effect.die("unused"),
660 }),
661 ),
662 );
663
664 expect(registration).toMatchObject({
665 _tag: "SshConnectionRegistration",
666 target: {
667 environmentId: "environment-ssh",
668 label: "Remote development box",
669 connectionId: 'ssh:environment-ssh:["devbox","devbox.example.test","developer",22]',
670 },
671 profile: {
672 environmentId: "environment-ssh",
673 label: "Remote development box",
674 connectionId: 'ssh:environment-ssh:["devbox","devbox.example.test","developer",22]',
675 target,
676 },
677 });
678 }),
679 );
680});