From 1c4bd6ebb9d881b89c4dfcc49251e8c2831264ef Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 02:23:20 -0700 Subject: [PATCH 1/4] Structure cloud HTTP boundary errors Co-authored-by: codex --- apps/server/src/cloud/http.test.ts | 173 +++++++++--- apps/server/src/cloud/http.ts | 415 ++++++++++++++++++++++------- 2 files changed, 460 insertions(+), 128 deletions(-) diff --git a/apps/server/src/cloud/http.test.ts b/apps/server/src/cloud/http.test.ts index 0075f70a795..561418173a6 100644 --- a/apps/server/src/cloud/http.test.ts +++ b/apps/server/src/cloud/http.test.ts @@ -1,17 +1,27 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { describe, expect, it } from "@effect/vitest"; +import * as ConfigProvider from "effect/ConfigProvider"; import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as PlatformError from "effect/PlatformError"; import * as Tracer from "effect/Tracer"; -import { HttpClient, HttpServerRequest } from "effect/unstable/http"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import * as HttpClientError from "effect/unstable/http/HttpClientError"; +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; +import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"; +import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest"; import { RelayClientTracer } from "@t3tools/shared/relayTracing"; import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts"; import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; import * as ServerEnvironment from "../environment/ServerEnvironment.ts"; import * as CliTokenManager from "./CliTokenManager.ts"; -import { consumeCloudReplayGuards, reconcileDesiredCloudLink } from "./http.ts"; +import { + CloudRelayRequestError, + consumeCloudReplayGuards, + reconcileDesiredCloudLink, +} from "./http.ts"; import * as ManagedEndpointRuntime from "./ManagedEndpointRuntime.ts"; import { traceAuthenticatedRelayRequest, traceRelayRequest } from "./traceRelayRequest.ts"; @@ -42,6 +52,55 @@ function makeSecretStore( }; } +function reconcileWith(input: { + readonly getExisting: CliTokenManager.CloudCliTokenManager["Service"]["getExisting"]; + readonly httpClient?: HttpClient.HttpClient; + readonly env?: Readonly>; +}) { + return reconcileDesiredCloudLink("http://127.0.0.1:3774").pipe( + Effect.provideService( + ServerSecretStore.ServerSecretStore, + makeSecretStore(unusedSecretStoreOperation), + ), + Effect.provideService( + ServerEnvironment.ServerEnvironment, + ServerEnvironment.ServerEnvironment.of({ + getEnvironmentId: unusedSecretStoreOperation(), + getDescriptor: unusedSecretStoreOperation(), + }), + ), + Effect.provideService( + ManagedEndpointRuntime.CloudManagedEndpointRuntime, + ManagedEndpointRuntime.CloudManagedEndpointRuntime.of({ + applyConfig: unusedSecretStoreOperation, + } satisfies ManagedEndpointRuntime.CloudManagedEndpointRuntime["Service"]), + ), + Effect.provideService( + EnvironmentAuth.EnvironmentAuth, + EnvironmentAuth.EnvironmentAuth.of({} as EnvironmentAuth.EnvironmentAuth["Service"]), + ), + Effect.provideService( + CliTokenManager.CloudCliTokenManager, + CliTokenManager.CloudCliTokenManager.of({ + get: unusedSecretStoreOperation(), + getExisting: input.getExisting, + hasCredential: unusedSecretStoreOperation(), + clear: unusedSecretStoreOperation(), + }), + ), + Effect.provideService( + HttpClient.HttpClient, + input.httpClient ?? HttpClient.make(() => unusedSecretStoreOperation()), + ), + Effect.provide( + Layer.mergeAll( + NodeServices.layer, + ConfigProvider.layer(ConfigProvider.fromEnv({ env: input.env ?? {} })), + ), + ), + ); +} + it("preserves messages surfaced by cloud 500 responses", () => { const cause = new Error("cloud operation failed"); @@ -95,6 +154,45 @@ describe("consumeCloudReplayGuards", () => { ); }); +describe("CloudRelayRequestError", () => { + it("classifies response failures without deriving its message from the cause", () => { + const request = HttpClientRequest.post( + "https://relay.example.test/v1/client/environment-links", + ); + const response = HttpClientResponse.fromWeb( + request, + new Response("sensitive upstream response", { status: 502 }), + ); + const upstreamCause = new Error("sensitive upstream response details"); + const cause = new HttpClientError.HttpClientError({ + reason: new HttpClientError.StatusCodeError({ + request, + response, + cause: upstreamCause, + }), + }); + + const error = CloudRelayRequestError.fromClientFailure({ + operation: "create-environment-link", + url: request.url, + cause, + }); + + expect(error).toMatchObject({ + operation: "create-environment-link", + phase: "check-response-status", + method: "POST", + url: request.url, + responseStatus: 502, + cause, + }); + expect(error.message).toBe( + "T3 Connect relay create-environment-link failed during check-response-status with response status 502.", + ); + expect(error.message).not.toContain(upstreamCause.message); + }); +}); + describe("relay request tracing", () => { it.effect("does not accept an unauthenticated request trace parent", () => Effect.gen(function* () { @@ -162,48 +260,47 @@ describe("relay request tracing", () => { describe("reconcileDesiredCloudLink", () => { it.effect("requires stored CLI authorization without exposing an HTTP endpoint", () => Effect.gen(function* () { - const error = yield* Effect.flip(reconcileDesiredCloudLink("http://127.0.0.1:3774")); + const error = yield* Effect.flip( + reconcileWith({ getExisting: Effect.succeed(Option.none()) }), + ); expect(error).toMatchObject({ _tag: "EnvironmentHttpUnauthorizedError", message: "Run `t3 connect link` to authorize this environment.", }); - }).pipe( - Effect.provideService( - ServerSecretStore.ServerSecretStore, - makeSecretStore(unusedSecretStoreOperation), - ), - Effect.provideService( - ServerEnvironment.ServerEnvironment, - ServerEnvironment.ServerEnvironment.of({ - getEnvironmentId: unusedSecretStoreOperation(), - getDescriptor: unusedSecretStoreOperation(), + }), + ); + + it.effect("redacts relay transport failures behind a stable structural message", () => { + const transportCause = new Error("upstream included a sensitive database password"); + const httpClient = HttpClient.make((request) => + Effect.fail( + new HttpClientError.HttpClientError({ + reason: new HttpClientError.TransportError({ request, cause: transportCause }), }), ), - Effect.provideService( - ManagedEndpointRuntime.CloudManagedEndpointRuntime, - ManagedEndpointRuntime.CloudManagedEndpointRuntime.of({ - applyConfig: unusedSecretStoreOperation, - } satisfies ManagedEndpointRuntime.CloudManagedEndpointRuntime["Service"]), - ), - Effect.provideService( - EnvironmentAuth.EnvironmentAuth, - EnvironmentAuth.EnvironmentAuth.of({} as EnvironmentAuth.EnvironmentAuth["Service"]), - ), - Effect.provideService( - CliTokenManager.CloudCliTokenManager, - CliTokenManager.CloudCliTokenManager.of({ - get: unusedSecretStoreOperation(), - getExisting: Effect.succeed(Option.none()), - hasCredential: unusedSecretStoreOperation(), - clear: unusedSecretStoreOperation(), + ); + + return Effect.gen(function* () { + const error = yield* Effect.flip( + reconcileWith({ + getExisting: Effect.succeed( + Option.some({ + accessToken: "access-token", + refreshToken: "refresh-token", + expiresAtEpochMs: Number.MAX_SAFE_INTEGER, + }), + ), + httpClient, + env: { T3CODE_RELAY_URL: "https://relay.example.test" }, }), - ), - Effect.provideService( - HttpClient.HttpClient, - HttpClient.make(() => unusedSecretStoreOperation()), - ), - Effect.provide(NodeServices.layer), - ), - ); + ); + + expect(error).toMatchObject({ + _tag: "EnvironmentHttpInternalServerError", + message: "T3 Connect relay create-link-challenge failed during send-request.", + }); + expect(error.message).not.toContain(transportCause.message); + }); + }); }); diff --git a/apps/server/src/cloud/http.ts b/apps/server/src/cloud/http.ts index fc2adca9fbc..acb78ea242a 100644 --- a/apps/server/src/cloud/http.ts +++ b/apps/server/src/cloud/http.ts @@ -48,8 +48,13 @@ import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import * as HttpEffect from "effect/unstable/http/HttpEffect"; -import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http"; -import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"; +import * as HttpBody from "effect/unstable/http/HttpBody"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import * as HttpClientError from "effect/unstable/http/HttpClientError"; +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; +import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"; +import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder"; import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts"; @@ -85,6 +90,104 @@ const CLOUD_CREDENTIAL_RESPONSE_HEADERS = { pragma: "no-cache", } as const; +const CloudRelayRequestOperation = Schema.Literals([ + "create-link-challenge", + "create-environment-link", +]); + +const CloudRelayRequestPhase = Schema.Literals([ + "encode-request", + "send-request", + "check-response-status", + "decode-response", +]); + +export class CloudRelayConfigurationError extends Schema.TaggedErrorClass()( + "CloudRelayConfigurationError", + { + configKey: Schema.Literal("T3CODE_RELAY_URL"), + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `${this.configKey} must be configured as a secure absolute HTTPS origin.`; + } +} + +export class CloudRelayRequestError extends Schema.TaggedErrorClass()( + "CloudRelayRequestError", + { + operation: CloudRelayRequestOperation, + phase: CloudRelayRequestPhase, + method: Schema.Literal("POST"), + url: Schema.String, + responseStatus: Schema.optional(Schema.Number), + cause: Schema.Defect(), + }, +) { + static fromClientFailure(input: { + readonly operation: CloudRelayRequestError["operation"]; + readonly url: string; + readonly cause: HttpBody.HttpBodyError | HttpClientError.HttpClientError | Schema.SchemaError; + readonly responseStatus?: number; + }): CloudRelayRequestError { + if (input.cause._tag === "SchemaError") { + return new CloudRelayRequestError({ + operation: input.operation, + phase: "decode-response", + method: "POST", + url: input.url, + ...(input.responseStatus === undefined ? {} : { responseStatus: input.responseStatus }), + cause: input.cause, + }); + } + + if (!HttpClientError.isHttpClientError(input.cause)) { + return new CloudRelayRequestError({ + operation: input.operation, + phase: "encode-request", + method: "POST", + url: input.url, + cause: input.cause, + }); + } + + const phase: CloudRelayRequestError["phase"] = (() => { + switch (input.cause.reason._tag) { + case "EncodeError": + return "encode-request"; + case "TransportError": + case "InvalidUrlError": + return "send-request"; + case "StatusCodeError": + return "check-response-status"; + case "DecodeError": + case "EmptyBodyError": + return "decode-response"; + } + })(); + + return new CloudRelayRequestError({ + operation: input.operation, + phase, + method: "POST", + url: input.url, + ...(input.cause.response === undefined + ? input.responseStatus === undefined + ? {} + : { responseStatus: input.responseStatus } + : { responseStatus: input.cause.response.status }), + cause: input.cause, + }); + } + + override get message(): string { + const responseStatus = + this.responseStatus === undefined ? "" : ` with response status ${this.responseStatus}`; + return `T3 Connect relay ${this.operation} failed during ${this.phase}${responseStatus}.`; + } +} + const appendCloudCredentialResponseHeaders = HttpEffect.appendPreResponseHandler( (_request, response) => Effect.succeed(HttpServerResponse.setHeaders(response, CLOUD_CREDENTIAL_RESPONSE_HEADERS)), @@ -97,14 +200,12 @@ const failEnvironmentCloudInternalError = Effect.flatMap(() => Effect.fail(new EnvironmentHttpInternalServerError({ message }))), ); -const failCloudCliTokenManagerError = (error: CliTokenManager.CloudCliTokenManagerError) => - failEnvironmentCloudInternalError(error.message)(error); - const requireRelayUrl = relayUrlConfig.pipe( Effect.mapError( - () => - new EnvironmentHttpInternalServerError({ - message: "T3CODE_RELAY_URL must be configured as a secure absolute HTTPS origin.", + (cause) => + new CloudRelayConfigurationError({ + configKey: "T3CODE_RELAY_URL", + cause, }), ), ); @@ -126,11 +227,12 @@ export function consumeCloudReplayGuards(input: { input.names.map((name) => input.secrets.create(name, input.value).pipe( Effect.as(true), - Effect.catchIf(ServerSecretStore.isSecretStoreError, (error) => - ServerSecretStore.isSecretAlreadyExistsError(error) - ? Effect.succeed(false) - : Effect.fail(error), - ), + Effect.catchTags({ + SecretStorePersistError: (error) => + ServerSecretStore.isSecretAlreadyExistsError(error) + ? Effect.succeed(false) + : Effect.fail(error), + }), ), ), { concurrency: input.names.length }, @@ -207,7 +309,7 @@ function validateRelayConfigPayload( function validateLinkedCloudUser(input: { readonly secrets: ServerSecretStore.ServerSecretStore["Service"]; readonly cloudUserId: string; -}): Effect.Effect { +}) { return input.secrets.get(CLOUD_LINKED_USER_ID).pipe( Effect.mapError( (cause) => @@ -232,9 +334,7 @@ function validateLinkedCloudUser(input: { ); } -function readInstalledCloudUserId( - secrets: ServerSecretStore.ServerSecretStore["Service"], -): Effect.Effect { +function readInstalledCloudUserId(secrets: ServerSecretStore.ServerSecretStore["Service"]) { return secrets.get(CLOUD_LINKED_USER_ID).pipe( Effect.mapError( (cause) => @@ -409,17 +509,26 @@ const cloudLinkProofHandler = Effect.fn("environment.cloud.linkProof")( yield* appendCloudCredentialResponseHeaders; return proof satisfies RelayEnvironmentLinkProof; }, - Effect.catchIf(EnvironmentAuth.isServerAuthInternalError, (error) => - failEnvironmentCloudInternalError(error.message)(error), - ), - Effect.catchIf( - ServerSecretStore.isSecretStoreError, - failEnvironmentCloudInternalError("Could not generate environment link proof."), - ), - Effect.catchTag( - "PlatformError", - failEnvironmentCloudInternalError("Could not generate environment link proof."), - ), + Effect.catchTags({ + ServerAuthCloudLinkJwtSigningError: (error) => + failEnvironmentCloudInternalError(error.message)(error), + SecretStoreReadError: failEnvironmentCloudInternalError( + "Could not generate environment link proof.", + ), + SecretStoreDecodeError: failEnvironmentCloudInternalError( + "Could not generate environment link proof.", + ), + SecretStoreEncodeError: failEnvironmentCloudInternalError( + "Could not generate environment link proof.", + ), + SecretStorePersistError: failEnvironmentCloudInternalError( + "Could not generate environment link proof.", + ), + SecretStoreConcurrentReadError: failEnvironmentCloudInternalError( + "Could not generate environment link proof.", + ), + PlatformError: failEnvironmentCloudInternalError("Could not generate environment link proof."), + }), ); const applyCloudRelayConfig = Effect.fn("environment.cloud.applyRelayConfig")(function* ( @@ -472,22 +581,28 @@ const cloudRelayConfigHandler = Effect.fn("environment.cloud.relayConfig")( yield* requireEnvironmentScope(AuthRelayWriteScope); return yield* applyCloudRelayConfig(dependencies, payload); }, - Effect.catchIf(EnvironmentAuth.isServerAuthInternalError, (error) => - failEnvironmentCloudInternalError(error.message)(error), - ), - Effect.catchIf( - ServerSecretStore.isSecretStoreError, - failEnvironmentCloudInternalError("Could not persist environment relay configuration."), - ), - Effect.catchTag( - "SchemaError", - failEnvironmentCloudInternalError("Could not persist environment relay configuration."), - ), + Effect.catchTags({ + ServerAuthLinkedCloudAccountVerificationError: (error) => + failEnvironmentCloudInternalError(error.message)(error), + SecretStoreTemporaryPathGenerationError: failEnvironmentCloudInternalError( + "Could not persist environment relay configuration.", + ), + SecretStorePersistError: failEnvironmentCloudInternalError( + "Could not persist environment relay configuration.", + ), + SecretStoreRemoveError: failEnvironmentCloudInternalError( + "Could not persist environment relay configuration.", + ), + SchemaError: failEnvironmentCloudInternalError( + "Could not persist environment relay configuration.", + ), + }), ); const relayClientRequest = ( dependencies: CloudHttpDependencies, input: { + readonly operation: CloudRelayRequestError["operation"]; readonly url: string; readonly token: string; readonly payload: unknown; @@ -497,14 +612,46 @@ const relayClientRequest = ( HttpClientRequest.post(input.url).pipe( HttpClientRequest.bearerToken(input.token), HttpClientRequest.bodyJson(input.payload), - Effect.flatMap(dependencies.httpClient.execute), - Effect.flatMap(HttpClientResponse.filterStatusOk), - Effect.flatMap(HttpClientResponse.schemaBodyJson(input.schema)), - Effect.mapError( - (cause) => - new EnvironmentHttpInternalServerError({ - message: `T3 Connect relay request failed: ${String(cause)}`, - }), + Effect.mapError((cause) => + CloudRelayRequestError.fromClientFailure({ + operation: input.operation, + url: input.url, + cause, + }), + ), + Effect.flatMap((request) => + dependencies.httpClient.execute(request).pipe( + Effect.mapError((cause) => + CloudRelayRequestError.fromClientFailure({ + operation: input.operation, + url: input.url, + cause, + }), + ), + ), + ), + Effect.flatMap((response) => + HttpClientResponse.filterStatusOk(response).pipe( + Effect.mapError((cause) => + CloudRelayRequestError.fromClientFailure({ + operation: input.operation, + url: input.url, + cause, + }), + ), + ), + ), + Effect.flatMap((response) => + HttpClientResponse.schemaBodyJson(input.schema)(response).pipe( + Effect.mapError((cause) => + CloudRelayRequestError.fromClientFailure({ + operation: input.operation, + url: input.url, + responseStatus: response.status, + cause, + }), + ), + ), ), withRelayClientTracing, ); @@ -539,6 +686,7 @@ const reconcileDesiredCloudLinkWith = Effect.fn("environment.cloud.reconcileDesi ); const relayUrl = yield* requireRelayUrl; const challenge = yield* relayClientRequest(dependencies, { + operation: "create-link-challenge", url: `${relayUrl}/v1/client/environment-link-challenges`, token: token.accessToken, payload: { @@ -566,6 +714,7 @@ const reconcileDesiredCloudLinkWith = Effect.fn("environment.cloud.reconcileDesi localOrigin, ); const link = yield* relayClientRequest(dependencies, { + operation: "create-environment-link", url: `${relayUrl}/v1/client/environment-links`, token: token.accessToken, payload: { @@ -586,16 +735,49 @@ const reconcileDesiredCloudLinkWith = Effect.fn("environment.cloud.reconcileDesi endpointRuntime: link.endpointRuntime, }); }, - Effect.catchIf( - ServerSecretStore.isSecretStoreError, - failEnvironmentCloudInternalError("Could not persist desired T3 Connect link state."), - ), Effect.catchTags({ - CloudCliCredentialRemovalError: failCloudCliTokenManagerError, - CloudCliCredentialRefreshError: failCloudCliTokenManagerError, - CloudCliCredentialReadError: failCloudCliTokenManagerError, - CloudCliAuthorizationError: failCloudCliTokenManagerError, - CloudCliAuthorizationTimeoutError: failCloudCliTokenManagerError, + ServerAuthLinkedCloudAccountVerificationError: (error) => + failEnvironmentCloudInternalError(error.message)(error), + ServerAuthCloudLinkJwtSigningError: (error) => + failEnvironmentCloudInternalError(error.message)(error), + SecretStoreReadError: failEnvironmentCloudInternalError( + "Could not persist desired T3 Connect link state.", + ), + SecretStoreTemporaryPathGenerationError: failEnvironmentCloudInternalError( + "Could not persist desired T3 Connect link state.", + ), + SecretStorePersistError: failEnvironmentCloudInternalError( + "Could not persist desired T3 Connect link state.", + ), + SecretStoreRemoveError: failEnvironmentCloudInternalError( + "Could not persist desired T3 Connect link state.", + ), + SecretStoreDecodeError: failEnvironmentCloudInternalError( + "Could not persist desired T3 Connect link state.", + ), + SecretStoreEncodeError: failEnvironmentCloudInternalError( + "Could not persist desired T3 Connect link state.", + ), + SecretStoreConcurrentReadError: failEnvironmentCloudInternalError( + "Could not persist desired T3 Connect link state.", + ), + SchemaError: failEnvironmentCloudInternalError( + "Could not persist desired T3 Connect link state.", + ), + PlatformError: failEnvironmentCloudInternalError( + "Could not persist desired T3 Connect link state.", + ), + CloudRelayConfigurationError: (error) => + failEnvironmentCloudInternalError(error.message)(error), + CloudRelayRequestError: (error) => failEnvironmentCloudInternalError(error.message)(error), + CloudCliCredentialRemovalError: (error) => + failEnvironmentCloudInternalError(error.message)(error), + CloudCliCredentialRefreshError: (error) => + failEnvironmentCloudInternalError(error.message)(error), + CloudCliCredentialReadError: (error) => failEnvironmentCloudInternalError(error.message)(error), + CloudCliAuthorizationError: (error) => failEnvironmentCloudInternalError(error.message)(error), + CloudCliAuthorizationTimeoutError: (error) => + failEnvironmentCloudInternalError(error.message)(error), }), ); @@ -633,10 +815,11 @@ const cloudLinkStateHandler = Effect.fn("environment.cloud.linkState")( yield* requireEnvironmentScope(AuthRelayReadScope); return yield* readCloudLinkState(dependencies); }, - Effect.catchIf( - ServerSecretStore.isSecretStoreError, - failEnvironmentCloudInternalError("Could not read environment relay configuration."), - ), + Effect.catchTags({ + SecretStoreReadError: failEnvironmentCloudInternalError( + "Could not read environment relay configuration.", + ), + }), ); const cloudUnlinkHandler = Effect.fn("environment.cloud.unlink")( @@ -658,10 +841,17 @@ const cloudUnlinkHandler = Effect.fn("environment.cloud.unlink")( yield* setCliDesiredCloudLink(false); return { ok: true, endpointRuntimeStatus } satisfies EnvironmentCloudRelayConfigResult; }, - Effect.catchIf( - ServerSecretStore.isSecretStoreError, - failEnvironmentCloudInternalError("Could not remove environment relay configuration."), - ), + Effect.catchTags({ + SecretStoreTemporaryPathGenerationError: failEnvironmentCloudInternalError( + "Could not remove environment relay configuration.", + ), + SecretStorePersistError: failEnvironmentCloudInternalError( + "Could not remove environment relay configuration.", + ), + SecretStoreRemoveError: failEnvironmentCloudInternalError( + "Could not remove environment relay configuration.", + ), + }), ); const cloudPreferencesHandler = Effect.fn("environment.cloud.preferences")( @@ -676,10 +866,17 @@ const cloudPreferencesHandler = Effect.fn("environment.cloud.preferences")( ); return yield* readCloudLinkState(dependencies); }, - Effect.catchIf( - ServerSecretStore.isSecretStoreError, - failEnvironmentCloudInternalError("Could not persist environment cloud preferences."), - ), + Effect.catchTags({ + SecretStoreReadError: failEnvironmentCloudInternalError( + "Could not persist environment cloud preferences.", + ), + SecretStoreTemporaryPathGenerationError: failEnvironmentCloudInternalError( + "Could not persist environment cloud preferences.", + ), + SecretStorePersistError: failEnvironmentCloudInternalError( + "Could not persist environment cloud preferences.", + ), + }), ); const cloudEnvironmentHealthHandler = Effect.fn("environment.cloud.health")( @@ -787,17 +984,34 @@ const cloudEnvironmentHealthHandler = Effect.fn("environment.cloud.health")( yield* appendCloudCredentialResponseHeaders; return response; }, - Effect.catchIf(EnvironmentAuth.isServerAuthInternalError, (error) => - failEnvironmentCloudInternalError(error.message)(error), - ), - Effect.catchIf( - ServerSecretStore.isSecretStoreError, - failEnvironmentCloudInternalError("Could not answer cloud health request."), - ), - Effect.catchTag( - "PlatformError", - failEnvironmentCloudInternalError("Could not answer cloud health request."), - ), + Effect.catchTags({ + ServerAuthLinkedCloudAccountReadError: (error) => + failEnvironmentCloudInternalError(error.message)(error), + ServerAuthLinkedCloudAccountMissingError: (error) => + failEnvironmentCloudInternalError(error.message)(error), + ServerAuthCloudMintPublicKeyMissingError: (error) => + failEnvironmentCloudInternalError(error.message)(error), + ServerAuthCloudRelayIssuerMissingError: (error) => + failEnvironmentCloudInternalError(error.message)(error), + ServerAuthCloudHealthJwtSigningError: (error) => + failEnvironmentCloudInternalError(error.message)(error), + SecretStoreReadError: failEnvironmentCloudInternalError( + "Could not answer cloud health request.", + ), + SecretStorePersistError: failEnvironmentCloudInternalError( + "Could not answer cloud health request.", + ), + SecretStoreDecodeError: failEnvironmentCloudInternalError( + "Could not answer cloud health request.", + ), + SecretStoreEncodeError: failEnvironmentCloudInternalError( + "Could not answer cloud health request.", + ), + SecretStoreConcurrentReadError: failEnvironmentCloudInternalError( + "Could not answer cloud health request.", + ), + PlatformError: failEnvironmentCloudInternalError("Could not answer cloud health request."), + }), ); const cloudMintCredentialHandler = Effect.fn("environment.cloud.mintCredential")( @@ -908,17 +1122,38 @@ const cloudMintCredentialHandler = Effect.fn("environment.cloud.mintCredential") yield* appendCloudCredentialResponseHeaders; return response; }, - Effect.catchIf(EnvironmentAuth.isServerAuthInternalError, (error) => - failEnvironmentCloudInternalError(error.message)(error), - ), - Effect.catchIf( - ServerSecretStore.isSecretStoreError, - failEnvironmentCloudInternalError("Could not issue cloud connection credential."), - ), - Effect.catchTag( - "PlatformError", - failEnvironmentCloudInternalError("Could not issue cloud connection credential."), - ), + Effect.catchTags({ + ServerAuthLinkedCloudAccountReadError: (error) => + failEnvironmentCloudInternalError(error.message)(error), + ServerAuthLinkedCloudAccountMissingError: (error) => + failEnvironmentCloudInternalError(error.message)(error), + ServerAuthCloudMintPublicKeyMissingError: (error) => + failEnvironmentCloudInternalError(error.message)(error), + ServerAuthCloudRelayIssuerMissingError: (error) => + failEnvironmentCloudInternalError(error.message)(error), + ServerAuthPairingLinkCreationError: (error) => + failEnvironmentCloudInternalError(error.message)(error), + ServerAuthCloudMintJwtSigningError: (error) => + failEnvironmentCloudInternalError(error.message)(error), + SecretStoreReadError: failEnvironmentCloudInternalError( + "Could not issue cloud connection credential.", + ), + SecretStorePersistError: failEnvironmentCloudInternalError( + "Could not issue cloud connection credential.", + ), + SecretStoreDecodeError: failEnvironmentCloudInternalError( + "Could not issue cloud connection credential.", + ), + SecretStoreEncodeError: failEnvironmentCloudInternalError( + "Could not issue cloud connection credential.", + ), + SecretStoreConcurrentReadError: failEnvironmentCloudInternalError( + "Could not issue cloud connection credential.", + ), + PlatformError: failEnvironmentCloudInternalError( + "Could not issue cloud connection credential.", + ), + }), ); export const connectHttpApiLayer = HttpApiBuilder.group( From fe10a0bae6c8471f2956d2ccfc014a53338bc285 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 10:09:31 -0700 Subject: [PATCH 2/4] Redact relay request targets at the cloud boundary Co-authored-by: codex --- apps/server/src/cloud/http.test.ts | 44 ++++++++++++++++++++--- apps/server/src/cloud/http.ts | 29 ++++++++------- packages/contracts/src/environmentHttp.ts | 5 +++ 3 files changed, 62 insertions(+), 16 deletions(-) diff --git a/apps/server/src/cloud/http.test.ts b/apps/server/src/cloud/http.test.ts index 561418173a6..c20552c8a64 100644 --- a/apps/server/src/cloud/http.test.ts +++ b/apps/server/src/cloud/http.test.ts @@ -5,12 +5,14 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as PlatformError from "effect/PlatformError"; +import * as Schema from "effect/Schema"; import * as Tracer from "effect/Tracer"; import * as HttpClient from "effect/unstable/http/HttpClient"; import * as HttpClientError from "effect/unstable/http/HttpClientError"; import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"; import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest"; +import { EnvironmentHttpInternalServerError } from "@t3tools/contracts"; import { RelayClientTracer } from "@t3tools/shared/relayTracing"; import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts"; @@ -156,9 +158,9 @@ describe("consumeCloudReplayGuards", () => { describe("CloudRelayRequestError", () => { it("classifies response failures without deriving its message from the cause", () => { - const request = HttpClientRequest.post( - "https://relay.example.test/v1/client/environment-links", - ); + const requestUrl = + "https://relay-user:relay-password@relay.example.test/private/environment-links?token=relay-secret#relay-fragment"; + const request = HttpClientRequest.post(requestUrl); const response = HttpClientResponse.fromWeb( request, new Response("sensitive upstream response", { status: 502 }), @@ -182,14 +184,43 @@ describe("CloudRelayRequestError", () => { operation: "create-environment-link", phase: "check-response-status", method: "POST", - url: request.url, + requestUrlInputLength: requestUrl.length, + requestUrlProtocol: "https:", + requestUrlHostname: "relay.example.test", responseStatus: 502, cause, }); + expect(error.cause).toBe(cause); + expect(error).not.toHaveProperty("url"); expect(error.message).toBe( "T3 Connect relay create-environment-link failed during check-response-status with response status 502.", ); expect(error.message).not.toContain(upstreamCause.message); + for (const secret of [ + "relay-user", + "relay-password", + "/private/environment-links", + "relay-secret", + "relay-fragment", + ]) { + expect(error.message).not.toContain(secret); + expect(Object.values(error).join(" ")).not.toContain(secret); + } + }); +}); + +it("preserves internal causes without encoding them into HTTP error bodies", () => { + const cause = new Error("private upstream detail"); + const error = new EnvironmentHttpInternalServerError({ + message: "Stable public failure.", + cause, + }); + const encodeError = Schema.encodeUnknownSync(EnvironmentHttpInternalServerError); + + expect(error.cause).toBe(cause); + expect(encodeError(error)).toEqual({ + _tag: "EnvironmentHttpInternalServerError", + message: "Stable public failure.", }); }); @@ -299,6 +330,11 @@ describe("reconcileDesiredCloudLink", () => { expect(error).toMatchObject({ _tag: "EnvironmentHttpInternalServerError", message: "T3 Connect relay create-link-challenge failed during send-request.", + cause: { + _tag: "CloudRelayRequestError", + operation: "create-link-challenge", + phase: "send-request", + }, }); expect(error.message).not.toContain(transportCause.message); }); diff --git a/apps/server/src/cloud/http.ts b/apps/server/src/cloud/http.ts index acb78ea242a..dc0626c89da 100644 --- a/apps/server/src/cloud/http.ts +++ b/apps/server/src/cloud/http.ts @@ -41,6 +41,7 @@ import { verifyRelayJwt, } from "@t3tools/shared/relayJwt"; import { isSecureRelayUrl } from "@t3tools/shared/relayUrl"; +import { getUrlDiagnostics } from "@t3tools/shared/urlDiagnostics"; import * as DateTime from "effect/DateTime"; import * as Crypto from "effect/Crypto"; import * as Duration from "effect/Duration"; @@ -120,7 +121,9 @@ export class CloudRelayRequestError extends Schema.TaggedErrorClass (cause: unknown): Effect.Effect => - Effect.logError(message, { cause }).pipe( - Effect.flatMap(() => Effect.fail(new EnvironmentHttpInternalServerError({ message }))), + Effect.logError(message).pipe( + Effect.flatMap(() => Effect.fail(new EnvironmentHttpInternalServerError({ message, cause }))), ); const requireRelayUrl = relayUrlConfig.pipe( diff --git a/packages/contracts/src/environmentHttp.ts b/packages/contracts/src/environmentHttp.ts index adc5f149cba..e2e9284c6a1 100644 --- a/packages/contracts/src/environmentHttp.ts +++ b/packages/contracts/src/environmentHttp.ts @@ -212,6 +212,11 @@ export class EnvironmentHttpInternalServerError extends Schema.TaggedErrorClass< }, { httpApiStatus: 500 }, ) { + // @effect-diagnostics-next-line overriddenSchemaConstructor:off + constructor(props: { readonly message: string; readonly cause?: unknown }) { + super(props as any); + } + [HttpServerRespondable.symbol]() { return HttpServerResponse.schemaJson(EnvironmentHttpInternalServerError)(this, { status: 500 }); } From 16dda3608297d028197dd06c26b72c3d7c1903e1 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 10:18:09 -0700 Subject: [PATCH 3/4] Log safe cause tags at the cloud HTTP boundary Co-authored-by: codex --- apps/server/src/cloud/http.test.ts | 16 +++++++++++++++- apps/server/src/cloud/http.ts | 15 ++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/apps/server/src/cloud/http.test.ts b/apps/server/src/cloud/http.test.ts index c20552c8a64..7cd828a0041 100644 --- a/apps/server/src/cloud/http.test.ts +++ b/apps/server/src/cloud/http.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from "@effect/vitest"; import * as ConfigProvider from "effect/ConfigProvider"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; +import * as Logger from "effect/Logger"; import * as Option from "effect/Option"; import * as PlatformError from "effect/PlatformError"; import * as Schema from "effect/Schema"; @@ -304,6 +305,10 @@ describe("reconcileDesiredCloudLink", () => { it.effect("redacts relay transport failures behind a stable structural message", () => { const transportCause = new Error("upstream included a sensitive database password"); + const capturedLogs: Array> = []; + const logger = Logger.make(({ message }) => { + capturedLogs.push(Array.isArray(message) ? message : [message]); + }); const httpClient = HttpClient.make((request) => Effect.fail( new HttpClientError.HttpClientError({ @@ -324,7 +329,7 @@ describe("reconcileDesiredCloudLink", () => { ), httpClient, env: { T3CODE_RELAY_URL: "https://relay.example.test" }, - }), + }).pipe(Effect.provide(Logger.layer([logger], { mergeWithExisting: false }))), ); expect(error).toMatchObject({ @@ -337,6 +342,15 @@ describe("reconcileDesiredCloudLink", () => { }, }); expect(error.message).not.toContain(transportCause.message); + expect(capturedLogs).toHaveLength(1); + const logFields = capturedLogs[0]?.find( + (value): value is Record => typeof value === "object" && value !== null, + ); + expect(logFields).toMatchObject({ causeTag: "CloudRelayRequestError" }); + expect(logFields).not.toHaveProperty("cause"); + expect(capturedLogs[0]?.filter((value) => typeof value === "string").join(" ")).not.toContain( + transportCause.message, + ); }); }); }); diff --git a/apps/server/src/cloud/http.ts b/apps/server/src/cloud/http.ts index dc0626c89da..3a5df351895 100644 --- a/apps/server/src/cloud/http.ts +++ b/apps/server/src/cloud/http.ts @@ -198,10 +198,23 @@ const appendCloudCredentialResponseHeaders = HttpEffect.appendPreResponseHandler Effect.succeed(HttpServerResponse.setHeaders(response, CLOUD_CREDENTIAL_RESPONSE_HEADERS)), ); +function errorDiagnosticTag(cause: unknown): string { + if ( + typeof cause === "object" && + cause !== null && + "_tag" in cause && + typeof cause._tag === "string" + ) { + return cause._tag; + } + if (cause instanceof Error) return cause.name; + return typeof cause; +} + const failEnvironmentCloudInternalError = (message: string) => (cause: unknown): Effect.Effect => - Effect.logError(message).pipe( + Effect.logError(message, { causeTag: errorDiagnosticTag(cause) }).pipe( Effect.flatMap(() => Effect.fail(new EnvironmentHttpInternalServerError({ message, cause }))), ); From f785979c028412eb9b213a0095f450ec9c2638e4 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 11:35:15 -0700 Subject: [PATCH 4/4] [codex] Structure environment cloud HTTP errors (#3392) Co-authored-by: codex --- .../features/cloud/linkEnvironment.test.ts | 5 +- apps/server/src/cloud/http.test.ts | 38 ++- apps/server/src/cloud/http.ts | 273 ++++++++---------- .../environments/EnvironmentConnector.test.ts | 1 + packages/contracts/src/environmentHttp.ts | 218 +++++++++++++- 5 files changed, 367 insertions(+), 168 deletions(-) diff --git a/apps/mobile/src/features/cloud/linkEnvironment.test.ts b/apps/mobile/src/features/cloud/linkEnvironment.test.ts index b9ab3aeab05..2e3476cdbf4 100644 --- a/apps/mobile/src/features/cloud/linkEnvironment.test.ts +++ b/apps/mobile/src/features/cloud/linkEnvironment.test.ts @@ -607,7 +607,8 @@ describe("mobile cloud link environment client", () => { Response.json( { _tag: "EnvironmentHttpUnauthorizedError", - message: "Invalid environment bearer session.", + reason: "cloud_cli_authorization_required", + message: "Run `t3 connect link` to authorize this environment.", }, { status: 401 }, ), @@ -623,7 +624,7 @@ describe("mobile cloud link environment client", () => { ).pipe(Effect.flip); expect(error._tag).toBe("CloudEnvironmentLinkError"); expect(error.message).toBe( - "Could not obtain environment link proof: Invalid environment bearer session.", + "Could not obtain environment link proof: Run `t3 connect link` to authorize this environment.", ); expect(fetchMock).toHaveBeenCalledTimes(2); }), diff --git a/apps/server/src/cloud/http.test.ts b/apps/server/src/cloud/http.test.ts index 7cd828a0041..c623d79e550 100644 --- a/apps/server/src/cloud/http.test.ts +++ b/apps/server/src/cloud/http.test.ts @@ -28,6 +28,13 @@ import { import * as ManagedEndpointRuntime from "./ManagedEndpointRuntime.ts"; import { traceAuthenticatedRelayRequest, traceRelayRequest } from "./traceRelayRequest.ts"; +const encodeEnvironmentHttpInternalServerError = Schema.encodeUnknownSync( + EnvironmentHttpInternalServerError, +); +const decodeEnvironmentHttpInternalServerError = Schema.decodeUnknownSync( + EnvironmentHttpInternalServerError, +); + const storeFailure = (tag: "AlreadyExists" | "PermissionDenied") => new ServerSecretStore.SecretStorePersistError({ operation: "create", @@ -210,21 +217,31 @@ describe("CloudRelayRequestError", () => { }); }); -it("preserves internal causes without encoding them into HTTP error bodies", () => { +it("keeps internal causes out of encoded HTTP error bodies", () => { const cause = new Error("private upstream detail"); const error = new EnvironmentHttpInternalServerError({ - message: "Stable public failure.", + operation: "generate_link_proof", cause, }); - const encodeError = Schema.encodeUnknownSync(EnvironmentHttpInternalServerError); expect(error.cause).toBe(cause); - expect(encodeError(error)).toEqual({ + expect(encodeEnvironmentHttpInternalServerError(error)).toEqual({ _tag: "EnvironmentHttpInternalServerError", - message: "Stable public failure.", + operation: "generate_link_proof", + message: "Could not generate environment link proof.", }); }); +it("decodes legacy message-only HTTP errors during rolling deployments", () => { + const error = decodeEnvironmentHttpInternalServerError({ + _tag: "EnvironmentHttpInternalServerError", + message: "Legacy environment server failure.", + }); + + expect(error.operation).toBeUndefined(); + expect(error.message).toBe("Legacy environment server failure."); +}); + describe("relay request tracing", () => { it.effect("does not accept an unauthenticated request trace parent", () => Effect.gen(function* () { @@ -298,6 +315,7 @@ describe("reconcileDesiredCloudLink", () => { expect(error).toMatchObject({ _tag: "EnvironmentHttpUnauthorizedError", + reason: "cloud_cli_authorization_required", message: "Run `t3 connect link` to authorize this environment.", }); }), @@ -334,6 +352,9 @@ describe("reconcileDesiredCloudLink", () => { expect(error).toMatchObject({ _tag: "EnvironmentHttpInternalServerError", + operation: "relay_request", + relayOperation: "create-link-challenge", + relayPhase: "send-request", message: "T3 Connect relay create-link-challenge failed during send-request.", cause: { _tag: "CloudRelayRequestError", @@ -346,7 +367,12 @@ describe("reconcileDesiredCloudLink", () => { const logFields = capturedLogs[0]?.find( (value): value is Record => typeof value === "object" && value !== null, ); - expect(logFields).toMatchObject({ causeTag: "CloudRelayRequestError" }); + expect(logFields).toMatchObject({ + operation: "relay_request", + relayOperation: "create-link-challenge", + relayPhase: "send-request", + causeTag: "CloudRelayRequestError", + }); expect(logFields).not.toHaveProperty("cause"); expect(capturedLogs[0]?.filter((value) => typeof value === "string").join(" ")).not.toContain( transportCause.message, diff --git a/apps/server/src/cloud/http.ts b/apps/server/src/cloud/http.ts index 3a5df351895..874ea8e2dc1 100644 --- a/apps/server/src/cloud/http.ts +++ b/apps/server/src/cloud/http.ts @@ -9,7 +9,10 @@ import { EnvironmentHttpApi, EnvironmentHttpBadRequestError, EnvironmentHttpConflictError, + type EnvironmentHttpInternalOperation, EnvironmentHttpInternalServerError, + EnvironmentHttpRelayOperation, + EnvironmentHttpRelayPhase, EnvironmentHttpUnauthorizedError, } from "@t3tools/contracts"; import { @@ -91,18 +94,6 @@ const CLOUD_CREDENTIAL_RESPONSE_HEADERS = { pragma: "no-cache", } as const; -const CloudRelayRequestOperation = Schema.Literals([ - "create-link-challenge", - "create-environment-link", -]); - -const CloudRelayRequestPhase = Schema.Literals([ - "encode-request", - "send-request", - "check-response-status", - "decode-response", -]); - export class CloudRelayConfigurationError extends Schema.TaggedErrorClass()( "CloudRelayConfigurationError", { @@ -118,8 +109,8 @@ export class CloudRelayConfigurationError extends Schema.TaggedErrorClass()( "CloudRelayRequestError", { - operation: CloudRelayRequestOperation, - phase: CloudRelayRequestPhase, + operation: EnvironmentHttpRelayOperation, + phase: EnvironmentHttpRelayPhase, method: Schema.Literal("POST"), requestUrlInputLength: Schema.Number, requestUrlProtocol: Schema.optionalKey(Schema.String), @@ -211,12 +202,30 @@ function errorDiagnosticTag(cause: unknown): string { return typeof cause; } +type EnvironmentCloudInternalErrorContext = + | Exclude + | { + readonly operation: "relay_request"; + readonly relayOperation: CloudRelayRequestError["operation"]; + readonly relayPhase: CloudRelayRequestError["phase"]; + readonly responseStatus?: number; + }; + const failEnvironmentCloudInternalError = - (message: string) => - (cause: unknown): Effect.Effect => - Effect.logError(message, { causeTag: errorDiagnosticTag(cause) }).pipe( - Effect.flatMap(() => Effect.fail(new EnvironmentHttpInternalServerError({ message, cause }))), + (context: EnvironmentCloudInternalErrorContext) => + (cause: unknown): Effect.Effect => { + const error = + typeof context === "string" + ? new EnvironmentHttpInternalServerError({ operation: context, cause }) + : new EnvironmentHttpInternalServerError({ ...context, cause }); + const logContext = + typeof context === "string" + ? { operation: context, causeTag: errorDiagnosticTag(cause) } + : { ...context, causeTag: errorDiagnosticTag(cause) }; + return Effect.logError(error.message, logContext).pipe( + Effect.flatMap(() => Effect.fail(error)), ); + }; const requireRelayUrl = relayUrlConfig.pipe( Effect.mapError( @@ -273,9 +282,10 @@ function validateCloudMintPublicKey( ): Effect.Effect { return Effect.try({ try: () => NodeCrypto.createPublicKey(publicKey.replace(/\\n/g, "\n")), - catch: () => + catch: (cause) => new EnvironmentHttpBadRequestError({ - message: "Cloud mint public key must be a valid Ed25519 public key.", + reason: "invalid_cloud_mint_public_key", + cause, }), }).pipe( Effect.flatMap((key) => @@ -283,7 +293,7 @@ function validateCloudMintPublicKey( ? Effect.void : Effect.fail( new EnvironmentHttpBadRequestError({ - message: "Cloud mint public key must be a valid Ed25519 public key.", + reason: "invalid_cloud_mint_public_key", }), ), ), @@ -296,28 +306,28 @@ function validateRelayConfigPayload( if (!isSecureRelayUrl(payload.relayUrl)) { return Effect.fail( new EnvironmentHttpBadRequestError({ - message: "Relay URL must be a secure absolute HTTPS URL.", + reason: "invalid_relay_url", }), ); } if (payload.relayIssuer !== undefined && !isSecureRelayUrl(payload.relayIssuer)) { return Effect.fail( new EnvironmentHttpBadRequestError({ - message: "Relay issuer must be a secure absolute HTTPS URL.", + reason: "invalid_relay_issuer", }), ); } if (payload.environmentCredential.trim().length === 0) { return Effect.fail( new EnvironmentHttpBadRequestError({ - message: "Relay environment credential is required.", + reason: "missing_relay_environment_credential", }), ); } if (payload.cloudUserId.trim().length === 0) { return Effect.fail( new EnvironmentHttpBadRequestError({ - message: "Cloud user id is required.", + reason: "missing_cloud_user_id", }), ); } @@ -344,8 +354,7 @@ function validateLinkedCloudUser(input: { ? Effect.void : Effect.fail( new EnvironmentHttpConflictError({ - message: - "This environment is already linked to a different cloud account. Unlink it before switching accounts.", + reason: "linked_to_different_cloud_account", }), ); }), @@ -477,7 +486,7 @@ const makeCloudLinkProof = Effect.fn("environment.cloud.makeLinkProof")(function }) ) { return yield* new EnvironmentHttpBadRequestError({ - message: "Invalid managed endpoint origin.", + reason: "invalid_managed_endpoint_origin", }); } const now = yield* DateTime.now; @@ -520,7 +529,7 @@ const cloudLinkProofHandler = Effect.fn("environment.cloud.linkProof")( const requestUrl = requestAbsoluteUrl(httpRequest); if (requestUrl === null || hasForwardedAuthorityHeaders(httpRequest)) { return yield* new EnvironmentHttpBadRequestError({ - message: "Invalid managed endpoint origin.", + reason: "invalid_managed_endpoint_origin", }); } const proof = yield* makeCloudLinkProof(dependencies, request, requestUrl); @@ -529,23 +538,13 @@ const cloudLinkProofHandler = Effect.fn("environment.cloud.linkProof")( }, Effect.catchTags({ ServerAuthCloudLinkJwtSigningError: (error) => - failEnvironmentCloudInternalError(error.message)(error), - SecretStoreReadError: failEnvironmentCloudInternalError( - "Could not generate environment link proof.", - ), - SecretStoreDecodeError: failEnvironmentCloudInternalError( - "Could not generate environment link proof.", - ), - SecretStoreEncodeError: failEnvironmentCloudInternalError( - "Could not generate environment link proof.", - ), - SecretStorePersistError: failEnvironmentCloudInternalError( - "Could not generate environment link proof.", - ), - SecretStoreConcurrentReadError: failEnvironmentCloudInternalError( - "Could not generate environment link proof.", - ), - PlatformError: failEnvironmentCloudInternalError("Could not generate environment link proof."), + failEnvironmentCloudInternalError("sign_cloud_link_jwt")(error), + SecretStoreReadError: failEnvironmentCloudInternalError("generate_link_proof"), + SecretStoreDecodeError: failEnvironmentCloudInternalError("generate_link_proof"), + SecretStoreEncodeError: failEnvironmentCloudInternalError("generate_link_proof"), + SecretStorePersistError: failEnvironmentCloudInternalError("generate_link_proof"), + SecretStoreConcurrentReadError: failEnvironmentCloudInternalError("generate_link_proof"), + PlatformError: failEnvironmentCloudInternalError("generate_link_proof"), }), ); @@ -566,7 +565,6 @@ const applyCloudRelayConfig = Effect.fn("environment.cloud.applyRelayConfig")(fu endpointRuntimeStatus.status === "disabled" || endpointRuntimeStatus.status === "running"; if (!ok) { return yield* new EnvironmentCloudEndpointUnavailableError({ - message: "Managed endpoint runtime could not be started.", endpointRuntimeStatus, }); } @@ -601,19 +599,13 @@ const cloudRelayConfigHandler = Effect.fn("environment.cloud.relayConfig")( }, Effect.catchTags({ ServerAuthLinkedCloudAccountVerificationError: (error) => - failEnvironmentCloudInternalError(error.message)(error), + failEnvironmentCloudInternalError("verify_linked_cloud_account")(error), SecretStoreTemporaryPathGenerationError: failEnvironmentCloudInternalError( - "Could not persist environment relay configuration.", - ), - SecretStorePersistError: failEnvironmentCloudInternalError( - "Could not persist environment relay configuration.", - ), - SecretStoreRemoveError: failEnvironmentCloudInternalError( - "Could not persist environment relay configuration.", - ), - SchemaError: failEnvironmentCloudInternalError( - "Could not persist environment relay configuration.", + "persist_relay_configuration", ), + SecretStorePersistError: failEnvironmentCloudInternalError("persist_relay_configuration"), + SecretStoreRemoveError: failEnvironmentCloudInternalError("persist_relay_configuration"), + SchemaError: failEnvironmentCloudInternalError("persist_relay_configuration"), }), ); @@ -678,14 +670,15 @@ const reconcileDesiredCloudLinkWith = Effect.fn("environment.cloud.reconcileDesi function* (dependencies: CloudHttpDependencies, localOrigin: string) { const localUrl = yield* Effect.try({ try: () => new URL(localOrigin), - catch: () => + catch: (cause) => new EnvironmentHttpBadRequestError({ - message: "Could not resolve local environment origin.", + reason: "invalid_local_environment_origin", + cause, }), }); if (localUrl.origin !== localOrigin) { return yield* new EnvironmentHttpBadRequestError({ - message: "Could not resolve local environment origin.", + reason: "invalid_local_environment_origin", }); } const localWsOrigin = localOrigin.replace(/^http/u, "ws"); @@ -695,7 +688,7 @@ const reconcileDesiredCloudLinkWith = Effect.fn("environment.cloud.reconcileDesi onNone: () => Effect.fail( new EnvironmentHttpUnauthorizedError({ - message: "Run `t3 connect link` to authorize this environment.", + reason: "cloud_cli_authorization_required", }), ), onSome: Effect.succeed, @@ -755,47 +748,39 @@ const reconcileDesiredCloudLinkWith = Effect.fn("environment.cloud.reconcileDesi }, Effect.catchTags({ ServerAuthLinkedCloudAccountVerificationError: (error) => - failEnvironmentCloudInternalError(error.message)(error), + failEnvironmentCloudInternalError("verify_linked_cloud_account")(error), ServerAuthCloudLinkJwtSigningError: (error) => - failEnvironmentCloudInternalError(error.message)(error), - SecretStoreReadError: failEnvironmentCloudInternalError( - "Could not persist desired T3 Connect link state.", - ), + failEnvironmentCloudInternalError("sign_cloud_link_jwt")(error), + SecretStoreReadError: failEnvironmentCloudInternalError("persist_desired_link_state"), SecretStoreTemporaryPathGenerationError: failEnvironmentCloudInternalError( - "Could not persist desired T3 Connect link state.", - ), - SecretStorePersistError: failEnvironmentCloudInternalError( - "Could not persist desired T3 Connect link state.", - ), - SecretStoreRemoveError: failEnvironmentCloudInternalError( - "Could not persist desired T3 Connect link state.", - ), - SecretStoreDecodeError: failEnvironmentCloudInternalError( - "Could not persist desired T3 Connect link state.", - ), - SecretStoreEncodeError: failEnvironmentCloudInternalError( - "Could not persist desired T3 Connect link state.", - ), - SecretStoreConcurrentReadError: failEnvironmentCloudInternalError( - "Could not persist desired T3 Connect link state.", - ), - SchemaError: failEnvironmentCloudInternalError( - "Could not persist desired T3 Connect link state.", - ), - PlatformError: failEnvironmentCloudInternalError( - "Could not persist desired T3 Connect link state.", + "persist_desired_link_state", ), + SecretStorePersistError: failEnvironmentCloudInternalError("persist_desired_link_state"), + SecretStoreRemoveError: failEnvironmentCloudInternalError("persist_desired_link_state"), + SecretStoreDecodeError: failEnvironmentCloudInternalError("persist_desired_link_state"), + SecretStoreEncodeError: failEnvironmentCloudInternalError("persist_desired_link_state"), + SecretStoreConcurrentReadError: failEnvironmentCloudInternalError("persist_desired_link_state"), + SchemaError: failEnvironmentCloudInternalError("persist_desired_link_state"), + PlatformError: failEnvironmentCloudInternalError("persist_desired_link_state"), CloudRelayConfigurationError: (error) => - failEnvironmentCloudInternalError(error.message)(error), - CloudRelayRequestError: (error) => failEnvironmentCloudInternalError(error.message)(error), + failEnvironmentCloudInternalError("read_relay_url_configuration")(error), + CloudRelayRequestError: (error) => + failEnvironmentCloudInternalError({ + operation: "relay_request", + relayOperation: error.operation, + relayPhase: error.phase, + ...(error.responseStatus === undefined ? {} : { responseStatus: error.responseStatus }), + })(error), CloudCliCredentialRemovalError: (error) => - failEnvironmentCloudInternalError(error.message)(error), + failEnvironmentCloudInternalError("remove_cloud_cli_credential")(error), CloudCliCredentialRefreshError: (error) => - failEnvironmentCloudInternalError(error.message)(error), - CloudCliCredentialReadError: (error) => failEnvironmentCloudInternalError(error.message)(error), - CloudCliAuthorizationError: (error) => failEnvironmentCloudInternalError(error.message)(error), + failEnvironmentCloudInternalError("refresh_cloud_cli_credential")(error), + CloudCliCredentialReadError: (error) => + failEnvironmentCloudInternalError("read_cloud_cli_credential")(error), + CloudCliAuthorizationError: (error) => + failEnvironmentCloudInternalError("authorize_cloud_cli")(error), CloudCliAuthorizationTimeoutError: (error) => - failEnvironmentCloudInternalError(error.message)(error), + failEnvironmentCloudInternalError("await_cloud_cli_authorization")(error), }), ); @@ -834,9 +819,7 @@ const cloudLinkStateHandler = Effect.fn("environment.cloud.linkState")( return yield* readCloudLinkState(dependencies); }, Effect.catchTags({ - SecretStoreReadError: failEnvironmentCloudInternalError( - "Could not read environment relay configuration.", - ), + SecretStoreReadError: failEnvironmentCloudInternalError("read_relay_configuration"), }), ); @@ -861,14 +844,10 @@ const cloudUnlinkHandler = Effect.fn("environment.cloud.unlink")( }, Effect.catchTags({ SecretStoreTemporaryPathGenerationError: failEnvironmentCloudInternalError( - "Could not remove environment relay configuration.", - ), - SecretStorePersistError: failEnvironmentCloudInternalError( - "Could not remove environment relay configuration.", - ), - SecretStoreRemoveError: failEnvironmentCloudInternalError( - "Could not remove environment relay configuration.", + "remove_relay_configuration", ), + SecretStorePersistError: failEnvironmentCloudInternalError("remove_relay_configuration"), + SecretStoreRemoveError: failEnvironmentCloudInternalError("remove_relay_configuration"), }), ); @@ -885,15 +864,11 @@ const cloudPreferencesHandler = Effect.fn("environment.cloud.preferences")( return yield* readCloudLinkState(dependencies); }, Effect.catchTags({ - SecretStoreReadError: failEnvironmentCloudInternalError( - "Could not persist environment cloud preferences.", - ), + SecretStoreReadError: failEnvironmentCloudInternalError("persist_cloud_preferences"), SecretStoreTemporaryPathGenerationError: failEnvironmentCloudInternalError( - "Could not persist environment cloud preferences.", - ), - SecretStorePersistError: failEnvironmentCloudInternalError( - "Could not persist environment cloud preferences.", + "persist_cloud_preferences", ), + SecretStorePersistError: failEnvironmentCloudInternalError("persist_cloud_preferences"), }), ); @@ -945,7 +920,7 @@ const cloudEnvironmentHealthHandler = Effect.fn("environment.cloud.health")( !hasExactScope({ scopes: proofOption.value.scope, expected: "environment:status" }) ) { return yield* new EnvironmentHttpUnauthorizedError({ - message: "Invalid cloud health request.", + reason: "invalid_cloud_health_request", }); } const proof = proofOption.value; @@ -959,7 +934,7 @@ const cloudEnvironmentHealthHandler = Effect.fn("environment.cloud.health")( }); if (!consumedReplayGuards) { return yield* new EnvironmentHttpConflictError({ - message: "Cloud health request was already consumed.", + reason: "cloud_health_request_replayed", }); } @@ -1004,31 +979,23 @@ const cloudEnvironmentHealthHandler = Effect.fn("environment.cloud.health")( }, Effect.catchTags({ ServerAuthLinkedCloudAccountReadError: (error) => - failEnvironmentCloudInternalError(error.message)(error), + failEnvironmentCloudInternalError("read_linked_cloud_account")(error), ServerAuthLinkedCloudAccountMissingError: (error) => - failEnvironmentCloudInternalError(error.message)(error), + failEnvironmentCloudInternalError("require_linked_cloud_account")(error), ServerAuthCloudMintPublicKeyMissingError: (error) => - failEnvironmentCloudInternalError(error.message)(error), + failEnvironmentCloudInternalError("read_cloud_mint_public_key")(error), ServerAuthCloudRelayIssuerMissingError: (error) => - failEnvironmentCloudInternalError(error.message)(error), + failEnvironmentCloudInternalError("read_cloud_relay_issuer")(error), ServerAuthCloudHealthJwtSigningError: (error) => - failEnvironmentCloudInternalError(error.message)(error), - SecretStoreReadError: failEnvironmentCloudInternalError( - "Could not answer cloud health request.", - ), - SecretStorePersistError: failEnvironmentCloudInternalError( - "Could not answer cloud health request.", - ), - SecretStoreDecodeError: failEnvironmentCloudInternalError( - "Could not answer cloud health request.", - ), - SecretStoreEncodeError: failEnvironmentCloudInternalError( - "Could not answer cloud health request.", - ), + failEnvironmentCloudInternalError("sign_cloud_health_jwt")(error), + SecretStoreReadError: failEnvironmentCloudInternalError("answer_cloud_health_request"), + SecretStorePersistError: failEnvironmentCloudInternalError("answer_cloud_health_request"), + SecretStoreDecodeError: failEnvironmentCloudInternalError("answer_cloud_health_request"), + SecretStoreEncodeError: failEnvironmentCloudInternalError("answer_cloud_health_request"), SecretStoreConcurrentReadError: failEnvironmentCloudInternalError( - "Could not answer cloud health request.", + "answer_cloud_health_request", ), - PlatformError: failEnvironmentCloudInternalError("Could not answer cloud health request."), + PlatformError: failEnvironmentCloudInternalError("answer_cloud_health_request"), }), ); @@ -1081,7 +1048,7 @@ const cloudMintCredentialHandler = Effect.fn("environment.cloud.mintCredential") !hasExactScope({ scopes: proofOption.value.scope, expected: "environment:connect" }) ) { return yield* new EnvironmentHttpUnauthorizedError({ - message: "Invalid cloud mint request.", + reason: "invalid_cloud_mint_request", }); } const proof = proofOption.value; @@ -1095,7 +1062,7 @@ const cloudMintCredentialHandler = Effect.fn("environment.cloud.mintCredential") }); if (!consumedReplayGuards) { return yield* new EnvironmentHttpConflictError({ - message: "Cloud mint request was already consumed.", + reason: "cloud_mint_request_replayed", }); } @@ -1142,35 +1109,25 @@ const cloudMintCredentialHandler = Effect.fn("environment.cloud.mintCredential") }, Effect.catchTags({ ServerAuthLinkedCloudAccountReadError: (error) => - failEnvironmentCloudInternalError(error.message)(error), + failEnvironmentCloudInternalError("read_linked_cloud_account")(error), ServerAuthLinkedCloudAccountMissingError: (error) => - failEnvironmentCloudInternalError(error.message)(error), + failEnvironmentCloudInternalError("require_linked_cloud_account")(error), ServerAuthCloudMintPublicKeyMissingError: (error) => - failEnvironmentCloudInternalError(error.message)(error), + failEnvironmentCloudInternalError("read_cloud_mint_public_key")(error), ServerAuthCloudRelayIssuerMissingError: (error) => - failEnvironmentCloudInternalError(error.message)(error), + failEnvironmentCloudInternalError("read_cloud_relay_issuer")(error), ServerAuthPairingLinkCreationError: (error) => - failEnvironmentCloudInternalError(error.message)(error), + failEnvironmentCloudInternalError("create_cloud_pairing_link")(error), ServerAuthCloudMintJwtSigningError: (error) => - failEnvironmentCloudInternalError(error.message)(error), - SecretStoreReadError: failEnvironmentCloudInternalError( - "Could not issue cloud connection credential.", - ), - SecretStorePersistError: failEnvironmentCloudInternalError( - "Could not issue cloud connection credential.", - ), - SecretStoreDecodeError: failEnvironmentCloudInternalError( - "Could not issue cloud connection credential.", - ), - SecretStoreEncodeError: failEnvironmentCloudInternalError( - "Could not issue cloud connection credential.", - ), + failEnvironmentCloudInternalError("sign_cloud_mint_jwt")(error), + SecretStoreReadError: failEnvironmentCloudInternalError("issue_cloud_connection_credential"), + SecretStorePersistError: failEnvironmentCloudInternalError("issue_cloud_connection_credential"), + SecretStoreDecodeError: failEnvironmentCloudInternalError("issue_cloud_connection_credential"), + SecretStoreEncodeError: failEnvironmentCloudInternalError("issue_cloud_connection_credential"), SecretStoreConcurrentReadError: failEnvironmentCloudInternalError( - "Could not issue cloud connection credential.", - ), - PlatformError: failEnvironmentCloudInternalError( - "Could not issue cloud connection credential.", + "issue_cloud_connection_credential", ), + PlatformError: failEnvironmentCloudInternalError("issue_cloud_connection_credential"), }), ); diff --git a/infra/relay/src/environments/EnvironmentConnector.test.ts b/infra/relay/src/environments/EnvironmentConnector.test.ts index 63f12379870..0fd82a138af 100644 --- a/infra/relay/src/environments/EnvironmentConnector.test.ts +++ b/infra/relay/src/environments/EnvironmentConnector.test.ts @@ -517,6 +517,7 @@ describe("EnvironmentConnector", () => { Response.json( { _tag: "EnvironmentHttpInternalServerError", + operation: "answer_cloud_health_request", message: "Environment is unavailable.", }, { status: 500 }, diff --git a/packages/contracts/src/environmentHttp.ts b/packages/contracts/src/environmentHttp.ts index e2e9284c6a1..015c0289d93 100644 --- a/packages/contracts/src/environmentHttp.ts +++ b/packages/contracts/src/environmentHttp.ts @@ -169,13 +169,154 @@ const EnvironmentAuthenticationErrors = [ EnvironmentInternalError, ] as const; +export const EnvironmentHttpBadRequestReason = Schema.Literals([ + "invalid_cloud_mint_public_key", + "invalid_relay_url", + "invalid_relay_issuer", + "missing_relay_environment_credential", + "missing_cloud_user_id", + "invalid_managed_endpoint_origin", + "invalid_local_environment_origin", +]); +export type EnvironmentHttpBadRequestReason = typeof EnvironmentHttpBadRequestReason.Type; + +const environmentHttpBadRequestMessages = { + invalid_cloud_mint_public_key: "Cloud mint public key must be a valid Ed25519 public key.", + invalid_relay_url: "Relay URL must be a secure absolute HTTPS URL.", + invalid_relay_issuer: "Relay issuer must be a secure absolute HTTPS URL.", + missing_relay_environment_credential: "Relay environment credential is required.", + missing_cloud_user_id: "Cloud user id is required.", + invalid_managed_endpoint_origin: "Invalid managed endpoint origin.", + invalid_local_environment_origin: "Could not resolve local environment origin.", +} satisfies Record; + +export const EnvironmentHttpUnauthorizedReason = Schema.Literals([ + "cloud_cli_authorization_required", + "invalid_cloud_health_request", + "invalid_cloud_mint_request", +]); +export type EnvironmentHttpUnauthorizedReason = typeof EnvironmentHttpUnauthorizedReason.Type; + +const environmentHttpUnauthorizedMessages = { + cloud_cli_authorization_required: "Run `t3 connect link` to authorize this environment.", + invalid_cloud_health_request: "Invalid cloud health request.", + invalid_cloud_mint_request: "Invalid cloud mint request.", +} satisfies Record; + +export const EnvironmentHttpInternalOperation = Schema.Literals([ + "generate_link_proof", + "verify_linked_cloud_account", + "persist_relay_configuration", + "persist_desired_link_state", + "read_relay_configuration", + "remove_relay_configuration", + "persist_cloud_preferences", + "answer_cloud_health_request", + "issue_cloud_connection_credential", + "read_linked_cloud_account", + "require_linked_cloud_account", + "sign_cloud_link_jwt", + "read_cloud_mint_public_key", + "read_cloud_relay_issuer", + "sign_cloud_health_jwt", + "sign_cloud_mint_jwt", + "create_cloud_pairing_link", + "read_relay_url_configuration", + "relay_request", + "remove_cloud_cli_credential", + "refresh_cloud_cli_credential", + "read_cloud_cli_credential", + "authorize_cloud_cli", + "await_cloud_cli_authorization", +]); +export type EnvironmentHttpInternalOperation = typeof EnvironmentHttpInternalOperation.Type; + +export const EnvironmentHttpRelayOperation = Schema.Literals([ + "create-link-challenge", + "create-environment-link", +]); +export type EnvironmentHttpRelayOperation = typeof EnvironmentHttpRelayOperation.Type; + +export const EnvironmentHttpRelayPhase = Schema.Literals([ + "encode-request", + "send-request", + "check-response-status", + "decode-response", +]); +export type EnvironmentHttpRelayPhase = typeof EnvironmentHttpRelayPhase.Type; + +const environmentHttpInternalMessages = { + generate_link_proof: "Could not generate environment link proof.", + verify_linked_cloud_account: "Could not verify the linked cloud account.", + persist_relay_configuration: "Could not persist environment relay configuration.", + persist_desired_link_state: "Could not persist desired T3 Connect link state.", + read_relay_configuration: "Could not read environment relay configuration.", + remove_relay_configuration: "Could not remove environment relay configuration.", + persist_cloud_preferences: "Could not persist environment cloud preferences.", + answer_cloud_health_request: "Could not answer cloud health request.", + issue_cloud_connection_credential: "Could not issue cloud connection credential.", + read_linked_cloud_account: "Could not read the linked cloud account.", + require_linked_cloud_account: "Cloud linked user is not installed for this environment.", + sign_cloud_link_jwt: "Failed to sign cloud link JWT.", + read_cloud_mint_public_key: "Cloud mint public key is not installed for this environment.", + read_cloud_relay_issuer: "Cloud relay issuer is not installed for this environment.", + sign_cloud_health_jwt: "Failed to sign cloud health JWT.", + sign_cloud_mint_jwt: "Failed to sign cloud mint JWT.", + create_cloud_pairing_link: "Failed to create pairing link.", + read_relay_url_configuration: + "T3CODE_RELAY_URL must be configured as a secure absolute HTTPS origin.", + remove_cloud_cli_credential: "Could not remove the stored T3 Connect CLI credential.", + refresh_cloud_cli_credential: "Could not refresh the T3 Connect CLI credential.", + read_cloud_cli_credential: "Could not read the stored T3 Connect CLI credential.", + authorize_cloud_cli: "Could not authorize the T3 Connect CLI.", + await_cloud_cli_authorization: "Timed out waiting for T3 Connect authorization.", +} satisfies Record, string>; + +export const EnvironmentHttpConflictReason = Schema.Literals([ + "linked_to_different_cloud_account", + "cloud_health_request_replayed", + "cloud_mint_request_replayed", +]); +export type EnvironmentHttpConflictReason = typeof EnvironmentHttpConflictReason.Type; + +const environmentHttpConflictMessages = { + linked_to_different_cloud_account: + "This environment is already linked to a different cloud account. Unlink it before switching accounts.", + cloud_health_request_replayed: "Cloud health request was already consumed.", + cloud_mint_request_replayed: "Cloud mint request was already consumed.", +} satisfies Record; + +// These HTTP errors cross independently deployed clients, relays, and environment servers. Keep +// newly added diagnostics optional while decoding so a newer peer can still consume the legacy +// message-only payload. The constructors below continue to require structured context from new +// application code and only preserve `message` when Schema is decoding an older wire payload. +function decodedEnvironmentHttpErrorMessage(props: object): string | undefined { + if (!("message" in props)) return undefined; + return typeof props.message === "string" ? props.message : undefined; +} + export class EnvironmentHttpBadRequestError extends Schema.TaggedErrorClass()( "EnvironmentHttpBadRequestError", { + reason: Schema.optional(EnvironmentHttpBadRequestReason), message: Schema.String, }, { httpApiStatus: 400 }, ) { + // @effect-diagnostics-next-line overriddenSchemaConstructor:off + constructor(props: { + readonly reason: EnvironmentHttpBadRequestReason; + readonly cause?: unknown; + }) { + super({ + reason: props.reason, + message: + decodedEnvironmentHttpErrorMessage(props) ?? + environmentHttpBadRequestMessages[props.reason], + ...(props.cause === undefined ? {} : { cause: props.cause }), + } as any); + } + [HttpServerRespondable.symbol]() { return HttpServerResponse.schemaJson(EnvironmentHttpBadRequestError)(this, { status: 400 }); } @@ -184,10 +325,25 @@ export class EnvironmentHttpBadRequestError extends Schema.TaggedErrorClass()( "EnvironmentHttpUnauthorizedError", { + reason: Schema.optional(EnvironmentHttpUnauthorizedReason), message: Schema.String, }, { httpApiStatus: 401 }, ) { + // @effect-diagnostics-next-line overriddenSchemaConstructor:off + constructor(props: { + readonly reason: EnvironmentHttpUnauthorizedReason; + readonly cause?: unknown; + }) { + super({ + reason: props.reason, + message: + decodedEnvironmentHttpErrorMessage(props) ?? + environmentHttpUnauthorizedMessages[props.reason], + ...(props.cause === undefined ? {} : { cause: props.cause }), + } as any); + } + [HttpServerRespondable.symbol]() { return HttpServerResponse.schemaJson(EnvironmentHttpUnauthorizedError)(this, { status: 401 }); } @@ -200,6 +356,14 @@ export class EnvironmentHttpForbiddenError extends Schema.TaggedErrorClass()( "EnvironmentHttpInternalServerError", { + operation: Schema.optional(EnvironmentHttpInternalOperation), + relayOperation: Schema.optional(EnvironmentHttpRelayOperation), + relayPhase: Schema.optional(EnvironmentHttpRelayPhase), + responseStatus: Schema.optional(Schema.Number), message: Schema.String, }, { httpApiStatus: 500 }, ) { // @effect-diagnostics-next-line overriddenSchemaConstructor:off - constructor(props: { readonly message: string; readonly cause?: unknown }) { - super(props as any); + constructor( + props: + | { + readonly operation: Exclude; + readonly cause?: unknown; + } + | { + readonly operation: "relay_request"; + readonly relayOperation: EnvironmentHttpRelayOperation; + readonly relayPhase: EnvironmentHttpRelayPhase; + readonly responseStatus?: number; + readonly cause?: unknown; + }, + ) { + const message = + decodedEnvironmentHttpErrorMessage(props) ?? + (props.operation === "relay_request" + ? `T3 Connect relay ${props.relayOperation} failed during ${props.relayPhase}${ + props.responseStatus === undefined + ? "" + : ` with response status ${props.responseStatus}` + }.` + : environmentHttpInternalMessages[props.operation]); + super({ + ...props, + message, + } as any); } [HttpServerRespondable.symbol]() { @@ -225,10 +418,21 @@ export class EnvironmentHttpInternalServerError extends Schema.TaggedErrorClass< export class EnvironmentHttpConflictError extends Schema.TaggedErrorClass()( "EnvironmentHttpConflictError", { + reason: Schema.optional(EnvironmentHttpConflictReason), message: Schema.String, }, { httpApiStatus: 409 }, ) { + // @effect-diagnostics-next-line overriddenSchemaConstructor:off + constructor(props: { readonly reason: EnvironmentHttpConflictReason; readonly cause?: unknown }) { + super({ + reason: props.reason, + message: + decodedEnvironmentHttpErrorMessage(props) ?? environmentHttpConflictMessages[props.reason], + ...(props.cause === undefined ? {} : { cause: props.cause }), + } as any); + } + [HttpServerRespondable.symbol]() { return HttpServerResponse.schemaJson(EnvironmentHttpConflictError)(this, { status: 409 }); } @@ -242,6 +446,16 @@ export class EnvironmentCloudEndpointUnavailableError extends Schema.TaggedError }, { httpApiStatus: 503 }, ) { + // @effect-diagnostics-next-line overriddenSchemaConstructor:off + constructor(props: { readonly endpointRuntimeStatus: unknown; readonly cause?: unknown }) { + super({ + ...props, + message: + decodedEnvironmentHttpErrorMessage(props) ?? + "Managed endpoint runtime could not be started.", + } as any); + } + [HttpServerRespondable.symbol]() { return HttpServerResponse.schemaJson(EnvironmentCloudEndpointUnavailableError)(this, { status: 503,