@ornncompute/cli 1.0.134 → 1.0.135

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.md CHANGED
@@ -478,5 +478,6 @@ see [the service contract](../../services/orchestrator/README.md#file-upload-rep
478
478
  Staff can use `ornn admin ssh <node-id>` for certificate access on nodes that trust
479
479
  the environment's SSH CA. It requires an interactive staff login, creates a temporary
480
480
  local key, obtains a 15-minute certificate through the gateway and launches
481
- OpenSSH with normal host-key verification. Temporary files are removed when
482
- the command exits. Shared and per-node key access remain available.
481
+ OpenSSH with normal host-key verification. Nodes configured for the staff SSH
482
+ relay connect through Orchestrator automatically. Temporary files are removed
483
+ when the command exits.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ornncompute/cli",
3
- "version": "1.0.134",
3
+ "version": "1.0.135",
4
4
  "description": "Command-line interface for Ornn compute access workflows.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,7 +44,8 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "picocolors": "^1.1.1",
47
- "zod": "^4.6.7",
48
- "update-notifier": "7.3.1"
47
+ "update-notifier": "7.3.1",
48
+ "ws": "8.22.1",
49
+ "zod": "^4.6.7"
49
50
  }
50
51
  }
@@ -0,0 +1,49 @@
1
+ import { once } from "node:events";
2
+ import { createServer } from "node:net";
3
+ import WebSocket, { createWebSocketStream } from "ws";
4
+ import { loadRequiredSession, resolveApiBaseUrl, userCredentialHeaders } from "./api-client.mjs";
5
+
6
+ export async function openSshRelay(nodeId, { env, signal }) {
7
+ const session = await loadRequiredSession({ env });
8
+ const url = new URL(
9
+ `/v1/orchestrator/nodes/${encodeURIComponent(nodeId)}/ssh-relay`,
10
+ resolveApiBaseUrl({ env, session })
11
+ );
12
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
13
+ signal.throwIfAborted();
14
+ const ws = new WebSocket(url, {
15
+ headers: userCredentialHeaders(session),
16
+ handshakeTimeout: 12_000,
17
+ maxPayload: 128 * 1024,
18
+ });
19
+ const stream = createWebSocketStream(ws);
20
+ let client;
21
+ const server = createServer((socket) => {
22
+ if (client || stream.destroyed) {
23
+ socket.destroy();
24
+ return;
25
+ }
26
+ client = socket;
27
+ server.close();
28
+ socket.on("error", close);
29
+ socket.on("close", close);
30
+ socket.pipe(stream).pipe(socket);
31
+ });
32
+ function close() {
33
+ if (server.listening) server.close();
34
+ client?.destroy();
35
+ stream.destroy();
36
+ }
37
+ stream.on("error", close);
38
+ stream.on("close", close);
39
+ try {
40
+ await once(ws, "open", { signal });
41
+ server.listen({ port: 0, host: "127.0.0.1", signal });
42
+ await once(server, "listening", { signal });
43
+ if (stream.destroyed) throw new Error("The SSH relay closed before connecting.");
44
+ return { port: server.address().port, close };
45
+ } catch (error) {
46
+ close();
47
+ throw new Error(`Could not open the SSH relay. ${error.message}`, { cause: error });
48
+ }
49
+ }
package/src/admin-ssh.mjs CHANGED
@@ -4,6 +4,7 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
4
4
  import { constants, tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { ADMIN_MUTATION_SAFETY } from "./admin-mutations.mjs";
7
+ import { openSshRelay } from "./admin-ssh-relay.mjs";
7
8
  import { forwardSignals } from "./signals.mjs";
8
9
 
9
10
  function run(program, args, signal) {
@@ -20,7 +21,10 @@ function run(program, args, signal) {
20
21
  });
21
22
  }
22
23
 
23
- export async function openAdminSsh({ input, fetch, execute, runtime }, { runProcess = run } = {}) {
24
+ export async function openAdminSsh(
25
+ { input, fetch, execute, runtime },
26
+ { runProcess = run, openRelay = openSshRelay } = {}
27
+ ) {
24
28
  // Read the existing node route; issuance returns only its own certificate.
25
29
  const node = await orchestrator.getNode(fetch, input.node_id);
26
30
  const host = node.ip;
@@ -41,6 +45,7 @@ export async function openAdminSsh({ input, fetch, execute, runtime }, { runProc
41
45
  : controller.signal;
42
46
  let interrupted;
43
47
  let directory;
48
+ let relay;
44
49
  const removeSignals = forwardSignals(process, null, (received) => {
45
50
  interrupted ??= received;
46
51
  controller.abort();
@@ -66,6 +71,21 @@ export async function openAdminSsh({ input, fetch, execute, runtime }, { runProc
66
71
  )
67
72
  throw new Error("The issuer returned an invalid SSH certificate.");
68
73
  await writeFile(`${key}-cert.pub`, `${issued.certificate}\n`, { mode: 0o600 });
74
+ if (node.ssh_relay_configured === true) {
75
+ relay = await openRelay(input.node_id, { env: runtime.env, signal });
76
+ }
77
+ const relayOptions = relay
78
+ ? [
79
+ "-o",
80
+ "HostName=127.0.0.1",
81
+ "-o",
82
+ `HostKeyAlias=${port === 22 ? host : `[${host}]:${port}`}`,
83
+ "-o",
84
+ "ProxyCommand=none",
85
+ "-o",
86
+ "ControlPath=none",
87
+ ]
88
+ : [];
69
89
  const exitCode = await runProcess(
70
90
  "ssh",
71
91
  [
@@ -76,7 +96,8 @@ export async function openAdminSsh({ input, fetch, execute, runtime }, { runProc
76
96
  "-i",
77
97
  key,
78
98
  "-p",
79
- String(port),
99
+ String(relay?.port ?? port),
100
+ ...relayOptions,
80
101
  "-l",
81
102
  "admin",
82
103
  host,
@@ -90,6 +111,7 @@ export async function openAdminSsh({ input, fetch, execute, runtime }, { runProc
90
111
  return { suppressResult: true, exitCode: 128 + constants.signals[interrupted] };
91
112
  } finally {
92
113
  try {
114
+ relay?.close();
93
115
  if (directory) await rm(directory, { recursive: true, force: true });
94
116
  } finally {
95
117
  removeSignals();