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