Skip to content
Source
Sign inSign up
Open History to see the last commit

CloudLink.lifecycle.test.ts

1,223 lines45.5 KB

1import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { describe, expect, it } from "@effect/vitest";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Layer from "effect/Layer";
7import * as Option from "effect/Option";
8import * as Path from "effect/Path";
9import * as PlatformError from "effect/PlatformError";
10import * as Schema from "effect/Schema";
11import * as Stream from "effect/Stream";
12import * as TestClock from "effect/testing/TestClock";
13import {
14 HttpClient,
15 HttpClientResponse,
16 HttpServer,
17 HttpServerRequest,
18 type HttpClientRequest,
19} from "effect/http";
20import { RELAY_LINK_PROOF_TYP, signRelayJwt, verifyRelayJwt } from "@t3tools/shared/relayJwt";
21import * as NetAddress from "effect/net/NetAddress";
22​
23import {
24 AuthSessionId,
25 type AuthEnvironmentScope,
26 EnvironmentAuthenticatedPrincipal,
27 EnvironmentId,
28} from "@t3tools/contracts";
29import * as EnvironmentKeys from "./environmentKeys.ts";
30import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts";
31import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
32import * as ServerConfigModule from "../config.ts";
33import { writeServiceState } from "../serviceLauncher.ts";
34import {
35 SERVICE_LAUNCHER_PROTOCOL,
36 SERVICE_STATE_FILE,
37 SERVICE_STOP_MARKER_FILE,
38 type ServiceUpdateRecord,
39} from "./serviceProtocol.ts";
40import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
41import * as AgentAwarenessRelay from "../relay/AgentAwarenessRelay.ts";
42import { CLOUD_CLI_DESIRED_LINK_SECRET } from "./CliState.ts";
43import * as CliTokenManager from "./CliTokenManager.ts";
44import {
45 RelayEnvironmentLinkProofPayload,
46 RelayManagedEndpointRecoveryRegistrationRequest,
47} from "@t3tools/contracts/relay";
48import {
49 CLOUD_ENDPOINT_CONFIRMED_ORIGIN,
50 CLOUD_ENDPOINT_RUNTIME_CONFIG,
51 CLOUD_LINK_REVOKED,
52 CLOUD_LINKED_USER_ID,
53 decodeRuntimeConfig,
54 RELAY_ENVIRONMENT_CREDENTIAL_SECRET,
55 RELAY_ISSUER_SECRET,
56 RELAY_URL_SECRET,
57} from "./config.ts";
58import * as CloudLink from "./CloudLink.ts";
59import * as ConnectReachability from "./ConnectReachability.ts";
60import { consumeCloudReplayGuards } from "./CloudLink.ts";
61import { pendingServiceUpdateExists } from "./updateHandoff.ts";
62import { managedTunnelStartupAction } from "./managedTunnelStartup.ts";
63import * as ManagedEndpointRuntime from "./ManagedEndpointRuntime.ts";
64import * as ManagedPolicy from "../policy/ManagedPolicy.ts";
65​
66const storeFailure = (tag: "AlreadyExists" | "PermissionDenied") =>
67 new ServerSecretStore.SecretStorePersistError({
68 resource: "cloud replay guard",
69 cause: PlatformError.systemError({
70 _tag: tag,
71 module: "FileSystem",
72 method: "open",
73 pathOrDescriptor: "cloud-replay-guard.bin",
74 }),
75 });
76​
77const unusedSecretStoreOperation = () => Effect.die("unused secret-store operation");
78​
79const idleAwarenessRelay = AgentAwarenessRelay.AgentAwarenessRelay.of({
80 publishThread: () => Effect.void,
81 drain: Effect.void,
82 requestCatchUp: () => Effect.void,
83 start: () => Effect.void,
84});
85​
86const idleHttpServer = HttpServer.HttpServer.of({
87 address: NetAddress.inetAddressFromIpStringUnsafe("127.0.0.1", 3773),
88 serve: (() => Effect.void) as HttpServer.HttpServer["Service"]["serve"],
89});
90​
91const decodeManagedTunnelRecoveryRegistration = Schema.decodeUnknownEffect(
92 Schema.fromJsonString(RelayManagedEndpointRecoveryRegistrationRequest),
93);
94​
95function makeSecretStore(
96 create: ServerSecretStore.ServerSecretStore["Service"]["create"],
97): ServerSecretStore.ServerSecretStore["Service"] {
98 return {
99 get: unusedSecretStoreOperation,
100 set: unusedSecretStoreOperation,
101 create,
102 getOrCreateRandom: unusedSecretStoreOperation,
103 remove: unusedSecretStoreOperation,
104 };
106​
107describe("consumeCloudReplayGuards", () => {
108 it.effect("reports already-created guards as replay conflicts", () =>
109 Effect.gen(function* () {
110 const consumed = yield* consumeCloudReplayGuards({
111 secrets: makeSecretStore(() => Effect.fail(storeFailure("AlreadyExists"))),
112 names: ["cloud-jti", "cloud-nonce"],
113 value: new Uint8Array(),
114 });
115​
116 expect(consumed).toBe(false);
117 }),
118 );
119});
120​
121describe("releaseManagedTunnelOnShutdown", () => {
122 const cliToken: CliTokenManager.PersistedToken = {
123 accessToken: "cli-access-token",
124 refreshToken: "cli-refresh-token",
125 expiresAtEpochMs: Number.MAX_SAFE_INTEGER,
126 };
127​
128 function makeMemorySecretStore(initial: Iterable<readonly [string, string]> = []) {
129 const values = new Map<string, Uint8Array>(
130 Array.from(initial, ([name, value]) => [name, new TextEncoder().encode(value)] as const),
131 );
132 const store: ServerSecretStore.ServerSecretStore["Service"] = {
133 get: (name) => Effect.sync(() => Option.fromNullishOr(values.get(name))),
134 set: (name, value) =>
135 Effect.sync(() => {
136 values.set(name, value);
137 }),
138 create: (name, value) =>
139 Effect.sync(() => {
140 values.set(name, value);
141 }),
142 getOrCreateRandom: unusedSecretStoreOperation,
143 remove: (name) =>
144 Effect.sync(() => {
145 values.delete(name);
146 }),
147 };
148 return { store, values };
149 }
150​
151 interface ReleaseHarness {
152 readonly store: ServerSecretStore.ServerSecretStore["Service"];
153 readonly applyConfigCalls: Array<unknown>;
154 readonly requests: Array<HttpClientRequest.HttpClientRequest>;
155 readonly onRequest?: (request: HttpClientRequest.HttpClientRequest) => Effect.Effect<void>;
156 readonly respond?: () => Response;
157 readonly address?: HttpServer.HttpServer["Service"]["address"];
158 readonly respondEffect?: Effect.Effect<Response>;
159 readonly getExisting?: CliTokenManager.CloudCliTokenManager["Service"]["getExisting"];
160 readonly latestReachability?: ConnectReachability.ConnectReachability["Service"]["latest"];
161 readonly connector?: ManagedEndpointRuntime.ConnectorFacts;
162 }
163​
164 const writeLauncherState = (update: ServiceUpdateRecord) =>
165 Effect.gen(function* () {
166 const path = yield* Path.Path;
167 const config = yield* ServerConfigModule.ServerConfig;
168 const statePath = path.join(config.baseDir, "runtime", SERVICE_STATE_FILE);
169 yield* Effect.promise(() =>
170 writeServiceState(statePath, {
171 protocol: SERVICE_LAUNCHER_PROTOCOL,
172 activeVersion: "0.0.30",
173 update,
174 }),
175 );
176 });
177​
178 const provideReleaseHarness =
179 (harness: ReleaseHarness) =>
180 <A, E, R>(effect: Effect.Effect<A, E, R>) =>
181 effect.pipe(
182 Effect.provide(
183 CloudLink.layer.pipe(
184 Layer.provide(EnvironmentKeys.layer),
185 Layer.provide(ManagedPolicy.layerTest()),
186 ),
187 ),
188 Effect.provideService(
189 ConnectReachability.ConnectReachability,
190 ConnectReachability.ConnectReachability.of({
191 check: Effect.die("unused"),
192 latest: harness.latestReachability ?? Effect.succeedNone,
193 }),
194 ),
195 Effect.provideService(HttpServer.HttpServer, {
196 ...idleHttpServer,
197 address: harness.address ?? idleHttpServer.address,
198 }),
199 Effect.provideService(ServerSecretStore.ServerSecretStore, harness.store),
200 Effect.provideService(AgentAwarenessRelay.AgentAwarenessRelay, idleAwarenessRelay),
201 Effect.provideService(
202 ServerEnvironment.ServerEnvironment,
203 ServerEnvironment.ServerEnvironment.of({
204 getEnvironmentId: Effect.succeed(EnvironmentId.make("env_123")),
205 getDescriptor: Effect.succeed({
206 environmentId: EnvironmentId.make("env_123"),
207 label: "Test environment",
208 platform: { os: "linux", arch: "x64" },
209 serverVersion: "0.0.0-test",
210 capabilities: { repositoryIdentity: true },
211 }),
212 }),
213 ),
214 Effect.provideService(
215 ManagedEndpointRuntime.CloudManagedEndpointRuntime,
216 ManagedEndpointRuntime.CloudManagedEndpointRuntime.of({
217 applyConfig: (config) =>
218 Effect.sync(() => {
219 harness.applyConfigCalls.push(config);
220 return config === null
221 ? ({
222 status: "disabled",
223 } satisfies ManagedEndpointRuntime.CloudManagedEndpointRuntimeStatus)
224 : ({
225 status: "running",
226 providerKind: "cloudflare_tunnel",
227 pid: 123,
228 } satisfies ManagedEndpointRuntime.CloudManagedEndpointRuntimeStatus);
229 }),
230 recoveryRequests: Stream.empty,
231 requestRecovery: () => Effect.void,
232 restartConnector: () => Effect.succeed(false),
233 tunnelConnected: Stream.empty,
234 inspectConnector: Effect.succeed(harness.connector ?? null),
235 readinessChanged: Stream.empty,
236 withLinkStateLock: (effect) => effect,
237 }),
238 ),
239 Effect.provideService(
240 EnvironmentAuth.EnvironmentAuth,
241 EnvironmentAuth.EnvironmentAuth.of({} as EnvironmentAuth.EnvironmentAuth["Service"]),
242 ),
243 Effect.provideService(
244 CliTokenManager.CloudCliTokenManager,
245 CliTokenManager.CloudCliTokenManager.of({
246 get: unusedSecretStoreOperation(),
247 getExisting: harness.getExisting ?? Effect.succeedSome(cliToken),
248 hasCredential: unusedSecretStoreOperation(),
249 store: () => unusedSecretStoreOperation(),
250 clear: unusedSecretStoreOperation(),
251 }),
252 ),
253 Effect.provideService(
254 HttpClient.HttpClient,
255 HttpClient.make((request) =>
256 Effect.sync(() => {
257 harness.requests.push(request);
258 }).pipe(
259 Effect.andThen(harness.onRequest?.(request) ?? Effect.void),
260 Effect.andThen(
261 harness.respondEffect ??
262 Effect.sync(() => (harness.respond ?? (() => Response.json({ ok: true })))()),
263 ),
264 Effect.map((response) => HttpClientResponse.fromWeb(request, response)),
265 ),
266 ),
267 ),
268​
269 Effect.provide(
270 ServerConfigModule.layerTest("/", { prefix: "t3-http-release-test-" }).pipe(
271 Layer.provideMerge(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer)),
272 ),
273 ),
274 Effect.scoped,
275 );
276​
277 it.effect.each([
278 { direction: "ahead", skewSeconds: 600 },
279 { direction: "behind", skewSeconds: -600 },
280 ])(
281 "signs a link proof accepted at relay time when the host clock is $direction",
282 ({ skewSeconds }) => {
283 const { store } = makeMemorySecretStore();
284​
285 return Effect.gen(function* () {
286 const relayNow = 1_800_000_000;
287 const relayIssuer = "https://relay.example.test";
288 const relayKeys = yield* EnvironmentKeys.getOrCreateEnvironmentKeyPairFromSecretStore(
289 makeMemorySecretStore().store,
290 );
291 const challenge = yield* signRelayJwt({
292 privateKey: relayKeys.privateKey,
293 typ: "t3-link-challenge+jwt",
294 payload: {
295 kind: "environment_link_challenge",
296 iss: relayIssuer,
297 aud: relayIssuer,
298 sub: "user-123",
299 jti: "challenge-jti",
300 iat: relayNow,
301 exp: relayNow + 300,
302 notificationsEnabled: true,
303 liveActivitiesEnabled: false,
304 managedTunnelsEnabled: false,
305 },
306 });
307 yield* TestClock.setTime((relayNow + skewSeconds) * 1_000);
308 const link = yield* CloudLink.CloudLink;
309 const proof = yield* link
310 .linkProof(
311 {
312 challenge,
313 relayIssuer,
314 endpoint: {
315 httpBaseUrl: "http://127.0.0.1:3773",
316 wsBaseUrl: "ws://127.0.0.1:3773",
317 providerKind: "manual",
318 },
319 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
320 },
321 HttpServerRequest.fromWeb(
322 new Request("http://127.0.0.1:3773/api/t3-connect/link-proof"),
323 ),
324 )
325 .pipe(
326 Effect.provideService(EnvironmentAuthenticatedPrincipal, {
327 sessionId: AuthSessionId.make("test-session"),
328 subject: "test",
329 method: "bearer-access-token",
330 scopes: new Set<AuthEnvironmentScope>(),
331 }),
332 );
333 const environmentKeys =
334 yield* EnvironmentKeys.getOrCreateEnvironmentKeyPairFromSecretStore(store);
335 yield* TestClock.setTime(relayNow * 1_000);
336 const verified = yield* verifyRelayJwt(RelayEnvironmentLinkProofPayload, {
337 publicKey: environmentKeys.publicKey,
338 token: proof,
339 typ: RELAY_LINK_PROOF_TYP,
340 issuer: "t3-env:env_123",
341 audience: relayIssuer,
342 });
343​
344 expect(verified).toMatchObject({
345 iat: relayNow,
346 exp: relayNow + 300,
347 challenge,
348 environmentId: "env_123",
349 scopes: ["agent_activity_notifications"],
350 });
351 }).pipe(provideReleaseHarness({ store, applyConfigCalls: [], requests: [] }));
352 },
353 );
354​
355 const managedLinkSecrets = [
356 [CLOUD_ENDPOINT_RUNTIME_CONFIG, "runtime-config"],
357 [CLOUD_ENDPOINT_CONFIRMED_ORIGIN, "confirmed-origin"],
358 [RELAY_URL_SECRET, "https://relay.example.test"],
359 [CLOUD_CLI_DESIRED_LINK_SECRET, "managed"],
360 ] as const;
361​
362 it.effect("does not recreate a link that was unlinked while startup registration retried", () => {
363 const { store, values } = makeMemorySecretStore(managedLinkSecrets);
364 const applyConfigCalls: Array<unknown> = [];
365 const requests: Array<HttpClientRequest.HttpClientRequest> = [];
366​
367 return Effect.gen(function* () {
368 const link = yield* CloudLink.CloudLink;
369​
370 values.delete(CLOUD_CLI_DESIRED_LINK_SECRET);
371​
372 expect(yield* link.reconcileDesiredLinkIfStillDesired("http://127.0.0.1:3773")).toBeNull();
373 expect(requests).toEqual([]);
374 expect(applyConfigCalls).toEqual([]);
375 }).pipe(provideReleaseHarness({ store, applyConfigCalls, requests }));
376 });
377​
378 it.effect("stops the connector, releases the relay tunnel, and drops the dead token", () => {
379 const { store, values } = makeMemorySecretStore(managedLinkSecrets);
380 const applyConfigCalls: Array<unknown> = [];
381 const requests: Array<HttpClientRequest.HttpClientRequest> = [];
382​
383 return Effect.gen(function* () {
384 const link = yield* CloudLink.CloudLink;
385 const released = yield* link.releaseManagedTunnelOnShutdown();
386​
387 expect(released).toBe(true);
388 expect(applyConfigCalls).toEqual([null]);
389 expect(requests).toHaveLength(1);
390 const request = requests[0]!;
391 expect(request.method).toBe("DELETE");
392 expect(request.url).toBe(
393 "https://relay.example.test/v1/client/environment-links/env_123/tunnel",
394 );
395 expect(request.headers.authorization).toBe("Bearer cli-access-token");
396 expect(values.has(CLOUD_ENDPOINT_RUNTIME_CONFIG)).toBe(false);
397 expect(values.has(CLOUD_ENDPOINT_CONFIRMED_ORIGIN)).toBe(false);
398 }).pipe(provideReleaseHarness({ store, applyConfigCalls, requests }));
399 });
400​
401 it.effect("leaves the tunnel of a web/mobile-installed link untouched", () => {
402 const { store, values } = makeMemorySecretStore([
403 [CLOUD_ENDPOINT_RUNTIME_CONFIG, "runtime-config"],
404 [RELAY_URL_SECRET, "https://relay.example.test"],
405 ]);
406 const applyConfigCalls: Array<unknown> = [];
407 const requests: Array<HttpClientRequest.HttpClientRequest> = [];
408​
409 return Effect.gen(function* () {
410 const link = yield* CloudLink.CloudLink;
411 const released = yield* link.releaseManagedTunnelOnShutdown();
412​
413 expect(released).toBe(false);
414 expect(applyConfigCalls).toEqual([]);
415 expect(requests).toEqual([]);
416 expect(values.has(CLOUD_ENDPOINT_RUNTIME_CONFIG)).toBe(true);
417 }).pipe(provideReleaseHarness({ store, applyConfigCalls, requests }));
418 });
419​
420 it.effect("keeps the tunnel when shutdown hands off to a pending update", () => {
421 const { store, values } = makeMemorySecretStore(managedLinkSecrets);
422 const applyConfigCalls: Array<unknown> = [];
423 const requests: Array<HttpClientRequest.HttpClientRequest> = [];
424​
425 return Effect.gen(function* () {
426 const link = yield* CloudLink.CloudLink;
427 yield* writeLauncherState({
428 id: "update-1",
429 fromVersion: "0.0.30",
430 targetVersion: "0.0.31",
431 dbPath: "/tmp/state.sqlite",
432 status: "pending",
433 });
434​
435 const released = yield* link.releaseManagedTunnelOnShutdown();
436​
437 expect(released).toBe(false);
438 expect(applyConfigCalls).toEqual([]);
439 expect(requests).toEqual([]);
440 expect(values.has(CLOUD_ENDPOINT_RUNTIME_CONFIG)).toBe(true);
441 }).pipe(provideReleaseHarness({ store, applyConfigCalls, requests }));
442 });
443​
444 it.effect("still releases a pending update when the launcher is stopping", () => {
445 const { store, values } = makeMemorySecretStore(managedLinkSecrets);
446 const applyConfigCalls: Array<unknown> = [];
447 const requests: Array<HttpClientRequest.HttpClientRequest> = [];
448​
449 return Effect.gen(function* () {
450 const link = yield* CloudLink.CloudLink;
451 yield* writeLauncherState({
452 id: "update-1",
453 fromVersion: "0.0.30",
454 targetVersion: "0.0.31",
455 dbPath: "/tmp/state.sqlite",
456 status: "pending",
457 });
458 const fs = yield* FileSystem.FileSystem;
459 const path = yield* Path.Path;
460 const config = yield* ServerConfigModule.ServerConfig;
461 yield* fs.writeFileString(path.join(config.baseDir, "runtime", SERVICE_STOP_MARKER_FILE), "");
462​
463 expect(yield* pendingServiceUpdateExists).toBe(true);
464 const released = yield* link.releaseManagedTunnelOnShutdown();
465​
466 expect(released).toBe(true);
467 expect(requests).toHaveLength(1);
468 expect(values.has(CLOUD_ENDPOINT_RUNTIME_CONFIG)).toBe(false);
469 }).pipe(provideReleaseHarness({ store, applyConfigCalls, requests }));
470 });
471​
472 // regression: 808d6853555 keep the managed tunnel across remote update restarts
473 it.effect("still releases when the recorded update already settled", () => {
474 const { store, values } = makeMemorySecretStore(managedLinkSecrets);
475 const applyConfigCalls: Array<unknown> = [];
476 const requests: Array<HttpClientRequest.HttpClientRequest> = [];
477​
478 return Effect.gen(function* () {
479 const link = yield* CloudLink.CloudLink;
480 yield* writeLauncherState({
481 id: "update-1",
482 fromVersion: "0.0.30",
483 targetVersion: "0.0.31",
484 status: "committed",
485 });
486​
487 const released = yield* link.releaseManagedTunnelOnShutdown();
488​
489 expect(released).toBe(true);
490 expect(requests).toHaveLength(1);
491 expect(values.has(CLOUD_ENDPOINT_RUNTIME_CONFIG)).toBe(false);
492 }).pipe(provideReleaseHarness({ store, applyConfigCalls, requests }));
493 });
494​
495 it.effect("keeps a runtime config that a fast restart replaced mid-release", () => {
496 const { store, values } = makeMemorySecretStore(managedLinkSecrets);
497 const applyConfigCalls: Array<unknown> = [];
498 const requests: Array<HttpClientRequest.HttpClientRequest> = [];
499 const freshConfig = new TextEncoder().encode("fresh-runtime-config");
500​
501 return Effect.gen(function* () {
502 const link = yield* CloudLink.CloudLink;
503 const released = yield* link.releaseManagedTunnelOnShutdown();
504​
505 expect(released).toBe(true);
506​
507 expect(values.get(CLOUD_ENDPOINT_RUNTIME_CONFIG)).toBe(freshConfig);
508 }).pipe(
509 provideReleaseHarness({
510 store,
511 applyConfigCalls,
512 requests,
513 respond: () => {
514 values.set(CLOUD_ENDPOINT_RUNTIME_CONFIG, freshConfig);
515 return Response.json({ ok: true });
516 },
517 }),
518 );
519 });
520​
521 it.effect("keeps the stored connector token when the relay skipped the release", () => {
522 const { store, values } = makeMemorySecretStore(managedLinkSecrets);
523 const applyConfigCalls: Array<unknown> = [];
524 const requests: Array<HttpClientRequest.HttpClientRequest> = [];
525​
526 return Effect.gen(function* () {
527 const link = yield* CloudLink.CloudLink;
528 const released = yield* link.releaseManagedTunnelOnShutdown();
529​
530 expect(released).toBe(false);
531 expect(requests).toHaveLength(1);
532 expect(values.has(CLOUD_ENDPOINT_RUNTIME_CONFIG)).toBe(true);
533 }).pipe(
534 provideReleaseHarness({
535 store,
536 applyConfigCalls,
537 requests,
538 respond: () => Response.json({ ok: false }),
539 }),
540 );
541 });
542​
543 it.effect("registers an existing tunnel and starts the confirmed connector", () => {
544 const { store } = makeMemorySecretStore([
545 [
546 CLOUD_ENDPOINT_RUNTIME_CONFIG,
547 '{"providerKind":"cloudflare_tunnel","connectorToken":"existing-token","tunnelId":"existing-tunnel"}',
548 ],
549 [RELAY_URL_SECRET, "https://relay.example.test"],
550 [CLOUD_LINKED_USER_ID, "user-123"],
551 [RELAY_ENVIRONMENT_CREDENTIAL_SECRET, "environment-credential"],
552 ]);
553 const applyConfigCalls: Array<unknown> = [];
554 const requests: Array<HttpClientRequest.HttpClientRequest> = [];
555​
556 return Effect.gen(function* () {
557 const link = yield* CloudLink.CloudLink;
558 expect(yield* link.registerManagedTunnelRecovery("http://127.0.0.1:3773")).toMatchObject({
559 status: "ready",
560 });
561 expect(requests).toHaveLength(1);
562 expect(requests[0]?.method).toBe("POST");
563 expect(requests[0]?.url).toBe(
564 "https://relay.example.test/v1/environments/env_123/tunnel/recovery",
565 );
566 expect(requests[0]?.headers.authorization).toBe("Bearer environment-credential");
567 const body = requests[0]?.body;
568 expect(body?._tag).toBe("Uint8Array");
569 if (body?._tag === "Uint8Array") {
570 expect(
571 yield* decodeManagedTunnelRecoveryRegistration(new TextDecoder().decode(body.body)),
572 ).toMatchObject({
573 cloudUserId: "user-123",
574 tunnelId: "existing-tunnel",
575 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
576 });
577 }
578 expect(applyConfigCalls).toHaveLength(1);
579 }).pipe(
580 provideReleaseHarness({
581 store,
582 applyConfigCalls,
583 requests,
584 respond: () => Response.json({ status: "ready" }),
585 }),
586 );
587 });
588​
589 it.effect("registers and starts a managed tunnel at the bound IPv6 loopback address", () => {
590 const { store } = makeMemorySecretStore();
591 const config = {
592 providerKind: "cloudflare_tunnel" as const,
593 connectorToken: "existing-token",
594 tunnelId: "existing-tunnel",
595 };
596 const applyConfigCalls: Array<unknown> = [];
597 const requests: Array<HttpClientRequest.HttpClientRequest> = [];
598​
599 return Effect.gen(function* () {
600 const link = yield* CloudLink.CloudLink;
601 expect(
602 yield* link.applyRelayConfig({
603 relayUrl: "https://relay.example.test",
604 cloudUserId: "user-123",
605 environmentCredential: "environment-credential",
606 cloudMintPublicKey:
607 "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAqZ2u2LjMoAyG2QTHXmITJ9w6RzZtVUAv394XCcPV0zE=\n-----END PUBLIC KEY-----",
608 endpointRuntime: config,
609 }),
610 ).toMatchObject({ ok: true, endpointRuntimeStatus: { status: "running" } });
611 expect(requests).toHaveLength(1);
612 const body = requests[0]!.body;
613 if (body._tag !== "Uint8Array") throw new Error("Expected a JSON recovery request");
614 expect(
615 yield* decodeManagedTunnelRecoveryRegistration(new TextDecoder().decode(body.body)),
616 ).toMatchObject({
617 origin: { localHttpHost: "[::1]", localHttpPort: 4884 },
618 });
619 expect(applyConfigCalls).toEqual([null, config]);
620 }).pipe(
621 provideReleaseHarness({
622 store,
623 applyConfigCalls,
624 requests,
625 address: NetAddress.inetAddressFromIpStringUnsafe("::1", 4884),
626 respond: () => Response.json({ status: "ready" }),
627 }),
628 );
629 });
630​
631 it.effect("marks a link the relay rejects as revoked without dropping or replacing it", () => {
632 const { store, values } = makeMemorySecretStore([
633 [
634 CLOUD_ENDPOINT_RUNTIME_CONFIG,
635 '{"providerKind":"cloudflare_tunnel","connectorToken":"existing-token","tunnelId":"existing-tunnel"}',
636 ],
637 [RELAY_URL_SECRET, "https://relay.example.test"],
638 [CLOUD_LINKED_USER_ID, "user-123"],
639 [RELAY_ENVIRONMENT_CREDENTIAL_SECRET, "environment-credential"],
640 [CLOUD_CLI_DESIRED_LINK_SECRET, "managed"],
641 ]);
642 const applyConfigCalls: Array<unknown> = [];
643 const requests: Array<HttpClientRequest.HttpClientRequest> = [];
644​
645 return Effect.gen(function* () {
646 const link = yield* CloudLink.CloudLink;
647 expect(yield* link.registerManagedTunnelRecovery("http://127.0.0.1:3773")).toEqual({
648 status: "revoked",
649 });
650 expect((yield* link.linkState()).linkRevoked).toBe(true);
651​
652 expect(requests).toHaveLength(1);
653 expect(applyConfigCalls).toEqual([]);
654 expect(values.has(RELAY_ENVIRONMENT_CREDENTIAL_SECRET)).toBe(true);
655 expect(values.has(CLOUD_ENDPOINT_RUNTIME_CONFIG)).toBe(true);
656 }).pipe(
657 provideReleaseHarness({
658 store,
659 applyConfigCalls,
660 requests,
661 respond: () =>
662 Response.json(
663 {
664 _tag: "RelayAuthInvalidError",
665 code: "auth_invalid",
666 reason: "not_authorized",
667 traceId: "trace-revoked",
668 },
669 { status: 401 },
670 ),
671 }),
672 );
673 });
674​
675 it.effect("does not mark a link that was replaced while its rejection was in flight", () => {
676 const { store, values } = makeMemorySecretStore([
677 [
678 CLOUD_ENDPOINT_RUNTIME_CONFIG,
679 '{"providerKind":"cloudflare_tunnel","connectorToken":"existing-token","tunnelId":"existing-tunnel"}',
680 ],
681 [RELAY_URL_SECRET, "https://relay.example.test"],
682 [CLOUD_LINKED_USER_ID, "user-123"],
683 [RELAY_ENVIRONMENT_CREDENTIAL_SECRET, "old-credential"],
684 ]);
685​
686 return Effect.gen(function* () {
687 const link = yield* CloudLink.CloudLink;
688 expect(yield* link.registerManagedTunnelRecovery("http://127.0.0.1:3773")).toEqual({
689 status: "superseded",
690 });
691 expect(values.has(CLOUD_LINK_REVOKED)).toBe(false);
692 }).pipe(
693 provideReleaseHarness({
694 store,
695 applyConfigCalls: [],
696 requests: [],
697 respond: () => {
698 values.set(
699 RELAY_ENVIRONMENT_CREDENTIAL_SECRET,
700 new TextEncoder().encode("replacement-credential"),
701 );
702 return Response.json(
703 {
704 _tag: "RelayAuthInvalidError",
705 code: "auth_invalid",
706 reason: "not_authorized",
707 traceId: "trace-stale",
708 },
709 { status: 401 },
710 );
711 },
712 }),
713 );
714 });
715​
716 it.effect("clears the revoked mark when the environment is linked again", () => {
717 const { store } = makeMemorySecretStore([
718 [RELAY_URL_SECRET, "https://relay.example.test"],
719 [CLOUD_LINKED_USER_ID, "user-123"],
720 [RELAY_ENVIRONMENT_CREDENTIAL_SECRET, "revoked-credential"],
721 [CLOUD_LINK_REVOKED, "true"],
722 ]);
723 const cloudMintPublicKey =
724 "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAqZ2u2LjMoAyG2QTHXmITJ9w6RzZtVUAv394XCcPV0zE=\n-----END PUBLIC KEY-----";
725​
726 return Effect.gen(function* () {
727 const link = yield* CloudLink.CloudLink;
728 expect((yield* link.linkState()).linkRevoked).toBe(true);
729 yield* link.applyRelayConfig({
730 relayUrl: "https://relay.example.test",
731 cloudUserId: "user-123",
732 environmentCredential: "fresh-credential",
733 cloudMintPublicKey,
734 endpointRuntime: null,
735 });
736 expect(yield* link.linkState()).toMatchObject({ linked: true, linkRevoked: false });
737 }).pipe(provideReleaseHarness({ store, applyConfigCalls: [], requests: [] }));
738 });
739​
740 it.effect(
741 "starts a connector with a marker for the current origin without contacting relay",
742 () => {
743 const configJson =
744 '{"providerKind":"cloudflare_tunnel","connectorToken":"existing-token","tunnelId":"existing-tunnel"}';
745 const config = {
746 providerKind: "cloudflare_tunnel" as const,
747 connectorToken: "existing-token",
748 tunnelId: "existing-tunnel",
749 };
750 const { store } = makeMemorySecretStore([
751 [CLOUD_ENDPOINT_RUNTIME_CONFIG, configJson],
752 [
753 CLOUD_ENDPOINT_CONFIRMED_ORIGIN,
754 `{"config":${configJson},"origin":{"localHttpHost":"127.0.0.1","localHttpPort":3773}}`,
755 ],
756 ]);
757 const applyConfigCalls: Array<unknown> = [];
758 const requests: Array<HttpClientRequest.HttpClientRequest> = [];
759​
760 return Effect.gen(function* () {
761 const link = yield* CloudLink.CloudLink;
762 expect(yield* link.startManagedTunnelIfOriginConfirmed("http://127.0.0.1:3773")).toBe(true);
763 expect(applyConfigCalls).toEqual([config]);
764 expect(requests).toEqual([]);
765 }).pipe(provideReleaseHarness({ store, applyConfigCalls, requests }));
766 },
767 );
768​
769 it.effect.each([
770 { name: "missing", marker: undefined, origin: "http://127.0.0.1:3773" },
771 {
772 name: "stale",
773 marker:
774 '{"config":{"providerKind":"cloudflare_tunnel","connectorToken":"existing-token","tunnelId":"existing-tunnel"},"origin":{"localHttpHost":"127.0.0.1","localHttpPort":3773}}',
775 origin: "http://127.0.0.1:4884",
776 },
777 ])("does not start a connector with a $name origin marker", ({ marker, origin }) => {
778 const entries: Array<readonly [string, string]> = [
779 [
780 CLOUD_ENDPOINT_RUNTIME_CONFIG,
781 '{"providerKind":"cloudflare_tunnel","connectorToken":"existing-token","tunnelId":"existing-tunnel"}',
782 ],
783 ];
784 if (marker !== undefined) entries.push([CLOUD_ENDPOINT_CONFIRMED_ORIGIN, marker]);
785 const { store } = makeMemorySecretStore(entries);
786 const applyConfigCalls: Array<unknown> = [];
787 const requests: Array<HttpClientRequest.HttpClientRequest> = [];
788​
789 return Effect.gen(function* () {
790 const link = yield* CloudLink.CloudLink;
791 expect(yield* link.startManagedTunnelIfOriginConfirmed(origin)).toBe(false);
792 expect(applyConfigCalls).toEqual([]);
793 expect(requests).toEqual([]);
794 }).pipe(provideReleaseHarness({ store, applyConfigCalls, requests }));
795 });
796​
797 // regression: #9386 remove tunnels after hosts go offline
798 it.effect(
799 "starts the stored connector without a marker when confirmation is not required",
800 () => {
801 const config = {
802 providerKind: "cloudflare_tunnel" as const,
803 connectorToken: "existing-token",
804 tunnelId: "existing-tunnel",
805 };
806 const { store } = makeMemorySecretStore([
807 [CLOUD_ENDPOINT_RUNTIME_CONFIG, JSON.stringify(config)],
808 ]);
809 const applyConfigCalls: Array<unknown> = [];
810 const requests: Array<HttpClientRequest.HttpClientRequest> = [];
811​
812 return Effect.gen(function* () {
813 const link = yield* CloudLink.CloudLink;
814 expect(
815 yield* link.startManagedTunnelIfOriginConfirmed("http://127.0.0.1:3773", {
816 requireConfirmedOrigin: false,
817 }),
818 ).toBe(true);
819 expect(applyConfigCalls).toEqual([config]);
820 expect(requests).toEqual([]);
821 }).pipe(provideReleaseHarness({ store, applyConfigCalls, requests }));
822 },
823 );
824​
825 it.effect.each(["replaced", "removed"] as const)(
826 "does not activate a tunnel when its runtime config is %s during registration",
827 (mutation) => {
828 const originalConfig =
829 '{"providerKind":"cloudflare_tunnel","connectorToken":"existing-token","tunnelId":"existing-tunnel"}';
830 const { store, values } = makeMemorySecretStore([
831 [CLOUD_ENDPOINT_RUNTIME_CONFIG, originalConfig],
832 [RELAY_URL_SECRET, "https://relay.example.test"],
833 [CLOUD_LINKED_USER_ID, "user-123"],
834 [RELAY_ENVIRONMENT_CREDENTIAL_SECRET, "environment-credential"],
835 ]);
836 const applyConfigCalls: Array<unknown> = [];
837 const requests: Array<HttpClientRequest.HttpClientRequest> = [];
838​
839 return Effect.gen(function* () {
840 const link = yield* CloudLink.CloudLink;
841 expect(yield* link.registerManagedTunnelRecovery("http://127.0.0.1:3773")).toEqual({
842 status: "superseded",
843 });
844 expect(applyConfigCalls).toEqual([]);
845 expect(values.has(CLOUD_ENDPOINT_CONFIRMED_ORIGIN)).toBe(false);
846 }).pipe(
847 provideReleaseHarness({
848 store,
849 applyConfigCalls,
850 requests,
851 respond: () => {
852 if (mutation === "replaced") {
853 values.set(
854 CLOUD_ENDPOINT_RUNTIME_CONFIG,
855 new TextEncoder().encode(
856 '{"providerKind":"cloudflare_tunnel","connectorToken":"fresh-token","tunnelId":"fresh-tunnel"}',
857 ),
858 );
859 } else {
860 values.delete(CLOUD_ENDPOINT_RUNTIME_CONFIG);
861 }
862 return Response.json({ status: "ready" });
863 },
864 }),
865 );
866 },
867 );
868​
869 it.effect("requests startup recovery for a legacy config without a recorded tunnel ID", () => {
870 const { store } = makeMemorySecretStore([
871 [
872 CLOUD_ENDPOINT_RUNTIME_CONFIG,
873 '{"providerKind":"cloudflare_tunnel","connectorToken":"token"}',
874 ],
875 [RELAY_URL_SECRET, "https://relay.example.test"],
876 [CLOUD_LINKED_USER_ID, "user-123"],
877 [RELAY_ENVIRONMENT_CREDENTIAL_SECRET, "environment-credential"],
878 ]);
879 const applyConfigCalls: Array<unknown> = [];
880 const requests: Array<HttpClientRequest.HttpClientRequest> = [];
881​
882 return Effect.gen(function* () {
883 const link = yield* CloudLink.CloudLink;
884 const registration = yield* link.registerManagedTunnelRecovery("http://127.0.0.1:3773");
885 expect(registration).toEqual({
886 status: "recovery_required",
887 config: { providerKind: "cloudflare_tunnel", connectorToken: "token" },
888 });
889 expect(
890 managedTunnelStartupAction({
891 wantsCliLink: false,
892 registration,
893 }),
894 ).toEqual({
895 action: "request_recovery",
896 config: { providerKind: "cloudflare_tunnel", connectorToken: "token" },
897 });
898 expect(requests).toEqual([]);
899 expect(applyConfigCalls).toEqual([]);
900 }).pipe(provideReleaseHarness({ store, applyConfigCalls, requests }));
901 });
902​
903 it.effect("recovers a web-linked tunnel and clears its revoked mark", () => {
904 const oldConfig =
905 '{"providerKind":"cloudflare_tunnel","connectorToken":"old-token","tunnelId":"old-tunnel"}';
906 const nextConfig = {
907 providerKind: "cloudflare_tunnel",
908 connectorToken: "new-token",
909 tunnelId: "new-tunnel",
910 } as const;
911 const { store, values } = makeMemorySecretStore([
912 [CLOUD_ENDPOINT_RUNTIME_CONFIG, oldConfig],
913 [RELAY_URL_SECRET, "https://relay.example.test"],
914 [CLOUD_LINKED_USER_ID, "user-123"],
915 [RELAY_ENVIRONMENT_CREDENTIAL_SECRET, "environment-credential"],
916 [CLOUD_LINK_REVOKED, "true"],
917 ]);
918 const applyConfigCalls: Array<unknown> = [];
919 const requests: Array<HttpClientRequest.HttpClientRequest> = [];
920​
921 return Effect.gen(function* () {
922 const link = yield* CloudLink.CloudLink;
923 expect(yield* link.recoverManagedTunnel("http://127.0.0.1:3773")).toBe(true);
924 expect(requests).toHaveLength(1);
925 expect(requests[0]?.method).toBe("POST");
926 expect(requests[0]?.url).toBe("https://relay.example.test/v1/environments/env_123/tunnel");
927 expect(requests[0]?.headers.authorization).toBe("Bearer environment-credential");
928 expect(applyConfigCalls).toEqual([nextConfig]);
929 expect(
930 Option.getOrNull(
931 decodeRuntimeConfig(new TextDecoder().decode(values.get(CLOUD_ENDPOINT_RUNTIME_CONFIG))),
932 ),
933 ).toEqual(nextConfig);
934 expect(values.has(CLOUD_LINK_REVOKED)).toBe(false);
935 }).pipe(
936 provideReleaseHarness({
937 store,
938 applyConfigCalls,
939 requests,
940 respond: () =>
941 Response.json({
942 endpoint: {
943 httpBaseUrl: "https://environment.example.test/",
944 wsBaseUrl: "wss://environment.example.test/ws",
945 providerKind: "cloudflare_tunnel",
946 },
947 endpointRuntime: nextConfig,
948 }),
949 }),
950 );
951 });
952​
953 it.effect("does not recover an environment without a managed tunnel credential", () => {
954 const { store } = makeMemorySecretStore([
955 [CLOUD_ENDPOINT_RUNTIME_CONFIG, "old-config"],
956 [RELAY_URL_SECRET, "https://relay.example.test"],
957 ]);
958 const applyConfigCalls: Array<unknown> = [];
959 const requests: Array<HttpClientRequest.HttpClientRequest> = [];
960​
961 return Effect.gen(function* () {
962 const link = yield* CloudLink.CloudLink;
963 expect(yield* link.recoverManagedTunnel("http://127.0.0.1:3773")).toBe(false);
964 expect(applyConfigCalls).toEqual([]);
965 expect(requests).toEqual([]);
966 }).pipe(provideReleaseHarness({ store, applyConfigCalls, requests }));
967 });
968​
969 it.effect("ignores recovery requests for a tunnel that has already been replaced", () => {
970 const { store } = makeMemorySecretStore([
971 [
972 CLOUD_ENDPOINT_RUNTIME_CONFIG,
973 '{"providerKind":"cloudflare_tunnel","connectorToken":"current-token","tunnelId":"current-tunnel"}',
974 ],
975 [RELAY_URL_SECRET, "https://relay.example.test"],
976 [CLOUD_LINKED_USER_ID, "user-123"],
977 [RELAY_ENVIRONMENT_CREDENTIAL_SECRET, "environment-credential"],
978 ]);
979 const applyConfigCalls: Array<unknown> = [];
980 const requests: Array<HttpClientRequest.HttpClientRequest> = [];
981​
982 return Effect.gen(function* () {
983 const link = yield* CloudLink.CloudLink;
984 expect(
985 yield* link.recoverManagedTunnel("http://127.0.0.1:3773", {
986 providerKind: "cloudflare_tunnel",
987 connectorToken: "old-token",
988 tunnelId: "old-tunnel",
989 }),
990 ).toBe(false);
991 expect(requests).toEqual([]);
992 expect(applyConfigCalls).toEqual([]);
993 }).pipe(provideReleaseHarness({ store, applyConfigCalls, requests }));
994 });
995​
996 it.effect.each([
997 {
998 status: 401,
999 body: {
1000 _tag: "RelayAuthInvalidError",
1001 code: "auth_invalid",
1002 reason: "invalid_bearer",
1003 traceId: "trace-revoked",
1004 },
1005 rejection: "unauthorized",
1006 },
1007 { status: 403, body: {}, rejection: "forbidden" },
1008 { status: 409, body: {}, rejection: "rejected" },
1009 ])("preserves a permanent $status relay recovery failure", ({ status, body, rejection }) => {
1010 const { store } = makeMemorySecretStore([
1011 [CLOUD_ENDPOINT_RUNTIME_CONFIG, "old-config"],
1012 [RELAY_URL_SECRET, "https://relay.example.test"],
1013 [CLOUD_LINKED_USER_ID, "user-123"],
1014 [RELAY_ENVIRONMENT_CREDENTIAL_SECRET, "environment-credential"],
1015 ]);
1016 const applyConfigCalls: Array<unknown> = [];
1017 const requests: Array<HttpClientRequest.HttpClientRequest> = [];
1018​
1019 return Effect.gen(function* () {
1020 const link = yield* CloudLink.CloudLink;
1021 const error = yield* Effect.flip(link.recoverManagedTunnel("http://127.0.0.1:3773"));
1022​
1023 expect(error).toMatchObject({ _tag: "RelayRequestError", rejection });
1024 expect(CloudLink.shouldRetryCloudLink(error)).toBe(false);
1025 expect(requests).toHaveLength(1);
1026 expect(applyConfigCalls).toEqual([]);
1027 }).pipe(
1028 provideReleaseHarness({
1029 store,
1030 applyConfigCalls,
1031 requests,
1032 respond: () => Response.json(body, { status }),
1033 }),
1034 );
1035 });
1036​
1037 it.effect("retries a relay link proof rejected as expired", () => {
1038 const { store } = makeMemorySecretStore([
1039 [CLOUD_ENDPOINT_RUNTIME_CONFIG, "old-config"],
1040 [RELAY_URL_SECRET, "https://relay.example.test"],
1041 [CLOUD_LINKED_USER_ID, "user-123"],
1042 [RELAY_ENVIRONMENT_CREDENTIAL_SECRET, "environment-credential"],
1043 ]);
1044 const applyConfigCalls: Array<unknown> = [];
1045 const requests: Array<HttpClientRequest.HttpClientRequest> = [];
1046​
1047 return Effect.gen(function* () {
1048 const link = yield* CloudLink.CloudLink;
1049 const error = yield* Effect.flip(link.recoverManagedTunnel("http://127.0.0.1:3773"));
1050​
1051 expect(error).toMatchObject({ _tag: "RelayRequestError", rejection: "unavailable" });
1052 expect(error.message).toContain("Trace ID: trace-expired");
1053 expect(CloudLink.shouldRetryCloudLink(error)).toBe(true);
1054 }).pipe(
1055 provideReleaseHarness({
1056 store,
1057 applyConfigCalls,
1058 requests,
1059 respond: () =>
1060 Response.json(
1062 _tag: "RelayEnvironmentLinkProofExpiredError",
1063 code: "environment_link_proof_expired",
1064 traceId: "trace-expired",
1065 },
1066 { status: 401 },
1067 ),
1068 }),
1069 );
1070 });
1071​
1072 it.effect("keeps a tunnel configuration replaced during recovery", () => {
1073 const { store, values } = makeMemorySecretStore([
1074 [CLOUD_ENDPOINT_RUNTIME_CONFIG, "old-config"],
1075 [RELAY_URL_SECRET, "https://relay.example.test"],
1076 [CLOUD_LINKED_USER_ID, "user-123"],
1077 [RELAY_ENVIRONMENT_CREDENTIAL_SECRET, "environment-credential"],
1078 ]);
1079 const applyConfigCalls: Array<unknown> = [];
1080 const requests: Array<HttpClientRequest.HttpClientRequest> = [];
1081 const freshConfig = new TextEncoder().encode("fresh-config");
1082​
1083 return Effect.gen(function* () {
1084 const link = yield* CloudLink.CloudLink;
1085 expect(yield* link.recoverManagedTunnel("http://127.0.0.1:3773")).toBe(false);
1086 expect(values.get(CLOUD_ENDPOINT_RUNTIME_CONFIG)).toBe(freshConfig);
1087 expect(applyConfigCalls).toEqual([]);
1088 }).pipe(
1089 provideReleaseHarness({
1090 store,
1091 applyConfigCalls,
1092 requests,
1093 respond: () => {
1094 values.set(CLOUD_ENDPOINT_RUNTIME_CONFIG, freshConfig);
1095 return Response.json({
1096 endpoint: {
1097 httpBaseUrl: "https://environment.example.test/",
1098 wsBaseUrl: "wss://environment.example.test/ws",
1099 providerKind: "cloudflare_tunnel",
1100 },
1101 endpointRuntime: {
1102 providerKind: "cloudflare_tunnel",
1103 connectorToken: "replacement-token",
1104 },
1105 });
1106 },
1107 }),
1108 );
1109 });
1110​
1111 it.effect("does not report a managed tunnel after a relay config write fails part-way", () => {
1112 const { store, values } = makeMemorySecretStore([
1113 [CLOUD_ENDPOINT_RUNTIME_CONFIG, "managed-config"],
1114 [CLOUD_ENDPOINT_CONFIRMED_ORIGIN, "confirmed-origin"],
1115 [RELAY_URL_SECRET, "https://relay.example.test"],
1116 [CLOUD_LINKED_USER_ID, "user-123"],
1117 [RELAY_ENVIRONMENT_CREDENTIAL_SECRET, "old-credential"],
1118 ]);
1119 const failingStore: ServerSecretStore.ServerSecretStore["Service"] = {
1120 ...store,
1121 set: (name, value) =>
1122 name === RELAY_ISSUER_SECRET
1123 ? Effect.fail(
1124 new ServerSecretStore.SecretStorePersistError({
1125 resource: name,
1126 cause: PlatformError.systemError({
1127 _tag: "PermissionDenied",
1128 module: "FileSystem",
1129 method: "rename",
1130 pathOrDescriptor: `${name}.bin`,
1131 }),
1132 }),
1134 : store.set(name, value),
1135 };
1136 const applyConfigCalls: Array<unknown> = [];
1137 const requests: Array<HttpClientRequest.HttpClientRequest> = [];
1138 const cloudMintPublicKey =
1139 "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAMY09Vo5CzEA6aEkhB/0h27t4Qs71SHS1fTSI4K0KvZQ=\n-----END PUBLIC KEY-----\n";
1140​
1141 return Effect.gen(function* () {
1142 const link = yield* CloudLink.CloudLink;
1143 const error = yield* Effect.flip(
1144 link.applyRelayConfig({
1145 relayUrl: "https://relay.example.test",
1146 cloudUserId: "user-123",
1147 environmentCredential: "new-credential",
1148 cloudMintPublicKey,
1149 endpointRuntime: null,
1150 }),
1151 );
1152​
1153 expect(error._tag).toBe("CloudLinkInternalError");
1154 expect(yield* link.linkState()).toMatchObject({
1155 linked: true,
1156 cloudUserId: "user-123",
1157 managedTunnelActive: false,
1158 });
1159 expect(values.has(CLOUD_ENDPOINT_RUNTIME_CONFIG)).toBe(false);
1160 }).pipe(provideReleaseHarness({ store: failingStore, applyConfigCalls, requests }));
1161 });
1162​
1163 it.effect("does not report a not-linked check once the managed tunnel config is stored", () => {
1164 const { store } = makeMemorySecretStore(managedLinkSecrets);
1165 const notLinked = ConnectReachability.classifyReachability(
1167 managedTunnelConfigured: false,
1168 connector: null,
1169 endpoint: null,
1170 edgeReachable: null,
1171 exitNode: null,
1172 },
1173 "2026-10-29T00:00:00.000Z",
1174 );
1175​
1176 return Effect.gen(function* () {
1177 const state = yield* (yield* CloudLink.CloudLink).linkState();
1178​
1179 expect(state.managedTunnelActive).toBe(true);
1180 expect(state.reachability?.status === "unreachable" && state.reachability.cause).not.toBe(
1181 "not_linked",
1182 );
1183 // The address shows before any check passes.
1184 expect(state.tunnelHostname).toBe("prod-abc.t3coderelay.com");
1185 }).pipe(
1186 provideReleaseHarness({
1187 store,
1188 applyConfigCalls: [],
1189 requests: [],
1190 latestReachability: Effect.succeedSome(notLinked),
1191 connector: {
1192 pid: 123,
1193 readyConnections: 0,
1194 hostname: "prod-abc.t3coderelay.com",
1195 protocol: null,
1196 },
1197 }),
1198 );
1199 });
1200​
1201 it.effect("reports a check that ran after the managed tunnel config was stored", () => {
1202 const { store } = makeMemorySecretStore(managedLinkSecrets);
1203 const reachable = {
1204 status: "reachable" as const,
1205 checkedAt: "2026-10-29T00:00:00.000Z",
1206 readyConnections: 4,
1207 protocol: "quic",
1208 };
1209​
1210 return Effect.gen(function* () {
1211 const state = yield* (yield* CloudLink.CloudLink).linkState();
1212​
1213 expect(state.reachability).toEqual(reachable);
1214 }).pipe(
1215 provideReleaseHarness({
1216 store,
1217 applyConfigCalls: [],
1218 requests: [],
1219 latestReachability: Effect.succeedSome(reachable),
1220 }),
1221 );
1222 });
1223});