authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 00:36:41-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 00:36:41-07:00
log7eda6ba5fce8290b3a13d1ab730827fc36a34e53
treeef3b23b49b1fc7dbc81a6e2acea428090f45ade1
parentc00e721c047ab0b832401196e64df26431fd9c98
signature Signed by PGP key B5690EEEBB952194

[codex] Migrate desktop shell and SSH Effect services (#3194)

Co-authored-by: codex <codex@users.noreply.github.com>

4 files changed, 240 insertions(+), 160 deletions(-)

apps/desktop/src/shell/DesktopShellEnvironment.ts+22-23
......@@ -3,7 +3,8 @@ import * as Duration from "effect/Duration";
33import * as Effect from "effect/Effect";
44import * as Layer from "effect/Layer";
55import * as Option from "effect/Option";
6import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
6import * as ChildProcess from "effect/unstable/process/ChildProcess";
7import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
78
89import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
910
......@@ -19,13 +20,11 @@ interface WindowsProbeOptions {
1920 readonly loadProfile: boolean;
2021}
2122
22export interface DesktopShellEnvironmentShape {
23 readonly installIntoProcess: Effect.Effect<void, never>;
24}
25
2623export class DesktopShellEnvironment extends Context.Service<
2724 DesktopShellEnvironment,
28 DesktopShellEnvironmentShape
25 {
26 readonly installIntoProcess: Effect.Effect<void>;
27 }
2928>()("@t3tools/desktop/shell/DesktopShellEnvironment") {}
3029
3130const LOGIN_SHELL_ENV_NAMES = [
......@@ -336,20 +335,20 @@ const installShellEnvironment = (
336335 return Effect.void;
337336};
338337
339export const layer = Layer.effect(
340 DesktopShellEnvironment,
341 Effect.gen(function* () {
342 const environment = yield* DesktopEnvironment.DesktopEnvironment;
343 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
344 return DesktopShellEnvironment.of({
345 installIntoProcess: installShellEnvironment({
346 env: process.env,
347 platform: environment.platform,
348 userShell: Option.none(),
349 }).pipe(
350 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
351 Effect.withSpan("desktop.shellEnvironment.installIntoProcess"),
352 ),
353 });
354 }),
355);
338export const make = Effect.gen(function* () {
339 const environment = yield* DesktopEnvironment.DesktopEnvironment;
340 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
341 const installIntoProcess: DesktopShellEnvironment["Service"]["installIntoProcess"] =
342 installShellEnvironment({
343 env: process.env,
344 platform: environment.platform,
345 userShell: Option.none(),
346 }).pipe(
347 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
348 Effect.withSpan("desktop.shellEnvironment.installIntoProcess"),
349 );
350
351 return DesktopShellEnvironment.of({ installIntoProcess });
352});
353
354export const layer = Layer.effect(DesktopShellEnvironment, make);
apps/desktop/src/ssh/DesktopSshEnvironment.test.ts+14-1
......@@ -19,6 +19,20 @@ function makeTempHomeDir() {
1919}
2020
2121describe("sshEnvironment", () => {
22 it("keeps prompt presentation diagnostics distinct from the legacy wrapper message", () => {
23 const cause = new DesktopSshPasswordPrompts.DesktopSshPromptPresentationError({
24 requestId: "prompt-1",
25 destination: "devbox",
26 cause: new Error("renderer send failed"),
27 });
28
29 assert.equal(cause.message, "Failed to present SSH password prompt for devbox.");
30 assert.equal(
31 DesktopSshEnvironment.toSshPasswordPromptError(cause).message,
32 "T3 Code window is not available for SSH authentication.",
33 );
34 });
35
2236 it("treats password prompt timeouts as cancellable authentication prompts", () => {
2337 assert.equal(
2438 DesktopSshEnvironment.isDesktopSshPasswordPromptCancellation(
......@@ -104,7 +118,6 @@ describe("sshEnvironment", () => {
104118 Layer.succeed(DesktopSshPasswordPrompts.DesktopSshPasswordPrompts, {
105119 request: () => Effect.die("unexpected password prompt request"),
106120 resolve: () => Effect.die("unexpected password prompt resolution"),
107 cancelPending: () => Effect.void,
108121 }),
109122 ),
110123 Layer.provideMerge(NodeServices.layer),
apps/desktop/src/ssh/DesktopSshEnvironment.ts+61-41
......@@ -4,11 +4,7 @@ import type {
44 DesktopSshEnvironmentTarget,
55} from "@t3tools/contracts";
66import * as NetService from "@t3tools/shared/Net";
7import {
8 SshPasswordPrompt,
9 type SshPasswordPromptShape,
10 type SshPasswordRequest,
11} from "@t3tools/ssh/auth";
7import * as SshAuth from "@t3tools/ssh/auth";
128import { discoverSshHosts } from "@t3tools/ssh/config";
139import {
1410 SshCommandError,
......@@ -19,14 +15,14 @@ import {
1915 SshPasswordPromptError,
2016 SshReadinessError,
2117} from "@t3tools/ssh/errors";
22import { SshEnvironmentManager, type RemoteT3RunnerOptions } from "@t3tools/ssh/tunnel";
18import * as SshTunnel from "@t3tools/ssh/tunnel";
2319import * as Context from "effect/Context";
2420import * as Effect from "effect/Effect";
2521import * as FileSystem from "effect/FileSystem";
2622import * as Layer from "effect/Layer";
2723import * as Path from "effect/Path";
28import { HttpClient } from "effect/unstable/http";
29import { ChildProcessSpawner } from "effect/unstable/process";
24import * as HttpClient from "effect/unstable/http/HttpClient";
25import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
3026
3127import * as DesktopSshPasswordPrompts from "./DesktopSshPasswordPrompts.ts";
3228
......@@ -52,27 +48,25 @@ export type DesktopSshEnvironmentError =
5248 | DesktopSshEnvironmentDiscoverError
5349 | DesktopSshEnvironmentOperationError;
5450
55export interface DesktopSshEnvironmentShape {
56 readonly discoverHosts: (input?: {
57 readonly homeDir?: string;
58 }) => Effect.Effect<readonly DesktopDiscoveredSshHost[], DesktopSshEnvironmentDiscoverError>;
59 readonly ensureEnvironment: (
60 target: DesktopSshEnvironmentTarget,
61 options?: { readonly issuePairingToken?: boolean },
62 ) => Effect.Effect<DesktopSshEnvironmentBootstrap, DesktopSshEnvironmentOperationError>;
63 readonly disconnectEnvironment: (
64 target: DesktopSshEnvironmentTarget,
65 ) => Effect.Effect<void, DesktopSshEnvironmentOperationError>;
66}
67
6851export class DesktopSshEnvironment extends Context.Service<
6952 DesktopSshEnvironment,
70 DesktopSshEnvironmentShape
53 {
54 readonly discoverHosts: (input?: {
55 readonly homeDir?: string;
56 }) => Effect.Effect<readonly DesktopDiscoveredSshHost[], DesktopSshEnvironmentDiscoverError>;
57 readonly ensureEnvironment: (
58 target: DesktopSshEnvironmentTarget,
59 options?: { readonly issuePairingToken?: boolean },
60 ) => Effect.Effect<DesktopSshEnvironmentBootstrap, DesktopSshEnvironmentOperationError>;
61 readonly disconnectEnvironment: (
62 target: DesktopSshEnvironmentTarget,
63 ) => Effect.Effect<void, DesktopSshEnvironmentOperationError>;
64 }
7165>()("@t3tools/desktop/ssh/DesktopSshEnvironment") {}
7266
7367export interface DesktopSshEnvironmentLayerOptions {
7468 readonly resolveCliPackageSpec?: () => string;
75 readonly resolveCliRunner?: Effect.Effect<RemoteT3RunnerOptions>;
69 readonly resolveCliRunner?: Effect.Effect<SshTunnel.RemoteT3RunnerOptions>;
7670}
7771
7872function discoverDesktopSshHostsEffect(input?: { readonly homeDir?: string }) {
......@@ -88,27 +82,53 @@ export function isDesktopSshPasswordPromptCancellation(
8882 );
8983}
9084
85function unexpectedPasswordPromptError(error: never): never {
86 throw new Error(`Unhandled desktop SSH password prompt error: ${String(error)}`);
87}
88
89export function toSshPasswordPromptError(
90 cause: DesktopSshPasswordPrompts.DesktopSshPasswordPromptRequestError,
91): SshPasswordPromptError {
92 let message: string;
93 switch (cause._tag) {
94 case "DesktopSshPromptRequestIdGenerationError":
95 message = "Secure randomness is unavailable.";
96 break;
97 case "DesktopSshPromptWindowUnavailableError":
98 case "DesktopSshPromptPresentationError":
99 message = "T3 Code window is not available for SSH authentication.";
100 break;
101 case "DesktopSshPromptTimedOutError":
102 message = `SSH authentication timed out for ${cause.destination}.`;
103 break;
104 case "DesktopSshPromptCancelledError":
105 message = `SSH authentication cancelled for ${cause.destination}.`;
106 break;
107 case "DesktopSshPromptWindowClosedError":
108 message = "SSH authentication was cancelled because the app window closed.";
109 break;
110 case "DesktopSshPromptServiceStoppedError":
111 message = "SSH password prompt service stopped.";
112 break;
113 default:
114 return unexpectedPasswordPromptError(cause);
115 }
116 return new SshPasswordPromptError({ message, cause });
117}
118
91119const makePasswordPrompt = (
92 prompts: DesktopSshPasswordPrompts.DesktopSshPasswordPromptsShape,
93): SshPasswordPromptShape => ({
120 prompts: DesktopSshPasswordPrompts.DesktopSshPasswordPrompts["Service"],
121): SshAuth.SshPasswordPrompt["Service"] => ({
94122 isAvailable: true,
95 request: (request: SshPasswordRequest) =>
96 prompts.request(request).pipe(
97 Effect.mapError(
98 (cause) =>
99 new SshPasswordPromptError({
100 message: cause.message,
101 cause,
102 }),
103 ),
104 ),
123 request: (request: SshAuth.SshPasswordRequest) =>
124 prompts.request(request).pipe(Effect.mapError(toSshPasswordPromptError)),
105125});
106126
107const make = Effect.gen(function* () {
108 const manager = yield* SshEnvironmentManager;
127export const make = Effect.gen(function* () {
128 const manager = yield* SshTunnel.SshEnvironmentManager;
109129 const prompts = yield* DesktopSshPasswordPrompts.DesktopSshPasswordPrompts;
110130 const runtimeContext = yield* Effect.context<DesktopSshEnvironmentRuntimeServices>();
111 const passwordPrompt = SshPasswordPrompt.of(makePasswordPrompt(prompts));
131 const passwordPrompt = SshAuth.SshPasswordPrompt.of(makePasswordPrompt(prompts));
112132
113133 return DesktopSshEnvironment.of({
114134 discoverHosts: (input) =>
......@@ -120,7 +140,7 @@ const make = Effect.gen(function* () {
120140 manager
121141 .ensureEnvironment(target, ensureOptions)
122142 .pipe(
123 Effect.provideService(SshPasswordPrompt, passwordPrompt),
143 Effect.provideService(SshAuth.SshPasswordPrompt, passwordPrompt),
124144 Effect.provide(runtimeContext),
125145 Effect.withSpan("desktop.ssh.ensureEnvironment"),
126146 ),
......@@ -128,7 +148,7 @@ const make = Effect.gen(function* () {
128148 manager
129149 .disconnectEnvironment(target)
130150 .pipe(
131 Effect.provideService(SshPasswordPrompt, passwordPrompt),
151 Effect.provideService(SshAuth.SshPasswordPrompt, passwordPrompt),
132152 Effect.provide(runtimeContext),
133153 Effect.withSpan("desktop.ssh.disconnectEnvironment"),
134154 ),
......@@ -138,7 +158,7 @@ const make = Effect.gen(function* () {
138158export const layer = (options: DesktopSshEnvironmentLayerOptions = {}) =>
139159 Layer.effect(DesktopSshEnvironment, make).pipe(
140160 Layer.provide(
141 SshEnvironmentManager.layer({
161 SshTunnel.SshEnvironmentManager.layer({
142162 ...(options.resolveCliPackageSpec === undefined
143163 ? {}
144164 : { resolveCliPackageSpec: options.resolveCliPackageSpec }),
apps/desktop/src/ssh/DesktopSshPasswordPrompts.ts+143-95
......@@ -3,7 +3,6 @@ import { DesktopSshPasswordPromptResolutionInputSchema } from "@t3tools/contract
33import type { SshPasswordRequest } from "@t3tools/ssh/auth";
44import * as Context from "effect/Context";
55import * as Crypto from "effect/Crypto";
6import * as Data from "effect/Data";
76import * as DateTime from "effect/DateTime";
87import * as Deferred from "effect/Deferred";
98import * as Duration from "effect/Duration";
......@@ -11,93 +10,134 @@ import * as Effect from "effect/Effect";
1110import * as Layer from "effect/Layer";
1211import * as Option from "effect/Option";
1312import * as Ref from "effect/Ref";
13import * as Schema from "effect/Schema";
1414
15import * as IpcChannels from "../ipc/channels.ts";
1615import * as ElectronWindow from "../electron/ElectronWindow.ts";
16import { SSH_PASSWORD_PROMPT_CHANNEL } from "../ipc/channels.ts";
1717
1818const DEFAULT_SSH_PASSWORD_PROMPT_TIMEOUT_MS = 3 * 60 * 1000;
19const WINDOW_UNAVAILABLE_MESSAGE = "T3 Code window is not available for SSH authentication.";
2019
2120type DesktopSshPasswordPromptResolutionInput =
2221 typeof DesktopSshPasswordPromptResolutionInputSchema.Type;
2322
24export class DesktopSshPromptUnavailableError extends Data.TaggedError(
25 "DesktopSshPromptUnavailableError",
26)<{
27 readonly reason: string;
28}> {
29 override get message() {
30 return this.reason;
23const WINDOW_UNAVAILABLE_MESSAGE = "T3 Code window is not available for SSH authentication.";
24
25export class DesktopSshPromptRequestIdGenerationError extends Schema.TaggedErrorClass<DesktopSshPromptRequestIdGenerationError>()(
26 "DesktopSshPromptRequestIdGenerationError",
27 {
28 destination: Schema.String,
29 cause: Schema.Defect(),
30 },
31) {
32 override get message(): string {
33 return "Secure randomness is unavailable.";
3134 }
3235}
3336
34export class DesktopSshPromptWindowUnavailableError extends Data.TaggedError(
37export class DesktopSshPromptWindowUnavailableError extends Schema.TaggedErrorClass<DesktopSshPromptWindowUnavailableError>()(
3538 "DesktopSshPromptWindowUnavailableError",
36)<{
37 readonly destination: string;
38}> {
39 override get message() {
39 {
40 destination: Schema.String,
41 },
42) {
43 override get message(): string {
4044 return WINDOW_UNAVAILABLE_MESSAGE;
4145 }
4246}
4347
44export class DesktopSshPromptSendError extends Data.TaggedError("DesktopSshPromptSendError")<{
45 readonly requestId: string;
46 readonly destination: string;
47 readonly cause: unknown;
48}> {
49 override get message() {
50 return WINDOW_UNAVAILABLE_MESSAGE;
48const isDesktopSshPromptWindowUnavailableError = Schema.is(DesktopSshPromptWindowUnavailableError);
49
50export class DesktopSshPromptPresentationError extends Schema.TaggedErrorClass<DesktopSshPromptPresentationError>()(
51 "DesktopSshPromptPresentationError",
52 {
53 requestId: Schema.String,
54 destination: Schema.String,
55 cause: Schema.Defect(),
56 },
57) {
58 override get message(): string {
59 return `Failed to present SSH password prompt for ${this.destination}.`;
5160 }
5261}
5362
54export class DesktopSshPromptTimedOutError extends Data.TaggedError(
63export class DesktopSshPromptTimedOutError extends Schema.TaggedErrorClass<DesktopSshPromptTimedOutError>()(
5564 "DesktopSshPromptTimedOutError",
56)<{
57 readonly requestId: string;
58 readonly destination: string;
59}> {
60 override get message() {
65 {
66 requestId: Schema.String,
67 destination: Schema.String,
68 },
69) {
70 override get message(): string {
6171 return `SSH authentication timed out for ${this.destination}.`;
6272 }
6373}
6474
65export class DesktopSshPromptCancelledError extends Data.TaggedError(
75export class DesktopSshPromptCancelledError extends Schema.TaggedErrorClass<DesktopSshPromptCancelledError>()(
6676 "DesktopSshPromptCancelledError",
67)<{
68 readonly requestId: string;
69 readonly destination: string;
70 readonly reason: string;
71}> {
72 override get message() {
73 return this.reason;
77 {
78 requestId: Schema.String,
79 destination: Schema.String,
80 },
81) {
82 override get message(): string {
83 return `SSH authentication cancelled for ${this.destination}.`;
7484 }
7585}
7686
77export class DesktopSshPromptInvalidRequestIdError extends Data.TaggedError(
87export class DesktopSshPromptWindowClosedError extends Schema.TaggedErrorClass<DesktopSshPromptWindowClosedError>()(
88 "DesktopSshPromptWindowClosedError",
89 {
90 requestId: Schema.String,
91 destination: Schema.String,
92 },
93) {
94 override get message(): string {
95 return "SSH authentication was cancelled because the app window closed.";
96 }
97}
98
99export class DesktopSshPromptServiceStoppedError extends Schema.TaggedErrorClass<DesktopSshPromptServiceStoppedError>()(
100 "DesktopSshPromptServiceStoppedError",
101 {
102 requestId: Schema.String,
103 destination: Schema.String,
104 },
105) {
106 override get message(): string {
107 return "SSH password prompt service stopped.";
108 }
109}
110
111export class DesktopSshPromptInvalidRequestIdError extends Schema.TaggedErrorClass<DesktopSshPromptInvalidRequestIdError>()(
78112 "DesktopSshPromptInvalidRequestIdError",
79)<{
80 readonly requestId: string;
81}> {
82 override get message() {
113 {
114 requestId: Schema.String,
115 },
116) {
117 override get message(): string {
83118 return "Invalid SSH password prompt id.";
84119 }
85120}
86121
87export class DesktopSshPromptExpiredError extends Data.TaggedError("DesktopSshPromptExpiredError")<{
88 readonly requestId: string;
89}> {
90 override get message() {
122export class DesktopSshPromptExpiredError extends Schema.TaggedErrorClass<DesktopSshPromptExpiredError>()(
123 "DesktopSshPromptExpiredError",
124 {
125 requestId: Schema.String,
126 },
127) {
128 override get message(): string {
91129 return "SSH password prompt expired. Try connecting again.";
92130 }
93131}
94132
95133export type DesktopSshPasswordPromptRequestError =
96 | DesktopSshPromptUnavailableError
134 | DesktopSshPromptRequestIdGenerationError
97135 | DesktopSshPromptWindowUnavailableError
98 | DesktopSshPromptSendError
136 | DesktopSshPromptPresentationError
99137 | DesktopSshPromptTimedOutError
100 | DesktopSshPromptCancelledError;
138 | DesktopSshPromptCancelledError
139 | DesktopSshPromptWindowClosedError
140 | DesktopSshPromptServiceStoppedError;
101141
102142export type DesktopSshPasswordPromptResolveError =
103143 | DesktopSshPromptInvalidRequestIdError
......@@ -107,28 +147,28 @@ export type DesktopSshPasswordPromptError =
107147 | DesktopSshPasswordPromptRequestError
108148 | DesktopSshPasswordPromptResolveError;
109149
110export function isDesktopSshPasswordPromptCancellation(
111 error: unknown,
112): error is DesktopSshPromptCancelledError | DesktopSshPromptTimedOutError {
113 return (
114 error instanceof DesktopSshPromptCancelledError ||
115 error instanceof DesktopSshPromptTimedOutError
116 );
117}
150export const DesktopSshPasswordPromptCancellation = Schema.Union([
151 DesktopSshPromptCancelledError,
152 DesktopSshPromptWindowClosedError,
153 DesktopSshPromptServiceStoppedError,
154 DesktopSshPromptTimedOutError,
155]);
156export type DesktopSshPasswordPromptCancellation = typeof DesktopSshPasswordPromptCancellation.Type;
118157
119export interface DesktopSshPasswordPromptsShape {
120 readonly request: (
121 request: SshPasswordRequest,
122 ) => Effect.Effect<string, DesktopSshPasswordPromptRequestError>;
123 readonly resolve: (
124 input: DesktopSshPasswordPromptResolutionInput,
125 ) => Effect.Effect<void, DesktopSshPasswordPromptResolveError>;
126 readonly cancelPending: (reason: string) => Effect.Effect<void>;
127}
158export const isDesktopSshPasswordPromptCancellation = Schema.is(
159 DesktopSshPasswordPromptCancellation,
160);
128161
129162export class DesktopSshPasswordPrompts extends Context.Service<
130163 DesktopSshPasswordPrompts,
131 DesktopSshPasswordPromptsShape
164 {
165 readonly request: (
166 request: SshPasswordRequest,
167 ) => Effect.Effect<string, DesktopSshPasswordPromptRequestError>;
168 readonly resolve: (
169 input: DesktopSshPasswordPromptResolutionInput,
170 ) => Effect.Effect<void, DesktopSshPasswordPromptResolveError>;
171 }
132172>()("@t3tools/desktop/ssh/DesktopSshPasswordPrompts") {}
133173
134174interface PendingSshPasswordPrompt {
......@@ -137,7 +177,7 @@ interface PendingSshPasswordPrompt {
137177 readonly deferred: Deferred.Deferred<string, DesktopSshPasswordPromptRequestError>;
138178}
139179
140interface LayerOptions {
180export interface DesktopSshPasswordPromptsOptions {
141181 readonly passwordPromptTimeoutMs?: number;
142182}
143183
......@@ -161,14 +201,16 @@ const failPending = (
161201 error: DesktopSshPasswordPromptRequestError,
162202) => Deferred.fail(pending.deferred, error).pipe(Effect.asVoid);
163203
164const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* (options: LayerOptions = {}) {
204export const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* (
205 options: DesktopSshPasswordPromptsOptions = {},
206) {
165207 const electronWindow = yield* ElectronWindow.ElectronWindow;
166208 const crypto = yield* Crypto.Crypto;
167209 const pendingRef = yield* Ref.make(new Map<string, PendingSshPasswordPrompt>());
168210 const passwordPromptTimeoutMs =
169211 options.passwordPromptTimeoutMs ?? DEFAULT_SSH_PASSWORD_PROMPT_TIMEOUT_MS;
170212
171 const cancelPending = (reason: string): Effect.Effect<void> =>
213 const cancelPending = () =>
172214 Ref.getAndSet(pendingRef, new Map()).pipe(
173215 Effect.flatMap((pending) =>
174216 Effect.forEach(
......@@ -176,10 +218,9 @@ const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* (options: La
176218 (entry) =>
177219 failPending(
178220 entry,
179 new DesktopSshPromptCancelledError({
221 new DesktopSshPromptServiceStoppedError({
180222 requestId: entry.requestId,
181223 destination: entry.destination,
182 reason,
183224 }),
184225 ),
185226 { discard: true },
......@@ -188,13 +229,11 @@ const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* (options: La
188229 Effect.asVoid,
189230 );
190231
191 yield* Effect.addFinalizer(() =>
192 cancelPending("SSH password prompt service stopped.").pipe(Effect.ignore),
193 );
232 yield* Effect.addFinalizer(() => cancelPending().pipe(Effect.ignore));
194233
195 const resolve = Effect.fn("desktop.sshPasswordPrompts.resolve")(function* (
196 input: DesktopSshPasswordPromptResolutionInput,
197 ): Effect.fn.Return<void, DesktopSshPasswordPromptResolveError> {
234 const resolve: DesktopSshPasswordPrompts["Service"]["resolve"] = Effect.fn(
235 "desktop.sshPasswordPrompts.resolve",
236 )(function* (input) {
198237 const requestId = input.requestId.trim();
199238 if (requestId.length === 0) {
200239 return yield* new DesktopSshPromptInvalidRequestIdError({ requestId: input.requestId });
......@@ -212,7 +251,6 @@ const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* (options: La
212251 new DesktopSshPromptCancelledError({
213252 requestId,
214253 destination: entry.destination,
215 reason: `SSH authentication cancelled for ${entry.destination}.`,
216254 }),
217255 );
218256 return;
......@@ -221,9 +259,9 @@ const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* (options: La
221259 yield* Deferred.succeed(entry.deferred, input.password).pipe(Effect.asVoid);
222260 });
223261
224 const request = Effect.fn("desktop.sshPasswordPrompts.request")(function* (
225 input: SshPasswordRequest,
226 ): Effect.fn.Return<string, DesktopSshPasswordPromptRequestError> {
262 const request: DesktopSshPasswordPrompts["Service"]["request"] = Effect.fn(
263 "desktop.sshPasswordPrompts.request",
264 )(function* (input) {
227265 const window = yield* electronWindow.main;
228266 if (Option.isNone(window) || window.value.isDestroyed()) {
229267 return yield* new DesktopSshPromptWindowUnavailableError({
......@@ -233,7 +271,11 @@ const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* (options: La
233271
234272 const requestId = yield* crypto.randomUUIDv4.pipe(
235273 Effect.mapError(
236 () => new DesktopSshPromptUnavailableError({ reason: "Secure randomness is unavailable." }),
274 (cause) =>
275 new DesktopSshPromptRequestIdGenerationError({
276 destination: input.destination,
277 cause,
278 }),
237279 ),
238280 );
239281 const now = yield* DateTime.now;
......@@ -267,10 +309,9 @@ const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* (options: La
267309 onSome: (pending) =>
268310 failPending(
269311 pending,
270 new DesktopSshPromptCancelledError({
312 new DesktopSshPromptWindowClosedError({
271313 requestId,
272314 destination: input.destination,
273 reason: "SSH authentication was cancelled because the app window closed.",
274315 }),
275316 ),
276317 }),
......@@ -302,36 +343,43 @@ const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* (options: La
302343 return yield* Effect.try({
303344 try: () => {
304345 if (window.value.isDestroyed()) {
305 throw new Error(WINDOW_UNAVAILABLE_MESSAGE);
346 throw new DesktopSshPromptWindowUnavailableError({
347 destination: input.destination,
348 });
306349 }
307350 window.value.once("closed", cancelOnWindowClosed);
308 window.value.webContents.send(IpcChannels.SSH_PASSWORD_PROMPT_CHANNEL, promptRequest);
351 window.value.webContents.send(SSH_PASSWORD_PROMPT_CHANNEL, promptRequest);
309352 if (window.value.isDestroyed()) {
310 throw new Error(WINDOW_UNAVAILABLE_MESSAGE);
353 throw new DesktopSshPromptWindowUnavailableError({
354 destination: input.destination,
355 });
311356 }
312357 if (window.value.isMinimized()) {
313358 window.value.restore();
314359 }
315360 if (window.value.isDestroyed()) {
316 throw new Error(WINDOW_UNAVAILABLE_MESSAGE);
361 throw new DesktopSshPromptWindowUnavailableError({
362 destination: input.destination,
363 });
317364 }
318365 window.value.focus();
319366 },
320367 catch: (cause) =>
321 new DesktopSshPromptSendError({
322 requestId,
323 destination: input.destination,
324 cause,
325 }),
368 isDesktopSshPromptWindowUnavailableError(cause)
369 ? cause
370 : new DesktopSshPromptPresentationError({
371 requestId,
372 destination: input.destination,
373 cause,
374 }),
326375 }).pipe(Effect.andThen(waitForPassword), Effect.ensuring(cleanup));
327376 });
328377
329378 return DesktopSshPasswordPrompts.of({
330379 request,
331380 resolve,
332 cancelPending,
333381 });
334382});
335383
336export const layer = (options: LayerOptions = {}) =>
384export const layer = (options: DesktopSshPasswordPromptsOptions = {}) =>
337385 Layer.effect(DesktopSshPasswordPrompts, make(options));