@linxin666/dsh-ssh 0.2.5 → 0.2.6
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.i18n.yaml +2 -2
- package/README.md +3 -2
- package/README.zh.md +3 -2
- package/lib/client.js +70 -27
- package/lib/client.js.map +1 -1
- package/lib/index.js +41 -9
- package/lib/types/client/locales.d.ts +4 -1
- package/lib/types/engine/connection-pool.d.ts +2 -0
- package/lib/types/index.d.ts +1 -1
- package/lib/types/protocol.d.ts +7 -1
- package/lib/types/store.d.ts +2 -0
- package/package.json +1 -1
- package/src/client/locales.ts +8 -2
- package/src/client/panel/HostFormDialog.tsx +18 -2
- package/src/client/panel/HostsTab.tsx +1 -1
- package/src/client/panel/panel.module.css +5 -0
- package/src/engine/connection-pool.ts +17 -1
- package/src/index.ts +1 -1
- package/src/protocol.ts +7 -1
- package/src/store.ts +33 -3
- package/src/tools.ts +1 -1
package/lib/index.js
CHANGED
|
@@ -1050,9 +1050,10 @@ function validateHostPayload(payload) {
|
|
|
1050
1050
|
const auth = p.auth;
|
|
1051
1051
|
if (auth !== void 0) {
|
|
1052
1052
|
if (typeof auth !== "object" || auth === null) return "auth must be an object";
|
|
1053
|
-
if (auth.kind !== "key" && auth.kind !== "password") return "auth.kind must be key or
|
|
1053
|
+
if (auth.kind !== "key" && auth.kind !== "password" && auth.kind !== "agent") return "auth.kind must be key, password or agent";
|
|
1054
1054
|
if (auth.kind === "key" && (typeof auth.keyPath !== "string" || auth.keyPath.trim() === "")) return "auth.keyPath is required for key auth";
|
|
1055
1055
|
if (auth.kind === "password" && auth.password !== void 0 && typeof auth.password !== "string") return "auth.password must be a string when provided";
|
|
1056
|
+
if (auth.kind === "agent" && auth.agentPath !== void 0 && typeof auth.agentPath !== "string") return "auth.agentPath must be a string when provided";
|
|
1056
1057
|
}
|
|
1057
1058
|
if (p.port !== void 0 && (typeof p.port !== "number" || !Number.isInteger(p.port) || p.port < 1 || p.port > 65535)) return "port must be an integer in 1..65535";
|
|
1058
1059
|
if (p.proxyJump !== void 0 && (!Array.isArray(p.proxyJump) || p.proxyJump.some((x) => typeof x !== "string" || x === ""))) return "proxyJump must be an array of alias strings";
|
|
@@ -1092,6 +1093,7 @@ var HostStore = class {
|
|
|
1092
1093
|
summarize(entry) {
|
|
1093
1094
|
let keyReady = true;
|
|
1094
1095
|
if (entry.auth.kind === "key" && entry.auth.keyPath) keyReady = existsSync(expandHome(entry.auth.keyPath));
|
|
1096
|
+
else if (entry.auth.kind === "agent") keyReady = false;
|
|
1095
1097
|
return {
|
|
1096
1098
|
alias: entry.alias,
|
|
1097
1099
|
host: entry.host,
|
|
@@ -1129,7 +1131,8 @@ var HostStore = class {
|
|
|
1129
1131
|
kind: payload.auth.kind,
|
|
1130
1132
|
keyPath: payload.auth.kind === "key" ? expandHome(payload.auth.keyPath?.trim() ?? "") : void 0,
|
|
1131
1133
|
passphrase: payload.auth.kind === "key" ? payload.auth.passphrase ?? void 0 : void 0,
|
|
1132
|
-
password: payload.auth.kind === "password" ? payload.auth.password : void 0
|
|
1134
|
+
password: payload.auth.kind === "password" ? payload.auth.password : void 0,
|
|
1135
|
+
agentPath: payload.auth.kind === "agent" ? normalizeAgentPath(payload.auth.agentPath) : void 0
|
|
1133
1136
|
},
|
|
1134
1137
|
proxyJump: [...payload.proxyJump ?? []],
|
|
1135
1138
|
description: payload.description?.trim() || void 0,
|
|
@@ -1158,15 +1161,17 @@ var HostStore = class {
|
|
|
1158
1161
|
if (patch.user !== void 0) entry.user = patch.user.trim();
|
|
1159
1162
|
if (patch.auth !== void 0) {
|
|
1160
1163
|
const auth = patch.auth;
|
|
1161
|
-
if (auth.kind !== "key" && auth.kind !== "password") throw new Error("auth.kind must be key or
|
|
1164
|
+
if (auth.kind !== "key" && auth.kind !== "password" && auth.kind !== "agent") throw new Error("auth.kind must be key, password or agent");
|
|
1162
1165
|
if (auth.kind === "key" && (typeof auth.keyPath !== "string" || auth.keyPath.trim() === "")) throw new Error("auth.keyPath is required for key auth");
|
|
1163
1166
|
if (auth.kind === "password" && auth.password !== void 0 && typeof auth.password !== "string") throw new Error("auth.password must be a string when provided");
|
|
1167
|
+
if (auth.kind === "agent" && auth.agentPath !== void 0 && typeof auth.agentPath !== "string") throw new Error("auth.agentPath must be a string when provided");
|
|
1164
1168
|
const keyChanged = auth.kind === "key" && auth.keyPath !== void 0 && expandHome(auth.keyPath.trim()) !== entry.auth.keyPath;
|
|
1165
1169
|
entry.auth = {
|
|
1166
1170
|
kind: auth.kind,
|
|
1167
1171
|
keyPath: auth.kind === "key" ? expandHome(auth.keyPath?.trim() ?? "") : void 0,
|
|
1168
1172
|
passphrase: auth.kind === "key" ? auth.passphrase !== void 0 ? auth.passphrase : keyChanged ? void 0 : entry.auth.passphrase : void 0,
|
|
1169
|
-
password: auth.kind === "password" ? auth.password : void 0
|
|
1173
|
+
password: auth.kind === "password" ? auth.password : void 0,
|
|
1174
|
+
agentPath: auth.kind === "agent" ? auth.agentPath !== void 0 ? normalizeAgentPath(auth.agentPath) : entry.auth.agentPath : void 0
|
|
1170
1175
|
};
|
|
1171
1176
|
}
|
|
1172
1177
|
if (patch.proxyJump !== void 0) entry.proxyJump = [...patch.proxyJump];
|
|
@@ -1247,9 +1252,10 @@ var HostStore = class {
|
|
|
1247
1252
|
port: block.props.port !== void 0 ? Number.parseInt(block.props.port, 10) : 22,
|
|
1248
1253
|
user: block.props.user ?? process.env.USER ?? "root",
|
|
1249
1254
|
auth: {
|
|
1250
|
-
kind: block.props.identityfile !== void 0 ? "key" : "password",
|
|
1255
|
+
kind: block.props.identityfile !== void 0 ? "key" : block.props.identityagent !== void 0 && block.props.identityagent.toLowerCase() !== "none" ? "agent" : "password",
|
|
1251
1256
|
keyPath: block.props.identityfile,
|
|
1252
|
-
password: block.props.password
|
|
1257
|
+
password: block.props.password,
|
|
1258
|
+
agentPath: block.props.identityagent !== void 0 && block.props.identityagent.toLowerCase() !== "none" ? normalizeAgentPath(block.props.identityagent) : void 0
|
|
1253
1259
|
},
|
|
1254
1260
|
proxyJump: block.props.proxyjump !== void 0 ? block.props.proxyjump.split(",").map((hop) => hop.trim()).filter((hop) => hop !== "") : [],
|
|
1255
1261
|
description: block.props.description,
|
|
@@ -1325,6 +1331,16 @@ var HostStore = class {
|
|
|
1325
1331
|
this.cache = void 0;
|
|
1326
1332
|
}
|
|
1327
1333
|
};
|
|
1334
|
+
/** Normalize an agent endpoint for storage: trim, expand `~`, and resolve the SSH_AUTH_SOCK token. */
|
|
1335
|
+
function normalizeAgentPath(agentPath) {
|
|
1336
|
+
const trimmed = agentPath?.trim();
|
|
1337
|
+
if (trimmed === void 0 || trimmed === "") return void 0;
|
|
1338
|
+
if (trimmed === "SSH_AUTH_SOCK" || trimmed === "$SSH_AUTH_SOCK") {
|
|
1339
|
+
const sock = process.env.SSH_AUTH_SOCK;
|
|
1340
|
+
return sock !== void 0 && sock !== "" ? sock : void 0;
|
|
1341
|
+
}
|
|
1342
|
+
return expandHome(trimmed);
|
|
1343
|
+
}
|
|
1328
1344
|
/** Expand a leading `~` in a filesystem path. */
|
|
1329
1345
|
function expandHome(path) {
|
|
1330
1346
|
if (path === "~") return homedir();
|
|
@@ -1359,7 +1375,11 @@ function buildConnectConfig(entry, sock, opts) {
|
|
|
1359
1375
|
};
|
|
1360
1376
|
if (sock !== void 0) config.sock = sock;
|
|
1361
1377
|
if (entry.auth.kind === "password") config.password = entry.auth.password;
|
|
1362
|
-
else {
|
|
1378
|
+
else if (entry.auth.kind === "agent") {
|
|
1379
|
+
const agentPath = resolveAgentPath(entry.auth.agentPath);
|
|
1380
|
+
if (agentPath === void 0) throw new Error("ssh-agent is not available: set SSH_AUTH_SOCK or configure an agent path (use 'pageant' for PuTTY Pageant on Windows)");
|
|
1381
|
+
config.agent = agentPath;
|
|
1382
|
+
} else {
|
|
1363
1383
|
const keyPath = entry.auth.keyPath === void 0 ? void 0 : expandHome(entry.auth.keyPath);
|
|
1364
1384
|
if (keyPath === void 0 || !existsSync(keyPath)) throw new Error("private key not found: " + (entry.auth.keyPath ?? "(unset)"));
|
|
1365
1385
|
config.privateKey = readFileSync(keyPath, "utf8");
|
|
@@ -1367,6 +1387,14 @@ function buildConnectConfig(entry, sock, opts) {
|
|
|
1367
1387
|
}
|
|
1368
1388
|
return config;
|
|
1369
1389
|
}
|
|
1390
|
+
/** Resolve the ssh2 agent path for 'agent' auth. */
|
|
1391
|
+
function resolveAgentPath(agentPath) {
|
|
1392
|
+
const explicit = normalizeAgentPath(agentPath);
|
|
1393
|
+
if (explicit !== void 0) return explicit;
|
|
1394
|
+
const sock = process.env.SSH_AUTH_SOCK;
|
|
1395
|
+
if (sock !== void 0 && sock !== "") return sock;
|
|
1396
|
+
if (process.platform === "win32") return "pageant";
|
|
1397
|
+
}
|
|
1370
1398
|
/** Connect one ssh2 client (resolve on ready, reject on error/close). */
|
|
1371
1399
|
function connectClient(config) {
|
|
1372
1400
|
return new Promise((resolve, reject) => {
|
|
@@ -2875,7 +2903,11 @@ function sshListTool(engine) {
|
|
|
2875
2903
|
},
|
|
2876
2904
|
auth: {
|
|
2877
2905
|
type: "string",
|
|
2878
|
-
enum: [
|
|
2906
|
+
enum: [
|
|
2907
|
+
"key",
|
|
2908
|
+
"password",
|
|
2909
|
+
"agent"
|
|
2910
|
+
],
|
|
2879
2911
|
required: true
|
|
2880
2912
|
},
|
|
2881
2913
|
keyReady: {
|
|
@@ -3425,7 +3457,7 @@ const DEFAULT_ANNOUNCE = true;
|
|
|
3425
3457
|
/** Order of the announcement section within the tool-guidance band. */
|
|
3426
3458
|
const SECTION_ORDER = 150;
|
|
3427
3459
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
3428
|
-
const SSH_GUIDANCE = "本机已安装 dsh-ssh 插件(DSH 远程 SSH 运维):侧边栏「SSH」入口;在 dsh-web-ui 插件全家桶仓库(packages/dsh-ssh)统一维护。能力:主机配置存 ~/.dsh/dsh-ssh.json(可从 ~/.ssh/config 导入);持久连接池复用长连接(空闲 30 分钟自动断开);ssh_list 列出主机、ssh_exec 执行远程命令、ssh_upload/ssh_download 传输文件、ssh_tunnel 本地端口转发(访问远程数据库/内网服务)、ssh_cluster
|
|
3460
|
+
const SSH_GUIDANCE = "本机已安装 dsh-ssh 插件(DSH 远程 SSH 运维):侧边栏「SSH」入口;在 dsh-web-ui 插件全家桶仓库(packages/dsh-ssh)统一维护。能力:主机配置存 ~/.dsh/dsh-ssh.json(可从 ~/.ssh/config 导入);持久连接池复用长连接(空闲 30 分钟自动断开);ssh_list 列出主机、ssh_exec 执行远程命令、ssh_upload/ssh_download 传输文件、ssh_tunnel 本地端口转发(访问远程数据库/内网服务)、ssh_cluster 集群并发执行;支持密钥/密码/ssh-agent 认证、passphrase 密钥与 ProxyJump 跳板机;Web 终端走 WebSocket。限制:主机操作由用户在 GUI 中配置后 agent 方可使用;密码以明文存在用户主目录私有文件(权限 0600);命令输出原样返回、可能含敏感信息;断线重连可能重放非幂等命令;传输/执行消耗真实远程资源,先确认再操作。用户提到「SSH / 远程服务器 / 服务器操作 / 跳板机 / 隧道 / 部署 / 上传下载」时即指本插件,请据此协作。";
|
|
3429
3461
|
/**
|
|
3430
3462
|
* Mount the SSH engine, routes, tools, and announcement.
|
|
3431
3463
|
* @param ctx - host plugin context carrying webServer/tools/systemPrompt.
|
|
@@ -51,7 +51,10 @@ export declare const zh: {
|
|
|
51
51
|
readonly 'form.auth': "认证方式";
|
|
52
52
|
readonly 'form.auth.key': "密钥";
|
|
53
53
|
readonly 'form.auth.password': "密码";
|
|
54
|
-
readonly 'form.
|
|
54
|
+
readonly 'form.auth.agent': "SSH Agent";
|
|
55
|
+
readonly 'form.agentPath': "Agent 路径";
|
|
56
|
+
readonly 'form.agentPathHint': "留空自动检测(SSH_AUTH_SOCK / Windows Pageant);也可填 socket 路径或 pageant";
|
|
57
|
+
readonly 'form.authKeepHint': "编辑时密钥/密码/Agent 留空将保留原认证方式与已存凭据;如需更换认证方式请填写新凭据。";
|
|
55
58
|
readonly 'form.keyPath': "私钥路径";
|
|
56
59
|
readonly 'form.keyPathHint': "如 ~/.ssh/id_ed25519";
|
|
57
60
|
readonly 'form.passphrase': "密钥口令";
|
|
@@ -48,6 +48,8 @@ export interface PoolEngine {
|
|
|
48
48
|
}
|
|
49
49
|
/** Build the ssh2 connect config for one entry (key read from disk). */
|
|
50
50
|
export declare function buildConnectConfig(entry: SshHostEntry, sock: ConnectConfig['sock'] | undefined, opts: Required<EngineOptions>): ConnectConfig;
|
|
51
|
+
/** Resolve the ssh2 agent path for 'agent' auth. */
|
|
52
|
+
export declare function resolveAgentPath(agentPath?: string): string | undefined;
|
|
51
53
|
/** Connect one ssh2 client (resolve on ready, reject on error/close). */
|
|
52
54
|
export declare function connectClient(config: ConnectConfig): Promise<Client>;
|
|
53
55
|
/** Cap captured output at the configured byte budget (marks truncation). */
|
package/lib/types/index.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export interface Config {
|
|
|
38
38
|
}
|
|
39
39
|
export declare const Config: z<Config>;
|
|
40
40
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
41
|
-
export declare const SSH_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-ssh \u63D2\u4EF6\uFF08DSH \u8FDC\u7A0B SSH \u8FD0\u7EF4\uFF09\uFF1A\u4FA7\u8FB9\u680F\u300CSSH\u300D\u5165\u53E3\uFF1B\u5728 dsh-web-ui \u63D2\u4EF6\u5168\u5BB6\u6876\u4ED3\u5E93\uFF08packages/dsh-ssh\uFF09\u7EDF\u4E00\u7EF4\u62A4\u3002\u80FD\u529B\uFF1A\u4E3B\u673A\u914D\u7F6E\u5B58 ~/.dsh/dsh-ssh.json\uFF08\u53EF\u4ECE ~/.ssh/config \u5BFC\u5165\uFF09\uFF1B\u6301\u4E45\u8FDE\u63A5\u6C60\u590D\u7528\u957F\u8FDE\u63A5\uFF08\u7A7A\u95F2 30 \u5206\u949F\u81EA\u52A8\u65AD\u5F00\uFF09\uFF1Bssh_list \u5217\u51FA\u4E3B\u673A\u3001ssh_exec \u6267\u884C\u8FDC\u7A0B\u547D\u4EE4\u3001ssh_upload/ssh_download \u4F20\u8F93\u6587\u4EF6\u3001ssh_tunnel \u672C\u5730\u7AEF\u53E3\u8F6C\u53D1\uFF08\u8BBF\u95EE\u8FDC\u7A0B\u6570\u636E\u5E93/\u5185\u7F51\u670D\u52A1\uFF09\u3001ssh_cluster \u96C6\u7FA4\u5E76\u53D1\u6267\u884C\uFF1B\u652F\u6301\u5BC6\u94A5/\u5BC6\u7801\u8BA4\u8BC1\u3001passphrase \u5BC6\u94A5\u4E0E ProxyJump \u8DF3\u677F\u673A\uFF1BWeb \u7EC8\u7AEF\u8D70 WebSocket\u3002\u9650\u5236\uFF1A\u4E3B\u673A\u64CD\u4F5C\u7531\u7528\u6237\u5728 GUI \u4E2D\u914D\u7F6E\u540E agent \u65B9\u53EF\u4F7F\u7528\uFF1B\u5BC6\u7801\u4EE5\u660E\u6587\u5B58\u5728\u7528\u6237\u4E3B\u76EE\u5F55\u79C1\u6709\u6587\u4EF6\uFF08\u6743\u9650 0600\uFF09\uFF1B\u547D\u4EE4\u8F93\u51FA\u539F\u6837\u8FD4\u56DE\u3001\u53EF\u80FD\u542B\u654F\u611F\u4FE1\u606F\uFF1B\u65AD\u7EBF\u91CD\u8FDE\u53EF\u80FD\u91CD\u653E\u975E\u5E42\u7B49\u547D\u4EE4\uFF1B\u4F20\u8F93/\u6267\u884C\u6D88\u8017\u771F\u5B9E\u8FDC\u7A0B\u8D44\u6E90\uFF0C\u5148\u786E\u8BA4\u518D\u64CD\u4F5C\u3002\u7528\u6237\u63D0\u5230\u300CSSH / \u8FDC\u7A0B\u670D\u52A1\u5668 / \u670D\u52A1\u5668\u64CD\u4F5C / \u8DF3\u677F\u673A / \u96A7\u9053 / \u90E8\u7F72 / \u4E0A\u4F20\u4E0B\u8F7D\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\uFF0C\u8BF7\u636E\u6B64\u534F\u4F5C\u3002";
|
|
41
|
+
export declare const SSH_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-ssh \u63D2\u4EF6\uFF08DSH \u8FDC\u7A0B SSH \u8FD0\u7EF4\uFF09\uFF1A\u4FA7\u8FB9\u680F\u300CSSH\u300D\u5165\u53E3\uFF1B\u5728 dsh-web-ui \u63D2\u4EF6\u5168\u5BB6\u6876\u4ED3\u5E93\uFF08packages/dsh-ssh\uFF09\u7EDF\u4E00\u7EF4\u62A4\u3002\u80FD\u529B\uFF1A\u4E3B\u673A\u914D\u7F6E\u5B58 ~/.dsh/dsh-ssh.json\uFF08\u53EF\u4ECE ~/.ssh/config \u5BFC\u5165\uFF09\uFF1B\u6301\u4E45\u8FDE\u63A5\u6C60\u590D\u7528\u957F\u8FDE\u63A5\uFF08\u7A7A\u95F2 30 \u5206\u949F\u81EA\u52A8\u65AD\u5F00\uFF09\uFF1Bssh_list \u5217\u51FA\u4E3B\u673A\u3001ssh_exec \u6267\u884C\u8FDC\u7A0B\u547D\u4EE4\u3001ssh_upload/ssh_download \u4F20\u8F93\u6587\u4EF6\u3001ssh_tunnel \u672C\u5730\u7AEF\u53E3\u8F6C\u53D1\uFF08\u8BBF\u95EE\u8FDC\u7A0B\u6570\u636E\u5E93/\u5185\u7F51\u670D\u52A1\uFF09\u3001ssh_cluster \u96C6\u7FA4\u5E76\u53D1\u6267\u884C\uFF1B\u652F\u6301\u5BC6\u94A5/\u5BC6\u7801/ssh-agent \u8BA4\u8BC1\u3001passphrase \u5BC6\u94A5\u4E0E ProxyJump \u8DF3\u677F\u673A\uFF1BWeb \u7EC8\u7AEF\u8D70 WebSocket\u3002\u9650\u5236\uFF1A\u4E3B\u673A\u64CD\u4F5C\u7531\u7528\u6237\u5728 GUI \u4E2D\u914D\u7F6E\u540E agent \u65B9\u53EF\u4F7F\u7528\uFF1B\u5BC6\u7801\u4EE5\u660E\u6587\u5B58\u5728\u7528\u6237\u4E3B\u76EE\u5F55\u79C1\u6709\u6587\u4EF6\uFF08\u6743\u9650 0600\uFF09\uFF1B\u547D\u4EE4\u8F93\u51FA\u539F\u6837\u8FD4\u56DE\u3001\u53EF\u80FD\u542B\u654F\u611F\u4FE1\u606F\uFF1B\u65AD\u7EBF\u91CD\u8FDE\u53EF\u80FD\u91CD\u653E\u975E\u5E42\u7B49\u547D\u4EE4\uFF1B\u4F20\u8F93/\u6267\u884C\u6D88\u8017\u771F\u5B9E\u8FDC\u7A0B\u8D44\u6E90\uFF0C\u5148\u786E\u8BA4\u518D\u64CD\u4F5C\u3002\u7528\u6237\u63D0\u5230\u300CSSH / \u8FDC\u7A0B\u670D\u52A1\u5668 / \u670D\u52A1\u5668\u64CD\u4F5C / \u8DF3\u677F\u673A / \u96A7\u9053 / \u90E8\u7F72 / \u4E0A\u4F20\u4E0B\u8F7D\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\uFF0C\u8BF7\u636E\u6B64\u534F\u4F5C\u3002";
|
|
42
42
|
/**
|
|
43
43
|
* Mount the SSH engine, routes, tools, and announcement.
|
|
44
44
|
* @param ctx - host plugin context carrying webServer/tools/systemPrompt.
|
package/lib/types/protocol.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* each, no runtime identity to share.
|
|
5
5
|
*/
|
|
6
6
|
/** Authentication flavors a host entry may carry. */
|
|
7
|
-
export type SshAuthKind = 'key' | 'password';
|
|
7
|
+
export type SshAuthKind = 'key' | 'password' | 'agent';
|
|
8
8
|
/** One stored host entry (the ~/.dsh/dsh-ssh.json store shape). */
|
|
9
9
|
export interface SshHostEntry {
|
|
10
10
|
/** Stable, user-chosen identifier used by every operation. */
|
|
@@ -24,6 +24,12 @@ export interface SshHostEntry {
|
|
|
24
24
|
passphrase?: string;
|
|
25
25
|
/** Password for 'password' auth. */
|
|
26
26
|
password?: string;
|
|
27
|
+
/**
|
|
28
|
+
* ssh-agent endpoint for 'agent' auth. A socket path, the special value
|
|
29
|
+
* 'pageant' (PuTTY Pageant on Windows), or undefined for auto-detect
|
|
30
|
+
* (SSH_AUTH_SOCK, then Pageant on Windows).
|
|
31
|
+
*/
|
|
32
|
+
agentPath?: string;
|
|
27
33
|
};
|
|
28
34
|
/** Jump chain: local aliases connected through in order (ProxyJump). */
|
|
29
35
|
proxyJump: string[];
|
package/lib/types/store.d.ts
CHANGED
|
@@ -56,5 +56,7 @@ export declare class HostStore {
|
|
|
56
56
|
private load;
|
|
57
57
|
private save;
|
|
58
58
|
}
|
|
59
|
+
/** Normalize an agent endpoint for storage: trim, expand `~`, and resolve the SSH_AUTH_SOCK token. */
|
|
60
|
+
export declare function normalizeAgentPath(agentPath: string | undefined): string | undefined;
|
|
59
61
|
/** Expand a leading `~` in a filesystem path. */
|
|
60
62
|
export declare function expandHome(path: string): string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-ssh",
|
|
3
3
|
"description": "Remote SSH operations for the dsh web GUI: host config store (~/.dsh/dsh-ssh.json, import from ~/.ssh/config), a persistent ssh2 connection pool with jump-host support, exec / PTY web terminal / SFTP transfer / local port-forward tunnels / cluster execution, plus agent tools (ssh_list, ssh_exec, ssh_upload, ssh_download, ssh_tunnel, ssh_cluster). Hot-pluggable — mounted via ~/.dsh/cordis.patch.yml + a profile node_modules symlink, no dsh source changes.",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.6",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
package/src/client/locales.ts
CHANGED
|
@@ -54,7 +54,10 @@ export const zh = {
|
|
|
54
54
|
'form.auth': '认证方式',
|
|
55
55
|
'form.auth.key': '密钥',
|
|
56
56
|
'form.auth.password': '密码',
|
|
57
|
-
'form.
|
|
57
|
+
'form.auth.agent': 'SSH Agent',
|
|
58
|
+
'form.agentPath': 'Agent 路径',
|
|
59
|
+
'form.agentPathHint': '留空自动检测(SSH_AUTH_SOCK / Windows Pageant);也可填 socket 路径或 pageant',
|
|
60
|
+
'form.authKeepHint': '编辑时密钥/密码/Agent 留空将保留原认证方式与已存凭据;如需更换认证方式请填写新凭据。',
|
|
58
61
|
'form.keyPath': '私钥路径',
|
|
59
62
|
'form.keyPathHint': '如 ~/.ssh/id_ed25519',
|
|
60
63
|
'form.passphrase': '密钥口令',
|
|
@@ -198,7 +201,10 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
198
201
|
'form.auth': 'Authentication',
|
|
199
202
|
'form.auth.key': 'Key',
|
|
200
203
|
'form.auth.password': 'Password',
|
|
201
|
-
'form.
|
|
204
|
+
'form.auth.agent': 'SSH Agent',
|
|
205
|
+
'form.agentPath': 'Agent path',
|
|
206
|
+
'form.agentPathHint': 'Leave empty to auto-detect (SSH_AUTH_SOCK / Pageant on Windows), or set a socket path or "pageant"',
|
|
207
|
+
'form.authKeepHint': 'While editing, empty secret fields keep the stored authentication and credentials; enter new values to switch methods.',
|
|
202
208
|
'form.keyPath': 'Private key path',
|
|
203
209
|
'form.keyPathHint': 'e.g. ~/.ssh/id_ed25519',
|
|
204
210
|
'form.passphrase': 'Key passphrase',
|
|
@@ -29,6 +29,7 @@ interface FormState {
|
|
|
29
29
|
keyPath: string
|
|
30
30
|
passphrase: string
|
|
31
31
|
password: string
|
|
32
|
+
agentPath: string
|
|
32
33
|
proxyJump: string
|
|
33
34
|
description: string
|
|
34
35
|
environment: string
|
|
@@ -52,6 +53,7 @@ function blankOf(editing: SshHostSummary | null | undefined): FormState {
|
|
|
52
53
|
keyPath: '',
|
|
53
54
|
passphrase: '',
|
|
54
55
|
password: '',
|
|
56
|
+
agentPath: '',
|
|
55
57
|
proxyJump: (editing?.proxyJump ?? []).join(', '),
|
|
56
58
|
description: editing?.description ?? '',
|
|
57
59
|
environment: editing?.environment ?? '',
|
|
@@ -103,12 +105,16 @@ export function HostFormDialog({ api, editing, onClose, onSaved }: HostFormDialo
|
|
|
103
105
|
// authentication is preserved.
|
|
104
106
|
const secretEmpty = form.authKind === 'password'
|
|
105
107
|
? form.password === ''
|
|
106
|
-
: form.
|
|
108
|
+
: form.authKind === 'key'
|
|
109
|
+
? form.keyPath.trim() === ''
|
|
110
|
+
: form.agentPath.trim() === ''
|
|
107
111
|
const auth: HostPayload['auth'] = editing != null && secretEmpty
|
|
108
112
|
? undefined
|
|
109
113
|
: form.authKind === 'password'
|
|
110
114
|
? { kind: 'password', password: form.password }
|
|
111
|
-
:
|
|
115
|
+
: form.authKind === 'key'
|
|
116
|
+
? { kind: 'key', keyPath: form.keyPath.trim(), passphrase: form.passphrase === '' ? undefined : form.passphrase }
|
|
117
|
+
: { kind: 'agent', agentPath: form.agentPath.trim() === '' ? undefined : form.agentPath.trim() }
|
|
112
118
|
const payload: HostPayload = {
|
|
113
119
|
host,
|
|
114
120
|
port,
|
|
@@ -169,6 +175,10 @@ export function HostFormDialog({ api, editing, onClose, onSaved }: HostFormDialo
|
|
|
169
175
|
<input type="radio" name="dsh-ssh-auth" checked={form.authKind === 'password'} onChange={() => { set('authKind', 'password') }} />
|
|
170
176
|
{tt('form.auth.password')}
|
|
171
177
|
</label>
|
|
178
|
+
<label className={css.radioLabel}>
|
|
179
|
+
<input type="radio" name="dsh-ssh-auth" checked={form.authKind === 'agent'} onChange={() => { set('authKind', 'agent') }} />
|
|
180
|
+
{tt('form.auth.agent')}
|
|
181
|
+
</label>
|
|
172
182
|
</div>
|
|
173
183
|
{editing != null && <span className={css.hint}>{tt('form.authKeepHint')}</span>}
|
|
174
184
|
</div>
|
|
@@ -184,6 +194,12 @@ export function HostFormDialog({ api, editing, onClose, onSaved }: HostFormDialo
|
|
|
184
194
|
<input className={css.input} type="password" value={form.passphrase} onChange={event => { set('passphrase', event.target.value) }} />
|
|
185
195
|
</label>
|
|
186
196
|
</div>
|
|
197
|
+
) : form.authKind === 'agent' ? (
|
|
198
|
+
<label className={css.field}>
|
|
199
|
+
<span className={css.fieldLabel}>{tt('form.agentPath')}</span>
|
|
200
|
+
<input className={css.input} value={form.agentPath} onChange={event => { set('agentPath', event.target.value) }} />
|
|
201
|
+
<span className={css.hint}>{tt('form.agentPathHint')}</span>
|
|
202
|
+
</label>
|
|
187
203
|
) : (
|
|
188
204
|
<label className={css.field}>
|
|
189
205
|
<span className={css.fieldLabel}>{tt('form.password')}</span>
|
|
@@ -157,7 +157,7 @@ export function HostsTab({ api, onConnect }: HostsTabProps) {
|
|
|
157
157
|
<td className={css.mono}>{host.alias}</td>
|
|
158
158
|
<td className={css.mono}>{host.host}:{host.port}</td>
|
|
159
159
|
<td>{host.user}</td>
|
|
160
|
-
<td><span className={css.badge} data-kind={host.auth}>{host.auth === 'key' ? tt('form.auth.key') : tt('form.auth.password')}</span></td>
|
|
160
|
+
<td><span className={css.badge} data-kind={host.auth}>{host.auth === 'key' ? tt('form.auth.key') : host.auth === 'password' ? tt('form.auth.password') : tt('form.auth.agent')}</span></td>
|
|
161
161
|
<td className={css.cellMuted}>{host.environment ?? ''}</td>
|
|
162
162
|
<td className={css.cellMuted}>{host.tags.join(', ')}</td>
|
|
163
163
|
<td className={css.cellMuted}>{host.description ?? ''}</td>
|
|
@@ -414,6 +414,11 @@ html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [class*='centerCol']
|
|
|
414
414
|
border-color: var(--dsw-alias-state-warn-primary);
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
+
.badge[data-kind='agent'] {
|
|
418
|
+
color: var(--dsw-alias-state-success-primary);
|
|
419
|
+
border-color: var(--dsw-alias-state-success-primary);
|
|
420
|
+
}
|
|
421
|
+
|
|
417
422
|
.badge[data-status='ok'] {
|
|
418
423
|
color: var(--dsw-alias-state-success-primary);
|
|
419
424
|
border-color: var(--dsw-alias-state-success-primary);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { existsSync, readFileSync } from 'node:fs'
|
|
7
7
|
import { Client, type ConnectConfig } from 'ssh2'
|
|
8
8
|
import type { ExecResult, SshHostEntry } from '../protocol.ts'
|
|
9
|
-
import { expandHome, type HostStore } from '../store.ts'
|
|
9
|
+
import { expandHome, normalizeAgentPath, type HostStore } from '../store.ts'
|
|
10
10
|
|
|
11
11
|
/** Default engine knobs. */
|
|
12
12
|
export interface EngineOptions {
|
|
@@ -74,6 +74,12 @@ export function buildConnectConfig(entry: SshHostEntry, sock: ConnectConfig['soc
|
|
|
74
74
|
if (sock !== undefined) config.sock = sock
|
|
75
75
|
if (entry.auth.kind === 'password') {
|
|
76
76
|
config.password = entry.auth.password
|
|
77
|
+
} else if (entry.auth.kind === 'agent') {
|
|
78
|
+
const agentPath = resolveAgentPath(entry.auth.agentPath)
|
|
79
|
+
if (agentPath === undefined) {
|
|
80
|
+
throw new Error('ssh-agent is not available: set SSH_AUTH_SOCK or configure an agent path (use \'pageant\' for PuTTY Pageant on Windows)')
|
|
81
|
+
}
|
|
82
|
+
config.agent = agentPath
|
|
77
83
|
} else {
|
|
78
84
|
const keyPath = entry.auth.keyPath === undefined ? undefined : expandHome(entry.auth.keyPath)
|
|
79
85
|
if (keyPath === undefined || !existsSync(keyPath)) {
|
|
@@ -87,6 +93,16 @@ export function buildConnectConfig(entry: SshHostEntry, sock: ConnectConfig['soc
|
|
|
87
93
|
return config
|
|
88
94
|
}
|
|
89
95
|
|
|
96
|
+
/** Resolve the ssh2 agent path for 'agent' auth. */
|
|
97
|
+
export function resolveAgentPath(agentPath?: string): string | undefined {
|
|
98
|
+
const explicit = normalizeAgentPath(agentPath)
|
|
99
|
+
if (explicit !== undefined) return explicit
|
|
100
|
+
const sock = process.env.SSH_AUTH_SOCK
|
|
101
|
+
if (sock !== undefined && sock !== '') return sock
|
|
102
|
+
if (process.platform === 'win32') return 'pageant'
|
|
103
|
+
return undefined
|
|
104
|
+
}
|
|
105
|
+
|
|
90
106
|
/** Connect one ssh2 client (resolve on ready, reject on error/close). */
|
|
91
107
|
export function connectClient(config: ConnectConfig): Promise<Client> {
|
|
92
108
|
return new Promise((resolve, reject) => {
|
package/src/index.ts
CHANGED
|
@@ -64,7 +64,7 @@ const DEFAULT_ANNOUNCE = true
|
|
|
64
64
|
const SECTION_ORDER = 150
|
|
65
65
|
|
|
66
66
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
67
|
-
export const SSH_GUIDANCE = '本机已安装 dsh-ssh 插件(DSH 远程 SSH 运维):侧边栏「SSH」入口;在 dsh-web-ui 插件全家桶仓库(packages/dsh-ssh)统一维护。能力:主机配置存 ~/.dsh/dsh-ssh.json(可从 ~/.ssh/config 导入);持久连接池复用长连接(空闲 30 分钟自动断开);ssh_list 列出主机、ssh_exec 执行远程命令、ssh_upload/ssh_download 传输文件、ssh_tunnel 本地端口转发(访问远程数据库/内网服务)、ssh_cluster
|
|
67
|
+
export const SSH_GUIDANCE = '本机已安装 dsh-ssh 插件(DSH 远程 SSH 运维):侧边栏「SSH」入口;在 dsh-web-ui 插件全家桶仓库(packages/dsh-ssh)统一维护。能力:主机配置存 ~/.dsh/dsh-ssh.json(可从 ~/.ssh/config 导入);持久连接池复用长连接(空闲 30 分钟自动断开);ssh_list 列出主机、ssh_exec 执行远程命令、ssh_upload/ssh_download 传输文件、ssh_tunnel 本地端口转发(访问远程数据库/内网服务)、ssh_cluster 集群并发执行;支持密钥/密码/ssh-agent 认证、passphrase 密钥与 ProxyJump 跳板机;Web 终端走 WebSocket。限制:主机操作由用户在 GUI 中配置后 agent 方可使用;密码以明文存在用户主目录私有文件(权限 0600);命令输出原样返回、可能含敏感信息;断线重连可能重放非幂等命令;传输/执行消耗真实远程资源,先确认再操作。用户提到「SSH / 远程服务器 / 服务器操作 / 跳板机 / 隧道 / 部署 / 上传下载」时即指本插件,请据此协作。'
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* Mount the SSH engine, routes, tools, and announcement.
|
package/src/protocol.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/** Authentication flavors a host entry may carry. */
|
|
8
|
-
export type SshAuthKind = 'key' | 'password'
|
|
8
|
+
export type SshAuthKind = 'key' | 'password' | 'agent'
|
|
9
9
|
|
|
10
10
|
/** One stored host entry (the ~/.dsh/dsh-ssh.json store shape). */
|
|
11
11
|
export interface SshHostEntry {
|
|
@@ -26,6 +26,12 @@ export interface SshHostEntry {
|
|
|
26
26
|
passphrase?: string
|
|
27
27
|
/** Password for 'password' auth. */
|
|
28
28
|
password?: string
|
|
29
|
+
/**
|
|
30
|
+
* ssh-agent endpoint for 'agent' auth. A socket path, the special value
|
|
31
|
+
* 'pageant' (PuTTY Pageant on Windows), or undefined for auto-detect
|
|
32
|
+
* (SSH_AUTH_SOCK, then Pageant on Windows).
|
|
33
|
+
*/
|
|
34
|
+
agentPath?: string
|
|
29
35
|
}
|
|
30
36
|
/** Jump chain: local aliases connected through in order (ProxyJump). */
|
|
31
37
|
proxyJump: string[]
|
package/src/store.ts
CHANGED
|
@@ -38,13 +38,16 @@ export function validateHostPayload(payload: unknown): string | undefined {
|
|
|
38
38
|
const auth = p.auth as Record<string, unknown> | undefined
|
|
39
39
|
if (auth !== undefined) {
|
|
40
40
|
if (typeof auth !== 'object' || auth === null) return 'auth must be an object'
|
|
41
|
-
if (auth.kind !== 'key' && auth.kind !== 'password') return 'auth.kind must be key or
|
|
41
|
+
if (auth.kind !== 'key' && auth.kind !== 'password' && auth.kind !== 'agent') return 'auth.kind must be key, password or agent'
|
|
42
42
|
if (auth.kind === 'key' && (typeof auth.keyPath !== 'string' || auth.keyPath.trim() === '')) {
|
|
43
43
|
return 'auth.keyPath is required for key auth'
|
|
44
44
|
}
|
|
45
45
|
if (auth.kind === 'password' && auth.password !== undefined && typeof auth.password !== 'string') {
|
|
46
46
|
return 'auth.password must be a string when provided'
|
|
47
47
|
}
|
|
48
|
+
if (auth.kind === 'agent' && auth.agentPath !== undefined && typeof auth.agentPath !== 'string') {
|
|
49
|
+
return 'auth.agentPath must be a string when provided'
|
|
50
|
+
}
|
|
48
51
|
}
|
|
49
52
|
if (p.port !== undefined && (typeof p.port !== 'number' || !Number.isInteger(p.port) || p.port < 1 || p.port > 65535)) {
|
|
50
53
|
return 'port must be an integer in 1..65535'
|
|
@@ -101,6 +104,8 @@ export class HostStore {
|
|
|
101
104
|
let keyReady = true
|
|
102
105
|
if (entry.auth.kind === 'key' && entry.auth.keyPath) {
|
|
103
106
|
keyReady = existsSync(expandHome(entry.auth.keyPath))
|
|
107
|
+
} else if (entry.auth.kind === 'agent') {
|
|
108
|
+
keyReady = false
|
|
104
109
|
}
|
|
105
110
|
return {
|
|
106
111
|
alias: entry.alias,
|
|
@@ -143,6 +148,7 @@ export class HostStore {
|
|
|
143
148
|
keyPath: payload.auth.kind === 'key' ? expandHome(payload.auth.keyPath?.trim() ?? '') : undefined,
|
|
144
149
|
passphrase: payload.auth.kind === 'key' ? payload.auth.passphrase ?? undefined : undefined,
|
|
145
150
|
password: payload.auth.kind === 'password' ? payload.auth.password : undefined,
|
|
151
|
+
agentPath: payload.auth.kind === 'agent' ? normalizeAgentPath(payload.auth.agentPath) : undefined,
|
|
146
152
|
},
|
|
147
153
|
proxyJump: [...(payload.proxyJump ?? [])],
|
|
148
154
|
description: payload.description?.trim() || undefined,
|
|
@@ -184,13 +190,16 @@ export class HostStore {
|
|
|
184
190
|
if (patch.user !== undefined) entry.user = patch.user.trim()
|
|
185
191
|
if (patch.auth !== undefined) {
|
|
186
192
|
const auth = patch.auth
|
|
187
|
-
if (auth.kind !== 'key' && auth.kind !== 'password') throw new Error('auth.kind must be key or
|
|
193
|
+
if (auth.kind !== 'key' && auth.kind !== 'password' && auth.kind !== 'agent') throw new Error('auth.kind must be key, password or agent')
|
|
188
194
|
if (auth.kind === 'key' && (typeof auth.keyPath !== 'string' || auth.keyPath.trim() === '')) {
|
|
189
195
|
throw new Error('auth.keyPath is required for key auth')
|
|
190
196
|
}
|
|
191
197
|
if (auth.kind === 'password' && auth.password !== undefined && typeof auth.password !== 'string') {
|
|
192
198
|
throw new Error('auth.password must be a string when provided')
|
|
193
199
|
}
|
|
200
|
+
if (auth.kind === 'agent' && auth.agentPath !== undefined && typeof auth.agentPath !== 'string') {
|
|
201
|
+
throw new Error('auth.agentPath must be a string when provided')
|
|
202
|
+
}
|
|
194
203
|
// A changed key path with no passphrase means the new key has none;
|
|
195
204
|
// only keep the old passphrase when the key path is unchanged.
|
|
196
205
|
const keyChanged = auth.kind === 'key'
|
|
@@ -203,6 +212,9 @@ export class HostStore {
|
|
|
203
212
|
? (auth.passphrase !== undefined ? auth.passphrase : (keyChanged ? undefined : entry.auth.passphrase))
|
|
204
213
|
: undefined,
|
|
205
214
|
password: auth.kind === 'password' ? auth.password : undefined,
|
|
215
|
+
agentPath: auth.kind === 'agent'
|
|
216
|
+
? (auth.agentPath !== undefined ? normalizeAgentPath(auth.agentPath) : entry.auth.agentPath)
|
|
217
|
+
: undefined,
|
|
206
218
|
}
|
|
207
219
|
}
|
|
208
220
|
if (patch.proxyJump !== undefined) entry.proxyJump = [...patch.proxyJump]
|
|
@@ -281,9 +293,16 @@ export class HostStore {
|
|
|
281
293
|
port: block.props.port !== undefined ? Number.parseInt(block.props.port, 10) : 22,
|
|
282
294
|
user: block.props.user ?? process.env.USER ?? 'root',
|
|
283
295
|
auth: {
|
|
284
|
-
kind: block.props.identityfile !== undefined
|
|
296
|
+
kind: block.props.identityfile !== undefined
|
|
297
|
+
? 'key'
|
|
298
|
+
: block.props.identityagent !== undefined && block.props.identityagent.toLowerCase() !== 'none'
|
|
299
|
+
? 'agent'
|
|
300
|
+
: 'password',
|
|
285
301
|
keyPath: block.props.identityfile,
|
|
286
302
|
password: block.props.password,
|
|
303
|
+
agentPath: block.props.identityagent !== undefined && block.props.identityagent.toLowerCase() !== 'none'
|
|
304
|
+
? normalizeAgentPath(block.props.identityagent)
|
|
305
|
+
: undefined,
|
|
287
306
|
},
|
|
288
307
|
proxyJump: block.props.proxyjump !== undefined
|
|
289
308
|
? block.props.proxyjump.split(',').map(hop => hop.trim()).filter(hop => hop !== '')
|
|
@@ -355,6 +374,17 @@ export class HostStore {
|
|
|
355
374
|
}
|
|
356
375
|
}
|
|
357
376
|
|
|
377
|
+
/** Normalize an agent endpoint for storage: trim, expand `~`, and resolve the SSH_AUTH_SOCK token. */
|
|
378
|
+
export function normalizeAgentPath(agentPath: string | undefined): string | undefined {
|
|
379
|
+
const trimmed = agentPath?.trim()
|
|
380
|
+
if (trimmed === undefined || trimmed === '') return undefined
|
|
381
|
+
if (trimmed === 'SSH_AUTH_SOCK' || trimmed === '$SSH_AUTH_SOCK') {
|
|
382
|
+
const sock = process.env.SSH_AUTH_SOCK
|
|
383
|
+
return sock !== undefined && sock !== '' ? sock : undefined
|
|
384
|
+
}
|
|
385
|
+
return expandHome(trimmed)
|
|
386
|
+
}
|
|
387
|
+
|
|
358
388
|
/** Expand a leading `~` in a filesystem path. */
|
|
359
389
|
export function expandHome(path: string): string {
|
|
360
390
|
if (path === '~') return homedir()
|
package/src/tools.ts
CHANGED
|
@@ -83,7 +83,7 @@ export function sshListTool(engine: SshEngine) {
|
|
|
83
83
|
host: { type: 'string', required: true },
|
|
84
84
|
port: { type: 'integer', required: true },
|
|
85
85
|
user: { type: 'string', required: true },
|
|
86
|
-
auth: { type: 'string', enum: ['key', 'password'], required: true },
|
|
86
|
+
auth: { type: 'string', enum: ['key', 'password', 'agent'], required: true },
|
|
87
87
|
keyReady: { type: 'boolean', required: true },
|
|
88
88
|
proxyJump: { type: 'array', items: { type: 'string' }, required: true },
|
|
89
89
|
description: { type: 'string' },
|