@slicervm/sdk 0.1.6 → 0.1.8

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. 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()`.
5
+ Mirrors the [Go SDK](https://github.com/slicervm/sdk) semantically. The top-level `SlicerClient` exposes `hostGroups`, `vms`, `commits`, 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
 
@@ -33,6 +33,34 @@ for await (const f of vm.bg.logs(bg.execId, { follow: true })) {
33
33
  await vm.delete();
34
34
  ```
35
35
 
36
+ ## Cold forks
37
+
38
+ A stopped persistent VM can be committed as an immutable disk parent, then
39
+ forked into cold-booted children. Fork calls always wait until the child agent
40
+ has finalised its guest identity.
41
+
42
+ ```ts
43
+ await vm.shutdown();
44
+ const parent = await vm.commit({
45
+ tags: ['node-build'],
46
+ cacheKey: 'node-build-v1',
47
+ });
48
+
49
+ const child = await parent.fork({
50
+ waitTimeoutSec: 120,
51
+ network: { allow: [], drop: ['0.0.0.0/0'] },
52
+ tags: ['job=node-build'],
53
+ });
54
+
55
+ const description = await child.describe();
56
+ console.log(description.parentCommitId, description.network.effective);
57
+
58
+ const cached = await client.commits.list({ cacheKey: 'node-build-v1' });
59
+ await child.delete();
60
+ await vm.delete();
61
+ await client.commits.delete(parent.commitId);
62
+ ```
63
+
36
64
  ## Status
37
65
 
38
66
  Pre-release. See `ts-plan.md`.
package/dist/index.cjs CHANGED
@@ -266,6 +266,7 @@ function createVMReqToWire(r) {
266
266
  if (r.ip !== void 0) o.ip = r.ip;
267
267
  if (r.tags !== void 0) o.tags = r.tags;
268
268
  if (r.secrets !== void 0) o.secrets = r.secrets;
269
+ if (r.network !== void 0) o.network = r.network;
269
270
  return o;
270
271
  }
271
272
  function createVMResFromWire(w) {
@@ -283,7 +284,71 @@ function agentHealthFromWire(w) {
283
284
  ...w.agent_uptime !== void 0 && { agentUptime: w.agent_uptime },
284
285
  ...w.agent_version !== void 0 && { agentVersion: w.agent_version },
285
286
  ...w.system_uptime !== void 0 && { systemUptime: w.system_uptime },
286
- ...w.userdata_ran !== void 0 && { userdataRan: w.userdata_ran }
287
+ ...w.userdata_ran !== void 0 && { userdataRan: w.userdata_ran },
288
+ ...w.userdata_exit_code !== void 0 && { userdataExitCode: w.userdata_exit_code }
289
+ };
290
+ }
291
+ function vmCommitFromWire(w) {
292
+ return {
293
+ hostname: w.hostname,
294
+ commitId: w.commit_id,
295
+ status: w.status,
296
+ ...w.parent_status !== void 0 && { parentStatus: w.parent_status },
297
+ mode: w.mode ?? "",
298
+ ...w.tags !== void 0 && { tags: w.tags },
299
+ ...w.labels !== void 0 && { labels: w.labels },
300
+ ...w.cache_key !== void 0 && { cacheKey: w.cache_key }
301
+ };
302
+ }
303
+ function vmCommitInfoFromWire(w) {
304
+ return {
305
+ commitId: w.commit_id,
306
+ sourceHostname: w.source_hostname,
307
+ sourceHostGroup: w.source_host_group,
308
+ createdAt: w.created_at,
309
+ mode: w.mode,
310
+ ...w.tags !== void 0 && { tags: w.tags },
311
+ ...w.labels !== void 0 && { labels: w.labels },
312
+ ...w.cache_key !== void 0 && { cacheKey: w.cache_key }
313
+ };
314
+ }
315
+ function vmCommitDeleteFromWire(w) {
316
+ return { commitId: w.commit_id, status: w.status };
317
+ }
318
+ function vmForkFromWire(w) {
319
+ return {
320
+ hostname: w.hostname,
321
+ sourceHostname: w.source_hostname,
322
+ ...w.commit_id !== void 0 && { commitId: w.commit_id },
323
+ status: w.status,
324
+ ...w.parent_status !== void 0 && { parentStatus: w.parent_status },
325
+ ...w.child_status !== void 0 && { childStatus: w.child_status },
326
+ mode: w.mode ?? ""
327
+ };
328
+ }
329
+ function vmDescriptionFromWire(w) {
330
+ return {
331
+ hostname: w.hostname,
332
+ ...w.hostgroup !== void 0 && { hostGroup: w.hostgroup },
333
+ ...w.ip !== void 0 && { ip: w.ip },
334
+ ...w.ram_bytes !== void 0 && { ramBytes: w.ram_bytes },
335
+ ...w.cpus !== void 0 && { cpus: w.cpus },
336
+ ...w.created_at !== void 0 && { createdAt: w.created_at },
337
+ ...w.arch !== void 0 && { arch: w.arch },
338
+ ...w.tags !== void 0 && { tags: w.tags },
339
+ ...w.status !== void 0 && { status: w.status },
340
+ ...w.persistent !== void 0 && { persistent: w.persistent },
341
+ ...w.storage !== void 0 && { storage: w.storage },
342
+ ...w.image !== void 0 && { image: w.image },
343
+ ...w.commit_id !== void 0 && { commitId: w.commit_id },
344
+ ...w.parent_commit_id !== void 0 && { parentCommitId: w.parent_commit_id },
345
+ network: {
346
+ ...w.network.mode !== void 0 && { mode: w.network.mode },
347
+ ...w.network.policy_source !== void 0 && { policySource: w.network.policy_source },
348
+ hostGroup: w.network.host_group,
349
+ ...w.network.override !== void 0 && { override: w.network.override },
350
+ effective: w.network.effective
351
+ }
287
352
  };
288
353
  }
