deskssh 0.0.2 → 0.1.0

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/dist/server.js ADDED
@@ -0,0 +1,851 @@
1
+ import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);
2
+
3
+ // ../server/dist/gateway.js
4
+ import { createServer } from "node:http";
5
+
6
+ // ../server/dist/session-manager.js
7
+ import { randomUUID } from "node:crypto";
8
+ function toSessionInfo(entry) {
9
+ return {
10
+ sessionId: entry.id,
11
+ host: entry.host,
12
+ home: entry.home,
13
+ os: { family: entry.os.family, prettyName: entry.os.prettyName }
14
+ };
15
+ }
16
+ var SessionManager = class {
17
+ sessions = /* @__PURE__ */ new Map();
18
+ /** Register a session under a fresh opaque id. */
19
+ add(entry) {
20
+ const id = randomUUID();
21
+ const full = { ...entry, id };
22
+ this.sessions.set(id, full);
23
+ return full;
24
+ }
25
+ get(id) {
26
+ return this.sessions.get(id);
27
+ }
28
+ /** Close and forget a session. Returns true if it existed. */
29
+ remove(id) {
30
+ const entry = this.sessions.get(id);
31
+ if (!entry)
32
+ return false;
33
+ entry.close();
34
+ this.sessions.delete(id);
35
+ return true;
36
+ }
37
+ /** Recent transparency records for a session (for the UI). */
38
+ transparency(id) {
39
+ return this.sessions.get(id)?.log.list() ?? [];
40
+ }
41
+ /** Close every session (graceful shutdown). */
42
+ closeAll() {
43
+ for (const id of [...this.sessions.keys()])
44
+ this.remove(id);
45
+ }
46
+ get size() {
47
+ return this.sessions.size;
48
+ }
49
+ };
50
+
51
+ // ../core/dist/transparency/log.js
52
+ var TransparencyLog = class {
53
+ entries = [];
54
+ listeners = /* @__PURE__ */ new Set();
55
+ nextId = 1;
56
+ /** Append a record, assigning it an id, and notify listeners. */
57
+ record(entry) {
58
+ const full = { ...entry, id: this.nextId++ };
59
+ this.entries.push(full);
60
+ for (const listener of this.listeners)
61
+ listener(full);
62
+ return full;
63
+ }
64
+ /** All records so far, in execution order. */
65
+ list() {
66
+ return this.entries;
67
+ }
68
+ /** Subscribe to new records; returns an unsubscribe function. */
69
+ subscribe(listener) {
70
+ this.listeners.add(listener);
71
+ return () => this.listeners.delete(listener);
72
+ }
73
+ };
74
+ function withTransparency(executor, log, host) {
75
+ return {
76
+ async exec(command) {
77
+ const startedAt = Date.now();
78
+ try {
79
+ const result = await executor.exec(command);
80
+ log.record({
81
+ command,
82
+ host,
83
+ startedAt,
84
+ durationMs: Date.now() - startedAt,
85
+ exitCode: result.exitCode
86
+ });
87
+ return result;
88
+ } catch (err) {
89
+ log.record({
90
+ command,
91
+ host,
92
+ startedAt,
93
+ durationMs: Date.now() - startedAt,
94
+ exitCode: void 0,
95
+ error: err instanceof Error ? err.message : String(err)
96
+ });
97
+ throw err;
98
+ }
99
+ }
100
+ };
101
+ }
102
+
103
+ // ../core/dist/contract/result.js
104
+ function ok(value, raw) {
105
+ return { kind: "ok", value, raw };
106
+ }
107
+ function degraded(raw, reason) {
108
+ return { kind: "degraded", raw, reason };
109
+ }
110
+ function unsupported(reason) {
111
+ return { kind: "unsupported", reason };
112
+ }
113
+ async function runParsed(executor, command, parser) {
114
+ const { stdout, stderr, exitCode } = await executor.exec(command);
115
+ if (exitCode !== 0) {
116
+ const reason = stderr.trim() || `command exited with code ${String(exitCode)}`;
117
+ return { kind: "failed", raw: stderr || stdout, exitCode, reason };
118
+ }
119
+ try {
120
+ return ok(parser(stdout), stdout);
121
+ } catch (err) {
122
+ return degraded(stdout, err instanceof Error ? err.message : String(err));
123
+ }
124
+ }
125
+
126
+ // ../core/dist/adapters/os.js
127
+ function parseOsRelease(raw) {
128
+ const out = {};
129
+ for (const line of raw.split("\n")) {
130
+ const trimmed = line.trim();
131
+ if (!trimmed || trimmed.startsWith("#"))
132
+ continue;
133
+ const eq = trimmed.indexOf("=");
134
+ if (eq === -1)
135
+ continue;
136
+ const key = trimmed.slice(0, eq);
137
+ let value = trimmed.slice(eq + 1).trim();
138
+ if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
139
+ value = value.slice(1, -1);
140
+ }
141
+ out[key] = value;
142
+ }
143
+ return out;
144
+ }
145
+ function familyFor(id, idLike) {
146
+ const haystack = `${id ?? ""} ${idLike ?? ""}`.toLowerCase();
147
+ if (/\b(debian|ubuntu|linuxmint|mint|raspbian|pop)\b/.test(haystack))
148
+ return "debian";
149
+ if (/\b(rhel|fedora|centos|rocky|almalinux)\b/.test(haystack))
150
+ return "rhel";
151
+ if (/\b(arch|manjaro)\b/.test(haystack))
152
+ return "arch";
153
+ return "unknown";
154
+ }
155
+ async function detectOs(executor) {
156
+ const { stdout: releaseRaw, exitCode: releaseCode } = await executor.exec("cat /etc/os-release 2>/dev/null");
157
+ const { stdout: unameRaw } = await executor.exec("uname -s 2>/dev/null");
158
+ const kernel = unameRaw.trim() || void 0;
159
+ if (releaseCode !== 0 || !releaseRaw.trim()) {
160
+ return { family: kernel ? "posix" : "unknown", kernel };
161
+ }
162
+ const fields = parseOsRelease(releaseRaw);
163
+ const id = fields["ID"];
164
+ const idLike = fields["ID_LIKE"];
165
+ const known = familyFor(id, idLike);
166
+ return {
167
+ family: known === "unknown" ? "posix" : known,
168
+ id,
169
+ idLike,
170
+ kernel,
171
+ prettyName: fields["PRETTY_NAME"]
172
+ };
173
+ }
174
+
175
+ // ../core/dist/adapters/shell.js
176
+ function quote(arg) {
177
+ return `'${arg.replace(/'/g, `'\\''`)}'`;
178
+ }
179
+
180
+ // ../core/dist/adapters/debian.js
181
+ var FIND_PRINTF = String.raw`%y\t%s\t%m\t%u\t%g\t%T@\t%f\n`;
182
+ var STAT_PRINTF = String.raw`%F\t%s\t%a\t%U\t%G\t%Y\t%n`;
183
+ function fileTypeFromFindCode(code) {
184
+ switch (code) {
185
+ case "f":
186
+ return "file";
187
+ case "d":
188
+ return "directory";
189
+ case "l":
190
+ return "symlink";
191
+ default:
192
+ return "other";
193
+ }
194
+ }
195
+ function fileTypeFromStatWord(word) {
196
+ if (word === "directory")
197
+ return "directory";
198
+ if (word === "symbolic link")
199
+ return "symlink";
200
+ if (word.endsWith("file"))
201
+ return "file";
202
+ return "other";
203
+ }
204
+ function epochSecondsToMs(value) {
205
+ return Math.round(parseFloat(value) * 1e3);
206
+ }
207
+ function parseFindLine(line) {
208
+ const parts = line.split(" ");
209
+ if (parts.length < 7)
210
+ throw new Error(`Unexpected find output: ${line}`);
211
+ const [type, size, mode, owner, group, mtime] = parts;
212
+ const name = parts.slice(6).join(" ");
213
+ return {
214
+ name,
215
+ type: fileTypeFromFindCode(type ?? ""),
216
+ size: Number.parseInt(size ?? "", 10) || 0,
217
+ mode: Number.parseInt(mode ?? "", 8) || 0,
218
+ owner: owner ?? "",
219
+ group: group ?? "",
220
+ mtime: epochSecondsToMs(mtime ?? "0")
221
+ };
222
+ }
223
+ function parseListDir(stdout) {
224
+ return stdout.split("\n").filter((line) => line.length > 0).map(parseFindLine);
225
+ }
226
+ function parseStat(stdout) {
227
+ const parts = stdout.split(" ");
228
+ if (parts.length < 7)
229
+ throw new Error(`Unexpected stat output: ${stdout}`);
230
+ const [typeWord, size, mode, owner, group, mtime] = parts;
231
+ const fullPath = parts.slice(6).join(" ");
232
+ const name = fullPath.replace(/\/+$/, "").split("/").pop() ?? fullPath;
233
+ return {
234
+ name,
235
+ type: fileTypeFromStatWord(typeWord ?? ""),
236
+ size: Number.parseInt(size ?? "", 10) || 0,
237
+ mode: Number.parseInt(mode ?? "", 8) || 0,
238
+ owner: owner ?? "",
239
+ group: group ?? "",
240
+ mtime: (Number.parseInt(mtime ?? "0", 10) || 0) * 1e3
241
+ };
242
+ }
243
+ function kvFromMeminfo(meminfo) {
244
+ const map = /* @__PURE__ */ new Map();
245
+ for (const line of meminfo.split("\n")) {
246
+ const match = /^(\w+):\s+(\d+)\s*kB$/.exec(line.trim());
247
+ if (match && match[1] && match[2])
248
+ map.set(match[1], Number.parseInt(match[2], 10) * 1024);
249
+ }
250
+ return map;
251
+ }
252
+ function parseSystemMetrics(stdout) {
253
+ const sections = /* @__PURE__ */ new Map();
254
+ let current = "";
255
+ const buffer = {};
256
+ for (const line of stdout.split("\n")) {
257
+ const marker = /^===(\w+)===$/.exec(line);
258
+ if (marker && marker[1]) {
259
+ current = marker[1];
260
+ buffer[current] = [];
261
+ continue;
262
+ }
263
+ if (current)
264
+ (buffer[current] ??= []).push(line);
265
+ }
266
+ for (const [key, lines] of Object.entries(buffer))
267
+ sections.set(key, lines.join("\n"));
268
+ const uptime = sections.get("UPTIME")?.trim().split(/\s+/)[0];
269
+ const load2 = sections.get("LOAD")?.trim().split(/\s+/) ?? [];
270
+ const mem = kvFromMeminfo(sections.get("MEM") ?? "");
271
+ const total = mem.get("MemTotal") ?? 0;
272
+ const available = mem.get("MemAvailable") ?? 0;
273
+ if (uptime === void 0 || load2.length < 3)
274
+ throw new Error("Incomplete metrics output");
275
+ return {
276
+ uptimeSeconds: Math.round(parseFloat(uptime)),
277
+ loadAverage: [
278
+ parseFloat(load2[0] ?? "0"),
279
+ parseFloat(load2[1] ?? "0"),
280
+ parseFloat(load2[2] ?? "0")
281
+ ],
282
+ memory: {
283
+ totalBytes: total,
284
+ usedBytes: Math.max(0, total - available),
285
+ availableBytes: available
286
+ }
287
+ };
288
+ }
289
+ var METRICS_COMMAND = "echo ===UPTIME===; cat /proc/uptime; echo ===LOAD===; cat /proc/loadavg; echo ===MEM===; cat /proc/meminfo";
290
+ var DebianAdapter = class {
291
+ exec;
292
+ constructor(exec) {
293
+ this.exec = exec;
294
+ }
295
+ listDir(path) {
296
+ const cmd = `find ${quote(path)} -maxdepth 1 -mindepth 1 -printf ${quote(FIND_PRINTF)}`;
297
+ return runParsed(this.exec, cmd, parseListDir);
298
+ }
299
+ stat(path) {
300
+ return runParsed(this.exec, `stat --printf ${quote(STAT_PRINTF)} ${quote(path)}`, parseStat);
301
+ }
302
+ async readFile(path) {
303
+ return runParsed(this.exec, `base64 -w0 ${quote(path)}`, (b64) => Uint8Array.from(Buffer.from(b64.trim(), "base64")));
304
+ }
305
+ async writeFile(path, contents) {
306
+ const b64 = Buffer.from(contents).toString("base64");
307
+ const cmd = `printf %s ${quote(b64)} | base64 -d > ${quote(path)}`;
308
+ const { stderr, exitCode } = await this.exec.exec(cmd);
309
+ if (exitCode !== 0) {
310
+ return { kind: "failed", raw: stderr, exitCode, reason: stderr.trim() || "write failed" };
311
+ }
312
+ return ok(void 0, "");
313
+ }
314
+ systemMetrics() {
315
+ return runParsed(this.exec, METRICS_COMMAND, parseSystemMetrics);
316
+ }
317
+ listProcesses() {
318
+ return Promise.resolve(unsupported("listProcesses is a post-v1 capability"));
319
+ }
320
+ listServices() {
321
+ return Promise.resolve(unsupported("listServices is a post-v1 capability"));
322
+ }
323
+ };
324
+
325
+ // ../core/dist/adapters/registry.js
326
+ function createUnsupportedAdapter(reason) {
327
+ const fail = () => Promise.resolve(unsupported(reason));
328
+ return {
329
+ listDir: fail,
330
+ stat: fail,
331
+ readFile: fail,
332
+ writeFile: fail,
333
+ systemMetrics: fail,
334
+ listProcesses: fail,
335
+ listServices: fail
336
+ };
337
+ }
338
+ function selectAdapter(os, executor) {
339
+ if (os.family === "debian")
340
+ return new DebianAdapter(executor);
341
+ return createUnsupportedAdapter(`OS family "${os.family}" is not supported in v1 (Debian/Ubuntu/Mint only); see the host roadmap`);
342
+ }
343
+
344
+ // ../core/dist/session/ssh-session.js
345
+ import { createHash } from "node:crypto";
346
+ import { Client } from "ssh2";
347
+ function fingerprint(key) {
348
+ return `SHA256:${createHash("sha256").update(key).digest("base64").replace(/=+$/, "")}`;
349
+ }
350
+ function keyAlgorithm(key) {
351
+ if (key.length < 4)
352
+ return "";
353
+ const len = key.readUInt32BE(0);
354
+ if (len <= 0 || key.length < 4 + len)
355
+ return "";
356
+ return key.subarray(4, 4 + len).toString("ascii");
357
+ }
358
+ var SshSession = class _SshSession {
359
+ options;
360
+ client = new Client();
361
+ currentState = "idle";
362
+ label;
363
+ constructor(options) {
364
+ this.options = options;
365
+ this.label = `${options.username}@${options.host}`;
366
+ }
367
+ /** Human-readable host label, e.g. "user@host", for the transparency log. */
368
+ get host() {
369
+ return this.label;
370
+ }
371
+ get state() {
372
+ return this.currentState;
373
+ }
374
+ /** Open and authenticate a session. Rejects on auth, host-key or network error. */
375
+ static connect(options) {
376
+ const session = new _SshSession(options);
377
+ return session.open().then(() => session);
378
+ }
379
+ open() {
380
+ this.currentState = "connecting";
381
+ const { host, port = 22, username, auth, verifyHostKey, timeoutMs = 2e4 } = this.options;
382
+ const config = {
383
+ host,
384
+ port,
385
+ username,
386
+ readyTimeout: timeoutMs,
387
+ hostVerifier: (key, verify) => {
388
+ const decide = verifyHostKey ? verifyHostKey({ algorithm: keyAlgorithm(key), fingerprint: fingerprint(key) }) : false;
389
+ Promise.resolve(decide).then(verify).catch(() => verify(false));
390
+ }
391
+ };
392
+ if (auth.kind === "password") {
393
+ config.password = auth.password;
394
+ } else {
395
+ config.privateKey = auth.privateKey;
396
+ if (auth.passphrase !== void 0)
397
+ config.passphrase = auth.passphrase;
398
+ }
399
+ return new Promise((resolve, reject) => {
400
+ this.client.on("ready", () => {
401
+ this.currentState = "connected";
402
+ resolve();
403
+ }).on("error", (err) => {
404
+ this.currentState = "error";
405
+ reject(err);
406
+ }).on("close", () => {
407
+ if (this.currentState !== "error")
408
+ this.currentState = "closed";
409
+ }).connect(config);
410
+ });
411
+ }
412
+ /** Run a command, capturing stdout/stderr and the exit code (FR-030 building block). */
413
+ exec(command) {
414
+ return new Promise((resolve, reject) => {
415
+ this.client.exec(command, (err, stream) => {
416
+ if (err)
417
+ return reject(err);
418
+ let stdout = "";
419
+ let stderr = "";
420
+ let exitCode = null;
421
+ stream.on("data", (chunk) => {
422
+ stdout += chunk.toString("utf8");
423
+ }).on("close", (code) => {
424
+ exitCode = typeof code === "number" ? code : null;
425
+ resolve({ stdout, stderr, exitCode });
426
+ });
427
+ stream.stderr.on("data", (chunk) => {
428
+ stderr += chunk.toString("utf8");
429
+ });
430
+ });
431
+ });
432
+ }
433
+ /** Open an interactive PTY/shell channel (Terminal app, FR-030/031). */
434
+ shell(window) {
435
+ return new Promise((resolve, reject) => {
436
+ this.client.shell(window ?? {}, (err, stream) => {
437
+ if (err)
438
+ return reject(err);
439
+ resolve(stream);
440
+ });
441
+ });
442
+ }
443
+ /** Open a PTY as a transport-agnostic {@link PtySession} (no ssh2 types leak). */
444
+ async openPty(cols = 80, rows = 24) {
445
+ const stream = await this.shell({ cols, rows, term: "xterm-256color" });
446
+ return {
447
+ onData: (listener) => {
448
+ stream.on("data", (chunk) => listener(chunk.toString("utf8")));
449
+ },
450
+ onClose: (listener) => {
451
+ stream.on("close", () => listener());
452
+ },
453
+ write: (data) => stream.write(data),
454
+ resize: (c, r) => stream.setWindow(r, c, 0, 0),
455
+ close: () => stream.close()
456
+ };
457
+ }
458
+ /** Open an SFTP channel (file manager / read-write capabilities). */
459
+ sftp() {
460
+ return new Promise((resolve, reject) => {
461
+ this.client.sftp((err, sftp) => {
462
+ if (err)
463
+ return reject(err);
464
+ resolve(sftp);
465
+ });
466
+ });
467
+ }
468
+ /** Close the session and release the connection. */
469
+ close() {
470
+ this.client.end();
471
+ this.currentState = "closed";
472
+ }
473
+ };
474
+
475
+ // ../server/dist/opener.js
476
+ var HostKeyUnknownError = class extends Error {
477
+ fingerprint;
478
+ algorithm;
479
+ constructor(fingerprint2, algorithm) {
480
+ super("Host key not trusted");
481
+ this.fingerprint = fingerprint2;
482
+ this.algorithm = algorithm;
483
+ this.name = "HostKeyUnknownError";
484
+ }
485
+ };
486
+ var HostKeyMismatchError = class extends Error {
487
+ fingerprint;
488
+ expected;
489
+ constructor(fingerprint2, expected) {
490
+ super("Host key mismatch \u2014 possible man-in-the-middle");
491
+ this.fingerprint = fingerprint2;
492
+ this.expected = expected;
493
+ this.name = "HostKeyMismatchError";
494
+ }
495
+ };
496
+ function createSshOpener(store) {
497
+ return async (req) => {
498
+ const hostPort = `${req.host}:${req.port ?? 22}`;
499
+ const known = store.get(hostPort);
500
+ let presented;
501
+ let rejection;
502
+ const session = await SshSession.connect({
503
+ host: req.host,
504
+ port: req.port ?? 22,
505
+ username: req.username,
506
+ auth: req.auth,
507
+ verifyHostKey: ({ fingerprint: fingerprint2, algorithm }) => {
508
+ presented = { fingerprint: fingerprint2, algorithm };
509
+ if (known) {
510
+ if (fingerprint2 === known)
511
+ return true;
512
+ rejection = "mismatch";
513
+ return false;
514
+ }
515
+ if (req.trustFingerprint && req.trustFingerprint === fingerprint2)
516
+ return true;
517
+ rejection = "unknown";
518
+ return false;
519
+ }
520
+ }).catch((err) => {
521
+ if (rejection === "unknown" && presented) {
522
+ throw new HostKeyUnknownError(presented.fingerprint, presented.algorithm);
523
+ }
524
+ if (rejection === "mismatch" && presented && known) {
525
+ throw new HostKeyMismatchError(presented.fingerprint, known);
526
+ }
527
+ throw err;
528
+ });
529
+ if (presented && !known)
530
+ store.add(hostPort, presented.fingerprint);
531
+ const log = new TransparencyLog();
532
+ const executor = withTransparency(session, log, session.host);
533
+ const os = await detectOs(executor);
534
+ const adapter = selectAdapter(os, executor);
535
+ const home = (await executor.exec('echo "$HOME"')).stdout.trim() || "/";
536
+ return {
537
+ host: session.host,
538
+ home,
539
+ os,
540
+ adapter,
541
+ log,
542
+ openPty: (cols, rows) => session.openPty(cols, rows),
543
+ close: () => session.close()
544
+ };
545
+ };
546
+ }
547
+
548
+ // ../server/dist/known-hosts.js
549
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
550
+ import { homedir } from "node:os";
551
+ import { dirname, join } from "node:path";
552
+ var InMemoryKnownHosts = class {
553
+ map = /* @__PURE__ */ new Map();
554
+ get(hostPort) {
555
+ return this.map.get(hostPort);
556
+ }
557
+ add(hostPort, fingerprint2) {
558
+ this.map.set(hostPort, fingerprint2);
559
+ }
560
+ };
561
+ var FileKnownHosts = class {
562
+ path;
563
+ map;
564
+ constructor(path = join(homedir(), ".deskssh", "known_hosts.json")) {
565
+ this.path = path;
566
+ this.map = new Map(Object.entries(load(path)));
567
+ }
568
+ get(hostPort) {
569
+ return this.map.get(hostPort);
570
+ }
571
+ add(hostPort, fingerprint2) {
572
+ this.map.set(hostPort, fingerprint2);
573
+ mkdirSync(dirname(this.path), { recursive: true });
574
+ writeFileSync(this.path, JSON.stringify(Object.fromEntries(this.map), null, 2), {
575
+ mode: 384
576
+ });
577
+ }
578
+ };
579
+ function load(path) {
580
+ if (!existsSync(path))
581
+ return {};
582
+ try {
583
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
584
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
585
+ } catch {
586
+ return {};
587
+ }
588
+ }
589
+
590
+ // ../server/dist/terminal.js
591
+ import { WebSocketServer } from "ws";
592
+ var TERMINAL_PATH = "/api/terminal";
593
+ function attachTerminal(server, manager) {
594
+ const wss = new WebSocketServer({ noServer: true });
595
+ server.on("upgrade", (req, socket, head) => {
596
+ const url = new URL(req.url ?? "/", "http://localhost");
597
+ if (url.pathname !== TERMINAL_PATH)
598
+ return;
599
+ const sessionId = url.searchParams.get("sessionId") ?? "";
600
+ const entry = manager.get(sessionId);
601
+ if (!entry?.openPty) {
602
+ socket.destroy();
603
+ return;
604
+ }
605
+ wss.handleUpgrade(req, socket, head, (ws) => bridge(ws, entry.openPty));
606
+ });
607
+ return wss;
608
+ }
609
+ function bridge(ws, openPty) {
610
+ void openPty(80, 24).then((pty) => {
611
+ pty.onData((chunk) => {
612
+ if (ws.readyState === ws.OPEN)
613
+ ws.send(JSON.stringify({ type: "output", data: chunk }));
614
+ });
615
+ pty.onClose(() => ws.close());
616
+ ws.on("message", (raw) => {
617
+ let msg;
618
+ try {
619
+ msg = JSON.parse(raw.toString("utf8"));
620
+ } catch {
621
+ return;
622
+ }
623
+ if (msg.type === "input")
624
+ pty.write(msg.data);
625
+ else if (msg.type === "resize")
626
+ pty.resize(msg.cols, msg.rows);
627
+ });
628
+ ws.on("close", () => pty.close());
629
+ }).catch(() => ws.close());
630
+ }
631
+
632
+ // ../server/dist/static.js
633
+ import { createReadStream, existsSync as existsSync2, statSync } from "node:fs";
634
+ import { join as join2, normalize, extname } from "node:path";
635
+ var MIME = {
636
+ ".html": "text/html; charset=utf-8",
637
+ ".js": "text/javascript; charset=utf-8",
638
+ ".css": "text/css; charset=utf-8",
639
+ ".json": "application/json; charset=utf-8",
640
+ ".svg": "image/svg+xml",
641
+ ".png": "image/png",
642
+ ".jpg": "image/jpeg",
643
+ ".ico": "image/x-icon",
644
+ ".woff": "font/woff",
645
+ ".woff2": "font/woff2"
646
+ };
647
+ function serveStatic(dir, urlPath, res) {
648
+ const rel = normalize(decodeURIComponent(urlPath)).replace(/^(\.\.[/\\])+/, "");
649
+ let filePath = join2(dir, rel);
650
+ if (!filePath.startsWith(dir))
651
+ return false;
652
+ if (!existsSync2(filePath) || statSync(filePath).isDirectory()) {
653
+ const indexPath = join2(dir, "index.html");
654
+ if (!existsSync2(indexPath))
655
+ return false;
656
+ filePath = indexPath;
657
+ }
658
+ res.writeHead(200, { "content-type": MIME[extname(filePath)] ?? "application/octet-stream" });
659
+ createReadStream(filePath).pipe(res);
660
+ return true;
661
+ }
662
+
663
+ // ../server/dist/gateway.js
664
+ var MAX_BODY_BYTES = 1e6;
665
+ var HttpError = class extends Error {
666
+ status;
667
+ constructor(status, message) {
668
+ super(message);
669
+ this.status = status;
670
+ }
671
+ };
672
+ function readJsonBody(req) {
673
+ return new Promise((resolve, reject) => {
674
+ let size = 0;
675
+ const chunks = [];
676
+ req.on("data", (chunk) => {
677
+ size += chunk.length;
678
+ if (size > MAX_BODY_BYTES) {
679
+ reject(new HttpError(413, "Request body too large"));
680
+ req.destroy();
681
+ return;
682
+ }
683
+ chunks.push(chunk);
684
+ });
685
+ req.on("end", () => {
686
+ const raw = Buffer.concat(chunks).toString("utf8");
687
+ if (!raw)
688
+ return resolve({});
689
+ try {
690
+ resolve(JSON.parse(raw));
691
+ } catch {
692
+ reject(new HttpError(400, "Invalid JSON body"));
693
+ }
694
+ });
695
+ req.on("error", reject);
696
+ });
697
+ }
698
+ function sendJson(res, status, body) {
699
+ const payload = JSON.stringify(body);
700
+ res.writeHead(status, {
701
+ "content-type": "application/json; charset=utf-8",
702
+ "content-length": Buffer.byteLength(payload)
703
+ });
704
+ res.end(payload);
705
+ }
706
+ function asString(obj, key) {
707
+ const value = obj[key];
708
+ if (typeof value !== "string" || value.length === 0) {
709
+ throw new HttpError(400, `Missing or invalid "${key}"`);
710
+ }
711
+ return value;
712
+ }
713
+ function createGateway(deps = {}) {
714
+ const manager = deps.manager ?? new SessionManager();
715
+ const opener = deps.opener ?? createSshOpener(new FileKnownHosts());
716
+ const server = createServer((req, res) => {
717
+ handle(req, res, manager, opener, deps.staticDir).catch((err) => {
718
+ const status = err instanceof HttpError ? err.status : 500;
719
+ const message = err instanceof Error ? err.message : "Internal error";
720
+ if (!res.headersSent)
721
+ sendJson(res, status, { error: message });
722
+ });
723
+ });
724
+ attachTerminal(server, manager);
725
+ return server;
726
+ }
727
+ async function handle(req, res, manager, opener, staticDir) {
728
+ const url = new URL(req.url ?? "/", "http://localhost");
729
+ const route = `${req.method ?? "GET"} ${url.pathname}`;
730
+ if (req.method === "GET" && !url.pathname.startsWith("/api/") && staticDir) {
731
+ if (serveStatic(staticDir, url.pathname, res))
732
+ return;
733
+ }
734
+ if (route === "GET /api/health")
735
+ return sendJson(res, 200, { ok: true });
736
+ if (route === "POST /api/connect") {
737
+ const body = await readJsonBody(req);
738
+ const auth = parseAuth(body["auth"]);
739
+ const request = {
740
+ host: asString(body, "host"),
741
+ port: body["port"] === void 0 ? void 0 : Number(body["port"]),
742
+ username: asString(body, "username"),
743
+ auth,
744
+ ...typeof body["trustFingerprint"] === "string" ? { trustFingerprint: body["trustFingerprint"] } : {}
745
+ };
746
+ try {
747
+ const entry = manager.add(await opener(request));
748
+ return sendJson(res, 200, { status: "connected", ...toSessionInfo(entry) });
749
+ } catch (err) {
750
+ if (err instanceof HostKeyUnknownError) {
751
+ return sendJson(res, 200, {
752
+ status: "verify-host-key",
753
+ fingerprint: err.fingerprint,
754
+ algorithm: err.algorithm
755
+ });
756
+ }
757
+ if (err instanceof HostKeyMismatchError) {
758
+ return sendJson(res, 409, {
759
+ error: err.message,
760
+ fingerprint: err.fingerprint,
761
+ expected: err.expected
762
+ });
763
+ }
764
+ throw err;
765
+ }
766
+ }
767
+ if (route === "POST /api/disconnect") {
768
+ const body = await readJsonBody(req);
769
+ return sendJson(res, 200, { ok: manager.remove(asString(body, "sessionId")) });
770
+ }
771
+ if (route === "POST /api/listdir") {
772
+ const body = await readJsonBody(req);
773
+ const entry = requireSession(manager, body);
774
+ const path = typeof body["path"] === "string" && body["path"] ? body["path"] : entry.home;
775
+ const result = await entry.adapter.listDir(path);
776
+ return sendJson(res, 200, { path, result, transparency: entry.log.list() });
777
+ }
778
+ if (route === "POST /api/metrics") {
779
+ const body = await readJsonBody(req);
780
+ const entry = requireSession(manager, body);
781
+ return sendJson(res, 200, { result: await entry.adapter.systemMetrics() });
782
+ }
783
+ if (route === "POST /api/readfile") {
784
+ const body = await readJsonBody(req);
785
+ const entry = requireSession(manager, body);
786
+ const result = await entry.adapter.readFile(asString(body, "path"));
787
+ if (result.kind === "ok") {
788
+ return sendJson(res, 200, {
789
+ result: { kind: "ok", base64: Buffer.from(result.value).toString("base64") }
790
+ });
791
+ }
792
+ return sendJson(res, 200, { result });
793
+ }
794
+ if (route === "POST /api/writefile") {
795
+ const body = await readJsonBody(req);
796
+ const entry = requireSession(manager, body);
797
+ const bytes = Buffer.from(asString(body, "base64"), "base64");
798
+ const result = await entry.adapter.writeFile(asString(body, "path"), new Uint8Array(bytes));
799
+ return sendJson(res, 200, { result });
800
+ }
801
+ sendJson(res, 404, { error: `No route for ${route}` });
802
+ }
803
+ function requireSession(manager, body) {
804
+ const entry = manager.get(asString(body, "sessionId"));
805
+ if (!entry)
806
+ throw new HttpError(404, "Unknown session");
807
+ return entry;
808
+ }
809
+ function parseAuth(value) {
810
+ if (typeof value !== "object" || value === null)
811
+ throw new HttpError(400, 'Missing "auth"');
812
+ const auth = value;
813
+ if (auth["kind"] === "password" && typeof auth["password"] === "string") {
814
+ return { kind: "password", password: auth["password"] };
815
+ }
816
+ if (auth["kind"] === "privateKey" && typeof auth["privateKey"] === "string") {
817
+ return {
818
+ kind: "privateKey",
819
+ privateKey: auth["privateKey"],
820
+ ...typeof auth["passphrase"] === "string" ? { passphrase: auth["passphrase"] } : {}
821
+ };
822
+ }
823
+ throw new HttpError(400, 'Invalid "auth": expected password or privateKey');
824
+ }
825
+
826
+ // ../server/dist/index.js
827
+ var SERVER_PACKAGE = "@deskssh/server";
828
+ function startGateway(options = {}) {
829
+ const port = options.port ?? Number(process.env["PORT"] ?? 8717);
830
+ const host = options.host ?? process.env["HOST"] ?? "127.0.0.1";
831
+ const server = createGateway({ staticDir: options.staticDir });
832
+ server.listen(port, host, () => {
833
+ console.log(`DeskSSH listening on http://${host}:${String(port)}`);
834
+ });
835
+ return server;
836
+ }
837
+ if (import.meta.url === `file://${process.argv[1] ?? ""}`) {
838
+ startGateway();
839
+ }
840
+ export {
841
+ FileKnownHosts,
842
+ HostKeyMismatchError,
843
+ HostKeyUnknownError,
844
+ InMemoryKnownHosts,
845
+ SERVER_PACKAGE,
846
+ SessionManager,
847
+ createGateway,
848
+ createSshOpener,
849
+ startGateway,
850
+ toSessionInfo
851
+ };