1import {
2 bootstrapRemoteBearerSession,
3 fetchRemoteSessionState,
4 issueRemoteWebSocketTicket,
5 RemoteEnvironmentAuthUndeclaredStatusError,
6 type RemoteEnvironmentAuthError,
7} from "@t3tools/client-runtime/authorization";
8import { fetchRemoteEnvironmentDescriptor } from "@t3tools/client-runtime/environment";
9import {
10 EnvironmentAuthInvalidError,
11 DesktopDiscoveredSshHostSchema,
12 DesktopSshBearerBootstrapInputSchema,
13 DesktopSshBearerRequestInputSchema,
14 DesktopSshEnvironmentEnsureInputSchema,
15 DesktopSshEnvironmentEnsureResultSchema,
16 DesktopSshEnvironmentTargetSchema,
17 DesktopSshHttpBaseUrlInputSchema,
18 DesktopSshPasswordPromptCancelledType,
19 DesktopSshPasswordPromptResolutionInputSchema,
20 ExecutionEnvironmentDescriptor,
21 EnvironmentInternalError,
22 EnvironmentOperationForbiddenError,
23 EnvironmentRequestInvalidError,
24 EnvironmentScopeRequiredError,
25 AuthAccessTokenResult,
26 AuthSessionState,
27 AuthWebSocketTicketResult,
28} from "@t3tools/contracts";
29import { SshHttpBridgeError } from "@t3tools/ssh/errors";
30import { resolveLoopbackSshHttpBaseUrl } from "@t3tools/ssh/tunnel";
31import * as Data from "effect/Data";
32import * as Effect from "effect/Effect";
33import * as Schema from "effect/Schema";
34
35import * as IpcChannels from "../channels.ts";
36import * as DesktopIpc from "../DesktopIpc.ts";
37import * as DesktopSshEnvironment from "../../ssh/DesktopSshEnvironment.ts";
38import * as DesktopSshPasswordPrompts from "../../ssh/DesktopSshPasswordPrompts.ts";
39
40type DesktopSshEnvironmentRequestOperation =
41 | "fetch-environment-descriptor"
42 | "bootstrap-bearer-session"
43 | "fetch-session-state"
44 | "issue-websocket-ticket";
45
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
79export class DesktopSshEnvironmentRequestError extends Data.TaggedError(
80 "DesktopSshEnvironmentRequestError",
81)<{
82 readonly operation: DesktopSshEnvironmentRequestOperation;
83 readonly cause: DesktopSshEnvironmentRequestCause;
84 readonly sshHttpStatus: number | null;
85}> {
86 override get message() {
87 const prefix = this.sshHttpStatus === null ? "" : `[ssh_http:${this.sshHttpStatus}] `;
88 return `${prefix}SSH remote API request failed during ${this.operation}.`;
89 }
90}
91
92const withLoopbackSshApi =
93 <A, R>(
94 operation: DesktopSshEnvironmentRequestOperation,
95 use: (httpBaseUrl: string) => Effect.Effect<A, RemoteEnvironmentAuthError, R>,
96 ) =>
97 (httpBaseUrl: string): Effect.Effect<A, DesktopSshEnvironmentRequestError, R> =>
98 resolveLoopbackSshHttpBaseUrl(httpBaseUrl).pipe(
99 Effect.flatMap(use),
100 Effect.mapError(
101 (cause) =>
102 new DesktopSshEnvironmentRequestError({
103 operation,
104 cause,
105 sshHttpStatus: readSshHttpStatus(cause),
106 }),
107 ),
108 );
109
110export const discoverSshHosts = DesktopIpc.makeIpcMethod({
111 channel: IpcChannels.DISCOVER_SSH_HOSTS_CHANNEL,
112 payload: Schema.Void,
113 result: Schema.Array(DesktopDiscoveredSshHostSchema),
114 handler: Effect.fn("desktop.ipc.sshEnvironment.discoverHosts")(function* () {
115 const sshEnvironment = yield* DesktopSshEnvironment.DesktopSshEnvironment;
116 return yield* sshEnvironment.discoverHosts();
117 }),
118});
119
120export const resolveSshHost = DesktopIpc.makeIpcMethod({
121 channel: IpcChannels.RESOLVE_SSH_HOST_CHANNEL,
122 payload: Schema.String,
123 result: DesktopSshEnvironmentTargetSchema,
124 handler: Effect.fn("desktop.ipc.sshEnvironment.resolveHost")(function* (alias) {
125 const sshEnvironment = yield* DesktopSshEnvironment.DesktopSshEnvironment;
126 return yield* sshEnvironment.resolveHost(alias);
127 }),
128});
129
130export const ensureSshEnvironment = DesktopIpc.makeIpcMethod({
131 channel: IpcChannels.ENSURE_SSH_ENVIRONMENT_CHANNEL,
132 payload: DesktopSshEnvironmentEnsureInputSchema,
133 result: DesktopSshEnvironmentEnsureResultSchema,
134 handler: Effect.fn("desktop.ipc.sshEnvironment.ensureEnvironment")(function* ({
135 target,
136 options,
137 }) {
138 const sshEnvironment = yield* DesktopSshEnvironment.DesktopSshEnvironment;
139 return yield* sshEnvironment.ensureEnvironment(target, options).pipe(
140 Effect.catchIf(DesktopSshEnvironment.isDesktopSshPasswordPromptCancellation, (error) =>
141 Effect.succeed({
142 type: DesktopSshPasswordPromptCancelledType,
143 message: error.message,
144 }),
145 ),
146 );
147 }),
148});
149
150export const disconnectSshEnvironment = DesktopIpc.makeIpcMethod({
151 channel: IpcChannels.DISCONNECT_SSH_ENVIRONMENT_CHANNEL,
152 payload: DesktopSshEnvironmentTargetSchema,
153 result: Schema.Void,
154 handler: Effect.fn("desktop.ipc.sshEnvironment.disconnectEnvironment")(function* (target) {
155 const sshEnvironment = yield* DesktopSshEnvironment.DesktopSshEnvironment;
156 yield* sshEnvironment.disconnectEnvironment(target);
157 }),
158});
159
160export const fetchSshEnvironmentDescriptor = DesktopIpc.makeIpcMethod({
161 channel: IpcChannels.FETCH_SSH_ENVIRONMENT_DESCRIPTOR_CHANNEL,
162 payload: DesktopSshHttpBaseUrlInputSchema,
163 result: ExecutionEnvironmentDescriptor,
164 handler: Effect.fn("desktop.ipc.sshEnvironment.fetchDescriptor")(function* ({ httpBaseUrl }) {
165 return yield* withLoopbackSshApi("fetch-environment-descriptor", (resolvedHttpBaseUrl) =>
166 fetchRemoteEnvironmentDescriptor({ httpBaseUrl: resolvedHttpBaseUrl }),
167 )(httpBaseUrl);
168 }),
169});
170
171export const bootstrapSshBearerSession = DesktopIpc.makeIpcMethod({
172 channel: IpcChannels.BOOTSTRAP_SSH_BEARER_SESSION_CHANNEL,
173 payload: DesktopSshBearerBootstrapInputSchema,
174 result: AuthAccessTokenResult,
175 handler: Effect.fn("desktop.ipc.sshEnvironment.bootstrapBearerSession")(function* ({
176 httpBaseUrl,
177 credential,
178 }) {
179 return yield* withLoopbackSshApi("bootstrap-bearer-session", (resolvedHttpBaseUrl) =>
180 bootstrapRemoteBearerSession({
181 httpBaseUrl: resolvedHttpBaseUrl,
182 credential,
183 }),
184 )(httpBaseUrl);
185 }),
186});
187
188export const fetchSshSessionState = DesktopIpc.makeIpcMethod({
189 channel: IpcChannels.FETCH_SSH_SESSION_STATE_CHANNEL,
190 payload: DesktopSshBearerRequestInputSchema,
191 result: AuthSessionState,
192 handler: Effect.fn("desktop.ipc.sshEnvironment.fetchSessionState")(function* ({
193 httpBaseUrl,
194 bearerToken,
195 }) {
196 return yield* withLoopbackSshApi("fetch-session-state", (resolvedHttpBaseUrl) =>
197 fetchRemoteSessionState({
198 httpBaseUrl: resolvedHttpBaseUrl,
199 bearerToken,
200 }),
201 )(httpBaseUrl);
202 }),
203});
204
205export const issueSshWebSocketTicket = DesktopIpc.makeIpcMethod({
206 channel: IpcChannels.ISSUE_SSH_WEBSOCKET_TOKEN_CHANNEL,
207 payload: DesktopSshBearerRequestInputSchema,
208 result: AuthWebSocketTicketResult,
209 handler: Effect.fn("desktop.ipc.sshEnvironment.issueWebSocketTicket")(function* ({
210 httpBaseUrl,
211 bearerToken,
212 }) {
213 return yield* withLoopbackSshApi("issue-websocket-ticket", (resolvedHttpBaseUrl) =>
214 issueRemoteWebSocketTicket({
215 httpBaseUrl: resolvedHttpBaseUrl,
216 bearerToken,
217 }),
218 )(httpBaseUrl);
219 }),
220});
221
222export const resolveSshPasswordPrompt = DesktopIpc.makeIpcMethod({
223 channel: IpcChannels.RESOLVE_SSH_PASSWORD_PROMPT_CHANNEL,
224 payload: DesktopSshPasswordPromptResolutionInputSchema,
225 result: Schema.Void,
226 handler: Effect.fn("desktop.ipc.sshEnvironment.resolvePasswordPrompt")(function* ({
227 requestId,
228 password,
229 }) {
230 const prompts = yield* DesktopSshPasswordPrompts.DesktopSshPasswordPrompts;
231 yield* prompts.resolve({ requestId, password });
232 }),
233});