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