@tiphareth/dsh-hardssh 0.1.2 → 0.2.2

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 (174) hide show
  1. package/LICENSE +30 -0
  2. package/README.md +49 -25
  3. package/SKILLS.md +140 -0
  4. package/lib/base/index.js +95 -0
  5. package/lib/client.js +1171 -1321
  6. package/lib/fs.js +307 -416
  7. package/lib/index.js +5428 -2991
  8. package/lib/ledger-B-LXlftp.js +43 -0
  9. package/lib/ledger-D2ezq1iW.js +376 -0
  10. package/lib/model-Cp7f70Mb.js +5 -0
  11. package/lib/registry-CViuzKYI.js +421 -0
  12. package/lib/subprocess.js +92 -549
  13. package/lib/types/backend.d.ts +73 -78
  14. package/lib/types/base/capability.d.ts +4 -0
  15. package/lib/types/base/ledger-router.d.ts +42 -26
  16. package/lib/types/base/ledger.d.ts +30 -19
  17. package/lib/types/base/model.d.ts +17 -8
  18. package/lib/types/base/plugin.d.ts +11 -0
  19. package/lib/types/base/registry.d.ts +28 -31
  20. package/lib/types/base/router.d.ts +0 -7
  21. package/lib/types/client/api.d.ts +8 -8
  22. package/lib/types/client/connect-host.d.ts +13 -0
  23. package/lib/types/client/index.d.ts +6 -4
  24. package/lib/types/client/locales.d.ts +15 -1
  25. package/lib/types/client/session-connect-gate.d.ts +71 -0
  26. package/lib/types/client/ssh/api.d.ts +11 -9
  27. package/lib/types/client/ssh/apply.d.ts +14 -8
  28. package/lib/types/client/ssh/locales.d.ts +8 -1
  29. package/lib/types/client/ssh/ops-tab.d.ts +34 -0
  30. package/lib/types/client/ssh/panel/ClusterTab.d.ts +3 -1
  31. package/lib/types/client/ssh/panel/ConnectionErrorDialog.d.ts +8 -0
  32. package/lib/types/client/ssh/panel/SessionSecretDialog.d.ts +4 -1
  33. package/lib/types/client/ssh/panel/SshPanel.d.ts +8 -8
  34. package/lib/types/client/ssh/panel/TerminalTab.d.ts +3 -5
  35. package/lib/types/client/ssh/panel/TransferTab.d.ts +5 -1
  36. package/lib/types/client/ssh/panel/TunnelsTab.d.ts +3 -1
  37. package/lib/types/client/ssh/session-target.d.ts +17 -0
  38. package/lib/types/client/state.d.ts +4 -1
  39. package/lib/types/client/workspace-panel-entry.d.ts +30 -0
  40. package/lib/types/client/workspace-panel.d.ts +24 -0
  41. package/lib/types/client-http.d.ts +77 -6
  42. package/lib/types/core.d.ts +5 -16
  43. package/lib/types/fs.d.ts +18 -25
  44. package/lib/types/index.d.ts +138 -9
  45. package/lib/types/ledger.d.ts +16 -111
  46. package/lib/types/protocol.d.ts +0 -5
  47. package/lib/types/providers/index.d.ts +6 -3
  48. package/lib/types/providers/local/provider.d.ts +83 -23
  49. package/lib/types/providers/ssh/provider.d.ts +24 -57
  50. package/lib/types/remote/environment.d.ts +2 -0
  51. package/lib/types/remote/remote-fs.d.ts +12 -1
  52. package/lib/types/remote/remote-process.d.ts +22 -1
  53. package/lib/types/remote/remote-subprocess.d.ts +13 -1
  54. package/lib/types/remote/remote-terminal.d.ts +4 -0
  55. package/lib/types/remote-search.d.ts +3 -3
  56. package/lib/types/routes.d.ts +36 -8
  57. package/lib/types/runtime/dsh-capabilities.d.ts +18 -0
  58. package/lib/types/runtime/workspace-core.d.ts +73 -38
  59. package/lib/types/runtime/workspace-migration.d.ts +113 -0
  60. package/lib/types/ssh/connection/lease.d.ts +23 -0
  61. package/lib/types/ssh/connection/manager.d.ts +359 -0
  62. package/lib/types/ssh/connection/pool.d.ts +8 -1
  63. package/lib/types/ssh/engine.d.ts +97 -267
  64. package/lib/types/ssh/known-hosts.d.ts +22 -5
  65. package/lib/types/ssh/local-transfer-policy.d.ts +20 -0
  66. package/lib/types/ssh/plugin.d.ts +10 -15
  67. package/lib/types/ssh/protocol.d.ts +18 -0
  68. package/lib/types/ssh/routes.d.ts +15 -7
  69. package/lib/types/ssh/sftp/service.d.ts +216 -0
  70. package/lib/types/ssh/store.d.ts +20 -8
  71. package/lib/types/ssh/terminal/service.d.ts +49 -0
  72. package/lib/types/ssh/tunnel/service.d.ts +26 -0
  73. package/lib/types/ssh/vault.d.ts +51 -4
  74. package/lib/types/subprocess.d.ts +13 -21
  75. package/lib/types/switch/switch-fs.d.ts +52 -2
  76. package/lib/types/switch/switch-subprocess.d.ts +32 -3
  77. package/lib/types/tools.d.ts +7 -6
  78. package/lib/types/workspace-tool-ops.d.ts +33 -0
  79. package/lib/types/workspace.d.ts +18 -0
  80. package/lib/vault-3gpWct2Q.js +559 -0
  81. package/lib/workspace.js +2 -0
  82. package/package.json +58 -25
  83. package/scripts/export-legacy-workspaces.mjs +136 -0
  84. package/src/backend.ts +155 -483
  85. package/src/base/capability.ts +4 -0
  86. package/src/base/ledger-router.ts +214 -65
  87. package/src/base/ledger.ts +175 -48
  88. package/src/base/model.ts +18 -8
  89. package/src/base/plugin.ts +15 -4
  90. package/src/base/registry.ts +49 -41
  91. package/src/base/router.ts +0 -8
  92. package/src/client/api.ts +8 -36
  93. package/src/client/connect-host.ts +199 -0
  94. package/src/client/directory-flow.tsx +91 -36
  95. package/src/client/index.ts +56 -53
  96. package/src/client/locales.ts +14 -0
  97. package/src/client/session-connect-gate.ts +163 -0
  98. package/src/client/ssh/api.ts +245 -108
  99. package/src/client/ssh/apply.ts +23 -24
  100. package/src/client/ssh/locales.ts +16 -2
  101. package/src/client/ssh/ops-tab.tsx +79 -0
  102. package/src/client/ssh/panel/ClusterTab.tsx +9 -20
  103. package/src/client/ssh/panel/ConnectionErrorDialog.tsx +33 -0
  104. package/src/client/ssh/panel/SessionSecretDialog.tsx +5 -1
  105. package/src/client/ssh/panel/SshPanel.tsx +81 -77
  106. package/src/client/ssh/panel/TerminalTab.tsx +18 -37
  107. package/src/client/ssh/panel/TransferTab.tsx +43 -33
  108. package/src/client/ssh/panel/TunnelsTab.tsx +9 -26
  109. package/src/client/ssh/panel/panel.module.css +90 -127
  110. package/src/client/ssh/session-target.ts +69 -0
  111. package/src/client/state.ts +9 -1
  112. package/src/client/workspace-badges.ts +99 -96
  113. package/src/client/workspace-panel-entry.tsx +91 -0
  114. package/src/client/workspace-panel.tsx +227 -0
  115. package/src/client/workspace.module.css +76 -2
  116. package/src/client-http.ts +118 -18
  117. package/src/core.ts +37 -47
  118. package/src/fs.ts +137 -85
  119. package/src/index.ts +535 -244
  120. package/src/ledger.ts +62 -416
  121. package/src/protocol.ts +0 -6
  122. package/src/providers/index.ts +8 -4
  123. package/src/providers/local/provider.ts +259 -87
  124. package/src/providers/ssh/provider.ts +158 -155
  125. package/src/remote/environment.ts +22 -1
  126. package/src/remote/remote-fs.ts +123 -34
  127. package/src/remote/remote-process.ts +265 -203
  128. package/src/remote/remote-subprocess.ts +61 -18
  129. package/src/remote/remote-terminal.ts +106 -21
  130. package/src/remote-search.ts +122 -26
  131. package/src/routes.ts +416 -395
  132. package/src/runtime/dsh-capabilities.ts +19 -0
  133. package/src/runtime/workspace-core.ts +171 -88
  134. package/src/runtime/workspace-migration.ts +472 -0
  135. package/src/ssh/connection/lease.ts +35 -0
  136. package/src/ssh/connection/manager.ts +1083 -0
  137. package/src/ssh/connection/pool.ts +341 -275
  138. package/src/ssh/engine.ts +269 -1477
  139. package/src/ssh/known-hosts.ts +42 -18
  140. package/src/ssh/local-transfer-policy.ts +83 -0
  141. package/src/ssh/plugin.ts +20 -19
  142. package/src/ssh/protocol.ts +22 -1
  143. package/src/ssh/routes.ts +419 -187
  144. package/src/ssh/sftp/service.ts +967 -0
  145. package/src/ssh/store.ts +169 -72
  146. package/src/ssh/terminal/service.ts +177 -0
  147. package/src/ssh/tools.ts +42 -20
  148. package/src/ssh/tunnel/service.ts +217 -0
  149. package/src/ssh/vault.ts +245 -91
  150. package/src/subprocess.ts +88 -71
  151. package/src/switch/switch-fs.ts +141 -10
  152. package/src/switch/switch-subprocess.ts +80 -6
  153. package/src/tools.ts +217 -221
  154. package/src/workspace-tool-ops.ts +101 -0
  155. package/src/workspace.ts +52 -0
  156. package/lib/environment-BL1jddfB.js +0 -449
  157. package/lib/switch-fs-CAJpFY9C.js +0 -193
  158. package/lib/switch-fs-RrZtG2gv.js +0 -210
  159. package/lib/types/client/manager-button.d.ts +0 -32
  160. package/lib/types/client/ssh/mount.d.ts +0 -13
  161. package/lib/types/client/ssh/panel/HostsTab.d.ts +0 -10
  162. package/lib/types/client/ssh/panel/controller.d.ts +0 -23
  163. package/lib/types/client/ssh/sidebar-entry.d.ts +0 -25
  164. package/lib/types/client/workspace-gate.d.ts +0 -15
  165. package/lib/types/remote-runner.d.ts +0 -83
  166. package/lib/types/seam-state.d.ts +0 -69
  167. package/src/client/manager-button.tsx +0 -269
  168. package/src/client/ssh/mount.tsx +0 -83
  169. package/src/client/ssh/panel/HostsTab.tsx +0 -236
  170. package/src/client/ssh/panel/controller.ts +0 -48
  171. package/src/client/ssh/sidebar-entry.ts +0 -123
  172. package/src/client/workspace-gate.ts +0 -225
  173. package/src/remote-runner.ts +0 -201
  174. package/src/seam-state.ts +0 -185
@@ -0,0 +1,217 @@
1
+ import { createServer, type Server as NetServer } from 'node:net'
2
+ import type { Client } from 'ssh2'
3
+ import type { ClientLease } from '../connection/lease.ts'
4
+ import type { SshConnectionService } from '../connection/pool.ts'
5
+ import type { SshHostEntry, TunnelInfo } from '../protocol.ts'
6
+
7
+ const TUNNEL_FORWARD_TIMEOUT_MS = 10_000
8
+
9
+ /** Narrow connection dependency used by the tunnel component. */
10
+ export interface TunnelConnectionAccess {
11
+ readonly connections: SshConnectionService
12
+ findEntry(alias: string): SshHostEntry | undefined
13
+ }
14
+
15
+ interface TunnelRecord {
16
+ info: TunnelInfo
17
+ server: NetServer
18
+ alias: string
19
+ lease: ClientLease
20
+ sockets: Set<import('node:net').Socket>
21
+ clientFailureHandler: (error?: unknown) => void
22
+ }
23
+
24
+ /** Owns local listeners, forwarded sockets, and tunnel leases. */
25
+ export class TunnelService {
26
+ private readonly tunnels = new Map<string, TunnelRecord>()
27
+ private nextTunnelId = 1
28
+
29
+ constructor(private readonly connection: TunnelConnectionAccess) {}
30
+
31
+ private removeClientFailureListener(tunnel: TunnelRecord): void {
32
+ const client = tunnel.lease.client
33
+ client.removeListener('error', tunnel.clientFailureHandler)
34
+ client.removeListener('close', tunnel.clientFailureHandler)
35
+ }
36
+
37
+ private markFailedForClient(client: Client, error?: unknown): void {
38
+ for (const tunnel of this.tunnels.values()) {
39
+ if (tunnel.lease.client !== client) continue
40
+ tunnel.info.state = 'failed'
41
+ this.removeClientFailureListener(tunnel)
42
+ try { tunnel.server.close() } catch { /* never listened or already closed */ }
43
+ for (const socket of tunnel.sockets) {
44
+ try { socket.destroy() } catch { /* peer already gone */ }
45
+ }
46
+ tunnel.sockets.clear()
47
+ tunnel.lease.markBroken(error)
48
+ tunnel.lease.release()
49
+ }
50
+ }
51
+
52
+ async startTunnel(alias: string, options: { remotePort: number; remoteHost?: string; localPort?: number }): Promise<TunnelInfo> {
53
+ if (!Number.isInteger(options.remotePort) || options.remotePort < 1 || options.remotePort > 65535) {
54
+ throw new Error('remotePort must be an integer in 1..65535')
55
+ }
56
+ if (options.localPort !== undefined && (!Number.isInteger(options.localPort) || options.localPort < 1 || options.localPort > 65535)) {
57
+ throw new Error('localPort must be an integer in 1..65535')
58
+ }
59
+ if (this.connection.findEntry(alias) === undefined) throw new Error(`alias '${alias}' not found —add it first`)
60
+
61
+ const remoteHost = options.remoteHost ?? '127.0.0.1'
62
+ const id = `tun-${this.nextTunnelId++}`
63
+ const info: TunnelInfo = {
64
+ id,
65
+ alias,
66
+ localPort: 0,
67
+ remoteHost,
68
+ remotePort: options.remotePort,
69
+ state: 'connecting',
70
+ startedAt: Date.now(),
71
+ }
72
+ const lease = await this.connection.connections.acquire(alias, { kind: 'tunnel' })
73
+ const client = lease.client
74
+ const sockets = new Set<import('node:net').Socket>()
75
+
76
+ const server = createServer((socket) => {
77
+ sockets.add(socket)
78
+ let forwardFinished = false
79
+ let forwardTimer: NodeJS.Timeout | undefined
80
+
81
+ const abandonForward = (): void => {
82
+ if (!forwardFinished) {
83
+ forwardFinished = true
84
+ if (forwardTimer !== undefined) clearTimeout(forwardTimer)
85
+ }
86
+ // HSSH-09: normal short connections must not remain retained.
87
+ sockets.delete(socket)
88
+ }
89
+ socket.once('close', abandonForward)
90
+
91
+ forwardTimer = setTimeout(() => {
92
+ if (forwardFinished) return
93
+ forwardFinished = true
94
+ try {
95
+ socket.destroy(new Error(`SSH tunnel forward timed out after ${TUNNEL_FORWARD_TIMEOUT_MS}ms`))
96
+ } catch { /* local peer already gone */ }
97
+ }, TUNNEL_FORWARD_TIMEOUT_MS)
98
+ forwardTimer.unref?.()
99
+
100
+ client.forwardOut('127.0.0.1', 0, remoteHost, options.remotePort, (error, stream) => {
101
+ if (forwardFinished || socket.destroyed) {
102
+ if (forwardTimer !== undefined) clearTimeout(forwardTimer)
103
+ forwardFinished = true
104
+ if (stream !== undefined) {
105
+ try { stream.close() } catch { /* late channel already closed */ }
106
+ }
107
+ return
108
+ }
109
+ forwardFinished = true
110
+ if (forwardTimer !== undefined) clearTimeout(forwardTimer)
111
+ if (error !== undefined) {
112
+ socket.destroy()
113
+ return
114
+ }
115
+ const destroy = (): void => {
116
+ try { socket.destroy() } catch { /* gone */ }
117
+ try { stream.close() } catch { /* gone */ }
118
+ }
119
+ stream.on('error', destroy)
120
+ socket.on('error', destroy)
121
+ stream.on('close', destroy)
122
+ socket.on('close', destroy)
123
+ stream.pipe(socket).pipe(stream)
124
+ })
125
+ })
126
+
127
+ let rejectStart: ((reason?: unknown) => void) | undefined
128
+ const clientFailureHandler = (error?: unknown): void => {
129
+ this.markFailedForClient(client, error)
130
+ rejectStart?.(error instanceof Error
131
+ ? error
132
+ : new Error(`SSH connection '${alias}' closed while starting tunnel`))
133
+ }
134
+ const tunnel: TunnelRecord = { info, server, alias, lease, sockets, clientFailureHandler }
135
+ this.tunnels.set(id, tunnel)
136
+ client.once('error', clientFailureHandler)
137
+ client.once('close', clientFailureHandler)
138
+
139
+ try {
140
+ await new Promise<void>((resolve, reject) => {
141
+ let settled = false
142
+ const resolveOnce = (): void => {
143
+ if (settled) return
144
+ settled = true
145
+ rejectStart = undefined
146
+ server.removeListener('error', rejectOnce)
147
+ resolve()
148
+ }
149
+ const rejectOnce = (error: unknown): void => {
150
+ if (settled) return
151
+ settled = true
152
+ rejectStart = undefined
153
+ server.removeListener('error', rejectOnce)
154
+ reject(error)
155
+ }
156
+ rejectStart = rejectOnce
157
+ server.once('error', rejectOnce)
158
+ server.listen(options.localPort ?? 0, '127.0.0.1', resolveOnce)
159
+ })
160
+ } catch (error) {
161
+ this.tunnels.delete(id)
162
+ this.removeClientFailureListener(tunnel)
163
+ try { server.close() } catch { /* never listened */ }
164
+ for (const socket of sockets) {
165
+ try { socket.destroy() } catch { /* already closed */ }
166
+ }
167
+ sockets.clear()
168
+ lease.release()
169
+ throw error
170
+ }
171
+
172
+ if (info.state === 'failed') {
173
+ this.tunnels.delete(id)
174
+ this.removeClientFailureListener(tunnel)
175
+ lease.release()
176
+ throw new Error(`SSH connection '${alias}' closed while starting tunnel`)
177
+ }
178
+
179
+ const address = server.address()
180
+ info.localPort = typeof address === 'object' && address !== null ? address.port : 0
181
+ info.state = 'forwarding'
182
+ return info
183
+ }
184
+
185
+ listTunnels(): TunnelInfo[] {
186
+ return [...this.tunnels.values()].map(tunnel => ({ ...tunnel.info }))
187
+ }
188
+
189
+ stopTunnel(id: string): boolean {
190
+ const tunnel = this.tunnels.get(id)
191
+ if (tunnel === undefined) return false
192
+ this.tunnels.delete(id)
193
+ this.removeClientFailureListener(tunnel)
194
+ try { tunnel.server.close() } catch { /* already closed */ }
195
+ for (const socket of tunnel.sockets) {
196
+ try { socket.destroy() } catch { /* already closed */ }
197
+ }
198
+ tunnel.sockets.clear()
199
+ tunnel.lease.release()
200
+ return true
201
+ }
202
+
203
+ stopAllTunnels(alias?: string): number {
204
+ let count = 0
205
+ for (const [id, tunnel] of [...this.tunnels]) {
206
+ if (alias === undefined || tunnel.alias === alias) {
207
+ this.stopTunnel(id)
208
+ count += 1
209
+ }
210
+ }
211
+ return count
212
+ }
213
+
214
+ dispose(): void {
215
+ this.stopAllTunnels()
216
+ }
217
+ }