1import * as Effect from "effect/Effect";
2import * as Option from "effect/Option";
3import * as Schema from "effect/Schema";
4import * as Stream from "effect/Stream";
5import * as SubscriptionRef from "effect/SubscriptionRef";
6import type { HttpClient } from "effect/http";
7import {
8 EnvironmentCloudEndpointUnavailableError,
9 type EnvironmentCloudLinkStateResult,
10 type EnvironmentConnectReachability,
11 EnvironmentHttpBadRequestError,
12 EnvironmentHttpConflictError,
13 EnvironmentHttpForbiddenError,
14 EnvironmentHttpInternalServerError,
15 EnvironmentHttpUnauthorizedError,
16 EnvironmentId,
17 WS_METHODS,
18} from "@t3tools/contracts";
19import {
20 type RelayEnvironmentLinkResponse,
21 type RelayManagedEndpointProviderKind,
22} from "@t3tools/contracts/relay";
23import { EnvironmentRegistry, EnvironmentSupervisor } from "@t3tools/client-runtime/connection";
24import { request, runStream } from "@t3tools/client-runtime/rpc";
25import { ManagedRelay, relayRequestFailureDetail } from "@t3tools/client-runtime/relay";
26import { executeConnectedEnvironmentHttpRequest } from "@t3tools/client-runtime/state/environmentHttpAuth";
27
28import { resolveCloudPublicConfig } from "./publicConfig";
29import {
30 finishRelayClientInstall,
31 reportRelayClientInstallProgress,
32 requestRelayClientInstallConfirmation,
33} from "./relayClientInstallDialog";
34
35function relayUrl(): string | null {
36 return resolveCloudPublicConfig().relayUrl;
37}
38
39export class CloudEnvironmentLinkError extends Schema.TaggedError<CloudEnvironmentLinkError>()(
40 "CloudEnvironmentLinkError",
41 {
42 message: Schema.String,
43 cause: Schema.optional(Schema.Defect()),
44 traceId: Schema.optional(Schema.String),
45 },
46) {}
47
48export const isCloudEnvironmentLinkError = Schema.is(CloudEnvironmentLinkError);
49
50const relayClientRpcError = (message: string) => (cause: unknown) =>
51 new CloudEnvironmentLinkError({
52 message,
53 cause,
54 });
55
56const ensureRelayClientAvailable = Effect.fn("linkEnvironment.ensureRelayClientAvailable")(
57 function* (
58 environmentId: EnvironmentId,
59 ): Effect.fn.Return<void, CloudEnvironmentLinkError, EnvironmentRegistry.EnvironmentRegistry> {
60 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
61 const status = yield* registry
62 .run(environmentId, request(WS_METHODS.cloudGetRelayClientStatus, {}))
63 .pipe(Effect.mapError(relayClientRpcError("Could not check relay client availability.")));
64 if (status.status === "available") return;
65 if (status.status === "unsupported") {
66 return yield* new CloudEnvironmentLinkError({
67 message: `T3 Code cannot install the relay client automatically on ${status.platform}-${status.arch}.`,
68 });
69 }
70
71 const confirmed = yield* Effect.tryPromise({
72 try: () => requestRelayClientInstallConfirmation(status.version),
73 catch: relayClientRpcError("Could not confirm relay client installation."),
74 });
75 if (!confirmed) {
76 return yield* new CloudEnvironmentLinkError({
77 message: "Relay client installation was cancelled.",
78 });
79 }
80
81 const installed = yield* registry
82 .runStream(
83 environmentId,
84 runStream(WS_METHODS.cloudInstallRelayClient, {}).pipe(
85 Stream.tap((event) => Effect.sync(() => reportRelayClientInstallProgress(event))),
86 ),
87 )
88 .pipe(
89 Stream.runLast,
90 Effect.mapError(relayClientRpcError("Could not install the relay client.")),
91 Effect.ensuring(Effect.sync(finishRelayClientInstall)),
92 );
93 if (Option.isNone(installed) || installed.value.type !== "complete") {
94 return yield* new CloudEnvironmentLinkError({
95 message: "The relay client install completed without a final status.",
96 });
97 }
98 const installedStatus = installed.value.status;
99 if (installedStatus.status !== "available") {
100 return yield* new CloudEnvironmentLinkError({
101 message:
102 installedStatus.status === "unsupported"
103 ? `T3 Code cannot install the relay client automatically on ${installedStatus.platform}-${installedStatus.arch}.`
104 : "The relay client is still unavailable after installation.",
105 });
106 }
107 },
108);
109
110const isEnvironmentCloudApiError = Schema.is(
111 Schema.Union([
112 EnvironmentHttpBadRequestError,
113 EnvironmentHttpUnauthorizedError,
114 EnvironmentHttpForbiddenError,
115 EnvironmentHttpConflictError,
116 EnvironmentHttpInternalServerError,
117 EnvironmentCloudEndpointUnavailableError,
118 ]),
119);
120
121function decodedRelayClientError(message: string) {
122 return (cause: ManagedRelay.ManagedRelayClientError) => {
123 const failed = cause._tag === "ManagedRelayRequestFailedError" ? cause : undefined;
124 const traceId = "traceId" in cause ? cause.traceId : undefined;
125 const detail = (failed && relayRequestFailureDetail(failed)) ?? cause.message;
126 return new CloudEnvironmentLinkError({
127 message: detail ? `${message}: ${detail}` : message,
128 cause,
129 ...(traceId ? { traceId } : {}),
130 });
131 };
132}
133
134function findEnvironmentCloudApiError(cause: unknown): { readonly message: string } | null {
135 if (isEnvironmentCloudApiError(cause)) {
136 return cause;
137 }
138 if (typeof cause !== "object" || cause === null) {
139 return null;
140 }
141 return "cause" in cause ? findEnvironmentCloudApiError(cause.cause) : null;
142}
143
144const environmentApiError = (message: string) => (cause: unknown) => {
145 const environmentError = findEnvironmentCloudApiError(cause);
146 return new CloudEnvironmentLinkError({
147 message: environmentError
148 ? `${message.replace(/[.:]$/, "")}: ${environmentError.message}`
149 : message,
150 cause,
151 });
152};
153
154function endpointOrigin(httpBaseUrl: string) {
155 const url = new URL(httpBaseUrl);
156 return {
157 localHttpHost: "127.0.0.1",
158 localHttpPort: Number(url.port || (url.protocol === "https:" ? 443 : 80)),
159 };
160}
161
162const MANAGED_ENDPOINT_PROVIDER_KIND =
163 "cloudflare_tunnel" satisfies RelayManagedEndpointProviderKind;
164
165function ensureLinkedEnvironmentMatches(input: {
166 readonly expectedEnvironmentId: string;
167 readonly expectedProviderKind: RelayManagedEndpointProviderKind;
168 readonly link: RelayEnvironmentLinkResponse;
169}): Effect.Effect<void, CloudEnvironmentLinkError> {
170 if (input.link.environmentId !== input.expectedEnvironmentId) {
171 return new CloudEnvironmentLinkError({
172 message: "Relay returned credentials for a different environment.",
173 });
174 }
175 if (input.link.endpoint.providerKind !== input.expectedProviderKind) {
176 return new CloudEnvironmentLinkError({
177 message: "Relay returned credentials for a different endpoint provider.",
178 });
179 }
180 return Effect.void;
181}
182
183export interface CloudLinkTarget {
184 readonly environmentId: EnvironmentId;
185}
186
187export type CloudLinkState = EnvironmentCloudLinkStateResult;
188
189// Linking can install the relay client and start a managed tunnel before it answers.
190const CLOUD_LINK_REQUEST_TIMEOUT_MS = 60_000;
191
192type ConnectedEnvironmentHttpRequest<A, E, R> = Parameters<
193 typeof executeConnectedEnvironmentHttpRequest<"connect", A, E, R>
194>[0];
195
196/** Runs on the target environment with this client's own credentials for it. */
197const connectRequest = <A, E, R>(
198 target: CloudLinkTarget,
199 input: Omit<ConnectedEnvironmentHttpRequest<A, E, R>, "group" | "timeoutMs">,
200 failureMessage: string,
201) =>
202 Effect.flatMap(EnvironmentRegistry.EnvironmentRegistry, (registry) =>
203 registry.run(
204 target.environmentId,
205 executeConnectedEnvironmentHttpRequest({
206 ...input,
207 group: "connect",
208 timeoutMs: CLOUD_LINK_REQUEST_TIMEOUT_MS,
209 }),
210 ),
211 ).pipe(Effect.mapError(environmentApiError(failureMessage)));
212
213const preparedEndpoint = (target: CloudLinkTarget) =>
214 Effect.flatMap(EnvironmentRegistry.EnvironmentRegistry, (registry) =>
215 registry.run(
216 target.environmentId,
217 Effect.flatMap(EnvironmentSupervisor.EnvironmentSupervisor, (supervisor) =>
218 SubscriptionRef.get(supervisor.prepared),
219 ),
220 ),
221 ).pipe(
222 Effect.mapError(environmentApiError("Could not reach the environment.")),
223 Effect.flatMap(
224 Option.match({
225 onNone: () =>
226 Effect.fail(
227 new CloudEnvironmentLinkError({ message: "The environment is not connected." }),
228 ),
229 onSome: ({ httpBaseUrl, target }) =>
230 Effect.succeed({
231 httpBaseUrl,
232 wsBaseUrl:
233 target._tag === "PrimaryConnectionTarget"
234 ? target.wsBaseUrl
235 : httpBaseUrl.replace(/^http/, "ws"),
236 }),
237 }),
238 ),
239 );
240
241type CloudLinkServices = EnvironmentRegistry.EnvironmentRegistry | HttpClient.HttpClient;
242
243export const readCloudLinkState: (input: {
244 readonly target: CloudLinkTarget;
245}) => Effect.Effect<CloudLinkState | null, CloudEnvironmentLinkError, CloudLinkServices> =
246 Effect.fn("linkEnvironment.readCloudLinkState")(function* (input: {
247 readonly target: CloudLinkTarget;
248 }) {
249 return yield* connectRequest(
250 input.target,
251 {
252 method: "GET",
253 url: (urls) => urls.linkState(),
254 request: ({ client, headers }) => client.linkState({ headers }),
255 },
256 "Could not read environment cloud link state.",
257 );
258 });
259
260/** Runs the environment's T3 Connect reachability check now; the link state then reports it. */
261export const checkCloudReachability: (input: {
262 readonly target: CloudLinkTarget;
263}) => Effect.Effect<EnvironmentConnectReachability, CloudEnvironmentLinkError, CloudLinkServices> =
264 Effect.fn("linkEnvironment.checkCloudReachability")(function* (input: {
265 readonly target: CloudLinkTarget;
266 }) {
267 return yield* connectRequest(
268 input.target,
269 {
270 method: "POST",
271 url: (urls) => urls.checkReachability(),
272 request: ({ client, headers }) => client.checkReachability({ headers }),
273 },
274 "Could not check T3 Connect.",
275 );
276 });
277
278export const updateCloudPreferences: (input: {
279 readonly target: CloudLinkTarget;
280 readonly publishAgentActivity: boolean;
281 readonly holdWebhooksWhileOffline?: boolean;
282}) => Effect.Effect<CloudLinkState, CloudEnvironmentLinkError, CloudLinkServices> = Effect.fn(
283 "linkEnvironment.updateCloudPreferences",
284)(function* (input: {
285 readonly target: CloudLinkTarget;
286 readonly publishAgentActivity: boolean;
287 readonly holdWebhooksWhileOffline?: boolean;
288}) {
289 const { target, ...payload } = input;
290 return yield* connectRequest(
291 target,
292 {
293 method: "POST",
294 url: (urls) => urls.preferences(),
295 request: ({ client, headers }) => client.preferences({ headers, payload }),
296 },
297 "Could not update environment cloud preferences.",
298 );
299});
300
301export const unlinkEnvironmentFromCloud: (input: {
302 readonly target: CloudLinkTarget;
303 readonly clerkToken: string | null;
304}) => Effect.Effect<
305 void,
306 CloudEnvironmentLinkError,
307 CloudLinkServices | ManagedRelay.ManagedRelayClient
308> = Effect.fn("linkEnvironment.unlinkEnvironmentFromCloud")(function* (input: {
309 readonly target: CloudLinkTarget;
310 readonly clerkToken: string | null;
311}) {
312 yield* connectRequest(
313 input.target,
314 {
315 method: "POST",
316 url: (urls) => urls.unlink(),
317 request: ({ client, headers }) => client.unlink({ headers }),
318 },
319 "Could not unlink the environment from cloud.",
320 );
321
322 const configuredRelayUrl = relayUrl();
323 if (configuredRelayUrl && input.clerkToken) {
324 const relayClient = yield* ManagedRelay.ManagedRelayClient;
325 yield* relayClient
326 .unlinkEnvironment({
327 clerkToken: input.clerkToken,
328 environmentId: input.target.environmentId,
329 })
330 .pipe(
331 Effect.mapError(
332 decodedRelayClientError(
333 "Disconnected locally, but T3 Connect did not confirm it removed this environment",
334 ),
335 ),
336 );
337 }
338});
339
340export type CloudLinkMode = "managed" | "publish_only";
341
342const PUBLISH_ONLY_PROVIDER_KIND = "manual" satisfies RelayManagedEndpointProviderKind;
343
344export const linkEnvironmentToCloud: (input: {
345 readonly target: CloudLinkTarget;
346 readonly clerkToken: string;
347 readonly mode?: CloudLinkMode;
348}) => Effect.Effect<
349 void,
350 CloudEnvironmentLinkError,
351 CloudLinkServices | ManagedRelay.ManagedRelayClient
352> = Effect.fn("linkEnvironment.linkEnvironmentToCloud")(function* (input: {
353 readonly target: CloudLinkTarget;
354 readonly clerkToken: string;
355 readonly mode?: CloudLinkMode;
356}) {
357 const configuredRelayUrl = relayUrl();
358 if (!configuredRelayUrl) {
359 return yield* new CloudEnvironmentLinkError({
360 message: "T3CODE_RELAY_URL is not configured.",
361 });
362 }
363 const managedTunnelsEnabled = (input.mode ?? "managed") === "managed";
364 const providerKind = managedTunnelsEnabled
365 ? MANAGED_ENDPOINT_PROVIDER_KIND
366 : PUBLISH_ONLY_PROVIDER_KIND;
367 const relayClient = yield* ManagedRelay.ManagedRelayClient;
368 // The environment only signs a proof for the loopback origin this request reaches it on.
369 const endpoint = yield* preparedEndpoint(input.target);
370 if (managedTunnelsEnabled) {
371 yield* ensureRelayClientAvailable(input.target.environmentId);
372 }
373
374 const challenge = yield* relayClient
375 .createEnvironmentLinkChallenge({
376 clerkToken: input.clerkToken,
377 payload: {
378 notificationsEnabled: true,
379 liveActivitiesEnabled: true,
380 managedTunnelsEnabled,
381 },
382 })
383 .pipe(
384 Effect.mapError(
385 decodedRelayClientError(
386 `${configuredRelayUrl}/v1/client/environment-link-challenges failed`,
387 ),
388 ),
389 );
390 const proof = yield* connectRequest(
391 input.target,
392 {
393 method: "POST",
394 url: (urls) => urls.linkProof(),
395 request: ({ client, headers }) =>
396 client.linkProof({
397 headers,
398 payload: {
399 challenge: challenge.challenge,
400 relayIssuer: configuredRelayUrl,
401 endpoint: { ...endpoint, providerKind },
402 origin: endpointOrigin(endpoint.httpBaseUrl),
403 },
404 }),
405 },
406 "Could not obtain environment link proof.",
407 );
408 const link = yield* relayClient
409 .linkEnvironment({
410 clerkToken: input.clerkToken,
411 payload: {
412 proof,
413 notificationsEnabled: true,
414 liveActivitiesEnabled: true,
415 managedTunnelsEnabled,
416 },
417 })
418 .pipe(
419 Effect.mapError(
420 decodedRelayClientError(`${configuredRelayUrl}/v1/client/environment-links failed`),
421 ),
422 );
423 yield* ensureLinkedEnvironmentMatches({
424 expectedEnvironmentId: input.target.environmentId,
425 expectedProviderKind: providerKind,
426 link,
427 });
428
429 yield* connectRequest(
430 input.target,
431 {
432 method: "POST",
433 url: (urls) => urls.relayConfig(),
434 request: ({ client, headers }) =>
435 client.relayConfig({
436 headers,
437 payload: {
438 relayUrl: configuredRelayUrl,
439 relayIssuer: link.relayIssuer,
440 cloudUserId: link.cloudUserId,
441 environmentCredential: link.environmentCredential,
442 cloudMintPublicKey: link.cloudMintPublicKey,
443 endpointRuntime: link.endpointRuntime,
444 },
445 }),
446 },
447 "Could not configure environment relay access.",
448 );
449});