import type { DesktopSshPasswordPromptRequest } from "@t3tools/contracts";import { DesktopSshPasswordPromptResolutionInputSchema } from "@t3tools/contracts";import type { SshPasswordRequest } from "@t3tools/ssh/auth";import * as Context from "effect/Context";import * as Crypto from "effect/Crypto";import * as DateTime from "effect/DateTime";import * as Deferred from "effect/Deferred";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Ref from "effect/Ref";import * as Schema from "effect/Schema"; import * as ElectronWindow from "../electron/ElectronWindow.ts";import { SSH_PASSWORD_PROMPT_CHANNEL } from "../ipc/channels.ts"; const DEFAULT_SSH_PASSWORD_PROMPT_TIMEOUT_MS = 3 * 60 * 1000; type DesktopSshPasswordPromptResolutionInput = typeof DesktopSshPasswordPromptResolutionInputSchema.Type; const DesktopSshPromptWindowAvailabilityStage = Schema.Literals([ "before-request", "before-presentation", "after-send", "after-restore",]); const DesktopSshPromptPresentationOperation = Schema.Literals([ "check-window-before-request", "check-window-before-presentation", "register-window-close-listener", "send-prompt-request", "check-window-after-send", "check-window-minimized", "restore-window", "check-window-after-restore", "focus-window", "remove-window-close-listener",]);type DesktopSshPromptPresentationOperation = typeof DesktopSshPromptPresentationOperation.Type; export class DesktopSshPromptRequestIdGenerationError extends Schema.TaggedError<DesktopSshPromptRequestIdGenerationError>()( "DesktopSshPromptRequestIdGenerationError", { destination: Schema.String, cause: Schema.Defect(), },) { override get message(): string { return "Secure randomness is unavailable."; }} export class DesktopSshPromptWindowUnavailableError extends Schema.TaggedError<DesktopSshPromptWindowUnavailableError>()( "DesktopSshPromptWindowUnavailableError", { destination: Schema.String, requestId: Schema.NullOr(Schema.String), stage: DesktopSshPromptWindowAvailabilityStage, },) { override get message(): string { const request = this.requestId === null ? "before a request id was assigned" : this.requestId; return `T3 Code window is unavailable during ${this.stage} for SSH authentication to ${this.destination} (request: ${request}).`; }} export class DesktopSshPromptPresentationError extends Schema.TaggedError<DesktopSshPromptPresentationError>()( "DesktopSshPromptPresentationError", { requestId: Schema.NullOr(Schema.String), destination: Schema.String, operation: DesktopSshPromptPresentationOperation, cause: Schema.Defect(), },) { override get message(): string { return `Failed to present SSH password prompt for ${this.destination}.`; }} export class DesktopSshPromptTimedOutError extends Schema.TaggedError<DesktopSshPromptTimedOutError>()( "DesktopSshPromptTimedOutError", { requestId: Schema.String, destination: Schema.String, },) { override get message(): string { return `SSH authentication timed out for ${this.destination}.`; }} export class DesktopSshPromptCancelledError extends Schema.TaggedError<DesktopSshPromptCancelledError>()( "DesktopSshPromptCancelledError", { requestId: Schema.String, destination: Schema.String, },) { override get message(): string { return `SSH authentication cancelled for ${this.destination}.`; }} export class DesktopSshPromptWindowClosedError extends Schema.TaggedError<DesktopSshPromptWindowClosedError>()( "DesktopSshPromptWindowClosedError", { requestId: Schema.String, destination: Schema.String, },) { override get message(): string { return "SSH authentication was cancelled because the app window closed."; }} export class DesktopSshPromptServiceStoppedError extends Schema.TaggedError<DesktopSshPromptServiceStoppedError>()( "DesktopSshPromptServiceStoppedError", { requestId: Schema.String, destination: Schema.String, },) { override get message(): string { return "SSH password prompt service stopped."; }} export class DesktopSshPromptInvalidRequestIdError extends Schema.TaggedError<DesktopSshPromptInvalidRequestIdError>()( "DesktopSshPromptInvalidRequestIdError", { requestId: Schema.String, },) { override get message(): string { return "Invalid SSH password prompt id."; }} export class DesktopSshPromptExpiredError extends Schema.TaggedError<DesktopSshPromptExpiredError>()( "DesktopSshPromptExpiredError", { requestId: Schema.String, },) { override get message(): string { return "SSH password prompt expired. Try connecting again."; }} export type DesktopSshPasswordPromptRequestError = | DesktopSshPromptRequestIdGenerationError | DesktopSshPromptWindowUnavailableError | DesktopSshPromptPresentationError | DesktopSshPromptTimedOutError | DesktopSshPromptCancelledError | DesktopSshPromptWindowClosedError | DesktopSshPromptServiceStoppedError; export type DesktopSshPasswordPromptResolveError = | DesktopSshPromptInvalidRequestIdError | DesktopSshPromptExpiredError; export type DesktopSshPasswordPromptError = | DesktopSshPasswordPromptRequestError | DesktopSshPasswordPromptResolveError; export const DesktopSshPasswordPromptCancellation = Schema.Union([ DesktopSshPromptCancelledError, DesktopSshPromptWindowClosedError, DesktopSshPromptServiceStoppedError, DesktopSshPromptTimedOutError,]);export type DesktopSshPasswordPromptCancellation = typeof DesktopSshPasswordPromptCancellation.Type; export const isDesktopSshPasswordPromptCancellation = Schema.is( DesktopSshPasswordPromptCancellation,); export class DesktopSshPasswordPrompts extends Context.Service< DesktopSshPasswordPrompts, { readonly request: ( request: SshPasswordRequest, ) => Effect.Effect<string, DesktopSshPasswordPromptRequestError>; readonly resolve: ( input: DesktopSshPasswordPromptResolutionInput, ) => Effect.Effect<void, DesktopSshPasswordPromptResolveError>; }>()("@t3tools/desktop/ssh/DesktopSshPasswordPrompts") {} interface PendingSshPasswordPrompt { readonly requestId: string; readonly destination: string; readonly deferred: Deferred.Deferred<string, DesktopSshPasswordPromptRequestError>;} export interface DesktopSshPasswordPromptsOptions { readonly passwordPromptTimeoutMs?: number;} const removePending = ( pendingRef: Ref.Ref<Map<string, PendingSshPasswordPrompt>>, requestId: string,) => Ref.modify(pendingRef, (pending) => { const entry = pending.get(requestId); if (entry === undefined) { return [Option.none<PendingSshPasswordPrompt>(), pending] as const; } const nextPending = new Map(pending); nextPending.delete(requestId); return [Option.some(entry), nextPending] as const; }); const failPending = ( pending: PendingSshPasswordPrompt, error: DesktopSshPasswordPromptRequestError,) => Deferred.fail(pending.deferred, error).pipe(Effect.asVoid); /** @public Service construction is part of the canonical Effect module API. */export const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* ( options: DesktopSshPasswordPromptsOptions = {},) { const electronWindow = yield* ElectronWindow.ElectronWindow; const crypto = yield* Crypto.Crypto; const pendingRef = yield* Ref.make(new Map<string, PendingSshPasswordPrompt>()); const passwordPromptTimeoutMs = options.passwordPromptTimeoutMs ?? DEFAULT_SSH_PASSWORD_PROMPT_TIMEOUT_MS; const cancelPending = () => Ref.getAndSet(pendingRef, new Map()).pipe( Effect.flatMap((pending) => Effect.forEach( pending.values(), (entry) => failPending( entry, new DesktopSshPromptServiceStoppedError({ requestId: entry.requestId, destination: entry.destination, }), ), { discard: true }, ), ), Effect.asVoid, ); yield* Effect.addFinalizer(() => cancelPending().pipe(Effect.ignore)); const resolve: DesktopSshPasswordPrompts["Service"]["resolve"] = Effect.fn( "desktop.sshPasswordPrompts.resolve", )(function* (input) { const requestId = input.requestId.trim(); if (requestId.length === 0) { return yield* new DesktopSshPromptInvalidRequestIdError({ requestId: input.requestId }); } const pending = yield* removePending(pendingRef, requestId); if (Option.isNone(pending)) { return yield* new DesktopSshPromptExpiredError({ requestId }); } const entry = pending.value; if (input.password === null) { yield* failPending( entry, new DesktopSshPromptCancelledError({ requestId, destination: entry.destination, }), ); return; } yield* Deferred.succeed(entry.deferred, input.password).pipe(Effect.asVoid); }); const request: DesktopSshPasswordPrompts["Service"]["request"] = Effect.fn( "desktop.sshPasswordPrompts.request", )(function* (input) { const window = yield* electronWindow.main; if (Option.isNone(window)) { return yield* new DesktopSshPromptWindowUnavailableError({ destination: input.destination, requestId: null, stage: "before-request", }); } const unavailableBeforeRequest = yield* Effect.try({ try: () => window.value.isDestroyed(), catch: (cause) => new DesktopSshPromptPresentationError({ requestId: null, destination: input.destination, operation: "check-window-before-request", cause, }), }); if (unavailableBeforeRequest) { return yield* new DesktopSshPromptWindowUnavailableError({ destination: input.destination, requestId: null, stage: "before-request", }); } const requestId = yield* crypto.randomUUIDv4.pipe( Effect.mapError( (cause) => new DesktopSshPromptRequestIdGenerationError({ destination: input.destination, cause, }), ), ); const now = yield* DateTime.now; const expiresAt = DateTime.formatIso( DateTime.add(now, { milliseconds: passwordPromptTimeoutMs }), ); const promptRequest: DesktopSshPasswordPromptRequest = { requestId, destination: input.destination, username: input.username, prompt: input.prompt, expiresAt, }; const deferred = yield* Deferred.make<string, DesktopSshPasswordPromptRequestError>(); const pending: PendingSshPasswordPrompt = { requestId, destination: input.destination, deferred, }; yield* Ref.update(pendingRef, (entries) => new Map(entries).set(requestId, pending)); const context = yield* Effect.context(); const runFork = Effect.runForkWith(context); const cancelOnWindowClosed = () => { runFork( removePending(pendingRef, requestId).pipe( Effect.flatMap((entry) => Option.match(entry, { onNone: () => Effect.void, onSome: (pending) => failPending( pending, new DesktopSshPromptWindowClosedError({ requestId, destination: input.destination, }), ), }), ), ), ); }; const runPresentationOperation = <A>( operation: DesktopSshPromptPresentationOperation, evaluate: () => A, ) => Effect.try({ try: evaluate, catch: (cause) => new DesktopSshPromptPresentationError({ requestId, destination: input.destination, operation, cause, }), }); const cleanup = runPresentationOperation("remove-window-close-listener", () => { if (!window.value.isDestroyed()) { window.value.removeListener("closed", cancelOnWindowClosed); } }).pipe(Effect.orDie, Effect.ensuring(removePending(pendingRef, requestId)), Effect.asVoid); const waitForPassword = Deferred.await(deferred).pipe( Effect.timeoutOption(Duration.millis(passwordPromptTimeoutMs)), Effect.flatMap( Option.match({ onNone: () => Effect.fail( new DesktopSshPromptTimedOutError({ requestId, destination: input.destination, }), ), onSome: Effect.succeed, }), ), ); const preferSubmittedPassword = (error: DesktopSshPasswordPromptRequestError) => Deferred.poll(deferred).pipe( Effect.flatMap( Option.match({ onSome: (completion) => completion, onNone: () => Ref.get(pendingRef).pipe( Effect.flatMap((entries) => entries.has(requestId) ? Effect.fail(error) : Deferred.await(deferred), ), ), }), ), ); return yield* Effect.gen(function* () { const unavailableBeforePresentation = yield* runPresentationOperation( "check-window-before-presentation", () => window.value.isDestroyed(), ); if (unavailableBeforePresentation) { return yield* new DesktopSshPromptWindowUnavailableError({ destination: input.destination, requestId, stage: "before-presentation", }); } yield* runPresentationOperation("register-window-close-listener", () => window.value.once("closed", cancelOnWindowClosed), ); return yield* Effect.gen(function* () { yield* runPresentationOperation("send-prompt-request", () => window.value.webContents.send(SSH_PASSWORD_PROMPT_CHANNEL, promptRequest), ); yield* Effect.yieldNow; const unavailableAfterSend = yield* runPresentationOperation( "check-window-after-send", () => window.value.isDestroyed(), ); if (unavailableAfterSend) { return yield* new DesktopSshPromptWindowUnavailableError({ destination: input.destination, requestId, stage: "after-send", }); } const minimized = yield* runPresentationOperation("check-window-minimized", () => window.value.isMinimized(), ); if (minimized) { yield* runPresentationOperation("restore-window", () => window.value.restore()); } const unavailableAfterRestore = yield* runPresentationOperation( "check-window-after-restore", () => window.value.isDestroyed(), ); if (unavailableAfterRestore) { return yield* new DesktopSshPromptWindowUnavailableError({ destination: input.destination, requestId, stage: "after-restore", }); } yield* runPresentationOperation("focus-window", () => window.value.focus()); return yield* waitForPassword; }).pipe(Effect.catch(preferSubmittedPassword)); }).pipe(Effect.ensuring(cleanup)); }); return DesktopSshPasswordPrompts.of({ request, resolve, });}); export const layer = (options: DesktopSshPasswordPromptsOptions = {}) => Layer.effect(DesktopSshPasswordPrompts, make(options));
import type { DesktopSshPasswordPromptRequest } from "@t3tools/contracts";
import { DesktopSshPasswordPromptResolutionInputSchema } from "@t3tools/contracts";
import type { SshPasswordRequest } from "@t3tools/ssh/auth";
import * as Context from "effect/Context";
import * as Crypto from "effect/Crypto";
import * as DateTime from "effect/DateTime";
import * as Deferred from "effect/Deferred";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Ref from "effect/Ref";
import * as Schema from "effect/Schema";
import * as ElectronWindow from "../electron/ElectronWindow.ts";
import { SSH_PASSWORD_PROMPT_CHANNEL } from "../ipc/channels.ts";
const DEFAULT_SSH_PASSWORD_PROMPT_TIMEOUT_MS = 3 * 60 * 1000;
type DesktopSshPasswordPromptResolutionInput =
typeof DesktopSshPasswordPromptResolutionInputSchema.Type;
const DesktopSshPromptWindowAvailabilityStage = Schema.Literals([
"before-request",
"before-presentation",
"after-send",
"after-restore",
]);
const DesktopSshPromptPresentationOperation = Schema.Literals([
"check-window-before-request",
"check-window-before-presentation",
"register-window-close-listener",
"send-prompt-request",
"check-window-after-send",
"check-window-minimized",
"restore-window",
"check-window-after-restore",
"focus-window",
"remove-window-close-listener",
]);
type DesktopSshPromptPresentationOperation = typeof DesktopSshPromptPresentationOperation.Type;
export class DesktopSshPromptRequestIdGenerationError extends Schema.TaggedError<DesktopSshPromptRequestIdGenerationError>()(
"DesktopSshPromptRequestIdGenerationError",
{
destination: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return "Secure randomness is unavailable.";
}
}
export class DesktopSshPromptWindowUnavailableError extends Schema.TaggedError<DesktopSshPromptWindowUnavailableError>()(
"DesktopSshPromptWindowUnavailableError",
{
destination: Schema.String,
requestId: Schema.NullOr(Schema.String),
stage: DesktopSshPromptWindowAvailabilityStage,
},
) {
override get message(): string {
const request = this.requestId === null ? "before a request id was assigned" : this.requestId;
return `T3 Code window is unavailable during ${this.stage} for SSH authentication to ${this.destination} (request: ${request}).`;
}
}
export class DesktopSshPromptPresentationError extends Schema.TaggedError<DesktopSshPromptPresentationError>()(
"DesktopSshPromptPresentationError",
{
requestId: Schema.NullOr(Schema.String),
destination: Schema.String,
operation: DesktopSshPromptPresentationOperation,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to present SSH password prompt for ${this.destination}.`;
}
}
export class DesktopSshPromptTimedOutError extends Schema.TaggedError<DesktopSshPromptTimedOutError>()(
"DesktopSshPromptTimedOutError",
{
requestId: Schema.String,
destination: Schema.String,
},
) {
override get message(): string {
return `SSH authentication timed out for ${this.destination}.`;
}
}
export class DesktopSshPromptCancelledError extends Schema.TaggedError<DesktopSshPromptCancelledError>()(
"DesktopSshPromptCancelledError",
{
requestId: Schema.String,
destination: Schema.String,
},
) {
override get message(): string {
return `SSH authentication cancelled for ${this.destination}.`;
}
}
export class DesktopSshPromptWindowClosedError extends Schema.TaggedError<DesktopSshPromptWindowClosedError>()(
"DesktopSshPromptWindowClosedError",
{
requestId: Schema.String,
destination: Schema.String,
},
) {
override get message(): string {
return "SSH authentication was cancelled because the app window closed.";
}
}
export class DesktopSshPromptServiceStoppedError extends Schema.TaggedError<DesktopSshPromptServiceStoppedError>()(
"DesktopSshPromptServiceStoppedError",
{
requestId: Schema.String,
destination: Schema.String,
},
) {
override get message(): string {
return "SSH password prompt service stopped.";
}
}
export class DesktopSshPromptInvalidRequestIdError extends Schema.TaggedError<DesktopSshPromptInvalidRequestIdError>()(
"DesktopSshPromptInvalidRequestIdError",
{
requestId: Schema.String,
},
) {
override get message(): string {
return "Invalid SSH password prompt id.";
}
}
export class DesktopSshPromptExpiredError extends Schema.TaggedError<DesktopSshPromptExpiredError>()(
"DesktopSshPromptExpiredError",
{
requestId: Schema.String,
},
) {
override get message(): string {
return "SSH password prompt expired. Try connecting again.";
}
}
export type DesktopSshPasswordPromptRequestError =
| DesktopSshPromptRequestIdGenerationError
| DesktopSshPromptWindowUnavailableError
| DesktopSshPromptPresentationError
| DesktopSshPromptTimedOutError
| DesktopSshPromptCancelledError
| DesktopSshPromptWindowClosedError
| DesktopSshPromptServiceStoppedError;
export type DesktopSshPasswordPromptResolveError =
| DesktopSshPromptInvalidRequestIdError
| DesktopSshPromptExpiredError;
export type DesktopSshPasswordPromptError =
| DesktopSshPasswordPromptRequestError
| DesktopSshPasswordPromptResolveError;
export const DesktopSshPasswordPromptCancellation = Schema.Union([
DesktopSshPromptCancelledError,
DesktopSshPromptWindowClosedError,
DesktopSshPromptServiceStoppedError,
DesktopSshPromptTimedOutError,
]);
export type DesktopSshPasswordPromptCancellation = typeof DesktopSshPasswordPromptCancellation.Type;
export const isDesktopSshPasswordPromptCancellation = Schema.is(
DesktopSshPasswordPromptCancellation,
);
export class DesktopSshPasswordPrompts extends Context.Service<
DesktopSshPasswordPrompts,
{
readonly request: (
request: SshPasswordRequest,
) => Effect.Effect<string, DesktopSshPasswordPromptRequestError>;
readonly resolve: (
input: DesktopSshPasswordPromptResolutionInput,
) => Effect.Effect<void, DesktopSshPasswordPromptResolveError>;
}
>()("@t3tools/desktop/ssh/DesktopSshPasswordPrompts") {}
interface PendingSshPasswordPrompt {
readonly requestId: string;
readonly destination: string;
readonly deferred: Deferred.Deferred<string, DesktopSshPasswordPromptRequestError>;
}
export interface DesktopSshPasswordPromptsOptions {
readonly passwordPromptTimeoutMs?: number;
}
const removePending = (
pendingRef: Ref.Ref<Map<string, PendingSshPasswordPrompt>>,
requestId: string,
) =>
Ref.modify(pendingRef, (pending) => {
const entry = pending.get(requestId);
if (entry === undefined) {
return [Option.none<PendingSshPasswordPrompt>(), pending] as const;
}
const nextPending = new Map(pending);
nextPending.delete(requestId);
return [Option.some(entry), nextPending] as const;
});
const failPending = (
pending: PendingSshPasswordPrompt,
error: DesktopSshPasswordPromptRequestError,
) => Deferred.fail(pending.deferred, error).pipe(Effect.asVoid);
/** @public Service construction is part of the canonical Effect module API. */
export const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* (
options: DesktopSshPasswordPromptsOptions = {},
) {
const electronWindow = yield* ElectronWindow.ElectronWindow;
const crypto = yield* Crypto.Crypto;
const pendingRef = yield* Ref.make(new Map<string, PendingSshPasswordPrompt>());
const passwordPromptTimeoutMs =
options.passwordPromptTimeoutMs ?? DEFAULT_SSH_PASSWORD_PROMPT_TIMEOUT_MS;
const cancelPending = () =>
Ref.getAndSet(pendingRef, new Map()).pipe(
Effect.flatMap((pending) =>
Effect.forEach(
pending.values(),
(entry) =>
failPending(
entry,
new DesktopSshPromptServiceStoppedError({
requestId: entry.requestId,
destination: entry.destination,
}),
),
{ discard: true },
),
),
Effect.asVoid,
);
yield* Effect.addFinalizer(() => cancelPending().pipe(Effect.ignore));
const resolve: DesktopSshPasswordPrompts["Service"]["resolve"] = Effect.fn(
"desktop.sshPasswordPrompts.resolve",
)(function* (input) {
const requestId = input.requestId.trim();
if (requestId.length === 0) {
return yield* new DesktopSshPromptInvalidRequestIdError({ requestId: input.requestId });
}
const pending = yield* removePending(pendingRef, requestId);
if (Option.isNone(pending)) {
return yield* new DesktopSshPromptExpiredError({ requestId });
}
const entry = pending.value;
if (input.password === null) {
yield* failPending(
entry,
new DesktopSshPromptCancelledError({
requestId,
destination: entry.destination,
}),
);
return;
}
yield* Deferred.succeed(entry.deferred, input.password).pipe(Effect.asVoid);
});
const request: DesktopSshPasswordPrompts["Service"]["request"] = Effect.fn(
"desktop.sshPasswordPrompts.request",
)(function* (input) {
const window = yield* electronWindow.main;
if (Option.isNone(window)) {
return yield* new DesktopSshPromptWindowUnavailableError({
destination: input.destination,
requestId: null,
stage: "before-request",
});
}
const unavailableBeforeRequest = yield* Effect.try({
try: () => window.value.isDestroyed(),
catch: (cause) =>
new DesktopSshPromptPresentationError({
requestId: null,
destination: input.destination,
operation: "check-window-before-request",
cause,
}),
});
if (unavailableBeforeRequest) {
return yield* new DesktopSshPromptWindowUnavailableError({
destination: input.destination,
requestId: null,
stage: "before-request",
});
}
const requestId = yield* crypto.randomUUIDv4.pipe(
Effect.mapError(
(cause) =>
new DesktopSshPromptRequestIdGenerationError({
destination: input.destination,
cause,
}),
),
);
const now = yield* DateTime.now;
const expiresAt = DateTime.formatIso(
DateTime.add(now, { milliseconds: passwordPromptTimeoutMs }),
);
const promptRequest: DesktopSshPasswordPromptRequest = {
requestId,
destination: input.destination,
username: input.username,
prompt: input.prompt,
expiresAt,
};
const deferred = yield* Deferred.make<string, DesktopSshPasswordPromptRequestError>();
const pending: PendingSshPasswordPrompt = {
requestId,
destination: input.destination,
deferred,
};
yield* Ref.update(pendingRef, (entries) => new Map(entries).set(requestId, pending));
const context = yield* Effect.context();
const runFork = Effect.runForkWith(context);
const cancelOnWindowClosed = () => {
runFork(
removePending(pendingRef, requestId).pipe(
Effect.flatMap((entry) =>
Option.match(entry, {
onNone: () => Effect.void,
onSome: (pending) =>
failPending(
pending,
new DesktopSshPromptWindowClosedError({
requestId,
destination: input.destination,
}),
),
}),
),
),
);
};
const runPresentationOperation = <A>(
operation: DesktopSshPromptPresentationOperation,
evaluate: () => A,
) =>
Effect.try({
try: evaluate,
catch: (cause) =>
new DesktopSshPromptPresentationError({
requestId,
destination: input.destination,
operation,
cause,
}),
});
const cleanup = runPresentationOperation("remove-window-close-listener", () => {
if (!window.value.isDestroyed()) {
window.value.removeListener("closed", cancelOnWindowClosed);
}
}).pipe(Effect.orDie, Effect.ensuring(removePending(pendingRef, requestId)), Effect.asVoid);
const waitForPassword = Deferred.await(deferred).pipe(
Effect.timeoutOption(Duration.millis(passwordPromptTimeoutMs)),
Effect.flatMap(
Option.match({
onNone: () =>
Effect.fail(
new DesktopSshPromptTimedOutError({
requestId,
destination: input.destination,
}),
),
onSome: Effect.succeed,
}),
),
);
const preferSubmittedPassword = (error: DesktopSshPasswordPromptRequestError) =>
Deferred.poll(deferred).pipe(
Effect.flatMap(
Option.match({
onSome: (completion) => completion,
onNone: () =>
Ref.get(pendingRef).pipe(
Effect.flatMap((entries) =>
entries.has(requestId) ? Effect.fail(error) : Deferred.await(deferred),
),
),
}),
),
);
return yield* Effect.gen(function* () {
const unavailableBeforePresentation = yield* runPresentationOperation(
"check-window-before-presentation",
() => window.value.isDestroyed(),
);
if (unavailableBeforePresentation) {
return yield* new DesktopSshPromptWindowUnavailableError({
destination: input.destination,
requestId,
stage: "before-presentation",
});
}
yield* runPresentationOperation("register-window-close-listener", () =>
window.value.once("closed", cancelOnWindowClosed),
);
return yield* Effect.gen(function* () {
yield* runPresentationOperation("send-prompt-request", () =>
window.value.webContents.send(SSH_PASSWORD_PROMPT_CHANNEL, promptRequest),
);
yield* Effect.yieldNow;
const unavailableAfterSend = yield* runPresentationOperation(
"check-window-after-send",
() => window.value.isDestroyed(),
);
if (unavailableAfterSend) {
return yield* new DesktopSshPromptWindowUnavailableError({
destination: input.destination,
requestId,
stage: "after-send",
});
}
const minimized = yield* runPresentationOperation("check-window-minimized", () =>
window.value.isMinimized(),
);
if (minimized) {
yield* runPresentationOperation("restore-window", () => window.value.restore());
}
const unavailableAfterRestore = yield* runPresentationOperation(
"check-window-after-restore",
() => window.value.isDestroyed(),
);
if (unavailableAfterRestore) {
return yield* new DesktopSshPromptWindowUnavailableError({
destination: input.destination,
requestId,
stage: "after-restore",
});
}
yield* runPresentationOperation("focus-window", () => window.value.focus());
return yield* waitForPassword;
}).pipe(Effect.catch(preferSubmittedPassword));
}).pipe(Effect.ensuring(cleanup));
});
return DesktopSshPasswordPrompts.of({
request,
resolve,
});
});
export const layer = (options: DesktopSshPasswordPromptsOptions = {}) =>
Layer.effect(DesktopSshPasswordPrompts, make(options));