@slicervm/sdk 0.1.7 → 0.1.9

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,42 @@ 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 wait for agent readiness and guest
40
+ identity finalisation by default. `wait: 'none'` acknowledges launch only; if
41
+ requested asynchronous finalisation fails, the daemon removes the child rather
42
+ than expose cloned identity or secrets with partially applied policy.
43
+
44
+ ```ts
45
+ await vm.shutdown();
46
+ const parent = await vm.commit({
47
+ tags: ['node-build'],
48
+ cacheKey: 'node-build-v1',
49
+ });
50
+
51
+ const child = await parent.fork({
52
+ wait: 'agent',
53
+ waitTimeoutSec: 120,
54
+ persistent: false,
55
+ vcpu: 1,
56
+ ramBytes: 512 * 1024 * 1024,
57
+ network: { allow: [], drop: ['0.0.0.0/0'] },
58
+ tagMode: 'replace',
59
+ tags: ['job=node-build'],
60
+ secrets: [],
61
+ });
62
+
63
+ const description = await child.describe();
64
+ console.log(description.parentCommitId, description.network.effective);
65
+
66
+ const cached = await client.commits.list({ cacheKey: 'node-build-v1' });
67
+ await child.delete();
68
+ await vm.delete();
69
+ await client.commits.delete(parent.commitId);
70
+ ```
71
+
36
72
  ## Status
37
73
 
38
74
  Pre-release. See `ts-plan.md`.
package/dist/index.cjs CHANGED
@@ -284,7 +284,72 @@ function agentHealthFromWire(w) {
284
284
  ...w.agent_uptime !== void 0 && { agentUptime: w.agent_uptime },
285
285
  ...w.agent_version !== void 0 && { agentVersion: w.agent_version },
286
286
  ...w.system_uptime !== void 0 && { systemUptime: w.system_uptime },
287
- ...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
+ ...w.persistent !== void 0 && { persistent: w.persistent }
328
+ };
329
+ }
330
+ function vmDescriptionFromWire(w) {
331
+ return {
332
+ hostname: w.hostname,
333
+ ...w.hostgroup !== void 0 && { hostGroup: w.hostgroup },
334
+ ...w.ip !== void 0 && { ip: w.ip },
335
+ ...w.ram_bytes !== void 0 && { ramBytes: w.ram_bytes },
336
+ ...w.cpus !== void 0 && { cpus: w.cpus },
337
+ ...w.created_at !== void 0 && { createdAt: w.created_at },
338
+ ...w.arch !== void 0 && { arch: w.arch },
339
+ ...w.tags !== void 0 && { tags: w.tags },
340
+ ...w.status !== void 0 && { status: w.status },
341
+ ...w.persistent !== void 0 && { persistent: w.persistent },
342
+ ...w.storage !== void 0 && { storage: w.storage },
343
+ ...w.image !== void 0 && { image: w.image },
344
+ ...w.commit_id !== void 0 && { commitId: w.commit_id },
345
+ ...w.parent_commit_id !== void 0 && { parentCommitId: w.parent_commit_id },
346
+ network: {
347
+ ...w.network.mode !== void 0 && { mode: w.network.mode },
348
+ ...w.network.policy_source !== void 0 && { policySource: w.network.policy_source },
349
+ hostGroup: w.network.host_group,
350
+ ...w.network.override !== void 0 && { override: w.network.override },
351
+ effective: w.network.effective
352
+ }
288
353
  };
289
354
  }
290
355
  function fsEntryFromWire(w) {
@@ -309,6 +374,31 @@ function secretFromWire(w) {
309
374
  ...w.modified_at !== void 0 && { modifiedAt: w.modified_at }
310
375
  };
311
376
  }
