@solarisdk/core 0.3.1 → 0.4.0

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.
Files changed (58) hide show
  1. package/README.md +4 -0
  2. package/dist/chunked-download.d.ts +0 -1
  3. package/dist/chunked-download.js +0 -11
  4. package/dist/desktop.d.ts +0 -1
  5. package/dist/desktop.js +0 -52
  6. package/dist/errors.d.ts +10 -1
  7. package/dist/errors.js +16 -39
  8. package/dist/handle.d.ts +7 -1
  9. package/dist/handle.js +87 -235
  10. package/dist/http.d.ts +2 -1
  11. package/dist/http.js +1 -49
  12. package/dist/ids.d.ts +0 -1
  13. package/dist/ids.js +0 -6
  14. package/dist/image.d.ts +0 -1
  15. package/dist/image.js +0 -40
  16. package/dist/index.d.ts +3 -4
  17. package/dist/index.js +3 -5
  18. package/dist/sandbox.d.ts +0 -1
  19. package/dist/sandbox.js +0 -6
  20. package/dist/template-client.d.ts +0 -1
  21. package/dist/template-client.js +3 -27
  22. package/dist/transport.d.ts +9 -2
  23. package/dist/transport.js +64 -102
  24. package/dist/types.d.ts +19 -7
  25. package/dist/types.js +0 -7
  26. package/dist/volume-client.d.ts +0 -1
  27. package/dist/volume-client.js +0 -17
  28. package/dist/ws.d.ts +0 -1
  29. package/dist/ws.js +0 -16
  30. package/package.json +4 -3
  31. package/dist/chunked-download.d.ts.map +0 -1
  32. package/dist/chunked-download.js.map +0 -1
  33. package/dist/desktop.d.ts.map +0 -1
  34. package/dist/desktop.js.map +0 -1
  35. package/dist/errors.d.ts.map +0 -1
  36. package/dist/errors.js.map +0 -1
  37. package/dist/handle.d.ts.map +0 -1
  38. package/dist/handle.js.map +0 -1
  39. package/dist/http.d.ts.map +0 -1
  40. package/dist/http.js.map +0 -1
  41. package/dist/ids.d.ts.map +0 -1
  42. package/dist/ids.js.map +0 -1
  43. package/dist/image.d.ts.map +0 -1
  44. package/dist/image.js.map +0 -1
  45. package/dist/index.d.ts.map +0 -1
  46. package/dist/index.js.map +0 -1
  47. package/dist/sandbox.d.ts.map +0 -1
  48. package/dist/sandbox.js.map +0 -1
  49. package/dist/template-client.d.ts.map +0 -1
  50. package/dist/template-client.js.map +0 -1
  51. package/dist/transport.d.ts.map +0 -1
  52. package/dist/transport.js.map +0 -1
  53. package/dist/types.d.ts.map +0 -1
  54. package/dist/types.js.map +0 -1
  55. package/dist/volume-client.d.ts.map +0 -1
  56. package/dist/volume-client.js.map +0 -1
  57. package/dist/ws.d.ts.map +0 -1
  58. package/dist/ws.js.map +0 -1
package/README.md CHANGED
@@ -16,6 +16,10 @@ npm install @solarisdk/sdk # unified client + `solari` CLI
16
16
 
17
17
  `@solarisdk/core` is a dependency of all three and is installed automatically.
18
18
 
19
+ ## Requirements
20
+
21
+ Node 18+ (or a browser with `WebSocket` and `fetch`). The type declarations need TypeScript 5.0 or newer; `await using` needs TypeScript 5.2+ and a runtime with `Symbol.asyncDispose` (Node 24, or a polyfill).
22
+
19
23
  ## License
20
24
 
21
25
  MIT
@@ -20,4 +20,3 @@ export declare function downloadAll(call: (method: string, params: Record<string
20
20
  maxBytes?: number;
21
21
  }): Promise<Uint8Array>;
22
22
  export {};
23
- //# sourceMappingURL=chunked-download.d.ts.map
@@ -1,17 +1,7 @@
1
1
  import { SolariError } from "./errors.js";
