@slicervm/sdk 0.1.0 → 0.1.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/README.md +1 -1
- package/dist/index.cjs +309 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +105 -3
- package/dist/index.d.ts +105 -3
- package/dist/index.js +305 -26
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
package/dist/index.d.ts
CHANGED
|
@@ -261,8 +261,8 @@ interface TransportClientOptions {
|
|
|
261
261
|
}
|
|
262
262
|
declare class TransportClient {
|
|
263
263
|
readonly transport: Transport;
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
readonly token?: string;
|
|
265
|
+
readonly userAgent: string;
|
|
266
266
|
constructor(opts: TransportClientOptions);
|
|
267
267
|
private agent;
|
|
268
268
|
private buildRequestOptions;
|
|
@@ -276,6 +276,94 @@ declare class TransportClient {
|
|
|
276
276
|
requestNDJSON<Frame = unknown>(method: string, reqPath: string, body?: Buffer | Readable): AsyncGenerator<Frame, void, void>;
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Port forwarding for Slicer VMs.
|
|
281
|
+
*
|
|
282
|
+
* Per-connection WebSocket model: each accepted local TCP/Unix connection
|
|
283
|
+
* opens a fresh WebSocket to /vm/{hostname}/forward on the daemon. The
|
|
284
|
+
* WebSocket carries raw bytes both directions (binary frames, no framing
|
|
285
|
+
* subprotocol). The daemon uses the `X-Inlets-Upstream` header on the
|
|
286
|
+
* upgrade request to decide where to dial inside the VM.
|
|
287
|
+
*/
|
|
288
|
+
|
|
289
|
+
interface AddressMapping {
|
|
290
|
+
rawSpec: string;
|
|
291
|
+
/** TCP listen address, or undefined when listening on a Unix socket. */
|
|
292
|
+
listenAddr?: string;
|
|
293
|
+
/** TCP listen port, or undefined when listening on a Unix socket. */
|
|
294
|
+
listenPort?: number;
|
|
295
|
+
/** Listen Unix socket path, or undefined when listening on TCP. */
|
|
296
|
+
listenUnixPath?: string;
|
|
297
|
+
/** Remote host inside the VM, or undefined when targeting a Unix socket. */
|
|
298
|
+
remoteHost?: string;
|
|
299
|
+
/** Remote port inside the VM, or undefined when targeting a Unix socket. */
|
|
300
|
+
remotePort?: number;
|
|
301
|
+
/** Remote Unix socket path inside the VM, or undefined when targeting TCP. */
|
|
302
|
+
remoteUnixPath?: string;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Parse a `-L`-style spec into an {@link AddressMapping}. Supported formats
|
|
306
|
+
* mirror the Go SDK's `slicer vm forward` CLI:
|
|
307
|
+
*
|
|
308
|
+
* - `127.0.0.1:9000` — listen and forward on the same TCP host:port
|
|
309
|
+
* - `9001:127.0.0.1:9000` — listen on `0.0.0.0:9001`, forward to `127.0.0.1:9000`
|
|
310
|
+
* - `0:127.0.0.1:9000` — listen on a random TCP port, forward as above
|
|
311
|
+
* - `0.0.0.0:9000:127.0.0.1:9000` — listen and forward, fully explicit
|
|
312
|
+
* - `127.0.0.1:9000:/var/run/docker.sock` — TCP listen, Unix socket forward
|
|
313
|
+
* - `9000:/var/run/docker.sock` — `0.0.0.0:9000` listen, Unix socket forward
|
|
314
|
+
* - `/tmp/docker.sock:/var/run/docker.sock` — Unix-to-Unix forward
|
|
315
|
+
* - `./docker.sock:/var/run/docker.sock` — Unix-to-Unix with relative local
|
|
316
|
+
*/
|
|
317
|
+
declare function parseAddressMapping(spec: string): AddressMapping;
|
|
318
|
+
interface ForwarderListener {
|
|
319
|
+
/** The original spec string that produced this listener. */
|
|
320
|
+
spec: string;
|
|
321
|
+
/** Human-readable local address (`127.0.0.1:8080`, `/tmp/docker.sock`, etc). */
|
|
322
|
+
local: string;
|
|
323
|
+
/** Human-readable upstream target inside the VM. */
|
|
324
|
+
remote: string;
|
|
325
|
+
/** Resolved port for TCP listeners (useful when caller asked for `0`). */
|
|
326
|
+
port?: number;
|
|
327
|
+
}
|
|
328
|
+
interface ForwarderOptions {
|
|
329
|
+
/** Identifies this client to the daemon. Defaults to `os.hostname()`. */
|
|
330
|
+
clientId?: string;
|
|
331
|
+
/** WebSocket dial timeout (ms). Default 10_000. */
|
|
332
|
+
dialTimeoutMs?: number;
|
|
333
|
+
/**
|
|
334
|
+
* Optional logger for connection events. Receives short strings. Default: silent.
|
|
335
|
+
*/
|
|
336
|
+
log?: (msg: string) => void;
|
|
337
|
+
}
|
|
338
|
+
interface ForwarderInit {
|
|
339
|
+
hostname: string;
|
|
340
|
+
transport: Transport;
|
|
341
|
+
token?: string;
|
|
342
|
+
userAgent: string;
|
|
343
|
+
specs: string[];
|
|
344
|
+
options?: ForwarderOptions;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* A live set of port forwards for one VM. Returned by `vm.forward(...)`.
|
|
348
|
+
* Closing the forwarder tears down all local listeners and any in-flight
|
|
349
|
+
* tunnel WebSockets.
|
|
350
|
+
*/
|
|
351
|
+
declare class Forwarder {
|
|
352
|
+
private readonly init;
|
|
353
|
+
private readonly mappings;
|
|
354
|
+
readonly listeners: ForwarderListener[];
|
|
355
|
+
private readonly servers;
|
|
356
|
+
private readonly liveSockets;
|
|
357
|
+
private closed;
|
|
358
|
+
private constructor();
|
|
359
|
+
static start(init: ForwarderInit): Promise<Forwarder>;
|
|
360
|
+
private bindAll;
|
|
361
|
+
private handleAccept;
|
|
362
|
+
/** Tear down all listeners and any in-flight tunnel sockets. */
|
|
363
|
+
close(): Promise<void>;
|
|
364
|
+
private log;
|
|
365
|
+
}
|
|
366
|
+
|
|
279
367
|
/**
|
|
280
368
|
* VM handle — returned from `client.vms.create()` / `client.vms.get()`.
|
|
281
369
|
* Exposes per-VM operations (exec, fs, power, lifecycle).
|
|
@@ -331,6 +419,20 @@ declare class VM {
|
|
|
331
419
|
suspend(): Promise<void>;
|
|
332
420
|
/** Mac-only on current daemons. Throws `SlicerAPIError 404` on Linux. */
|
|
333
421
|
restore(): Promise<void>;
|
|
422
|
+
/**
|
|
423
|
+
* Open one or more port forwards from the host to this VM. Each spec follows
|
|
424
|
+
* the same syntax as `slicer vm forward -L`:
|
|
425
|
+
*
|
|
426
|
+
* `127.0.0.1:9000` — listen and forward on the same TCP port
|
|
427
|
+
* `8081:127.0.0.1:8080` — listen on `0.0.0.0:8081`, forward to `127.0.0.1:8080`
|
|
428
|
+
* `0.0.0.0:8080:127.0.0.1:8080` — fully explicit
|
|
429
|
+
* `9000:/var/run/docker.sock` — TCP listen, Unix socket forward
|
|
430
|
+
* `/tmp/docker.sock:/var/run/docker.sock` — Unix-to-Unix
|
|
431
|
+
*
|
|
432
|
+
* Returns a {@link Forwarder} handle. Call `forwarder.close()` to tear down
|
|
433
|
+
* all listeners and any in-flight tunnel sockets.
|
|
434
|
+
*/
|
|
435
|
+
forward(specs: string | string[], options?: ForwarderOptions): Promise<Forwarder>;
|
|
334
436
|
/**
|
|
335
437
|
* Streaming exec — yields NDJSON frames (`started`, `stdout`, `stderr`, `exit`).
|
|
336
438
|
* When `req.stdio === 'base64'`, each frame's `data`/`stdout`/`stderr` string
|
|
@@ -421,4 +523,4 @@ declare class SlicerClient {
|
|
|
421
523
|
getInfo(): Promise<SlicerInfo>;
|
|
422
524
|
}
|
|
423
525
|
|
|
424
|
-
export { type AgentHealth, type CreateSecretRequest, type CreateVMOptions, type CreateVMRequest, type CreateVMResponse, type DeleteResponse, type ExecFrame, type ExecRequest, type ExecResult, type ExecResultBinary, type ExecStdio, ExecStdioBase64, ExecStdioText, type FSEntry, type FSMkdirRequest, GiB, type HostGroup, HostGroupsAPI, type ListOptions, MiB, NonRootUser, type Secret, SecretExistsError, SecretsAPI, type ShutdownRequest, SlicerAPIError, SlicerClient, type SlicerClientOptions, type SlicerInfo, type UpdateSecretRequest, VM, VMFileSystem, type VMInfo, type VMInit, type VMLogs, type VMSnapshot, type VMStat, VMsAPI, type WaitOptions, resolveTransport };
|
|
526
|
+
export { type AddressMapping, type AgentHealth, type CreateSecretRequest, type CreateVMOptions, type CreateVMRequest, type CreateVMResponse, type DeleteResponse, type ExecFrame, type ExecRequest, type ExecResult, type ExecResultBinary, type ExecStdio, ExecStdioBase64, ExecStdioText, type FSEntry, type FSMkdirRequest, Forwarder, type ForwarderListener, type ForwarderOptions, GiB, type HostGroup, HostGroupsAPI, type ListOptions, MiB, NonRootUser, type Secret, SecretExistsError, SecretsAPI, type ShutdownRequest, SlicerAPIError, SlicerClient, type SlicerClientOptions, type SlicerInfo, type UpdateSecretRequest, VM, VMFileSystem, type VMInfo, type VMInit, type VMLogs, type VMSnapshot, type VMStat, VMsAPI, type WaitOptions, parseAddressMapping, resolveTransport };
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,10 @@ import http from 'http';
|
|
|
2
2
|
import https from 'https';
|
|
3
3
|
import { URL } from 'url';
|
|
4
4
|
import os from 'os';
|
|
5
|
-
import
|
|
5
|
+
import path2 from 'path';
|
|
6
|
+
import net, { createServer } from 'net';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import { createWebSocketStream, WebSocket } from 'ws';
|
|
6
9
|
|
|
7
10
|
// src/types.ts
|
|
8
11
|
var ExecStdioText = "text";
|
|
@@ -18,11 +21,11 @@ var SlicerAPIError = class extends Error {
|
|
|
18
21
|
method;
|
|
19
22
|
path;
|
|
20
23
|
body;
|
|
21
|
-
constructor(method,
|
|
22
|
-
super(`slicer ${method} ${
|
|
24
|
+
constructor(method, path3, status, body) {
|
|
25
|
+
super(`slicer ${method} ${path3} failed: ${status} ${body}`);
|
|
23
26
|
this.name = "SlicerAPIError";
|
|
24
27
|
this.method = method;
|
|
25
|
-
this.path =
|
|
28
|
+
this.path = path3;
|
|
26
29
|
this.status = status;
|
|
27
30
|
this.body = body;
|
|
28
31
|
}
|
|
@@ -35,7 +38,7 @@ function resolveTransport(baseURL) {
|
|
|
35
38
|
if (!trimmed) throw new Error("Slicer baseURL is required");
|
|
36
39
|
let candidate = trimmed;
|
|
37
40
|
if (candidate.startsWith("unix://")) candidate = candidate.slice("unix://".length);
|
|
38
|
-
if (candidate.startsWith("~/")) candidate =
|
|
41
|
+
if (candidate.startsWith("~/")) candidate = path2.join(os.homedir(), candidate.slice(2));
|
|
39
42
|
const socketLike = candidate.startsWith("/") || candidate.startsWith("./") || candidate.startsWith("../") || candidate.endsWith(".sock");
|
|
40
43
|
if (socketLike) return { kind: "socket", socketPath: candidate };
|
|
41
44
|
return { kind: "net", url: new URL(trimmed) };
|
|
@@ -294,6 +297,258 @@ function secretFromWire(w) {
|
|
|
294
297
|
...w.modified_at !== void 0 && { modifiedAt: w.modified_at }
|
|
295
298
|
};
|
|
296
299
|
}
|
|
300
|
+
function looksLikeUnixSocketPath(s) {
|
|
301
|
+
if (!s) return false;
|
|
302
|
+
return s.startsWith("/") || s.startsWith("./") || s.startsWith("../") || s.includes("/");
|
|
303
|
+
}
|
|
304
|
+
function parseAddressMapping(spec) {
|
|
305
|
+
const lastColon = spec.lastIndexOf(":");
|
|
306
|
+
if (lastColon !== -1) {
|
|
307
|
+
const localPart = spec.slice(0, lastColon);
|
|
308
|
+
const remotePart = spec.slice(lastColon + 1);
|
|
309
|
+
if (looksLikeUnixSocketPath(localPart) && looksLikeUnixSocketPath(remotePart)) {
|
|
310
|
+
const localAbs = path2.isAbsolute(localPart) ? localPart : path2.resolve(localPart);
|
|
311
|
+
return { rawSpec: spec, listenUnixPath: localAbs, remoteUnixPath: remotePart };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const tcpToUnix = spec.indexOf(":/");
|
|
315
|
+
if (tcpToUnix !== -1) {
|
|
316
|
+
const listenPart = spec.slice(0, tcpToUnix);
|
|
317
|
+
const socketPath = spec.slice(tcpToUnix + 1);
|
|
318
|
+
const innerColon = listenPart.lastIndexOf(":");
|
|
319
|
+
if (innerColon === -1) {
|
|
320
|
+
return {
|
|
321
|
+
rawSpec: spec,
|
|
322
|
+
listenAddr: "0.0.0.0",
|
|
323
|
+
listenPort: parsePort(listenPart, spec),
|
|
324
|
+
remoteUnixPath: socketPath
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
rawSpec: spec,
|
|
329
|
+
listenAddr: listenPart.slice(0, innerColon),
|
|
330
|
+
listenPort: parsePort(listenPart.slice(innerColon + 1), spec),
|
|
331
|
+
remoteUnixPath: socketPath
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const parts = spec.split(":");
|
|
335
|
+
switch (parts.length) {
|
|
336
|
+
case 2:
|
|
337
|
+
return {
|
|
338
|
+
rawSpec: spec,
|
|
339
|
+
listenAddr: parts[0],
|
|
340
|
+
listenPort: parsePort(parts[1], spec),
|
|
341
|
+
remoteHost: parts[0],
|
|
342
|
+
remotePort: parsePort(parts[1], spec)
|
|
343
|
+
};
|
|
344
|
+
case 3:
|
|
345
|
+
return {
|
|
346
|
+
rawSpec: spec,
|
|
347
|
+
listenAddr: parts[1],
|
|
348
|
+
listenPort: parsePort(parts[0], spec),
|
|
349
|
+
remoteHost: parts[1],
|
|
350
|
+
remotePort: parsePort(parts[2], spec)
|
|
351
|
+
};
|
|
352
|
+
case 4:
|
|
353
|
+
return {
|
|
354
|
+
rawSpec: spec,
|
|
355
|
+
listenAddr: parts[0],
|
|
356
|
+
listenPort: parsePort(parts[1], spec),
|
|
357
|
+
remoteHost: parts[2],
|
|
358
|
+
remotePort: parsePort(parts[3], spec)
|
|
359
|
+
};
|
|
360
|
+
default:
|
|
361
|
+
throw new Error(`invalid forward spec ${JSON.stringify(spec)}: expected 2-4 colon-separated parts`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function parsePort(s, spec) {
|
|
365
|
+
const n = Number(s);
|
|
366
|
+
if (!Number.isInteger(n) || n < 0 || n > 65535) {
|
|
367
|
+
throw new Error(`invalid port ${JSON.stringify(s)} in forward spec ${JSON.stringify(spec)}`);
|
|
368
|
+
}
|
|
369
|
+
return n;
|
|
370
|
+
}
|
|
371
|
+
function isListenUnix(m) {
|
|
372
|
+
return m.listenUnixPath !== void 0;
|
|
373
|
+
}
|
|
374
|
+
function remoteTargetHeader(m) {
|
|
375
|
+
if (m.remoteUnixPath) return `unix:${m.remoteUnixPath}`;
|
|
376
|
+
return `${m.remoteHost}:${m.remotePort}`;
|
|
377
|
+
}
|
|
378
|
+
function listenAddressDescription(m) {
|
|
379
|
+
if (isListenUnix(m)) return m.listenUnixPath;
|
|
380
|
+
return `${m.listenAddr}:${m.listenPort}`;
|
|
381
|
+
}
|
|
382
|
+
var Forwarder = class _Forwarder {
|
|
383
|
+
constructor(init, mappings) {
|
|
384
|
+
this.init = init;
|
|
385
|
+
this.mappings = mappings;
|
|
386
|
+
}
|
|
387
|
+
init;
|
|
388
|
+
mappings;
|
|
389
|
+
listeners = [];
|
|
390
|
+
servers = [];
|
|
391
|
+
liveSockets = /* @__PURE__ */ new Set();
|
|
392
|
+
closed = false;
|
|
393
|
+
static async start(init) {
|
|
394
|
+
if (init.specs.length === 0) {
|
|
395
|
+
throw new Error("Forwarder requires at least one forward spec");
|
|
396
|
+
}
|
|
397
|
+
const mappings = init.specs.map(parseAddressMapping);
|
|
398
|
+
const fwd = new _Forwarder(init, mappings);
|
|
399
|
+
try {
|
|
400
|
+
await fwd.bindAll();
|
|
401
|
+
} catch (err) {
|
|
402
|
+
await fwd.close();
|
|
403
|
+
throw err;
|
|
404
|
+
}
|
|
405
|
+
return fwd;
|
|
406
|
+
}
|
|
407
|
+
async bindAll() {
|
|
408
|
+
for (const m of this.mappings) {
|
|
409
|
+
const server = createServer((socket) => this.handleAccept(m, socket));
|
|
410
|
+
const { local, port } = await listen(server, m);
|
|
411
|
+
this.servers.push(server);
|
|
412
|
+
this.listeners.push({
|
|
413
|
+
spec: m.rawSpec,
|
|
414
|
+
local,
|
|
415
|
+
remote: remoteTargetHeader(m),
|
|
416
|
+
...port !== void 0 && { port }
|
|
417
|
+
});
|
|
418
|
+
this.log(`listen ${local} \u2192 ${remoteTargetHeader(m)}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
handleAccept(mapping, socket) {
|
|
422
|
+
if (this.closed) {
|
|
423
|
+
socket.destroy();
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
socket.on("error", () => {
|
|
427
|
+
});
|
|
428
|
+
const ws = openWebSocket(this.init, mapping);
|
|
429
|
+
let closed = false;
|
|
430
|
+
const cleanup = () => {
|
|
431
|
+
if (closed) return;
|
|
432
|
+
closed = true;
|
|
433
|
+
try {
|
|
434
|
+
socket.destroy();
|
|
435
|
+
} catch {
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
ws.close();
|
|
439
|
+
} catch {
|
|
440
|
+
}
|
|
441
|
+
this.liveSockets.delete(handle);
|
|
442
|
+
};
|
|
443
|
+
const handle = { close: cleanup };
|
|
444
|
+
this.liveSockets.add(handle);
|
|
445
|
+
ws.binaryType = "nodebuffer";
|
|
446
|
+
ws.on("open", () => {
|
|
447
|
+
const wsStream = createWebSocketStream(ws);
|
|
448
|
+
wsStream.on("error", cleanup);
|
|
449
|
+
socket.pipe(wsStream);
|
|
450
|
+
wsStream.pipe(socket);
|
|
451
|
+
});
|
|
452
|
+
ws.on("close", cleanup);
|
|
453
|
+
ws.on("error", (err) => {
|
|
454
|
+
this.log(`tunnel error: ${err.message}`);
|
|
455
|
+
cleanup();
|
|
456
|
+
});
|
|
457
|
+
socket.on("close", cleanup);
|
|
458
|
+
}
|
|
459
|
+
/** Tear down all listeners and any in-flight tunnel sockets. */
|
|
460
|
+
async close() {
|
|
461
|
+
this.closed = true;
|
|
462
|
+
for (const handle of [...this.liveSockets]) handle.close();
|
|
463
|
+
this.liveSockets.clear();
|
|
464
|
+
await Promise.all(
|
|
465
|
+
this.servers.map(
|
|
466
|
+
(s) => new Promise((resolve) => {
|
|
467
|
+
s.close(() => resolve());
|
|
468
|
+
})
|
|
469
|
+
)
|
|
470
|
+
);
|
|
471
|
+
for (const m of this.mappings) {
|
|
472
|
+
if (isListenUnix(m) && m.listenUnixPath) {
|
|
473
|
+
try {
|
|
474
|
+
fs.rmSync(m.listenUnixPath, { force: true });
|
|
475
|
+
} catch {
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
log(msg) {
|
|
481
|
+
this.init.options?.log?.(msg);
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
function listen(server, mapping) {
|
|
485
|
+
return new Promise((resolve, reject) => {
|
|
486
|
+
server.once("error", reject);
|
|
487
|
+
if (isListenUnix(mapping)) {
|
|
488
|
+
const p = mapping.listenUnixPath;
|
|
489
|
+
try {
|
|
490
|
+
fs.rmSync(p, { force: true });
|
|
491
|
+
} catch {
|
|
492
|
+
}
|
|
493
|
+
server.listen(p, () => {
|
|
494
|
+
try {
|
|
495
|
+
fs.chmodSync(p, 432);
|
|
496
|
+
} catch {
|
|
497
|
+
}
|
|
498
|
+
resolve({ local: p });
|
|
499
|
+
});
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
server.listen({ host: mapping.listenAddr, port: mapping.listenPort }, () => {
|
|
503
|
+
const addr = server.address();
|
|
504
|
+
if (addr && typeof addr === "object") {
|
|
505
|
+
const local = `${mapping.listenAddr}:${addr.port}`;
|
|
506
|
+
resolve({ local, port: addr.port });
|
|
507
|
+
} else {
|
|
508
|
+
resolve({ local: listenAddressDescription(mapping) });
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
function openWebSocket(init, mapping) {
|
|
514
|
+
const url = wsURLForVM(init.transport, init.hostname);
|
|
515
|
+
const headers = {
|
|
516
|
+
"X-Inlets-Client-ID": init.options?.clientId ?? os.hostname(),
|
|
517
|
+
"X-Inlets-Mode": "local",
|
|
518
|
+
"X-Inlets-Upstream": remoteTargetHeader(mapping),
|
|
519
|
+
"User-Agent": init.userAgent
|
|
520
|
+
};
|
|
521
|
+
if (init.token) headers["Authorization"] = `Bearer ${init.token}`;
|
|
522
|
+
const opts = {
|
|
523
|
+
headers,
|
|
524
|
+
handshakeTimeout: init.options?.dialTimeoutMs ?? 1e4
|
|
525
|
+
};
|
|
526
|
+
if (init.transport.kind === "socket") {
|
|
527
|
+
opts.agent = unixAgent(init.transport.socketPath);
|
|
528
|
+
}
|
|
529
|
+
return new WebSocket(url, opts);
|
|
530
|
+
}
|
|
531
|
+
function wsURLForVM(transport, hostname) {
|
|
532
|
+
if (transport.kind === "socket") {
|
|
533
|
+
return `ws://localhost/vm/${encodeURIComponent(hostname)}/forward`;
|
|
534
|
+
}
|
|
535
|
+
const u = transport.url;
|
|
536
|
+
const scheme = u.protocol === "https:" ? "wss" : "ws";
|
|
537
|
+
const port = u.port ? `:${u.port}` : "";
|
|
538
|
+
return `${scheme}://${u.hostname}${port}/vm/${encodeURIComponent(hostname)}/forward`;
|
|
539
|
+
}
|
|
540
|
+
function unixAgent(socketPath) {
|
|
541
|
+
const agent = new http.Agent({ keepAlive: false });
|
|
542
|
+
agent.createConnection = ((_opts, cb) => {
|
|
543
|
+
const conn = net.createConnection({ path: socketPath });
|
|
544
|
+
if (cb) {
|
|
545
|
+
conn.once("connect", () => cb(null, conn));
|
|
546
|
+
conn.once("error", (err) => cb(err));
|
|
547
|
+
}
|
|
548
|
+
return conn;
|
|
549
|
+
});
|
|
550
|
+
return agent;
|
|
551
|
+
}
|
|
297
552
|
|
|
298
553
|
// src/vm.ts
|
|
299
554
|
var VMFileSystem = class {
|
|
@@ -303,16 +558,16 @@ var VMFileSystem = class {
|
|
|
303
558
|
}
|
|
304
559
|
transport;
|
|
305
560
|
hostname;
|
|
306
|
-
async readDir(
|
|
307
|
-
const q = new URLSearchParams({ path:
|
|
561
|
+
async readDir(path3) {
|
|
562
|
+
const q = new URLSearchParams({ path: path3 });
|
|
308
563
|
const wire = await this.transport.request(
|
|
309
564
|
"GET",
|
|
310
565
|
`/vm/${encodeURIComponent(this.hostname)}/fs/readdir?${q.toString()}`
|
|
311
566
|
);
|
|
312
567
|
return (wire ?? []).map(fsEntryFromWire);
|
|
313
568
|
}
|
|
314
|
-
async stat(
|
|
315
|
-
const q = new URLSearchParams({ path:
|
|
569
|
+
async stat(path3) {
|
|
570
|
+
const q = new URLSearchParams({ path: path3 });
|
|
316
571
|
try {
|
|
317
572
|
const wire = await this.transport.request(
|
|
318
573
|
"GET",
|
|
@@ -324,8 +579,8 @@ var VMFileSystem = class {
|
|
|
324
579
|
throw err;
|
|
325
580
|
}
|
|
326
581
|
}
|
|
327
|
-
async exists(
|
|
328
|
-
return await this.stat(
|
|
582
|
+
async exists(path3) {
|
|
583
|
+
return await this.stat(path3) !== null;
|
|
329
584
|
}
|
|
330
585
|
async mkdir(req) {
|
|
331
586
|
await this.transport.request(
|
|
@@ -338,22 +593,22 @@ var VMFileSystem = class {
|
|
|
338
593
|
}
|
|
339
594
|
);
|
|
340
595
|
}
|
|
341
|
-
async remove(
|
|
342
|
-
const q = new URLSearchParams({ path:
|
|
596
|
+
async remove(path3, recursive = false) {
|
|
597
|
+
const q = new URLSearchParams({ path: path3, recursive: String(recursive) });
|
|
343
598
|
await this.transport.request(
|
|
344
599
|
"DELETE",
|
|
345
600
|
`/vm/${encodeURIComponent(this.hostname)}/fs/remove?${q.toString()}`
|
|
346
601
|
);
|
|
347
602
|
}
|
|
348
|
-
async readFile(
|
|
349
|
-
const q = new URLSearchParams({ path:
|
|
603
|
+
async readFile(path3) {
|
|
604
|
+
const q = new URLSearchParams({ path: path3, mode: "binary" });
|
|
350
605
|
return this.transport.requestRaw(
|
|
351
606
|
"GET",
|
|
352
607
|
`/vm/${encodeURIComponent(this.hostname)}/cp?${q.toString()}`
|
|
353
608
|
);
|
|
354
609
|
}
|
|
355
|
-
async writeFile(
|
|
356
|
-
const q = new URLSearchParams({ path:
|
|
610
|
+
async writeFile(path3, content, opts = {}) {
|
|
611
|
+
const q = new URLSearchParams({ path: path3, mode: "binary" });
|
|
357
612
|
if (opts.uid !== void 0) q.set("uid", String(opts.uid));
|
|
358
613
|
if (opts.gid !== void 0) q.set("gid", String(opts.gid));
|
|
359
614
|
if (opts.permissions) q.set("permissions", opts.permissions);
|
|
@@ -365,8 +620,8 @@ var VMFileSystem = class {
|
|
|
365
620
|
);
|
|
366
621
|
}
|
|
367
622
|
/** Upload a tar archive, expanded into the VM at `path`. */
|
|
368
|
-
async tarTo(
|
|
369
|
-
const q = new URLSearchParams({ path:
|
|
623
|
+
async tarTo(path3, tar) {
|
|
624
|
+
const q = new URLSearchParams({ path: path3, mode: "tar" });
|
|
370
625
|
if (tar instanceof Buffer) {
|
|
371
626
|
await this.transport.requestRaw(
|
|
372
627
|
"POST",
|
|
@@ -385,8 +640,8 @@ var VMFileSystem = class {
|
|
|
385
640
|
for await (const _ of res) void _;
|
|
386
641
|
}
|
|
387
642
|
/** Download `path` from the VM as a tar archive. */
|
|
388
|
-
async tarFrom(
|
|
389
|
-
const q = new URLSearchParams({ path:
|
|
643
|
+
async tarFrom(path3) {
|
|
644
|
+
const q = new URLSearchParams({ path: path3, mode: "tar" });
|
|
390
645
|
return this.transport.requestRaw(
|
|
391
646
|
"GET",
|
|
392
647
|
`/vm/${encodeURIComponent(this.hostname)}/cp?${q.toString()}`
|
|
@@ -491,6 +746,30 @@ var VM = class {
|
|
|
491
746
|
async restore() {
|
|
492
747
|
await this.transport.request("POST", `/vm/${encodeURIComponent(this.hostname)}/restore`);
|
|
493
748
|
}
|
|
749
|
+
// --- port forwarding ---------------------------------------------------
|
|
750
|
+
/**
|
|
751
|
+
* Open one or more port forwards from the host to this VM. Each spec follows
|
|
752
|
+
* the same syntax as `slicer vm forward -L`:
|
|
753
|
+
*
|
|
754
|
+
* `127.0.0.1:9000` — listen and forward on the same TCP port
|
|
755
|
+
* `8081:127.0.0.1:8080` — listen on `0.0.0.0:8081`, forward to `127.0.0.1:8080`
|
|
756
|
+
* `0.0.0.0:8080:127.0.0.1:8080` — fully explicit
|
|
757
|
+
* `9000:/var/run/docker.sock` — TCP listen, Unix socket forward
|
|
758
|
+
* `/tmp/docker.sock:/var/run/docker.sock` — Unix-to-Unix
|
|
759
|
+
*
|
|
760
|
+
* Returns a {@link Forwarder} handle. Call `forwarder.close()` to tear down
|
|
761
|
+
* all listeners and any in-flight tunnel sockets.
|
|
762
|
+
*/
|
|
763
|
+
async forward(specs, options) {
|
|
764
|
+
return Forwarder.start({
|
|
765
|
+
hostname: this.hostname,
|
|
766
|
+
transport: this.transport.transport,
|
|
767
|
+
...this.transport.token !== void 0 && { token: this.transport.token },
|
|
768
|
+
userAgent: this.transport.userAgent,
|
|
769
|
+
specs: typeof specs === "string" ? [specs] : specs,
|
|
770
|
+
...options !== void 0 && { options }
|
|
771
|
+
});
|
|
772
|
+
}
|
|
494
773
|
// --- exec -------------------------------------------------------------
|
|
495
774
|
/**
|
|
496
775
|
* Streaming exec — yields NDJSON frames (`started`, `stdout`, `stderr`, `exit`).
|
|
@@ -499,8 +778,8 @@ var VM = class {
|
|
|
499
778
|
* `dataBytes`/`stdoutBytes`/`stderrBytes` Buffers alongside for convenience.
|
|
500
779
|
*/
|
|
501
780
|
async *exec(req) {
|
|
502
|
-
const { path:
|
|
503
|
-
for await (const frame of this.transport.requestNDJSON("POST",
|
|
781
|
+
const { path: path3, body } = buildExecPath(this.hostname, req, false);
|
|
782
|
+
for await (const frame of this.transport.requestNDJSON("POST", path3, body)) {
|
|
504
783
|
if (frame.encoding === "base64") {
|
|
505
784
|
if (frame.data) frame.dataBytes = Buffer.from(frame.data, "base64");
|
|
506
785
|
if (frame.stdout) frame.stdoutBytes = Buffer.from(frame.stdout, "base64");
|
|
@@ -513,8 +792,8 @@ var VM = class {
|
|
|
513
792
|
if (req.stdin !== void 0) {
|
|
514
793
|
throw new Error("stdin is not supported by execBuffered; use exec() instead");
|
|
515
794
|
}
|
|
516
|
-
const { path:
|
|
517
|
-
const raw = await this.transport.requestRaw("POST",
|
|
795
|
+
const { path: path3, body } = buildExecPath(this.hostname, req, true);
|
|
796
|
+
const raw = await this.transport.requestRaw("POST", path3, body);
|
|
518
797
|
const text = raw.toString("utf8");
|
|
519
798
|
const parsed = text ? JSON.parse(text) : {};
|
|
520
799
|
const common = {
|
|
@@ -738,6 +1017,6 @@ var SlicerClient = class _SlicerClient {
|
|
|
738
1017
|
}
|
|
739
1018
|
};
|
|
740
1019
|
|
|
741
|
-
export { ExecStdioBase64, ExecStdioText, GiB, HostGroupsAPI, MiB, NonRootUser, SecretExistsError, SecretsAPI, SlicerAPIError, SlicerClient, VM, VMFileSystem, VMsAPI, resolveTransport };
|
|
1020
|
+
export { ExecStdioBase64, ExecStdioText, Forwarder, GiB, HostGroupsAPI, MiB, NonRootUser, SecretExistsError, SecretsAPI, SlicerAPIError, SlicerClient, VM, VMFileSystem, VMsAPI, parseAddressMapping, resolveTransport };
|
|
742
1021
|
//# sourceMappingURL=index.js.map
|
|
743
1022
|
//# sourceMappingURL=index.js.map
|