/** * Building arguments for the system ssh/scp * * Pure functions with no side effects: profile configuration goes in, an * argument array comes out. The array is passed to spawn without a shell, so * special characters in paths and names can't be interpreted. * * Secrets (password, passphrase) never appear here: process arguments are * visible via `ps` to any user on the system. The secret is delivered through askpass. */ import type { SSHConfig } from '../utils/ssh-config.js'; /** * Profile configuration for the transport. * * Matches the profile configuration: transport settings (host key checking, * opting out of ~/.ssh/config) live alongside host and username — they're * part of the profile format, not a separate entity. */ export type RunnerConfig = SSHConfig; /** * Environment capabilities that affect the argument set */ export interface SshCapabilities { /** Whether multiplexing is supported (not on native Windows) */ multiplexing: boolean; /** Directory for control sockets (0700 permissions) */ controlDir: string; /** * Whether scp transfers files over SFTP (client 9.0+). This determines the * fate of the remote path: in the classic protocol it's parsed by the * server's shell, in SFTP mode the destination path is taken literally. */ scpOverSftp: boolean; } /** * Settings that have sensible defaults */ export interface SshArgsOptions { /** Connection setup timeout, seconds */ connectTimeoutSec?: number; /** Keepalive probe interval, seconds */ serverAliveIntervalSec?: number; /** How many unanswered probes count as a dropped connection */ serverAliveCountMax?: number; } /** A control socket's name starts with this — also how it's recognized in the directory */ export declare const CONTROL_SOCKET_PREFIX = "s-"; /** * How long the connection lives after the last command, in seconds. * * The single source of truth: this value flows both into the ssh command and * into tool responses about what's left running on the machine. Zero means "close immediately". */ export declare function resolveControlPersistSec(env?: NodeJS.ProcessEnv): number; /** Forget that the value was already warned about (used in tests) */ export declare function resetControlPersistWarning(): void; /** * Whether the profile needs interactive secret input via askpass */ export declare function needsAskpass(config: RunnerConfig): boolean; /** * Credential fingerprint: profiles with different keys don't share a connection. */ export declare function configFingerprint(config: RunnerConfig): string; /** * Path to the control socket. * * The name is a hash of the destination and the credentials. The * destination makes the socket shared across every client process: separate * windows land on the same connection, which is the whole point of * multiplexing. The credentials in the name are mandatory: without them a * profile with no key would ride on a connection brought up by someone * else's key — the socket doesn't check access, it already grants it. * * Computed ourselves rather than via the client's `%C`: that only knows * host, port and username. Path length matters — the unix socket address * limit on macOS is 104 bytes. */ export declare function buildControlPath(controlDir: string, config: RunnerConfig): string; /** * Options shared by ssh and scp */ export declare function buildCommonOptions(config: RunnerConfig, caps: SshCapabilities, options?: SshArgsOptions): string[]; /** * Arguments for running a command: ssh [options] */ export declare function buildSshArgs(config: RunnerConfig, caps: SshCapabilities, command: string, options?: SshArgsOptions & { requestTty?: boolean; }): string[]; /** * Arguments for a control command: ssh -O check|exit */ export declare function buildControlArgs(config: RunnerConfig, caps: SshCapabilities, controlCommand: 'check' | 'exit' | 'stop', options?: SshArgsOptions): string[]; /** * What happens to the remote path on its way to the server. * * - `literal` — the upload destination in SFTP mode. The path travels as-is, * and a backslash would become part of the name: the file would land under * the name `a\ b.txt`, breaking verification, renaming and cleanup right * after — they look for the path without it. * - `glob` — the download source. Patterns are expanded by the client, so * `star*name.txt` pulls in three unrelated files; a backslash escape avoids that. * - `shell` — the classic protocol (clients before 9.0). The path is parsed * by the server's shell: a space splits it into two arguments, and `$(id)` gets executed. */ export type RemotePathUse = 'literal' | 'glob' | 'shell'; /** * Prepare a remote path for transfer. * * Backslash-escaping is the only thing that works: single quotes become part * of the name in SFTP mode, and produce a `protocol error` in the classic one. * * The path separator, safe ASCII letters and digits, the tilde, and anything * outside ASCII are left untouched. The tilde, because the server expands * it, and `\~/app.conf` would land in a nonexistent directory named `~`. * Cyrillic, because it works fine without escaping. * * Newline and carriage return are not escaped: a `\` right before a newline * means line continuation, the character disappears, and the name becomes * different. In SFTP mode such a path works as-is, while in the classic * protocol the rest of the line runs on the server as a command — so it's * rejected there instead. */ export declare function prepareRemotePath(remotePath: string, use: RemotePathUse): string; /** Escape everything the remote side would read as syntax rather than as part of the name */ export declare function escapeRemotePath(remotePath: string): string; /** * Remote path in scp format. * * An IPv6 address is wrapped in brackets, otherwise the colons inside it * would be read as the host/path separator. */ export declare function buildRemoteSpec(host: string, remotePath: string): string; /** * Local path for scp. * * A path containing a colon before its first slash would be taken by scp as * remote, and a path starting with a dash as an option. The "./" prefix * clears up both ambiguities. */ export declare function normalizeLocalSpec(localPath: string): string; /** * Arguments for a file transfer: scp [options] * * scp reuses the same master connection as ssh, because it gets the same * ControlPath/ControlMaster — a file transfer doesn't create a new login. */ export declare function buildScpArgs(config: RunnerConfig, caps: SshCapabilities, direction: 'upload' | 'download', localPath: string, remotePath: string, options?: SshArgsOptions & { recursive?: boolean; legacyProtocol?: boolean; preserveMode?: boolean; }): string[];