From 83e0a84bf75a03e801e7b50148d9807ac3e6e277 Mon Sep 17 00:00:00 2001 From: Mike Olson Date: Tue, 2 Jun 2026 23:41:14 -0400 Subject: [PATCH 1/2] fix(desktop): Preserve SSH HTTP auth status --- .../src/ipc/methods/sshEnvironment.test.ts | 36 +++++++++++ .../desktop/src/ipc/methods/sshEnvironment.ts | 60 +++++++++++++++++-- 2 files changed, 91 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/ipc/methods/sshEnvironment.test.ts b/apps/desktop/src/ipc/methods/sshEnvironment.test.ts index fa53486d5e2..a18527ebf3b 100644 --- a/apps/desktop/src/ipc/methods/sshEnvironment.test.ts +++ b/apps/desktop/src/ipc/methods/sshEnvironment.test.ts @@ -10,6 +10,7 @@ import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstab import { DesktopSshEnvironmentRequestError, fetchSshEnvironmentDescriptor, + fetchSshSessionState, } from "./sshEnvironment.ts"; function jsonResponse(request: HttpClientRequest.HttpClientRequest, body: unknown, status = 200) { @@ -112,4 +113,39 @@ describe("SSH environment IPC", () => { assert.equal(requestCount, 0); }).pipe(Effect.provide(layer)); }); + + it.effect("preserves SSH HTTP status in top-level request error messages", () => { + const layer = makeHttpClientLayer((request) => + Effect.succeed( + jsonResponse( + request, + { + _tag: "EnvironmentAuthInvalidError", + code: "auth_invalid", + reason: "invalid_credential", + traceId: "11111111111111111111111111111111", + }, + 401, + ), + ), + ); + + return Effect.gen(function* () { + const exit = yield* Effect.exit( + fetchSshSessionState.handler({ + bearerToken: "stale-bearer-token", + httpBaseUrl: "http://127.0.0.1:41773/", + }), + ); + assert(Exit.isFailure(exit)); + const failure = Cause.findErrorOption(exit.cause); + assert(Option.isSome(failure)); + const error = failure.value; + + assert.instanceOf(error, DesktopSshEnvironmentRequestError); + assert.equal(error.operation, "fetch-session-state"); + assert.equal(error.sshHttpStatus, 401); + assert.match(error.message, /^\[ssh_http:401\] /u); + }).pipe(Effect.provide(layer)); + }); }); 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({ From bbb95c70eb0f5d4be00243f2ef3ede010a6bd4cc Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 3 Jun 2026 19:06:42 -0700 Subject: [PATCH 2/2] Discard changes to apps/desktop/src/ipc/methods/sshEnvironment.test.ts --- .../src/ipc/methods/sshEnvironment.test.ts | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/apps/desktop/src/ipc/methods/sshEnvironment.test.ts b/apps/desktop/src/ipc/methods/sshEnvironment.test.ts index a18527ebf3b..fa53486d5e2 100644 --- a/apps/desktop/src/ipc/methods/sshEnvironment.test.ts +++ b/apps/desktop/src/ipc/methods/sshEnvironment.test.ts @@ -10,7 +10,6 @@ import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstab import { DesktopSshEnvironmentRequestError, fetchSshEnvironmentDescriptor, - fetchSshSessionState, } from "./sshEnvironment.ts"; function jsonResponse(request: HttpClientRequest.HttpClientRequest, body: unknown, status = 200) { @@ -113,39 +112,4 @@ describe("SSH environment IPC", () => { assert.equal(requestCount, 0); }).pipe(Effect.provide(layer)); }); - - it.effect("preserves SSH HTTP status in top-level request error messages", () => { - const layer = makeHttpClientLayer((request) => - Effect.succeed( - jsonResponse( - request, - { - _tag: "EnvironmentAuthInvalidError", - code: "auth_invalid", - reason: "invalid_credential", - traceId: "11111111111111111111111111111111", - }, - 401, - ), - ), - ); - - return Effect.gen(function* () { - const exit = yield* Effect.exit( - fetchSshSessionState.handler({ - bearerToken: "stale-bearer-token", - httpBaseUrl: "http://127.0.0.1:41773/", - }), - ); - assert(Exit.isFailure(exit)); - const failure = Cause.findErrorOption(exit.cause); - assert(Option.isSome(failure)); - const error = failure.value; - - assert.instanceOf(error, DesktopSshEnvironmentRequestError); - assert.equal(error.operation, "fetch-session-state"); - assert.equal(error.sshHttpStatus, 401); - assert.match(error.message, /^\[ssh_http:401\] /u); - }).pipe(Effect.provide(layer)); - }); });