dsh-over-ssh 0.0.0-stage → 0.1.0

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.
Files changed (254) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +119 -2
  3. package/agents/agent-manifest.json +20 -0
  4. package/agents/linux-amd64/dsh-over-ssh-agent +0 -0
  5. package/agents/linux-arm64/dsh-over-ssh-agent +0 -0
  6. package/cordis.patch.yml +84 -0
  7. package/lib/client.js +6611 -0
  8. package/lib/file-references.js +366 -0
  9. package/lib/fs.js +1368 -0
  10. package/lib/index.js +19034 -0
  11. package/lib/local-bridge-refs.js +16 -0
  12. package/lib/local-bridge.js +16 -0
  13. package/lib/terminal-wrapper.js +294 -0
  14. package/lib/types/src/client/controls.d.ts +131 -0
  15. package/lib/types/src/client/core/card-focus-trap.d.ts +45 -0
  16. package/lib/types/src/client/core/controls.d.ts +164 -0
  17. package/lib/types/src/client/core/latest.d.ts +48 -0
  18. package/lib/types/src/client/core/locale.d.ts +38 -0
  19. package/lib/types/src/client/core/message.d.ts +37 -0
  20. package/lib/types/src/client/core/panel-model.d.ts +134 -0
  21. package/lib/types/src/client/core/portal.d.ts +266 -0
  22. package/lib/types/src/client/core/remote.d.ts +84 -0
  23. package/lib/types/src/client/core/styles.d.ts +458 -0
  24. package/lib/types/src/client/file-tree-io.d.ts +19 -0
  25. package/lib/types/src/client/file-tree.d.ts +271 -0
  26. package/lib/types/src/client/flows/directory-browser.d.ts +587 -0
  27. package/lib/types/src/client/flows/session-directory.d.ts +415 -0
  28. package/lib/types/src/client/flows/workspace-directory-flow.d.ts +480 -0
  29. package/lib/types/src/client/index.d.ts +49 -0
  30. package/lib/types/src/client/locale.d.ts +31 -0
  31. package/lib/types/src/client/panel-model.d.ts +37 -0
  32. package/lib/types/src/client/panel.d.ts +77 -0
  33. package/lib/types/src/client/roots/connection-test.d.ts +69 -0
  34. package/lib/types/src/client/roots/roots-panel-state.d.ts +307 -0
  35. package/lib/types/src/client/roots/roots-panel-view.d.ts +280 -0
  36. package/lib/types/src/client/roots/roots-panel.d.ts +364 -0
  37. package/lib/types/src/client/roots/transfer-panel-view.d.ts +35 -0
  38. package/lib/types/src/client/roots/transfer-panel.d.ts +241 -0
  39. package/lib/types/src/client/roots/transfer-progress.d.ts +67 -0
  40. package/lib/types/src/client/roots-panel-view.d.ts +236 -0
  41. package/lib/types/src/client/roots-panel.d.ts +347 -0
  42. package/lib/types/src/client/session/session-panel-view.d.ts +336 -0
  43. package/lib/types/src/client/session/session-panel.d.ts +322 -0
  44. package/lib/types/src/client/session/session-remote-mark.d.ts +110 -0
  45. package/lib/types/src/client/session/session-row-actions.d.ts +398 -0
  46. package/lib/types/src/client/session-panel.d.ts +152 -0
  47. package/lib/types/src/client/styles.d.ts +252 -0
  48. package/lib/types/src/client/workspace-directory-flow.d.ts +342 -0
  49. package/lib/types/src/config.d.ts +312 -0
  50. package/lib/types/src/file-references.d.ts +25 -0
  51. package/lib/types/src/fs.d.ts +46 -0
  52. package/lib/types/src/host/app/backend.d.ts +170 -0
  53. package/lib/types/src/host/app/recycle-bin/recycle-bin-contract.d.ts +247 -0
  54. package/lib/types/src/host/app/recycle-bin/recycle-bin-move.d.ts +18 -0
  55. package/lib/types/src/host/app/recycle-bin/recycle-bin-service.d.ts +45 -0
  56. package/lib/types/src/host/app/recycle-bin/recycle-bin-world.d.ts +38 -0
  57. package/lib/types/src/host/app/recycle-bin-contract.d.ts +215 -0
  58. package/lib/types/src/host/app/recycle-bin-move.d.ts +18 -0
  59. package/lib/types/src/host/app/recycle-bin-service.d.ts +45 -0
  60. package/lib/types/src/host/app/recycle-bin-world.d.ts +35 -0
  61. package/lib/types/src/host/app/roots/roots-api-connections.d.ts +49 -0
  62. package/lib/types/src/host/app/roots/roots-api-contract.d.ts +369 -0
  63. package/lib/types/src/host/app/roots/roots-api-directories.d.ts +13 -0
  64. package/lib/types/src/host/app/roots/roots-api-http.d.ts +77 -0
  65. package/lib/types/src/host/app/roots/roots-api-long-poll.d.ts +130 -0
  66. package/lib/types/src/host/app/roots/roots-api-projections.d.ts +102 -0
  67. package/lib/types/src/host/app/roots/roots-api-roots.d.ts +13 -0
  68. package/lib/types/src/host/app/roots/roots-api-state.d.ts +22 -0
  69. package/lib/types/src/host/app/roots/roots-api-transfers.d.ts +26 -0
  70. package/lib/types/src/host/app/roots/roots-api.d.ts +55 -0
  71. package/lib/types/src/host/app/roots/user-roots.d.ts +552 -0
  72. package/lib/types/src/host/app/roots-api-connections.d.ts +49 -0
  73. package/lib/types/src/host/app/roots-api-contract.d.ts +339 -0
  74. package/lib/types/src/host/app/roots-api-directories.d.ts +13 -0
  75. package/lib/types/src/host/app/roots-api-http.d.ts +77 -0
  76. package/lib/types/src/host/app/roots-api-projections.d.ts +102 -0
  77. package/lib/types/src/host/app/roots-api-roots.d.ts +13 -0
  78. package/lib/types/src/host/app/roots-api-state.d.ts +22 -0
  79. package/lib/types/src/host/app/roots-api-support.d.ts +182 -0
  80. package/lib/types/src/host/app/roots-api-transfers.d.ts +13 -0
  81. package/lib/types/src/host/app/roots-api.d.ts +55 -0
  82. package/lib/types/src/host/app/routed-fs.d.ts +239 -0
  83. package/lib/types/src/host/app/serialize.d.ts +37 -0
  84. package/lib/types/src/host/app/session-admin/session-admin-context.d.ts +36 -0
  85. package/lib/types/src/host/app/session-admin/session-admin-contract.d.ts +221 -0
  86. package/lib/types/src/host/app/session-admin/session-admin-delete.d.ts +34 -0
  87. package/lib/types/src/host/app/session-admin/session-admin-service.d.ts +40 -0
  88. package/lib/types/src/host/app/session-admin-context.d.ts +36 -0
  89. package/lib/types/src/host/app/session-admin-contract.d.ts +214 -0
  90. package/lib/types/src/host/app/session-admin-delete.d.ts +34 -0
  91. package/lib/types/src/host/app/session-admin-service.d.ts +38 -0
  92. package/lib/types/src/host/app/takeover/takeover-plan.d.ts +227 -0
  93. package/lib/types/src/host/app/takeover/takeover-rows.d.ts +55 -0
  94. package/lib/types/src/host/app/transport/transport-selector.d.ts +98 -0
  95. package/lib/types/src/host/app/transport/transport-service.d.ts +82 -0
  96. package/lib/types/src/host/app/transport-selector.d.ts +98 -0
  97. package/lib/types/src/host/app/transport-service.d.ts +75 -0
  98. package/lib/types/src/host/app/user-roots.d.ts +461 -0
  99. package/lib/types/src/host/domain/bin-location.d.ts +79 -0
  100. package/lib/types/src/host/domain/capability.d.ts +78 -0
  101. package/lib/types/src/host/domain/connection.d.ts +95 -0
  102. package/lib/types/src/host/domain/errors.d.ts +55 -0
  103. package/lib/types/src/host/domain/glob-pattern.d.ts +62 -0
  104. package/lib/types/src/host/domain/literal-text.d.ts +63 -0
  105. package/lib/types/src/host/domain/path-grammar.d.ts +84 -0
  106. package/lib/types/src/host/domain/recycle-bin.d.ts +337 -0
  107. package/lib/types/src/host/domain/root.d.ts +74 -0
  108. package/lib/types/src/host/domain/roots/bin-location.d.ts +91 -0
  109. package/lib/types/src/host/domain/roots/connection.d.ts +106 -0
  110. package/lib/types/src/host/domain/roots/root.d.ts +108 -0
  111. package/lib/types/src/host/domain/roots/route-table.d.ts +102 -0
  112. package/lib/types/src/host/domain/roots/session-roots.d.ts +151 -0
  113. package/lib/types/src/host/domain/roots/ssh-config.d.ts +73 -0
  114. package/lib/types/src/host/domain/route-table.d.ts +80 -0
  115. package/lib/types/src/host/domain/session/session-admin.d.ts +345 -0
  116. package/lib/types/src/host/domain/session/terminal-target.d.ts +136 -0
  117. package/lib/types/src/host/domain/session-admin.d.ts +330 -0
  118. package/lib/types/src/host/domain/shell.d.ts +47 -0
  119. package/lib/types/src/host/domain/ssh-config.d.ts +73 -0
  120. package/lib/types/src/host/domain/terminal-target.d.ts +132 -0
  121. package/lib/types/src/host/domain/tool-requests.d.ts +369 -0
  122. package/lib/types/src/host/domain/transfer/recycle-bin.d.ts +337 -0
  123. package/lib/types/src/host/domain/transfer/transfer.d.ts +260 -0
  124. package/lib/types/src/host/domain/transfer.d.ts +260 -0
  125. package/lib/types/src/host/infra/backend/local-backend.d.ts +84 -0
  126. package/lib/types/src/host/infra/backend/local-version.d.ts +43 -0
  127. package/lib/types/src/host/infra/backend/platform-local-backend.d.ts +89 -0
  128. package/lib/types/src/host/infra/backend/remote-backend.d.ts +239 -0
  129. package/lib/types/src/host/infra/cache/lru.d.ts +52 -0
  130. package/lib/types/src/host/infra/cache/versioned.d.ts +77 -0
  131. package/lib/types/src/host/infra/temp-name.d.ts +19 -0
  132. package/lib/types/src/host/infra/transport/agent/agent-installer.d.ts +121 -0
  133. package/lib/types/src/host/infra/transport/agent/agent-protocol-client.d.ts +132 -0
  134. package/lib/types/src/host/infra/transport/agent/agent-protocol-codec.d.ts +140 -0
  135. package/lib/types/src/host/infra/transport/agent/agent-protocol-errors.d.ts +41 -0
  136. package/lib/types/src/host/infra/transport/agent/agent-protocol-frames.d.ts +197 -0
  137. package/lib/types/src/host/infra/transport/agent/agent-protocol-types.d.ts +564 -0
  138. package/lib/types/src/host/infra/transport/agent/agent-transport.d.ts +418 -0
  139. package/lib/types/src/host/infra/transport/agent-installer.d.ts +121 -0
  140. package/lib/types/src/host/infra/transport/agent-protocol-client.d.ts +123 -0
  141. package/lib/types/src/host/infra/transport/agent-protocol-codec.d.ts +131 -0
  142. package/lib/types/src/host/infra/transport/agent-protocol-errors.d.ts +41 -0
  143. package/lib/types/src/host/infra/transport/agent-protocol-frames.d.ts +197 -0
  144. package/lib/types/src/host/infra/transport/agent-protocol-types.d.ts +509 -0
  145. package/lib/types/src/host/infra/transport/agent-transport.d.ts +353 -0
  146. package/lib/types/src/host/infra/transport/alias-transport.d.ts +206 -0
  147. package/lib/types/src/host/infra/transport/askpass-fs.d.ts +20 -0
  148. package/lib/types/src/host/infra/transport/askpass.d.ts +133 -0
  149. package/lib/types/src/host/infra/transport/command-pool.d.ts +61 -0
  150. package/lib/types/src/host/infra/transport/fs-commands.d.ts +140 -0
  151. package/lib/types/src/host/infra/transport/git-status-route.d.ts +63 -0
  152. package/lib/types/src/host/infra/transport/git-status.d.ts +101 -0
  153. package/lib/types/src/host/infra/transport/node-spawn.d.ts +26 -0
  154. package/lib/types/src/host/infra/transport/session-window.d.ts +51 -0
  155. package/lib/types/src/host/infra/transport/shell-quote.d.ts +59 -0
  156. package/lib/types/src/host/infra/transport/ssh/alias-transport.d.ts +206 -0
  157. package/lib/types/src/host/infra/transport/ssh/askpass-fs.d.ts +20 -0
  158. package/lib/types/src/host/infra/transport/ssh/askpass.d.ts +133 -0
  159. package/lib/types/src/host/infra/transport/ssh/shell-quote.d.ts +62 -0
  160. package/lib/types/src/host/infra/transport/ssh/ssh-argv.d.ts +43 -0
  161. package/lib/types/src/host/infra/transport/ssh/ssh2-transport.d.ts +328 -0
  162. package/lib/types/src/host/infra/transport/ssh-argv.d.ts +43 -0
  163. package/lib/types/src/host/infra/transport/ssh2-transport.d.ts +302 -0
  164. package/lib/types/src/host/infra/transport/transport-base.d.ts +34 -0
  165. package/lib/types/src/host/infra/transport/transport.d.ts +527 -0
  166. package/lib/types/src/host/integration/agent/agent-install-io.d.ts +33 -0
  167. package/lib/types/src/host/integration/agent/agent-preflight.d.ts +56 -0
  168. package/lib/types/src/host/integration/agent/askpass-registry.d.ts +66 -0
  169. package/lib/types/src/host/integration/agent-install-io.d.ts +33 -0
  170. package/lib/types/src/host/integration/agent-preflight.d.ts +56 -0
  171. package/lib/types/src/host/integration/anchor-directory.d.ts +56 -0
  172. package/lib/types/src/host/integration/askpass-registry.d.ts +59 -0
  173. package/lib/types/src/host/integration/bin-root-resolver.d.ts +81 -0
  174. package/lib/types/src/host/integration/connection-test.d.ts +73 -0
  175. package/lib/types/src/host/integration/file-reference-provider.d.ts +115 -0
  176. package/lib/types/src/host/integration/fs/file-reference-provider.d.ts +142 -0
  177. package/lib/types/src/host/integration/fs/fs-provider.d.ts +368 -0
  178. package/lib/types/src/host/integration/fs/git-status-wiring.d.ts +21 -0
  179. package/lib/types/src/host/integration/fs/platform-errors.d.ts +94 -0
  180. package/lib/types/src/host/integration/fs/remote-file-tools.d.ts +369 -0
  181. package/lib/types/src/host/integration/fs/remote-index.d.ts +177 -0
  182. package/lib/types/src/host/integration/fs-provider.d.ts +226 -0
  183. package/lib/types/src/host/integration/lsp/lsp-framing.d.ts +38 -0
  184. package/lib/types/src/host/integration/lsp/lsp-registry.d.ts +67 -0
  185. package/lib/types/src/host/integration/lsp/lsp-session.d.ts +130 -0
  186. package/lib/types/src/host/integration/management-tools.d.ts +64 -0
  187. package/lib/types/src/host/integration/recycle-bin-service.d.ts +247 -0
  188. package/lib/types/src/host/integration/remote/connection-test.d.ts +73 -0
  189. package/lib/types/src/host/integration/remote/remote-face.d.ts +127 -0
  190. package/lib/types/src/host/integration/remote/remote-tool-backend.d.ts +108 -0
  191. package/lib/types/src/host/integration/remote-face.d.ts +127 -0
  192. package/lib/types/src/host/integration/remote-file-tools.d.ts +241 -0
  193. package/lib/types/src/host/integration/remote-home.d.ts +69 -0
  194. package/lib/types/src/host/integration/remote-index.d.ts +165 -0
  195. package/lib/types/src/host/integration/remote-job-tools.d.ts +295 -0
  196. package/lib/types/src/host/integration/remote-tool-backend.d.ts +97 -0
  197. package/lib/types/src/host/integration/remote-tools.d.ts +206 -0
  198. package/lib/types/src/host/integration/root-registry.d.ts +115 -0
  199. package/lib/types/src/host/integration/roots/anchor-directory.d.ts +56 -0
  200. package/lib/types/src/host/integration/roots/bin-root-resolver.d.ts +81 -0
  201. package/lib/types/src/host/integration/roots/remote-home.d.ts +69 -0
  202. package/lib/types/src/host/integration/roots/root-registry.d.ts +144 -0
  203. package/lib/types/src/host/integration/roots/roots-wiring.d.ts +219 -0
  204. package/lib/types/src/host/integration/roots/session-root-index.d.ts +52 -0
  205. package/lib/types/src/host/integration/roots-api.d.ts +407 -0
  206. package/lib/types/src/host/integration/roots-wiring.d.ts +211 -0
  207. package/lib/types/src/host/integration/session/session-api.d.ts +88 -0
  208. package/lib/types/src/host/integration/session/session-bin-lock.d.ts +80 -0
  209. package/lib/types/src/host/integration/session/session-header.d.ts +151 -0
  210. package/lib/types/src/host/integration/session/session-labels.d.ts +56 -0
  211. package/lib/types/src/host/integration/session/session-route-wiring.d.ts +50 -0
  212. package/lib/types/src/host/integration/session-admin-service.d.ts +236 -0
  213. package/lib/types/src/host/integration/session-api.d.ts +81 -0
  214. package/lib/types/src/host/integration/session-bin-lock.d.ts +80 -0
  215. package/lib/types/src/host/integration/session-header.d.ts +118 -0
  216. package/lib/types/src/host/integration/session-labels.d.ts +56 -0
  217. package/lib/types/src/host/integration/session-route-wiring.d.ts +50 -0
  218. package/lib/types/src/host/integration/session-scope.d.ts +113 -0
  219. package/lib/types/src/host/integration/session-tools.d.ts +44 -0
  220. package/lib/types/src/host/integration/strict-arguments.d.ts +79 -0
  221. package/lib/types/src/host/integration/takeover/takeover-api.d.ts +62 -0
  222. package/lib/types/src/host/integration/takeover/takeover-route-wiring.d.ts +60 -0
  223. package/lib/types/src/host/integration/takeover/takeover-service.d.ts +167 -0
  224. package/lib/types/src/host/integration/terminal-manifest.d.ts +94 -0
  225. package/lib/types/src/host/integration/tools/management-tools.d.ts +245 -0
  226. package/lib/types/src/host/integration/tools/remote-job-tools.d.ts +316 -0
  227. package/lib/types/src/host/integration/tools/remote-lsp-tools.d.ts +43 -0
  228. package/lib/types/src/host/integration/tools/remote-tools.d.ts +236 -0
  229. package/lib/types/src/host/integration/tools/session-target.d.ts +52 -0
  230. package/lib/types/src/host/integration/tools/session-tools.d.ts +46 -0
  231. package/lib/types/src/host/integration/tools/strict-arguments.d.ts +79 -0
  232. package/lib/types/src/host/integration/tools/terminal-manifest.d.ts +94 -0
  233. package/lib/types/src/host/integration/transfer/remote-copy.d.ts +66 -0
  234. package/lib/types/src/host/integration/transfer/transfer-adapters.d.ts +48 -0
  235. package/lib/types/src/host/integration/transfer/transfer-service.d.ts +212 -0
  236. package/lib/types/src/host/integration/transfer/transfer-tracker.d.ts +106 -0
  237. package/lib/types/src/host/integration/transfer-adapters.d.ts +44 -0
  238. package/lib/types/src/host/integration/transfer-service.d.ts +164 -0
  239. package/lib/types/src/host/integration/transfer-tracker.d.ts +106 -0
  240. package/lib/types/src/host/integration/user-roots.d.ts +461 -0
  241. package/lib/types/src/index.d.ts +66 -0
  242. package/lib/types/src/local-bridge-refs.d.ts +26 -0
  243. package/lib/types/src/local-bridge.d.ts +36 -0
  244. package/lib/types/src/shared/format.d.ts +13 -0
  245. package/lib/types/src/wiring/connections.d.ts +189 -0
  246. package/lib/types/src/wiring/fs.d.ts +13 -0
  247. package/lib/types/src/wiring/helpers.d.ts +144 -0
  248. package/lib/types/src/wiring/install-memo.d.ts +32 -0
  249. package/lib/types/src/wiring/legacy-dirs.d.ts +62 -0
  250. package/lib/types/src/wiring/package-paths.d.ts +35 -0
  251. package/lib/types/src/wiring/roots.d.ts +21 -0
  252. package/lib/types/src/wiring/state-dir.d.ts +29 -0
  253. package/lib/types/src/wiring/tools.d.ts +35 -0
  254. package/package.json +120 -4
