@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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  TypeScript SDK for the [Slicer](https://slicervm.com) VM control-plane API.
4
4
 
5
- Mirrors the [Go SDK](https://github.com/slicervm/sdk) semantically, with a grouped TypeScript shape inspired by `@e2b` and `modal`. The top-level `SlicerClient` exposes `hostGroups`, `vms`, and `secrets` namespaces; per-VM operations live on a `VM` handle returned from `client.vms.create()` / `client.vms.attach()`.
5
+ Mirrors the [Go SDK](https://github.com/slicervm/sdk) semantically. The top-level `SlicerClient` exposes `hostGroups`, `vms`, and `secrets` namespaces for control-plane operations; per-VM operations live on a `VM` handle returned from `client.vms.create()` / `client.vms.attach()`.
6
6
 
7
7
  Supports Unix socket and HTTP(S) transports.
8
8
 
package/dist/index.cjs CHANGED
@@ -4,14 +4,19 @@ var http = require('http');
4
4
  var https = require('https');
5
5
  var url = require('url');
6
6
  var os = require('os');
7
- var path = require('path');
7
+ var path2 = require('path');
8
+ var net = require('net');
9
+ var fs = require('fs');
10
+ var ws = require('ws');
8
11
 
9
12
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
10
13
 
11
14
  var http__default = /*#__PURE__*/_interopDefault(http);
12
15
  var https__default = /*#__PURE__*/_interopDefault(https);
13
16
  var os__default = /*#__PURE__*/_interopDefault(os);
14
- var path__default = /*#__PURE__*/_interopDefault(path);
17
+ var path2__default = /*#__PURE__*/_interopDefault(path2);
18
+ var net__default = /*#__PURE__*/_interopDefault(net);
19
+ var fs__default = /*#__PURE__*/_interopDefault(fs);
15
20
 
16
21
  // src/types.ts
17
22
  var ExecStdioText = "text";
@@ -27,11 +32,11 @@ var SlicerAPIError = class extends Error {
27
32
  method;
28
33
  path;
29
34
  body;
30
- constructor(method, path2, status, body) {
31
- super(`slicer ${method} ${path2} failed: ${status} ${body}`);
35
+ constructor(method, path3, status, body) {
36
+ super(`slicer ${method} ${path3} failed: ${status} ${body}`);
32
37
  this.name = "SlicerAPIError";
33
38
  this.method = method;
34
- this.path = path2;
39
+ this.path = path3;
35
40
  this.status = status;
36
41
  this.body = body;
37
42
  }
@@ -44,7 +49,7 @@ function resolveTransport(baseURL) {
44
49
  if (!trimmed) throw new Error("Slicer baseURL is required");
45
50
  let candidate = trimmed;
46
51
  if (candidate.startsWith("unix://")) candidate = candidate.slice("unix://".length);
47
- if (candidate.startsWith("~/")) candidate = path__default.default.join(os__default.default.homedir(), candidate.slice(2));
52
+ if (candidate.startsWith("~/")) candidate = path2__default.default.join(os__default.default.homedir(), candidate.slice(2));
48
53
  const socketLike = candidate.startsWith("/") || candidate.startsWith("./") || candidate.startsWith("../") || candidate.endsWith(".sock");
49
54
  if (socketLike) return { kind: "socket", socketPath: candidate };
50
55
  return { kind: "net", url: new url.URL(trimmed) };
@@ -303,6 +308,258 @@ function secretFromWire(w) {
303
308
  ...w.modified_at !== void 0 && { modifiedAt: w.modified_at }
304
309
  };
305
310
  }
311
+ function looksLikeUnixSocketPath(s) {
312
+ if (!s) return false;
313
+ return s.startsWith("/") || s.startsWith("./") || s.startsWith("../") || s.includes("/");
314
+ }
315
+ function parseAddressMapping(spec) {
316
+ const lastColon = spec.lastIndexOf(":");
317
+ if (lastColon !== -1) {
318
+ const localPart = spec.slice(0, lastColon);
319
+ const remotePart = spec.slice(lastColon + 1);
320
+ if (looksLikeUnixSocketPath(localPart) && looksLikeUnixSocketPath(remotePart)) {
321
+ const localAbs = path2__default.default.isAbsolute(localPart) ? localPart : path2__default.default.resolve(localPart);
322
+ return { rawSpec: spec, listenUnixPath: localAbs, remoteUnixPath: remotePart };
323
+ }
324
+ }
325
+ const tcpToUnix = spec.indexOf(":/");
326
+ if (tcpToUnix !== -1) {
327
+ const listenPart = spec.slice(0, tcpToUnix);
328
+ const socketPath = spec.slice(tcpToUnix + 1);
329
+ const innerColon = listenPart.lastIndexOf(":");
330
+ if (innerColon === -1) {
331
+ return {
332
+ rawSpec: spec,
333
+ listenAddr: "0.0.0.0",
334
+ listenPort: parsePort(listenPart, spec),
335
+ remoteUnixPath: socketPath
336
+ };
337
+ }
338
+ return {
339
+ rawSpec: spec,
340
+ listenAddr: listenPart.slice(0, innerColon),
341
+ listenPort: parsePort(listenPart.slice(innerColon + 1), spec),
342
+ remoteUnixPath: socketPath
343
+ };
344
+ }
345
+ const parts = spec.split(":");
346
+ switch (parts.length) {
347
+ case 2:
348
+ return {
349
+ rawSpec: spec,
350
+ listenAddr: parts[0],
351
+ listenPort: parsePort(parts[1], spec),
352
+ remoteHost: parts[0],
353
+ remotePort: parsePort(parts[1], spec)
354
+ };
355
+ case 3:
356
+ return {
357
+ rawSpec: spec,
358
+ listenAddr: parts[1],
359
+ listenPort: parsePort(parts[0], spec),
360
+ remoteHost: parts[1],
361
+ remotePort: parsePort(parts[2], spec)
362
+ };
363
+ case 4:
364
+ return {
365
+ rawSpec: spec,
366
+ listenAddr: parts[0],
367
+ listenPort: parsePort(parts[1], spec),
368
+ remoteHost: parts[2],
369
+ remotePort: parsePort(parts[3], spec)
370
+ };
371
+ default:
372
+ throw new Error(`invalid forward spec ${JSON.stringify(spec)}: expected 2-4 colon-separated parts`);
373
+ }
374
+ }
375
+ function parsePort(s, spec) {
376
+ const n = Number(s);
377
+ if (!Number.isInteger(n) || n < 0 || n > 65535) {
378
+ throw new Error(`invalid port ${JSON.stringify(s)} in forward spec ${JSON.stringify(spec)}`);
379
+ }
380
+ return n;
381
+ }
382
+ function isListenUnix(m) {
383
+ return m.listenUnixPath !== void 0;
384
+ }
385
+ function remoteTargetHeader(m) {
386
+ if (m.remoteUnixPath) return `unix:${m.remoteUnixPath}`;
387
+ return `${m.remoteHost}:${m.remotePort}`;
388
+ }
389
+ function listenAddressDescription(m) {
390
+ if (isListenUnix(m)) return m.listenUnixPath;
391
+ return `${m.listenAddr}:${m.listenPort}`;
392
+ }
393
+ var Forwarder = class _Forwarder {
394
+ constructor(init, mappings) {
395
+ this.init = init;
396
+ this.mappings = mappings;
397
+ }
398
+ init;
399
+ mappings;
400
+ listeners = [];
401
+ servers = [];
402
+ liveSockets = /* @__PURE__ */ new Set();
403
+ closed = false;
404
+ static async start(init) {
405
+ if (init.specs.length === 0) {
406
+ throw new Error("Forwarder requires at least one forward spec");
407
+ }
408
+ const mappings = init.specs.map(parseAddressMapping);
409
+ const fwd = new _Forwarder(init, mappings);
410
+ try {
411
+ await fwd.bindAll();
412
+ } catch (err) {
413
+ await fwd.close();
414
+ throw err;
415
+ }
416
+ return fwd;
417
+ }
418
+ async bindAll() {
419
+ for (const m of this.mappings) {
420
+ const server = net.createServer((socket) => this.handleAccept(m, socket));
421
+ const { local, port } = await listen(server, m);
422
+ this.servers.push(server);
423
+ this.listeners.push({
424
+ spec: m.rawSpec,
425
+ local,
426
+ remote: remoteTargetHeader(m),
427
+ ...port !== void 0 && { port }
428
+ });
429
+ this.log(`listen ${local} \u2192 ${remoteTargetHeader(m)}`);
430
+ }
431
+ }
432
+ handleAccept(mapping, socket) {
433
+ if (this.closed) {
434
+ socket.destroy();
435
+ return;
436
+ }
437
+ socket.on("error", () => {
438
+ });
439
+ const ws$1 = openWebSocket(this.init, mapping);
440
+ let closed = false;
441
+ const cleanup = () => {
442
+ if (closed) return;
443
+ closed = true;
444
+ try {
445
+ socket.destroy();
446
+ } catch {
447
+ }
448
+ try {
449
+ ws$1.close();
450
+ } catch {
451
+ }
452
+ this.liveSockets.delete(handle);
453
+ };
454
+ const handle = { close: cleanup };
455
+ this.liveSockets.add(handle);
456
+ ws$1.binaryType = "nodebuffer";
457
+ ws$1.on("open", () => {
458
+ const wsStream = ws.createWebSocketStream(ws$1);
459
+ wsStream.on("error", cleanup);
460
+ socket.pipe(wsStream);
461
+ wsStream.pipe(socket);
462
+ });
463
+ ws$1.on("close", cleanup);
464
+ ws$1.on("error", (err) => {
465
+ this.log(`tunnel error: ${err.message}`);
466
+ cleanup();
467
+ });
468
+ socket.on("close", cleanup);
469
+ }
470
+ /** Tear down all listeners and any in-flight tunnel sockets. */
471
+ async close() {
472
+ this.closed = true;
473
+ for (const handle of [...this.liveSockets]) handle.close();
474
+ this.liveSockets.clear();
475
+ await Promise.all(
476
+ this.servers.map(
477
+ (s) => new Promise((resolve) => {
478
+ s.close(() => resolve());
479
+ })
480
+ )
481
+ );
482
+ for (const m of this.mappings) {
483
+ if (isListenUnix(m) && m.listenUnixPath) {
484
+ try {
485
+ fs__default.default.rmSync(m.listenUnixPath, { force: true });
486
+ } catch {
487
+ }
488
+ }
489
+ }
490
+ }
491
+ log(msg) {
492
+ this.init.options?.log?.(msg);
493
+ }
494
+ };
495
+ function listen(server, mapping) {
496
+ return new Promise((resolve, reject) => {
497
+ server.once("error", reject);
498
+ if (isListenUnix(mapping)) {
499
+ const p = mapping.listenUnixPath;
500
+ try {
501
+ fs__default.default.rmSync(p, { force: true });
502
+ } catch {
503
+ }
504
+ server.listen(p, () => {
505
+ try {
506
+ fs__default.default.chmodSync(p, 432);
507
+ } catch {
508
+ }
509
+ resolve({ local: p });
510
+ });
511
+ return;
512
+ }
513
+ server.listen({ host: mapping.listenAddr, port: mapping.listenPort }, () => {
514
+ const addr = server.address();
515
+ if (addr && typeof addr === "object") {
516
+ const local = `${mapping.listenAddr}:${addr.port}`;
517
+ resolve({ local, port: addr.port });
518
+ } else {
519
+ resolve({ local: listenAddressDescription(mapping) });
520
+ }
521
+ });
522
+ });
523
+ }
524
+ function openWebSocket(init, mapping) {
525
+ const url = wsURLForVM(init.transport, init.hostname);
526
+ const headers = {
527
+ "X-Inlets-Client-ID": init.options?.clientId ?? os__default.default.hostname(),
528
+ "X-Inlets-Mode": "local",
529
+ "X-Inlets-Upstream": remoteTargetHeader(mapping),
530
+ "User-Agent": init.userAgent
531
+ };
532
+ if (init.token) headers["Authorization"] = `Bearer ${init.token}`;
533
+ const opts = {
534
+ headers,
535
+ handshakeTimeout: init.options?.dialTimeoutMs ?? 1e4
536
+ };
537
+ if (init.transport.kind === "socket") {
538
+ opts.agent = unixAgent(init.transport.socketPath);
539
+ }
540
+ return new ws.WebSocket(url, opts);
541
+ }
542
+ function wsURLForVM(transport, hostname) {
543
+ if (transport.kind === "socket") {
544
+ return `ws://localhost/vm/${encodeURIComponent(hostname)}/forward`;
545
+ }
546
+ const u = transport.url;
547
+ const scheme = u.protocol === "https:" ? "wss" : "ws";
548
+ const port = u.port ? `:${u.port}` : "";
549
+ return `${scheme}://${u.hostname}${port}/vm/${encodeURIComponent(hostname)}/forward`;
550
+ }
551
+ function unixAgent(socketPath) {
552
+ const agent = new http__default.default.Agent({ keepAlive: false });
553
+ agent.createConnection = ((_opts, cb) => {
554
+ const conn = net__default.default.createConnection({ path: socketPath });
555
+ if (cb) {
556
+ conn.once("connect", () => cb(null, conn));
557
+ conn.once("error", (err) => cb(err));
558
+ }
559
+ return conn;
560
+ });
561
+ return agent;
562
+ }
306
563
 
307
564
  // src/vm.ts
308
565
  var VMFileSystem = class {
@@ -312,16 +569,16 @@ var VMFileSystem = class {
312
569
  }
313
570
  transport;
314
571
  hostname;
315
- async readDir(path2) {
316
- const q = new URLSearchParams({ path: path2 });
572
+ async readDir(path3) {
573
+ const q = new URLSearchParams({ path: path3 });
317
574
  const wire = await this.transport.request(
318
575
  "GET",
319
576
  `/vm/${encodeURIComponent(this.hostname)}/fs/readdir?${q.toString()}`
320
577
  );
321
578
  return (wire ?? []).map(fsEntryFromWire);
322
579
  }
323
- async stat(path2) {
324
- const q = new URLSearchParams({ path: path2 });
580
+ async stat(path3) {
581
+ const q = new URLSearchParams({ path: path3 });
325
582
  try {
326
583
  const wire = await this.transport.request(
327
584
  "GET",
@@ -333,8 +590,8 @@ var VMFileSystem = class {
333
590
  throw err;
334
591
  }
335
592
  }
336
- async exists(path2) {
337
- return await this.stat(path2) !== null;
593
+ async exists(path3) {
594
+ return await this.stat(path3) !== null;
338
595
  }
339
596
  async mkdir(req) {
340
597
  await this.transport.request(
@@ -347,22 +604,22 @@ var VMFileSystem = class {
347
604
  }
348
605
  );
349
606
  }
350
- async remove(path2, recursive = false) {
351
- const q = new URLSearchParams({ path: path2, recursive: String(recursive) });
607
+ async remove(path3, recursive = false) {
608
+ const q = new URLSearchParams({ path: path3, recursive: String(recursive) });
352
609
  await this.transport.request(
353
610
  "DELETE",
354
611
  `/vm/${encodeURIComponent(this.hostname)}/fs/remove?${q.toString()}`
355
612
  );
356
613
  }
357
- async readFile(path2) {
358
- const q = new URLSearchParams({ path: path2, mode: "binary" });
614
+ async readFile(path3) {
615
+ const q = new URLSearchParams({ path: path3, mode: "binary" });
359
616
  return this.transport.requestRaw(
360
617
  "GET",
361
618
  `/vm/${encodeURIComponent(this.hostname)}/cp?${q.toString()}`
362
619
  );
363
620
  }
364
- async writeFile(path2, content, opts = {}) {
365
- const q = new URLSearchParams({ path: path2, mode: "binary" });
621
+ async writeFile(path3, content, opts = {}) {
622
+ const q = new URLSearchParams({ path: path3, mode: "binary" });
366
623
  if (opts.uid !== void 0) q.set("uid", String(opts.uid));
367
624
  if (opts.gid !== void 0) q.set("gid", String(opts.gid));
368
625
  if (opts.permissions) q.set("permissions", opts.permissions);
@@ -374,8 +631,8 @@ var VMFileSystem = class {
374
631
  );
375
632
  }
376
633
  /** Upload a tar archive, expanded into the VM at `path`. */
377
- async tarTo(path2, tar) {
378
- const q = new URLSearchParams({ path: path2, mode: "tar" });
634
+ async tarTo(path3, tar) {
635
+ const q = new URLSearchParams({ path: path3, mode: "tar" });
379
636
  if (tar instanceof Buffer) {
380
637
  await this.transport.requestRaw(
381
638
  "POST",
@@ -394,8 +651,8 @@ var VMFileSystem = class {
394
651
  for await (const _ of res) void _;
395
652
  }
396
653
  /** Download `path` from the VM as a tar archive. */
397
- async tarFrom(path2) {
398
- const q = new URLSearchParams({ path: path2, mode: "tar" });
654
+ async tarFrom(path3) {
655
+ const q = new URLSearchParams({ path: path3, mode: "tar" });
399
656
  return this.transport.requestRaw(
400
657
  "GET",
401
658
  `/vm/${encodeURIComponent(this.hostname)}/cp?${q.toString()}`
@@ -500,6 +757,30 @@ var VM = class {
500
757
  async restore() {
501
758
  await this.transport.request("POST", `/vm/${encodeURIComponent(this.hostname)}/restore`);
502
759
  }
760
+ // --- port forwarding ---------------------------------------------------
761
+ /**
762
+ * Open one or more port forwards from the host to this VM. Each spec follows
763
+ * the same syntax as `slicer vm forward -L`:
764
+ *
765
+ * `127.0.0.1:9000` — listen and forward on the same TCP port
766
+ * `8081:127.0.0.1:8080` — listen on `0.0.0.0:8081`, forward to `127.0.0.1:8080`
767
+ * `0.0.0.0:8080:127.0.0.1:8080` — fully explicit
768
+ * `9000:/var/run/docker.sock` — TCP listen, Unix socket forward
769
+ * `/tmp/docker.sock:/var/run/docker.sock` — Unix-to-Unix
770
+ *
771
+ * Returns a {@link Forwarder} handle. Call `forwarder.close()` to tear down
772
+ * all listeners and any in-flight tunnel sockets.
773
+ */
774
+ async forward(specs, options) {
775
+ return Forwarder.start({
776
+ hostname: this.hostname,
777
+ transport: this.transport.transport,
778
+ ...this.transport.token !== void 0 && { token: this.transport.token },
779
+ userAgent: this.transport.userAgent,
780
+ specs: typeof specs === "string" ? [specs] : specs,
781
+ ...options !== void 0 && { options }
782
+ });
783
+ }
503
784
  // --- exec -------------------------------------------------------------
504
785
  /**
505
786
  * Streaming exec — yields NDJSON frames (`started`, `stdout`, `stderr`, `exit`).
@@ -508,8 +789,8 @@ var VM = class {
508
789
  * `dataBytes`/`stdoutBytes`/`stderrBytes` Buffers alongside for convenience.
509
790
  */
510
791
  async *exec(req) {
511
- const { path: path2, body } = buildExecPath(this.hostname, req, false);
512
- for await (const frame of this.transport.requestNDJSON("POST", path2, body)) {
792
+ const { path: path3, body } = buildExecPath(this.hostname, req, false);
793
+ for await (const frame of this.transport.requestNDJSON("POST", path3, body)) {
513
794
  if (frame.encoding === "base64") {
514
795
  if (frame.data) frame.dataBytes = Buffer.from(frame.data, "base64");
515
796
  if (frame.stdout) frame.stdoutBytes = Buffer.from(frame.stdout, "base64");
@@ -522,8 +803,8 @@ var VM = class {
522
803
  if (req.stdin !== void 0) {
523
804
  throw new Error("stdin is not supported by execBuffered; use exec() instead");
524
805
  }
525
- const { path: path2, body } = buildExecPath(this.hostname, req, true);
526
- const raw = await this.transport.requestRaw("POST", path2, body);
806
+ const { path: path3, body } = buildExecPath(this.hostname, req, true);
807
+ const raw = await this.transport.requestRaw("POST", path3, body);
527
808
  const text = raw.toString("utf8");
528
809
  const parsed = text ? JSON.parse(text) : {};
529
810
  const common = {
@@ -749,6 +1030,7 @@ var SlicerClient = class _SlicerClient {
749
1030
 
750
1031
  exports.ExecStdioBase64 = ExecStdioBase64;
751
1032
  exports.ExecStdioText = ExecStdioText;
1033
+ exports.Forwarder = Forwarder;
752
1034
  exports.GiB = GiB;
753
1035
  exports.HostGroupsAPI = HostGroupsAPI;
754
1036
  exports.MiB = MiB;
@@ -760,6 +1042,7 @@ exports.SlicerClient = SlicerClient;
760
1042
  exports.VM = VM;
761
1043
  exports.VMFileSystem = VMFileSystem;
762
1044
  exports.VMsAPI = VMsAPI;
1045
+ exports.parseAddressMapping = parseAddressMapping;
763
1046
  exports.resolveTransport = resolveTransport;
764
1047
  //# sourceMappingURL=index.cjs.map
765
1048
  //# sourceMappingURL=index.cjs.map