2
2
  import { base64ToBytes } from "./transport.js";
3
- /** Default ceiling for one download: the whole file is held in memory. */
4
3
  export const DEFAULT_MAX_DOWNLOAD_BYTES = 2 * 1024 * 1024 * 1024;
5
4
  const MAX_CHUNKS = 20_000;
6
- /**
7
- * Download a whole in-guest file. `fs.download` returns at most one chunk (1 MiB by default) per call and says
8
- * where the next one starts, so a single call silently truncates anything bigger. This follows `nextOffset` until
9
- * `eof`. An agent that does not report `eof` sent the whole file in one reply, as before.
10
- *
11
- * Guards: the result is capped at `maxBytes` (default 2 GiB) and at a fixed chunk count, so an agent that never
12
- * says `eof` cannot loop forever or exhaust memory; a `nextOffset` that is not exactly where the chunk ended would
13
- * silently skip or repeat bytes, so it is an error; a file whose reported size changes mid-read is an error.
14
- */
15
5
  export async function downloadAll(call, path, opts = {}) {
16
6
  const maxBytes = opts.maxBytes ?? DEFAULT_MAX_DOWNLOAD_BYTES;
17
7
  const fail = (why) => new SolariError(`fs.download ${JSON.stringify(path)}: ${why}`);
@@ -52,4 +42,3 @@ export async function downloadAll(call, path, opts = {}) {
52
42
  }
53
43
  return out;
54
44
  }
55
- //# sourceMappingURL=chunked-download.js.map
package/dist/desktop.d.ts CHANGED
@@ -110,4 +110,3 @@ export declare class Desktop extends SessionHandle {
110
110
  stop: () => Promise<RecordStopResult>;
111
111
  };
112
112
  }
113
- //# sourceMappingURL=desktop.d.ts.map
package/dist/desktop.js CHANGED
@@ -1,22 +1,5 @@
1
- // `Desktop` — a handle to one live GUI session. Extends the shared
2
- // {@link SessionHandle} (commands, pty, runCode, files, metrics, snapshot,
3
- // revert, pause/resume, setTimeout, env, volumes, kill) and adds the
4
- // computer-use GUI surface (CONTRACTS §4 + CONTRACTS-V2 §4): screenshot,
5
- // mouse.*, keyboard.*, display.*, clipboard.*, open, stream.*, record.*.
6
- //
7
- // Back-compat: the v1 `DesktopClient` constructs `new Desktop(session, config)`
8
- // where `config.onPause(sessionId)` calls `POST /desktops/:id/pause`. That path
9
- // is preserved; the older convenience methods (`exec`, `execStream`, `health`,
10
- // `process`, `ports`, `pkg`) are kept too.
11
1
  import { SessionHandle } from "./handle.js";
12
2
  import { base64ToBytes } from "./transport.js";
13
- /**
14
- * Map a named {@link MouseButton} to the X11/xdotool button code the guest
15
- * agent expects (`mouseParams.button` is an `int`): left=1, middle=2, right=3.
16
- * `undefined` is passed through so the guest applies its own default (left).
17
- * Without this the wire would carry a raw string and the guest's JSON decode
18
- * fails with `cannot unmarshal string into ... button of type int`.
19
- */
20
3
  function buttonToCode(button) {
21
4
  if (button === undefined)
22
5
  return undefined;
@@ -29,22 +12,13 @@ function buttonToCode(button) {
29
12
  return 3;
30
13
  }
31
14
  }