289
354
  function fsEntryFromWire(w) {
@@ -738,6 +803,48 @@ var VMFileSystem = class {
738
803
  );
739
804
  }
740
805
  };
806
+ var CommittedVM = class {
807
+ constructor(transport, hostGroup, response) {
808
+ this.transport = transport;
809
+ this.hostname = response.hostname;
810
+ this.hostGroup = hostGroup;
811
+ this.commitId = response.commitId;
812
+ this.status = response.status;
813
+ if (response.parentStatus !== void 0) this.parentStatus = response.parentStatus;
814
+ this.mode = response.mode;
815
+ if (response.tags !== void 0) this.tags = response.tags;
816
+ if (response.labels !== void 0) this.labels = response.labels;
817
+ if (response.cacheKey !== void 0) this.cacheKey = response.cacheKey;
818
+ }
819
+ transport;
820
+ hostname;
821
+ hostGroup;
822
+ commitId;
823
+ status;
824
+ parentStatus;
825
+ mode;
826
+ tags;
827
+ labels;
828
+ cacheKey;
829
+ async fork(opts = {}) {
830
+ const res = await forkPath(
831
+ this.transport,
832
+ this.commitId,
833
+ opts
834
+ );
835
+ return new VM(this.transport, {
836
+ hostname: res.hostname,
837
+ hostGroup: this.hostGroup
838
+ });
839
+ }
840
+ async forkRaw(opts = {}) {
841
+ return forkPath(
842
+ this.transport,
843
+ this.commitId,
844
+ opts
845
+ );
846
+ }
847
+ };
741
848
  var VM = class {
742
849
  hostname;
743
850
  hostGroup;
@@ -777,6 +884,14 @@ var VM = class {
777
884
  async logs() {
778
885
  return this.transport.request("GET", `/vm/${encodeURIComponent(this.hostname)}/logs`);
779
886
  }
887
+ /** Return configuration, fork lineage, and effective network policy. */
888
+ async describe() {
889
+ const wire = await this.transport.request(
890
+ "GET",
891
+ `/vm/${encodeURIComponent(this.hostname)}`
892
+ );
893
+ return vmDescriptionFromWire(wire);
894
+ }
780
895
  async waitForAgent(opts = {}) {
781
896
  const timeoutMs = opts.timeoutMs ?? 12e4;
782
897
  const intervalMs = opts.intervalMs ?? 500;
@@ -803,10 +918,15 @@ var VM = class {
803
918
  while (Date.now() < deadline) {
804
919
  try {
805
920
  last = await this.health();
806
- if (last.userdataRan) return last;
807
921
  } catch (err) {
808
922
  lastErr = err;
809
923
  }
924
+ if (last?.userdataRan) {
925
+ if (last.userdataExitCode !== void 0 && last.userdataExitCode !== 0) {
926
+ throw new Error(`userdata failed with exit code ${last.userdataExitCode}`);
927
+ }
928
+ return last;
929
+ }
810
930
  await sleep(intervalMs);
811
931
  }
812
932
  throw new Error(
@@ -838,6 +958,25 @@ var VM = class {
838
958
  async restore() {
839
959
  await this.transport.request("POST", `/vm/${encodeURIComponent(this.hostname)}/restore`);
840
960
  }
961
+ /**
962
+ * Commit a stopped persistent VM disk into an immutable parent.
963
+ */
964
+ async commit(opts = {}) {
965
+ const cacheKey = opts.cacheKey?.trim();
966
+ const tags = opts.tags && opts.tags.length > 0 ? opts.tags : void 0;
967
+ const labels = opts.labels && Object.keys(opts.labels).length > 0 ? opts.labels : void 0;
968
+ const body = tags !== void 0 || labels !== void 0 || cacheKey ? {
969
+ ...tags !== void 0 && { tags },
970
+ ...labels !== void 0 && { labels },
971
+ ...cacheKey && { cache_key: cacheKey }
972
+ } : void 0;
973
+ const wire = await this.transport.request(
974
+ "POST",
975
+ `/vm/${encodeURIComponent(this.hostname)}/commit`,
976
+ body
977
+ );
978
+ return new CommittedVM(this.transport, this.hostGroup, vmCommitFromWire(wire));
979
+ }
841
980
  // --- port forwarding ---------------------------------------------------
842
981
  /**
843
982
  * Open one or more port forwards from the host to this VM. Each spec follows
@@ -957,6 +1096,32 @@ function sleep(ms) {
957
1096
  function errMsg(e) {
958
1097
  return e instanceof Error ? e.message : String(e);
959
1098
  }
1099
+ async function forkPath(transport, commitId, opts) {
1100
+ const id = validateCommitId(commitId);
1101
+ const q = new URLSearchParams();
1102
+ q.set("wait", "agent");
1103
+ if (opts.waitTimeoutSec !== void 0 && opts.waitTimeoutSec > 0) {
1104
+ q.set("timeout", `${opts.waitTimeoutSec}s`);
1105
+ }
1106
+ const query = q.toString() ? `?${q.toString()}` : "";
1107
+ const body = opts.network !== void 0 || (opts.tags?.length ?? 0) > 0 ? {
1108
+ ...opts.network !== void 0 && { network: opts.network },
1109
+ ...(opts.tags?.length ?? 0) > 0 && { tags: opts.tags }
1110
+ } : void 0;
1111
+ const wire = await transport.request(
1112
+ "POST",
1113
+ `/vm/commits/${encodeURIComponent(id)}/fork${query}`,
1114
+ body
1115
+ );
1116
+ return vmForkFromWire(wire);
1117
+ }
1118
+ function validateCommitId(commitId) {
1119
+ const value = commitId.trim();
1120
+ if (!value || value === "." || value === ".." || value.includes("..") || value.includes("/") || value.includes("\\") || value.includes("\0")) {
1121
+ throw new Error(`invalid commit ID ${JSON.stringify(value)}`);
1122
+ }
1123
+ return value;
1124
+ }
960
1125
  var VMBg = class {
961
1126
  constructor(transport, hostname) {
962
1127
  this.transport = transport;
@@ -1215,6 +1380,54 @@ var VMsAPI = class {
1215
1380
  return createVMResFromWire(wire);
1216
1381
  }
1217
1382
  };
1383
+ var CommitsAPI = class {
1384
+ constructor(transport) {
1385
+ this.transport = transport;
1386
+ }
1387
+ transport;
1388
+ async list(opts = {}) {
1389
+ const qs = new URLSearchParams();
1390
+ for (const tag of opts.tags ?? []) {
1391
+ const value = tag.trim();
1392
+ if (value) qs.append("tag", value);
1393
+ }
1394
+ const cacheKey = opts.cacheKey?.trim();
1395
+ const source = opts.source?.trim();
1396
+ const mode = opts.mode?.trim();
1397
+ if (cacheKey) qs.set("cache_key", cacheKey);
1398
+ if (source) qs.set("source", source);
1399
+ if (mode) qs.set("mode", mode);
1400
+ const encoded = qs.toString();
1401
+ const query = encoded ? `?${encoded}` : "";
1402
+ const wire = await this.transport.request("GET", `/vm/commits${query}`);
1403
+ return (wire ?? []).map(vmCommitInfoFromWire);
1404
+ }
1405
+ async delete(commitId) {
1406
+ const id = validateCommitId2(commitId);
1407
+ const wire = await this.transport.request(
1408
+ "DELETE",
1409
+ `/vm/commits/${encodeURIComponent(id)}`
1410
+ );
1411
+ return vmCommitDeleteFromWire(wire);
1412
+ }
1413
+ async fork(commitId, opts = {}) {
1414
+ const id = validateCommitId2(commitId);
1415
+ const qs = new URLSearchParams({ wait: "agent" });
1416
+ if (opts.waitTimeoutSec !== void 0 && opts.waitTimeoutSec > 0) {
1417
+ qs.set("timeout", `${opts.waitTimeoutSec}s`);
1418
+ }
1419
+ const body = opts.network !== void 0 || (opts.tags?.length ?? 0) > 0 ? {
1420
+ ...opts.network !== void 0 && { network: opts.network },
1421
+ ...(opts.tags?.length ?? 0) > 0 && { tags: opts.tags }
1422
+ } : void 0;
1423
+ const wire = await this.transport.request(
1424
+ "POST",
1425
+ `/vm/commits/${encodeURIComponent(id)}/fork?${qs.toString()}`,
1426
+ body
1427
+ );
1428
+ return vmForkFromWire(wire);
1429
+ }
1430
+ };
1218
1431
  var SecretsAPI = class {
1219
1432
  constructor(transport) {
1220
1433
  this.transport = transport;
@@ -1261,18 +1474,184 @@ function buildListQuery(opts) {
1261
1474
  const s = qs.toString();
1262
1475
  return s ? `?${s}` : "";
1263
1476
  }
1477
+ function validateCommitId2(commitId) {
1478
+ const value = commitId.trim();
1479
+ if (!value || value === "." || value === ".." || value.includes("..") || value.includes("/") || value.includes("\\") || value.includes("\0")) {
1480
+ throw new Error(`invalid commit ID ${JSON.stringify(value)}`);
1481
+ }
1482
+ return value;
1483
+ }
1484
+
1485
+ // src/proxy.ts
1486
+ var ProxySecretBearer = "bearer";
1487
+ var ProxySecretBasic = "basic";
1488
+ var ProxySecretOAuthClientCredentials = "oauth-client-creds";
1489
+ function clientFromWire(w) {
1490
+ return { name: w.name, createdAt: w.created_at };
1491
+ }
1492
+ function clientCreatedFromWire(w) {
1493
+ return { name: w.name, token: w.token, createdAt: w.created_at };
1494
+ }
1495
+ function secretFromWire2(w) {
1496
+ const out = { name: w.name, host: w.host, createdAt: w.created_at };
1497
+ if (w.type) out.type = w.type;
1498
+ return out;
1499
+ }
1500
+ function ruleFromWire(w) {
1501
+ const out = { host: w.host };
1502
+ if (w.secret) out.secret = w.secret;
1503
+ if (w.methods && w.methods.length > 0) out.methods = w.methods;
1504
+ if (w.paths && w.paths.length > 0) out.paths = w.paths;
1505
+ if (w.expires && w.expires !== "0001-01-01T00:00:00Z") out.expires = w.expires;
1506
+ if (w.passthrough) out.passthrough = w.passthrough;
1507
+ return out;
1508
+ }
1509
+ var ProxyAPI = class {
1510
+ constructor(transport) {
1511
+ this.transport = transport;
1512
+ this.clients = new ProxyClientsAPI(transport);
1513
+ this.secrets = new ProxySecretsAPI(transport);
1514
+ this.allows = new ProxyAllowsAPI(transport);
1515
+ }
1516
+ transport;
1517
+ clients;
1518
+ secrets;
1519
+ allows;
1520
+ };
1521
+ var ProxyClientsAPI = class {
1522
+ constructor(transport) {
1523
+ this.transport = transport;
1524
+ }
1525
+ transport;
1526
+ /** Mint a new proxy client. The returned token is shown once. */
1527
+ async create(name, opts = {}) {
1528
+ const body = { name };
1529
+ if (opts.token) body.token = opts.token;
1530
+ const wire = await this.transport.request(
1531
+ "POST",
1532
+ "/proxy/v1/clients",
1533
+ body
1534
+ );
1535
+ return clientCreatedFromWire(wire);
1536
+ }
1537
+ async list() {
1538
+ const wire = await this.transport.request("GET", "/proxy/v1/clients");
1539
+ return (wire ?? []).map(clientFromWire);
1540
+ }
1541
+ /**
1542
+ * Revoke the token, drop every allow rule the client owned, and
1543
+ * remove the client.
1544
+ */
1545
+ async delete(name) {
1546
+ await this.transport.request("DELETE", `/proxy/v1/clients/${encodeURIComponent(name)}`);
1547
+ }
1548
+ /** List a client's allow rules in declaration order (first-match-wins). */
1549
+ async rules(name) {
1550
+ const wire = await this.transport.request(
1551
+ "GET",
1552
+ `/proxy/v1/clients/${encodeURIComponent(name)}`
1553
+ );
1554
+ return (wire ?? []).map(ruleFromWire);
1555
+ }
1556
+ };
1557
+ var ProxySecretsAPI = class {
1558
+ constructor(transport) {
1559
+ this.transport = transport;
1560
+ }
1561
+ transport;
1562
+ async create(req) {
1563
+ const body = {
1564
+ name: req.name,
1565
+ host: req.host,
1566
+ value: req.value
1567
+ };
1568
+ if (req.type) body.type = req.type;
1569
+ await this.transport.request("POST", "/proxy/v1/secrets", body);
1570
+ }
1571
+ async list() {
1572
+ const wire = await this.transport.request("GET", "/proxy/v1/secrets");
1573
+ return (wire ?? []).map(secretFromWire2);
1574
+ }
1575
+ /**
1576
+ * Remove a secret. Allow rules that reference it stop matching until
1577
+ * the secret is recreated or the rule is rewritten.
1578
+ */
1579
+ async delete(name) {
1580
+ await this.transport.request("DELETE", `/proxy/v1/secrets/${encodeURIComponent(name)}`);
1581
+ }
1582
+ };
1583
+ var ProxyAllowsAPI = class {
1584
+ constructor(transport) {
1585
+ this.transport = transport;
1586
+ }
1587
+ transport;
1588
+ /** Add an allow rule. Returns the resolved rule with absolute `expires`. */
1589
+ async add(req) {
1590
+ const body = {
1591
+ client: req.client,
1592
+ host: req.host
1593
+ };
1594
+ if (req.secret) body.secret = req.secret;
1595
+ if (req.methods && req.methods.length > 0) body.methods = req.methods;
1596
+ if (req.paths && req.paths.length > 0) body.paths = req.paths;
1597
+ if (req.ttlSeconds && req.ttlSeconds > 0) body.ttl_seconds = req.ttlSeconds;
1598
+ if (req.passthrough) body.passthrough = true;
1599
+ const wire = await this.transport.request(
1600
+ "POST",
1601
+ "/proxy/v1/allows",
1602
+ body
1603
+ );
1604
+ return ruleFromWire(wire);
1605
+ }
1606
+ /**
1607
+ * Host-bulk revoke: removes **every** rule on the client whose host
1608
+ * matches. For surgical removal of one rule among siblings on the
1609
+ * same host (e.g. several path-scoped rules on `github.com`), use
1610
+ * `removeByTuple` instead.
1611
+ */
1612
+ async remove(client, host) {
1613
+ await this.transport.request(
1614
+ "DELETE",
1615
+ `/proxy/v1/allows/${encodeURIComponent(client)}/${encodeURIComponent(host)}`
1616
+ );
1617
+ }
1618
+ /**
1619
+ * Surgical revoke: removes the single rule whose
1620
+ * (host, methods, paths, passthrough) tuple matches the request.
1621
+ * Pass exactly the same fields you used at create time. Method and
1622
+ * host casing are normalised server-side, so `"GET"` / `"get"` and
1623
+ * `"github.com"` / `"GITHUB.COM"` all match the same stored rule.
1624
+ *
1625
+ * Returns 404 (surfaced as a SlicerAPIError) when no rule matches.
1626
+ */
1627
+ async removeByTuple(req) {
1628
+ const body = {
1629
+ client: req.client,
1630
+ host: req.host
1631
+ };
1632
+ if (req.secret) body.secret = req.secret;
1633
+ if (req.methods && req.methods.length > 0) body.methods = req.methods;
1634
+ if (req.paths && req.paths.length > 0) body.paths = req.paths;
1635
+ if (req.passthrough) body.passthrough = true;
1636
+ await this.transport.request("POST", "/proxy/v1/allows/revoke", body);
1637
+ }
1638
+ };
1264
1639
 
1265
1640
  // src/client.ts
1266
1641
  var SlicerClient = class _SlicerClient {
1267
1642
  transport;
1268
1643
  hostGroups;
1269
1644
  vms;
1645
+ commits;
1270
1646
  secrets;
1647
+ proxy;
1271
1648
  constructor(opts) {
1272
1649
  this.transport = new TransportClient(opts);
1273
1650
  this.hostGroups = new HostGroupsAPI(this.transport);
1274
1651
  this.vms = new VMsAPI(this.transport);
1652
+ this.commits = new CommitsAPI(this.transport);
1275
1653
  this.secrets = new SecretsAPI(this.transport);
1654
+ this.proxy = new ProxyAPI(this.transport);
1276
1655
  }
1277
1656
  static fromEnv(overrides = {}) {
1278
1657
  const baseURL = overrides.baseURL ?? process.env.SLICER_URL;
@@ -1429,6 +1808,8 @@ var SlicerShellSession = class {
1429
1808
  }
1430
1809
  };
1431
1810
 
1811
+ exports.CommitsAPI = CommitsAPI;
1812
+ exports.CommittedVM = CommittedVM;
1432
1813
  exports.ExecStdioBase64 = ExecStdioBase64;
1433
1814
  exports.ExecStdioText = ExecStdioText;
1434
1815
  exports.FRAME_TYPE_DATA = FRAME_TYPE_DATA;
@@ -1441,6 +1822,13 @@ exports.GiB = GiB;
1441
1822
  exports.HostGroupsAPI = HostGroupsAPI;
1442
1823
  exports.MiB = MiB;
1443
1824
  exports.NonRootUser = NonRootUser;
1825
+ exports.ProxyAPI = ProxyAPI;
1826
+ exports.ProxyAllowsAPI = ProxyAllowsAPI;
1827
+ exports.ProxyClientsAPI = ProxyClientsAPI;
1828
+ exports.ProxySecretBasic = ProxySecretBasic;
1829
+ exports.ProxySecretBearer = ProxySecretBearer;
1830
+ exports.ProxySecretOAuthClientCredentials = ProxySecretOAuthClientCredentials;
1831
+ exports.ProxySecretsAPI = ProxySecretsAPI;
1444
1832
  exports.SecretExistsError = SecretExistsError;
1445
1833
  exports.SecretsAPI = SecretsAPI;
1446
1834
  exports.SlicerAPIError = SlicerAPIError;