1import type { DpopFailureReason } from "@t3tools/contracts";
2import {
3 T3CHAT_PLAN_TUNNEL_LIMITS,
4 type RelayEnvironmentStatusResponse,
5 type RelayProtectedError,
6} from "@t3tools/contracts/relay";
7
8export const DPOP_CLOCK_HINT =
9 "Hint: Check that automatic date and time is enabled on both devices, then try again.";
10
11/** Older servers omit the DPoP category, but newer servers can also omit it for
12 * a credential failure that happens after proof verification. */
13export const DPOP_UNKNOWN_HINT =
14 "Hint: Try again. If it still fails, clock skew may be the cause; check that automatic date and time is enabled on both devices.";
15
16export const DPOP_RETRY_HINT = "Hint: Try again. If the problem continues, copy the trace ID.";
17
18function dpopFailureHint(reason: DpopFailureReason | undefined): string {
19 if (reason === "time_window") return DPOP_CLOCK_HINT;
20 if (reason === undefined) return DPOP_UNKNOWN_HINT;
21 return DPOP_RETRY_HINT;
22}
23
24export function dpopFailureMessage(message: string, reason: DpopFailureReason | undefined): string {
25 return `${message} ${dpopFailureHint(reason)}`;
26}
27
28export function relayProtectedErrorMessage(error: RelayProtectedError): string {
29 switch (error._tag) {
30 case "RelayAuthInvalidError":
31 switch (error.reason) {
32 case "missing_bearer":
33 case "invalid_bearer":
34 return "Relay rejected the cloud session token.";
35 case "invalid_dpop":
36 return dpopFailureMessage("Relay rejected the DPoP proof.", error.dpopFailureReason);
37 case "not_authorized":
38 return "Relay rejected the authenticated request.";
39 }
40 case "RelayEnvironmentLinkProofExpiredError":
41 return "Relay rejected an expired environment link proof.";
42 case "RelayEnvironmentLinkProofInvalidError":
43 return `Relay rejected the environment link proof (${error.reason}).`;
44 case "RelayEnvironmentConnectNotAuthorizedError":
45 // "Not authorized" covers non-auth causes too; surface the reason so a
46 // missing link does not read as a credential problem.
47 if (error.reason === "environment_link_not_found") {
48 return "Relay has no active link for this environment. If it was deregistered from T3 Connect, link it again on that machine.";
49 }
50 if (error.reason === "managed_endpoint_allocation_not_ready") {
51 return "The environment's T3 Connect tunnel is still being set up.";
52 }
53 if (error.reason === "endpoint_provider_not_managed") {
54 return "This environment is linked only to publish agent activity, so it cannot accept connections. On that computer, open Settings > Sharing & access and turn T3 Connect on.";
55 }
56 return error.reason
57 ? `Relay rejected the environment connection request (${error.reason}).`
58 : "Relay rejected the environment connection request.";
59 case "RelayEnvironmentEndpointUnavailableError":
60 return `Relay could not reach the environment endpoint (${error.reason}).`;
61 case "RelayEnvironmentEndpointTimedOutError":
62 return "Relay timed out while contacting the environment endpoint.";
63 case "RelayEnvironmentLinkFailedError":
64 return `Relay could not link the environment (${error.reason}).`;
65 case "RelayEnvironmentLinkUnavailableError":
66 return `Relay cannot provision the managed endpoint (${error.reason}).${
67 error.provisioningStage ? ` Failed stage: ${error.provisioningStage}.` : ""
68 }`;
69 case "RelayEnvironmentLinkLimitExceededError":
70 return error.maxTunnels < T3CHAT_PLAN_TUNNEL_LIMITS.premier
71 ? `Relay refused the link: this account already has its maximum of ${error.maxTunnels} managed tunnels. Unlink an environment, or link a paid T3 Chat account in your T3 Connect account settings on web or desktop. Pro raises the default limit to ${T3CHAT_PLAN_TUNNEL_LIMITS.pro} and Premier to ${T3CHAT_PLAN_TUNNEL_LIMITS.premier}.`
72 : `Relay refused the link: this account already has its maximum of ${error.maxTunnels} managed tunnels. Unlink an environment to free one up.`;
73 case "RelayAgentActivityPublishProofExpiredError":
74 return "Relay rejected an expired agent activity publish proof.";
75 case "RelayAgentActivityPublishProofInvalidError":
76 return `Relay rejected the agent activity publish proof (${error.reason}).`;
77 case "RelayInternalError":
78 return `Relay encountered an internal error (${error.reason}).`;
79 }
80}
81
82// The relay cuts off a request it cannot finish in time with a 504 that no
83// endpoint declares, so the client sees only the status.
84export const RELAY_GATEWAY_TIMEOUT_MESSAGE =
85 "Relay timed out before it finished the request (HTTP 504). Try again in a few minutes. If it keeps failing, copy the trace ID when you report it.";
86
87export function relayRequestFailureDetail(error: {
88 readonly relayError?: RelayProtectedError | undefined;
89 readonly status?: number | undefined;
90}): string | null {
91 if (error.relayError) return relayProtectedErrorMessage(error.relayError);
92 if (error.status === 504) return RELAY_GATEWAY_TIMEOUT_MESSAGE;
93 if (error.status !== undefined) return `Relay returned an unexpected HTTP ${error.status}.`;
94 return null;
95}
96
97const RELAY_TUNNEL_RELEASED_MESSAGE =
98 "Offline for a while, so its T3 Connect tunnel was removed. Start T3 Code on that computer and update it to the latest version to reconnect.";
99
100export function relayOfflineReasonMessage(
101 status: Pick<RelayEnvironmentStatusResponse, "offlineReason">,
102): string | null {
103 switch (status.offlineReason) {
104 case "tunnel_released":
105 return RELAY_TUNNEL_RELEASED_MESSAGE;
106 case undefined:
107 return null;
108 }
109}