dsh-ssh-manage 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +5 -5
- package/README.md +5 -5
- package/client.js +107 -4
- package/index.js +6 -0
- package/lib/ssh-client.js +10 -1
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
- **Host management** — scan the config, groups, tags, favorites, notes, latency and host inspection;
|
|
26
26
|
- **Interactive terminal** — ssh2 allocates a PTY, xterm.js renders it, with colors, arrow keys, full-screen programs such as `vim`/`top`, and window resizing;
|
|
27
27
|
- **Day-to-day operations** — batch commands, SFTP browse/mkdir/rename/delete/upload/download, connection tests;
|
|
28
|
-
- **Credentials** — one AES-256-GCM encrypted password per host, never written to `~/.ssh/config
|
|
28
|
+
- **Credentials** — one AES-256-GCM encrypted password per host, never written to `~/.ssh/config`; passwords are not returned to the browser by default, but Show password decrypts one on demand in the current page and auto-hides it after 30 seconds;
|
|
29
29
|
- **Agent tool** — `ssh_manager` exposes the same capabilities to the model.
|
|
30
30
|
|
|
31
31
|
No more jumping between a terminal and DSH, and no need to remember where each host lives in the config file.
|
|
@@ -60,7 +60,7 @@ No more jumping between a terminal and DSH, and no need to remember where each h
|
|
|
60
60
|
### Credentials and data
|
|
61
61
|
|
|
62
62
|
- Passwords are AES-256-GCM encrypted in the plugin state directory and unlocked automatically at startup; no master password, no system Keychain.
|
|
63
|
-
- Passwords are only used for SSH password authentication (also compatible with hosts that only offer keyboard-interactive). They are never written to `~/.ssh/config`, command arguments
|
|
63
|
+
- Passwords are only used for SSH password authentication (also compatible with hosts that only offer keyboard-interactive). They are never written to `~/.ssh/config`, command arguments or logs. They are not returned by default; Show password explicitly decrypts one into the current page for 30 seconds and it is never exposed through the Agent tool.
|
|
64
64
|
- Ciphertext and key live in the same user directory, so this mainly protects against accidental file reads, not against malicious processes running as the same user.
|
|
65
65
|
- Groups, tags, favorites, notes and inspection data live in `hosts.json`; passwords live in `vault.json`; configuration changes are snapshotted to the backup directory.
|
|
66
66
|
|
|
@@ -96,16 +96,16 @@ Creating a host requires alias, hostname and password. The password is not writt
|
|
|
96
96
|
|
|
97
97
|
### From npm (recommended)
|
|
98
98
|
|
|
99
|
-
npm package: [`dsh-ssh-manage`](https://www.npmjs.com/package/dsh-ssh-manage) (current version `0.3.
|
|
99
|
+
npm package: [`dsh-ssh-manage`](https://www.npmjs.com/package/dsh-ssh-manage) (current version `0.3.5`).
|
|
100
100
|
|
|
101
101
|
```text
|
|
102
|
-
plugin_manager action=install_bundle target=dsh-ssh-manage@0.3.
|
|
102
|
+
plugin_manager action=install_bundle target=dsh-ssh-manage@0.3.5
|
|
103
103
|
```
|
|
104
104
|
|
|
105
105
|
Or with the DSH CLI:
|
|
106
106
|
|
|
107
107
|
```bash
|
|
108
|
-
dsh plugin add dsh-ssh-manage@0.3.
|
|
108
|
+
dsh plugin add dsh-ssh-manage@0.3.5
|
|
109
109
|
```
|
|
110
110
|
|
|
111
111
|
After installation, refresh the Web page; the sidebar shows an "SSH Hosts" entry with id `dsh-ssh-manage`.
|
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
- **主机管理**:扫描配置、分组、标签、收藏、备注、连接延迟、主机巡检;
|
|
26
26
|
- **交互式终端**:ssh2 分配 PTY,xterm.js 渲染,支持彩色输出、方向键、`vim`/`top` 等全屏程序和窗口自适应;
|
|
27
27
|
- **日常运维**:批量命令、SFTP 目录浏览 / 新建 / 重命名 / 删除 / 上传 / 下载、测试连接;
|
|
28
|
-
- **凭据管理**:每台主机保存一份 AES-256-GCM 加密密码,不写进 `~/.ssh/config
|
|
28
|
+
- **凭据管理**:每台主机保存一份 AES-256-GCM 加密密码,不写进 `~/.ssh/config`;默认不返回浏览器,在主机详情点“显示密码”时才按需解密到当前页面,30 秒后自动隐藏;
|
|
29
29
|
- **Agent 工具**:`ssh_manager` 把同一套能力开放给模型。
|
|
30
30
|
|
|
31
31
|
不用再在终端和 DSH 之间来回切换,也不用记每个主机在配置文件里的位置。
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
### 凭据与数据
|
|
61
61
|
|
|
62
62
|
- 密码使用 AES-256-GCM 加密保存在插件自己的状态目录,插件启动时自动解锁,不需要每次输入主密码,也不使用系统 Keychain。
|
|
63
|
-
- 密码只用于 SSH password authentication,同时兼容只提供 keyboard-interactive 的主机;不会写入 `~/.ssh/config
|
|
63
|
+
- 密码只用于 SSH password authentication,同时兼容只提供 keyboard-interactive 的主机;不会写入 `~/.ssh/config`、命令参数或日志。默认不返回浏览器;仅在你主动点击“显示密码”时按需解密到当前页面,30 秒后自动隐藏,且不会通过 Agent 工具返回。
|
|
64
64
|
- 密码密文与本地密钥位于同一用户目录,主要防止文件被直接误读,不能抵御同一用户下的恶意进程。
|
|
65
65
|
- 主机分组、标签、收藏、备注、巡检结果保存在 `hosts.json`;密码保存在 `vault.json`;删除/修改配置前写入备份目录。
|
|
66
66
|
|
|
@@ -98,16 +98,16 @@
|
|
|
98
98
|
|
|
99
99
|
### 从 npm 安装(推荐)
|
|
100
100
|
|
|
101
|
-
npm 包:[`dsh-ssh-manage`](https://www.npmjs.com/package/dsh-ssh-manage)(当前版本 `0.3.
|
|
101
|
+
npm 包:[`dsh-ssh-manage`](https://www.npmjs.com/package/dsh-ssh-manage)(当前版本 `0.3.5`)。
|
|
102
102
|
|
|
103
103
|
```text
|
|
104
|
-
plugin_manager action=install_bundle target=dsh-ssh-manage@0.3.
|
|
104
|
+
plugin_manager action=install_bundle target=dsh-ssh-manage@0.3.5
|
|
105
105
|
```
|
|
106
106
|
|
|
107
107
|
或使用 DSH CLI:
|
|
108
108
|
|
|
109
109
|
```bash
|
|
110
|
-
dsh plugin add dsh-ssh-manage@0.3.
|
|
110
|
+
dsh plugin add dsh-ssh-manage@0.3.5
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
安装后刷新 Web 页面,左侧栏会出现「SSH 主机」入口,入口 id 为 `dsh-ssh-manage`。
|
package/client.js
CHANGED
|
@@ -209,7 +209,13 @@ window.__ModuleLoader__.load({
|
|
|
209
209
|
'drawer.details.password': ['密码', 'Password'],
|
|
210
210
|
'drawer.details.savePassword': ['保存密码', 'Save password'],
|
|
211
211
|
'drawer.details.clearPassword': ['清除密码', 'Clear password'],
|
|
212
|
-
'drawer.details.passwordHint': ['密码使用 AES-256-GCM
|
|
212
|
+
'drawer.details.passwordHint': ['密码使用 AES-256-GCM 加密保存在本机;点击“显示密码”可在当前页面临时明文查看,30 秒后自动隐藏。', 'Passwords are AES-256-GCM encrypted at rest; use Show password to reveal one temporarily on this page (auto-hides after 30 seconds).'],
|
|
213
|
+
'drawer.details.reveal': ['显示密码', 'Show password'],
|
|
214
|
+
'drawer.details.hide': ['隐藏密码', 'Hide password'],
|
|
215
|
+
'drawer.details.copyPassword': ['复制密码', 'Copy password'],
|
|
216
|
+
'drawer.details.passwordCopied': ['密码已复制', 'Password copied'],
|
|
217
|
+
'drawer.details.revealWarning': ['密码已明文显示;注意旁人、录屏和截图,30 秒后自动隐藏。', 'The password is visible in plain text; watch for onlookers, screen recording and screenshots. It hides after 30 seconds.'],
|
|
218
|
+
'drawer.details.revealFailed': ['无法显示密码:{message}', 'Could not reveal the password: {message}'],
|
|
213
219
|
'drawer.details.system': ['主机信息', 'System'],
|
|
214
220
|
'drawer.details.detect': ['检测信息', 'Detect'],
|
|
215
221
|
'drawer.details.detecting': ['检测中…', 'Detecting…'],
|
|
@@ -783,6 +789,25 @@ window.__ModuleLoader__.load({
|
|
|
783
789
|
)
|
|
784
790
|
}
|
|
785
791
|
|
|
792
|
+
function IconEye({ size = 14 }) {
|
|
793
|
+
return h(
|
|
794
|
+
Svg,
|
|
795
|
+
{ size },
|
|
796
|
+
h(P, { d: 'M2.5 12s3.6-6.2 9.5-6.2S21.5 12 21.5 12s-3.6 6.2-9.5 6.2S2.5 12 2.5 12Z' }),
|
|
797
|
+
h('circle', { cx: 12, cy: 12, r: 3, stroke: 'currentColor', strokeWidth: 1.5 }),
|
|
798
|
+
)
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function IconEyeOff({ size = 14 }) {
|
|
802
|
+
return h(
|
|
803
|
+
Svg,
|
|
804
|
+
{ size },
|
|
805
|
+
h(P, { d: 'M3 3l18 18' }),
|
|
806
|
+
h(P, { d: 'M10.6 6.1A10.7 10.7 0 0 1 12 6c5.9 0 9.5 6 9.5 6a17 17 0 0 1-3.1 3.6M6.4 6.8C3.9 8.7 2.5 12 2.5 12s3.6 6 9.5 6a10.9 10.9 0 0 0 4.5-1' }),
|
|
807
|
+
h(P, { d: 'M9.9 9.9a3 3 0 0 0 4.2 4.2' }),
|
|
808
|
+
)
|
|
809
|
+
}
|
|
810
|
+
|
|
786
811
|
function IconEraser({ size = 15 }) {
|
|
787
812
|
return h(
|
|
788
813
|
Svg,
|
|
@@ -4432,20 +4457,71 @@ window.__ModuleLoader__.load({
|
|
|
4432
4457
|
}
|
|
4433
4458
|
|
|
4434
4459
|
function DetailsView({ model, host, onEdit, onDelete }) {
|
|
4435
|
-
const { t, localeId, isPending, testHost, inspectHost, savePassword, removePassword } = model
|
|
4460
|
+
const { t, notify, localeId, isPending, testHost, inspectHost, savePassword, removePassword } = model
|
|
4436
4461
|
const [password, setPassword] = React.useState('')
|
|
4437
4462
|
const [saving, setSaving] = React.useState(false)
|
|
4463
|
+
const [revealedPassword, setRevealedPassword] = React.useState('')
|
|
4464
|
+
const [revealing, setRevealing] = React.useState(false)
|
|
4465
|
+
const revealTimer = React.useRef(null)
|
|
4438
4466
|
const credential = credentialInfo(host, t)
|
|
4439
4467
|
const testing = isPending(`test:${host.host}`)
|
|
4440
4468
|
const inspecting = isPending(`inspect:${host.host}`)
|
|
4441
4469
|
const hasSystem = Boolean(host.system || host.cpuCores || host.memoryGB || host.diskGB || host.latencyMs)
|
|
4442
4470
|
|
|
4471
|
+
const hideRevealed = () => {
|
|
4472
|
+
if (revealTimer.current) {
|
|
4473
|
+
clearTimeout(revealTimer.current)
|
|
4474
|
+
revealTimer.current = null
|
|
4475
|
+
}
|
|
4476
|
+
setRevealedPassword('')
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4479
|
+
React.useEffect(() => {
|
|
4480
|
+
hideRevealed()
|
|
4481
|
+
return () => {
|
|
4482
|
+
if (revealTimer.current) clearTimeout(revealTimer.current)
|
|
4483
|
+
}
|
|
4484
|
+
}, [host.host])
|
|
4485
|
+
|
|
4486
|
+
async function toggleReveal() {
|
|
4487
|
+
if (revealedPassword) {
|
|
4488
|
+
hideRevealed()
|
|
4489
|
+
return
|
|
4490
|
+
}
|
|
4491
|
+
if (revealing || host.passwordConfigured !== true) return
|
|
4492
|
+
setRevealing(true)
|
|
4493
|
+
try {
|
|
4494
|
+
const value = await callAction('credential/reveal', { host: host.host })
|
|
4495
|
+
const revealed = String(value?.password ?? '')
|
|
4496
|
+
if (!revealed) return
|
|
4497
|
+
setRevealedPassword(revealed)
|
|
4498
|
+
if (revealTimer.current) clearTimeout(revealTimer.current)
|
|
4499
|
+
revealTimer.current = setTimeout(() => {
|
|
4500
|
+
revealTimer.current = null
|
|
4501
|
+
setRevealedPassword('')
|
|
4502
|
+
}, 30_000)
|
|
4503
|
+
} catch (failure) {
|
|
4504
|
+
notify(t('drawer.details.revealFailed', { message: errorMessage(failure) }), 'error')
|
|
4505
|
+
} finally {
|
|
4506
|
+
setRevealing(false)
|
|
4507
|
+
}
|
|
4508
|
+
}
|
|
4509
|
+
|
|
4510
|
+
async function copyRevealed() {
|
|
4511
|
+
if (!revealedPassword) return
|
|
4512
|
+
const copied = await copyTextToClipboard(revealedPassword)
|
|
4513
|
+
notify(copied ? t('drawer.details.passwordCopied') : t('drawer.details.revealFailed', { message: t('terminal.copyFailed') }), copied ? 'success' : 'error')
|
|
4514
|
+
}
|
|
4515
|
+
|
|
4443
4516
|
async function submitPassword() {
|
|
4444
4517
|
if (!password || saving) return
|
|
4445
4518
|
setSaving(true)
|
|
4446
4519
|
const ok = await savePassword(host, password)
|
|
4447
4520
|
setSaving(false)
|
|
4448
|
-
if (ok)
|
|
4521
|
+
if (ok) {
|
|
4522
|
+
setPassword('')
|
|
4523
|
+
hideRevealed()
|
|
4524
|
+
}
|
|
4449
4525
|
}
|
|
4450
4526
|
|
|
4451
4527
|
return h(
|
|
@@ -4487,6 +4563,30 @@ window.__ModuleLoader__.load({
|
|
|
4487
4563
|
h('h3', { className: 'ssh-sectionTitle' }, t('drawer.details.credentials')),
|
|
4488
4564
|
h(Badge, { tone: credential.tone }, credential.label),
|
|
4489
4565
|
),
|
|
4566
|
+
h(
|
|
4567
|
+
'div',
|
|
4568
|
+
{ className: 'ssh-credentialReveal' },
|
|
4569
|
+
h(
|
|
4570
|
+
Button,
|
|
4571
|
+
{
|
|
4572
|
+
size: 'sm',
|
|
4573
|
+
variant: 'ghost',
|
|
4574
|
+
onClick: () => void toggleReveal(),
|
|
4575
|
+
disabled: revealing || (!host.passwordConfigured && !revealedPassword),
|
|
4576
|
+
},
|
|
4577
|
+
revealing ? h(Spinner, { size: 12 }) : revealedPassword ? h(IconEyeOff, { size: 13 }) : h(IconEye, { size: 13 }),
|
|
4578
|
+
revealedPassword ? t('drawer.details.hide') : t('drawer.details.reveal'),
|
|
4579
|
+
),
|
|
4580
|
+
revealedPassword
|
|
4581
|
+
? h('code', { className: 'ssh-revealValue' }, revealedPassword)
|
|
4582
|
+
: null,
|
|
4583
|
+
revealedPassword
|
|
4584
|
+
? h(IconButton, { title: t('drawer.details.copyPassword'), onClick: () => void copyRevealed() }, h(IconCopy, { size: 14 }))
|
|
4585
|
+
: null,
|
|
4586
|
+
),
|
|
4587
|
+
revealedPassword
|
|
4588
|
+
? h('div', { className: 'ssh-sectionHint ssh-revealWarning' }, h(IconEye, { size: 12 }), h('span', null, t('drawer.details.revealWarning')))
|
|
4589
|
+
: null,
|
|
4490
4590
|
h(
|
|
4491
4591
|
'div',
|
|
4492
4592
|
{ className: 'ssh-credentialRow' },
|
|
@@ -4508,7 +4608,7 @@ window.__ModuleLoader__.load({
|
|
|
4508
4608
|
host.passwordConfigured
|
|
4509
4609
|
? h(
|
|
4510
4610
|
Button,
|
|
4511
|
-
{ size: 'sm', variant: 'dangerGhost', onClick: () => void removePassword(host) },
|
|
4611
|
+
{ size: 'sm', variant: 'dangerGhost', onClick: () => { hideRevealed(); void removePassword(host) } },
|
|
4512
4612
|
t('drawer.details.clearPassword'),
|
|
4513
4613
|
)
|
|
4514
4614
|
: null,
|
|
@@ -5627,6 +5727,9 @@ body.ssh-col-resizing,body.ssh-col-resizing *{cursor:col-resize!important;user-s
|
|
|
5627
5727
|
.ssh-kvValue{color:var(--ssh-text-2);min-width:0;word-break:break-word}
|
|
5628
5728
|
.ssh-kvValue.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px}
|
|
5629
5729
|
.ssh-sectionHint{font-size:11.5px;color:var(--ssh-text-3);line-height:1.65}
|
|
5730
|
+
.ssh-credentialReveal{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:2px}
|
|
5731
|
+
.ssh-revealValue{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;padding:5px 8px;border:.5px solid var(--ssh-border-strong);border-radius:6px;background:var(--ssh-layer-3);color:var(--ssh-text);word-break:break-all;user-select:all}
|
|
5732
|
+
.ssh-revealWarning{display:flex;align-items:flex-start;gap:5px;color:var(--ssh-warn)}
|
|
5630
5733
|
.ssh-tags{display:flex;gap:5px;flex-wrap:wrap}
|
|
5631
5734
|
.ssh-detailsActions{display:flex;gap:8px;padding-top:2px}
|
|
5632
5735
|
.ssh-credentialRow{display:flex;align-items:center;gap:6px}
|
package/index.js
CHANGED
|
@@ -167,6 +167,12 @@ export async function runAction(paths, action, params = {}) {
|
|
|
167
167
|
}
|
|
168
168
|
case 'credential/delete':
|
|
169
169
|
return await removePassword(paths, requireString(params, 'host'))
|
|
170
|
+
case 'credential/reveal': {
|
|
171
|
+
const host = await findHost(paths, params.host)
|
|
172
|
+
const password = await getPassword(paths, host.host)
|
|
173
|
+
if (!password) throw new Error('No password is stored for this host')
|
|
174
|
+
return { password }
|
|
175
|
+
}
|
|
170
176
|
case 'host/metadata': {
|
|
171
177
|
const alias = requireString(params, 'host')
|
|
172
178
|
await findHost(paths, alias)
|
package/lib/ssh-client.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
-
import {
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
3
|
import { verifyHostKey } from './known-hosts.js'
|
|
4
4
|
|
|
5
|
+
// DSH Host bundles/loads plugins in a variety of Node ESM/CJS interop modes.
|
|
6
|
+
// Resolve the CJS ssh2 export explicitly so `Client` is always a constructor.
|
|
7
|
+
const require = createRequire(import.meta.url)
|
|
8
|
+
const ssh2 = require('ssh2')
|
|
9
|
+
const Client = ssh2.Client ?? ssh2.default?.Client
|
|
10
|
+
if (typeof Client !== 'function') {
|
|
11
|
+
throw new Error('ssh2 is unavailable: Client export is missing from the installed ssh2 module')
|
|
12
|
+
}
|
|
13
|
+
|
|
5
14
|
const DEFAULT_TIMEOUT = 15_000
|
|
6
15
|
const MAX_OUTPUT_BYTES = 1024 * 1024
|
|
7
16
|
const MAX_FILE_BYTES = 10 * 1024 * 1024
|