1import * as Effect from "effect/Effect";
2import * as Schema from "effect/Schema";
3import { HttpClient } from "effect/http";
4import {
5 EnvironmentCloudEndpointUnavailableError,
6 EnvironmentHttpBadRequestError,
7 EnvironmentHttpConflictError,
8 EnvironmentHttpForbiddenError,
9 EnvironmentHttpInternalServerError,
10 EnvironmentHttpUnauthorizedError,
11} from "@t3tools/contracts";
12import {
13 type RelayEnvironmentLinkResponse as RelayEnvironmentLinkResponseType,
14 type RelayManagedEndpointProviderKind,
15} from "@t3tools/contracts/relay";
16import { findErrorTraceId } from "@t3tools/client-runtime/errors";
17import { ManagedRelay, relayRequestFailureDetail } from "@t3tools/client-runtime/relay";
18import { makeEnvironmentHttpApiClient } from "@t3tools/client-runtime/rpc";
19
20import type { SavedRemoteConnection } from "../../lib/connection";
21import * as MobileStorage from "../../persistence/mobile-storage";
22import { resolveCloudPublicConfig } from "./publicConfig";
23
24function readRelayUrl(): string | null {
25 return resolveCloudPublicConfig().relay.url;
26}
27
28export class CloudEnvironmentLinkError extends Schema.TaggedError<CloudEnvironmentLinkError>()(
29 "CloudEnvironmentLinkError",
30 {
31 message: Schema.String,
32 cause: Schema.optional(Schema.Defect()),
33 traceId: Schema.optional(Schema.String),
34 },
35) {}
36
37const isEnvironmentCloudApiError = Schema.is(
38 Schema.Union([
39 EnvironmentHttpBadRequestError,
40 EnvironmentHttpUnauthorizedError,
41 EnvironmentHttpForbiddenError,
42 EnvironmentHttpConflictError,
43 EnvironmentHttpInternalServerError,
44 EnvironmentCloudEndpointUnavailableError,
45 ]),
46);
47
48const MANAGED_ENDPOINT_PROVIDER_KIND =
49 "cloudflare_tunnel" satisfies RelayManagedEndpointProviderKind;
50
51function cloudEnvironmentLinkError(message: string) {
52 return (cause: unknown) => {
53 const environmentError = findEnvironmentCloudApiError(cause);
54 const traceId = findErrorTraceId(cause);
55 const detail = environmentError
56 ? `${message.replace(/[.:]$/, "")}: ${environmentError.message}`
57 : withDevCause(message, cause);
58 return new CloudEnvironmentLinkError({
59 message: detail,
60 cause,
61 ...(traceId === null ? {} : { traceId }),
62 });
63 };
64}
65
66function isDevRuntime(): boolean {
67 return typeof __DEV__ !== "undefined" && __DEV__;
68}
69
70function causeMessage(cause: unknown): string | null {
71 if (cause instanceof Error && cause.message) {
72 return cause.message;
73 }
74 if (typeof cause === "object" && cause !== null) {
75 const record = cause as { readonly message?: unknown; readonly cause?: unknown };
76 if (typeof record.message === "string" && record.message.length > 0) {
77 const nested = causeMessage(record.cause);
78 return nested ? `${record.message}: ${nested}` : record.message;
79 }
80 }
81 return null;
82}
83
84function withDevCause(message: string, cause: unknown): string {
85 if (!isDevRuntime()) {
86 return message;
87 }
88 const detail = causeMessage(cause);
89 return detail ? `${message} (${detail})` : message;
90}
91
92function decodedRelayClientError(message: string) {
93 return (cause: ManagedRelay.ManagedRelayClientError) => {
94 const failed = cause._tag === "ManagedRelayRequestFailedError" ? cause : undefined;
95 const traceId = failed?.traceId;
96 const detail = failed ? relayRequestFailureDetail(failed) : null;
97 return new CloudEnvironmentLinkError({
98 message: detail ? `${message}: ${detail}` : message,
99 cause,
100 ...(traceId ? { traceId } : {}),
101 });
102 };
103}
104
105function findEnvironmentCloudApiError(cause: unknown): { readonly message: string } | null {
106 if (isEnvironmentCloudApiError(cause)) {
107 return cause;
108 }
109 if (typeof cause !== "object" || cause === null) {
110 return null;
111 }
112 return "cause" in cause ? findEnvironmentCloudApiError(cause.cause) : null;
113}
114
115function requireRelayUrl(): Effect.Effect<string, CloudEnvironmentLinkError> {
116 const relayUrl = readRelayUrl();
117 return relayUrl
118 ? Effect.succeed(relayUrl)
119 : Effect.fail(new CloudEnvironmentLinkError({ message: "Relay URL is not configured." }));
120}
121
122function endpointOrigin(httpBaseUrl: string) {
123 const url = new URL(httpBaseUrl);
124 return {
125 localHttpHost: "127.0.0.1",
126 localHttpPort: Number(url.port || (url.protocol === "https:" ? 443 : 80)),
127 };
128}
129
130function ensureLinkedEnvironmentMatches(input: {
131 readonly expectedEnvironmentId: string;
132 readonly expectedProviderKind: RelayManagedEndpointProviderKind;
133 readonly link: RelayEnvironmentLinkResponseType;
134}): Effect.Effect<void, CloudEnvironmentLinkError> {
135 if (input.link.environmentId !== input.expectedEnvironmentId) {
136 return new CloudEnvironmentLinkError({
137 message: "Relay returned credentials for a different environment.",
138 });
139 }
140 if (input.link.endpoint.providerKind !== input.expectedProviderKind) {
141 return new CloudEnvironmentLinkError({
142 message: "Relay returned credentials for a different endpoint provider.",
143 });
144 }
145 return Effect.void;
146}
147
148interface LinkEnvironmentToCloudInput {
149 readonly connection: SavedRemoteConnection;
150 readonly clerkToken: string;
151}
152
153type LinkEnvironmentToCloudRequirements =
154 | HttpClient.HttpClient
155 | ManagedRelay.ManagedRelayClient
156 | MobileStorage.MobileStorage;
157
158export const linkEnvironmentToCloudWithPreference = Effect.fn(
159 "linkEnvironment.linkEnvironmentToCloudWithPreference",
160)(function* (
161 input: LinkEnvironmentToCloudInput & { readonly liveActivitiesEnabled: boolean },
162): Effect.fn.Return<void, CloudEnvironmentLinkError, LinkEnvironmentToCloudRequirements> {
163 if (!input.connection.bearerToken) {
164 return yield* new CloudEnvironmentLinkError({
165 message: "Only a locally paired bearer connection can be linked to the cloud.",
166 });
167 }
168 const localBearerToken = input.connection.bearerToken;
169 const relayUrl = yield* requireRelayUrl();
170 const relayClient = yield* ManagedRelay.ManagedRelayClient;
171 const storage = yield* MobileStorage.MobileStorage;
172 const deviceId = yield* storage.loadOrCreateAgentAwarenessDeviceId.pipe(
173 Effect.mapError(cloudEnvironmentLinkError("Could not load the mobile device id.")),
174 );
175 const liveActivitiesEnabled = input.liveActivitiesEnabled;
176 const challenge = yield* relayClient
177 .createEnvironmentLinkChallenge({
178 clerkToken: input.clerkToken,
179 payload: {
180 notificationsEnabled: true,
181 liveActivitiesEnabled,
182 managedTunnelsEnabled: true,
183 },
184 })
185 .pipe(
186 Effect.mapError(
187 decodedRelayClientError(`${relayUrl}/v1/client/environment-link-challenges failed`),
188 ),
189 );
190 const environmentClient = yield* makeEnvironmentHttpApiClient(input.connection.httpBaseUrl);
191 const proof = yield* environmentClient.connect
192 .linkProof({
193 headers: { authorization: `Bearer ${localBearerToken}` },
194 payload: {
195 challenge: challenge.challenge,
196 relayIssuer: relayUrl,
197 endpoint: {
198 httpBaseUrl: input.connection.httpBaseUrl,
199 wsBaseUrl: input.connection.wsBaseUrl,
200 providerKind: MANAGED_ENDPOINT_PROVIDER_KIND,
201 },
202 origin: endpointOrigin(input.connection.httpBaseUrl),
203 },
204 })
205 .pipe(Effect.mapError(cloudEnvironmentLinkError("Could not obtain environment link proof.")));
206 const link = yield* relayClient
207 .linkEnvironment({
208 clerkToken: input.clerkToken,
209 payload: {
210 deviceId,
211 proof,
212 notificationsEnabled: true,
213 liveActivitiesEnabled,
214 managedTunnelsEnabled: true,
215 },
216 })
217 .pipe(
218 Effect.mapError(decodedRelayClientError(`${relayUrl}/v1/client/environment-links failed`)),
219 );
220 yield* ensureLinkedEnvironmentMatches({
221 expectedEnvironmentId: input.connection.environmentId,
222 expectedProviderKind: MANAGED_ENDPOINT_PROVIDER_KIND,
223 link,
224 });
225
226 yield* environmentClient.connect
227 .relayConfig({
228 headers: { authorization: `Bearer ${localBearerToken}` },
229 payload: {
230 relayUrl,
231 relayIssuer: link.relayIssuer,
232 cloudUserId: link.cloudUserId,
233 environmentCredential: link.environmentCredential,
234 cloudMintPublicKey: link.cloudMintPublicKey,
235 endpointRuntime: link.endpointRuntime,
236 },
237 })
238 .pipe(
239 Effect.mapError(cloudEnvironmentLinkError("Could not configure environment relay access.")),
240 );
241});