32
- /** A live desktop session. Construct via {@link DesktopClient}. */
33
15
  export class Desktop extends SessionHandle {
34
16
  streamUrl;
35
- /**
36
- * Presigned playback URL for the session recording — set when the session was
37
- * created with `record: true`, `undefined` otherwise. The guest uploads the
38
- * mp4 on `record.stop()`, so the URL only resolves once a recording has been
39
- * started and stopped.
40
- */
41
17
  recordingUrl;
42
- /** The session id (alias of {@link SessionHandle.id}; back-compat). */
43
18
  get sessionId() {
44
19
  return this.id;
45
20
  }
46
21
  constructor(session, config = {}) {
47
- // Fold the legacy onPause shim into the unified hooks.pause.
48
22
  const hooks = { ...(config.hooks ?? {}) };
49
23
  if (config.onPause && !hooks.pause) {
50
24
  const onPause = config.onPause;
@@ -57,16 +31,12 @@ export class Desktop extends SessionHandle {
57
31
  this.streamUrl = session.streamUrl;
58
32
  this.recordingUrl = session.recordingUrl;
59
33
  }
60
- // GUI RPCs go over the shared control channel via the protected `rpc` hook.
61
34
  guiCall(method, params) {
62
35
  return this.rpc(method, params);
63
36
  }
64
- // --- readiness ------------------------------------------------------------
65
- /** `health` — readiness probe (display + VNC + agent). */
66
37
  health() {
67
38
  return this.guiCall("health", {});
68
39
  }
69
- // --- exec (v1 convenience; prefer `commands.run`) -------------------------
70
40
  exec(cmd, opts = {}) {
71
41
  return this.guiCall("exec", {
72
42
  cmd,
@@ -82,7 +52,6 @@ export class Desktop extends SessionHandle {
82
52
  onChunk({ stream: c.stream, text: new TextDecoder().decode(bytes), bytes });
83
53
  });
84
54
  }
85
- // --- fs (v1 alias kept; `files` is the canonical surface) -----------------
86
55
  fs = {
87
56
  read: (path) => this.files.read(path),
88
57
  readText: (path) => this.files.readText(path),
@@ -92,7 +61,6 @@ export class Desktop extends SessionHandle {
92
61
  remove: (path, recursive = false) => this.files.remove(path, recursive),
93
62
  mkdir: (path) => this.files.mkdir(path),
94
63
  };
95
- // --- mouse ----------------------------------------------------------------
96
64
  mouse = {
97
65
  move: (x, y, opts = {}) => this.mouseCall(x, y, "move", undefined, opts.humanize),
98
66
  click: (x, y, opts = {}) => this.mouseCall(x, y, "click", opts.button, opts.humanize),
@@ -115,11 +83,9 @@ export class Desktop extends SessionHandle {
115
83
  humanize,
116
84
  }).then(() => undefined);
117
85
  }
118
- // --- keyboard -------------------------------------------------------------
119
86
  keyboard = {
120
87
  type: (text) => this.keyCall({ text, action: "press" }),
121
88
  press: (keys) => this.keyCall({ keys: Array.isArray(keys) ? keys : [keys], action: "press" }),
122
- /** Press a chord (alias of `press` for an array, e.g. `hotkey("ctrl","c")`). */
123
89
  hotkey: (...keys) => this.keyCall({ keys, action: "press" }),
124
90
  down: (keys) => this.keyCall({ keys: Array.isArray(keys) ? keys : [keys], action: "down" }),
125
91
  up: (keys) => this.keyCall({ keys: Array.isArray(keys) ? keys : [keys], action: "up" }),
@@ -127,7 +93,6 @@ export class Desktop extends SessionHandle {
127
93
  keyCall(params) {
128
94
  return this.guiCall("input.key", params).then(() => undefined);
129
95
  }
130
- // --- screenshot -----------------------------------------------------------
131
96
  async screenshot(opts = {}) {
132
97
  const r = await this.guiCall("screenshot", {
133
98
  format: opts.format ?? "png",
@@ -135,15 +100,11 @@ export class Desktop extends SessionHandle {
135
100
  });
136
101
  return base64ToBytes(r.base64);
137
102
  }
138
- // --- display --------------------------------------------------------------
139
103
  display = {
140
104
  set: (w, h) => this.guiCall("display.set", { w, h }).then(() => undefined),
141
- /** Current display size. */
142
105
  size: () => this.guiCall("display.size", {}),
143
- /** Current cursor position. */
144
106
  cursor: () => this.guiCall("display.cursor", {}),
145
107
  };
146
- // --- clipboard ------------------------------------------------------------
147
108
  clipboard = {
148
109
  get: async () => {
149
110
  const r = await this.guiCall("clipboard.get", {});
@@ -151,26 +112,17 @@ export class Desktop extends SessionHandle {
151
112
  },
152
113
  set: (text) => this.guiCall("clipboard.set", { text }).then(() => undefined),
153
114
  };
154
- // --- open an app ----------------------------------------------------------
155
- /** Launch a GUI app by name (`app.open`); returns its pid. */
156
115
  async open(name, args) {
157
116
  const r = await this.guiCall("app.open", { name, args });
158
117
  return r.pid;
159
118
  }
160
- // --- live stream ----------------------------------------------------------
161
119
  stream = {
162
- /** Start/obtain the live VNC stream; returns the embeddable URL + token. */
163
120
  start: async () => {
164
- // The stream URL is minted at create time; expose it (+ any token) here so
165
- // callers have a single `stream.start()` entry point.
166
121
  return { streamUrl: this.streamUrl };
167
122
  },
168
- /** Stop the live stream (best-effort; the channel stays up). */
169
123
  stop: async () => {
170
- /* the RFB stream is a separate socket the caller owns; nothing to tear down here */
171
124
  },
172
125
  };
173
- // --- process (v1 convenience) ---------------------------------------------
174
126
  process = {
175
127
  list: async () => {
176
128
  const r = await this.guiCall("process.list", {});
@@ -187,21 +139,17 @@ export class Desktop extends SessionHandle {
187
139
  },
188
140
  signal: (pid, signal) => this.guiCall("process.signal", { pid, signal }).then(() => undefined),
189
141
  };
190
- // --- ports (v1 convenience) -----------------------------------------------
191
142
  ports = {
192
143
  list: async () => {
193
144
  const r = await this.guiCall("ports.list", {});
194
145
  return r.ports;
195
146
  },
196
147
  };
197
- // --- packages -------------------------------------------------------------
198
148
  pkg = {
199
149
  install: (manager, packages) => this.guiCall("pkg.install", { manager, packages }),
200
150
  };
201
- // --- recording ------------------------------------------------------------
202
151
  record = {
203
152
  start: (opts = {}) => this.guiCall("record.start", opts),
204
153
  stop: () => this.guiCall("record.stop", {}),
205
154
  };
206
155
  }
207
- //# sourceMappingURL=desktop.js.map
package/dist/errors.d.ts CHANGED
@@ -8,8 +8,18 @@ export declare class GatewayError extends SolariError {
8
8
  readonly status: number;
9
9
  readonly code?: string;
10
10
  readonly body?: GatewayErrorBody;
11
+ /** The gateway's hint that retrying may help (`body.retryable`), on every gateway error. */
12
+ readonly retryable?: boolean;
11
13
  constructor(status: number, message: string, body?: GatewayErrorBody);
12
14
  }
15
+ /** `404` — the sandbox, desktop, snapshot, template or volume does not exist (or is not yours). */
16
+ export declare class NotFoundError extends GatewayError {
17
+ constructor(message?: string, body?: GatewayErrorBody);
18
+ }
19
+ /** `400` — the request was refused as invalid (a bad field, a limit exceeded); `body.code` / `message` say which. */
20
+ export declare class ValidationError extends GatewayError {
21
+ constructor(message?: string, body?: GatewayErrorBody);
22
+ }
13
23
  /** `401` — the API key was missing, malformed, or rejected. */
14
24
  export declare class AuthError extends GatewayError {
15
25
  constructor(message?: string, body?: GatewayErrorBody);
@@ -71,4 +81,3 @@ export declare class ConnectionError extends SolariError {
71
81
  * Falls back to a generic {@link GatewayError} for unrecognized statuses.
72
82
  */
73
83
  export declare function mapGatewayError(status: number, body: GatewayErrorBody | undefined): GatewayError;
74
- //# sourceMappingURL=errors.d.ts.map
package/dist/errors.js CHANGED
@@ -1,76 +1,59 @@
1
- // Typed errors. Gateway HTTP status codes (§1) and control-channel RPC
2
- // failures (§4) are mapped onto these so callers can branch on `instanceof`.
3
- /** Base class for every error thrown by the SDK. */
4
1
  export class SolariError extends Error {
5
2
  constructor(message) {
6
3
  super(message);
7
4
  this.name = new.target.name;
8
5
  }
9
6
  }
10
- /** Any non-2xx response from the gateway that isn't otherwise specialized. */
11
7
  export class GatewayError extends SolariError {
12
8
  status;
13
9
  code;
14
10
  body;
11
+ retryable;
15
12
  constructor(status, message, body) {
16
13
  super(message);
17
14
  this.status = status;
18
15
  this.code = body?.code;
19
16
  this.body = body;
17
+ this.retryable = body?.retryable;
18
+ }
19
+ }
20
+ export class NotFoundError extends GatewayError {
21
+ constructor(message = "Not found", body) {
22
+ super(404, message, body);
23
+ }
24
+ }
25
+ export class ValidationError extends GatewayError {
26
+ constructor(message = "Invalid request", body) {
27
+ super(400, message, body);
20
28
  }
21
29
  }
22
- /** `401` — the API key was missing, malformed, or rejected. */
23
30
  export class AuthError extends GatewayError {
24
31
  constructor(message = "Invalid or missing API key", body) {
25
32
  super(401, message, body);
26
33
  }
27
34
  }
28
- /**
29
- * The plan doesn't allow this: `402` (`FeatureRequiresPlan`, `InsufficientCredit`) or `403 {code:"NotEntitled"}` (the plan
30
- * does not include sandboxes/desktops). `status` is the status the gateway really sent.
31
- */
32
35
  export class PlanError extends GatewayError {
33
36
  constructor(message = "This feature requires a different plan", body, status = 402) {
34
37
  super(status, message, body);
35
38
  }
36
39
  }
37
- /**
38
- * `429 {code:"ConcurrencyLimitExceeded"}` — the org is at its cap of live sessions. Other 429s (for example
39
- * `TooManyExecutions`, a per-sandbox limit that can be retried) are plain {@link GatewayError}s with the gateway's `code`.
40
- */
41
40
  export class ConcurrencyLimitError extends GatewayError {
42
41
  constructor(message = "Concurrency limit exceeded", body) {
43
42
  super(429, message, body);
44
43
  }
45
44
  }
46
- /**
47
- * `503 {code:"NoCapacity"|"StartTimeout"}` (or a `503` with no code) — no desktop host currently available to serve the
48
- * request. Other 503s (`SandboxUnavailable`, `StoreUnavailable`, `AdmissionUnavailable`, `DependencyUnavailable`) are plain
49
- * {@link GatewayError}s: the sandbox, store or admission service is unavailable, which is not a capacity problem.
50
- */
51
45
  export class NoCapacityError extends GatewayError {
52
46
  constructor(message = "No desktop host available", body) {
53
47
  super(503, message, body);
54
48
  }
55
49
  }
56
- /**
57
- * `409 {code:"NotStopped"}` or `409 {code:"StartInProgress"}` from
58
- * `POST /sandboxes/:id/start`. `NotStopped` means the sandbox is in another
59
- * state (use `resume()` for a paused one); `StartInProgress` means a start is
60
- * already under way, so poll until the sandbox is running. Not retryable as-is.
61
- * Host refusals (`StoppedDiskMissing`, `StoppedDiskUnsafe`, `GoldenMismatch`, `DiskPlatformMismatch`,
62
- * `DiskShaMismatch`, `DiskExceedsSpec`, `DiskOwnerMismatch`) and `HostCapabilityMissing` (host pool not armed
63
- * for start; `body.missingCapabilities`) stay plain {@link GatewayError}s with the host's `code`.
64
- */
65
50
  export class SandboxNotStoppedError extends GatewayError {
66
- /** The sandbox's current state as reported by the gateway, when present. */
67
51
  state;
68
52
  constructor(message = "Sandbox is not stopped", body) {
69
53
  super(409, message, body);
70
54
  this.state = body?.state;
71
55
  }
72
56
  }
73
- /** A computer-use RPC returned `{ok:false}`. */
74
57
  export class ActionError extends SolariError {
75
58
  method;
76
59
  code;
@@ -80,7 +63,6 @@ export class ActionError extends SolariError {
80
63
  this.code = code;
81
64
  }
82
65
  }
83
- /** An RPC did not receive a reply within the per-call timeout. */
84
66
  export class TimeoutError extends SolariError {
85
67
  method;
86
68
  timeoutMs;
@@ -90,28 +72,24 @@ export class TimeoutError extends SolariError {
90
72
  this.timeoutMs = timeoutMs;
91
73
  }
92
74
  }
93
- /** The control WebSocket is not open (never connected, or closed). */
94
75
  export class ConnectionError extends SolariError {
95
76
  constructor(message = "Control channel is not connected") {
96
77
  super(message);
97
78
  }
98
79
  }
99
- /**
100
- * Map a gateway HTTP response onto the appropriate typed error.
101
- * Falls back to a generic {@link GatewayError} for unrecognized statuses.
102
- */
103
80
  export function mapGatewayError(status, body) {
104
81
  const msg = body?.message ?? body?.error ?? body?.code;
105
- // `code` is the discriminator (sdk/PROTOCOL.md section 4); the status alone is not enough because one status carries
106
- // several meanings. A response with no code keeps the type its status has always had.
107
82
  const code = body?.code;
108
83
  switch (status) {
84
+ case 400:
85
+ return new ValidationError(msg, body);
86
+ case 404:
87
+ return new NotFoundError(msg, body);
109
88
  case 401:
110
89
  return new AuthError(msg, body);
111
90
  case 402:
112
91
  return new PlanError(msg, body);
113
92
  case 403:
114
- // The gateway's only JSON 403 is NotEntitled (the plan does not include sandboxes/desktops).
115
93
  if (code === "NotEntitled")
116
94
  return new PlanError(msg, body, 403);
117
95
  return new GatewayError(403, msg ?? "Forbidden", body);
@@ -132,4 +110,3 @@ export function mapGatewayError(status, body) {
132
110
  return new GatewayError(status, msg ?? `Gateway request failed with status ${status}`, body);
133
111
  }
134
112
  }
135
- //# sourceMappingURL=errors.js.map
package/dist/handle.d.ts CHANGED
@@ -54,6 +54,8 @@ export interface SessionHooks {
54
54
  export interface SessionConfig extends ControlChannelConfig {
55
55
  hooks?: SessionHooks;
56
56
  }
57
+ /** The longest `timeoutMs` the gateway accepts for a command (600 000 ms), and the default for `commands.run`. */
58
+ export declare const DEFAULT_COMMAND_TIMEOUT_MS = 600000;
57
59
  /** Cap on PTY output held before the first onData() subscriber (bytes, total). */
58
60
  export declare const PTY_EARLY_BUFFER_MAX: number;
59
61
  export declare class SessionHandle {
@@ -66,6 +68,8 @@ export declare class SessionHandle {
66
68
  /** Set by {@link kill} so {@link close} doesn't warn about a machine we
67
69
  * have just released. */
68
70
  private killed;
71
+ /** Key of this handle in the exit-reminder list (see `unreleased`). */
72
+ private readonly releaseToken;
69
73
  constructor(id: string, controlUrl: string, expiresAt: string, config?: SessionConfig);
70
74
  /** Whether the control channel is open. */
71
75
  get connected(): boolean;
@@ -257,6 +261,9 @@ export declare class SessionHandle {
257
261
  * Resolve a public preview URL for an in-guest port (e.g. a dev server on
258
262
  * `:3000`). Returns the routable `https://` URL and, when the gateway signs
259
263
  * one, an access `token`.
264
+ *
265
+ * The URL can carry that token as `?slr_token=…`: it is a credential for the port, so don't print or log the whole
266
+ * URL and avoid opening it in a shared browser profile, where it lands in history.
260
267
  */
261
268
  previewUrl(port: number): Promise<{
262
269
  url: string;
@@ -268,4 +275,3 @@ export declare class SessionHandle {
268
275
  /** Local helper type for `createCodeContext` to avoid a hard import cycle. */
269
276
  type CodeLanguageArg = "python" | "javascript" | "typescript" | "bash" | "r";
270
277
  export {};
271
- //# sourceMappingURL=handle.d.ts.map