import { HostProcessPlatform } from "@t3tools/shared/hostProcess";import * as Config from "effect/Config";import * as Context from "effect/Context";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Path from "effect/Path";import * as PlatformError from "effect/PlatformError"; import { SshPasswordPromptError } from "./errors.ts"; export interface SshPasswordRequest { readonly destination: string; readonly username: string | null; readonly prompt: string; readonly attempt: number;} export interface SshAskpassFile { readonly path: string; readonly contents: string; readonly mode?: number;} export interface SshAskpassHelperDescriptor { readonly launcherPath: string; readonly files: ReadonlyArray<SshAskpassFile>;} export interface SshAuthOptions { readonly authSecret?: string | null; readonly batchMode?: "yes" | "no"; readonly interactiveAuth?: boolean;} export interface SshPasswordPromptShape { readonly isAvailable: boolean; readonly request: ( request: SshPasswordRequest, ) => Effect.Effect<string | null, SshPasswordPromptError>;} export class SshPasswordPrompt extends Context.Service<SshPasswordPrompt, SshPasswordPromptShape>()( "@t3tools/ssh/auth/SshPasswordPrompt",) { static readonly disabledLayer = Layer.succeed( SshPasswordPrompt, SshPasswordPrompt.of({ isAvailable: false, request: () => Effect.succeed(null), }), );} export interface SshChildEnvironmentOptions { readonly interactiveAuth?: boolean; readonly baseEnv?: NodeJS.ProcessEnv; readonly askpassDirectory?: string; readonly authSecret?: string | null;} const SSH_ASKPASS_DIR_NAME = "t3code-ssh-askpass"; function joinSshAskpassPath( directory: string, fileName: string, platform: NodeJS.Platform,): string { const trimmed = directory.replace(/[\\/]+$/u, ""); return platform === "win32" ? `${trimmed}\\${fileName}` : `${trimmed}/${fileName}`;} export const ASKPASS_POSIX_SCRIPT = `#!/bin/sh# Invoked by ssh via SSH_ASKPASS when T3 Code re-runs ssh with a cached password# from the renderer's in-app prompt. We never expose a native dialog here - if# T3_SSH_AUTH_SECRET is missing, that's a caller bug and we fail loudly.if [ "\${T3_SSH_AUTH_SECRET+x}" = "x" ]; then printf "%s\\n" "$T3_SSH_AUTH_SECRET" exit 0fiprintf 'T3 Code ssh-askpass invoked without T3_SSH_AUTH_SECRET.\\n' >&2exit 1`; export const ASKPASS_WINDOWS_LAUNCHER_SCRIPT = `@echo off\rpowershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ssh-askpass.ps1" %*\r`; export const ASKPASS_WINDOWS_SCRIPT = `# Invoked by ssh via SSH_ASKPASS (through ssh-askpass.cmd) when T3 Code re-runs\r# ssh with a cached password from the renderer's in-app prompt. We never expose\r# a native dialog here - if T3_SSH_AUTH_SECRET is missing, that's a caller bug\r# and we fail loudly.\rif ($null -ne $env:T3_SSH_AUTH_SECRET) {\r [Console]::Out.WriteLine($env:T3_SSH_AUTH_SECRET)\r exit 0\r}\r[Console]::Error.WriteLine("T3 Code ssh-askpass invoked without T3_SSH_AUTH_SECRET.")\rexit 1\r`; export const getDefaultSshAskpassDirectory = Effect.fn("ssh/auth.getDefaultSshAskpassDirectory")( function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const parentDirectory = yield* fs.makeTempDirectory({ prefix: "t3code-ssh-runtime-" }); return path.join(parentDirectory, SSH_ASKPASS_DIR_NAME); },); export const buildSshAskpassHelperDescriptor = Effect.fn( "ssh/auth.buildSshAskpassHelperDescriptor",)(function* (input: { readonly directory: string;}): Effect.fn.Return<SshAskpassHelperDescriptor, never, Path.Path> { const platform = yield* HostProcessPlatform; const path = yield* Path.Path; const directory = input.directory; if (platform === "win32") { const powershellPath = joinSshAskpassPath(directory, "ssh-askpass.ps1", platform); return { launcherPath: joinSshAskpassPath(directory, "ssh-askpass.cmd", platform), files: [ { path: joinSshAskpassPath(directory, "ssh-askpass.cmd", platform), contents: ASKPASS_WINDOWS_LAUNCHER_SCRIPT, }, { path: powershellPath, contents: ASKPASS_WINDOWS_SCRIPT, }, ], }; } return { launcherPath: path.join(directory, "ssh-askpass.sh"), files: [ { path: path.join(directory, "ssh-askpass.sh"), contents: ASKPASS_POSIX_SCRIPT, mode: 0o700, }, ], };}); export const ensureSshAskpassHelpers = Effect.fn("ssh/auth.ensureSshAskpassHelpers")( function* (input: { readonly directory: string; }): Effect.fn.Return<string, PlatformError.PlatformError, FileSystem.FileSystem | Path.Path> { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const descriptor = yield* buildSshAskpassHelperDescriptor(input); const platform = yield* HostProcessPlatform; yield* fs.makeDirectory(path.dirname(descriptor.launcherPath), { recursive: true }); for (const file of descriptor.files) { const existing = yield* fs.exists(file.path); const current = existing ? yield* fs.readFileString(file.path) : null; if (current !== file.contents) { yield* fs.writeFileString(file.path, file.contents); } if (file.mode !== undefined && platform !== "win32") { yield* fs.chmod(file.path, file.mode); } } return descriptor.launcherPath; },); export const buildSshChildEnvironment = Effect.fn("ssh/auth.buildSshChildEnvironment")(function* ( input: SshChildEnvironmentOptions = {},): Effect.fn.Return< NodeJS.ProcessEnv, PlatformError.PlatformError, FileSystem.FileSystem | Path.Path> { const baseEnv = { ...input.baseEnv }; if (!input.interactiveAuth) { return baseEnv; } const platform = yield* HostProcessPlatform; const hostDisplay = input.baseEnv ? input.baseEnv.DISPLAY : yield* Config.string("DISPLAY").pipe( Config.option, Effect.orElseSucceed(() => Option.none<string>()), Effect.map(Option.getOrUndefined), ); const directory = input.askpassDirectory ?? (yield* getDefaultSshAskpassDirectory()); const sshAskpass = yield* ensureSshAskpassHelpers({ directory }); return { ...baseEnv, SSH_ASKPASS: sshAskpass, SSH_ASKPASS_REQUIRE: "force", ...(input.authSecret === undefined ? {} : { T3_SSH_AUTH_SECRET: input.authSecret ?? "" }), ...(platform === "win32" || baseEnv.DISPLAY || hostDisplay ? {} : { DISPLAY: "t3code" }), };}); export function isSshAuthFailure(error: unknown): boolean { const message = error instanceof Error ? error.message : String(error); const normalized = message.toLowerCase(); return ( /permission denied \((?:publickey|password|keyboard-interactive|hostbased|gssapi-with-mic)[^)]*\)/u.test( normalized, ) || /authentication failed/u.test(normalized) || /too many authentication failures/u.test(normalized) );}
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import * as Config from "effect/Config";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Path from "effect/Path";
import * as PlatformError from "effect/PlatformError";
import { SshPasswordPromptError } from "./errors.ts";
export interface SshPasswordRequest {
readonly destination: string;
readonly username: string | null;
readonly prompt: string;
readonly attempt: number;
}
export interface SshAskpassFile {
readonly path: string;
readonly contents: string;
readonly mode?: number;
}
export interface SshAskpassHelperDescriptor {
readonly launcherPath: string;
readonly files: ReadonlyArray<SshAskpassFile>;
}
export interface SshAuthOptions {
readonly authSecret?: string | null;
readonly batchMode?: "yes" | "no";
readonly interactiveAuth?: boolean;
}
export interface SshPasswordPromptShape {
readonly isAvailable: boolean;
readonly request: (
request: SshPasswordRequest,
) => Effect.Effect<string | null, SshPasswordPromptError>;
}
export class SshPasswordPrompt extends Context.Service<SshPasswordPrompt, SshPasswordPromptShape>()(
"@t3tools/ssh/auth/SshPasswordPrompt",
) {
static readonly disabledLayer = Layer.succeed(
SshPasswordPrompt,
SshPasswordPrompt.of({
isAvailable: false,
request: () => Effect.succeed(null),
}),
);
}
export interface SshChildEnvironmentOptions {
readonly interactiveAuth?: boolean;
readonly baseEnv?: NodeJS.ProcessEnv;
readonly askpassDirectory?: string;
readonly authSecret?: string | null;
}
const SSH_ASKPASS_DIR_NAME = "t3code-ssh-askpass";
function joinSshAskpassPath(
directory: string,
fileName: string,
platform: NodeJS.Platform,
): string {
const trimmed = directory.replace(/[\\/]+$/u, "");
return platform === "win32" ? `${trimmed}\\${fileName}` : `${trimmed}/${fileName}`;
}
export const ASKPASS_POSIX_SCRIPT = `#!/bin/sh
# Invoked by ssh via SSH_ASKPASS when T3 Code re-runs ssh with a cached password
# from the renderer's in-app prompt. We never expose a native dialog here - if
# T3_SSH_AUTH_SECRET is missing, that's a caller bug and we fail loudly.
if [ "\${T3_SSH_AUTH_SECRET+x}" = "x" ]; then
printf "%s\\n" "$T3_SSH_AUTH_SECRET"
exit 0
fi
printf 'T3 Code ssh-askpass invoked without T3_SSH_AUTH_SECRET.\\n' >&2
exit 1
`;
export const ASKPASS_WINDOWS_LAUNCHER_SCRIPT = `@echo off\r
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ssh-askpass.ps1" %*\r
`;
export const ASKPASS_WINDOWS_SCRIPT = `# Invoked by ssh via SSH_ASKPASS (through ssh-askpass.cmd) when T3 Code re-runs\r
# ssh with a cached password from the renderer's in-app prompt. We never expose\r
# a native dialog here - if T3_SSH_AUTH_SECRET is missing, that's a caller bug\r
# and we fail loudly.\r
if ($null -ne $env:T3_SSH_AUTH_SECRET) {\r
[Console]::Out.WriteLine($env:T3_SSH_AUTH_SECRET)\r
exit 0\r
}\r
[Console]::Error.WriteLine("T3 Code ssh-askpass invoked without T3_SSH_AUTH_SECRET.")\r
exit 1\r
`;
export const getDefaultSshAskpassDirectory = Effect.fn("ssh/auth.getDefaultSshAskpassDirectory")(
function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const parentDirectory = yield* fs.makeTempDirectory({ prefix: "t3code-ssh-runtime-" });
return path.join(parentDirectory, SSH_ASKPASS_DIR_NAME);
},
);
export const buildSshAskpassHelperDescriptor = Effect.fn(
"ssh/auth.buildSshAskpassHelperDescriptor",
)(function* (input: {
readonly directory: string;
}): Effect.fn.Return<SshAskpassHelperDescriptor, never, Path.Path> {
const platform = yield* HostProcessPlatform;
const path = yield* Path.Path;
const directory = input.directory;
if (platform === "win32") {
const powershellPath = joinSshAskpassPath(directory, "ssh-askpass.ps1", platform);
return {
launcherPath: joinSshAskpassPath(directory, "ssh-askpass.cmd", platform),
files: [
{
path: joinSshAskpassPath(directory, "ssh-askpass.cmd", platform),
contents: ASKPASS_WINDOWS_LAUNCHER_SCRIPT,
},
{
path: powershellPath,
contents: ASKPASS_WINDOWS_SCRIPT,
},
],
};
}
return {
launcherPath: path.join(directory, "ssh-askpass.sh"),
files: [
{
path: path.join(directory, "ssh-askpass.sh"),
contents: ASKPASS_POSIX_SCRIPT,
mode: 0o700,
},
],
};
});
export const ensureSshAskpassHelpers = Effect.fn("ssh/auth.ensureSshAskpassHelpers")(
function* (input: {
readonly directory: string;
}): Effect.fn.Return<string, PlatformError.PlatformError, FileSystem.FileSystem | Path.Path> {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const descriptor = yield* buildSshAskpassHelperDescriptor(input);
const platform = yield* HostProcessPlatform;
yield* fs.makeDirectory(path.dirname(descriptor.launcherPath), { recursive: true });
for (const file of descriptor.files) {
const existing = yield* fs.exists(file.path);
const current = existing ? yield* fs.readFileString(file.path) : null;
if (current !== file.contents) {
yield* fs.writeFileString(file.path, file.contents);
}
if (file.mode !== undefined && platform !== "win32") {
yield* fs.chmod(file.path, file.mode);
}
}
return descriptor.launcherPath;
},
);
export const buildSshChildEnvironment = Effect.fn("ssh/auth.buildSshChildEnvironment")(function* (
input: SshChildEnvironmentOptions = {},
): Effect.fn.Return<
NodeJS.ProcessEnv,
PlatformError.PlatformError,
FileSystem.FileSystem | Path.Path
> {
const baseEnv = { ...input.baseEnv };
if (!input.interactiveAuth) {
return baseEnv;
}
const platform = yield* HostProcessPlatform;
const hostDisplay = input.baseEnv
? input.baseEnv.DISPLAY
: yield* Config.string("DISPLAY").pipe(
Config.option,
Effect.orElseSucceed(() => Option.none<string>()),
Effect.map(Option.getOrUndefined),
);
const directory = input.askpassDirectory ?? (yield* getDefaultSshAskpassDirectory());
const sshAskpass = yield* ensureSshAskpassHelpers({ directory });
return {
...baseEnv,
SSH_ASKPASS: sshAskpass,
SSH_ASKPASS_REQUIRE: "force",
...(input.authSecret === undefined ? {} : { T3_SSH_AUTH_SECRET: input.authSecret ?? "" }),
...(platform === "win32" || baseEnv.DISPLAY || hostDisplay ? {} : { DISPLAY: "t3code" }),
};
});
export function isSshAuthFailure(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error);
const normalized = message.toLowerCase();
return (
/permission denied \((?:publickey|password|keyboard-interactive|hostbased|gssapi-with-mic)[^)]*\)/u.test(
normalized,
) ||
/authentication failed/u.test(normalized) ||
/too many authentication failures/u.test(normalized)
);
}