Skip to content
Source
Sign inSign up

errors.ts

276 lines9.5 KB

1import type { ClientConnectionMethod, EnvironmentId } from "@t3tools/contracts";
2import type { RelayProtectedError } from "@t3tools/contracts/relay";
3import type { ManagedRelayClientError } from "../relay/managedRelay.ts";
4import { dpopFailureMessage, relayProtectedErrorMessage } from "../relay/errorPresentation.ts";
5import type { RemoteEnvironmentAuthError } from "../authorization/remote.ts";
6import { NETWORK_BLOCKING_HINT } from "../errors/network.ts";
7import {
8 ConnectionBlockedError,
9 type ConnectionAttemptError,
10 ConnectionTransientError,
11} from "./model.ts";
12​
13export function profileMissingError(connectionId: string): ConnectionBlockedError {
14 return new ConnectionBlockedError({
15 reason: "configuration",
16 detail: `Connection profile ${connectionId} is unavailable.`,
17 hop: "device",
18 });
20​
21export function credentialMissingError(connectionId: string): ConnectionBlockedError {
22 return new ConnectionBlockedError({
23 reason: "authentication",
24 detail: `Connection credential ${connectionId} is unavailable.`,
25 hop: "device",
26 });
28​
29export function environmentMismatchError(input: {
30 readonly expected: EnvironmentId;
31 readonly actual: EnvironmentId;
32}): ConnectionBlockedError {
33 return new ConnectionBlockedError({
34 reason: "configuration",
35 detail: `Connected environment ${input.actual} does not match ${input.expected}.`,
36 });
38​
39function relayProtectedError(error: RelayProtectedError): ConnectionAttemptError {
40 const detail = relayProtectedErrorMessage(error);
41 const traceId = error.traceId;
42 switch (error._tag) {
43 case "RelayAuthInvalidError":
44 return new ConnectionBlockedError({
45 reason: "authentication",
46 detail,
47 traceId,
48 hop: "relay",
49 ...(error.dpopFailureReason === "time_window" ? { failureCause: "clock_skew" } : {}),
50 });
51 case "RelayEnvironmentLinkProofExpiredError":
52 case "RelayAgentActivityPublishProofExpiredError":
53 case "RelayAgentActivityPublishProofInvalidError":
54 return new ConnectionBlockedError({
55 reason: "authentication",
56 detail,
57 traceId,
58 hop: "relay",
59 });
60 case "RelayEnvironmentConnectNotAuthorizedError":
61 // A host swapping in a new tunnel leaves its allocation unready until
62 // the tunnel is up, and nothing on the client changes when it is.
63 if (error.reason === "managed_endpoint_allocation_not_ready") {
64 return new ConnectionTransientError({
65 reason: "endpoint-unavailable",
66 detail,
67 traceId,
68 hop: "tunnel",
69 failureCause: "tunnel_pending",
70 });
71 }
72 if (error.reason === "endpoint_provider_not_managed") {
73 return new ConnectionBlockedError({
74 reason: "permission",
75 detail,
76 traceId,
77 hop: "tunnel",
78 failureCause: "tunnel_not_enabled",
79 });
80 }
81 return new ConnectionBlockedError({
82 reason: "permission",
83 detail,
84 traceId,
85 hop: "relay",
86 ...(error.reason === "environment_link_not_found" ? { failureCause: "not_linked" } : {}),
87 });
88 case "RelayEnvironmentLinkProofInvalidError":
89 case "RelayEnvironmentLinkLimitExceededError":
90 return new ConnectionBlockedError({ reason: "permission", detail, traceId, hop: "relay" });
91 case "RelayEnvironmentEndpointTimedOutError":
92 return new ConnectionTransientError({
93 reason: "timeout",
94 detail,
95 traceId,
96 hop: "tunnel",
97 failureCause: "timeout",
98 });
99 case "RelayEnvironmentEndpointUnavailableError": {
100 const endpointFailure =
101 error.endpointFailure ??
102 (error.reason === "endpoint_response_invalid" ? "invalid_response" : undefined);
103 return new ConnectionTransientError({
104 reason: "endpoint-unavailable",
105 detail,
106 traceId,
107 // The tunnel carried the request when T3 Code is what failed to answer.
108 hop:
109 endpointFailure === "origin_unreachable" || endpointFailure === "invalid_response"
110 ? "server"
111 : "tunnel",
112 via: "relay",
113 ...(endpointFailure ? { failureCause: endpointFailure } : {}),
114 ...(error.endpointStatus ? { httpStatus: error.endpointStatus } : {}),
115 });
116 }
117 case "RelayEnvironmentLinkUnavailableError":
118 return new ConnectionTransientError({
119 reason: "endpoint-unavailable",
120 detail,
121 traceId,
122 hop: "tunnel",
123 });
124 case "RelayEnvironmentLinkFailedError":
125 case "RelayInternalError":
126 return new ConnectionTransientError({
127 reason: "relay-unavailable",
128 detail,
129 traceId,
130 hop: "relay",
131 failureCause: "relay_error",
132 });
133 }
135​
136export function mapManagedRelayError(error: ManagedRelayClientError): ConnectionAttemptError {
137 switch (error._tag) {
138 case "ManagedRelayRequestFailedError":
139 if (error.relayError) {
140 return relayProtectedError(error.relayError);
141 }
142 return new ConnectionTransientError({
143 reason: "relay-unavailable",
144 detail: error.message,
145 hop: "relay",
146 // A status means the relay answered, so this device did reach it.
147 ...(error.status !== undefined
148 ? { httpStatus: error.status, failureCause: "relay_error" }
149 : {}),
150 ...(error.traceId ? { traceId: error.traceId } : {}),
151 });
152 case "ManagedRelayRequestTimeoutError":
153 return new ConnectionTransientError({
154 reason: "timeout",
155 detail: error.message,
156 hop: "relay",
157 });
158 case "ManagedRelayUrlInvalidError":
159 return new ConnectionBlockedError({
160 reason: "configuration",
161 detail: error.message,
162 hop: "relay",
163 });
164 case "ManagedRelayAccessTokenScopesUnexpectedError":
165 return new ConnectionBlockedError({
166 reason: "permission",
167 detail: error.message,
168 hop: "relay",
169 });
170 case "ManagedRelayDpopKeyLoadError":
171 case "ManagedRelayTokenProofCreationError":
172 case "ManagedRelayRequestProofCreationError":
173 return new ConnectionBlockedError({
174 reason: "authentication",
175 detail: error.message,
176 hop: "device",
177 });
178 }
180​
181export function mapRemoteEnvironmentError(
182 error: RemoteEnvironmentAuthError,
183 connectionMethod: ClientConnectionMethod = "direct",
184): ConnectionAttemptError {
185 const networkHint = connectionMethod === "relay" ? ` ${NETWORK_BLOCKING_HINT}` : "";
186 // Over T3 Connect the relay already reached the host, so a request that never
187 // arrives was blocked between this device and the tunnel.
188 const unreachableHop = connectionMethod === "relay" ? "tunnel" : "server";
189 const via = connectionMethod === "relay" ? "relay" : "direct";
190 switch (error._tag) {
191 case "EnvironmentAuthInvalidError":
192 return new ConnectionBlockedError({
193 reason: "authentication",
194 detail: "The environment credential is invalid.",
195 traceId: error.traceId,
196 hop: "server",
197 via,
198 });
199 case "EnvironmentScopeRequiredError":
200 case "EnvironmentOperationForbiddenError":
201 return new ConnectionBlockedError({
202 reason: "permission",
203 detail: "The environment credential does not grant the required access.",
204 traceId: error.traceId,
205 hop: "server",
206 via,
207 });
208 case "EnvironmentRequestInvalidError":
209 return new ConnectionBlockedError({
210 reason: "configuration",
211 detail: "The environment rejected the authentication request.",
212 traceId: error.traceId,
213 hop: "server",
214 via,
215 });
216 case "EnvironmentResourceNotFoundError":
217 return new ConnectionBlockedError({
218 reason: "configuration",
219 detail: "The environment endpoint could not be found.",
220 traceId: error.traceId,
221 hop: "server",
222 via,
223 });
224 case "RemoteEnvironmentAuthTimeoutError":
225 return new ConnectionTransientError({
226 reason: "timeout",
227 detail: `${error.message}${networkHint}`,
228 hop: unreachableHop,
229 via,
230 });
231 case "RemoteEnvironmentAuthFetchError":
232 return new ConnectionTransientError({
233 reason: "network",
234 detail: `${error.message}${networkHint}`,
235 hop: unreachableHop,
236 via,
237 });
238 case "EnvironmentInternalError":
239 return new ConnectionTransientError({
240 reason: "remote-unavailable",
241 detail: "The environment could not authorize the connection.",
242 traceId: error.traceId,
243 hop: "server",
244 via,
245 });
246 case "RemoteEnvironmentAuthInvalidJsonError":
247 case "RemoteEnvironmentAuthUndeclaredStatusError":
248 return new ConnectionTransientError({
249 reason: "remote-unavailable",
250 detail: error.message,
251 hop: "server",
252 via,
253 });
254 }
256​
257/**
258 * An older environment server reports a DPoP clock failure as the same generic
259 * invalid-credential response as other failures, so keep the compatibility hint
260 * cautious when the server omits the category.
261 */
262export function mapRemoteDpopEnvironmentError(
263 error: RemoteEnvironmentAuthError,
264): ConnectionAttemptError {
265 if (error._tag === "EnvironmentAuthInvalidError" && error.reason === "invalid_credential") {
266 return new ConnectionBlockedError({
267 reason: "authentication",
268 detail: dpopFailureMessage("The environment credential is invalid.", error.dpopFailureReason),
269 traceId: error.traceId,
270 hop: "server",
271 via: "relay",
272 ...(error.dpopFailureReason === "time_window" ? { failureCause: "clock_skew" } : {}),
273 });
274 }
275 return mapRemoteEnvironmentError(error, "relay");