import { Client, type ConnectConfig } from 'ssh2' import { db } from '../db/index.js' import { loadSecrets } from '../db/secrets.js' interface IntegrationRow { id: number type: string config_json: string } export function loadSshHost(integrationId: number) { const row = db .prepare('SELECT id, type, config_json FROM integrations WHERE id = ?') .get(integrationId) as IntegrationRow | undefined if (!row || row.type !== 'ssh') return null const config = JSON.parse(row.config_json) as Record const secrets = loadSecrets(row.id) return { id: row.id, config, secrets } } export type SshHost = NonNullable> function makeHostVerifier(integrationId: number): ConnectConfig['hostVerifier'] { return (keyHash: string): boolean => { const row = db .prepare('SELECT fingerprint FROM ssh_host_keys WHERE integration_id = ?') .get(integrationId) as { fingerprint: string } | undefined if (!row) { db.prepare('INSERT INTO ssh_host_keys (integration_id, fingerprint) VALUES (?, ?)').run(integrationId, keyHash) return true } return row.fingerprint === keyHash } } export function baseConnectConfig(host: SshHost): ConnectConfig { return { host: host.config.host, port: Number(host.config.port) || 22, username: host.config.username, password: host.secrets.password || undefined, privateKey: host.secrets.privateKey || undefined, passphrase: host.secrets.passphrase || undefined, readyTimeout: 8000, hostHash: 'sha256', hostVerifier: makeHostVerifier(host.id), } } /** Connects to `target`, transparently chaining through its jump host (if configured) via forwardOut(). */ export function connectTarget( target: SshHost, onReady: (client: Client) => void, onError: (message: string) => void, ): { conn: Client; jumpConn: Client | null } { const jumpHostId = target.config.jumpHostIntegrationId ? Number(target.config.jumpHostIntegrationId) : null if (jumpHostId) { const jumpHost = loadSshHost(jumpHostId) if (!jumpHost) { onError('Jump host integration not found') return { conn: new Client(), jumpConn: null } } const jumpConn = new Client() jumpConn.on('ready', () => { jumpConn.forwardOut('127.0.0.1', 0, target.config.host, Number(target.config.port) || 22, (err, sock) => { if (err) { onError(`Jump host forward failed: ${err.message}`) return } client.connect({ ...baseConnectConfig(target), sock }) }) }) jumpConn.on('error', (err) => onError(`Jump host error: ${err.message}`)) const client = new Client() client.on('ready', () => onReady(client)) client.on('error', (err) => onError(err.message)) jumpConn.connect(baseConnectConfig(jumpHost)) return { conn: client, jumpConn } } const client = new Client() client.on('ready', () => onReady(client)) client.on('error', (err) => onError(err.message)) client.connect(baseConnectConfig(target)) return { conn: client, jumpConn: null } }