@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/dist/index.d.ts CHANGED
@@ -261,8 +261,8 @@ interface TransportClientOptions {
261
261
  }
262
262
  declare class TransportClient {
263
263
  readonly transport: Transport;
264
- private readonly token?;
265
- private readonly userAgent;
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 path from 'path';
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, path2, status, body) {
22
- super(`slicer ${method} ${path2} failed: ${status} ${body}`);
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 = path2;
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 = path.join(os.homedir(), candidate.slice(2));
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(path2) {
307
- const q = new URLSearchParams({ path: path2 });
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(path2) {
315
- const q = new URLSearchParams({ path: path2 });
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(path2) {
328
- return await this.stat(path2) !== null;
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(path2, recursive = false) {
342
- const q = new URLSearchParams({ path: path2, recursive: String(recursive) });
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(path2) {
349
- const q = new URLSearchParams({ path: path2, mode: "binary" });
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(path2, content, opts = {}) {
356
- const q = new URLSearchParams({ path: path2, mode: "binary" });
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(path2, tar) {
369
- const q = new URLSearchParams({ path: path2, mode: "tar" });
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(path2) {
389
- const q = new URLSearchParams({ path: path2, mode: "tar" });
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: path2, body } = buildExecPath(this.hostname, req, false);
503
- for await (const frame of this.transport.requestNDJSON("POST", path2, body)) {
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: path2, body } = buildExecPath(this.hostname, req, true);
517
- const raw = await this.transport.requestRaw("POST", path2, body);
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