377
+
378
+ // src/fork.ts
379
+ function buildForkRequest(opts) {
380
+ const wait = opts.wait ?? "agent";
381
+ if (wait !== "agent" && wait !== "none") {
382
+ throw new Error(`invalid fork wait mode ${JSON.stringify(wait)}`);
383
+ }
384
+ const query = new URLSearchParams({ wait });
385
+ if (opts.waitTimeoutSec !== void 0 && opts.waitTimeoutSec > 0) {
386
+ query.set("timeout", `${opts.waitTimeoutSec}s`);
387
+ }
388
+ const body = {};
389
+ if (opts.network !== void 0) body.network = opts.network;
390
+ if (opts.tags !== void 0) body.tags = opts.tags;
391
+ if (opts.tagMode !== void 0) body.tag_mode = opts.tagMode;
392
+ if (opts.secrets !== void 0) body.secrets = opts.secrets;
393
+ if (opts.persistent !== void 0) body.persistent = opts.persistent;
394
+ if (opts.fixups !== void 0) body.fixups = opts.fixups;
395
+ if (opts.vcpu !== void 0) body.vcpu = opts.vcpu;
396
+ if (opts.ramBytes !== void 0) body.ram_bytes = opts.ramBytes;
397
+ return {
398
+ query: query.toString(),
399
+ ...Object.keys(body).length > 0 && { body }
400
+ };
401
+ }
312
402
  function looksLikeUnixSocketPath(s) {
313
403
  if (!s) return false;
314
404
  return s.startsWith("/") || s.startsWith("./") || s.startsWith("../") || s.includes("/");
@@ -739,6 +829,48 @@ var VMFileSystem = class {
739
829
  );
740
830
  }
741
831
  };
