@sandbank.dev/boxlite 0.1.1 → 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 +66 -65
- package/dist/client.d.ts +6 -22
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +75 -128
- 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 +13 -4
package/dist/client.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
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
|
-
const prefix = config.prefix ?? '
|
|
7
|
+
const prefix = config.prefix ?? '';
|
|
4
8
|
const baseUrl = apiUrl.replace(/\/$/, '') + '/v1';
|
|
5
9
|
// --- Token management ---
|
|
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
|
-
|
|
15
|
+
if (!config.clientId || !config.clientSecret)
|
|
16
|
+
return '';
|
|
13
17
|
if (token && Date.now() < tokenExpiresAt)
|
|
14
18
|
return token;
|
|
15
|
-
// Acquire token via OAuth2 client credentials
|
|
16
|
-
if (!config.clientId || !config.clientSecret) {
|
|
17
|
-
throw new Error('BoxLite: either apiToken or clientId+clientSecret must be provided');
|
|
18
|
-
}
|
|
19
19
|
const response = await fetch(`${baseUrl}/oauth/tokens`, {
|
|
20
20
|
method: 'POST',
|
|
21
21
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
@@ -31,21 +31,20 @@ export function createBoxLiteClient(config) {
|
|
|
31
31
|
}
|
|
32
32
|
const data = await response.json();
|
|
33
33
|
token = data.access_token;
|
|
34
|
-
// Refresh 60s before expiry
|
|
35
34
|
tokenExpiresAt = Date.now() + (data.expires_in - 60) * 1000;
|
|
36
35
|
return token;
|
|
37
36
|
}
|
|
38
37
|
async function request(path, options = {}, rawResponse = false) {
|
|
39
38
|
const bearerToken = await ensureToken();
|
|
40
|
-
const url = `${baseUrl}/${prefix}${path}`;
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
headers
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
});
|
|
39
|
+
const url = prefix ? `${baseUrl}/${prefix}${path}` : `${baseUrl}${path}`;
|
|
40
|
+
const headers = {
|
|
41
|
+
'Content-Type': 'application/json',
|
|
42
|
+
...options.headers,
|
|
43
|
+
};
|
|
44
|
+
if (bearerToken) {
|
|
45
|
+
headers['Authorization'] = `Bearer ${bearerToken}`;
|
|
46
|
+
}
|
|
47
|
+
const response = await fetch(url, { ...options, headers });
|
|
49
48
|
if (rawResponse)
|
|
50
49
|
return response;
|
|
51
50
|
if (!response.ok) {
|
|
@@ -57,57 +56,7 @@ export function createBoxLiteClient(config) {
|
|
|
57
56
|
return {};
|
|
58
57
|
return JSON.parse(text);
|
|
59
58
|
}
|
|
60
|
-
/**
|
|
61
|
-
* Parse SSE data field — may be JSON `{"data":"<base64>"}` or raw base64.
|
|
62
|
-
*/
|
|
63
|
-
function decodeSSEData(raw) {
|
|
64
|
-
try {
|
|
65
|
-
const parsed = JSON.parse(raw);
|
|
66
|
-
if (parsed.data)
|
|
67
|
-
return atob(parsed.data);
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
// Fall through to raw base64
|
|
71
|
-
}
|
|
72
|
-
return atob(raw);
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Consume an SSE stream from BoxLite exec output.
|
|
76
|
-
* SSE events: stdout/stderr data is base64-encoded, exit event has exit_code.
|
|
77
|
-
*/
|
|
78
|
-
function parseSSE(text) {
|
|
79
|
-
let stdout = '';
|
|
80
|
-
let stderr = '';
|
|
81
|
-
let exitCode = 0;
|
|
82
|
-
const lines = text.split('\n');
|
|
83
|
-
let currentEvent = '';
|
|
84
|
-
for (const line of lines) {
|
|
85
|
-
if (line.startsWith('event:')) {
|
|
86
|
-
currentEvent = line.slice(6).trim();
|
|
87
|
-
}
|
|
88
|
-
else if (line.startsWith('data:')) {
|
|
89
|
-
const data = line.slice(5).trim();
|
|
90
|
-
if (currentEvent === 'stdout') {
|
|
91
|
-
stdout += decodeSSEData(data);
|
|
92
|
-
}
|
|
93
|
-
else if (currentEvent === 'stderr') {
|
|
94
|
-
stderr += decodeSSEData(data);
|
|
95
|
-
}
|
|
96
|
-
else if (currentEvent === 'exit') {
|
|
97
|
-
try {
|
|
98
|
-
const parsed = JSON.parse(data);
|
|
99
|
-
exitCode = parsed.exit_code;
|
|
100
|
-
}
|
|
101
|
-
catch {
|
|
102
|
-
exitCode = parseInt(data, 10) || 0;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return { stdout, stderr, exitCode };
|
|
108
|
-
}
|
|
109
59
|
return {
|
|
110
|
-
// --- Box lifecycle ---
|
|
111
60
|
async createBox(params) {
|
|
112
61
|
return request('/boxes', {
|
|
113
62
|
method: 'POST',
|
|
@@ -125,6 +74,8 @@ export function createBoxLiteClient(config) {
|
|
|
125
74
|
params.set('page_size', String(pageSize));
|
|
126
75
|
const qs = params.toString();
|
|
127
76
|
const data = await request(`/boxes${qs ? `?${qs}` : ''}`);
|
|
77
|
+
if (Array.isArray(data))
|
|
78
|
+
return data;
|
|
128
79
|
return data.boxes ?? [];
|
|
129
80
|
},
|
|
130
81
|
async deleteBox(boxId, force = false) {
|
|
@@ -138,84 +89,81 @@ export function createBoxLiteClient(config) {
|
|
|
138
89
|
async stopBox(boxId) {
|
|
139
90
|
await request(`/boxes/${boxId}/stop`, { method: 'POST' });
|
|
140
91
|
},
|
|
141
|
-
// --- Exec ---
|
|
142
92
|
async exec(boxId, req) {
|
|
143
|
-
// 1. POST /exec to start execution
|
|
144
93
|
const execution = await request(`/boxes/${boxId}/exec`, {
|
|
145
94
|
method: 'POST',
|
|
146
95
|
body: JSON.stringify(req),
|
|
147
96
|
});
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
+
};
|
|
103
|
+
}
|
|
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
|
+
}
|
|
153
118
|
}
|
|
154
|
-
|
|
155
|
-
return parseSSE(sseText);
|
|
119
|
+
throw new Error('BoxLite exec timed out waiting for completion');
|
|
156
120
|
},
|
|
157
121
|
async execStream(boxId, req) {
|
|
158
|
-
// 1. POST /exec to start execution
|
|
159
122
|
const execution = await request(`/boxes/${boxId}/exec`, {
|
|
160
123
|
method: 'POST',
|
|
161
124
|
body: JSON.stringify(req),
|
|
162
125
|
});
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
let buffer = '';
|
|
175
|
-
return response.body.pipeThrough(new TransformStream({
|
|
176
|
-
transform(chunk, controller) {
|
|
177
|
-
buffer += decoder.decode(chunk, { stream: true });
|
|
178
|
-
const lines = buffer.split('\n');
|
|
179
|
-
buffer = lines.pop() ?? '';
|
|
180
|
-
let currentEvent = '';
|
|
181
|
-
for (const line of lines) {
|
|
182
|
-
if (line.startsWith('event:')) {
|
|
183
|
-
currentEvent = line.slice(6).trim();
|
|
184
|
-
}
|
|
185
|
-
else if (line.startsWith('data:')) {
|
|
186
|
-
const data = line.slice(5).trim();
|
|
187
|
-
if (currentEvent === 'stdout' || currentEvent === 'stderr') {
|
|
188
|
-
const decoded = decodeSSEData(data);
|
|
189
|
-
controller.enqueue(new TextEncoder().encode(decoded));
|
|
190
|
-
}
|
|
191
|
-
}
|
|
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;
|
|
192
137
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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;
|
|
208
153
|
}
|
|
209
154
|
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
controller.error(err);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
210
159
|
}
|
|
211
|
-
controller.
|
|
160
|
+
controller.error(new Error('BoxLite exec stream timed out'));
|
|
212
161
|
},
|
|
213
|
-
})
|
|
162
|
+
});
|
|
214
163
|
},
|
|
215
|
-
// --- Files (native tar API) ---
|
|
216
164
|
async uploadFiles(boxId, path, tarData) {
|
|
217
165
|
const bearerToken = await ensureToken();
|
|
218
|
-
const url = `${baseUrl}
|
|
166
|
+
const url = `${baseUrl}${prefix ? `/${prefix}` : ''}/boxes/${boxId}/files?path=${encodeURIComponent(path)}`;
|
|
219
167
|
const response = await fetch(url, {
|
|
220
168
|
method: 'PUT',
|
|
221
169
|
headers: {
|
|
@@ -231,7 +179,7 @@ export function createBoxLiteClient(config) {
|
|
|
231
179
|
},
|
|
232
180
|
async downloadFiles(boxId, path) {
|
|
233
181
|
const bearerToken = await ensureToken();
|
|
234
|
-
const url = `${baseUrl}
|
|
182
|
+
const url = `${baseUrl}${prefix ? `/${prefix}` : ''}/boxes/${boxId}/files?path=${encodeURIComponent(path)}`;
|
|
235
183
|
const response = await fetch(url, {
|
|
236
184
|
headers: {
|
|
237
185
|
'Authorization': `Bearer ${bearerToken}`,
|
|
@@ -247,7 +195,6 @@ export function createBoxLiteClient(config) {
|
|
|
247
195
|
}
|
|
248
196
|
return response.body;
|
|
249
197
|
},
|
|
250
|
-
// --- Snapshots ---
|
|
251
198
|
async createSnapshot(boxId, name) {
|
|
252
199
|
return request(`/boxes/${boxId}/snapshots`, {
|
|
253
200
|
method: 'POST',
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAC7C,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAC7C,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,kBAAkB,EAClB,aAAa,GACd,MAAM,YAAY,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
// @sandbank.dev/boxlite — BoxLite
|
|
1
|
+
// @sandbank.dev/boxlite — BoxLite sandbox adapter (remote REST API + local Python SDK)
|
|
2
2
|
export { BoxLiteAdapter } from './adapter.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BoxLiteClient, BoxLiteLocalConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create a BoxLite local client that communicates with the boxlite Python SDK
|
|
4
|
+
* via a JSON-line subprocess bridge.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createBoxLiteLocalClient(config: BoxLiteLocalConfig): BoxLiteClient;
|
|
7
|
+
//# sourceMappingURL=local-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-client.d.ts","sourceRoot":"","sources":["../src/local-client.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAEV,aAAa,EAGb,kBAAkB,EAEnB,MAAM,YAAY,CAAA;AAwRnB;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,kBAAkB,GAAG,aAAa,CAgRlF"}
|