dsh-ssh-workbench 0.0.0-stage → 1.6.4

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 (88) hide show
  1. package/CHANGELOG.md +504 -0
  2. package/CONTRIBUTING.md +62 -0
  3. package/LICENSE +21 -0
  4. package/README.md +159 -2
  5. package/SECURITY.md +25 -0
  6. package/cordis.patch.yml +5 -0
  7. package/docs/architecture.md +124 -0
  8. package/docs/quickstart.md +94 -0
  9. package/lib/client.js +4977 -0
  10. package/lib/core/activity.js +83 -0
  11. package/lib/core/background.js +69 -0
  12. package/lib/core/broker.js +799 -0
  13. package/lib/core/dialect.js +28 -0
  14. package/lib/core/errors.js +138 -0
  15. package/lib/core/gate-pwsh.js +88 -0
  16. package/lib/core/gate.js +572 -0
  17. package/lib/core/guard.js +71 -0
  18. package/lib/core/known-hosts.js +74 -0
  19. package/lib/core/local-file-api.js +174 -0
  20. package/lib/core/localfs.js +381 -0
  21. package/lib/core/paths.js +104 -0
  22. package/lib/core/posix-quote.js +4 -0
  23. package/lib/core/prefs.js +38 -0
  24. package/lib/core/remote-bash-tool.js +321 -0
  25. package/lib/core/remote-bash.js +337 -0
  26. package/lib/core/secrets.js +96 -0
  27. package/lib/core/server-reference.js +299 -0
  28. package/lib/core/shell-adapters.js +48 -0
  29. package/lib/core/ssh.js +761 -0
  30. package/lib/core/store.js +83 -0
  31. package/lib/core/targets.js +250 -0
  32. package/lib/core/terminals.js +299 -0
  33. package/lib/core/win32-adapter.js +141 -0
  34. package/lib/index.js +450 -0
  35. package/lib/routes.js +733 -0
  36. package/locale/en.json +6 -0
  37. package/locale/zh.json +6 -0
  38. package/package.json +88 -4
  39. package/vendor/NOTICE.md +50 -0
  40. package/vendor/XTERM-LICENSE +21 -0
  41. package/vendor/node_modules/asn1/LICENSE +19 -0
  42. package/vendor/node_modules/asn1/README.md +50 -0
  43. package/vendor/node_modules/asn1/lib/ber/errors.js +13 -0
  44. package/vendor/node_modules/asn1/lib/ber/index.js +27 -0
  45. package/vendor/node_modules/asn1/lib/ber/reader.js +262 -0
  46. package/vendor/node_modules/asn1/lib/ber/types.js +36 -0
  47. package/vendor/node_modules/asn1/lib/ber/writer.js +317 -0
  48. package/vendor/node_modules/asn1/lib/index.js +20 -0
  49. package/vendor/node_modules/asn1/package.json +31 -0
  50. package/vendor/node_modules/bcrypt-pbkdf/CONTRIBUTING.md +13 -0
  51. package/vendor/node_modules/bcrypt-pbkdf/LICENSE +66 -0
  52. package/vendor/node_modules/bcrypt-pbkdf/README.md +45 -0
  53. package/vendor/node_modules/bcrypt-pbkdf/index.js +556 -0
  54. package/vendor/node_modules/bcrypt-pbkdf/package.json +15 -0
  55. package/vendor/node_modules/safer-buffer/LICENSE +21 -0
  56. package/vendor/node_modules/safer-buffer/Readme.md +156 -0
  57. package/vendor/node_modules/safer-buffer/dangerous.js +58 -0
  58. package/vendor/node_modules/safer-buffer/package.json +34 -0
  59. package/vendor/node_modules/safer-buffer/safer.js +77 -0
  60. package/vendor/node_modules/tweetnacl/AUTHORS.md +28 -0
  61. package/vendor/node_modules/tweetnacl/LICENSE +24 -0
  62. package/vendor/node_modules/tweetnacl/README.md +459 -0
  63. package/vendor/node_modules/tweetnacl/nacl-fast.js +2388 -0
  64. package/vendor/node_modules/tweetnacl/nacl.js +1175 -0
  65. package/vendor/node_modules/tweetnacl/package.json +58 -0
  66. package/vendor/ssh2/LICENSE +19 -0
  67. package/vendor/ssh2/lib/Channel.js +295 -0
  68. package/vendor/ssh2/lib/agent.js +1123 -0
  69. package/vendor/ssh2/lib/client.js +2176 -0
  70. package/vendor/ssh2/lib/http-agents.js +84 -0
  71. package/vendor/ssh2/lib/index.js +44 -0
  72. package/vendor/ssh2/lib/keygen.js +582 -0
  73. package/vendor/ssh2/lib/protocol/Protocol.js +2136 -0
  74. package/vendor/ssh2/lib/protocol/SFTP.js +4052 -0
  75. package/vendor/ssh2/lib/protocol/constants.js +356 -0
  76. package/vendor/ssh2/lib/protocol/crypto/poly1305.js +43 -0
  77. package/vendor/ssh2/lib/protocol/crypto.js +1602 -0
  78. package/vendor/ssh2/lib/protocol/handlers.js +16 -0
  79. package/vendor/ssh2/lib/protocol/handlers.misc.js +1285 -0
  80. package/vendor/ssh2/lib/protocol/kex.js +1908 -0
  81. package/vendor/ssh2/lib/protocol/keyParser.js +1484 -0
  82. package/vendor/ssh2/lib/protocol/node-fs-compat.js +115 -0
  83. package/vendor/ssh2/lib/protocol/utils.js +356 -0
  84. package/vendor/ssh2/lib/protocol/zlib.js +255 -0
  85. package/vendor/ssh2/lib/server.js +1380 -0
  86. package/vendor/ssh2/lib/utils.js +336 -0
  87. package/vendor/ssh2/package.json +34 -0
  88. package/vendor/xterm.js +53 -0
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Target dialect helpers.
3
+ *
4
+ * A server record keeps a hand-maintained `platform` hint (`posix`/`windows`)
5
+ * for the UI and for pre-probe compatibility, but the only trustworthy source
6
+ * is a live probe. `facts.dialect` stores that probe result; every decision
7
+ * that changes the command syntax (which remote shell tool to use, how to
8
+ * wrap a timeout) reads it through `effectiveDialect()`.
9
+ */
10
+
11
+ /** The dialect values this plugin understands. */
12
+ export const DIALECTS = Object.freeze({ POSIX: 'posix', WINDOWS: 'windows' })
13
+
14
+ /** Normalize one raw dialect/platform value. */
15
+ export function normalizeDialect(value) {
16
+ return value === DIALECTS.WINDOWS || value === 'win32' ? DIALECTS.WINDOWS : DIALECTS.POSIX
17
+ }
18
+
19
+ /**
20
+ * The dialect to build commands for.
21
+ * A probed value wins; the record's `platform` is only a fallback hint.
22
+ */
23
+ export function effectiveDialect(server) {
24
+ const probed = server?.facts?.dialect
25
+ if (probed === DIALECTS.WINDOWS || probed === DIALECTS.POSIX) return probed
26
+ return normalizeDialect(server?.platform)
27
+ }
28
+
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Shared SSH error taxonomy.
3
+ *
4
+ * Kept in its own module so the transport broker and the tool-facing runtime
5
+ * can share the same `SshError` class without an import cycle.
6
+ */
7
+
8
+ export class SshError extends Error {
9
+ constructor(code, message, extra = {}) {
10
+ super(message)
11
+ this.name = 'SshError'
12
+ this.code = code
13
+ Object.assign(this, extra)
14
+ }
15
+ }
16
+
17
+ export function normalizeError(error, fingerprint) {
18
+ // Keep structured errors intact: a missing private key is an auth problem,
19
+ // not a generic execution failure.
20
+ if (error instanceof SshError) return error
21
+ const message = error instanceof Error ? error.message : String(error)
22
+ const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : ''
23
+ if (/host key verification failed|host denied \(verification failed\)|hostVerifier/i.test(message)) {
24
+ return new SshError('HOST_KEY_UNKNOWN', message, { fingerprint })
25
+ }
26
+ if (/all configured authentication methods failed|authentication failed|permission denied/i.test(message)) {
27
+ return new SshError('AUTH_FAILED', message)
28
+ }
29
+ if (/timed out while waiting|connect timeout|timeout/i.test(message) && !/command/i.test(message)) {
30
+ return new SshError('CONNECT_TIMEOUT', message)
31
+ }
32
+ if (/econnrefused|ehostunreach|enotfound|getaddrinfo|network unreachable|econnreset/i.test(message) || /^(ECONN|ENOTFOUND|EHOST)/.test(code)) {
33
+ return new SshError('NETWORK', message)
34
+ }
35
+ return new SshError('EXEC_FAILED', message)
36
+ }
37
+
38
+ export function suggestedActionFor(code) {
39
+ switch (code) {
40
+ case 'AUTH_MISSING':
41
+ return 'Set the SSH password (or switch to key auth) in Settings → SSH Workbench → managed resources.'
42
+ case 'AUTH_FAILED':
43
+ return 'Check the username, private key path, or password for this resource.'
44
+ case 'HOST_KEY_UNKNOWN':
45
+ return 'Compare the shown fingerprint with the server operator, then trust it in Settings → SSH Workbench.'
46
+ case 'CONNECT_TIMEOUT':
47
+ return 'Check the host/port and network path; raise connectTimeoutMs if the host is slow.'
48
+ case 'NETWORK':
49
+ return 'The host is unreachable: check DNS, routing, VPN, or the SSH port.'
50
+ case 'TIMEOUT':
51
+ return 'The command exceeded its timeout. Narrow the command or raise timeoutMs.'
52
+ case 'CANCELLED':
53
+ return 'The operation was cancelled.'
54
+ case 'COMMAND_NOT_FOUND':
55
+ return 'The remote shell reported a missing command; check the remote PATH.'
56
+ case 'SFTP_FAILED':
57
+ return 'The SFTP operation failed; check the remote path, permissions, and the SSH server SFTP subsystem.'
58
+ case 'COMMAND_NOT_EXECUTABLE':
59
+ return 'The file exists but is not executable (exit 126); check the shebang and chmod +x.'
60
+ case 'PERMISSION_DENIED':
61
+ return 'Permission denied: check the remote user, file ownership/mode, or use a privileged account.'
62
+ case 'DISK_FULL':
63
+ return 'The remote filesystem is full or over quota; free space (df -h) and retry.'
64
+ case 'READ_ONLY_FS':
65
+ return 'The remote filesystem is read-only; remount it read-write or pick another path.'
66
+ case 'INTERRUPTED':
67
+ return 'The command was interrupted (exit 130 / SIGINT).'
68
+ case 'KILLED':
69
+ return 'The command was killed (exit 137 / SIGKILL); it may have hit the OOM killer or a manual kill.'
70
+ default:
71
+ return 'Inspect stderr for details, or run a smaller diagnostic command first.'
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Classify a failed exec result into the most specific error code we can infer
77
+ * from the exit status and stderr. Used for both `errorCode` and the
78
+ * human-readable suggestion.
79
+ */
80
+ export function classifyExecFailure({ exitCode, stderr, signal } = {}) {
81
+ const text = String(stderr ?? '')
82
+ if (/command not found/i.test(text)) return 'COMMAND_NOT_FOUND'
83
+ if (/permission denied/i.test(text)) return 'PERMISSION_DENIED'
84
+ if (/no space left on device|disk quota exceeded/i.test(text)) return 'DISK_FULL'
85
+ if (/read-only file system/i.test(text)) return 'READ_ONLY_FS'
86
+ if (signal) return 'KILLED'
87
+ switch (exitCode) {
88
+ case 126: return 'COMMAND_NOT_EXECUTABLE'
89
+ case 127: return 'COMMAND_NOT_FOUND'
90
+ case 130: return 'INTERRUPTED'
91
+ case 137: return 'KILLED'
92
+ default: return 'EXEC_FAILED'
93
+ }
94
+ }
95
+
96
+ export function errorResult(code, message, extra = {}) {
97
+ return {
98
+ outcome: code === 'CANCELLED' ? 'cancelled' : code === 'TIMEOUT' ? 'timeout' : 'failed',
99
+ exitCode: null,
100
+ signal: null,
101
+ stdout: '',
102
+ stderr: message,
103
+ durationMs: 0,
104
+ truncated: { stdout: false, stderr: false },
105
+ errorCode: code,
106
+ suggestedAction: suggestedActionFor(code),
107
+ ...extra,
108
+ }
109
+ }
110
+
111
+ /**
112
+ * `grep`/`diff`/`pgrep` exit 1 for "no match" / "files differ": that is a
113
+ * normal answer, not an execution failure. Reinterpret it only when the whole
114
+ * command was classified read-only and the remote wrote nothing to stderr —
115
+ * a read-only command that exits 1 with a silent stderr is almost certainly
116
+ * reporting content, not a broken execution.
117
+ */
118
+ export function isBenignReadOnlyExit({ readOnly, exitCode, stderr, signal } = {}) {
119
+ return readOnly === true && exitCode === 1 && !signal && !String(stderr ?? '').trim()
120
+ }
121
+
122
+ /** Normalize any SFTP-layer error into a structured `SFTP_FAILED` SshError. */
123
+ export function toSftpError(error) {
124
+ if (error instanceof SshError) {
125
+ return error.code === 'EXEC_FAILED' ? new SshError('SFTP_FAILED', error.message) : error
126
+ }
127
+ return new SshError('SFTP_FAILED', error instanceof Error ? error.message : String(error))
128
+ }
129
+
130
+ /** Connection-level failure detection (used to evict a dead pooled connection). */
131
+ export function isConnectionLevelError(error) {
132
+ const message = error instanceof Error ? error.message : String(error ?? '')
133
+ const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : ''
134
+ if (['ECONNRESET', 'ECONNREFUSED', 'EPIPE', 'ETIMEDOUT', 'EHOSTUNREACH', 'ENETUNREACH'].includes(code)) return true
135
+ return /not\s+connected|connection\s+(lost|reset|closed|force\s+closed)|socket\s+closed|force\s+closed|keepalive\s+timeout|read\s+ec\w+|broken\s+pipe|econn\w+|epipe|etimedout/i.test(
136
+ message,
137
+ )
138
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * PowerShell blacklist for the Windows remote shell.
3
+ *
4
+ * Same philosophy as the POSIX gate: this is a deterministic accident stop,
5
+ * not a permission boundary. It only catches machine-destroying commands a
6
+ * model might emit by mistake (disk/volume formatting, partition edits, boot
7
+ * configuration edits, whole-drive recursive deletes). Everything else is left
8
+ * to the DSH session approval preset.
9
+ */
10
+
11
+ const RULES = [
12
+ {
13
+ ruleId: 'format-volume',
14
+ reason: 'formatting a volume (Format-Volume)',
15
+ pattern: /\bformat-volume\b/,
16
+ },
17
+ {
18
+ ruleId: 'disk-management',
19
+ reason: 'disk/partition management (Clear-Disk, Initialize-Disk, Remove-Partition)',
20
+ pattern: /\b(?:clear-disk|initialize-disk|remove-partition|resize-partition)\b/,
21
+ },
22
+ {
23
+ ruleId: 'diskpart',
24
+ reason: 'interactive disk partitioning (diskpart)',
25
+ pattern: /\bdiskpart(?:\.exe)?\b/,
26
+ },
27
+ {
28
+ ruleId: 'format-command',
29
+ reason: 'formatting a drive (format)',
30
+ pattern: /\bformat(?:\.com)?\s+[a-z]:/,
31
+ },
32
+ {
33
+ ruleId: 'boot-config',
34
+ reason: 'boot configuration edit (bcdedit)',
35
+ pattern: /\bbcdedit(?:\.exe)?\b/,
36
+ },
37
+ {
38
+ ruleId: 'wipe-free-space',
39
+ reason: 'wiping free space (cipher /w)',
40
+ pattern: /\bcipher(?:\.exe)?\s+\/w\b/,
41
+ },
42
+ {
43
+ ruleId: 'shadow-delete',
44
+ reason: 'deleting volume shadow copies (vssadmin/wbadmin)',
45
+ pattern: /\b(?:vssadmin|wbadmin)(?:\.exe)?\s+delete\b/,
46
+ },
47
+ {
48
+ ruleId: 'rm-root',
49
+ reason: 'recursive delete of a drive root or system directory',
50
+ pattern:
51
+ /\b(?:remove-item|rm|rd|rmdir|del)\b(?=[^|;]*(?:-recurse|-r\b|\/s)\b)[^|;]*?(?:[a-z]:[\\/]{1,2}(?:windows|users|programdata|program files)\b|[a-z]:[\\/]{0,2}(?:['"]|\s|$)|\$(?:env:)?systemdrive\b|%systemdrive%)/,
52
+ },
53
+ {
54
+ ruleId: 'remove-root-recursive',
55
+ reason: 'recursive delete of a whole drive',
56
+ pattern: /\b(?:remove-item|rm|rd|rmdir|del)\b[^|;]*?['"]?[a-z]:[\\/]?['"]?\s+(?:-recurse|-force|\/s|\/q)/,
57
+ },
58
+ ]
59
+
60
+ /** Split one PowerShell command into independently-executed statements. */
61
+ export function powerShellStatements(command) {
62
+ return String(command ?? '')
63
+ .split(/[;\r\n]|\|\||&&|\|/)
64
+ .map((part) => part.trim())
65
+ .filter(Boolean)
66
+ }
67
+
68
+ /** Lowercase + collapse whitespace, without touching quoted content semantics. */
69
+ function normalizeStatement(statement) {
70
+ return statement.toLowerCase().replace(/\s+/g, ' ').trim()
71
+ }
72
+
73
+ /**
74
+ * @param {string} command
75
+ * @returns {{decision: 'deny'|'allow', reason: string, ruleId: string}}
76
+ */
77
+ export function classifyPowerShellCommand(command) {
78
+ if (!String(command ?? '').trim()) return { decision: 'allow', reason: 'empty command', ruleId: 'empty' }
79
+ for (const statement of powerShellStatements(command)) {
80
+ const text = normalizeStatement(statement)
81
+ for (const rule of RULES) {
82
+ if (rule.pattern.test(text)) {
83
+ return { decision: 'deny', reason: rule.reason, ruleId: rule.ruleId }
84
+ }
85
+ }
86
+ }
87
+ return { decision: 'allow', reason: 'not on the PowerShell machine-destruction blacklist', ruleId: 'allow' }
88
+ }