832
+ var CommittedVM = class {
833
+ constructor(transport, hostGroup, response) {
834
+ this.transport = transport;
835
+ this.hostname = response.hostname;
836
+ this.hostGroup = hostGroup;
837
+ this.commitId = response.commitId;
838
+ this.status = response.status;
839
+ if (response.parentStatus !== void 0) this.parentStatus = response.parentStatus;
840
+ this.mode = response.mode;
841
+ if (response.tags !== void 0) this.tags = response.tags;
842
+ if (response.labels !== void 0) this.labels = response.labels;
843
+ if (response.cacheKey !== void 0) this.cacheKey = response.cacheKey;
844
+ }
845
+ transport;
846
+ hostname;
847
+ hostGroup;
848
+ commitId;
849
+ status;
850
+ parentStatus;
851
+ mode;
852
+ tags;
853
+ labels;
854
+ cacheKey;
855
+ async fork(opts = {}) {
856
+ const res = await forkPath(
857
+ this.transport,
858
+ this.commitId,
859
+ opts
860
+ );
861
+ return new VM(this.transport, {
862
+ hostname: res.hostname,
863
+ hostGroup: this.hostGroup
864
+ });
865
+ }
866
+ async forkRaw(opts = {}) {
867
+ return forkPath(
868
+ this.transport,
869
+ this.commitId,
870
+ opts
871
+ );
872
+ }
873
+ };
742
874
  var VM = class {
743
875
  hostname;
744
876
  hostGroup;
@@ -778,6 +910,14 @@ var VM = class {
778
910
  async logs() {
779
911
  return this.transport.request("GET", `/vm/${encodeURIComponent(this.hostname)}/logs`);
780
912
  }
913
+ /** Return configuration, fork lineage, and effective network policy. */
914
+ async describe() {
915
+ const wire = await this.transport.request(
916
+ "GET",
917
+ `/vm/${encodeURIComponent(this.hostname)}`
918
+ );
919
+ return vmDescriptionFromWire(wire);
920
+ }
781
921
  async waitForAgent(opts = {}) {
782
922
  const timeoutMs = opts.timeoutMs ?? 12e4;
783
923
  const intervalMs = opts.intervalMs ?? 500;
@@ -804,10 +944,15 @@ var VM = class {
804
944
  while (Date.now() < deadline) {
805
945
  try {
806
946
  last = await this.health();
807
- if (last.userdataRan) return last;
808
947
  } catch (err) {
809
948
  lastErr = err;
810
949
  }
950
+ if (last?.userdataRan) {
951
+ if (last.userdataExitCode !== void 0 && last.userdataExitCode !== 0) {
952
+ throw new Error(`userdata failed with exit code ${last.userdataExitCode}`);
953
+ }
954
+ return last;
955
+ }
811
956
  await sleep(intervalMs);
812
957
  }
813
958
  throw new Error(
@@ -839,6 +984,25 @@ var VM = class {
839
984
  async restore() {
840
985
  await this.transport.request("POST", `/vm/${encodeURIComponent(this.hostname)}/restore`);
841
986
  }
987
+ /**
988
+ * Commit a stopped persistent VM disk into an immutable parent.
989
+ */
990
+ async commit(opts = {}) {
991
+ const cacheKey = opts.cacheKey?.trim();
992
+ const tags = opts.tags && opts.tags.length > 0 ? opts.tags : void 0;
993
+ const labels = opts.labels && Object.keys(opts.labels).length > 0 ? opts.labels : void 0;
994
+ const body = tags !== void 0 || labels !== void 0 || cacheKey ? {
995
+ ...tags !== void 0 && { tags },
996
+ ...labels !== void 0 && { labels },
997
+ ...cacheKey && { cache_key: cacheKey }
998
+ } : void 0;
999
+ const wire = await this.transport.request(
1000
+ "POST",
1001
+ `/vm/${encodeURIComponent(this.hostname)}/commit`,
1002
+ body
1003
+ );
1004
+ return new CommittedVM(this.transport, this.hostGroup, vmCommitFromWire(wire));
1005
+ }
842
1006
  // --- port forwarding ---------------------------------------------------
843
1007
  /**
844
1008
  * Open one or more port forwards from the host to this VM. Each spec follows
@@ -958,6 +1122,23 @@ function sleep(ms) {
958
1122
  function errMsg(e) {
959
1123
  return e instanceof Error ? e.message : String(e);
960
1124
  }
1125
+ async function forkPath(transport, commitId, opts) {
1126
+ const id = validateCommitId(commitId);
1127
+ const request = buildForkRequest(opts);
1128
+ const wire = await transport.request(
1129
+ "POST",
1130
+ `/vm/commits/${encodeURIComponent(id)}/fork?${request.query}`,
1131
+ request.body
1132
+ );
1133
+ return vmForkFromWire(wire);
1134
+ }
1135
+ function validateCommitId(commitId) {
1136
+ const value = commitId.trim();
1137
+ if (!value || value === "." || value === ".." || value.includes("..") || value.includes("/") || value.includes("\\") || value.includes("\0")) {
1138
+ throw new Error(`invalid commit ID ${JSON.stringify(value)}`);
1139
+ }
1140
+ return value;
1141
+ }
961
1142
  var VMBg = class {
962
1143
  constructor(transport, hostname) {
963
1144
  this.transport = transport;
@@ -1216,6 +1397,47 @@ var VMsAPI = class {
1216
1397
  return createVMResFromWire(wire);
1217
1398
  }
1218
1399
  };
1400
+ var CommitsAPI = class {
1401
+ constructor(transport) {
1402
+ this.transport = transport;
1403
+ }
1404
+ transport;
1405
+ async list(opts = {}) {
1406
+ const qs = new URLSearchParams();
1407
+ for (const tag of opts.tags ?? []) {
1408
+ const value = tag.trim();
1409
+ if (value) qs.append("tag", value);
1410
+ }
1411
+ const cacheKey = opts.cacheKey?.trim();
1412
+ const source = opts.source?.trim();
1413
+ const mode = opts.mode?.trim();
1414
+ if (cacheKey) qs.set("cache_key", cacheKey);
1415
+ if (source) qs.set("source", source);
1416
+ if (mode) qs.set("mode", mode);
1417
+ const encoded = qs.toString();
1418
+ const query = encoded ? `?${encoded}` : "";
1419
+ const wire = await this.transport.request("GET", `/vm/commits${query}`);
1420
+ return (wire ?? []).map(vmCommitInfoFromWire);
1421
+ }
1422
+ async delete(commitId) {
1423
+ const id = validateCommitId2(commitId);
1424
+ const wire = await this.transport.request(
1425
+ "DELETE",
1426
+ `/vm/commits/${encodeURIComponent(id)}`
1427
+ );
1428
+ return vmCommitDeleteFromWire(wire);
1429
+ }
1430
+ async fork(commitId, opts = {}) {
1431
+ const id = validateCommitId2(commitId);
1432
+ const request = buildForkRequest(opts);
1433
+ const wire = await this.transport.request(
1434
+ "POST",
1435
+ `/vm/commits/${encodeURIComponent(id)}/fork?${request.query}`,
1436
+ request.body
1437
+ );
1438
+ return vmForkFromWire(wire);
1439
+ }
1440
+ };
1219
1441
  var SecretsAPI = class {
1220
1442
  constructor(transport) {
1221
1443
  this.transport = transport;
@@ -1262,10 +1484,18 @@ function buildListQuery(opts) {
1262
1484
  const s = qs.toString();
1263
1485
  return s ? `?${s}` : "";
1264
1486
  }
1487
+ function validateCommitId2(commitId) {
1488
+ const value = commitId.trim();
1489
+ if (!value || value === "." || value === ".." || value.includes("..") || value.includes("/") || value.includes("\\") || value.includes("\0")) {
1490
+ throw new Error(`invalid commit ID ${JSON.stringify(value)}`);
1491
+ }
1492
+ return value;
1493
+ }
1265
1494
 
1266
1495
  // src/proxy.ts
1267
1496
  var ProxySecretBearer = "bearer";
1268
1497
  var ProxySecretBasic = "basic";
1498
+ var ProxySecretOAuthClientCredentials = "oauth-client-creds";
1269
1499
  function clientFromWire(w) {
1270
1500
  return { name: w.name, createdAt: w.created_at };
1271
1501
  }
@@ -1422,12 +1652,14 @@ var SlicerClient = class _SlicerClient {
1422
1652
  transport;
1423
1653
  hostGroups;
1424
1654
  vms;
1655
+ commits;
1425
1656
  secrets;
1426
1657
  proxy;
1427
1658
  constructor(opts) {
1428
1659
  this.transport = new TransportClient(opts);
1429
1660
  this.hostGroups = new HostGroupsAPI(this.transport);
1430
1661
  this.vms = new VMsAPI(this.transport);
1662
+ this.commits = new CommitsAPI(this.transport);
1431
1663
  this.secrets = new SecretsAPI(this.transport);
1432
1664
  this.proxy = new ProxyAPI(this.transport);
1433
1665
  }
@@ -1586,6 +1818,8 @@ var SlicerShellSession = class {
1586
1818
  }
1587
1819
  };
1588
1820
 
1821
+ exports.CommitsAPI = CommitsAPI;
1822
+ exports.CommittedVM = CommittedVM;
1589
1823
  exports.ExecStdioBase64 = ExecStdioBase64;
1590
1824
  exports.ExecStdioText = ExecStdioText;
1591
1825
  exports.FRAME_TYPE_DATA = FRAME_TYPE_DATA;
@@ -1603,6 +1837,7 @@ exports.ProxyAllowsAPI = ProxyAllowsAPI;
1603
1837
  exports.ProxyClientsAPI = ProxyClientsAPI;
1604
1838
  exports.ProxySecretBasic = ProxySecretBasic;
1605
1839
  exports.ProxySecretBearer = ProxySecretBearer;
1840
+ exports.ProxySecretOAuthClientCredentials = ProxySecretOAuthClientCredentials;
1606
1841
  exports.ProxySecretsAPI = ProxySecretsAPI;
1607
1842
  exports.SecretExistsError = SecretExistsError;
1608
1843
  exports.SecretsAPI = SecretsAPI;