feat(sshclient): add a promise-first SSH client with secure host verification and improve SSH key/config handling

This commit is contained in:
2026-05-02 09:43:21 +00:00
parent 4a97d63c04
commit 3b20db79d0
17 changed files with 1332 additions and 170 deletions
+61 -10
View File
@@ -1,6 +1,8 @@
import * as plugins from './smartssh.plugins.js';
import * as helpers from './smartssh.classes.helpers.js';
export type TSshKeyType = 'duplex' | 'private' | 'public';
export class SshKey {
private _privKey: string;
private _pubKey: string;
@@ -15,6 +17,9 @@ export class SshKey {
) {
this._privKey = optionsArg.private ?? '';
this._pubKey = optionsArg.public ?? '';
if (optionsArg.host) {
helpers.assertSafeHost(optionsArg.host);
}
this._hostVar = optionsArg.host ?? '';
this._authorized = optionsArg.authorized ?? false;
}
@@ -24,6 +29,9 @@ export class SshKey {
return this._hostVar;
}
set host(hostArg: string) {
if (hostArg) {
helpers.assertSafeHost(hostArg);
}
this._hostVar = hostArg;
}
@@ -69,7 +77,7 @@ export class SshKey {
/**
* returns wether there is a private, a public or both keys
*/
get type() {
get type(): TSshKeyType | undefined {
if (this._privKey && this._pubKey) {
return 'duplex';
} else if (this._privKey) {
@@ -78,25 +86,68 @@ export class SshKey {
return 'public';
}
}
set type(someVlueArg: any) {
console.log('the type of an SshKey connot be set. This value is autocomputed.');
}
// methods
read(filePathArg: string) {}
read(filePathArg: string) {
const resolvedPath = plugins.path.resolve(filePathArg);
const fileName = plugins.path.basename(resolvedPath);
const isPublicKey = fileName.endsWith('.pub');
const host = isPublicKey ? fileName.slice(0, -4) : fileName;
helpers.assertSafeHost(host);
this._hostVar = host;
if (isPublicKey) {
this._pubKey = plugins.fs.readFileSync(resolvedPath, 'utf8');
} else {
this._privKey = plugins.fs.readFileSync(resolvedPath, 'utf8');
}
}
static fromFile(filePathArg: string) {
const sshKey = new SshKey();
sshKey.read(filePathArg);
return sshKey;
}
static fromFiles(optionsArg: { privateKeyPath?: string; publicKeyPath?: string; host?: string }) {
const sshKey = new SshKey({ host: optionsArg.host });
if (optionsArg.privateKeyPath) {
sshKey.privKey = plugins.fs.readFileSync(plugins.path.resolve(optionsArg.privateKeyPath), 'utf8');
if (!sshKey.host) {
const fileName = plugins.path.basename(optionsArg.privateKeyPath);
helpers.assertSafeHost(fileName);
sshKey.host = fileName;
}
}
if (optionsArg.publicKeyPath) {
sshKey.pubKey = plugins.fs.readFileSync(plugins.path.resolve(optionsArg.publicKeyPath), 'utf8');
if (!sshKey.host) {
const fileName = plugins.path.basename(optionsArg.publicKeyPath).replace(/\.pub$/, '');
helpers.assertSafeHost(fileName);
sshKey.host = fileName;
}
}
return sshKey;
}
async store(dirPathArg: string) {
plugins.fs.ensureDirSync(dirPathArg);
let fileNameBase = this.host;
this.storeSync(dirPathArg);
}
storeSync(dirPathArg: string) {
helpers.assertSafeHost(this.host);
const resolvedDir = helpers.resolveSshDirPath(dirPathArg);
helpers.ensureSshDirSync(resolvedDir);
const fileNameBase = this.host;
if (this._privKey) {
let filePath = plugins.path.join(dirPathArg, fileNameBase);
const filePath = plugins.path.join(resolvedDir, fileNameBase);
plugins.fs.writeFileSync(filePath, this._privKey);
plugins.fs.chmodSync(filePath, 0o600);
}
if (this._pubKey) {
let filePath = plugins.path.join(dirPathArg, fileNameBase + '.pub');
const filePath = plugins.path.join(resolvedDir, fileNameBase + '.pub');
plugins.fs.writeFileSync(filePath, this._pubKey);
plugins.fs.chmodSync(filePath, 0o600);
plugins.fs.chmodSync(filePath, 0o644);
}
}
}