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 {
33 fetchRemoteEnvironmentDescriptor,
44 fetchRemoteSessionState,
55 issueRemoteWebSocketTicket,
6 RemoteEnvironmentAuthUndeclaredStatusError,
7 type RemoteEnvironmentAuthError,
68} from "@t3tools/client-runtime";
79import {
10 EnvironmentAuthInvalidError,
811 DesktopDiscoveredSshHostSchema,
912 DesktopSshBearerBootstrapInputSchema,
1013 DesktopSshBearerRequestInputSchema,
......@@ -15,10 +18,15 @@ import {
1518 DesktopSshPasswordPromptCancelledType,
1619 DesktopSshPasswordPromptResolutionInputSchema,
1720 ExecutionEnvironmentDescriptor,
21 EnvironmentInternalError,
22 EnvironmentOperationForbiddenError,
23 EnvironmentRequestInvalidError,
24 EnvironmentScopeRequiredError,
1825 AuthAccessTokenResult,
1926 AuthSessionState,
2027 AuthWebSocketTicketResult,
2128} from "@t3tools/contracts";
29import { SshHttpBridgeError } from "@t3tools/ssh/errors";
2230import { resolveLoopbackSshHttpBaseUrl } from "@t3tools/ssh/tunnel";
2331import * as Data from "effect/Data";
2432import * as Effect from "effect/Effect";
......@@ -35,26 +43,68 @@ type DesktopSshEnvironmentRequestOperation =
3543 | "fetch-session-state"
3644 | "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
3879export class DesktopSshEnvironmentRequestError extends Data.TaggedError(
3980 "DesktopSshEnvironmentRequestError",
4081)<{
4182 readonly operation: DesktopSshEnvironmentRequestOperation;
42 readonly cause: unknown;
83 readonly cause: DesktopSshEnvironmentRequestCause;
84 readonly sshHttpStatus: number | null;
4385}> {
4486 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}.`;
4689 }
4790}
4891
4992const withLoopbackSshApi =
50 <A, E, R>(
93 <A, R>(
5194 operation: DesktopSshEnvironmentRequestOperation,
52 use: (httpBaseUrl: string) => Effect.Effect<A, E, R>,
95 use: (httpBaseUrl: string) => Effect.Effect<A, RemoteEnvironmentAuthError, R>,
5396 ) =>
5497 (httpBaseUrl: string): Effect.Effect<A, DesktopSshEnvironmentRequestError, R> =>
5598 resolveLoopbackSshHttpBaseUrl(httpBaseUrl).pipe(
5699 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 ),
58108 );
59109
60110export const discoverSshHosts = makeIpcMethod({