diff --git a/apps/desktop/src/ipc/methods/sshEnvironment.ts b/apps/desktop/src/ipc/methods/sshEnvironment.ts index 9a50339c8d8..6eeaa3202d9 100644 --- a/apps/desktop/src/ipc/methods/sshEnvironment.ts +++ b/apps/desktop/src/ipc/methods/sshEnvironment.ts @@ -3,8 +3,11 @@ import { fetchRemoteEnvironmentDescriptor, fetchRemoteSessionState, issueRemoteWebSocketTicket, + RemoteEnvironmentAuthUndeclaredStatusError, + type RemoteEnvironmentAuthError, } from "@t3tools/client-runtime"; import { + EnvironmentAuthInvalidError, DesktopDiscoveredSshHostSchema, DesktopSshBearerBootstrapInputSchema, DesktopSshBearerRequestInputSchema, @@ -15,10 +18,15 @@ import { DesktopSshPasswordPromptCancelledType, DesktopSshPasswordPromptResolutionInputSchema, ExecutionEnvironmentDescriptor, + EnvironmentInternalError, + EnvironmentOperationForbiddenError, + EnvironmentRequestInvalidError, + EnvironmentScopeRequiredError, AuthAccessTokenResult, AuthSessionState, AuthWebSocketTicketResult, } from "@t3tools/contracts"; +import { SshHttpBridgeError } from "@t3tools/ssh/errors"; import { resolveLoopbackSshHttpBaseUrl } from "@t3tools/ssh/tunnel"; import * as Data from "effect/Data"; import * as Effect from "effect/Effect"; @@ -35,26 +43,68 @@ type DesktopSshEnvironmentRequestOperation = | "fetch-session-state" | "issue-websocket-ticket"; +type DesktopSshEnvironmentRequestCause = RemoteEnvironmentAuthError | SshHttpBridgeError; + +const isEnvironmentAuthInvalidError = Schema.is(EnvironmentAuthInvalidError); +const isEnvironmentInternalError = Schema.is(EnvironmentInternalError); +const isEnvironmentOperationForbiddenError = Schema.is(EnvironmentOperationForbiddenError); +const isEnvironmentRequestInvalidError = Schema.is(EnvironmentRequestInvalidError); +const isEnvironmentScopeRequiredError = Schema.is(EnvironmentScopeRequiredError); + +function readSshHttpStatus(cause: DesktopSshEnvironmentRequestCause): number | null { + if ( + cause instanceof RemoteEnvironmentAuthUndeclaredStatusError || + cause instanceof SshHttpBridgeError + ) { + return cause.status ?? null; + } + if (isEnvironmentRequestInvalidError(cause)) { + return 400; + } + if (isEnvironmentAuthInvalidError(cause)) { + return 401; + } + if (isEnvironmentScopeRequiredError(cause)) { + return 403; + } + if (isEnvironmentOperationForbiddenError(cause)) { + return 403; + } + if (isEnvironmentInternalError(cause)) { + return 500; + } + return null; +} + export class DesktopSshEnvironmentRequestError extends Data.TaggedError( "DesktopSshEnvironmentRequestError", )<{ readonly operation: DesktopSshEnvironmentRequestOperation; - readonly cause: unknown; + readonly cause: DesktopSshEnvironmentRequestCause; + readonly sshHttpStatus: number | null; }> { override get message() { - return `SSH remote API request failed during ${this.operation}.`; + const prefix = this.sshHttpStatus === null ? "" : `[ssh_http:${this.sshHttpStatus}] `; + return `${prefix}SSH remote API request failed during ${this.operation}.`; } } const withLoopbackSshApi = - ( + ( operation: DesktopSshEnvironmentRequestOperation, - use: (httpBaseUrl: string) => Effect.Effect, + use: (httpBaseUrl: string) => Effect.Effect, ) => (httpBaseUrl: string): Effect.Effect => resolveLoopbackSshHttpBaseUrl(httpBaseUrl).pipe( Effect.flatMap(use), - Effect.mapError((cause) => new DesktopSshEnvironmentRequestError({ operation, cause })), + Effect.mapError( + (cause) => + new DesktopSshEnvironmentRequestError({ + operation, + cause, + sshHttpStatus: readSshHttpStatus(cause), + }), + ), ); export const discoverSshHosts = makeIpcMethod({