@watasu/sdk 0.1.0 → 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 +29 -0
- package/dist/connectionConfig.d.ts +1 -0
- package/dist/connectionConfig.js +4 -1
- package/dist/sandbox.d.ts +6 -9
- package/dist/sandbox.js +55 -29
- package/package.json +2 -2
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;
|
package/dist/connectionConfig.js
CHANGED
|
@@ -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 =
|
|
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,14 +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
|
-
diskMb?: number;
|
|
19
|
-
networkClass?: string;
|
|
20
|
-
allowPackageRegistryAccess?: boolean;
|
|
21
|
-
exposedPorts?: unknown[];
|
|
22
15
|
mcp?: unknown;
|
|
23
16
|
volumeMounts?: unknown;
|
|
24
17
|
}
|
|
@@ -28,7 +21,8 @@ export interface SandboxConnectOpts extends ConnectionOpts {
|
|
|
28
21
|
}
|
|
29
22
|
export interface SandboxInfo {
|
|
30
23
|
sandboxId: string;
|
|
31
|
-
|
|
24
|
+
templateId?: string;
|
|
25
|
+
name?: string;
|
|
32
26
|
state?: string;
|
|
33
27
|
metadata: Record<string, string>;
|
|
34
28
|
startedAt?: string;
|
|
@@ -49,6 +43,7 @@ export declare class Sandbox {
|
|
|
49
43
|
};
|
|
50
44
|
private readonly config;
|
|
51
45
|
private readonly control;
|
|
46
|
+
private readonly envs;
|
|
52
47
|
private dataPlane;
|
|
53
48
|
private sandbox;
|
|
54
49
|
constructor(opts: {
|
|
@@ -69,6 +64,8 @@ export declare class Sandbox {
|
|
|
69
64
|
static kill(sandboxId: string, opts?: ConnectionOpts): Promise<boolean>;
|
|
70
65
|
/** Destroy this sandbox. */
|
|
71
66
|
kill(): Promise<boolean>;
|
|
67
|
+
/** Check if this sandbox is in a runtime-active lifecycle state. */
|
|
68
|
+
isRunning(opts?: Pick<ConnectionOpts, 'requestTimeoutMs'>): Promise<boolean>;
|
|
72
69
|
/** Set a sandbox's lifetime by id. */
|
|
73
70
|
static setTimeout(sandboxId: string, timeoutMs: number, opts?: ConnectionOpts): Promise<void>;
|
|
74
71
|
/** Set this sandbox's lifetime. */
|
|
@@ -82,7 +79,7 @@ export declare class Sandbox {
|
|
|
82
79
|
team?: string;
|
|
83
80
|
}): Promise<SandboxInfo[]>;
|
|
84
81
|
/** Return the public hostname for an exposed sandbox port. */
|
|
85
|
-
getHost(port: number):
|
|
82
|
+
getHost(port: number): string;
|
|
86
83
|
pause(): never;
|
|
87
84
|
resume(): never;
|
|
88
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,
|
|
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,21 +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
|
-
|
|
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, 'disk_mb', sandboxOpts.diskMb);
|
|
52
|
-
putIfPresent(sandboxPayload, 'network_class', sandboxOpts.networkClass);
|
|
53
|
-
putIfPresent(sandboxPayload, 'allow_package_registry_access', sandboxOpts.allowPackageRegistryAccess);
|
|
54
|
-
putIfPresent(sandboxPayload, 'exposed_ports', sandboxOpts.exposedPorts);
|
|
55
52
|
const response = await control.post('/sandboxes', {
|
|
56
|
-
json:
|
|
53
|
+
json: sandboxPayload,
|
|
57
54
|
requestTimeoutMs: sessionOperationRequestTimeout(config, sandboxOpts),
|
|
58
55
|
});
|
|
59
56
|
const sandbox = record(response.sandbox ?? response);
|
|
@@ -75,7 +72,7 @@ export class Sandbox {
|
|
|
75
72
|
const control = new ControlClient(config);
|
|
76
73
|
const info = await control.get(`/sandboxes/${sandboxId}`);
|
|
77
74
|
const response = await control.post(`/sandboxes/${sandboxId}/connect`, {
|
|
78
|
-
json:
|
|
75
|
+
json: opts.timeoutMs ? { timeout: Math.ceil(opts.timeoutMs / 1000) } : {},
|
|
79
76
|
requestTimeoutMs: sessionOperationRequestTimeout(config, opts),
|
|
80
77
|
});
|
|
81
78
|
return new Sandbox({
|
|
@@ -89,14 +86,14 @@ export class Sandbox {
|
|
|
89
86
|
/** Refresh this sandbox's data-plane session in place. */
|
|
90
87
|
async connect(opts = {}) {
|
|
91
88
|
const response = await this.control.post(`/sandboxes/${this.sandboxId}/connect`, {
|
|
92
|
-
json:
|
|
89
|
+
json: opts.timeoutMs ? { timeout: Math.ceil(opts.timeoutMs / 1000) } : {},
|
|
93
90
|
requestTimeoutMs: sessionOperationRequestTimeout(this.config, opts),
|
|
94
91
|
});
|
|
95
92
|
this.sandbox = record(response.sandbox ?? this.sandbox);
|
|
96
93
|
const dataPlane = dataPlaneFromSession(response.session, this.config);
|
|
97
94
|
this.dataPlane = dataPlane;
|
|
98
95
|
this.files = new Filesystem(dataPlane);
|
|
99
|
-
this.commands = new Commands(dataPlane, this.config);
|
|
96
|
+
this.commands = new Commands(dataPlane, this.config, this.envs);
|
|
100
97
|
return this;
|
|
101
98
|
}
|
|
102
99
|
/** Destroy a sandbox by id. */
|
|
@@ -110,17 +107,32 @@ export class Sandbox {
|
|
|
110
107
|
await this.control.delete(`/sandboxes/${this.sandboxId}`);
|
|
111
108
|
return true;
|
|
112
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
|
+
}
|
|
113
125
|
/** Set a sandbox's lifetime by id. */
|
|
114
126
|
static async setTimeout(sandboxId, timeoutMs, opts = {}) {
|
|
115
127
|
const control = new ControlClient(new ConnectionConfig(opts));
|
|
116
|
-
await control.
|
|
117
|
-
json: {
|
|
128
|
+
await control.post(`/sandboxes/${sandboxId}/timeout`, {
|
|
129
|
+
json: { timeout: Math.ceil(timeoutMs / 1000) },
|
|
118
130
|
});
|
|
119
131
|
}
|
|
120
132
|
/** Set this sandbox's lifetime. */
|
|
121
133
|
async setTimeout(timeoutMs) {
|
|
122
|
-
await this.control.
|
|
123
|
-
json: {
|
|
134
|
+
await this.control.post(`/sandboxes/${this.sandboxId}/timeout`, {
|
|
135
|
+
json: { timeout: Math.ceil(timeoutMs / 1000) },
|
|
124
136
|
});
|
|
125
137
|
}
|
|
126
138
|
/** Fetch control-plane metadata for a sandbox by id. */
|
|
@@ -142,13 +154,10 @@ export class Sandbox {
|
|
|
142
154
|
return sandboxes.map((item) => sandboxInfo(record(item)));
|
|
143
155
|
}
|
|
144
156
|
/** Return the public hostname for an exposed sandbox port. */
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (typeof value === 'string')
|
|
150
|
-
return hostOnly(value);
|
|
151
|
-
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);
|
|
152
161
|
if (typeof routeToken !== 'string')
|
|
153
162
|
throw new SandboxError('port response did not include host or url');
|
|
154
163
|
return `p${port}-${routeToken}.sandbox.${this.config.dataPlaneDomain}`;
|
|
@@ -178,13 +187,22 @@ function sessionOperationRequestTimeout(config, opts) {
|
|
|
178
187
|
function sandboxInfo(payload) {
|
|
179
188
|
return {
|
|
180
189
|
sandboxId: String(payload.id ?? payload.sandbox_id ?? ''),
|
|
181
|
-
|
|
190
|
+
templateId: typeof payload.template_id === 'string' ? payload.template_id : templateSlug(payload.template),
|
|
191
|
+
name: typeof payload.name === 'string' ? payload.name : undefined,
|
|
182
192
|
state: typeof payload.state === 'string' ? payload.state : undefined,
|
|
183
193
|
metadata: recordOfStrings(payload.metadata),
|
|
184
|
-
startedAt: typeof payload.
|
|
185
|
-
|
|
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,
|
|
186
200
|
};
|
|
187
201
|
}
|
|
202
|
+
function templateSlug(value) {
|
|
203
|
+
const template = record(value);
|
|
204
|
+
return typeof template.slug === 'string' ? template.slug : undefined;
|
|
205
|
+
}
|
|
188
206
|
function putIfPresent(target, key, value) {
|
|
189
207
|
if (value !== undefined && value !== null)
|
|
190
208
|
target[key] = value;
|
|
@@ -202,3 +220,11 @@ function hostOnly(value) {
|
|
|
202
220
|
return new URL(value).host;
|
|
203
221
|
return value.split('/')[0];
|
|
204
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.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
|
-
"description": "TypeScript SDK for Watasu
|
|
6
|
+
"description": "TypeScript SDK for Watasu",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|