Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion apps/mobile/src/features/cloud/linkEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,14 @@ function relayProtectedErrorMessage(error: RelayProtectedErrorType): string {
case "RelayEnvironmentLinkProofInvalidError":
return `Relay rejected the environment link proof (${error.reason}).`;
case "RelayEnvironmentConnectNotAuthorizedError":
return "Relay rejected the environment connection request.";
// "Not authorized" covers non-auth causes too; surface the reason so a
// missing link doesn't read as a credential problem.
if (error.reason === "environment_link_not_found") {
return "Relay has no active link for this environment. The environment server may not have re-established its link yet.";
}
return error.reason
? `Relay rejected the environment connection request (${error.reason}).`
: "Relay rejected the environment connection request.";
case "RelayEnvironmentEndpointUnavailableError":
return `Relay could not reach the environment endpoint (${error.reason}).`;
case "RelayEnvironmentEndpointTimedOutError":
Expand Down
22 changes: 21 additions & 1 deletion apps/server/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { EnvironmentHttpApi } from "@t3tools/contracts";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Schedule from "effect/Schedule";
import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http";
import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder";

Expand Down Expand Up @@ -535,7 +537,25 @@ export const makeServerLayer = Layer.unwrap(
yield* Effect.forkScoped(
Effect.sleep("250 millis").pipe(
Effect.andThen(reconcileDesiredCloudLink(`http://127.0.0.1:${address.port}`)),
Effect.retry({ times: 4 }),
// On reboot this races NIC/DNS bring-up, so back off exponentially
// (capped at 30s) instead of burning all retries in a second.
// Bounded overall so a permanently broken setup still surfaces the
// warning below. Bad-request/unauthorized/conflict are
// deterministic failures (malformed origin, not linked yet, linked
// to a different cloud account) that no amount of retrying
// converges.
Effect.retry({
while: (error) =>
error._tag !== "EnvironmentHttpBadRequestError" &&
error._tag !== "EnvironmentHttpUnauthorizedError" &&
error._tag !== "EnvironmentHttpConflictError",
schedule: Schedule.exponential("1 second").pipe(
Schedule.modifyDelay(({ duration }) =>
Effect.succeed(Duration.min(duration, Duration.seconds(30))),
),
Schedule.upTo({ duration: "10 minutes" }),
),
}),
Comment thread
cursor[bot] marked this conversation as resolved.
Effect.tap(() => Effect.logInfo("T3 Connect desired link reconciled on startup")),
Effect.catch((cause) =>
Effect.logWarning("Failed to reconcile T3 Connect desired link on startup", {
Expand Down
9 changes: 8 additions & 1 deletion apps/web/src/cloud/linkEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,14 @@ function relayProtectedErrorMessage(error: RelayProtectedErrorType): string {
case "RelayEnvironmentLinkProofInvalidError":
return `Relay rejected the environment link proof (${error.reason}).`;
case "RelayEnvironmentConnectNotAuthorizedError":
return "Relay rejected the environment connection request.";
// "Not authorized" covers non-auth causes too; surface the reason so a
// missing link doesn't read as a credential problem.
if (error.reason === "environment_link_not_found") {
return "Relay has no active link for this environment. The environment server may not have re-established its link yet.";
}
return error.reason
? `Relay rejected the environment connection request (${error.reason}).`
: "Relay rejected the environment connection request.";
case "RelayEnvironmentEndpointUnavailableError":
return `Relay could not reach the environment endpoint (${error.reason}).`;
case "RelayEnvironmentEndpointTimedOutError":
Expand Down
18 changes: 3 additions & 15 deletions infra/relay/src/environments/EnvironmentConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
RelayEnvironmentMintResponse,
RelayEnvironmentMintResponseProofPayload,
RelayCloudMintCredentialProofPayload,
RelayEnvironmentConnectNotAuthorizedReason,
type RelayEnvironmentConnectResponse,
type RelayEnvironmentStatusResponse,
} from "@t3tools/contracts/relay";
Expand Down Expand Up @@ -44,21 +45,8 @@ import * as ManagedEndpointAllocations from "./ManagedEndpointAllocations.ts";
import * as RelayConfiguration from "../Config.ts";
import { isManagedEndpointHostname } from "../deploymentConfig.ts";

export const EnvironmentConnectNotAuthorizedReason = Schema.Literals([
"client_proof_key_thumbprint_missing",
"environment_link_not_found",
"endpoint_provider_not_managed",
"managed_endpoint_allocation_not_found",
"managed_endpoint_base_domain_not_configured",
"managed_endpoint_allocation_not_ready",
"managed_endpoint_hostname_invalid",
"managed_endpoint_mismatch",
]);
export type EnvironmentConnectNotAuthorizedReason =
typeof EnvironmentConnectNotAuthorizedReason.Type;

function environmentConnectNotAuthorizedReasonMessage(
reason: EnvironmentConnectNotAuthorizedReason,
reason: RelayEnvironmentConnectNotAuthorizedReason,
): string {
switch (reason) {
case "client_proof_key_thumbprint_missing":
Expand All @@ -85,7 +73,7 @@ export class EnvironmentConnectNotAuthorized extends Schema.TaggedErrorClass<Env
{
environmentId: Schema.String,
operation: Schema.Literals(["connect", "status"]),
reason: EnvironmentConnectNotAuthorizedReason,
reason: RelayEnvironmentConnectNotAuthorizedReason,
},
) {
override get message(): string {
Expand Down
6 changes: 4 additions & 2 deletions infra/relay/src/http/Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -778,9 +778,10 @@ export const dpopClientApi = HttpApiBuilder.group(
},
mapRelayCommonApiErrors("invalid_dpop"),
mapErrorTags({
EnvironmentConnectNotAuthorized: (_error, traceId) =>
EnvironmentConnectNotAuthorized: (error, traceId) =>
new RelayEnvironmentConnectNotAuthorizedError({
code: "environment_connect_not_authorized",
reason: error.reason,
traceId,
}),
EnvironmentMintRequestFailed: (_error, traceId) =>
Expand Down Expand Up @@ -820,9 +821,10 @@ export const dpopClientApi = HttpApiBuilder.group(
},
mapRelayCommonApiErrors("invalid_dpop"),
mapErrorTags({
EnvironmentConnectNotAuthorized: (_error, traceId) =>
EnvironmentConnectNotAuthorized: (error, traceId) =>
new RelayEnvironmentConnectNotAuthorizedError({
code: "environment_connect_not_authorized",
reason: error.reason,
traceId,
}),
EnvironmentMintRequestFailed: (_error, traceId) =>
Expand Down
20 changes: 19 additions & 1 deletion packages/contracts/src/relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,16 +371,34 @@ export class RelayEnvironmentLinkProofInvalidError extends Schema.TaggedErrorCla
}
}

export const RelayEnvironmentConnectNotAuthorizedReason = Schema.Literals([
"client_proof_key_thumbprint_missing",
"environment_link_not_found",
"endpoint_provider_not_managed",
"managed_endpoint_allocation_not_found",
"managed_endpoint_base_domain_not_configured",
"managed_endpoint_allocation_not_ready",
"managed_endpoint_hostname_invalid",
"managed_endpoint_mismatch",
]);
export type RelayEnvironmentConnectNotAuthorizedReason =
typeof RelayEnvironmentConnectNotAuthorizedReason.Type;

export class RelayEnvironmentConnectNotAuthorizedError extends Schema.TaggedErrorClass<RelayEnvironmentConnectNotAuthorizedError>()(
"RelayEnvironmentConnectNotAuthorizedError",
{
code: Schema.Literal("environment_connect_not_authorized"),
// Optional so responses from relays deployed before the reason was
// threaded through still decode.
reason: Schema.optional(RelayEnvironmentConnectNotAuthorizedReason),
traceId: TrimmedNonEmptyString,
},
{ httpApiStatus: 403 },
) {
override get message(): string {
return "Relay environment connection is not authorized";
return this.reason
? `Relay environment connection is not authorized: ${this.reason}`
: "Relay environment connection is not authorized";
}
}

Expand Down
Loading