authorgravatar for mwolson@member.fsf.orgMike Olson <mwolson@member.fsf.org> 2026-06-03 22:10:17-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-04 02:10:17+00:00
log4956415f7fb5413583fc43d4b370ceb67e6e3eee
tree4c8e37f7def050ee40832c114785eb3029c7d92f
parentf5849f7d78e4ebea277c4a9b5086d592f2aae3a4
signature Signed by PGP key B5690EEEBB952194

fix(desktop): Preserve SSH HTTP auth status (#2923)

Co-authored-by: Julius Marminge <jmarminge@gmail.com>

1 files changed, 55 insertions(+), 5 deletions(-)

apps/desktop/src/ipc/methods/sshEnvironment.ts+55-5
...@@ -3,8 +3,11 @@ import {...@@ -3,8 +3,11 @@ import {
3 fetchRemoteEnvironmentDescriptor,3 fetchRemoteEnvironmentDescriptor,
4 fetchRemoteSessionState,4 fetchRemoteSessionState,
5 issueRemoteWebSocketTicket,5 issueRemoteWebSocketTicket,
6 RemoteEnvironmentAuthUndeclaredStatusError,
7 type RemoteEnvironmentAuthError,
6} from "@t3tools/client-runtime";8} from "@t3tools/client-runtime";
7import {9import {
10 EnvironmentAuthInvalidError,
8 DesktopDiscoveredSshHostSchema,11 DesktopDiscoveredSshHostSchema,
9 DesktopSshBearerBootstrapInputSchema,12 DesktopSshBearerBootstrapInputSchema,
10 DesktopSshBearerRequestInputSchema,13 DesktopSshBearerRequestInputSchema,
...@@ -15,10 +18,15 @@ import {...@@ -15,10 +18,15 @@ import {
15 DesktopSshPasswordPromptCancelledType,18 DesktopSshPasswordPromptCancelledType,
16 DesktopSshPasswordPromptResolutionInputSchema,19 DesktopSshPasswordPromptResolutionInputSchema,
17 ExecutionEnvironmentDescriptor,20 ExecutionEnvironmentDescriptor,
21 EnvironmentInternalError,
22 EnvironmentOperationForbiddenError,
23 EnvironmentRequestInvalidError,
24 EnvironmentScopeRequiredError,
18 AuthAccessTokenResult,25 AuthAccessTokenResult,
19 AuthSessionState,26 AuthSessionState,
20 AuthWebSocketTicketResult,27 AuthWebSocketTicketResult,
21} from "@t3tools/contracts";28} from "@t3tools/contracts";
29import { SshHttpBridgeError } from "@t3tools/ssh/errors";
22import { resolveLoopbackSshHttpBaseUrl } from "@t3tools/ssh/tunnel";30import { resolveLoopbackSshHttpBaseUrl } from "@t3tools/ssh/tunnel";
23import * as Data from "effect/Data";31import * as Data from "effect/Data";
24import * as Effect from "effect/Effect";32import * as Effect from "effect/Effect";
...@@ -35,26 +43,68 @@ type DesktopSshEnvironmentRequestOperation =...@@ -35,26 +43,68 @@ type DesktopSshEnvironmentRequestOperation =
35 | "fetch-session-state"43 | "fetch-session-state"
36 | "issue-websocket-ticket";44 | "issue-websocket-ticket";
3745
46type DesktopSshEnvironmentRequestCause = RemoteEnvironmentAuthError | SshHttpBridgeError;
47
48const isEnvironmentAuthInvalidError = Schema.is(EnvironmentAuthInvalidError);
49const isEnvironmentInternalError = Schema.is(EnvironmentInternalError);
50const isEnvironmentOperationForbiddenError = Schema.is(EnvironmentOperationForbiddenError);
51const isEnvironmentRequestInvalidError = Schema.is(EnvironmentRequestInvalidError);
52const isEnvironmentScopeRequiredError = Schema.is(EnvironmentScopeRequiredError);
53
54function readSshHttpStatus(cause: DesktopSshEnvironmentRequestCause): number | null {
55 if (
56 cause instanceof RemoteEnvironmentAuthUndeclaredStatusError ||
57 cause instanceof SshHttpBridgeError
58 ) {
59 return cause.status ?? null;
60 }
61 if (isEnvironmentRequestInvalidError(cause)) {
62 return 400;
63 }
64 if (isEnvironmentAuthInvalidError(cause)) {
65 return 401;
66 }
67 if (isEnvironmentScopeRequiredError(cause)) {
68 return 403;
69 }
70 if (isEnvironmentOperationForbiddenError(cause)) {
71 return 403;
72 }
73 if (isEnvironmentInternalError(cause)) {
74 return 500;
75 }
76 return null;
77}
78
38export class DesktopSshEnvironmentRequestError extends Data.TaggedError(79export class DesktopSshEnvironmentRequestError extends Data.TaggedError(
39 "DesktopSshEnvironmentRequestError",80 "DesktopSshEnvironmentRequestError",
40)<{81)<{
41 readonly operation: DesktopSshEnvironmentRequestOperation;82 readonly operation: DesktopSshEnvironmentRequestOperation;
42 readonly cause: unknown;83 readonly cause: DesktopSshEnvironmentRequestCause;
84 readonly sshHttpStatus: number | null;
43}> {85}> {
44 override get message() {86 override get message() {
45 return `SSH remote API request failed during ${this.operation}.`;87 const prefix = this.sshHttpStatus === null ? "" : `[ssh_http:${this.sshHttpStatus}] `;
88 return `${prefix}SSH remote API request failed during ${this.operation}.`;
46 }89 }
47}90}
4891
49const withLoopbackSshApi =92const withLoopbackSshApi =
50 <A, E, R>(93 <A, R>(
51 operation: DesktopSshEnvironmentRequestOperation,94 operation: DesktopSshEnvironmentRequestOperation,
52 use: (httpBaseUrl: string) => Effect.Effect<A, E, R>,95 use: (httpBaseUrl: string) => Effect.Effect<A, RemoteEnvironmentAuthError, R>,
53 ) =>96 ) =>
54 (httpBaseUrl: string): Effect.Effect<A, DesktopSshEnvironmentRequestError, R> =>97 (httpBaseUrl: string): Effect.Effect<A, DesktopSshEnvironmentRequestError, R> =>
55 resolveLoopbackSshHttpBaseUrl(httpBaseUrl).pipe(98 resolveLoopbackSshHttpBaseUrl(httpBaseUrl).pipe(
56 Effect.flatMap(use),99 Effect.flatMap(use),
57 Effect.mapError((cause) => new DesktopSshEnvironmentRequestError({ operation, cause })),100 Effect.mapError(
101 (cause) =>
102 new DesktopSshEnvironmentRequestError({
103 operation,
104 cause,
105 sshHttpStatus: readSshHttpStatus(cause),
106 }),
107 ),
58 );108 );
59109
60export const discoverSshHosts = makeIpcMethod({110export const discoverSshHosts = makeIpcMethod({