@watasu/sdk 0.1.1 → 0.1.4

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 ADDED
@@ -0,0 +1,29 @@
1
+ # Watasu TypeScript SDK
2
+
3
+ TypeScript SDK for Watasu.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @watasu/sdk
9
+ ```
10
+
11
+ Set `WATASU_API_KEY` before using the SDK.
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { Sandbox } from '@watasu/sdk'
17
+
18
+ const sbx = await Sandbox.create()
19
+ await sbx.files.write('/home/user/a.js', 'console.log(2 + 2)')
20
+ const result = await sbx.commands.run('node /home/user/a.js')
21
+ console.log(result.stdout)
22
+ console.log(await sbx.isRunning())
23
+ await sbx.kill()
24
+ ```
25
+
26
+ `Sandbox.create` and `Sandbox.connect` return only after the Watasu API supplies
27
+ a usable data-plane session. The SDK does not poll sandbox readiness.
28
+
29
+ The SDK is ESM-first and ships TypeScript declarations.
@@ -3,6 +3,7 @@ export declare const SESSION_OPERATION_REQUEST_TIMEOUT_MS = 150000;
3
3
  /** Connection options accepted by Watasu SDK entrypoints. */
4
4
  export interface ConnectionOpts {
5
5
  apiKey?: string;
6
+ accessToken?: string;
6
7
  domain?: string;
7
8
  apiUrl?: string;
8
9
  dataPlaneDomain?: string;
@@ -11,7 +11,10 @@ export class ConnectionConfig {
11
11
  proxy;
12
12
  constructor(opts = {}) {
13
13
  const env = typeof process !== 'undefined' ? process.env : {};
14
- this.apiKey = opts.apiKey ?? env.WATASU_API_KEY;
14
+ this.apiKey =
15
+ opts.apiKey ??
16
+ opts.accessToken ??
17
+ env.WATASU_API_KEY;
15
18
  this.domain = opts.domain ?? env.WATASU_DOMAIN ?? 'watasu.io';
16
19
  this.apiUrl =
17
20
  opts.apiUrl ?? env.WATASU_API_URL ?? `https://api.${this.domain}/v1`;
package/dist/sandbox.d.ts CHANGED
@@ -11,13 +11,7 @@ export interface SandboxCreateOpts extends ConnectionOpts {
11
11
  envs?: Record<string, string>;
12
12
  secure?: boolean;
13
13
  allowInternetAccess?: boolean;
14
- templateVersionId?: number | string;
15
14
  team?: string;
16
- cpu?: number;
17
- memoryMb?: number;
18
- networkClass?: string;
19
- allowPackageRegistryAccess?: boolean;
20
- exposedPorts?: unknown[];
21
15
  mcp?: unknown;
22
16
  volumeMounts?: unknown;
23
17
  }
@@ -27,7 +21,8 @@ export interface SandboxConnectOpts extends ConnectionOpts {
27
21
  }
28
22
  export interface SandboxInfo {
29
23
  sandboxId: string;
30
- templateVersionId?: number;
24
+ templateId?: string;
25
+ name?: string;
31
26
  state?: string;
32
27
  metadata: Record<string, string>;
33
28
  startedAt?: string;
@@ -48,6 +43,7 @@ export declare class Sandbox {
48
43
  };
49
44
  private readonly config;
50
45
  private readonly control;
46
+ private readonly envs;
51
47
  private dataPlane;
52
48
  private sandbox;
53
49
  constructor(opts: {
@@ -68,6 +64,8 @@ export declare class Sandbox {
68
64
  static kill(sandboxId: string, opts?: ConnectionOpts): Promise<boolean>;
69
65
  /** Destroy this sandbox. */
70
66
  kill(): Promise<boolean>;
67
+ /** Check if this sandbox is in a runtime-active lifecycle state. */
68
+ isRunning(opts?: Pick<ConnectionOpts, 'requestTimeoutMs'>): Promise<boolean>;
71
69
  /** Set a sandbox's lifetime by id. */
72
70
  static setTimeout(sandboxId: string, timeoutMs: number, opts?: ConnectionOpts): Promise<void>;
73
71
  /** Set this sandbox's lifetime. */
@@ -81,7 +79,7 @@ export declare class Sandbox {
81
79
  team?: string;
82
80
  }): Promise<SandboxInfo[]>;
83
81
  /** Return the public hostname for an exposed sandbox port. */
84
- getHost(port: number): Promise<string>;
82
+ getHost(port: number): string;
85
83
  pause(): never;
86
84
  resume(): never;
87
85
  createSnapshot(): never;
package/dist/sandbox.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Commands } from './commands.js';
2
2
  import { ConnectionConfig, SESSION_OPERATION_REQUEST_TIMEOUT_MS } from './connectionConfig.js';
3
3
  import { DataPlaneClient, ControlClient } from './transport.js';
4
- import { SandboxError, unsupported } from './errors.js';
4
+ import { NotFoundError, SandboxError, unsupported } from './errors.js';
5
5
  import { Filesystem } from './filesystem.js';
6
6
  /** Running Watasu sandbox with ready `files` and `commands` helpers. */
7
7
  export class Sandbox {
@@ -14,17 +14,19 @@ export class Sandbox {
14
14
  git = { clone: () => unsupported('sandbox.git') };
15
15
  config;
16
16
  control;
17
+ envs;
17
18
  dataPlane;
18
19
  sandbox;
19
20
  constructor(opts) {
20
21
  this.sandboxId = String(opts.sandboxId);
21
22
  this.config = opts.connectionConfig;
22
23
  this.control = opts.control ?? new ControlClient(this.config);
24
+ this.envs = opts.envs ?? {};
23
25
  this.sandbox = opts.sandbox ?? {};
24
26
  const dataPlane = dataPlaneFromSession(opts.session, this.config);
25
27
  this.dataPlane = dataPlane;
26
28
  this.files = new Filesystem(dataPlane);
27
- this.commands = new Commands(dataPlane, this.config, opts.envs ?? {});
29
+ this.commands = new Commands(dataPlane, this.config, this.envs);
28
30
  }
29
31
  /** Create a sandbox and return it only after the API supplies a data-plane session. */
30
32
  static async create(templateOrOpts, opts = {}) {
@@ -39,20 +41,16 @@ export class Sandbox {
39
41
  const config = new ConnectionConfig(sandboxOpts);
40
42
  const control = new ControlClient(config);
41
43
  const sandboxPayload = {
42
- template,
43
- timeout_seconds: Math.ceil((sandboxOpts.timeoutMs ?? 300_000) / 1000),
44
+ template_id: template,
45
+ timeout: Math.ceil((sandboxOpts.timeoutMs ?? 300_000) / 1000),
44
46
  metadata: sandboxOpts.metadata ?? {},
47
+ env_vars: sandboxOpts.envs ?? {},
48
+ secure: sandboxOpts.secure ?? true,
45
49
  allow_internet_access: sandboxOpts.allowInternetAccess ?? true,
46
50
  };
47
- putIfPresent(sandboxPayload, 'template_version_id', sandboxOpts.templateVersionId);
48
51
  putIfPresent(sandboxPayload, 'team', sandboxOpts.team);
49
- putIfPresent(sandboxPayload, 'cpu', sandboxOpts.cpu);
50
- putIfPresent(sandboxPayload, 'memory_mb', sandboxOpts.memoryMb);
51
- putIfPresent(sandboxPayload, 'network_class', sandboxOpts.networkClass);
52
- putIfPresent(sandboxPayload, 'allow_package_registry_access', sandboxOpts.allowPackageRegistryAccess);
53
- putIfPresent(sandboxPayload, 'exposed_ports', sandboxOpts.exposedPorts);
54
52
  const response = await control.post('/sandboxes', {
55
- json: { sandbox: sandboxPayload },
53
+ json: sandboxPayload,
56
54
  requestTimeoutMs: sessionOperationRequestTimeout(config, sandboxOpts),
57
55
  });
58
56
  const sandbox = record(response.sandbox ?? response);
@@ -74,7 +72,7 @@ export class Sandbox {
74
72
  const control = new ControlClient(config);
75
73
  const info = await control.get(`/sandboxes/${sandboxId}`);
76
74
  const response = await control.post(`/sandboxes/${sandboxId}/connect`, {
77
- json: { connect: opts.timeoutMs ? { timeout_seconds: Math.ceil(opts.timeoutMs / 1000) } : {} },
75
+ json: opts.timeoutMs ? { timeout: Math.ceil(opts.timeoutMs / 1000) } : {},
78
76
  requestTimeoutMs: sessionOperationRequestTimeout(config, opts),
79
77
  });
80
78
  return new Sandbox({
@@ -88,14 +86,14 @@ export class Sandbox {
88
86
  /** Refresh this sandbox's data-plane session in place. */
89
87
  async connect(opts = {}) {
90
88
  const response = await this.control.post(`/sandboxes/${this.sandboxId}/connect`, {
91
- json: { connect: opts.timeoutMs ? { timeout_seconds: Math.ceil(opts.timeoutMs / 1000) } : {} },
89
+ json: opts.timeoutMs ? { timeout: Math.ceil(opts.timeoutMs / 1000) } : {},
92
90
  requestTimeoutMs: sessionOperationRequestTimeout(this.config, opts),
93
91
  });
94
92
  this.sandbox = record(response.sandbox ?? this.sandbox);
95
93
  const dataPlane = dataPlaneFromSession(response.session, this.config);
96
94
  this.dataPlane = dataPlane;
97
95
  this.files = new Filesystem(dataPlane);
98
- this.commands = new Commands(dataPlane, this.config);
96
+ this.commands = new Commands(dataPlane, this.config, this.envs);
99
97
  return this;
100
98
  }
101
99
  /** Destroy a sandbox by id. */
@@ -109,17 +107,32 @@ export class Sandbox {
109
107
  await this.control.delete(`/sandboxes/${this.sandboxId}`);
110
108
  return true;
111
109
  }
110
+ /** Check if this sandbox is in a runtime-active lifecycle state. */
111
+ async isRunning(opts = {}) {
112
+ try {
113
+ const payload = await this.control.get(`/sandboxes/${this.sandboxId}`, {
114
+ requestTimeoutMs: opts.requestTimeoutMs,
115
+ });
116
+ const item = record(payload.sandbox ?? payload);
117
+ return ['creating', 'ready', 'checkpointing', 'restoring', 'stopping'].includes(String(item.state ?? ''));
118
+ }
119
+ catch (error) {
120
+ if (error instanceof NotFoundError)
121
+ return false;
122
+ throw error;
123
+ }
124
+ }
112
125
  /** Set a sandbox's lifetime by id. */
113
126
  static async setTimeout(sandboxId, timeoutMs, opts = {}) {
114
127
  const control = new ControlClient(new ConnectionConfig(opts));
115
- await control.patch(`/sandboxes/${sandboxId}`, {
116
- json: { sandbox: { timeout_seconds: Math.ceil(timeoutMs / 1000) } },
128
+ await control.post(`/sandboxes/${sandboxId}/timeout`, {
129
+ json: { timeout: Math.ceil(timeoutMs / 1000) },
117
130
  });
118
131
  }
119
132
  /** Set this sandbox's lifetime. */
120
133
  async setTimeout(timeoutMs) {
121
- await this.control.patch(`/sandboxes/${this.sandboxId}`, {
122
- json: { sandbox: { timeout_seconds: Math.ceil(timeoutMs / 1000) } },
134
+ await this.control.post(`/sandboxes/${this.sandboxId}/timeout`, {
135
+ json: { timeout: Math.ceil(timeoutMs / 1000) },
123
136
  });
124
137
  }
125
138
  /** Fetch control-plane metadata for a sandbox by id. */
@@ -141,13 +154,10 @@ export class Sandbox {
141
154
  return sandboxes.map((item) => sandboxInfo(record(item)));
142
155
  }
143
156
  /** Return the public hostname for an exposed sandbox port. */
144
- async getHost(port) {
145
- const payload = await this.control.get(`/sandboxes/${this.sandboxId}/ports/${port}`);
146
- const portInfo = record(payload.sandbox_port ?? payload.port ?? payload);
147
- const value = portInfo.host ?? portInfo.url;
148
- if (typeof value === 'string')
149
- return hostOnly(value);
150
- const routeToken = this.sandbox.route_token;
157
+ getHost(port) {
158
+ const routeToken = this.sandbox.route_token ??
159
+ this.sandbox.routeToken ??
160
+ routeTokenFromDataPlaneUrl(this.dataPlane.baseUrl, this.config.dataPlaneDomain);
151
161
  if (typeof routeToken !== 'string')
152
162
  throw new SandboxError('port response did not include host or url');
153
163
  return `p${port}-${routeToken}.sandbox.${this.config.dataPlaneDomain}`;
@@ -177,13 +187,22 @@ function sessionOperationRequestTimeout(config, opts) {
177
187
  function sandboxInfo(payload) {
178
188
  return {
179
189
  sandboxId: String(payload.id ?? payload.sandbox_id ?? ''),
180
- templateVersionId: typeof payload.template_version_id === 'number' ? payload.template_version_id : undefined,
190
+ templateId: typeof payload.template_id === 'string' ? payload.template_id : templateSlug(payload.template),
191
+ name: typeof payload.name === 'string' ? payload.name : undefined,
181
192
  state: typeof payload.state === 'string' ? payload.state : undefined,
182
193
  metadata: recordOfStrings(payload.metadata),
183
- startedAt: typeof payload.created_at === 'string' ? payload.created_at : undefined,
184
- endAt: typeof payload.deadline_at === 'string' ? payload.deadline_at : undefined,
194
+ startedAt: typeof payload.started_at === 'string'
195
+ ? payload.started_at
196
+ : typeof payload.created_at === 'string' ? payload.created_at : undefined,
197
+ endAt: typeof payload.end_at === 'string'
198
+ ? payload.end_at
199
+ : typeof payload.deadline_at === 'string' ? payload.deadline_at : undefined,
185
200
  };
186
201
  }
202
+ function templateSlug(value) {
203
+ const template = record(value);
204
+ return typeof template.slug === 'string' ? template.slug : undefined;
205
+ }
187
206
  function putIfPresent(target, key, value) {
188
207
  if (value !== undefined && value !== null)
189
208
  target[key] = value;
@@ -201,3 +220,11 @@ function hostOnly(value) {
201
220
  return new URL(value).host;
202
221
  return value.split('/')[0];
203
222
  }
223
+ function routeTokenFromDataPlaneUrl(value, dataPlaneDomain) {
224
+ const host = hostOnly(value);
225
+ const suffix = `.sandbox.${dataPlaneDomain}`;
226
+ if (!host.endsWith(suffix))
227
+ return undefined;
228
+ const token = host.slice(0, -suffix.length);
229
+ return token || undefined;
230
+ }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@watasu/sdk",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "license": "MIT OR Apache-2.0",
6
- "description": "TypeScript SDK for Watasu sandboxes",
6
+ "description": "TypeScript SDK for Watasu",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },