@judaharagao/opencode-ssh 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,6 +12,11 @@ SSH access plugin for OpenCode — secure remote command execution with command
12
12
  - 📤 **File Transfer** — Upload and download files via SCP/SFTP
13
13
  - 🎯 **Command Safety Checker** — Preview command safety before execution
14
14
  - ⚙️ **Configurable Policies** — Add custom blocklist/allowlist patterns
15
+ - 🛤️ **Proxy Support** — `ProxyJump` and `ProxyCommand` from your ssh config
16
+ - 🛡️ **Host Key Verification** — optional `strict_host_key` MITM protection via `known_hosts`
17
+ - ⏳ **Rate Limiting** — per-host command rate limit and cooldown
18
+ - 🔁 **Auto-Reconnect** — dropped sessions reconnect automatically on the next command
19
+ - 🔐 **Credential Hygiene** — passwords are cleared from memory after a successful handshake
15
20
 
16
21
  ## Installation
17
22
 
@@ -48,15 +53,21 @@ Or with configuration:
48
53
  | `audit_enabled` | boolean | `true` | Enable audit logging |
49
54
  | `blocklist_extra` | string[] | `[]` | Additional regex patterns to block |
50
55
  | `allowlist` | string[] | `[]` | Additional allowed patterns (restricted mode) |
51
- | `ssh_config_path` | string | `~/.ssh/config` | Path to the SSH config file used to resolve host aliases, defaults, and auto-connect targets |
52
- | `auto_connect` | boolean | `false` | Connect automatically at startup to every `Host` entry in the ssh config that has a `HostName` |
56
+ | `ssh_config_path` | string | `~/.ssh/config` | Path to the SSH config file used to resolve host aliases, defaults, proxies, and auto-connect targets |
57
+ | `auto_connect` | boolean | `false` | Connect automatically at startup to every `Host` entry in the ssh config that has a `HostName` (with retry/backoff) |
58
+ | `auto_reconnect` | boolean | `true` | Reconnect a dropped session automatically before the next `ssh.exec`/`ssh.upload`/`ssh.download` (key-authenticated sessions) |
59
+ | `strict_host_key` | boolean | `false` | Reject connections whose host key is not an exact match in `~/.ssh/known_hosts` (MITM protection) |
60
+ | `rate_limit_per_minute` | number | `120` | Max commands allowed per host per minute |
61
+ | `cooldown_seconds` | number | `0` | Minimum delay (seconds) between commands on the same host |
53
62
 
54
63
  ## SSH Config Integration
55
64
 
56
65
  The plugin can reuse your existing `~/.ssh/config` (or a custom path via `ssh_config_path`):
57
66
 
58
67
  - **Host resolution** — `ssh.connect(host="myalias")` resolves the alias to its `HostName`, and automatically applies the `User`, `Port` and `IdentityFile` from the config. `username`, `port` and `auth_method`/`key_path` become optional when configured.
59
- - **Auto-connect** — with `"auto_connect": true`, the plugin connects at startup to every `Host` entry that has a `HostName`, using the configured key (or `~/.ssh/id_rsa`). Unreachable hosts are skipped without blocking startup.
68
+ - **ProxyJump / ProxyCommand** — `ProxyJump` and `ProxyCommand` directives in the ssh config are honored, so connections route through bastion/jump hosts transparently.
69
+ - **Auto-connect** — with `"auto_connect": true`, the plugin connects at startup to every `Host` entry that has a `HostName`, using the configured key (or `~/.ssh/id_rsa`). Unreachable hosts are retried with backoff and skipped without blocking startup.
70
+ - **Missing file alert** — if `ssh_config_path` points to a file that does not exist, the plugin warns on connect instead of silently ignoring the setting.
60
71
 
61
72
  Example ssh config:
62
73
 
@@ -66,9 +77,13 @@ Host prod-web
66
77
  User deploy
67
78
  Port 2222
68
79
  IdentityFile ~/.ssh/prod_key
80
+
81
+ Host prod-db
82
+ HostName 10.0.0.50
83
+ ProxyJump jumpuser@bastion:2200
69
84
  ```
70
85
 
71
- With `ssh.connect(host="prod-web")` the plugin connects to `deploy@10.0.0.10:2222` using `~/.ssh/prod_key`.
86
+ With `ssh.connect(host="prod-web")` the plugin connects to `deploy@10.0.0.10:2222` using `~/.ssh/prod_key`. `prod-db` is reached tunneled through `bastion`.
72
87
 
73
88
  ## Security Modes
74
89
 
@@ -157,6 +172,12 @@ ssh.audit_log(session_id="prod-server", command_filter="docker")
157
172
 
158
173
  ## Security
159
174
 
175
+ ### Host Key Verification
176
+ With `"strict_host_key": true` the plugin verifies the remote server's host key against `~/.ssh/known_hosts` before completing the handshake:
177
+ - a **matching** key → connection allowed;
178
+ - a **changed** key (possible MITM) → connection refused;
179
+ - an **unknown** host → connection refused (add the host key to `known_hosts` first, e.g. via `ssh-keyscan host >> ~/.ssh/known_hosts`).
180
+
160
181
  ### Destructive Commands (100% Blocked)
161
182
  These commands are **permanently blocked** with no exceptions:
162
183
 
@@ -183,7 +204,8 @@ These commands require your explicit approval each time:
183
204
  ### Credential Safety
184
205
  - Passwords and SSH keys are **never** stored in logs or output
185
206
  - Session info only contains host, username, and port
186
- - Credentials are held in memory only during the session
207
+ - Passwords are cleared from memory right after a successful handshake
208
+ - For this reason password-authenticated sessions do **not** auto-reconnect (re-issue `ssh.connect`); key-authenticated sessions reconnect automatically from the on-disk key
187
209
  - All sessions are destroyed when the plugin is disposed
188
210
 
189
211
  ## Audit Log
@@ -27,6 +27,14 @@ export declare const SshPluginConfig: z.ZodObject<{
27
27
  ssh_config_path: z.ZodOptional<z.ZodString>;
28
28
  /** Auto-connect on plugin boot (requires saved session) */
29
29
  auto_connect: z.ZodDefault<z.ZodBoolean>;
30
+ /** Reconnect a dropped session automatically on the next command */
31
+ auto_reconnect: z.ZodDefault<z.ZodBoolean>;
32
+ /** Reject hosts whose key is not in known_hosts (MITM protection) */
33
+ strict_host_key: z.ZodDefault<z.ZodBoolean>;
34
+ /** Max commands allowed per host per minute (rate limiting) */
35
+ rate_limit_per_minute: z.ZodDefault<z.ZodNumber>;
36
+ /** Minimum delay in seconds between commands on the same host (cooldown) */
37
+ cooldown_seconds: z.ZodDefault<z.ZodNumber>;
30
38
  }, "strict", z.ZodTypeAny, {
31
39
  mode: "full" | "restricted" | "read_only";
32
40
  max_sessions: number;
@@ -35,6 +43,10 @@ export declare const SshPluginConfig: z.ZodObject<{
35
43
  blocklist_extra: string[];
36
44
  allowlist: string[];
37
45
  auto_connect: boolean;
46
+ auto_reconnect: boolean;
47
+ strict_host_key: boolean;
48
+ rate_limit_per_minute: number;
49
+ cooldown_seconds: number;
38
50
  ssh_config_path?: string | undefined;
39
51
  }, {
40
52
  mode?: "full" | "restricted" | "read_only" | undefined;
@@ -45,6 +57,10 @@ export declare const SshPluginConfig: z.ZodObject<{
45
57
  allowlist?: string[] | undefined;
46
58
  ssh_config_path?: string | undefined;
47
59
  auto_connect?: boolean | undefined;
60
+ auto_reconnect?: boolean | undefined;
61
+ strict_host_key?: boolean | undefined;
62
+ rate_limit_per_minute?: number | undefined;
63
+ cooldown_seconds?: number | undefined;
48
64
  }>;
49
65
  export type SshPluginConfigType = z.infer<typeof SshPluginConfig>;
50
66
  export declare const DEFAULT_CONFIG: SshPluginConfigType;
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB;;;;;GAKG;AACH,eAAO,MAAM,YAAY,gDAA8C,CAAA;AACvE,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAA;AAEvD;;GAEG;AACH,eAAO,MAAM,eAAe;IAExB,0CAA0C;;IAG1C,sCAAsC;;IAGtC,yCAAyC;;IAGzC,sCAAsC;;IAGtC,2DAA2D;;IAG3D,qEAAqE;;IAGrE,8CAA8C;;IAG9C,2DAA2D;;;;;;;;;;;;;;;;;;;;EAGpD,CAAA;AAEX,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAA;AAEjE,eAAO,MAAM,cAAc,EAAE,mBAQ5B,CAAA"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB;;;;;GAKG;AACH,eAAO,MAAM,YAAY,gDAA8C,CAAA;AACvE,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAA;AAEvD;;GAEG;AACH,eAAO,MAAM,eAAe;IAExB,0CAA0C;;IAG1C,sCAAsC;;IAGtC,yCAAyC;;IAGzC,sCAAsC;;IAGtC,2DAA2D;;IAG3D,qEAAqE;;IAGrE,8CAA8C;;IAG9C,2DAA2D;;IAG3D,oEAAoE;;IAGpE,qEAAqE;;IAGrE,+DAA+D;;IAG/D,4EAA4E;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAGrE,CAAA;AAEX,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAA;AAEjE,eAAO,MAAM,cAAc,EAAE,mBAY5B,CAAA"}
@@ -27,6 +27,14 @@ export const SshPluginConfig = z
27
27
  ssh_config_path: z.string().optional(),
28
28
  /** Auto-connect on plugin boot (requires saved session) */
29
29
  auto_connect: z.boolean().default(false),
30
+ /** Reconnect a dropped session automatically on the next command */
31
+ auto_reconnect: z.boolean().default(true),
32
+ /** Reject hosts whose key is not in known_hosts (MITM protection) */
33
+ strict_host_key: z.boolean().default(false),
34
+ /** Max commands allowed per host per minute (rate limiting) */
35
+ rate_limit_per_minute: z.number().int().min(1).max(10000).default(120),
36
+ /** Minimum delay in seconds between commands on the same host (cooldown) */
37
+ cooldown_seconds: z.number().int().min(0).max(3600).default(0),
30
38
  })
31
39
  .strict();
32
40
  export const DEFAULT_CONFIG = {
@@ -37,5 +45,9 @@ export const DEFAULT_CONFIG = {
37
45
  blocklist_extra: [],
38
46
  allowlist: [],
39
47
  auto_connect: false,
48
+ auto_reconnect: true,
49
+ strict_host_key: false,
50
+ rate_limit_per_minute: 120,
51
+ cooldown_seconds: 0,
40
52
  };
41
53
  //# sourceMappingURL=schema.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,YAAY,EAAE,WAAW,CAAC,CAAC,CAAA;AAGvE;;GAEG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC;KAC7B,MAAM,CAAC;IACN,0CAA0C;IAC1C,IAAI,EAAE,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC;IAElC,sCAAsC;IACtC,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAExD,yCAAyC;IACzC,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAE7D,sCAAsC;IACtC,aAAa,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IAExC,2DAA2D;IAC3D,eAAe,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAEhD,qEAAqE;IACrE,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAE1C,8CAA8C;IAC9C,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAEtC,2DAA2D;IAC3D,YAAY,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;CACzC,CAAC;KACD,MAAM,EAAE,CAAA;AAIX,MAAM,CAAC,MAAM,cAAc,GAAwB;IACjD,IAAI,EAAE,MAAM;IACZ,YAAY,EAAE,CAAC;IACf,eAAe,EAAE,EAAE;IACnB,aAAa,EAAE,IAAI;IACnB,eAAe,EAAE,EAAE;IACnB,SAAS,EAAE,EAAE;IACb,YAAY,EAAE,KAAK;CACpB,CAAA"}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,YAAY,EAAE,WAAW,CAAC,CAAC,CAAA;AAGvE;;GAEG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC;KAC7B,MAAM,CAAC;IACN,0CAA0C;IAC1C,IAAI,EAAE,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC;IAElC,sCAAsC;IACtC,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAExD,yCAAyC;IACzC,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAE7D,sCAAsC;IACtC,aAAa,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IAExC,2DAA2D;IAC3D,eAAe,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAEhD,qEAAqE;IACrE,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAE1C,8CAA8C;IAC9C,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAEtC,2DAA2D;IAC3D,YAAY,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IAExC,oEAAoE;IACpE,cAAc,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IAEzC,qEAAqE;IACrE,eAAe,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IAE3C,+DAA+D;IAC/D,qBAAqB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC;IAEtE,4EAA4E;IAC5E,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;CAC/D,CAAC;KACD,MAAM,EAAE,CAAA;AAIX,MAAM,CAAC,MAAM,cAAc,GAAwB;IACjD,IAAI,EAAE,MAAM;IACZ,YAAY,EAAE,CAAC;IACf,eAAe,EAAE,EAAE;IACnB,aAAa,EAAE,IAAI;IACnB,eAAe,EAAE,EAAE;IACnB,SAAS,EAAE,EAAE;IACb,YAAY,EAAE,KAAK;IACnB,cAAc,EAAE,IAAI;IACpB,eAAe,EAAE,KAAK;IACtB,qBAAqB,EAAE,GAAG;IAC1B,gBAAgB,EAAE,CAAC;CACpB,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;;;;;AA2BjD,wBAGC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;;;;;AAiCjD,wBAGC"}
package/dist/index.js CHANGED
@@ -5,7 +5,12 @@ const SshPlugin = async (_ctx, options) => {
5
5
  // ── Resolve configuration ──
6
6
  const config = resolveConfig(options);
7
7
  // ── Create tools and hooks with resolved config ──
8
- const tools = createSshTools(config.mode, config.max_sessions, config.default_timeout, config.blocklist_extra, config.ssh_config_path, config.auto_connect);
8
+ const tools = createSshTools(config.mode, config.max_sessions, config.default_timeout, config.blocklist_extra, config.ssh_config_path, config.auto_connect, {
9
+ strictHostKey: config.strict_host_key,
10
+ rateLimitPerMinute: config.rate_limit_per_minute,
11
+ cooldownSeconds: config.cooldown_seconds,
12
+ autoReconnect: config.auto_reconnect,
13
+ });
9
14
  const hooks = createSshHooks(config.mode, config.blocklist_extra);
10
15
  return {
11
16
  tool: tools,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAGpD,MAAM,SAAS,GAAW,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;IAChD,8BAA8B;IAC9B,MAAM,MAAM,GAAG,aAAa,CAAC,OAAkC,CAAC,CAAA;IAEhE,oDAAoD;IACpD,MAAM,KAAK,GAAG,cAAc,CAC1B,MAAM,CAAC,IAAI,EACX,MAAM,CAAC,YAAY,EACnB,MAAM,CAAC,eAAe,EACtB,MAAM,CAAC,eAAe,EACtB,MAAM,CAAC,eAAe,EACtB,MAAM,CAAC,YAAY,CACpB,CAAA;IACD,MAAM,KAAK,GAAG,cAAc,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,eAAe,CAAC,CAAA;IAEjE,OAAO;QACL,IAAI,EAAE,KAAK;QACX,GAAG,KAAK;KACT,CAAA;AACH,CAAC,CAAA;AAED,eAAe;IACb,EAAE,EAAE,cAAc;IAClB,MAAM,EAAE,SAAS;CAClB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAGpD,MAAM,SAAS,GAAW,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;IAChD,8BAA8B;IAC9B,MAAM,MAAM,GAAwB,aAAa,CAAC,OAAkC,CAAC,CAAA;IAErF,oDAAoD;IACpD,MAAM,KAAK,GAAG,cAAc,CAC1B,MAAM,CAAC,IAAI,EACX,MAAM,CAAC,YAAY,EACnB,MAAM,CAAC,eAAe,EACtB,MAAM,CAAC,eAAe,EACtB,MAAM,CAAC,eAAe,EACtB,MAAM,CAAC,YAAY,EACnB;QACE,aAAa,EAAE,MAAM,CAAC,eAAe;QACrC,kBAAkB,EAAE,MAAM,CAAC,qBAAqB;QAChD,eAAe,EAAE,MAAM,CAAC,gBAAgB;QACxC,aAAa,EAAE,MAAM,CAAC,cAAc;KACrC,CACF,CAAA;IACD,MAAM,KAAK,GAAG,cAAc,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,eAAe,CAAC,CAAA;IAEjE,OAAO;QACL,IAAI,EAAE,KAAK;QACX,GAAG,KAAK;KACT,CAAA;AACH,CAAC,CAAA;AAED,eAAe;IACb,EAAE,EAAE,cAAc;IAClB,MAAM,EAAE,SAAS;CAClB,CAAA"}
@@ -9,10 +9,16 @@ export interface SshConfigEntry {
9
9
  port?: number;
10
10
  /** Identity file (IdentityFile). */
11
11
  identityFile?: string;
12
+ /** ProxyJump: comma-separated list of user@host:port hops. */
13
+ proxyJump?: string;
14
+ /** ProxyCommand: external command to open the tunnel. */
15
+ proxyCommand?: string;
12
16
  }
13
17
  export interface LoadedSshConfig {
14
18
  entries: SshConfigEntry[];
15
19
  path: string;
20
+ /** Whether the configured ssh config file exists on disk. */
21
+ exists: boolean;
16
22
  }
17
23
  /** Expand a leading `~` to the current user's home directory. */
18
24
  export declare function expandHomePath(p: string): string;
@@ -31,6 +37,15 @@ export declare function parseSshConfig(content: string): SshConfigEntry[];
31
37
  export declare function findHostConfig(entries: SshConfigEntry[], host: string): SshConfigEntry | undefined;
32
38
  /** Load and parse the ssh config file. Empty entry list if it does not exist. */
33
39
  export declare function loadSshConfig(configuredPath?: string): LoadedSshConfig;
40
+ /**
41
+ * Parse a ProxyJump value ("user@host:port,user2@host2").
42
+ */
43
+ export declare function parseProxyJump(value: string, defaultUser?: string, defaultKeyPath?: string): Array<{
44
+ host: string;
45
+ port: number;
46
+ username?: string;
47
+ keyPath?: string;
48
+ }>;
34
49
  /**
35
50
  * Concrete machines suitable for auto-connecting: `Host` entries that
36
51
  * resolve to a real `HostName`.
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/ssh/config.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,cAAc;IAC7B,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mBAAmB;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,mBAAmB;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,oCAAoC;IACpC,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,cAAc,EAAE,CAAA;IACzB,IAAI,EAAE,MAAM,CAAA;CACb;AAED,iEAAiE;AACjE,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAKhD;AAED,6EAA6E;AAC7E,wBAAgB,oBAAoB,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,CAMpE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CAoDhE;AAaD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,cAAc,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAelG;AAED,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,eAAe,CAItE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,cAAc,CAAC,EAAE,MAAM,GACtB,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAiBjG"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/ssh/config.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,cAAc;IAC7B,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mBAAmB;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,mBAAmB;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,oCAAoC;IACpC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,yDAAyD;IACzD,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,cAAc,EAAE,CAAA;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,6DAA6D;IAC7D,MAAM,EAAE,OAAO,CAAA;CAChB;AAED,iEAAiE;AACjE,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAKhD;AAED,6EAA6E;AAC7E,wBAAgB,oBAAoB,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,CAMpE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CA0DhE;AAaD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,cAAc,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAelG;AAED,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,eAAe,CAItE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,WAAW,CAAC,EAAE,MAAM,EACpB,cAAc,CAAC,EAAE,MAAM,GACtB,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA2B5E;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,cAAc,CAAC,EAAE,MAAM,GACtB,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAiBjG"}
@@ -68,6 +68,12 @@ export function parseSshConfig(content) {
68
68
  case "identityfile":
69
69
  current.identityFile = value;
70
70
  break;
71
+ case "proxyjump":
72
+ current.proxyJump = value;
73
+ break;
74
+ case "proxycommand":
75
+ current.proxyCommand = value;
76
+ break;
71
77
  }
72
78
  }
73
79
  return entries;
@@ -111,8 +117,39 @@ export function findHostConfig(entries, host) {
111
117
  export function loadSshConfig(configuredPath) {
112
118
  const path = resolveSshConfigPath(configuredPath);
113
119
  if (!existsSync(path))
114
- return { entries: [], path };
115
- return { entries: parseSshConfig(readFileSync(path, "utf-8")), path };
120
+ return { entries: [], path, exists: false };
121
+ return { entries: parseSshConfig(readFileSync(path, "utf-8")), path, exists: true };
122
+ }
123
+ /**
124
+ * Parse a ProxyJump value ("user@host:port,user2@host2").
125
+ */
126
+ export function parseProxyJump(value, defaultUser, defaultKeyPath) {
127
+ return value
128
+ .split(",")
129
+ .map((hop) => hop.trim())
130
+ .filter(Boolean)
131
+ .map((hop) => {
132
+ let user;
133
+ let rest = hop;
134
+ if (hop.includes("@")) {
135
+ const at = hop.lastIndexOf("@");
136
+ user = hop.slice(0, at);
137
+ rest = hop.slice(at + 1);
138
+ }
139
+ let port = 22;
140
+ let host = rest;
141
+ const colon = rest.match(/^(.*):(\d+)$/);
142
+ if (colon) {
143
+ host = colon[1];
144
+ port = Number(colon[2]);
145
+ }
146
+ return {
147
+ host,
148
+ port,
149
+ username: user || defaultUser,
150
+ keyPath: defaultKeyPath,
151
+ };
152
+ });
116
153
  }
117
154
  /**
118
155
  * Concrete machines suitable for auto-connecting: `Host` entries that
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/ssh/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/ssh/config.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { type ProxySpec } from "./proxy.js";
1
2
  export interface SshConnectionConfig {
2
3
  host: string;
3
4
  port: number;
@@ -11,6 +12,16 @@ export interface SshConnectionConfig {
11
12
  timeout?: number;
12
13
  /** Connection alias for display */
13
14
  alias?: string;
15
+ /** Reject hosts whose key is not (an identical match) in known_hosts. */
16
+ strictHostKey?: boolean;
17
+ /** Custom path to a known_hosts file. */
18
+ knownHostsPath?: string;
19
+ /** SSH config default path, used to locate known_hosts next to it. */
20
+ sshConfigPath?: string;
21
+ /** ProxyJump / ProxyCommand support. */
22
+ proxy?: ProxySpec;
23
+ /** Automatically reconnect a dropped session on the next exec/sftp. */
24
+ autoReconnect?: boolean;
14
25
  }
15
26
  export interface SshConnectionInfo {
16
27
  id: string;
@@ -26,21 +37,28 @@ export declare class SshConnection {
26
37
  private _connected;
27
38
  private _connectedAt?;
28
39
  private _lastActivity?;
29
- private _reconnectAttempts;
30
- private _maxReconnectAttempts;
40
+ private _maxAttempts;
41
+ private _attempts;
42
+ private _proxyCleanup?;
43
+ private _hostKeyVerdict?;
31
44
  constructor(id: string, config: SshConnectionConfig);
32
45
  get id(): string;
33
46
  get config(): SshConnectionConfig;
34
47
  get connected(): boolean;
35
48
  get connectedAt(): Date | undefined;
36
49
  get lastActivity(): Date | undefined;
50
+ /** Result of the last known_hosts check (undefined when disabled). */
51
+ get hostKeyVerdict(): "ok" | "changed" | "unknown" | undefined;
37
52
  /**
38
- * Connect to the SSH server.
53
+ * Connect to the SSH server, with bounded retries.
39
54
  */
40
55
  connect(): Promise<void>;
56
+ private connectWithBackoff;
57
+ private buildConnectConfig;
58
+ private openConnection;
41
59
  /**
42
- * Execute a command on the remote server.
43
- * Returns a stream for real-time output.
60
+ * Execute a command on the remote server. Automatically reconnects a
61
+ * dropped session before running.
44
62
  */
45
63
  exec(command: string, options?: {
46
64
  cwd?: string;
@@ -59,7 +77,8 @@ export declare class SshConnection {
59
77
  timeout?: number;
60
78
  }): NodeJS.ReadableStream | null;
61
79
  /**
62
- * Get an SFTP session for file transfers.
80
+ * Get an SFTP session for file transfers. Automatically reconnects a
81
+ * dropped session before returning.
63
82
  */
64
83
  sftp(): Promise<any>;
65
84
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/ssh/connection.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,UAAU,GAAG,KAAK,CAAA;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,mCAAmC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,mBAAmB,CAAA;IAC3B,SAAS,EAAE,OAAO,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,GAAG,CAAQ;IACnB,OAAO,CAAC,UAAU,CAAQ;IAC1B,OAAO,CAAC,YAAY,CAAC,CAAM;IAC3B,OAAO,CAAC,aAAa,CAAC,CAAM;IAC5B,OAAO,CAAC,kBAAkB,CAAI;IAC9B,OAAO,CAAC,qBAAqB,CAAI;gBAErB,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB;IAMnD,IAAI,EAAE,IAAI,MAAM,CAEf;IAED,IAAI,MAAM,IAAI,mBAAmB,CAEhC;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,WAAW,IAAI,IAAI,GAAG,SAAS,CAElC;IAED,IAAI,YAAY,IAAI,IAAI,GAAG,SAAS,CAEnC;IAED;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAmE9B;;;OAGG;IACG,IAAI,CACR,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAC3C,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC;IA8D/F;;OAEG;IACH,UAAU,CACR,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAC3C,MAAM,CAAC,cAAc,GAAG,IAAI;IAgB/B;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC;IAgB1B;;OAEG;IACH,UAAU,IAAI,IAAI;IAOlB;;OAEG;IACH,OAAO,IAAI,iBAAiB;CAgB7B"}
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/ssh/connection.ts"],"names":[],"mappings":"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"}
@@ -2,6 +2,9 @@ import { Client } from "ssh2";
2
2
  import { readFileSync, existsSync } from "fs";
3
3
  import { resolve as pathResolve } from "path";
4
4
  import { escapeShellArg } from "./sanitizer.js";
5
+ import { verifyHostKey } from "./known_hosts.js";
6
+ import { prepareProxy } from "./proxy.js";
7
+ const DEFAULT_KNOWN_HOSTS = () => pathResolve(process.env.HOME || "", ".ssh", "known_hosts");
5
8
  export class SshConnection {
6
9
  client;
7
10
  _config;
@@ -9,12 +12,15 @@ export class SshConnection {
9
12
  _connected = false;
10
13
  _connectedAt;
11
14
  _lastActivity;
12
- _reconnectAttempts = 0;
13
- _maxReconnectAttempts = 3;
15
+ _maxAttempts = 3;
16
+ _attempts = 0;
17
+ _proxyCleanup;
18
+ _hostKeyVerdict;
14
19
  constructor(id, config) {
15
20
  this._id = id;
16
21
  this._config = config;
17
- this.client = new Client();
22
+ this._config.strictHostKey = this._config.strictHostKey ?? false;
23
+ this._config.autoReconnect = this._config.autoReconnect ?? true;
18
24
  }
19
25
  get id() {
20
26
  return this._id;
@@ -31,74 +37,124 @@ export class SshConnection {
31
37
  get lastActivity() {
32
38
  return this._lastActivity;
33
39
  }
40
+ /** Result of the last known_hosts check (undefined when disabled). */
41
+ get hostKeyVerdict() {
42
+ return this._hostKeyVerdict;
43
+ }
34
44
  /**
35
- * Connect to the SSH server.
45
+ * Connect to the SSH server, with bounded retries.
36
46
  */
37
47
  async connect() {
38
48
  if (!this._config.username) {
39
49
  throw new Error("A username is required to connect (provide one or set User in the ssh config).");
40
50
  }
41
- return new Promise((resolve, reject) => {
42
- const connectConfig = {
51
+ this._attempts = 0;
52
+ await this.connectWithBackoff();
53
+ // Credentials are only needed for the handshake; drop them from memory
54
+ // once the connection is established so they are not retained.
55
+ this._config.password = undefined;
56
+ }
57
+ async connectWithBackoff() {
58
+ try {
59
+ const cfg = await this.buildConnectConfig();
60
+ await this.openConnection(cfg);
61
+ }
62
+ catch (err) {
63
+ this._attempts++;
64
+ if (this._attempts < this._maxAttempts) {
65
+ await new Promise((r) => setTimeout(r, 1000 * this._attempts));
66
+ return this.connectWithBackoff();
67
+ }
68
+ throw err;
69
+ }
70
+ }
71
+ async buildConnectConfig() {
72
+ const connectConfig = {
73
+ host: this._config.host,
74
+ port: this._config.port,
75
+ username: this._config.username,
76
+ readyTimeout: this._config.timeout || 10000,
77
+ keepaliveInterval: 10000,
78
+ keepaliveCountMax: 3,
79
+ };
80
+ // ── Host key verification (MITM protection) ──
81
+ if (this._config.strictHostKey) {
82
+ const knownHostsPath = this._config.knownHostsPath || DEFAULT_KNOWN_HOSTS();
83
+ connectConfig.hostVerifier = (key) => {
84
+ const verdict = verifyHostKey(this._config.host, key, knownHostsPath, this._config.port);
85
+ this._hostKeyVerdict = verdict;
86
+ // A changed key is always rejected. An unknown key is only accepted
87
+ // in non-strict mode; here strict mode requires it to be present.
88
+ return verdict === "ok";
89
+ };
90
+ }
91
+ // ── Proxy tunnel (ProxyJump / ProxyCommand) ──
92
+ if (this._config.proxy) {
93
+ const prepared = await prepareProxy(this._config.proxy, {
43
94
  host: this._config.host,
44
95
  port: this._config.port,
45
96
  username: this._config.username,
46
- readyTimeout: this._config.timeout || 10000,
47
- keepaliveInterval: 10000,
48
- keepaliveCountMax: 3,
49
- };
50
- // Configure authentication
51
- if (this._config.authMethod === "password") {
97
+ keyPath: this._config.keyPath,
98
+ passphrase: this._config.passphrase,
99
+ });
100
+ this._proxyCleanup = prepared.cleanup;
101
+ connectConfig.sock = prepared.sock;
102
+ }
103
+ // ── Authentication ──
104
+ if (this._config.authMethod === "password") {
105
+ if (this._config.password) {
52
106
  connectConfig.password = this._config.password;
53
107
  }
54
- else {
55
- // Key-based authentication
56
- const keyPath = this._config.keyPath
57
- ? pathResolve(this._config.keyPath.replace("~", process.env.HOME || ""))
58
- : pathResolve(process.env.HOME || "", ".ssh", "id_rsa");
59
- if (!existsSync(keyPath)) {
60
- reject(new Error(`SSH key not found: ${keyPath}`));
61
- return;
62
- }
63
- connectConfig.privateKey = readFileSync(keyPath, "utf-8");
64
- if (this._config.passphrase) {
65
- connectConfig.passphrase = this._config.passphrase;
66
- }
108
+ }
109
+ else {
110
+ const keyPath = this._config.keyPath
111
+ ? pathResolve(this._config.keyPath.replace("~", process.env.HOME || ""))
112
+ : pathResolve(process.env.HOME || "", ".ssh", "id_rsa");
113
+ if (!existsSync(keyPath)) {
114
+ throw new Error(`SSH key not found: ${keyPath}`);
115
+ }
116
+ connectConfig.privateKey = readFileSync(keyPath, "utf-8");
117
+ if (this._config.passphrase) {
118
+ connectConfig.passphrase = this._config.passphrase;
67
119
  }
68
- this.client.on("ready", () => {
120
+ }
121
+ return connectConfig;
122
+ }
123
+ async openConnection(cfg) {
124
+ // A fresh Client per attempt so a failed handshake never leaves the
125
+ // previous transport in an unusable state.
126
+ const client = new Client();
127
+ this.client = client;
128
+ return new Promise((resolve, reject) => {
129
+ client.on("ready", () => {
69
130
  this._connected = true;
70
131
  this._connectedAt = new Date();
71
132
  this._lastActivity = new Date();
72
- this._reconnectAttempts = 0;
73
133
  resolve();
74
134
  });
75
- this.client.on("error", (err) => {
135
+ client.on("error", (err) => {
76
136
  this._connected = false;
77
- if (this._reconnectAttempts < this._maxReconnectAttempts) {
78
- this._reconnectAttempts++;
79
- // Attempt reconnect after delay
80
- setTimeout(() => {
81
- this.connect().catch(() => { });
82
- }, 1000 * this._reconnectAttempts);
83
- }
84
137
  reject(err);
85
138
  });
86
- this.client.on("close", () => {
139
+ client.on("close", () => {
87
140
  this._connected = false;
88
141
  });
89
- this.client.on("end", () => {
142
+ client.on("end", () => {
90
143
  this._connected = false;
91
144
  });
92
- this.client.connect(connectConfig);
145
+ client.connect(cfg);
93
146
  });
94
147
  }
95
148
  /**
96
- * Execute a command on the remote server.
97
- * Returns a stream for real-time output.
149
+ * Execute a command on the remote server. Automatically reconnects a
150
+ * dropped session before running.
98
151
  */
99
152
  async exec(command, options) {
100
153
  if (!this._connected) {
101
- throw new Error("SSH connection is not active");
154
+ if (this._config.autoReconnect === false) {
155
+ throw new Error("SSH connection is not active");
156
+ }
157
+ await this.connect();
102
158
  }
103
159
  this._lastActivity = new Date();
104
160
  const wrappedCommand = options?.cwd
@@ -115,7 +171,6 @@ export class SshConnection {
115
171
  let stdout = "";
116
172
  let stderr = "";
117
173
  let killed = false;
118
- // Set timeout
119
174
  if (timeoutMs > 0) {
120
175
  timeoutId = setTimeout(() => {
121
176
  killed = true;
@@ -165,12 +220,17 @@ export class SshConnection {
165
220
  return null; // Placeholder - actual streaming handled in executor.ts
166
221
  }
167
222
  /**
168
- * Get an SFTP session for file transfers.
223
+ * Get an SFTP session for file transfers. Automatically reconnects a
224
+ * dropped session before returning.
169
225
  */
170
226
  async sftp() {
171
227
  if (!this._connected) {
172
- throw new Error("SSH connection is not active");
228
+ if (this._config.autoReconnect === false) {
229
+ throw new Error("SSH connection is not active");
230
+ }
231
+ await this.connect();
173
232
  }
233
+ this._lastActivity = new Date();
174
234
  return new Promise((resolve, reject) => {
175
235
  this.client.sftp((err, sftp) => {
176
236
  if (err) {
@@ -185,10 +245,12 @@ export class SshConnection {
185
245
  * Disconnect from the SSH server.
186
246
  */
187
247
  disconnect() {
188
- if (this._connected) {
248
+ if (this.client) {
189
249
  this.client.end();
190
- this._connected = false;
191
250
  }
251
+ this._connected = false;
252
+ this._proxyCleanup?.();
253
+ this._proxyCleanup = undefined;
192
254
  }
193
255
  /**
194
256
  * Get connection info for display.