@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/dist/client.js CHANGED
@@ -1,21 +1,21 @@
1
- export function createBoxLiteClient(config) {
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 ?? 'default';
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
- // If we have a valid cached token, use it
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 response = await fetch(url, {
42
- ...options,
43
- headers: {
44
- 'Authorization': `Bearer ${bearerToken}`,
45
- 'Content-Type': 'application/json',
46
- ...options.headers,
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
- // 2. GET /executions/{id}/output SSE stream
149
- const response = await request(`/boxes/${boxId}/executions/${execution.execution_id}/output`, { headers: { 'Accept': 'text/event-stream' } }, true);
150
- if (!response.ok) {
151
- const body = await response.text();
152
- throw new Error(`BoxLite API error ${response.status}: ${body}`);
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
- const sseText = await response.text();
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
- // 2. GET /executions/{id}/output — return raw SSE stream
164
- const response = await request(`/boxes/${boxId}/executions/${execution.execution_id}/output`, { headers: { 'Accept': 'text/event-stream' } }, true);
165
- if (!response.ok) {
166
- const body = await response.text();
167
- throw new Error(`BoxLite API error ${response.status}: ${body}`);
168
- }
169
- if (!response.body) {
170
- throw new Error('BoxLite exec stream: no response body');
171
- }
172
- // Transform SSE events into decoded data chunks
173
- const decoder = new TextDecoder();
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
- flush(controller) {
195
- if (buffer) {
196
- const lines = buffer.split('\n');
197
- let currentEvent = '';
198
- for (const line of lines) {
199
- if (line.startsWith('event:')) {
200
- currentEvent = line.slice(6).trim();
201
- }
202
- else if (line.startsWith('data:')) {
203
- const data = line.slice(5).trim();
204
- if (currentEvent === 'stdout' || currentEvent === 'stderr') {
205
- const decoded = decodeSSEData(data);
206
- controller.enqueue(new TextEncoder().encode(decoded));
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.terminate();
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}/${prefix}/boxes/${boxId}/files?path=${encodeURIComponent(path)}`;
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}/${prefix}/boxes/${boxId}/files?path=${encodeURIComponent(path)}`;
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
@@ -1,3 +1,3 @@
1
1
  export { BoxLiteAdapter } from './adapter.js';
2
- export type { BoxLiteAdapterConfig } from './types.js';
2
+ export type { BoxLiteAdapterConfig, BoxLiteRemoteConfig, BoxLiteLocalConfig, BoxLiteClient, } from './types.js';
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -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,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA"}
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 bare-metal sandbox adapter
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"}