@@ -0,0 +1,328 @@
1
+ /**
2
+ * The `ssh2` transport: a pure-JS SSH client.
3
+ *
4
+ * Why a second implementation:
5
+ *
6
+ * - **Password authentication** cannot be done by a non-interactive system `ssh` (it hardcodes
7
+ * `BatchMode`-style behaviour and would block on a prompt).
8
+ * - **Dynamic ProxyJump** chains (per-connection host/port/credentials) cannot be expressed
9
+ * through the system client's argv without writing a temporary ssh config.
10
+ * - **Connection reuse on Windows**: the host platform's OpenSSH does not support ControlMaster
11
+ * (measured: `getsockname failed: Not a socket`), so the only way to reuse one connection for
12
+ * many commands there is a client we own. See `doc/03-decisions/0006-windows-host-no-controlmaster.md`.
13
+ *
14
+ * Everything the plugin needs from the remote host is expressed with the *same* helpers as the
15
+ * alias transport — the same probe script, the same shell quoting, the same `size:mtimeSeconds`
16
+ * version token, the same failure classification — so behaviour does not change when a connection
17
+ * switches transports. File bytes travel over the SFTP subsystem (part of OpenSSH, still zero
18
+ * install) instead of base64 over the exec channel.
19
+ *
20
+ * The client itself is injected (`Ssh2Connector`), so this module never imports the `ssh2`
21
+ * package: unit tests run without it and without a network.
22
+ *
23
+ * @module dsh-over-ssh/infra/transport/ssh2-transport
24
+ */
25
+ import { type RemoteCapabilities } from '../../../domain/capability.ts';
26
+ import type { HostKeyPolicy } from '../../../domain/roots/connection.ts';
27
+ import { SshDevError } from '../../../domain/errors.ts';
28
+ import { type ExecRequest, type ExecResult, type ReadFileRangeRequest, type ReadFileRequest, type RemoteFileChunk, type RemoteWriteOutcome, type RemoveRequest, type RenameRequest, type Transport, type WriteFileRangeRequest, type WriteFileRequest } from '../transport.ts';
29
+ import { TransportBase } from '../transport-base.ts';
30
+ /** Authentication material for one hop. */
31
+ export interface Ssh2Auth {
32
+ readonly password?: string;
33
+ readonly privateKey?: string | Uint8Array;
34
+ readonly passphrase?: string;
35
+ /** ssh-agent socket path, or `pageant` on Windows. */
36
+ readonly agent?: string;
37
+ }
38
+ /** One host to connect to. */
39
+ export interface Ssh2Target {
40
+ readonly host: string;
41
+ readonly port?: number;
42
+ readonly username: string;
43
+ readonly auth: Ssh2Auth;
44
+ }
45
+ /** Everything needed to open a session. */
46
+ export interface Ssh2ConnectConfig {
47
+ /** Final target. */
48
+ readonly target: Ssh2Target;
49
+ /** Ordered jump hosts: hop 0 is reached directly, each next hop through the previous one. */
50
+ readonly jump?: readonly Ssh2Target[];
51
+ readonly hostKeyPolicy?: HostKeyPolicy;
52
+ /** Accepted host key fingerprints (`SHA256:…`), required when the policy is `strict`. */
53
+ readonly trustedHostKeys?: readonly string[];
54
+ readonly connectTimeoutMs?: number;
55
+ readonly keepAliveMs?: number;
56
+ /** Cancellation for the connect attempt. */
57
+ readonly signal?: AbortSignal;
58
+ }
59
+ /**
60
+ * An SFTP file handle.
61
+ *
62
+ * Opaque on purpose: `ssh2` hands back a raw `Buffer` handle that is only meaningful to
63
+ * `sftp.read` / `sftp.close` (verified against the real library — there is **no**
64
+ * `handle.read` method, which is exactly the mistake this shape prevents).
65
+ */
66
+ export type Ssh2FileHandle = unknown;
67
+ /** SFTP statistics as far as this transport needs them. */
68
+ export interface Ssh2Stats {
69
+ /** Base name of a `readdir` entry; absent on objects returned by `stat`. */
70
+ readonly filename?: string;
71
+ readonly size: number;
72
+ /**
73
+ * Modification time in **seconds** since the epoch.
74
+ *
75
+ * SFTP v3 attributes carry seconds, and `ssh2` passes them through unchanged (verified on a
76
+ * real host: `mtime = 1789217901`). The alias transport's `stat -c %Y` also reports seconds, so
77
+ * keeping the same unit is what makes version tokens comparable across transports.
78
+ */
79
+ readonly mtime: number;
80
+ /**
81
+ * SFTP file mode bits (`stats.mode & 0o170000`).
82
+ *
83
+ * Optional because a caller may hand over a `Stats`-shaped object without it, but the real
84
+ * library always sets it. It is the only reliable way to detect a **symbolic link** during a
85
+ * recursive removal: SFTP v3 `readdir` reports the link itself and never follows it.
86
+ */
87
+ readonly mode?: number;
88
+ /** `readdir` long name; starts with `l` for a symbolic link (`-` file, `d` directory). */
89
+ readonly longname?: string;
90
+ isDirectory?(): boolean;
91
+ isFile?(): boolean;
92
+ isSymbolicLink?(): boolean;
93
+ }
94
+ /**
95
+ * The narrow SFTP surface this transport uses, mirroring the real `ssh2` API:
96
+ * `open` yields a handle, and reads go through `read(handle, …)`, not through the handle.
97
+ *
98
+ * `readdir` / `rmdir` are optional for the same reason `ext_openssh_rename` is: a fake or a
99
+ * non-OpenSSH server may not provide them, and {@link Ssh2Transport.remove} must then report
100
+ * `REMOTE_UNSUPPORTED` instead of pretending a tree went away.
101
+ */
102
+ export interface Ssh2Sftp {
103
+ stat(path: string, callback: (error?: Error, stats?: Ssh2Stats) => void): void;
104
+ open(path: string, flags: string, callback: (error?: Error, handle?: Ssh2FileHandle) => void): void;
105
+ /** Read into a real `Buffer` (the library rejects plain `Uint8Array` views). */
106
+ read(handle: Ssh2FileHandle, buffer: Buffer, offset: number, length: number, position: number, callback: (error?: Error, bytesRead?: number) => void): void;
107
+ /**
108
+ * Positioned write into an open handle. `position` is the **file** offset; `offset` is the
109
+ * offset inside `buffer` (the same parameter order as the real library).
110
+ */
111
+ write(handle: Ssh2FileHandle, buffer: Buffer, offset: number, length: number, position: number, callback: (error?: Error) => void): void;
112
+ close(handle: Ssh2FileHandle, callback: (error?: Error) => void): void;
113
+ /** Statistics for an **open handle**, used to size a positioned write before truncating. */
114
+ fstat?(handle: Ssh2FileHandle, callback: (error?: Error, stats?: Ssh2Stats) => void): void;
115
+ /** Truncate an open handle to `length` bytes. */
116
+ ftruncate?(handle: Ssh2FileHandle, length: number, callback: (error?: Error) => void): void;
117
+ writeFile(path: string, data: Uint8Array, callback: (error?: Error) => void): void;
118
+ mkdir(path: string, attributes: {
119
+ readonly mode: number;
120
+ }, callback: (error?: Error) => void): void;
121
+ /** List a directory. The v3 protocol returns `.` and `..` too, so callers must skip them. */
122
+ readdir?(path: string, callback: (error?: Error, entries?: readonly Ssh2Stats[]) => void): void;
123
+ /** Remove an **empty** directory. */
124
+ rmdir?(path: string, callback: (error?: Error) => void): void;
125
+ /**
126
+ * Plain SFTP rename.
127
+ *
128
+ * Measured on a real OpenSSH server: this **fails with SSH_FX_FAILURE when the destination
129
+ * already exists**, so it cannot be the only path for an overwriting write.
130
+ */
131
+ rename(from: string, to: string, callback: (error?: Error) => void): void;
132
+ /**
133
+ * `posix-rename@openssh.com`: atomic overwrite. Present on OpenSSH servers/clients, absent
134
+ * elsewhere — which is why {@link writeFile} keeps a fallback ladder.
135
+ */
136
+ ext_openssh_rename?: ((from: string, to: string, callback: (error?: Error) => void) => void) | undefined;
137
+ unlink(path: string, callback: (error?: Error) => void): void;
138
+ end(): void;
139
+ }
140
+ /** A command channel as far as this transport needs it. */
141
+ export interface Ssh2Channel {
142
+ on(event: 'data', listener: (chunk: Buffer) => void): this;
143
+ on(event: 'close', listener: (code: number | null, signal: string | null) => void): this;
144
+ on(event: string, listener: (...args: never[]) => void): this;
145
+ stderr: {
146
+ on(event: 'data', listener: (chunk: Buffer) => void): unknown;
147
+ };
148
+ write(data: string | Uint8Array): boolean;
149
+ end(data?: string | Uint8Array): void;
150
+ close(): void;
151
+ }
152
+ /** An open SSH session: many channels, one connection. */
153
+ export interface Ssh2Session {
154
+ exec(command: string, options: {
155
+ readonly pty?: boolean;
156
+ }, callback: (error?: Error, channel?: Ssh2Channel) => void): void;
157
+ sftp(callback: (error?: Error, sftp?: Ssh2Sftp) => void): void;
158
+ /** Open a TCP channel through this session, used to chain jump hosts. */
159
+ forwardOut(sourceAddress: string, sourcePort: number, destinationAddress: string, destinationPort: number, callback: (error?: Error, stream?: unknown) => void): void;
160
+ end(): void;
161
+ destroy(): void;
162
+ /**
163
+ * Intermediate jump-host sessions this session rides on, so `dispose()` can close them too.
164
+ *
165
+ * **Deliberately per-session, not a module-level slot.** The first version of this fix kept them in
166
+ * a module-level `lastHops`, which is wrong as soon as one process has two connections
167
+ * (`TransportService` caches by `profile.id`, so multiple profiles are normal):
168
+ * - the second `connect` cleared the array, so the first connection's hops were **leaked**;
169
+ * - the first `dispose()` then took the *second* connection's hops and **ended a live connection
170
+ * another transport was still using**.
171
+ * Hanging them on the session they belong to makes ownership structural instead of positional.
172
+ * `ssh2`'s real `Client` has no such property, so it is optional and set only by
173
+ * {@link defaultConnector}.
174
+ */
175
+ readonly hops?: readonly Ssh2Session[];
176
+ }
177
+ /** Opens a session; injected so tests never load the real client. */
178
+ export type Ssh2Connector = (config: Ssh2ConnectConfig) => Promise<Ssh2Session>;
179
+ /** Construction options. */
180
+ export interface Ssh2TransportOptions {
181
+ readonly connectionId: string;
182
+ /** Session factory. Defaults to the real `ssh2` client (loaded lazily). */
183
+ readonly connector?: Ssh2Connector;
184
+ readonly connect?: Ssh2ConnectConfig;
185
+ readonly commandTimeoutMs?: number;
186
+ readonly now?: () => number;
187
+ }
188
+ /**
189
+ * Bytes of a command's **stderr** that are retained.
190
+ *
191
+ * `maxBytes` bounds stdout only, and stderr used to be accumulated without any bound at all: a command
192
+ * that writes diagnostics in a loop (`ssh -v`, a verbose build, a remote program that logs errors) grew
193
+ * this process's heap until the channel closed. Measured before the fix: a peer emitting 64 MiB of
194
+ * stderr retained **all 64 MiB** while stdout was correctly capped at 1 KiB (审计 0050).
195
+ *
196
+ * The **tail** is kept, not the head: every consumer of this string wants the last thing the command
197
+ * said. `domain/errors.ts` reduces it with `tail(stderr, 2000)` for classification, and a failing
198
+ * command's useful diagnostic (the final error, the `DSH_ERROR=` refusal) is at the end.
199
+ */
200
+ export declare const SSH2_STDERR_MAX_BYTES: number;
201
+ /**
202
+ * First line prepended to a truncated stderr capture.
203
+ *
204
+ * The truncation is announced **in the string itself** because `ExecResult.truncated` is stdout's flag
205
+ * (see its JSDoc) and must keep that meaning: `remote-backend` treats a truncated batch read as "these
206
+ * paths were not read", so setting it for a chatty command's stderr would report readable files as
207
+ * missing. Announcing it here keeps the diagnostic honest without overloading the flag.
208
+ */
209
+ export declare const STDERR_TRUNCATION_NOTICE = "\uFF08stderr \u8FC7\u957F\uFF0C\u4EC5\u4FDD\u7559\u672B\u5C3E\u7247\u6BB5\uFF09";
210
+ /**
211
+ * The pure-JS transport.
212
+ *
213
+ * One session serves every command and every file transfer, which is what gives Windows hosts
214
+ * the connection reuse their OpenSSH client cannot provide.
215
+ */
216
+ export declare class Ssh2Transport extends TransportBase implements Transport {
217
+ #private;
218
+ readonly id: string;
219
+ constructor(options: Ssh2TransportOptions);
220
+ get sessionCount(): number;
221
+ /**
222
+ * Open the session; concurrent calls share one attempt.
223
+ *
224
+ * The attempt owns its own controller and is aborted only when the **last** waiter gives up, so one
225
+ * caller's cancellation no longer rejects the others with `CANCELLED` (audit 2026-10-31 P1) while a lone
226
+ * cancelled dial still stops instead of running to `readyTimeout`.
227
+ */
228
+ connect(signal?: AbortSignal): Promise<void>;
229
+ /** Close the session. Idempotent. */
230
+ dispose(): Promise<void>;
231
+ /** Run one remote command over a channel of the shared session. */
232
+ exec(request: ExecRequest, signal?: AbortSignal): Promise<ExecResult>;
233
+ /**
234
+ * Read a remote file over SFTP.
235
+ *
236
+ * The version token is `size:mtimeSeconds` — the same shape the alias transport produces, so
237
+ * caches keyed on version tokens keep working when a connection changes transport.
238
+ */
239
+ readFile(request: ReadFileRequest, signal?: AbortSignal): Promise<RemoteFileChunk>;
240
+ /**
241
+ * Read `maxBytes` from `offset` over SFTP, seeking with a real `position`.
242
+ *
243
+ * Why the seek matters: `readFile` always starts at 0, so paging through a large file by
244
+ * reading `[0, offset+len)` and slicing transfers every earlier byte on every page. Here the
245
+ * server only sends the requested window, and only `maxBytes` is ever allocated locally.
246
+ */
247
+ readFileRange(request: ReadFileRangeRequest, signal?: AbortSignal): Promise<RemoteFileChunk>;
248
+ /**
249
+ * Write at `offset` over SFTP, leaving the bytes before it untouched.
250
+ *
251
+ * Flags are chosen by offset, and the semantics are deliberately narrow:
252
+ *
253
+ * - `offset === 0` uses `'w'`: the file is **created or truncated**, so a chunked upload that
254
+ * restarts from 0 overwrites a longer previous file instead of leaving a stale tail.
255
+ * - `offset > 0` uses `'r+'`: the file **must already exist** (there is no other way to preserve
256
+ * the earlier bytes), otherwise `REMOTE_NOT_FOUND`.
257
+ *
258
+ * Neither branch truncates: the bytes outside the written range are left alone (POSIX `pwrite`), the same
259
+ * meaning the shell transports give `dd conv=notrunc`. An earlier version of this comment claimed the file
260
+ * was truncated to `offset + bytes.byteLength` — it never was, and doing so would have made the same call
261
+ * mean two different things depending on which transport answered.
262
+ */
263
+ writeFileRange(request: WriteFileRangeRequest, signal?: AbortSignal): Promise<RemoteWriteOutcome>;
264
+ /**
265
+ * Move a remote path with a plain SFTP `rename`.
266
+ *
267
+ * A cross-device move makes the server fail (`SSH_FX_FAILURE` / `EXDEV`) and that failure is
268
+ * **propagated as-is**: copying would silently turn a move into a copy, which breaks the
269
+ * "move, then delete the source only after confirmation" semantics callers rely on.
270
+ */
271
+ rename(request: RenameRequest, signal?: AbortSignal): Promise<void>;
272
+ /**
273
+ * Remove a file, or a directory tree when `recursive` is set.
274
+ *
275
+ * A directory without `recursive` is an explicit error, never a silent success: a caller that
276
+ * hears "removed" must be able to trust that the path is gone.
277
+ *
278
+ * Symbolic links are never followed. SFTP v3 `readdir` reports a link as an entry with
279
+ * `mode & 0o170000 === 0o120000`; treating it as a directory would either recurse forever on a
280
+ * self-referential link or descend into a tree that was never requested for deletion.
281
+ */
282
+ remove(request: RemoveRequest, signal?: AbortSignal): Promise<void>;
283
+ /**
284
+ * Write a remote file over SFTP: temp file in the target directory, then a rename.
285
+ *
286
+ * The optional `expectedSha256` guard runs the same remote digest tool the alias transport
287
+ * uses, so a guarded write means the same thing on both transports.
288
+ */
289
+ writeFile(request: WriteFileRequest, signal?: AbortSignal): Promise<RemoteWriteOutcome>;
290
+ /**
291
+ * Probe the remote host once and remember the result.
292
+ *
293
+ * @param signal - cancellation.
294
+ * @param maxAgeMs - reuse a previous probe inside this window (default 5 minutes).
295
+ * @returns the discovered capabilities.
296
+ */
297
+ probeCapabilities(signal?: AbortSignal, maxAgeMs?: number): Promise<RemoteCapabilities>;
298
+ }
299
+ /**
300
+ * Tier 1 的版本令牌:`size:mtimeSeconds`。
301
+ *
302
+ * **不是「所有传输共用」**:Tier 2 的令牌由代理侧产生,形状以 `infra/transport/transport.ts` 的
303
+ * `RemoteStatResult.version` 注释为准(本文件不再复述那个形状——三处注释曾经互相矛盾,见审计
304
+ * 2026-10-31 P3;能验证的只有"两端从不互相比较"这一条,因为令牌只在产生它的那条连接里回传)。
305
+ *
306
+ * 已知限制:这里的 mtime 只有**秒**级(来自远端 `ls`/`stat` 的文本输出),同一秒内、大小不变的
307
+ * 两次写入无法区分。Tier 2 的令牌更细(代理侧能拿到更精确的时间);Tier 1 要补得改远端命令,
308
+ * 代价与收益不成比例,先如实记在这里而不是假装没有。
309
+ */
310
+ export declare function versionToken(size: number, mtimeSeconds: number): string;
311
+ /**
312
+ * Map an SFTP failure onto a plugin error code.
313
+ *
314
+ * A remote `permission denied` is **not** `SANDBOX_DENIED`: that code means "the plugin's own fence refused
315
+ * this write" (`doc/07-reference/error-codes.md`, and `fs-provider.ts` maps it to `FS_SANDBOX_DENIED`). A 0755
316
+ * directory or a foreign owner on the server is an ordinary remote failure, and reporting it as a fence refusal
317
+ * sends the caller looking for a different path instead of at permissions (audit 2026-10-31 P2).
318
+ */
319
+ export declare function sftpError(operation: string, path: string, error?: Error): SshDevError;
320
+ /**
321
+ * The real connector: wraps the `ssh2` package.
322
+ *
323
+ * Loaded lazily so this module (and its unit tests) never require the dependency, and required
324
+ * for dynamic jump chains: each hop is reached by forwarding a TCP channel through the previous
325
+ * session.
326
+ */
327
+ export declare const defaultConnector: Ssh2Connector;
328
+ //# sourceMappingURL=ssh2-transport.d.ts.map
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The one place that decides which `ssh` arguments a connection's credentials turn into.
3
+ *
4
+ * Two call sites need exactly the same answer — the alias transport (which builds master and per-command
5
+ * argv) and the Tier 2 agent launcher (which spawns `ssh <alias> <agent> --stdio`) — and the agent-path e2e
6
+ * caught both of them drifting: the transport ignored `keyPath`, then the launcher ignored `user`, so the
7
+ * ladder authenticated and the agent session did not. Duplicating this list is what allowed that.
8
+ *
9
+ * Honesty rule: a value is only emitted when the connection actually set it. Emitting `-l`/`-i`
10
+ * unconditionally would override the user's own `~/.ssh/config`, which is the alias transport's whole point.
11
+ *
12
+ * @module dsh-remote-workspace/infra/transport/ssh-argv
13
+ */
14
+ import type { HostKeyPolicy } from '../../domain/connection.ts';
15
+ /** Credential-ish options shared by every `ssh` invocation this plugin makes. */
16
+ export interface SshArgvOptions {
17
+ /** Destination: an `~/.ssh/config` alias or a bare `user@host`-less host. */
18
+ readonly alias: string;
19
+ /** Remote user; omitted or empty means "let ssh decide". */
20
+ readonly user?: string;
21
+ /** `-i` identity file. */
22
+ readonly identityFile?: string;
23
+ /** `-o UserKnownHostsFile=…`. */
24
+ readonly knownHostsFile?: string;
25
+ readonly hostKeyPolicy?: HostKeyPolicy;
26
+ readonly connectTimeoutMs?: number;
27
+ /** `-o BatchMode=yes` (never prompt). Defaults to true: this plugin never has a human at the tty. */
28
+ readonly batchMode?: boolean;
29
+ /** Extra `-o` options appended verbatim, in order. */
30
+ readonly extraOptions?: readonly string[];
31
+ }
32
+ /** `-i`, `-o UserKnownHostsFile`, `-l` — only what the connection configured. */
33
+ export declare function credentialArgv(options: SshArgvOptions): readonly string[];
34
+ /** Common `-o` options (policy, timeout, batch mode) plus {@link credentialArgv}. */
35
+ export declare function commonSshOptions(options: SshArgvOptions): readonly string[];
36
+ /**
37
+ * A complete `ssh` prefix ending at the destination: `ssh -T <options…> <alias>`.
38
+ *
39
+ * The Tier 2 launcher appends the remote command to this; the alias transport appends its own control
40
+ * sockets and the command.
41
+ */
42
+ export declare function sshPrefix(sshBinary: string, options: SshArgvOptions): readonly string[];
43
+ //# sourceMappingURL=ssh-argv.d.ts.map
@@ -0,0 +1,302 @@
1
+ /**
2
+ * The `ssh2` transport: a pure-JS SSH client.
3
+ *
4
+ * Why a second implementation:
5
+ *
6
+ * - **Password authentication** cannot be done by a non-interactive system `ssh` (it hardcodes
7
+ * `BatchMode`-style behaviour and would block on a prompt).
8
+ * - **Dynamic ProxyJump** chains (per-connection host/port/credentials) cannot be expressed
9
+ * through the system client's argv without writing a temporary ssh config.
10
+ * - **Connection reuse on Windows**: the host platform's OpenSSH does not support ControlMaster
11
+ * (measured: `getsockname failed: Not a socket`), so the only way to reuse one connection for
12
+ * many commands there is a client we own. See `doc/03-decisions/0006-windows-host-no-controlmaster.md`.
13
+ *
14
+ * Everything the plugin needs from the remote host is expressed with the *same* helpers as the
15
+ * alias transport — the same probe script, the same shell quoting, the same `size:mtimeSeconds`
16
+ * version token, the same failure classification — so behaviour does not change when a connection
17
+ * switches transports. File bytes travel over the SFTP subsystem (part of OpenSSH, still zero
18
+ * install) instead of base64 over the exec channel.
19
+ *
20
+ * The client itself is injected (`Ssh2Connector`), so this module never imports the `ssh2`
21
+ * package: unit tests run without it and without a network.
22
+ *
23
+ * @module dsh-remote-workspace/infra/transport/ssh2-transport
24
+ */
25
+ import { type RemoteCapabilities } from '../../domain/capability.ts';
26
+ import type { HostKeyPolicy } from '../../domain/connection.ts';
27
+ import { SshDevError } from '../../domain/errors.ts';
28
+ import { type ExecRequest, type ExecResult, type ReadFileRangeRequest, type ReadFileRequest, type RemoteFileChunk, type RemoteWriteOutcome, type RemoveRequest, type RenameRequest, type Transport, type WriteFileRangeRequest, type WriteFileRequest } from './transport.ts';
29
+ import { TransportBase } from './transport-base.ts';
30
+ /** Authentication material for one hop. */
31
+ export interface Ssh2Auth {
32
+ readonly password?: string;
33
+ readonly privateKey?: string | Uint8Array;
34
+ readonly passphrase?: string;
35
+ /** ssh-agent socket path, or `pageant` on Windows. */
36
+ readonly agent?: string;
37
+ }
38
+ /** One host to connect to. */
39
+ export interface Ssh2Target {
40
+ readonly host: string;
41
+ readonly port?: number;
42
+ readonly username: string;
43
+ readonly auth: Ssh2Auth;
44
+ }
45
+ /** Everything needed to open a session. */
46
+ export interface Ssh2ConnectConfig {
47
+ /** Final target. */
48
+ readonly target: Ssh2Target;
49
+ /** Ordered jump hosts: hop 0 is reached directly, each next hop through the previous one. */
50
+ readonly jump?: readonly Ssh2Target[];
51
+ readonly hostKeyPolicy?: HostKeyPolicy;
52
+ /** Accepted host key fingerprints (`SHA256:…`), required when the policy is `strict`. */
53
+ readonly trustedHostKeys?: readonly string[];
54
+ readonly connectTimeoutMs?: number;
55
+ readonly keepAliveMs?: number;
56
+ /** Cancellation for the connect attempt. */
57
+ readonly signal?: AbortSignal;
58
+ }
59
+ /**
60
+ * An SFTP file handle.
61
+ *
62
+ * Opaque on purpose: `ssh2` hands back a raw `Buffer` handle that is only meaningful to
63
+ * `sftp.read` / `sftp.close` (verified against the real library — there is **no**
64
+ * `handle.read` method, which is exactly the mistake this shape prevents).
65
+ */
66
+ export type Ssh2FileHandle = unknown;
67
+ /** SFTP statistics as far as this transport needs them. */
68
+ export interface Ssh2Stats {
69
+ /** Base name of a `readdir` entry; absent on objects returned by `stat`. */
70
+ readonly filename?: string;
71
+ readonly size: number;
72
+ /**
73
+ * Modification time in **seconds** since the epoch.
74
+ *
75
+ * SFTP v3 attributes carry seconds, and `ssh2` passes them through unchanged (verified on a
76
+ * real host: `mtime = 1789217901`). The alias transport's `stat -c %Y` also reports seconds, so
77
+ * keeping the same unit is what makes version tokens comparable across transports.
78
+ */
79
+ readonly mtime: number;
80
+ /**
81
+ * SFTP file mode bits (`stats.mode & 0o170000`).
82
+ *
83
+ * Optional because a caller may hand over a `Stats`-shaped object without it, but the real
84
+ * library always sets it. It is the only reliable way to detect a **symbolic link** during a
85
+ * recursive removal: SFTP v3 `readdir` reports the link itself and never follows it.
86
+ */
87
+ readonly mode?: number;
88
+ /** `readdir` long name; starts with `l` for a symbolic link (`-` file, `d` directory). */
89
+ readonly longname?: string;
90
+ isDirectory?(): boolean;
91
+ isFile?(): boolean;
92
+ isSymbolicLink?(): boolean;
93
+ }
94
+ /**
95
+ * The narrow SFTP surface this transport uses, mirroring the real `ssh2` API:
96
+ * `open` yields a handle, and reads go through `read(handle, …)`, not through the handle.
97
+ *
98
+ * `readdir` / `rmdir` are optional for the same reason `ext_openssh_rename` is: a fake or a
99
+ * non-OpenSSH server may not provide them, and {@link Ssh2Transport.remove} must then report
100
+ * `REMOTE_UNSUPPORTED` instead of pretending a tree went away.
101
+ */
102
+ export interface Ssh2Sftp {
103
+ stat(path: string, callback: (error?: Error, stats?: Ssh2Stats) => void): void;
104
+ open(path: string, flags: string, callback: (error?: Error, handle?: Ssh2FileHandle) => void): void;
105
+ /** Read into a real `Buffer` (the library rejects plain `Uint8Array` views). */
106
+ read(handle: Ssh2FileHandle, buffer: Buffer, offset: number, length: number, position: number, callback: (error?: Error, bytesRead?: number) => void): void;
107
+ /**
108
+ * Positioned write into an open handle. `position` is the **file** offset; `offset` is the
109
+ * offset inside `buffer` (the same parameter order as the real library).
110
+ */
111
+ write(handle: Ssh2FileHandle, buffer: Buffer, offset: number, length: number, position: number, callback: (error?: Error) => void): void;
112
+ close(handle: Ssh2FileHandle, callback: (error?: Error) => void): void;
113
+ /** Statistics for an **open handle**, used to size a positioned write before truncating. */
114
+ fstat?(handle: Ssh2FileHandle, callback: (error?: Error, stats?: Ssh2Stats) => void): void;
115
+ /** Truncate an open handle to `length` bytes. */
116
+ ftruncate?(handle: Ssh2FileHandle, length: number, callback: (error?: Error) => void): void;
117
+ writeFile(path: string, data: Uint8Array, callback: (error?: Error) => void): void;
118
+ mkdir(path: string, attributes: {
119
+ readonly mode: number;
120
+ }, callback: (error?: Error) => void): void;
121
+ /** List a directory. The v3 protocol returns `.` and `..` too, so callers must skip them. */
122
+ readdir?(path: string, callback: (error?: Error, entries?: readonly Ssh2Stats[]) => void): void;
123
+ /** Remove an **empty** directory. */
124
+ rmdir?(path: string, callback: (error?: Error) => void): void;
125
+ /**
126
+ * Plain SFTP rename.
127
+ *
128
+ * Measured on a real OpenSSH server: this **fails with SSH_FX_FAILURE when the destination
129
+ * already exists**, so it cannot be the only path for an overwriting write.
130
+ */
131
+ rename(from: string, to: string, callback: (error?: Error) => void): void;
132
+ /**
133
+ * `posix-rename@openssh.com`: atomic overwrite. Present on OpenSSH servers/clients, absent
134
+ * elsewhere — which is why {@link writeFile} keeps a fallback ladder.
135
+ */
136
+ ext_openssh_rename?: ((from: string, to: string, callback: (error?: Error) => void) => void) | undefined;
137
+ unlink(path: string, callback: (error?: Error) => void): void;
138
+ end(): void;
139
+ }
140
+ /** A command channel as far as this transport needs it. */
141
+ export interface Ssh2Channel {
142
+ on(event: 'data', listener: (chunk: Buffer) => void): this;
143
+ on(event: 'close', listener: (code: number | null, signal: string | null) => void): this;
144
+ on(event: string, listener: (...args: never[]) => void): this;
145
+ stderr: {
146
+ on(event: 'data', listener: (chunk: Buffer) => void): unknown;
147
+ };
148
+ write(data: string | Uint8Array): boolean;
149
+ end(data?: string | Uint8Array): void;
150
+ close(): void;
151
+ }
152
+ /** An open SSH session: many channels, one connection. */
153
+ export interface Ssh2Session {
154
+ exec(command: string, options: {
155
+ readonly pty?: boolean;
156
+ }, callback: (error?: Error, channel?: Ssh2Channel) => void): void;
157
+ sftp(callback: (error?: Error, sftp?: Ssh2Sftp) => void): void;
158
+ /** Open a TCP channel through this session, used to chain jump hosts. */
159
+ forwardOut(sourceAddress: string, sourcePort: number, destinationAddress: string, destinationPort: number, callback: (error?: Error, stream?: unknown) => void): void;
160
+ end(): void;
161
+ destroy(): void;
162
+ /**
163
+ * Intermediate jump-host sessions this session rides on, so `dispose()` can close them too.
164
+ *
165
+ * **Deliberately per-session, not a module-level slot.** The first version of this fix kept them in
166
+ * a module-level `lastHops`, which is wrong as soon as one process has two connections
167
+ * (`TransportService` caches by `profile.id`, so multiple profiles are normal):
168
+ * - the second `connect` cleared the array, so the first connection's hops were **leaked**;
169
+ * - the first `dispose()` then took the *second* connection's hops and **ended a live connection
170
+ * another transport was still using**.
171
+ * Hanging them on the session they belong to makes ownership structural instead of positional.
172
+ * `ssh2`'s real `Client` has no such property, so it is optional and set only by
173
+ * {@link defaultConnector}.
174
+ */
175
+ readonly hops?: readonly Ssh2Session[];
176
+ }
177
+ /** Opens a session; injected so tests never load the real client. */
178
+ export type Ssh2Connector = (config: Ssh2ConnectConfig) => Promise<Ssh2Session>;
179
+ /** Construction options. */
180
+ export interface Ssh2TransportOptions {
181
+ readonly connectionId: string;
182
+ /** Session factory. Defaults to the real `ssh2` client (loaded lazily). */
183
+ readonly connector?: Ssh2Connector;
184
+ readonly connect?: Ssh2ConnectConfig;
185
+ readonly commandTimeoutMs?: number;
186
+ readonly now?: () => number;
187
+ }
188
+ /**
189
+ * Bytes of a command's **stderr** that are retained.
190
+ *
191
+ * `maxBytes` bounds stdout only, and stderr used to be accumulated without any bound at all: a command
192
+ * that writes diagnostics in a loop (`ssh -v`, a verbose build, a remote program that logs errors) grew
193
+ * this process's heap until the channel closed. Measured before the fix: a peer emitting 64 MiB of
194
+ * stderr retained **all 64 MiB** while stdout was correctly capped at 1 KiB (审计 0050).
195
+ *
196
+ * The **tail** is kept, not the head: every consumer of this string wants the last thing the command
197
+ * said. `domain/errors.ts` reduces it with `tail(stderr, 2000)` for classification, and a failing
198
+ * command's useful diagnostic (the final error, the `DSH_ERROR=` refusal) is at the end.
199
+ */
200
+ export declare const SSH2_STDERR_MAX_BYTES: number;
201
+ /**
202
+ * First line prepended to a truncated stderr capture.
203
+ *
204
+ * The truncation is announced **in the string itself** because `ExecResult.truncated` is stdout's flag
205
+ * (see its JSDoc) and must keep that meaning: `remote-backend` treats a truncated batch read as "these
206
+ * paths were not read", so setting it for a chatty command's stderr would report readable files as
207
+ * missing. Announcing it here keeps the diagnostic honest without overloading the flag.
208
+ */
209
+ export declare const STDERR_TRUNCATION_NOTICE = "\uFF08stderr \u8FC7\u957F\uFF0C\u4EC5\u4FDD\u7559\u672B\u5C3E\u7247\u6BB5\uFF09";
210
+ /**
211
+ * The pure-JS transport.
212
+ *
213
+ * One session serves every command and every file transfer, which is what gives Windows hosts
214
+ * the connection reuse their OpenSSH client cannot provide.
215
+ */
216
+ export declare class Ssh2Transport extends TransportBase implements Transport {
217
+ #private;
218
+ readonly id: string;
219
+ constructor(options: Ssh2TransportOptions);
220
+ get sessionCount(): number;
221
+ /** Open the session; concurrent calls share one attempt. */
222
+ connect(signal?: AbortSignal): Promise<void>;
223
+ /** Close the session. Idempotent. */
224
+ dispose(): Promise<void>;
225
+ /** Run one remote command over a channel of the shared session. */
226
+ exec(request: ExecRequest, signal?: AbortSignal): Promise<ExecResult>;
227
+ /**
228
+ * Read a remote file over SFTP.
229
+ *
230
+ * The version token is `size:mtimeSeconds` — the same shape the alias transport produces, so
231
+ * caches keyed on version tokens keep working when a connection changes transport.
232
+ */
233
+ readFile(request: ReadFileRequest, signal?: AbortSignal): Promise<RemoteFileChunk>;
234
+ /**
235
+ * Read `maxBytes` from `offset` over SFTP, seeking with a real `position`.
236
+ *
237
+ * Why the seek matters: `readFile` always starts at 0, so paging through a large file by
238
+ * reading `[0, offset+len)` and slicing transfers every earlier byte on every page. Here the
239
+ * server only sends the requested window, and only `maxBytes` is ever allocated locally.
240
+ */
241
+ readFileRange(request: ReadFileRangeRequest, signal?: AbortSignal): Promise<RemoteFileChunk>;
242
+ /**
243
+ * Write at `offset` over SFTP, leaving the bytes before it untouched.
244
+ *
245
+ * Flags are chosen by offset, and the semantics are deliberately narrow:
246
+ *
247
+ * - `offset === 0` uses `'w'`: the file is **created or truncated**, so a chunked upload that
248
+ * restarts from 0 overwrites a longer previous file instead of leaving a stale tail.
249
+ * - `offset > 0` uses `'r+'`: the file **must already exist** (there is no other way to preserve
250
+ * the earlier bytes), otherwise `REMOTE_NOT_FOUND`. After the positioned write the file is
251
+ * truncated to `offset + bytes.byteLength` — a shorter final chunk must shorten the file, or
252
+ * the tail of a previous, longer upload would survive and corrupt the result.
253
+ */
254
+ writeFileRange(request: WriteFileRangeRequest, signal?: AbortSignal): Promise<RemoteWriteOutcome>;
255
+ /**
256
+ * Move a remote path with a plain SFTP `rename`.
257
+ *
258
+ * A cross-device move makes the server fail (`SSH_FX_FAILURE` / `EXDEV`) and that failure is
259
+ * **propagated as-is**: copying would silently turn a move into a copy, which breaks the
260
+ * "move, then delete the source only after confirmation" semantics callers rely on.
261
+ */
262
+ rename(request: RenameRequest, signal?: AbortSignal): Promise<void>;
263
+ /**
264
+ * Remove a file, or a directory tree when `recursive` is set.
265
+ *
266
+ * A directory without `recursive` is an explicit error, never a silent success: a caller that
267
+ * hears "removed" must be able to trust that the path is gone.
268
+ *
269
+ * Symbolic links are never followed. SFTP v3 `readdir` reports a link as an entry with
270
+ * `mode & 0o170000 === 0o120000`; treating it as a directory would either recurse forever on a
271
+ * self-referential link or descend into a tree that was never requested for deletion.
272
+ */
273
+ remove(request: RemoveRequest, signal?: AbortSignal): Promise<void>;
274
+ /**
275
+ * Write a remote file over SFTP: temp file in the target directory, then a rename.
276
+ *
277
+ * The optional `expectedSha256` guard runs the same remote digest tool the alias transport
278
+ * uses, so a guarded write means the same thing on both transports.
279
+ */
280
+ writeFile(request: WriteFileRequest, signal?: AbortSignal): Promise<RemoteWriteOutcome>;
281
+ /**
282
+ * Probe the remote host once and remember the result.
283
+ *
284
+ * @param signal - cancellation.
285
+ * @param maxAgeMs - reuse a previous probe inside this window (default 5 minutes).
286
+ * @returns the discovered capabilities.
287
+ */
288
+ probeCapabilities(signal?: AbortSignal, maxAgeMs?: number): Promise<RemoteCapabilities>;
289
+ }
290
+ /** Version token shared by every transport: `size:mtimeSeconds`. */
291
+ export declare function versionToken(size: number, mtimeSeconds: number): string;
292
+ /** Map an SFTP failure onto a plugin error code. */
293
+ export declare function sftpError(operation: string, path: string, error?: Error): SshDevError;
294
+ /**
295
+ * The real connector: wraps the `ssh2` package.
296
+ *
297
+ * Loaded lazily so this module (and its unit tests) never require the dependency, and required
298
+ * for dynamic jump chains: each hop is reached by forwarding a TCP channel through the previous
299
+ * session.
300
+ */
301
+ export declare const defaultConnector: Ssh2Connector;
302
+ //# sourceMappingURL=ssh2-transport.d.ts.map