@ebowwa/terminal 0.3.1 → 0.3.2
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/package.json +8 -70
- package/dist/api.d.ts +0 -7
- package/dist/client.d.ts +0 -14
- package/dist/config.d.ts +0 -85
- package/dist/cpufeatures-vxqw2k6s.node +0 -0
- package/dist/error.d.ts +0 -7
- package/dist/exec.d.ts +0 -46
- package/dist/files.d.ts +0 -123
- package/dist/fingerprint.d.ts +0 -66
- package/dist/index.d.ts +0 -20
- package/dist/manager.d.ts +0 -102
- package/dist/mcp/cpufeatures-vxqw2k6s.node +0 -0
- package/dist/mcp/index.d.ts +0 -8
- package/dist/mcp/sshcrypto-gez6h7ch.node +0 -0
- package/dist/mcp/stdio.d.ts +0 -8
- package/dist/network-error-detector.d.ts +0 -18
- package/dist/pool.d.ts +0 -142
- package/dist/pty.d.ts +0 -58
- package/dist/resources.d.ts +0 -62
- package/dist/scp.d.ts +0 -29
- package/dist/sessions.d.ts +0 -100
- package/dist/sshcrypto-gez6h7ch.node +0 -0
- package/dist/tmux-exec.d.ts +0 -49
- package/dist/tmux-local.d.ts +0 -272
- package/dist/tmux-manager.d.ts +0 -327
- package/dist/tmux.d.ts +0 -212
- package/dist/types.d.ts +0 -17
- package/mcp/README.md +0 -181
- package/mcp/package.json +0 -40
- package/mcp/stdio.js +0 -555
- package/mcp/test-fix.sh +0 -273
- package/mcp/wrapper.mjs +0 -10
- package/src/api.js +0 -861
- package/src/api.ts +0 -752
- package/src/client.js +0 -92
- package/src/client.ts +0 -55
- package/src/config.js +0 -490
- package/src/config.ts +0 -489
- package/src/error.js +0 -32
- package/src/error.ts +0 -13
- package/src/exec.js +0 -183
- package/src/exec.ts +0 -128
- package/src/files.js +0 -521
- package/src/files.ts +0 -636
- package/src/fingerprint.js +0 -336
- package/src/fingerprint.ts +0 -263
- package/src/index.js +0 -127
- package/src/index.ts +0 -148
- package/src/manager.js +0 -358
- package/src/manager.ts +0 -319
- package/src/mcp/index.js +0 -555
- package/src/mcp/index.ts +0 -467
- package/src/mcp/stdio.js +0 -840
- package/src/mcp/stdio.ts +0 -708
- package/src/network-error-detector.js +0 -101
- package/src/network-error-detector.ts +0 -121
- package/src/pool.js +0 -840
- package/src/pool.ts +0 -662
- package/src/pty.js +0 -344
- package/src/pty.ts +0 -285
- package/src/resources.js +0 -64
- package/src/resources.ts +0 -72
- package/src/scp.js +0 -166
- package/src/scp.ts +0 -109
- package/src/sessions.js +0 -895
- package/src/sessions.ts +0 -861
- package/src/tmux-exec.js +0 -169
- package/src/tmux-exec.ts +0 -96
- package/src/tmux-local.js +0 -937
- package/src/tmux-local.ts +0 -839
- package/src/tmux-manager.js +0 -1026
- package/src/tmux-manager.ts +0 -962
- package/src/tmux.js +0 -826
- package/src/tmux.ts +0 -711
- package/src/types.js +0 -5
- package/src/types.ts +0 -19
- package/tsconfig.json +0 -28
package/src/fingerprint.js
DELETED
|
@@ -1,336 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* SSH fingerprint utilities with validation and recovery
|
|
4
|
-
*/
|
|
5
|
-
var __extends = (this && this.__extends) || (function () {
|
|
6
|
-
var extendStatics = function (d, b) {
|
|
7
|
-
extendStatics = Object.setPrototypeOf ||
|
|
8
|
-
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
|
9
|
-
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
|
|
10
|
-
return extendStatics(d, b);
|
|
11
|
-
};
|
|
12
|
-
return function (d, b) {
|
|
13
|
-
if (typeof b !== "function" && b !== null)
|
|
14
|
-
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
|
|
15
|
-
extendStatics(d, b);
|
|
16
|
-
function __() { this.constructor = d; }
|
|
17
|
-
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
|
18
|
-
};
|
|
19
|
-
})();
|
|
20
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
21
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
22
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
23
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
24
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
25
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
26
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
27
|
-
});
|
|
28
|
-
};
|
|
29
|
-
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
30
|
-
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
31
|
-
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
32
|
-
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
33
|
-
function step(op) {
|
|
34
|
-
if (f) throw new TypeError("Generator is already executing.");
|
|
35
|
-
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
36
|
-
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
37
|
-
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
38
|
-
switch (op[0]) {
|
|
39
|
-
case 0: case 1: t = op; break;
|
|
40
|
-
case 4: _.label++; return { value: op[1], done: false };
|
|
41
|
-
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
42
|
-
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
43
|
-
default:
|
|
44
|
-
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
45
|
-
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
46
|
-
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
47
|
-
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
48
|
-
if (t[2]) _.ops.pop();
|
|
49
|
-
_.trys.pop(); continue;
|
|
50
|
-
}
|
|
51
|
-
op = body.call(thisArg, _);
|
|
52
|
-
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
53
|
-
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
|
-
exports.SSHKeyMismatchError = void 0;
|
|
58
|
-
exports.getSSHFingerprint = getSSHFingerprint;
|
|
59
|
-
exports.getLocalKeyFingerprint = getLocalKeyFingerprint;
|
|
60
|
-
exports.normalizeFingerprint = normalizeFingerprint;
|
|
61
|
-
exports.validateSSHKeyMatch = validateSSHKeyMatch;
|
|
62
|
-
exports.testSSHKeyConnection = testSSHKeyConnection;
|
|
63
|
-
exports.validateSSHKeyForServer = validateSSHKeyForServer;
|
|
64
|
-
var util_1 = require("util");
|
|
65
|
-
var promises_1 = require("fs/promises");
|
|
66
|
-
var execAsync = (0, util_1.promisify)(require("child_process").exec);
|
|
67
|
-
/**
|
|
68
|
-
* Get SSH fingerprint from remote server
|
|
69
|
-
* @param options - SSH connection options
|
|
70
|
-
* @returns SSH fingerprint or null
|
|
71
|
-
*/
|
|
72
|
-
function getSSHFingerprint(options) {
|
|
73
|
-
return __awaiter(this, void 0, void 0, function () {
|
|
74
|
-
var host, _a, port, proc, output, lines, _i, lines_1, line, parts, _b;
|
|
75
|
-
return __generator(this, function (_c) {
|
|
76
|
-
switch (_c.label) {
|
|
77
|
-
case 0:
|
|
78
|
-
host = options.host, _a = options.port, port = _a === void 0 ? 22 : _a;
|
|
79
|
-
_c.label = 1;
|
|
80
|
-
case 1:
|
|
81
|
-
_c.trys.push([1, 4, , 5]);
|
|
82
|
-
proc = Bun.spawn(["ssh-keyscan", "-p", port.toString(), "".concat(host)], {
|
|
83
|
-
stdout: "pipe",
|
|
84
|
-
stderr: "pipe",
|
|
85
|
-
});
|
|
86
|
-
return [4 /*yield*/, new Response(proc.stdout).text()];
|
|
87
|
-
case 2:
|
|
88
|
-
output = _c.sent();
|
|
89
|
-
return [4 /*yield*/, proc.exited];
|
|
90
|
-
case 3:
|
|
91
|
-
_c.sent();
|
|
92
|
-
lines = output.split("\n");
|
|
93
|
-
for (_i = 0, lines_1 = lines; _i < lines_1.length; _i++) {
|
|
94
|
-
line = lines_1[_i];
|
|
95
|
-
if (line.includes(host)) {
|
|
96
|
-
parts = line.split(" ");
|
|
97
|
-
if (parts.length >= 3) {
|
|
98
|
-
return [2 /*return*/, parts[2]]; // Return the fingerprint
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return [2 /*return*/, null];
|
|
103
|
-
case 4:
|
|
104
|
-
_b = _c.sent();
|
|
105
|
-
return [2 /*return*/, null];
|
|
106
|
-
case 5: return [2 /*return*/];
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Get SSH fingerprint from a local private key file
|
|
113
|
-
* @param keyPath - Path to the private key file
|
|
114
|
-
* @returns SSH fingerprint (SHA256 format) or null
|
|
115
|
-
*/
|
|
116
|
-
function getLocalKeyFingerprint(keyPath) {
|
|
117
|
-
return __awaiter(this, void 0, void 0, function () {
|
|
118
|
-
var stdout, match, _a, keyContent, lines, _i, lines_2, line, stdout, match, error_1;
|
|
119
|
-
return __generator(this, function (_b) {
|
|
120
|
-
switch (_b.label) {
|
|
121
|
-
case 0:
|
|
122
|
-
_b.trys.push([0, 10, , 11]);
|
|
123
|
-
_b.label = 1;
|
|
124
|
-
case 1:
|
|
125
|
-
_b.trys.push([1, 3, , 4]);
|
|
126
|
-
return [4 /*yield*/, execAsync("ssh-keygen -lf \"".concat(keyPath, "\""))];
|
|
127
|
-
case 2:
|
|
128
|
-
stdout = (_b.sent()).stdout;
|
|
129
|
-
match = stdout.match(/SHA256:(\S+)/i);
|
|
130
|
-
if (match) {
|
|
131
|
-
return [2 /*return*/, match[1]];
|
|
132
|
-
}
|
|
133
|
-
return [3 /*break*/, 4];
|
|
134
|
-
case 3:
|
|
135
|
-
_a = _b.sent();
|
|
136
|
-
return [3 /*break*/, 4];
|
|
137
|
-
case 4: return [4 /*yield*/, (0, promises_1.readFile)(keyPath, "utf-8")];
|
|
138
|
-
case 5:
|
|
139
|
-
keyContent = _b.sent();
|
|
140
|
-
if (!(keyContent.includes("OPENSSH") && keyContent.includes("ssh-ed25519"))) return [3 /*break*/, 9];
|
|
141
|
-
lines = keyContent.split("\n");
|
|
142
|
-
_i = 0, lines_2 = lines;
|
|
143
|
-
_b.label = 6;
|
|
144
|
-
case 6:
|
|
145
|
-
if (!(_i < lines_2.length)) return [3 /*break*/, 9];
|
|
146
|
-
line = lines_2[_i];
|
|
147
|
-
if (!line.startsWith("ssh-ed25519")) return [3 /*break*/, 8];
|
|
148
|
-
return [4 /*yield*/, execAsync("echo '".concat(line, "' | ssh-keygen -lf -"))];
|
|
149
|
-
case 7:
|
|
150
|
-
stdout = (_b.sent()).stdout;
|
|
151
|
-
match = stdout.match(/SHA256:(\S+)/i);
|
|
152
|
-
if (match) {
|
|
153
|
-
return [2 /*return*/, match[1]];
|
|
154
|
-
}
|
|
155
|
-
_b.label = 8;
|
|
156
|
-
case 8:
|
|
157
|
-
_i++;
|
|
158
|
-
return [3 /*break*/, 6];
|
|
159
|
-
case 9: return [2 /*return*/, null];
|
|
160
|
-
case 10:
|
|
161
|
-
error_1 = _b.sent();
|
|
162
|
-
console.error("[Fingerprint] Failed to get local key fingerprint:", error_1);
|
|
163
|
-
return [2 /*return*/, null];
|
|
164
|
-
case 11: return [2 /*return*/];
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Convert MD5 fingerprint format to SHA256 format (for comparison)
|
|
171
|
-
* Hetzner returns MD5 like "29:cd:c1:c3:84:eb:ca:31:a4:1f:94:69:0c:84:b3:56"
|
|
172
|
-
* We need to handle both formats
|
|
173
|
-
*/
|
|
174
|
-
function normalizeFingerprint(fingerprint) {
|
|
175
|
-
// Remove colons from MD5 format for easier comparison
|
|
176
|
-
return fingerprint.replace(/:/g, "").toLowerCase();
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Validate that a local SSH key matches what's on a remote server
|
|
180
|
-
* @param host - Server hostname or IP
|
|
181
|
-
* @param keyPath - Path to local private key
|
|
182
|
-
* @returns Validation result
|
|
183
|
-
*/
|
|
184
|
-
function validateSSHKeyMatch(host, keyPath) {
|
|
185
|
-
return __awaiter(this, void 0, void 0, function () {
|
|
186
|
-
var localFingerprint, remoteFingerprint, localNormalized, remoteNormalized, match, error_2;
|
|
187
|
-
return __generator(this, function (_a) {
|
|
188
|
-
switch (_a.label) {
|
|
189
|
-
case 0:
|
|
190
|
-
_a.trys.push([0, 3, , 4]);
|
|
191
|
-
return [4 /*yield*/, getLocalKeyFingerprint(keyPath)];
|
|
192
|
-
case 1:
|
|
193
|
-
localFingerprint = _a.sent();
|
|
194
|
-
if (!localFingerprint) {
|
|
195
|
-
return [2 /*return*/, {
|
|
196
|
-
valid: false,
|
|
197
|
-
error: "Could not read local SSH key fingerprint",
|
|
198
|
-
}];
|
|
199
|
-
}
|
|
200
|
-
return [4 /*yield*/, getSSHFingerprint({ host: host })];
|
|
201
|
-
case 2:
|
|
202
|
-
remoteFingerprint = _a.sent();
|
|
203
|
-
if (!remoteFingerprint) {
|
|
204
|
-
return [2 /*return*/, {
|
|
205
|
-
valid: false,
|
|
206
|
-
error: "Could not get remote SSH fingerprint",
|
|
207
|
-
}];
|
|
208
|
-
}
|
|
209
|
-
localNormalized = normalizeFingerprint(localFingerprint);
|
|
210
|
-
remoteNormalized = normalizeFingerprint(remoteFingerprint);
|
|
211
|
-
match = localNormalized === remoteNormalized ||
|
|
212
|
-
localFingerprint === remoteFingerprint ||
|
|
213
|
-
localFingerprint === remoteFingerprint.replace(/:/g, "");
|
|
214
|
-
return [2 /*return*/, {
|
|
215
|
-
valid: match,
|
|
216
|
-
localFingerprint: localFingerprint,
|
|
217
|
-
remoteFingerprint: remoteFingerprint,
|
|
218
|
-
error: match ? undefined : "Fingerprints do not match",
|
|
219
|
-
}];
|
|
220
|
-
case 3:
|
|
221
|
-
error_2 = _a.sent();
|
|
222
|
-
return [2 /*return*/, {
|
|
223
|
-
valid: false,
|
|
224
|
-
error: error_2 instanceof Error ? error_2.message : String(error_2),
|
|
225
|
-
}];
|
|
226
|
-
case 4: return [2 /*return*/];
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
/**
|
|
232
|
-
* Check if we can SSH to a server with a given key
|
|
233
|
-
* @param host - Server hostname or IP
|
|
234
|
-
* @param keyPath - Path to SSH private key
|
|
235
|
-
* @returns true if SSH works
|
|
236
|
-
*/
|
|
237
|
-
function testSSHKeyConnection(host, keyPath) {
|
|
238
|
-
return __awaiter(this, void 0, void 0, function () {
|
|
239
|
-
var _a;
|
|
240
|
-
return __generator(this, function (_b) {
|
|
241
|
-
switch (_b.label) {
|
|
242
|
-
case 0:
|
|
243
|
-
_b.trys.push([0, 2, , 3]);
|
|
244
|
-
return [4 /*yield*/, execAsync("ssh -F /dev/null -i \"".concat(keyPath, "\" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 ").concat(host, " \"echo ok\""), { timeout: 10000 })];
|
|
245
|
-
case 1:
|
|
246
|
-
_b.sent();
|
|
247
|
-
return [2 /*return*/, true];
|
|
248
|
-
case 2:
|
|
249
|
-
_a = _b.sent();
|
|
250
|
-
return [2 /*return*/, false];
|
|
251
|
-
case 3: return [2 /*return*/];
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
|
-
* SSH Key Mismatch Error with recovery suggestions
|
|
258
|
-
*/
|
|
259
|
-
var SSHKeyMismatchError = /** @class */ (function (_super) {
|
|
260
|
-
__extends(SSHKeyMismatchError, _super);
|
|
261
|
-
function SSHKeyMismatchError(host, localFingerprint, hetznerFingerprint, keyPath) {
|
|
262
|
-
var _this = this;
|
|
263
|
-
var hetznerShort = hetznerFingerprint.split(":").slice(0, 4).join(":");
|
|
264
|
-
var localShort = localFingerprint.slice(0, 16);
|
|
265
|
-
_this = _super.call(this, "SSH key mismatch for ".concat(host, "\n") +
|
|
266
|
-
" Local key (".concat(keyPath, "): ").concat(localShort, "...\n") +
|
|
267
|
-
" Hetzner key: ".concat(hetznerShort, "...\n\n") +
|
|
268
|
-
"The local private key doesn't match the public key on Hetzner.\n\n" +
|
|
269
|
-
"RECOVERY OPTIONS:\n" +
|
|
270
|
-
"1. Create a new SSH key and upload to Hetzner\n" +
|
|
271
|
-
"2. Regenerate local key to match Hetzner's key\n" +
|
|
272
|
-
"3. Use the correct key path for this server") || this;
|
|
273
|
-
_this.host = host;
|
|
274
|
-
_this.localFingerprint = localFingerprint;
|
|
275
|
-
_this.hetznerFingerprint = hetznerFingerprint;
|
|
276
|
-
_this.keyPath = keyPath;
|
|
277
|
-
_this.name = "SSHKeyMismatchError";
|
|
278
|
-
return _this;
|
|
279
|
-
}
|
|
280
|
-
return SSHKeyMismatchError;
|
|
281
|
-
}(Error));
|
|
282
|
-
exports.SSHKeyMismatchError = SSHKeyMismatchError;
|
|
283
|
-
/**
|
|
284
|
-
* Comprehensive SSH key validation for server creation
|
|
285
|
-
* @param host - Server hostname or IP
|
|
286
|
-
* @param keyPath - Path to local SSH key
|
|
287
|
-
* @param hetznerKeyId - SSH key ID on Hetzner (for comparison)
|
|
288
|
-
* @returns Validation result with recovery suggestions
|
|
289
|
-
*/
|
|
290
|
-
function validateSSHKeyForServer(host, keyPath, hetznerKeyId) {
|
|
291
|
-
return __awaiter(this, void 0, void 0, function () {
|
|
292
|
-
var result, _a, validation;
|
|
293
|
-
return __generator(this, function (_b) {
|
|
294
|
-
switch (_b.label) {
|
|
295
|
-
case 0:
|
|
296
|
-
result = {
|
|
297
|
-
canConnect: false,
|
|
298
|
-
fingerprintMatch: false,
|
|
299
|
-
recovery: [],
|
|
300
|
-
};
|
|
301
|
-
// Test if SSH works at all
|
|
302
|
-
_a = result;
|
|
303
|
-
return [4 /*yield*/, testSSHKeyConnection(host, keyPath)];
|
|
304
|
-
case 1:
|
|
305
|
-
// Test if SSH works at all
|
|
306
|
-
_a.canConnect = _b.sent();
|
|
307
|
-
return [4 /*yield*/, validateSSHKeyMatch(host, keyPath)];
|
|
308
|
-
case 2:
|
|
309
|
-
validation = _b.sent();
|
|
310
|
-
result.localFingerprint = validation.localFingerprint;
|
|
311
|
-
result.remoteFingerprint = validation.remoteFingerprint;
|
|
312
|
-
result.fingerprintMatch = validation.valid;
|
|
313
|
-
if (!result.canConnect) {
|
|
314
|
-
result.error = "Cannot connect to server with this key";
|
|
315
|
-
result.recovery = [
|
|
316
|
-
"1. Check if the server is fully initialized (may still be booting)",
|
|
317
|
-
"2. Verify the SSH key was added to the server's ~/.ssh/authorized_keys",
|
|
318
|
-
"3. Try a different SSH key or regenerate the key pair",
|
|
319
|
-
];
|
|
320
|
-
return [2 /*return*/, result];
|
|
321
|
-
}
|
|
322
|
-
if (!result.fingerprintMatch) {
|
|
323
|
-
result.error = "SSH key fingerprint mismatch";
|
|
324
|
-
result.recovery = [
|
|
325
|
-
"1. Generate a new SSH key pair: `ssh-keygen -t ed25519 -f ~/.ssh/hetzner-new`",
|
|
326
|
-
"2. Upload public key to Hetzner (via GUI or API)",
|
|
327
|
-
"3. Update the server's metadata with the new key path",
|
|
328
|
-
"4. Alternatively: The Hetzner key ID ".concat(hetznerKeyId, " may need key regeneration"),
|
|
329
|
-
];
|
|
330
|
-
return [2 /*return*/, result];
|
|
331
|
-
}
|
|
332
|
-
return [2 /*return*/, result];
|
|
333
|
-
}
|
|
334
|
-
});
|
|
335
|
-
});
|
|
336
|
-
}
|
package/src/fingerprint.ts
DELETED
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SSH fingerprint utilities with validation and recovery
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { SSHOptions } from "./types.js";
|
|
6
|
-
import { spawn } from "child_process";
|
|
7
|
-
import { promisify } from "util";
|
|
8
|
-
import { readFile } from "fs/promises";
|
|
9
|
-
|
|
10
|
-
const execAsync = promisify(require("child_process").exec);
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Get SSH fingerprint from remote server
|
|
14
|
-
* @param options - SSH connection options
|
|
15
|
-
* @returns SSH fingerprint or null
|
|
16
|
-
*/
|
|
17
|
-
export async function getSSHFingerprint(
|
|
18
|
-
options: SSHOptions,
|
|
19
|
-
): Promise<string | null> {
|
|
20
|
-
const { host, port = 22 } = options;
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
const proc = Bun.spawn(["ssh-keyscan", "-p", port.toString(), `${host}`], {
|
|
24
|
-
stdout: "pipe",
|
|
25
|
-
stderr: "pipe",
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const output = await new Response(proc.stdout).text();
|
|
29
|
-
await proc.exited;
|
|
30
|
-
|
|
31
|
-
// Parse fingerprint from output
|
|
32
|
-
const lines = output.split("\n");
|
|
33
|
-
for (const line of lines) {
|
|
34
|
-
if (line.includes(host)) {
|
|
35
|
-
const parts = line.split(" ");
|
|
36
|
-
if (parts.length >= 3) {
|
|
37
|
-
return parts[2]; // Return the fingerprint
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return null;
|
|
42
|
-
} catch {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Get SSH fingerprint from a local private key file
|
|
49
|
-
* @param keyPath - Path to the private key file
|
|
50
|
-
* @returns SSH fingerprint (SHA256 format) or null
|
|
51
|
-
*/
|
|
52
|
-
export async function getLocalKeyFingerprint(
|
|
53
|
-
keyPath: string,
|
|
54
|
-
): Promise<string | null> {
|
|
55
|
-
try {
|
|
56
|
-
// Try ssh-keygen first (most reliable)
|
|
57
|
-
try {
|
|
58
|
-
const { stdout } = await execAsync(`ssh-keygen -lf "${keyPath}"`);
|
|
59
|
-
const match = stdout.match(/SHA256:(\S+)/i);
|
|
60
|
-
if (match) {
|
|
61
|
-
return match[1];
|
|
62
|
-
}
|
|
63
|
-
} catch {
|
|
64
|
-
// ssh-keygen might not be available, try fallback
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Fallback: Try to extract from the key file directly
|
|
68
|
-
const keyContent = await readFile(keyPath, "utf-8");
|
|
69
|
-
|
|
70
|
-
// For ED25519 keys, the public key is embedded
|
|
71
|
-
if (keyContent.includes("OPENSSH") && keyContent.includes("ssh-ed25519")) {
|
|
72
|
-
// Extract the public key part
|
|
73
|
-
const lines = keyContent.split("\n");
|
|
74
|
-
for (const line of lines) {
|
|
75
|
-
if (line.startsWith("ssh-ed25519")) {
|
|
76
|
-
// Calculate fingerprint from public key
|
|
77
|
-
const { stdout } = await execAsync(`echo '${line}' | ssh-keygen -lf -`);
|
|
78
|
-
const match = stdout.match(/SHA256:(\S+)/i);
|
|
79
|
-
if (match) {
|
|
80
|
-
return match[1];
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return null;
|
|
87
|
-
} catch (error) {
|
|
88
|
-
console.error("[Fingerprint] Failed to get local key fingerprint:", error);
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Convert MD5 fingerprint format to SHA256 format (for comparison)
|
|
95
|
-
* Hetzner returns MD5 like "29:cd:c1:c3:84:eb:ca:31:a4:1f:94:69:0c:84:b3:56"
|
|
96
|
-
* We need to handle both formats
|
|
97
|
-
*/
|
|
98
|
-
export function normalizeFingerprint(fingerprint: string): string {
|
|
99
|
-
// Remove colons from MD5 format for easier comparison
|
|
100
|
-
return fingerprint.replace(/:/g, "").toLowerCase();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Validate that a local SSH key matches what's on a remote server
|
|
105
|
-
* @param host - Server hostname or IP
|
|
106
|
-
* @param keyPath - Path to local private key
|
|
107
|
-
* @returns Validation result
|
|
108
|
-
*/
|
|
109
|
-
export async function validateSSHKeyMatch(
|
|
110
|
-
host: string,
|
|
111
|
-
keyPath: string,
|
|
112
|
-
): Promise<{
|
|
113
|
-
valid: boolean;
|
|
114
|
-
localFingerprint?: string;
|
|
115
|
-
remoteFingerprint?: string;
|
|
116
|
-
error?: string;
|
|
117
|
-
}> {
|
|
118
|
-
try {
|
|
119
|
-
// Get local fingerprint
|
|
120
|
-
const localFingerprint = await getLocalKeyFingerprint(keyPath);
|
|
121
|
-
if (!localFingerprint) {
|
|
122
|
-
return {
|
|
123
|
-
valid: false,
|
|
124
|
-
error: "Could not read local SSH key fingerprint",
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Get remote fingerprint
|
|
129
|
-
const remoteFingerprint = await getSSHFingerprint({ host });
|
|
130
|
-
if (!remoteFingerprint) {
|
|
131
|
-
return {
|
|
132
|
-
valid: false,
|
|
133
|
-
error: "Could not get remote SSH fingerprint",
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Normalize both for comparison
|
|
138
|
-
const localNormalized = normalizeFingerprint(localFingerprint);
|
|
139
|
-
const remoteNormalized = normalizeFingerprint(remoteFingerprint);
|
|
140
|
-
|
|
141
|
-
const match = localNormalized === remoteNormalized ||
|
|
142
|
-
localFingerprint === remoteFingerprint ||
|
|
143
|
-
localFingerprint === remoteFingerprint.replace(/:/g, "");
|
|
144
|
-
|
|
145
|
-
return {
|
|
146
|
-
valid: match,
|
|
147
|
-
localFingerprint: localFingerprint,
|
|
148
|
-
remoteFingerprint: remoteFingerprint,
|
|
149
|
-
error: match ? undefined : "Fingerprints do not match",
|
|
150
|
-
};
|
|
151
|
-
} catch (error) {
|
|
152
|
-
return {
|
|
153
|
-
valid: false,
|
|
154
|
-
error: error instanceof Error ? error.message : String(error),
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Check if we can SSH to a server with a given key
|
|
161
|
-
* @param host - Server hostname or IP
|
|
162
|
-
* @param keyPath - Path to SSH private key
|
|
163
|
-
* @returns true if SSH works
|
|
164
|
-
*/
|
|
165
|
-
export async function testSSHKeyConnection(
|
|
166
|
-
host: string,
|
|
167
|
-
keyPath: string,
|
|
168
|
-
): Promise<boolean> {
|
|
169
|
-
try {
|
|
170
|
-
await execAsync(
|
|
171
|
-
`ssh -F /dev/null -i "${keyPath}" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 ${host} "echo ok"`,
|
|
172
|
-
{ timeout: 10000 }
|
|
173
|
-
);
|
|
174
|
-
return true;
|
|
175
|
-
} catch {
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* SSH Key Mismatch Error with recovery suggestions
|
|
182
|
-
*/
|
|
183
|
-
export class SSHKeyMismatchError extends Error {
|
|
184
|
-
constructor(
|
|
185
|
-
public host: string,
|
|
186
|
-
public localFingerprint: string,
|
|
187
|
-
public hetznerFingerprint: string,
|
|
188
|
-
public keyPath: string,
|
|
189
|
-
) {
|
|
190
|
-
const hetznerShort = hetznerFingerprint.split(":").slice(0, 4).join(":");
|
|
191
|
-
const localShort = localFingerprint.slice(0, 16);
|
|
192
|
-
|
|
193
|
-
super(
|
|
194
|
-
`SSH key mismatch for ${host}\n` +
|
|
195
|
-
` Local key (${keyPath}): ${localShort}...\n` +
|
|
196
|
-
` Hetzner key: ${hetznerShort}...\n\n` +
|
|
197
|
-
`The local private key doesn't match the public key on Hetzner.\n\n` +
|
|
198
|
-
`RECOVERY OPTIONS:\n` +
|
|
199
|
-
`1. Create a new SSH key and upload to Hetzner\n` +
|
|
200
|
-
`2. Regenerate local key to match Hetzner's key\n` +
|
|
201
|
-
`3. Use the correct key path for this server`
|
|
202
|
-
);
|
|
203
|
-
this.name = "SSHKeyMismatchError";
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Comprehensive SSH key validation for server creation
|
|
209
|
-
* @param host - Server hostname or IP
|
|
210
|
-
* @param keyPath - Path to local SSH key
|
|
211
|
-
* @param hetznerKeyId - SSH key ID on Hetzner (for comparison)
|
|
212
|
-
* @returns Validation result with recovery suggestions
|
|
213
|
-
*/
|
|
214
|
-
export async function validateSSHKeyForServer(
|
|
215
|
-
host: string,
|
|
216
|
-
keyPath: string,
|
|
217
|
-
hetznerKeyId?: string,
|
|
218
|
-
): Promise<{
|
|
219
|
-
canConnect: boolean;
|
|
220
|
-
fingerprintMatch: boolean;
|
|
221
|
-
localFingerprint?: string;
|
|
222
|
-
remoteFingerprint?: string;
|
|
223
|
-
error?: string;
|
|
224
|
-
recovery?: string[];
|
|
225
|
-
}> {
|
|
226
|
-
const result: Awaited<ReturnType<typeof validateSSHKeyForServer>> = {
|
|
227
|
-
canConnect: false,
|
|
228
|
-
fingerprintMatch: false,
|
|
229
|
-
recovery: [],
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
// Test if SSH works at all
|
|
233
|
-
result.canConnect = await testSSHKeyConnection(host, keyPath);
|
|
234
|
-
|
|
235
|
-
// Get fingerprints for comparison
|
|
236
|
-
const validation = await validateSSHKeyMatch(host, keyPath);
|
|
237
|
-
result.localFingerprint = validation.localFingerprint;
|
|
238
|
-
result.remoteFingerprint = validation.remoteFingerprint;
|
|
239
|
-
result.fingerprintMatch = validation.valid;
|
|
240
|
-
|
|
241
|
-
if (!result.canConnect) {
|
|
242
|
-
result.error = "Cannot connect to server with this key";
|
|
243
|
-
result.recovery = [
|
|
244
|
-
"1. Check if the server is fully initialized (may still be booting)",
|
|
245
|
-
"2. Verify the SSH key was added to the server's ~/.ssh/authorized_keys",
|
|
246
|
-
"3. Try a different SSH key or regenerate the key pair",
|
|
247
|
-
];
|
|
248
|
-
return result;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (!result.fingerprintMatch) {
|
|
252
|
-
result.error = "SSH key fingerprint mismatch";
|
|
253
|
-
result.recovery = [
|
|
254
|
-
"1. Generate a new SSH key pair: `ssh-keygen -t ed25519 -f ~/.ssh/hetzner-new`",
|
|
255
|
-
"2. Upload public key to Hetzner (via GUI or API)",
|
|
256
|
-
"3. Update the server's metadata with the new key path",
|
|
257
|
-
`4. Alternatively: The Hetzner key ID ${hetznerKeyId} may need key regeneration`,
|
|
258
|
-
];
|
|
259
|
-
return result;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
return result;
|
|
263
|
-
}
|