| @@ -1,4 +1,3 @@ |
| 1 | | −import * as Data from "effect/Data"; |
| 2 | 1 | import * as Effect from "effect/Effect"; |
| 3 | 2 | import * as Option from "effect/Option"; |
| 4 | 3 | import * as Schema from "effect/Schema"; |
| @@ -19,13 +18,14 @@ import { · 14 unmodified lines |
| 19 | 18 | type RelayClientDeviceRecord, |
| 20 | 19 | type RelayClientEnvironmentRecord, |
| 21 | 20 | type RelayEnvironmentLinkResponse, |
| 22 | | − type RelayProtectedError as RelayProtectedErrorType, |
| 23 | 21 | type RelayManagedEndpointProviderKind, |
| 22 | + RelayProtectedError, |
| 24 | 23 | } from "@t3tools/contracts/relay"; |
| 25 | 24 | import { EnvironmentRegistry } from "@t3tools/client-runtime/connection"; |
| 26 | 25 | import { request, runStream } from "@t3tools/client-runtime/rpc"; |
| 27 | 26 | import { makeEnvironmentHttpApiClient } from "@t3tools/client-runtime/rpc"; |
| 28 | 27 | import { ManagedRelay } from "@t3tools/client-runtime/relay"; |
| 28 | +import { getUrlDiagnostics } from "@t3tools/shared/urlDiagnostics"; |
| 29 | 29 | |
| 30 | 30 | import { |
| 31 | 31 | readPrimaryEnvironmentDescriptor, |
| @@ -51,17 +51,218 @@ function relayUrl(): string | null { · 19 unmodified lines |
| 51 | 51 | return resolveCloudPublicConfig().relayUrl; |
| 52 | 52 | } |
| 53 | 53 | |
| 54 | | −export class CloudEnvironmentLinkError extends Data.TaggedError("CloudEnvironmentLinkError")<{ |
| 55 | | − readonly message: string; |
| 56 | | − readonly cause?: unknown; |
| 57 | | − readonly traceId?: string; |
| 58 | | −}> {} |
| 54 | +const EnvironmentCloudApiError = Schema.Union([ |
| 55 | + EnvironmentHttpBadRequestError, |
| 56 | + EnvironmentHttpUnauthorizedError, |
| 57 | + EnvironmentHttpForbiddenError, |
| 58 | + EnvironmentHttpConflictError, |
| 59 | + EnvironmentHttpInternalServerError, |
| 60 | + EnvironmentCloudEndpointUnavailableError, |
| 61 | +]); |
| 62 | +type EnvironmentCloudApiError = typeof EnvironmentCloudApiError.Type; |
| 63 | +const isEnvironmentCloudApiError = Schema.is(EnvironmentCloudApiError); |
| 59 | 64 | |
| 60 | | −const relayClientRpcError = (message: string) => (cause: unknown) => |
| 61 | | − new CloudEnvironmentLinkError({ |
| 62 | | − message, |
| 63 | | − cause, |
| 64 | | − }); |
| 65 | +function relayUrlDiagnosticFields(relayUrl: string | undefined) { |
| 66 | + if (relayUrl === undefined) return {}; |
| 67 | + const diagnostics = getUrlDiagnostics(relayUrl); |
| 68 | + return { |
| 69 | + relayUrlInputLength: diagnostics.inputLength, |
| 70 | + ...(diagnostics.protocol === undefined ? {} : { relayUrlProtocol: diagnostics.protocol }), |
| 71 | + ...(diagnostics.hostname === undefined ? {} : { relayUrlHostname: diagnostics.hostname }), |
| 72 | + }; |
| 73 | +} |
| 74 | + |
| 75 | +function httpBaseUrlDiagnosticFields(httpBaseUrl: string | undefined) { |
| 76 | + if (httpBaseUrl === undefined) return {}; |
| 77 | + const diagnostics = getUrlDiagnostics(httpBaseUrl); |
| 78 | + return { |
| 79 | + httpBaseUrlInputLength: diagnostics.inputLength, |
| 80 | + ...(diagnostics.protocol === undefined ? {} : { httpBaseUrlProtocol: diagnostics.protocol }), |
| 81 | + ...(diagnostics.hostname === undefined ? {} : { httpBaseUrlHostname: diagnostics.hostname }), |
| 82 | + }; |
| 83 | +} |
| 84 | + |
| 85 | +export const CloudEnvironmentLinkAction = Schema.Literals([ |
| 86 | + "check relay client availability", |
| 87 | + "confirm relay client installation", |
| 88 | + "install the relay client", |
| 89 | + "list relay-managed environments", |
| 90 | + "list cloud devices", |
| 91 | + "read environment cloud link state", |
| 92 | + "update environment cloud preferences", |
| 93 | + "unlink the environment from cloud", |
| 94 | + "revoke the cloud environment link", |
| 95 | + "create an environment link challenge", |
| 96 | + "obtain an environment link proof", |
| 97 | + "link the environment", |
| 98 | + "derive the environment endpoint origin", |
| 99 | + "initialize the environment HTTP client", |
| 100 | + "configure environment relay access", |
| 101 | +]); |
| 102 | +export type CloudEnvironmentLinkAction = typeof CloudEnvironmentLinkAction.Type; |
| 103 | + |
| 104 | +export class CloudEnvironmentLinkOperationError extends Schema.TaggedErrorClass<CloudEnvironmentLinkOperationError>()( |
| 105 | + "CloudEnvironmentLinkOperationError", |
| 106 | + { |
| 107 | + action: CloudEnvironmentLinkAction, |
| 108 | + environmentId: Schema.optionalKey(Schema.String), |
| 109 | + relayUrlInputLength: Schema.optionalKey(Schema.Number), |
| 110 | + relayUrlProtocol: Schema.optionalKey(Schema.String), |
| 111 | + relayUrlHostname: Schema.optionalKey(Schema.String), |
| 112 | + httpBaseUrlInputLength: Schema.optionalKey(Schema.Number), |
| 113 | + httpBaseUrlProtocol: Schema.optionalKey(Schema.String), |
| 114 | + httpBaseUrlHostname: Schema.optionalKey(Schema.String), |
| 115 | + traceId: Schema.optionalKey(Schema.String), |
| 116 | + relayError: Schema.optionalKey(RelayProtectedError), |
| 117 | + environmentError: Schema.optionalKey(EnvironmentCloudApiError), |
| 118 | + cause: Schema.Defect(), |
| 119 | + }, |
| 120 | +) { |
| 121 | + static fromManagedRelay(input: { |
| 122 | + readonly action: CloudEnvironmentLinkAction; |
| 123 | + readonly cause: ManagedRelay.ManagedRelayClientError; |
| 124 | + readonly environmentId?: string; |
| 125 | + readonly relayUrl?: string; |
| 126 | + readonly httpBaseUrl?: string; |
| 127 | + }): CloudEnvironmentLinkOperationError { |
| 128 | + const requestFailure = |
| 129 | + input.cause._tag === "ManagedRelayRequestFailedError" ? input.cause : undefined; |
| 130 | + return new CloudEnvironmentLinkOperationError({ |
| 131 | + action: input.action, |
| 132 | + cause: input.cause, |
| 133 | + ...(input.environmentId === undefined ? {} : { environmentId: input.environmentId }), |
| 134 | + ...relayUrlDiagnosticFields(input.relayUrl), |
| 135 | + ...httpBaseUrlDiagnosticFields(input.httpBaseUrl), |
| 136 | + ...(requestFailure?.traceId === undefined ? {} : { traceId: requestFailure.traceId }), |
| 137 | + ...(requestFailure?.relayError === undefined |
| 138 | + ? {} |
| 139 | + : { relayError: requestFailure.relayError }), |
| 140 | + }); |
| 141 | + } |
| 142 | + |
| 143 | + static fromEnvironmentApi(input: { |
| 144 | + readonly action: CloudEnvironmentLinkAction; |
| 145 | + readonly cause: unknown; |
| 146 | + readonly environmentId: string; |
| 147 | + readonly httpBaseUrl: string; |
| 148 | + }): CloudEnvironmentLinkOperationError { |
| 149 | + const environmentError = CloudEnvironmentLinkOperationError.findEnvironmentApiError( |
| 150 | + input.cause, |
| 151 | + ); |
| 152 | + return new CloudEnvironmentLinkOperationError({ |
| 153 | + action: input.action, |
| 154 | + environmentId: input.environmentId, |
| 155 | + ...httpBaseUrlDiagnosticFields(input.httpBaseUrl), |
| 156 | + cause: input.cause, |
| 157 | + ...(environmentError === undefined ? {} : { environmentError }), |
| 158 | + }); |
| 159 | + } |
| 160 | + |
| 161 | + private static findEnvironmentApiError(cause: unknown): EnvironmentCloudApiError | undefined { |
| 162 | + const seen = new Set<unknown>(); |
| 163 | + let current = cause; |
| 164 | + while (typeof current === "object" && current !== null && !seen.has(current)) { |
| 165 | + if (isEnvironmentCloudApiError(current)) { |
| 166 | + return current; |
| 167 | + } |
| 168 | + seen.add(current); |
| 169 | + current = "cause" in current ? current.cause : undefined; |
| 170 | + } |
| 171 | + return undefined; |
| 172 | + } |
| 173 | + |
| 174 | + override get message(): string { |
| 175 | + const environment = |
| 176 | + this.environmentId === undefined ? "" : ` for environment "${this.environmentId}"`; |
| 177 | + return `Could not ${this.action}${environment}.`; |
| 178 | + } |
| 179 | +} |
| 180 | + |
| 181 | +export class CloudRelayUrlNotConfiguredError extends Schema.TaggedErrorClass<CloudRelayUrlNotConfiguredError>()( |
| 182 | + "CloudRelayUrlNotConfiguredError", |
| 183 | + { environmentId: Schema.optionalKey(Schema.String) }, |
| 184 | +) { |
| 185 | + override get message(): string { |
| 186 | + return "T3CODE_RELAY_URL is not configured."; |
| 187 | + } |
| 188 | +} |
| 189 | + |
| 190 | +export class CloudRelayClientInstallUnsupportedError extends Schema.TaggedErrorClass<CloudRelayClientInstallUnsupportedError>()( |
| 191 | + "CloudRelayClientInstallUnsupportedError", |
| 192 | + { |
| 193 | + environmentId: Schema.String, |
| 194 | + phase: Schema.Literals(["preflight", "post-install"]), |
| 195 | + version: Schema.String, |
| 196 | + platform: Schema.String, |
| 197 | + architecture: Schema.String, |
| 198 | + }, |
| 199 | +) { |
| 200 | + override get message(): string { |
| 201 | + return `T3 Code cannot install the relay client automatically on ${this.platform}-${this.architecture}.`; |
| 202 | + } |
| 203 | +} |
| 204 | + |
| 205 | +export class CloudRelayClientInstallCancelledError extends Schema.TaggedErrorClass<CloudRelayClientInstallCancelledError>()( |
| 206 | + "CloudRelayClientInstallCancelledError", |
| 207 | + { |
| 208 | + environmentId: Schema.String, |
| 209 | + version: Schema.String, |
| 210 | + }, |
| 211 | +) { |
| 212 | + override get message(): string { |
| 213 | + return "Relay client installation was cancelled."; |
| 214 | + } |
| 215 | +} |
| 216 | + |
| 217 | +export class CloudRelayClientInstallIncompleteError extends Schema.TaggedErrorClass<CloudRelayClientInstallIncompleteError>()( |
| 218 | + "CloudRelayClientInstallIncompleteError", |
| 219 | + { |
| 220 | + environmentId: Schema.String, |
| 221 | + version: Schema.String, |
| 222 | + }, |
| 223 | +) { |
| 224 | + override get message(): string { |
| 225 | + return "The relay client install completed without a final status."; |
| 226 | + } |
| 227 | +} |
| 228 | + |
| 229 | +export class CloudRelayClientUnavailableAfterInstallError extends Schema.TaggedErrorClass<CloudRelayClientUnavailableAfterInstallError>()( |
| 230 | + "CloudRelayClientUnavailableAfterInstallError", |
| 231 | + { |
| 232 | + environmentId: Schema.String, |
| 233 | + version: Schema.String, |
| 234 | + }, |
| 235 | +) { |
| 236 | + override get message(): string { |
| 237 | + return "The relay client is still unavailable after installation."; |
| 238 | + } |
| 239 | +} |
| 240 | + |
| 241 | +export class CloudEnvironmentLinkResponseMismatchError extends Schema.TaggedErrorClass<CloudEnvironmentLinkResponseMismatchError>()( |
| 242 | + "CloudEnvironmentLinkResponseMismatchError", |
| 243 | + { |
| 244 | + environmentId: Schema.String, |
| 245 | + field: Schema.Literals(["environment id", "endpoint provider"]), |
| 246 | + expected: Schema.String, |
| 247 | + actual: Schema.String, |
| 248 | + }, |
| 249 | +) { |
| 250 | + override get message(): string { |
| 251 | + return `Relay returned link credentials with an unexpected ${this.field}.`; |
| 252 | + } |
| 253 | +} |
| 254 | + |
| 255 | +export const CloudEnvironmentLinkError = Schema.Union([ |
| 256 | + CloudEnvironmentLinkOperationError, |
| 257 | + CloudRelayUrlNotConfiguredError, |
| 258 | + CloudRelayClientInstallUnsupportedError, |
| 259 | + CloudRelayClientInstallCancelledError, |
| 260 | + CloudRelayClientInstallIncompleteError, |
| 261 | + CloudRelayClientUnavailableAfterInstallError, |
| 262 | + CloudEnvironmentLinkResponseMismatchError, |
| 263 | +]); |
| 264 | +export type CloudEnvironmentLinkError = typeof CloudEnvironmentLinkError.Type; |
| 265 | +export const isCloudEnvironmentLinkError = Schema.is(CloudEnvironmentLinkError); |
| 65 | 266 | |
| 66 | 267 | function ensureRelayClientAvailable( |
| 67 | 268 | environmentId: EnvironmentId, |
| @@ -70,21 +271,40 @@ function ensureRelayClientAvailable( · 2 unmodified lines |
| 70 | 271 | const registry = yield* EnvironmentRegistry; |
| 71 | 272 | const status = yield* registry |
| 72 | 273 | .run(environmentId, request(WS_METHODS.cloudGetRelayClientStatus, {})) |
| 73 | | − .pipe(Effect.mapError(relayClientRpcError("Could not check relay client availability."))); |
| 274 | + .pipe( |
| 275 | + Effect.mapError( |
| 276 | + (cause) => |
| 277 | + new CloudEnvironmentLinkOperationError({ |
| 278 | + action: "check relay client availability", |
| 279 | + environmentId, |
| 280 | + cause, |
| 281 | + }), |
| 282 | + ), |
| 283 | + ); |
| 74 | 284 | if (status.status === "available") return; |
| 75 | 285 | if (status.status === "unsupported") { |
| 76 | | − return yield* new CloudEnvironmentLinkError({ |
| 77 | | − message: `T3 Code cannot install the relay client automatically on ${status.platform}-${status.arch}.`, |
| 286 | + return yield* new CloudRelayClientInstallUnsupportedError({ |
| 287 | + environmentId, |
| 288 | + phase: "preflight", |
| 289 | + version: status.version, |
| 290 | + platform: status.platform, |
| 291 | + architecture: status.arch, |
| 78 | 292 | }); |
| 79 | 293 | } |
| 80 | 294 | |
| 81 | 295 | const confirmed = yield* Effect.tryPromise({ |
| 82 | 296 | try: () => requestRelayClientInstallConfirmation(status.version), |
| 83 | | − catch: relayClientRpcError("Could not confirm relay client installation."), |
| 297 | + catch: (cause) => |
| 298 | + new CloudEnvironmentLinkOperationError({ |
| 299 | + action: "confirm relay client installation", |
| 300 | + environmentId, |
| 301 | + cause, |
| 302 | + }), |
| 84 | 303 | }); |
| 85 | 304 | if (!confirmed) { |
| 86 | | − return yield* new CloudEnvironmentLinkError({ |
| 87 | | − message: "Relay client installation was cancelled.", |
| 305 | + return yield* new CloudRelayClientInstallCancelledError({ |
| 306 | + environmentId, |
| 307 | + version: status.version, |
| 88 | 308 | }); |
| 89 | 309 | } |
| 90 | 310 | |
| @@ -97,112 +317,73 @@ function ensureRelayClientAvailable( · 6 unmodified lines |
| 97 | 317 | ) |
| 98 | 318 | .pipe( |
| 99 | 319 | Stream.runLast, |
| 100 | | − Effect.mapError(relayClientRpcError("Could not install the relay client.")), |
| 320 | + Effect.mapError( |
| 321 | + (cause) => |
| 322 | + new CloudEnvironmentLinkOperationError({ |
| 323 | + action: "install the relay client", |
| 324 | + environmentId, |
| 325 | + cause, |
| 326 | + }), |
| 327 | + ), |
| 101 | 328 | Effect.ensuring(Effect.sync(finishRelayClientInstall)), |
| 102 | 329 | ); |
| 103 | 330 | if (Option.isNone(installed) || installed.value.type !== "complete") { |
| 104 | | − return yield* new CloudEnvironmentLinkError({ |
| 105 | | − message: "The relay client install completed without a final status.", |
| 331 | + return yield* new CloudRelayClientInstallIncompleteError({ |
| 332 | + environmentId, |
| 333 | + version: status.version, |
| 106 | 334 | }); |
| 107 | 335 | } |
| 108 | 336 | const installedStatus = installed.value.status; |
| 109 | 337 | if (installedStatus.status !== "available") { |
| 110 | | − return yield* new CloudEnvironmentLinkError({ |
| 111 | | − message: |
| 112 | | − installedStatus.status === "unsupported" |
| 113 | | − ? `T3 Code cannot install the relay client automatically on ${installedStatus.platform}-${installedStatus.arch}.` |
| 114 | | − : "The relay client is still unavailable after installation.", |
| 115 | | − }); |
| 338 | + return yield* installedStatus.status === "unsupported" |
| 339 | + ? new CloudRelayClientInstallUnsupportedError({ |
| 340 | + environmentId, |
| 341 | + phase: "post-install", |
| 342 | + version: installedStatus.version, |
| 343 | + platform: installedStatus.platform, |
| 344 | + architecture: installedStatus.arch, |
| 345 | + }) |
| 346 | + : new CloudRelayClientUnavailableAfterInstallError({ |
| 347 | + environmentId, |
| 348 | + version: installedStatus.version, |
| 349 | + }); |
| 116 | 350 | } |
| 117 | 351 | }); |
| 118 | 352 | } |
| 119 | 353 | |
| 120 | | −const isEnvironmentCloudApiError = Schema.is( |
| 121 | | − Schema.Union([ |
| 122 | | − EnvironmentHttpBadRequestError, |
| 123 | | − EnvironmentHttpUnauthorizedError, |
| 124 | | − EnvironmentHttpForbiddenError, |
| 125 | | − EnvironmentHttpConflictError, |
| 126 | | − EnvironmentHttpInternalServerError, |
| 127 | | − EnvironmentCloudEndpointUnavailableError, |
| 128 | | − ]), |
| 129 | | −); |
| 130 | | − |
| 131 | | −function relayProtectedErrorMessage(error: RelayProtectedErrorType): string { |
| 132 | | − switch (error._tag) { |
| 133 | | − case "RelayAuthInvalidError": |
| 134 | | − switch (error.reason) { |
| 135 | | − case "missing_bearer": |
| 136 | | − case "invalid_bearer": |
| 137 | | − return "Relay rejected the cloud session token."; |
| 138 | | − case "invalid_dpop": |
| 139 | | − return "Relay rejected the DPoP proof."; |
| 140 | | − case "not_authorized": |
| 141 | | − return "Relay rejected the authenticated request."; |
| 142 | | − } |
| 143 | | − case "RelayEnvironmentLinkProofExpiredError": |
| 144 | | − return "Relay rejected an expired environment link proof."; |
| 145 | | − case "RelayEnvironmentLinkProofInvalidError": |
| 146 | | − return `Relay rejected the environment link proof (${error.reason}).`; |
| 147 | | − case "RelayEnvironmentConnectNotAuthorizedError": |
| 148 | | − return "Relay rejected the environment connection request."; |
| 149 | | − case "RelayEnvironmentEndpointUnavailableError": |
| 150 | | − return `Relay could not reach the environment endpoint (${error.reason}).`; |
| 151 | | − case "RelayEnvironmentEndpointTimedOutError": |
| 152 | | − return "Relay timed out while contacting the environment endpoint."; |
| 153 | | − case "RelayEnvironmentLinkFailedError": |
| 154 | | − return `Relay could not link the environment (${error.reason}).`; |
| 155 | | − case "RelayEnvironmentLinkUnavailableError": |
| 156 | | − return `Relay cannot provision the managed endpoint (${error.reason}).`; |
| 157 | | − case "RelayAgentActivityPublishProofExpiredError": |
| 158 | | − return "Relay rejected an expired agent activity publish proof."; |
| 159 | | − case "RelayAgentActivityPublishProofInvalidError": |
| 160 | | − return `Relay rejected the agent activity publish proof (${error.reason}).`; |
| 161 | | − case "RelayInternalError": |
| 162 | | − return `Relay encountered an internal error (${error.reason}).`; |
| 163 | | − } |
| 164 | | −} |
| 165 | | − |
| 166 | | −function decodedRelayClientError(message: string) { |
| 167 | | − return (cause: ManagedRelay.ManagedRelayClientError) => { |
| 168 | | − const relayError = |
| 169 | | − cause._tag === "ManagedRelayRequestFailedError" ? cause.relayError : undefined; |
| 170 | | − const traceId = cause._tag === "ManagedRelayRequestFailedError" ? cause.traceId : undefined; |
| 171 | | − const detail = relayError ? relayProtectedErrorMessage(relayError) : null; |
| 172 | | − return new CloudEnvironmentLinkError({ |
| 173 | | − message: detail ? `${message}: ${detail}` : message, |
| 174 | | − cause, |
| 175 | | − ...(traceId ? { traceId } : {}), |
| 176 | | − }); |
| 177 | | − }; |
| 178 | | −} |
| 179 | | − |
| 180 | | −function findEnvironmentCloudApiError(cause: unknown): { readonly message: string } | null { |
| 181 | | − if (isEnvironmentCloudApiError(cause)) { |
| 182 | | − return cause; |
| 183 | | − } |
| 184 | | − if (typeof cause !== "object" || cause === null) { |
| 185 | | − return null; |
| 186 | | − } |
| 187 | | − return "cause" in cause ? findEnvironmentCloudApiError(cause.cause) : null; |
| 188 | | −} |
| 189 | | − |
| 190 | | −const environmentApiError = (message: string) => (cause: unknown) => { |
| 191 | | − const environmentError = findEnvironmentCloudApiError(cause); |
| 192 | | − return new CloudEnvironmentLinkError({ |
| 193 | | − message: environmentError |
| 194 | | − ? `${message.replace(/[.:]$/, "")}: ${environmentError.message}` |
| 195 | | − : message, |
| 196 | | − cause, |
| 354 | +function endpointOrigin(input: { readonly environmentId: string; readonly httpBaseUrl: string }) { |
| 355 | + return Effect.try({ |
| 356 | + try: () => { |
| 357 | + const url = new URL(input.httpBaseUrl); |
| 358 | + return { |
| 359 | + localHttpHost: "127.0.0.1", |
| 360 | + localHttpPort: Number(url.port || (url.protocol === "https:" ? 443 : 80)), |
| 361 | + }; |
| 362 | + }, |
| 363 | + catch: (cause) => |
| 364 | + new CloudEnvironmentLinkOperationError({ |
| 365 | + action: "derive the environment endpoint origin", |
| 366 | + environmentId: input.environmentId, |
| 367 | + ...httpBaseUrlDiagnosticFields(input.httpBaseUrl), |
| 368 | + cause, |
| 369 | + }), |
| 197 | 370 | }); |
| 198 | | −}; |
| 371 | +} |
| 199 | 372 | |
| 200 | | −function endpointOrigin(httpBaseUrl: string) { |
| 201 | | − const url = new URL(httpBaseUrl); |
| 202 | | − return { |
| 203 | | − localHttpHost: "127.0.0.1", |
| 204 | | − localHttpPort: Number(url.port || (url.protocol === "https:" ? 443 : 80)), |
| 205 | | − }; |
| 373 | +function makeCloudEnvironmentHttpApiClient(input: { |
| 374 | + readonly environmentId: string; |
| 375 | + readonly httpBaseUrl: string; |
| 376 | +}) { |
| 377 | + return Effect.try({ |
| 378 | + try: () => makeEnvironmentHttpApiClient(input.httpBaseUrl), |
| 379 | + catch: (cause) => |
| 380 | + new CloudEnvironmentLinkOperationError({ |
| 381 | + action: "initialize the environment HTTP client", |
| 382 | + environmentId: input.environmentId, |
| 383 | + ...httpBaseUrlDiagnosticFields(input.httpBaseUrl), |
| 384 | + cause, |
| 385 | + }), |
| 386 | + }).pipe(Effect.flatten); |
| 206 | 387 | } |
| 207 | 388 | |
| 208 | 389 | const MANAGED_ENDPOINT_PROVIDER_KIND = |
| @@ -214,13 +395,19 @@ function ensureLinkedEnvironmentMatches(input: { · 5 unmodified lines |
| 214 | 395 | readonly link: RelayEnvironmentLinkResponse; |
| 215 | 396 | }): Effect.Effect<void, CloudEnvironmentLinkError> { |
| 216 | 397 | if (input.link.environmentId !== input.expectedEnvironmentId) { |
| 217 | | − return new CloudEnvironmentLinkError({ |
| 218 | | − message: "Relay returned credentials for a different environment.", |
| 398 | + return new CloudEnvironmentLinkResponseMismatchError({ |
| 399 | + environmentId: input.expectedEnvironmentId, |
| 400 | + field: "environment id", |
| 401 | + expected: input.expectedEnvironmentId, |
| 402 | + actual: input.link.environmentId, |
| 219 | 403 | }); |
| 220 | 404 | } |
| 221 | 405 | if (input.link.endpoint.providerKind !== input.expectedProviderKind) { |
| 222 | | − return new CloudEnvironmentLinkError({ |
| 223 | | − message: "Relay returned credentials for a different endpoint provider.", |
| 406 | + return new CloudEnvironmentLinkResponseMismatchError({ |
| 407 | + environmentId: input.expectedEnvironmentId, |
| 408 | + field: "endpoint provider", |
| 409 | + expected: input.expectedProviderKind, |
| 410 | + actual: input.link.endpoint.providerKind, |
| 224 | 411 | }); |
| 225 | 412 | } |
| 226 | 413 | return Effect.void; |
| @@ -275,9 +462,7 @@ export function listManagedCloudEnvironments(input: { · 48 unmodified lines |
| 275 | 462 | return Effect.gen(function* () { |
| 276 | 463 | const configuredRelayUrl = relayUrl(); |
| 277 | 464 | if (!configuredRelayUrl) { |
| 278 | | − return yield* new CloudEnvironmentLinkError({ |
| 279 | | − message: "T3CODE_RELAY_URL is not configured.", |
| 280 | | − }); |
| 465 | + return yield* new CloudRelayUrlNotConfiguredError({}); |
| 281 | 466 | } |
| 282 | 467 | const relayClient = yield* ManagedRelay.ManagedRelayClient; |
| 283 | 468 | return yield* relayClient |
| @@ -285,12 +470,12 @@ export function listManagedCloudEnvironments(input: { · 1 unmodified line |
| 285 | 470 | clerkToken: input.clerkToken, |
| 286 | 471 | }) |
| 287 | 472 | .pipe( |
| 288 | | − Effect.mapError( |
| 289 | | − (cause) => |
| 290 | | − new CloudEnvironmentLinkError({ |
| 291 | | − message: "Could not list relay-managed environments.", |
| 292 | | − cause, |
| 293 | | − }), |
| 473 | + Effect.mapError((cause) => |
| 474 | + CloudEnvironmentLinkOperationError.fromManagedRelay({ |
| 475 | + action: "list relay-managed environments", |
| 476 | + relayUrl: configuredRelayUrl, |
| 477 | + cause, |
| 478 | + }), |
| 294 | 479 | ), |
| 295 | 480 | ); |
| 296 | 481 | }); |
| @@ -304,19 +489,18 @@ export function listCloudDevices(input: { · 7 unmodified lines |
| 304 | 489 | ManagedRelay.ManagedRelayClient |
| 305 | 490 | > { |
| 306 | 491 | return Effect.gen(function* () { |
| 307 | | − if (!relayUrl()) { |
| 308 | | − return yield* new CloudEnvironmentLinkError({ |
| 309 | | − message: "T3CODE_RELAY_URL is not configured.", |
| 310 | | − }); |
| 492 | + const configuredRelayUrl = relayUrl(); |
| 493 | + if (!configuredRelayUrl) { |
| 494 | + return yield* new CloudRelayUrlNotConfiguredError({}); |
| 311 | 495 | } |
| 312 | 496 | const relayClient = yield* ManagedRelay.ManagedRelayClient; |
| 313 | 497 | return yield* relayClient.listDevices({ clerkToken: input.clerkToken }).pipe( |
| 314 | | − Effect.mapError( |
| 315 | | − (cause) => |
| 316 | | − new CloudEnvironmentLinkError({ |
| 317 | | − message: "Could not list cloud devices.", |
| 318 | | − cause, |
| 319 | | − }), |
| 498 | + Effect.mapError((cause) => |
| 499 | + CloudEnvironmentLinkOperationError.fromManagedRelay({ |
| 500 | + action: "list cloud devices", |
| 501 | + relayUrl: configuredRelayUrl, |
| 502 | + cause, |
| 503 | + }), |
| 320 | 504 | ), |
| 321 | 505 | ); |
| 322 | 506 | }); |
| @@ -326,10 +510,20 @@ export function readPrimaryCloudLinkState(input: { · 3 unmodified lines |
| 326 | 510 | readonly target: CloudLinkTarget; |
| 327 | 511 | }): Effect.Effect<CloudLinkState | null, CloudEnvironmentLinkError, HttpClient.HttpClient> { |
| 328 | 512 | return Effect.gen(function* () { |
| 329 | | − const client = yield* makeEnvironmentHttpApiClient(input.target.httpBaseUrl); |
| 330 | | − return yield* client.connect |
| 331 | | − .linkState({ headers: {} }) |
| 332 | | − .pipe(Effect.mapError(environmentApiError("Could not read environment cloud link state."))); |
| 513 | + const client = yield* makeCloudEnvironmentHttpApiClient({ |
| 514 | + environmentId: input.target.environmentId, |
| 515 | + httpBaseUrl: input.target.httpBaseUrl, |
| 516 | + }); |
| 517 | + return yield* client.connect.linkState({ headers: {} }).pipe( |
| 518 | + Effect.mapError((cause) => |
| 519 | + CloudEnvironmentLinkOperationError.fromEnvironmentApi({ |
| 520 | + action: "read environment cloud link state", |
| 521 | + environmentId: input.target.environmentId, |
| 522 | + httpBaseUrl: input.target.httpBaseUrl, |
| 523 | + cause, |
| 524 | + }), |
| 525 | + ), |
| 526 | + ); |
| 333 | 527 | }).pipe(Effect.provide(primaryEnvironmentHttpLayer)); |
| 334 | 528 | } |
| 335 | 529 | |
| @@ -338,14 +532,24 @@ export function updatePrimaryCloudPreferences(input: { · 2 unmodified lines |
| 338 | 532 | readonly publishAgentActivity: boolean; |
| 339 | 533 | }): Effect.Effect<CloudLinkState, CloudEnvironmentLinkError, HttpClient.HttpClient> { |
| 340 | 534 | return Effect.gen(function* () { |
| 341 | | − const client = yield* makeEnvironmentHttpApiClient(input.target.httpBaseUrl); |
| 535 | + const client = yield* makeCloudEnvironmentHttpApiClient({ |
| 536 | + environmentId: input.target.environmentId, |
| 537 | + httpBaseUrl: input.target.httpBaseUrl, |
| 538 | + }); |
| 342 | 539 | return yield* client.connect |
| 343 | 540 | .preferences({ |
| 344 | 541 | headers: {}, |
| 345 | 542 | payload: input, |
| 346 | 543 | }) |
| 347 | 544 | .pipe( |
| 348 | | − Effect.mapError(environmentApiError("Could not update environment cloud preferences.")), |
| 545 | + Effect.mapError((cause) => |
| 546 | + CloudEnvironmentLinkOperationError.fromEnvironmentApi({ |
| 547 | + action: "update environment cloud preferences", |
| 548 | + environmentId: input.target.environmentId, |
| 549 | + httpBaseUrl: input.target.httpBaseUrl, |
| 550 | + cause, |
| 551 | + }), |
| 552 | + ), |
| 349 | 553 | ); |
| 350 | 554 | }).pipe(Effect.provide(primaryEnvironmentHttpLayer)); |
| 351 | 555 | } |
| @@ -359,10 +563,20 @@ export function unlinkPrimaryEnvironmentFromCloud(input: { · 7 unmodified lines |
| 359 | 563 | HttpClient.HttpClient | ManagedRelay.ManagedRelayClient |
| 360 | 564 | > { |
| 361 | 565 | return Effect.gen(function* () { |
| 362 | | − const client = yield* makeEnvironmentHttpApiClient(input.target.httpBaseUrl); |
| 363 | | − yield* client.connect |
| 364 | | − .unlink({ headers: {} }) |
| 365 | | − .pipe(Effect.mapError(environmentApiError("Could not unlink the environment from cloud."))); |
| 566 | + const client = yield* makeCloudEnvironmentHttpApiClient({ |
| 567 | + environmentId: input.target.environmentId, |
| 568 | + httpBaseUrl: input.target.httpBaseUrl, |
| 569 | + }); |
| 570 | + yield* client.connect.unlink({ headers: {} }).pipe( |
| 571 | + Effect.mapError((cause) => |
| 572 | + CloudEnvironmentLinkOperationError.fromEnvironmentApi({ |
| 573 | + action: "unlink the environment from cloud", |
| 574 | + environmentId: input.target.environmentId, |
| 575 | + httpBaseUrl: input.target.httpBaseUrl, |
| 576 | + cause, |
| 577 | + }), |
| 578 | + ), |
| 579 | + ); |
| 366 | 580 | |
| 367 | 581 | const configuredRelayUrl = relayUrl(); |
| 368 | 582 | if (configuredRelayUrl && input.clerkToken) { |
| @@ -373,11 +587,27 @@ export function unlinkPrimaryEnvironmentFromCloud(input: { · 4 unmodified lines |
| 373 | 587 | environmentId: EnvironmentId.make(input.target.environmentId), |
| 374 | 588 | }) |
| 375 | 589 | .pipe( |
| 376 | | − Effect.catch((cause) => |
| 377 | | − Effect.logWarning("Could not revoke cloud environment link after local unlink.", { |
| 590 | + Effect.catch((cause) => { |
| 591 | + const error = CloudEnvironmentLinkOperationError.fromManagedRelay({ |
| 592 | + action: "revoke the cloud environment link", |
| 593 | + environmentId: input.target.environmentId, |
| 594 | + relayUrl: configuredRelayUrl, |
| 378 | 595 | cause, |
| 379 | | − }), |
| 380 | | − ), |
| 596 | + }); |
| 597 | + return Effect.logWarning(error.message, { |
| 598 | + environmentId: input.target.environmentId, |
| 599 | + ...(error.relayUrlInputLength === undefined |
| 600 | + ? {} |
| 601 | + : { relayUrlInputLength: error.relayUrlInputLength }), |
| 602 | + ...(error.relayUrlProtocol === undefined |
| 603 | + ? {} |
| 604 | + : { relayUrlProtocol: error.relayUrlProtocol }), |
| 605 | + ...(error.relayUrlHostname === undefined |
| 606 | + ? {} |
| 607 | + : { relayUrlHostname: error.relayUrlHostname }), |
| 608 | + causeTag: cause._tag, |
| 609 | + }); |
| 610 | + }), |
| 381 | 611 | ); |
| 382 | 612 | } |
| 383 | 613 | }).pipe(Effect.provide(primaryEnvironmentHttpLayer)); |
| @@ -394,12 +624,19 @@ export function linkPrimaryEnvironmentToCloud(input: { · 10 unmodified lines |
| 394 | 624 | return Effect.gen(function* () { |
| 395 | 625 | const configuredRelayUrl = relayUrl(); |
| 396 | 626 | if (!configuredRelayUrl) { |
| 397 | | − return yield* new CloudEnvironmentLinkError({ |
| 398 | | − message: "T3CODE_RELAY_URL is not configured.", |
| 627 | + return yield* new CloudRelayUrlNotConfiguredError({ |
| 628 | + environmentId: input.target.environmentId, |
| 399 | 629 | }); |
| 400 | 630 | } |
| 631 | + const origin = yield* endpointOrigin({ |
| 632 | + environmentId: input.target.environmentId, |
| 633 | + httpBaseUrl: input.target.httpBaseUrl, |
| 634 | + }); |
| 635 | + const environmentClient = yield* makeCloudEnvironmentHttpApiClient({ |
| 636 | + environmentId: input.target.environmentId, |
| 637 | + httpBaseUrl: input.target.httpBaseUrl, |
| 638 | + }); |
| 401 | 639 | const relayClient = yield* ManagedRelay.ManagedRelayClient; |
| 402 | | − const environmentClient = yield* makeEnvironmentHttpApiClient(input.target.httpBaseUrl); |
| 403 | 640 | yield* ensureRelayClientAvailable(EnvironmentId.make(input.target.environmentId)); |
| 404 | 641 | |
| 405 | 642 | const challenge = yield* relayClient |
| @@ -412,10 +649,13 @@ export function linkPrimaryEnvironmentToCloud(input: { · 6 unmodified lines |
| 412 | 649 | }, |
| 413 | 650 | }) |
| 414 | 651 | .pipe( |
| 415 | | − Effect.mapError( |
| 416 | | − decodedRelayClientError( |
| 417 | | − `${configuredRelayUrl}/v1/client/environment-link-challenges failed`, |
| 418 | | − ), |
| 652 | + Effect.mapError((cause) => |
| 653 | + CloudEnvironmentLinkOperationError.fromManagedRelay({ |
| 654 | + action: "create an environment link challenge", |
| 655 | + environmentId: input.target.environmentId, |
| 656 | + relayUrl: configuredRelayUrl, |
| 657 | + cause, |
| 658 | + }), |
| 419 | 659 | ), |
| 420 | 660 | ); |
| 421 | 661 | const proof = yield* environmentClient.connect |
| @@ -429,10 +669,19 @@ export function linkPrimaryEnvironmentToCloud(input: { · 7 unmodified lines |
| 429 | 669 | wsBaseUrl: input.target.wsBaseUrl, |
| 430 | 670 | providerKind: MANAGED_ENDPOINT_PROVIDER_KIND, |
| 431 | 671 | }, |
| 432 | | − origin: endpointOrigin(input.target.httpBaseUrl), |
| 672 | + origin, |
| 433 | 673 | }, |
| 434 | 674 | }) |
| 435 | | − .pipe(Effect.mapError(environmentApiError("Could not obtain environment link proof."))); |
| 675 | + .pipe( |
| 676 | + Effect.mapError((cause) => |
| 677 | + CloudEnvironmentLinkOperationError.fromEnvironmentApi({ |
| 678 | + action: "obtain an environment link proof", |
| 679 | + environmentId: input.target.environmentId, |
| 680 | + httpBaseUrl: input.target.httpBaseUrl, |
| 681 | + cause, |
| 682 | + }), |
| 683 | + ), |
| 684 | + ); |
| 436 | 685 | const link = yield* relayClient |
| 437 | 686 | .linkEnvironment({ |
| 438 | 687 | clerkToken: input.clerkToken, |
| @@ -444,8 +693,13 @@ export function linkPrimaryEnvironmentToCloud(input: { · 5 unmodified lines |
| 444 | 693 | }, |
| 445 | 694 | }) |
| 446 | 695 | .pipe( |
| 447 | | − Effect.mapError( |
| 448 | | − decodedRelayClientError(`${configuredRelayUrl}/v1/client/environment-links failed`), |
| 696 | + Effect.mapError((cause) => |
| 697 | + CloudEnvironmentLinkOperationError.fromManagedRelay({ |
| 698 | + action: "link the environment", |
| 699 | + environmentId: input.target.environmentId, |
| 700 | + relayUrl: configuredRelayUrl, |
| 701 | + cause, |
| 702 | + }), |
| 449 | 703 | ), |
| 450 | 704 | ); |
| 451 | 705 | yield* ensureLinkedEnvironmentMatches({ |
| @@ -466,6 +720,15 @@ export function linkPrimaryEnvironmentToCloud(input: { · 14 unmodified lines |
| 466 | 720 | endpointRuntime: link.endpointRuntime, |
| 467 | 721 | }, |
| 468 | 722 | }) |
| 469 | | − .pipe(Effect.mapError(environmentApiError("Could not configure environment relay access."))); |
| 723 | + .pipe( |
| 724 | + Effect.mapError((cause) => |
| 725 | + CloudEnvironmentLinkOperationError.fromEnvironmentApi({ |
| 726 | + action: "configure environment relay access", |
| 727 | + environmentId: input.target.environmentId, |
| 728 | + httpBaseUrl: input.target.httpBaseUrl, |
| 729 | + cause, |
| 730 | + }), |
| 731 | + ), |
| 732 | + ); |
| 470 | 733 | }).pipe(Effect.provide(primaryEnvironmentHttpLayer)); |
| 471 | 734 | } |