@sandbank.dev/boxlite 0.2.0 → 0.3.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.
- package/README.md +111 -0
- package/dist/adapter.d.ts +3 -1
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +60 -61
- package/dist/client.d.ts +6 -22
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +61 -113
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/local-client.d.ts +7 -0
- package/dist/local-client.d.ts.map +1 -0
- package/dist/local-client.js +509 -0
- package/dist/types.d.ts +60 -13
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/package.json +12 -3
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# @sandbank.dev/boxlite
|
|
2
|
+
|
|
3
|
+
> BoxLite bare-metal micro-VM sandbox adapter for [Sandbank](../../README.md).
|
|
4
|
+
|
|
5
|
+
BoxLite provides lightweight micro-VMs using libkrun (Hypervisor.framework on macOS, KVM on Linux). This adapter supports two modes of operation:
|
|
6
|
+
|
|
7
|
+
- **Remote mode** — Connect to a [BoxRun](https://github.com/nicholasgasior/boxlite) REST API server
|
|
8
|
+
- **Local mode** — Run VMs directly on the local machine via the boxlite Python SDK
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pnpm add @sandbank.dev/core @sandbank.dev/boxlite
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
For local mode, you also need the boxlite Python package:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install boxlite
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
### Remote mode (BoxRun REST API)
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { createProvider } from '@sandbank.dev/core'
|
|
28
|
+
import { BoxLiteAdapter } from '@sandbank.dev/boxlite'
|
|
29
|
+
|
|
30
|
+
const provider = createProvider(
|
|
31
|
+
new BoxLiteAdapter({
|
|
32
|
+
apiUrl: 'http://localhost:9090',
|
|
33
|
+
apiToken: process.env.BOXLITE_API_TOKEN,
|
|
34
|
+
prefix: 'default', // multi-tenant prefix (optional)
|
|
35
|
+
})
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const sandbox = await provider.create({
|
|
39
|
+
image: 'ubuntu:24.04',
|
|
40
|
+
resources: { cpu: 2, memory: 1024 },
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const { stdout } = await sandbox.exec('uname -a')
|
|
44
|
+
await provider.destroy(sandbox.id)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Local mode (Python SDK)
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { createProvider } from '@sandbank.dev/core'
|
|
51
|
+
import { BoxLiteAdapter } from '@sandbank.dev/boxlite'
|
|
52
|
+
|
|
53
|
+
const provider = createProvider(
|
|
54
|
+
new BoxLiteAdapter({
|
|
55
|
+
mode: 'local',
|
|
56
|
+
pythonPath: '/usr/bin/python3', // optional, defaults to 'python3'
|
|
57
|
+
boxliteHome: '~/.boxlite', // optional
|
|
58
|
+
})
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const sandbox = await provider.create({ image: 'ubuntu:24.04' })
|
|
62
|
+
const { stdout } = await sandbox.exec('echo hello')
|
|
63
|
+
await provider.destroy(sandbox.id)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### OAuth2 authentication (remote mode)
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
new BoxLiteAdapter({
|
|
70
|
+
apiUrl: 'http://boxrun.example.com:9090',
|
|
71
|
+
clientId: process.env.BOXLITE_CLIENT_ID,
|
|
72
|
+
clientSecret: process.env.BOXLITE_CLIENT_SECRET,
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Capabilities
|
|
77
|
+
|
|
78
|
+
| Capability | Remote | Local |
|
|
79
|
+
|------------|:------:|:-----:|
|
|
80
|
+
| `exec.stream` | ✅ | ✅ |
|
|
81
|
+
| `terminal` | ✅ | ✅ |
|
|
82
|
+
| `sleep` | ✅ | ✅ |
|
|
83
|
+
| `port.expose` | ✅ | ✅ |
|
|
84
|
+
| `snapshot` | ✅ | — |
|
|
85
|
+
|
|
86
|
+
## Characteristics
|
|
87
|
+
|
|
88
|
+
- **Runtime:** Micro-VM (libkrun)
|
|
89
|
+
- **Cold start:** ~3-5s
|
|
90
|
+
- **File I/O:** tar archive upload/download
|
|
91
|
+
- **Hypervisor:** Hypervisor.framework (macOS) / KVM (Linux)
|
|
92
|
+
- **Local dependency:** `boxlite` Python package (local mode only)
|
|
93
|
+
|
|
94
|
+
## Architecture
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
┌─────────────────────────────────────┐
|
|
98
|
+
│ BoxLiteAdapter │
|
|
99
|
+
│ mode: 'remote' | 'local' │
|
|
100
|
+
├──────────────┬──────────────────────┤
|
|
101
|
+
│ REST Client │ Local Client │
|
|
102
|
+
│ (fetch) │ (Python subprocess) │
|
|
103
|
+
├──────────────┼──────────────────────┤
|
|
104
|
+
│ BoxRun API │ boxlite Python SDK │
|
|
105
|
+
│ (HTTP/JSON) │ (JSON-line bridge) │
|
|
106
|
+
└──────────────┴──────────────────────┘
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT
|
package/dist/adapter.d.ts
CHANGED
|
@@ -5,12 +5,14 @@ export declare class BoxLiteAdapter implements SandboxAdapter {
|
|
|
5
5
|
readonly capabilities: ReadonlySet<Capability>;
|
|
6
6
|
private readonly client;
|
|
7
7
|
private readonly config;
|
|
8
|
-
|
|
8
|
+
private readonly host;
|
|
9
9
|
private readonly portMaps;
|
|
10
10
|
constructor(config: BoxLiteAdapterConfig);
|
|
11
11
|
createSandbox(config: CreateConfig): Promise<AdapterSandbox>;
|
|
12
12
|
getSandbox(id: string): Promise<AdapterSandbox>;
|
|
13
13
|
listSandboxes(filter?: ListFilter): Promise<SandboxInfo[]>;
|
|
14
14
|
destroySandbox(id: string): Promise<void>;
|
|
15
|
+
/** Dispose the adapter and clean up resources (e.g. Python bridge process) */
|
|
16
|
+
dispose(): Promise<void>;
|
|
15
17
|
}
|
|
16
18
|
//# sourceMappingURL=adapter.d.ts.map
|
package/dist/adapter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,UAAU,EACV,YAAY,EAGZ,UAAU,EACV,cAAc,EACd,WAAW,EAIZ,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,UAAU,EACV,YAAY,EAGZ,UAAU,EACV,cAAc,EACd,WAAW,EAIZ,MAAM,oBAAoB,CAAA;AAI3B,OAAO,KAAK,EAAE,oBAAoB,EAA6B,MAAM,YAAY,CAAA;AAoKjF,qBAAa,cAAe,YAAW,cAAc;IACnD,QAAQ,CAAC,IAAI,aAAY;IACzB,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,UAAU,CAAC,CAAA;IAE9C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsB;IAC7C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAQ;IAC7B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyC;gBAEtD,MAAM,EAAE,oBAAoB;IAalC,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC;IAuC5D,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAW/C,aAAa,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IA0B1D,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAU/C,8EAA8E;IACxE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAG/B"}
|
package/dist/adapter.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SandboxNotFoundError, ProviderError } from '@sandbank.dev/core';
|
|
2
|
-
import {
|
|
2
|
+
import { createBoxLiteRestClient } from './client.js';
|
|
3
|
+
import { createBoxLiteLocalClient } from './local-client.js';
|
|
3
4
|
/** Map BoxLite box status to Sandbank SandboxState */
|
|
4
5
|
function mapState(status) {
|
|
5
6
|
switch (status) {
|
|
@@ -15,26 +16,26 @@ function mapState(status) {
|
|
|
15
16
|
return 'error';
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
|
-
/**
|
|
19
|
-
function
|
|
19
|
+
/** Resolve the host used for port exposure and terminal URLs */
|
|
20
|
+
function resolveHost(config) {
|
|
21
|
+
if (config.mode === 'local')
|
|
22
|
+
return '127.0.0.1';
|
|
20
23
|
try {
|
|
21
|
-
|
|
22
|
-
return url.hostname;
|
|
24
|
+
return new URL(config.apiUrl).hostname;
|
|
23
25
|
}
|
|
24
26
|
catch {
|
|
25
|
-
return apiUrl;
|
|
27
|
+
return config.apiUrl;
|
|
26
28
|
}
|
|
27
29
|
}
|
|
28
30
|
/** Wrap a BoxLite box into an AdapterSandbox */
|
|
29
|
-
function wrapBox(box, client,
|
|
31
|
+
function wrapBox(box, client, host, portMappings) {
|
|
30
32
|
return {
|
|
31
|
-
get id() { return box.
|
|
33
|
+
get id() { return box.id; },
|
|
32
34
|
get state() { return mapState(box.status); },
|
|
33
35
|
get createdAt() { return box.created_at; },
|
|
34
36
|
async exec(command, options) {
|
|
35
|
-
const result = await client.exec(box.
|
|
36
|
-
|
|
37
|
-
args: ['-c', command],
|
|
37
|
+
const result = await client.exec(box.id, {
|
|
38
|
+
cmd: ['bash', '-c', command],
|
|
38
39
|
working_dir: options?.cwd,
|
|
39
40
|
timeout_seconds: options?.timeout ? Math.ceil(options.timeout / 1000) : undefined,
|
|
40
41
|
});
|
|
@@ -45,9 +46,8 @@ function wrapBox(box, client, config, portMappings) {
|
|
|
45
46
|
};
|
|
46
47
|
},
|
|
47
48
|
async execStream(command, options) {
|
|
48
|
-
return client.execStream(box.
|
|
49
|
-
|
|
50
|
-
args: ['-c', command],
|
|
49
|
+
return client.execStream(box.id, {
|
|
50
|
+
cmd: ['bash', '-c', command],
|
|
51
51
|
working_dir: options?.cwd,
|
|
52
52
|
timeout_seconds: options?.timeout ? Math.ceil(options.timeout / 1000) : undefined,
|
|
53
53
|
});
|
|
@@ -58,7 +58,6 @@ function wrapBox(box, client, config, portMappings) {
|
|
|
58
58
|
data = archive;
|
|
59
59
|
}
|
|
60
60
|
else {
|
|
61
|
-
// Collect ReadableStream into Uint8Array
|
|
62
61
|
const reader = archive.getReader();
|
|
63
62
|
const chunks = [];
|
|
64
63
|
while (true) {
|
|
@@ -75,40 +74,37 @@ function wrapBox(box, client, config, portMappings) {
|
|
|
75
74
|
offset += chunk.length;
|
|
76
75
|
}
|
|
77
76
|
}
|
|
78
|
-
await client.uploadFiles(box.
|
|
77
|
+
await client.uploadFiles(box.id, destDir ?? '/', data);
|
|
79
78
|
},
|
|
80
79
|
async downloadArchive(srcDir) {
|
|
81
|
-
return client.downloadFiles(box.
|
|
80
|
+
return client.downloadFiles(box.id, srcDir ?? '/');
|
|
82
81
|
},
|
|
83
82
|
async sleep() {
|
|
84
|
-
await client.stopBox(box.
|
|
83
|
+
await client.stopBox(box.id);
|
|
85
84
|
},
|
|
86
85
|
async wake() {
|
|
87
|
-
await client.startBox(box.
|
|
86
|
+
await client.startBox(box.id);
|
|
88
87
|
},
|
|
89
88
|
async createSnapshot(name) {
|
|
90
89
|
const snapshotName = name ?? `snap-${Date.now()}`;
|
|
91
|
-
await client.createSnapshot(box.
|
|
90
|
+
await client.createSnapshot(box.id, snapshotName);
|
|
92
91
|
return { snapshotId: snapshotName };
|
|
93
92
|
},
|
|
94
93
|
async restoreSnapshot(snapshotId) {
|
|
95
|
-
await client.restoreSnapshot(box.
|
|
94
|
+
await client.restoreSnapshot(box.id, snapshotId);
|
|
96
95
|
},
|
|
97
96
|
async exposePort(port) {
|
|
98
97
|
const hostPort = portMappings.get(port) ?? port;
|
|
99
|
-
const host = getApiHost(config.apiUrl);
|
|
100
98
|
return { url: `http://${host}:${hostPort}` };
|
|
101
99
|
},
|
|
102
100
|
async startTerminal(options) {
|
|
103
101
|
const port = 7681;
|
|
104
102
|
const shell = options?.shell ?? '/bin/bash';
|
|
105
103
|
const ttydBase = 'https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd';
|
|
106
|
-
|
|
107
|
-
const check = await client.exec(box.box_id, { command: 'which', args: ['ttyd'] });
|
|
104
|
+
const check = await client.exec(box.id, { cmd: ['which', 'ttyd'] });
|
|
108
105
|
if (check.exitCode !== 0) {
|
|
109
|
-
await client.exec(box.
|
|
110
|
-
|
|
111
|
-
args: ['-c',
|
|
106
|
+
await client.exec(box.id, {
|
|
107
|
+
cmd: ['bash', '-c',
|
|
112
108
|
`ARCH=$(uname -m); case "$ARCH" in aarch64|arm64) ARCH=aarch64;; x86_64) ARCH=x86_64;; *) echo "Unsupported arch: $ARCH" >&2; exit 1;; esac; `
|
|
113
109
|
+ `TTYD_URL="${ttydBase}.$ARCH"; `
|
|
114
110
|
+ `command -v curl > /dev/null && curl -sL "$TTYD_URL" -o /usr/local/bin/ttyd`
|
|
@@ -116,23 +112,17 @@ function wrapBox(box, client, config, portMappings) {
|
|
|
116
112
|
+ ` || { apt-get update -qq && apt-get install -y -qq wget > /dev/null && wget -qO /usr/local/bin/ttyd "$TTYD_URL"; }`,
|
|
117
113
|
],
|
|
118
114
|
});
|
|
119
|
-
await client.exec(box.
|
|
120
|
-
|
|
121
|
-
args: ['+x', '/usr/local/bin/ttyd'],
|
|
115
|
+
await client.exec(box.id, {
|
|
116
|
+
cmd: ['chmod', '+x', '/usr/local/bin/ttyd'],
|
|
122
117
|
});
|
|
123
118
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
command: 'bash',
|
|
127
|
-
args: ['-c', `nohup ttyd -W -p ${port} '${shell.replace(/'/g, "'\\''")}' > /dev/null 2>&1 &`],
|
|
119
|
+
await client.exec(box.id, {
|
|
120
|
+
cmd: ['bash', '-c', `nohup ttyd -W -p ${port} '${shell.replace(/'/g, "'\\''")}' > /dev/null 2>&1 &`],
|
|
128
121
|
});
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
command: 'bash',
|
|
132
|
-
args: ['-c', `for i in $(seq 1 20); do pgrep -x ttyd > /dev/null && break || sleep 0.5; done`],
|
|
122
|
+
await client.exec(box.id, {
|
|
123
|
+
cmd: ['bash', '-c', `for i in $(seq 1 20); do pgrep -x ttyd > /dev/null && break || sleep 0.5; done`],
|
|
133
124
|
});
|
|
134
125
|
const hostPort = portMappings.get(port) ?? port;
|
|
135
|
-
const host = getApiHost(config.apiUrl);
|
|
136
126
|
return {
|
|
137
127
|
url: `ws://${host}:${hostPort}/ws`,
|
|
138
128
|
port,
|
|
@@ -145,55 +135,60 @@ function isNotFound(err) {
|
|
|
145
135
|
const msg = err instanceof Error ? err.message : String(err);
|
|
146
136
|
return msg.includes('404') || msg.includes('not found') || msg.includes('Not Found');
|
|
147
137
|
}
|
|
138
|
+
/** Create the appropriate client based on config mode */
|
|
139
|
+
function createClient(config) {
|
|
140
|
+
if (config.mode === 'local') {
|
|
141
|
+
return createBoxLiteLocalClient(config);
|
|
142
|
+
}
|
|
143
|
+
return createBoxLiteRestClient(config);
|
|
144
|
+
}
|
|
148
145
|
export class BoxLiteAdapter {
|
|
149
146
|
name = 'boxlite';
|
|
150
|
-
capabilities
|
|
151
|
-
'exec.stream',
|
|
152
|
-
'terminal',
|
|
153
|
-
'sleep',
|
|
154
|
-
'snapshot',
|
|
155
|
-
'port.expose',
|
|
156
|
-
]);
|
|
147
|
+
capabilities;
|
|
157
148
|
client;
|
|
158
149
|
config;
|
|
159
|
-
|
|
150
|
+
host;
|
|
160
151
|
portMaps = new Map();
|
|
161
152
|
constructor(config) {
|
|
162
153
|
this.config = config;
|
|
163
|
-
this.
|
|
154
|
+
this.host = resolveHost(config);
|
|
155
|
+
this.client = createClient(config);
|
|
156
|
+
// Local mode: snapshots not supported yet
|
|
157
|
+
const caps = ['exec.stream', 'terminal', 'sleep', 'port.expose'];
|
|
158
|
+
if (config.mode !== 'local') {
|
|
159
|
+
caps.push('snapshot');
|
|
160
|
+
}
|
|
161
|
+
this.capabilities = new Set(caps);
|
|
164
162
|
}
|
|
165
163
|
async createSandbox(config) {
|
|
166
164
|
try {
|
|
167
165
|
const box = await this.client.createBox({
|
|
168
166
|
image: config.image,
|
|
169
|
-
|
|
170
|
-
|
|
167
|
+
cpu: config.resources?.cpu,
|
|
168
|
+
memory_mb: config.resources?.memory,
|
|
171
169
|
env: config.env,
|
|
172
170
|
auto_remove: false,
|
|
173
171
|
});
|
|
174
|
-
// Store port mappings if they were specified at creation
|
|
175
172
|
const portMap = new Map();
|
|
176
|
-
this.portMaps.set(box.
|
|
177
|
-
// Start the box if it was created in configured state
|
|
173
|
+
this.portMaps.set(box.id, portMap);
|
|
178
174
|
if (box.status === 'configured' || box.status === 'stopped') {
|
|
179
|
-
await this.client.startBox(box.
|
|
175
|
+
await this.client.startBox(box.id);
|
|
180
176
|
}
|
|
181
|
-
// Wait for box to be running (timeout is in seconds per CreateConfig docs)
|
|
182
177
|
const timeoutSec = config.timeout ?? 30;
|
|
183
178
|
const maxAttempts = Math.max(1, timeoutSec);
|
|
184
179
|
let current = box;
|
|
185
180
|
for (let i = 0; i < maxAttempts; i++) {
|
|
186
|
-
current = await this.client.getBox(box.
|
|
181
|
+
current = await this.client.getBox(box.id);
|
|
187
182
|
if (current.status === 'running')
|
|
188
183
|
break;
|
|
189
184
|
await new Promise(r => setTimeout(r, 1000));
|
|
190
185
|
}
|
|
191
186
|
if (current.status !== 'running') {
|
|
192
|
-
await this.client.deleteBox(box.
|
|
193
|
-
this.portMaps.delete(box.
|
|
187
|
+
await this.client.deleteBox(box.id, true).catch(() => { });
|
|
188
|
+
this.portMaps.delete(box.id);
|
|
194
189
|
throw new ProviderError('boxlite', new Error(`Sandbox failed to start within ${timeoutSec}s (status: ${current.status})`));
|
|
195
190
|
}
|
|
196
|
-
return wrapBox(current, this.client, this.
|
|
191
|
+
return wrapBox(current, this.client, this.host, portMap);
|
|
197
192
|
}
|
|
198
193
|
catch (err) {
|
|
199
194
|
if (err instanceof ProviderError)
|
|
@@ -205,7 +200,7 @@ export class BoxLiteAdapter {
|
|
|
205
200
|
try {
|
|
206
201
|
const box = await this.client.getBox(id);
|
|
207
202
|
const portMap = this.portMaps.get(id) ?? new Map();
|
|
208
|
-
return wrapBox(box, this.client, this.
|
|
203
|
+
return wrapBox(box, this.client, this.host, portMap);
|
|
209
204
|
}
|
|
210
205
|
catch (err) {
|
|
211
206
|
if (isNotFound(err))
|
|
@@ -217,7 +212,7 @@ export class BoxLiteAdapter {
|
|
|
217
212
|
try {
|
|
218
213
|
const boxes = await this.client.listBoxes();
|
|
219
214
|
let infos = boxes.map((b) => ({
|
|
220
|
-
id: b.
|
|
215
|
+
id: b.id,
|
|
221
216
|
state: mapState(b.status),
|
|
222
217
|
createdAt: b.created_at,
|
|
223
218
|
image: b.image,
|
|
@@ -246,4 +241,8 @@ export class BoxLiteAdapter {
|
|
|
246
241
|
throw new ProviderError('boxlite', err, id);
|
|
247
242
|
}
|
|
248
243
|
}
|
|
244
|
+
/** Dispose the adapter and clean up resources (e.g. Python bridge process) */
|
|
245
|
+
async dispose() {
|
|
246
|
+
await this.client.dispose?.();
|
|
247
|
+
}
|
|
249
248
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -1,23 +1,7 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
startBox(boxId: string): Promise<void>;
|
|
8
|
-
stopBox(boxId: string): Promise<void>;
|
|
9
|
-
exec(boxId: string, req: BoxLiteExecRequest): Promise<{
|
|
10
|
-
stdout: string;
|
|
11
|
-
stderr: string;
|
|
12
|
-
exitCode: number;
|
|
13
|
-
}>;
|
|
14
|
-
execStream(boxId: string, req: BoxLiteExecRequest): Promise<ReadableStream<Uint8Array>>;
|
|
15
|
-
uploadFiles(boxId: string, path: string, tarData: Uint8Array): Promise<void>;
|
|
16
|
-
downloadFiles(boxId: string, path: string): Promise<ReadableStream<Uint8Array>>;
|
|
17
|
-
createSnapshot(boxId: string, name: string): Promise<BoxLiteSnapshot>;
|
|
18
|
-
restoreSnapshot(boxId: string, name: string): Promise<void>;
|
|
19
|
-
listSnapshots(boxId: string): Promise<BoxLiteSnapshot[]>;
|
|
20
|
-
deleteSnapshot(boxId: string, name: string): Promise<void>;
|
|
21
|
-
};
|
|
22
|
-
export type BoxLiteClient = ReturnType<typeof createBoxLiteClient>;
|
|
1
|
+
import type { BoxLiteClient, BoxLiteRemoteConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create a BoxLite REST client for communicating with a BoxRun REST API.
|
|
4
|
+
* Used in remote mode.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createBoxLiteRestClient(config: BoxLiteRemoteConfig): BoxLiteClient;
|
|
23
7
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,aAAa,EAIb,mBAAmB,EAGpB,MAAM,YAAY,CAAA;AAEnB;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,mBAAmB,GAAG,aAAa,CAgQlF"}
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Create a BoxLite REST client for communicating with a BoxRun REST API.
|
|
3
|
+
* Used in remote mode.
|
|
4
|
+
*/
|
|
5
|
+
export function createBoxLiteRestClient(config) {
|
|
2
6
|
const { apiUrl } = config;
|
|
3
7
|
const prefix = config.prefix ?? '';
|
|
4
8
|
const baseUrl = apiUrl.replace(/\/$/, '') + '/v1';
|
|
@@ -6,13 +10,10 @@ export function createBoxLiteClient(config) {
|
|
|
6
10
|
let token = config.apiToken ?? '';
|
|
7
11
|
let tokenExpiresAt = 0;
|
|
8
12
|
async function ensureToken() {
|
|
9
|
-
// If a static token was provided, always use it
|
|
10
13
|
if (config.apiToken)
|
|
11
14
|
return config.apiToken;
|
|
12
|
-
// No auth configured — run without authentication
|
|
13
15
|
if (!config.clientId || !config.clientSecret)
|
|
14
16
|
return '';
|
|
15
|
-
// If we have a valid cached token, use it
|
|
16
17
|
if (token && Date.now() < tokenExpiresAt)
|
|
17
18
|
return token;
|
|
18
19
|
const response = await fetch(`${baseUrl}/oauth/tokens`, {
|
|
@@ -30,7 +31,6 @@ export function createBoxLiteClient(config) {
|
|
|
30
31
|
}
|
|
31
32
|
const data = await response.json();
|
|
32
33
|
token = data.access_token;
|
|
33
|
-
// Refresh 60s before expiry
|
|
34
34
|
tokenExpiresAt = Date.now() + (data.expires_in - 60) * 1000;
|
|
35
35
|
return token;
|
|
36
36
|
}
|
|
@@ -56,57 +56,7 @@ export function createBoxLiteClient(config) {
|
|
|
56
56
|
return {};
|
|
57
57
|
return JSON.parse(text);
|
|
58
58
|
}
|
|
59
|
-
/**
|
|
60
|
-
* Parse SSE data field — may be JSON `{"data":"<base64>"}` or raw base64.
|
|
61
|
-
*/
|
|
62
|
-
function decodeSSEData(raw) {
|
|
63
|
-
try {
|
|
64
|
-
const parsed = JSON.parse(raw);
|
|
65
|
-
if (parsed.data)
|
|
66
|
-
return atob(parsed.data);
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
// Fall through to raw base64
|
|
70
|
-
}
|
|
71
|
-
return atob(raw);
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Consume an SSE stream from BoxLite exec output.
|
|
75
|
-
* SSE events: stdout/stderr data is base64-encoded, exit event has exit_code.
|
|
76
|
-
*/
|
|
77
|
-
function parseSSE(text) {
|
|
78
|
-
let stdout = '';
|
|
79
|
-
let stderr = '';
|
|
80
|
-
let exitCode = 0;
|
|
81
|
-
const lines = text.split('\n');
|
|
82
|
-
let currentEvent = '';
|
|
83
|
-
for (const line of lines) {
|
|
84
|
-
if (line.startsWith('event:')) {
|
|
85
|
-
currentEvent = line.slice(6).trim();
|
|
86
|
-
}
|
|
87
|
-
else if (line.startsWith('data:')) {
|
|
88
|
-
const data = line.slice(5).trim();
|
|
89
|
-
if (currentEvent === 'stdout') {
|
|
90
|
-
stdout += decodeSSEData(data);
|
|
91
|
-
}
|
|
92
|
-
else if (currentEvent === 'stderr') {
|
|
93
|
-
stderr += decodeSSEData(data);
|
|
94
|
-
}
|
|
95
|
-
else if (currentEvent === 'exit') {
|
|
96
|
-
try {
|
|
97
|
-
const parsed = JSON.parse(data);
|
|
98
|
-
exitCode = parsed.exit_code;
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
exitCode = parseInt(data, 10) || 0;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return { stdout, stderr, exitCode };
|
|
107
|
-
}
|
|
108
59
|
return {
|
|
109
|
-
// --- Box lifecycle ---
|
|
110
60
|
async createBox(params) {
|
|
111
61
|
return request('/boxes', {
|
|
112
62
|
method: 'POST',
|
|
@@ -124,6 +74,8 @@ export function createBoxLiteClient(config) {
|
|
|
124
74
|
params.set('page_size', String(pageSize));
|
|
125
75
|
const qs = params.toString();
|
|
126
76
|
const data = await request(`/boxes${qs ? `?${qs}` : ''}`);
|
|
77
|
+
if (Array.isArray(data))
|
|
78
|
+
return data;
|
|
127
79
|
return data.boxes ?? [];
|
|
128
80
|
},
|
|
129
81
|
async deleteBox(boxId, force = false) {
|
|
@@ -137,81 +89,78 @@ export function createBoxLiteClient(config) {
|
|
|
137
89
|
async stopBox(boxId) {
|
|
138
90
|
await request(`/boxes/${boxId}/stop`, { method: 'POST' });
|
|
139
91
|
},
|
|
140
|
-
// --- Exec ---
|
|
141
92
|
async exec(boxId, req) {
|
|
142
|
-
// 1. POST /exec to start execution
|
|
143
93
|
const execution = await request(`/boxes/${boxId}/exec`, {
|
|
144
94
|
method: 'POST',
|
|
145
95
|
body: JSON.stringify(req),
|
|
146
96
|
});
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
97
|
+
if (execution.exit_code !== null && execution.exit_code !== undefined) {
|
|
98
|
+
return {
|
|
99
|
+
stdout: execution.stdout ?? '',
|
|
100
|
+
stderr: execution.stderr ?? '',
|
|
101
|
+
exitCode: execution.exit_code,
|
|
102
|
+
};
|
|
152
103
|
}
|
|
153
|
-
const
|
|
154
|
-
|
|
104
|
+
const timeoutMs = (req.timeout_seconds ?? 300) * 1000;
|
|
105
|
+
const startTime = Date.now();
|
|
106
|
+
let pollInterval = 100;
|
|
107
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
108
|
+
await new Promise(r => setTimeout(r, pollInterval));
|
|
109
|
+
pollInterval = Math.min(pollInterval * 2, 2000);
|
|
110
|
+
const result = await request(`/boxes/${boxId}/executions/${execution.id}`);
|
|
111
|
+
if (result.exit_code !== null && result.exit_code !== undefined) {
|
|
112
|
+
return {
|
|
113
|
+
stdout: result.stdout ?? '',
|
|
114
|
+
stderr: result.stderr ?? '',
|
|
115
|
+
exitCode: result.exit_code,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
throw new Error('BoxLite exec timed out waiting for completion');
|
|
155
120
|
},
|
|
156
121
|
async execStream(boxId, req) {
|
|
157
|
-
// 1. POST /exec to start execution
|
|
158
122
|
const execution = await request(`/boxes/${boxId}/exec`, {
|
|
159
123
|
method: 'POST',
|
|
160
124
|
body: JSON.stringify(req),
|
|
161
125
|
});
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
let buffer = '';
|
|
174
|
-
return response.body.pipeThrough(new TransformStream({
|
|
175
|
-
transform(chunk, controller) {
|
|
176
|
-
buffer += decoder.decode(chunk, { stream: true });
|
|
177
|
-
const lines = buffer.split('\n');
|
|
178
|
-
buffer = lines.pop() ?? '';
|
|
179
|
-
let currentEvent = '';
|
|
180
|
-
for (const line of lines) {
|
|
181
|
-
if (line.startsWith('event:')) {
|
|
182
|
-
currentEvent = line.slice(6).trim();
|
|
183
|
-
}
|
|
184
|
-
else if (line.startsWith('data:')) {
|
|
185
|
-
const data = line.slice(5).trim();
|
|
186
|
-
if (currentEvent === 'stdout' || currentEvent === 'stderr') {
|
|
187
|
-
const decoded = decodeSSEData(data);
|
|
188
|
-
controller.enqueue(new TextEncoder().encode(decoded));
|
|
189
|
-
}
|
|
190
|
-
}
|
|
126
|
+
const encoder = new TextEncoder();
|
|
127
|
+
const self = { request };
|
|
128
|
+
return new ReadableStream({
|
|
129
|
+
async start(controller) {
|
|
130
|
+
if (execution.exit_code !== null && execution.exit_code !== undefined) {
|
|
131
|
+
if (execution.stdout)
|
|
132
|
+
controller.enqueue(encoder.encode(execution.stdout));
|
|
133
|
+
if (execution.stderr)
|
|
134
|
+
controller.enqueue(encoder.encode(execution.stderr));
|
|
135
|
+
controller.close();
|
|
136
|
+
return;
|
|
191
137
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
138
|
+
const timeoutMs = (req.timeout_seconds ?? 300) * 1000;
|
|
139
|
+
const startTime = Date.now();
|
|
140
|
+
let pollInterval = 100;
|
|
141
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
142
|
+
await new Promise(r => setTimeout(r, pollInterval));
|
|
143
|
+
pollInterval = Math.min(pollInterval * 2, 2000);
|
|
144
|
+
try {
|
|
145
|
+
const result = await self.request(`/boxes/${boxId}/executions/${execution.id}`);
|
|
146
|
+
if (result.exit_code !== null && result.exit_code !== undefined) {
|
|
147
|
+
if (result.stdout)
|
|
148
|
+
controller.enqueue(encoder.encode(result.stdout));
|
|
149
|
+
if (result.stderr)
|
|
150
|
+
controller.enqueue(encoder.encode(result.stderr));
|
|
151
|
+
controller.close();
|
|
152
|
+
return;
|
|
207
153
|
}
|
|
208
154
|
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
controller.error(err);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
209
159
|
}
|
|
210
|
-
controller.
|
|
160
|
+
controller.error(new Error('BoxLite exec stream timed out'));
|
|
211
161
|
},
|
|
212
|
-
})
|
|
162
|
+
});
|
|
213
163
|
},
|
|
214
|
-
// --- Files (native tar API) ---
|
|
215
164
|
async uploadFiles(boxId, path, tarData) {
|
|
216
165
|
const bearerToken = await ensureToken();
|
|
217
166
|
const url = `${baseUrl}${prefix ? `/${prefix}` : ''}/boxes/${boxId}/files?path=${encodeURIComponent(path)}`;
|
|
@@ -246,7 +195,6 @@ export function createBoxLiteClient(config) {
|
|
|
246
195
|
}
|
|
247
196
|
return response.body;
|
|
248
197
|
},
|
|
249
|
-
// --- Snapshots ---
|
|
250
198
|
async createSnapshot(boxId, name) {
|
|
251
199
|
return request(`/boxes/${boxId}/snapshots`, {
|
|
252
200
|
method: 'POST',
|