@tiphareth/dsh-hardssh 0.1.2 → 0.2.3
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/LICENSE +30 -0
- package/README.md +77 -50
- package/SKILLS.md +140 -0
- package/lib/base/index.js +95 -0
- package/lib/client.js +1171 -1321
- package/lib/fs.js +333 -416
- package/lib/index.js +5428 -2991
- package/lib/ledger-B-LXlftp.js +43 -0
- package/lib/ledger-D2ezq1iW.js +376 -0
- package/lib/model-Cp7f70Mb.js +5 -0
- package/lib/registry-CViuzKYI.js +421 -0
- package/lib/subprocess.js +92 -549
- package/lib/types/backend.d.ts +73 -78
- package/lib/types/base/capability.d.ts +4 -0
- package/lib/types/base/ledger-router.d.ts +42 -26
- package/lib/types/base/ledger.d.ts +30 -19
- package/lib/types/base/model.d.ts +17 -8
- package/lib/types/base/plugin.d.ts +11 -0
- package/lib/types/base/registry.d.ts +28 -31
- package/lib/types/base/router.d.ts +0 -7
- package/lib/types/client/api.d.ts +8 -8
- package/lib/types/client/connect-host.d.ts +13 -0
- package/lib/types/client/index.d.ts +6 -4
- package/lib/types/client/locales.d.ts +15 -1
- package/lib/types/client/session-connect-gate.d.ts +71 -0
- package/lib/types/client/ssh/api.d.ts +11 -9
- package/lib/types/client/ssh/apply.d.ts +14 -8
- package/lib/types/client/ssh/locales.d.ts +8 -1
- package/lib/types/client/ssh/ops-tab.d.ts +34 -0
- package/lib/types/client/ssh/panel/ClusterTab.d.ts +3 -1
- package/lib/types/client/ssh/panel/ConnectionErrorDialog.d.ts +8 -0
- package/lib/types/client/ssh/panel/SessionSecretDialog.d.ts +4 -1
- package/lib/types/client/ssh/panel/SshPanel.d.ts +8 -8
- package/lib/types/client/ssh/panel/TerminalTab.d.ts +3 -5
- package/lib/types/client/ssh/panel/TransferTab.d.ts +5 -1
- package/lib/types/client/ssh/panel/TunnelsTab.d.ts +3 -1
- package/lib/types/client/ssh/session-target.d.ts +17 -0
- package/lib/types/client/state.d.ts +4 -1
- package/lib/types/client/workspace-panel-entry.d.ts +30 -0
- package/lib/types/client/workspace-panel.d.ts +24 -0
- package/lib/types/client-http.d.ts +77 -6
- package/lib/types/core.d.ts +5 -16
- package/lib/types/fs.d.ts +42 -25
- package/lib/types/index.d.ts +138 -9
- package/lib/types/ledger.d.ts +16 -111
- package/lib/types/protocol.d.ts +0 -5
- package/lib/types/providers/index.d.ts +6 -3
- package/lib/types/providers/local/provider.d.ts +83 -23
- package/lib/types/providers/ssh/provider.d.ts +24 -57
- package/lib/types/remote/environment.d.ts +2 -0
- package/lib/types/remote/remote-fs.d.ts +12 -1
- package/lib/types/remote/remote-process.d.ts +22 -1
- package/lib/types/remote/remote-subprocess.d.ts +13 -1
- package/lib/types/remote/remote-terminal.d.ts +4 -0
- package/lib/types/remote-search.d.ts +3 -3
- package/lib/types/routes.d.ts +36 -8
- package/lib/types/runtime/dsh-capabilities.d.ts +18 -0
- package/lib/types/runtime/workspace-core.d.ts +73 -38
- package/lib/types/runtime/workspace-migration.d.ts +113 -0
- package/lib/types/ssh/connection/lease.d.ts +23 -0
- package/lib/types/ssh/connection/manager.d.ts +359 -0
- package/lib/types/ssh/connection/pool.d.ts +8 -1
- package/lib/types/ssh/engine.d.ts +97 -267
- package/lib/types/ssh/known-hosts.d.ts +22 -5
- package/lib/types/ssh/local-transfer-policy.d.ts +20 -0
- package/lib/types/ssh/plugin.d.ts +10 -15
- package/lib/types/ssh/protocol.d.ts +18 -0
- package/lib/types/ssh/routes.d.ts +15 -7
- package/lib/types/ssh/sftp/service.d.ts +216 -0
- package/lib/types/ssh/store.d.ts +20 -8
- package/lib/types/ssh/terminal/service.d.ts +49 -0
- package/lib/types/ssh/tunnel/service.d.ts +26 -0
- package/lib/types/ssh/vault.d.ts +51 -4
- package/lib/types/subprocess.d.ts +13 -21
- package/lib/types/switch/switch-fs.d.ts +52 -2
- package/lib/types/switch/switch-subprocess.d.ts +32 -3
- package/lib/types/tools.d.ts +7 -6
- package/lib/types/workspace-tool-ops.d.ts +33 -0
- package/lib/types/workspace.d.ts +18 -0
- package/lib/vault-3gpWct2Q.js +559 -0
- package/lib/workspace.js +2 -0
- package/package.json +59 -26
- package/scripts/export-legacy-workspaces.mjs +136 -0
- package/src/backend.ts +155 -483
- package/src/base/capability.ts +4 -0
- package/src/base/ledger-router.ts +214 -65
- package/src/base/ledger.ts +175 -48
- package/src/base/model.ts +18 -8
- package/src/base/plugin.ts +15 -4
- package/src/base/registry.ts +49 -41
- package/src/base/router.ts +0 -8
- package/src/client/api.ts +8 -36
- package/src/client/connect-host.ts +199 -0
- package/src/client/directory-flow.tsx +91 -36
- package/src/client/index.ts +56 -53
- package/src/client/locales.ts +14 -0
- package/src/client/session-connect-gate.ts +163 -0
- package/src/client/ssh/api.ts +245 -108
- package/src/client/ssh/apply.ts +23 -24
- package/src/client/ssh/locales.ts +16 -2
- package/src/client/ssh/ops-tab.tsx +79 -0
- package/src/client/ssh/panel/ClusterTab.tsx +9 -20
- package/src/client/ssh/panel/ConnectionErrorDialog.tsx +33 -0
- package/src/client/ssh/panel/SessionSecretDialog.tsx +5 -1
- package/src/client/ssh/panel/SshPanel.tsx +81 -77
- package/src/client/ssh/panel/TerminalTab.tsx +18 -37
- package/src/client/ssh/panel/TransferTab.tsx +43 -33
- package/src/client/ssh/panel/TunnelsTab.tsx +9 -26
- package/src/client/ssh/panel/panel.module.css +90 -127
- package/src/client/ssh/session-target.ts +69 -0
- package/src/client/state.ts +9 -1
- package/src/client/workspace-badges.ts +99 -96
- package/src/client/workspace-panel-entry.tsx +91 -0
- package/src/client/workspace-panel.tsx +227 -0
- package/src/client/workspace.module.css +76 -2
- package/src/client-http.ts +118 -18
- package/src/core.ts +37 -47
- package/src/fs.ts +171 -85
- package/src/index.ts +535 -244
- package/src/ledger.ts +62 -416
- package/src/protocol.ts +0 -6
- package/src/providers/index.ts +8 -4
- package/src/providers/local/provider.ts +259 -87
- package/src/providers/ssh/provider.ts +158 -155
- package/src/remote/environment.ts +22 -1
- package/src/remote/remote-fs.ts +123 -34
- package/src/remote/remote-process.ts +265 -203
- package/src/remote/remote-subprocess.ts +61 -18
- package/src/remote/remote-terminal.ts +106 -21
- package/src/remote-search.ts +122 -26
- package/src/routes.ts +416 -395
- package/src/runtime/dsh-capabilities.ts +19 -0
- package/src/runtime/workspace-core.ts +171 -88
- package/src/runtime/workspace-migration.ts +472 -0
- package/src/ssh/connection/lease.ts +35 -0
- package/src/ssh/connection/manager.ts +1083 -0
- package/src/ssh/connection/pool.ts +341 -275
- package/src/ssh/engine.ts +269 -1477
- package/src/ssh/known-hosts.ts +42 -18
- package/src/ssh/local-transfer-policy.ts +83 -0
- package/src/ssh/plugin.ts +20 -19
- package/src/ssh/protocol.ts +22 -1
- package/src/ssh/routes.ts +419 -187
- package/src/ssh/sftp/service.ts +967 -0
- package/src/ssh/store.ts +169 -72
- package/src/ssh/terminal/service.ts +177 -0
- package/src/ssh/tools.ts +42 -20
- package/src/ssh/tunnel/service.ts +217 -0
- package/src/ssh/vault.ts +245 -91
- package/src/subprocess.ts +88 -71
- package/src/switch/switch-fs.ts +141 -10
- package/src/switch/switch-subprocess.ts +80 -6
- package/src/tools.ts +217 -221
- package/src/workspace-tool-ops.ts +101 -0
- package/src/workspace.ts +52 -0
- package/lib/environment-BL1jddfB.js +0 -449
- package/lib/switch-fs-CAJpFY9C.js +0 -193
- package/lib/switch-fs-RrZtG2gv.js +0 -210
- package/lib/types/client/manager-button.d.ts +0 -32
- package/lib/types/client/ssh/mount.d.ts +0 -13
- package/lib/types/client/ssh/panel/HostsTab.d.ts +0 -10
- package/lib/types/client/ssh/panel/controller.d.ts +0 -23
- package/lib/types/client/ssh/sidebar-entry.d.ts +0 -25
- package/lib/types/client/workspace-gate.d.ts +0 -15
- package/lib/types/remote-runner.d.ts +0 -83
- package/lib/types/seam-state.d.ts +0 -69
- package/src/client/manager-button.tsx +0 -269
- package/src/client/ssh/mount.tsx +0 -83
- package/src/client/ssh/panel/HostsTab.tsx +0 -236
- package/src/client/ssh/panel/controller.ts +0 -48
- package/src/client/ssh/sidebar-entry.ts +0 -123
- package/src/client/workspace-gate.ts +0 -225
- package/src/remote-runner.ts +0 -201
- 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
|
+
}
|