@sandbank.dev/boxlite 0.1.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.
@@ -0,0 +1,16 @@
1
+ import type { AdapterSandbox, Capability, CreateConfig, ListFilter, SandboxAdapter, SandboxInfo } from '@sandbank.dev/core';
2
+ import type { BoxLiteAdapterConfig } from './types.js';
3
+ export declare class BoxLiteAdapter implements SandboxAdapter {
4
+ readonly name = "boxlite";
5
+ readonly capabilities: ReadonlySet<Capability>;
6
+ private readonly client;
7
+ private readonly config;
8
+ /** Track port mappings per box: boxId → Map<guestPort, hostPort> */
9
+ private readonly portMaps;
10
+ constructor(config: BoxLiteAdapterConfig);
11
+ createSandbox(config: CreateConfig): Promise<AdapterSandbox>;
12
+ getSandbox(id: string): Promise<AdapterSandbox>;
13
+ listSandboxes(filter?: ListFilter): Promise<SandboxInfo[]>;
14
+ destroySandbox(id: string): Promise<void>;
15
+ }
16
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +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;AAG3B,OAAO,KAAK,EAAE,oBAAoB,EAAc,MAAM,YAAY,CAAA;AAsKlE,qBAAa,cAAe,YAAW,cAAc;IACnD,QAAQ,CAAC,IAAI,aAAY;IACzB,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,UAAU,CAAC,CAM5C;IAEF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsB;IAC7C,oEAAoE;IACpE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyC;gBAEtD,MAAM,EAAE,oBAAoB;IAKlC,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC;IA0C5D,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;CAShD"}
@@ -0,0 +1,247 @@
1
+ import { SandboxNotFoundError, ProviderError } from '@sandbank.dev/core';
2
+ import { createBoxLiteClient } from './client.js';
3
+ /** Map BoxLite box status to Sandbank SandboxState */
4
+ function mapState(status) {
5
+ switch (status) {
6
+ case 'configured':
7
+ return 'creating';
8
+ case 'running':
9
+ return 'running';
10
+ case 'stopping':
11
+ case 'stopped':
12
+ case 'paused':
13
+ return 'stopped';
14
+ default:
15
+ return 'error';
16
+ }
17
+ }
18
+ /** Extract host from API URL for port exposure */
19
+ function getApiHost(apiUrl) {
20
+ try {
21
+ const url = new URL(apiUrl);
22
+ return url.hostname;
23
+ }
24
+ catch {
25
+ return apiUrl;
26
+ }
27
+ }
28
+ /** Wrap a BoxLite box into an AdapterSandbox */
29
+ function wrapBox(box, client, config, portMappings) {
30
+ return {
31
+ get id() { return box.box_id; },
32
+ get state() { return mapState(box.status); },
33
+ get createdAt() { return box.created_at; },
34
+ async exec(command, options) {
35
+ const result = await client.exec(box.box_id, {
36
+ command: 'bash',
37
+ args: ['-c', command],
38
+ working_dir: options?.cwd,
39
+ timeout_seconds: options?.timeout ? Math.ceil(options.timeout / 1000) : undefined,
40
+ });
41
+ return {
42
+ exitCode: result.exitCode,
43
+ stdout: result.stdout,
44
+ stderr: result.stderr,
45
+ };
46
+ },
47
+ async execStream(command, options) {
48
+ return client.execStream(box.box_id, {
49
+ command: 'bash',
50
+ args: ['-c', command],
51
+ working_dir: options?.cwd,
52
+ timeout_seconds: options?.timeout ? Math.ceil(options.timeout / 1000) : undefined,
53
+ });
54
+ },
55
+ async uploadArchive(archive, destDir) {
56
+ let data;
57
+ if (archive instanceof Uint8Array) {
58
+ data = archive;
59
+ }
60
+ else {
61
+ // Collect ReadableStream into Uint8Array
62
+ const reader = archive.getReader();
63
+ const chunks = [];
64
+ while (true) {
65
+ const { done, value } = await reader.read();
66
+ if (done)
67
+ break;
68
+ chunks.push(value);
69
+ }
70
+ const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
71
+ data = new Uint8Array(totalLength);
72
+ let offset = 0;
73
+ for (const chunk of chunks) {
74
+ data.set(chunk, offset);
75
+ offset += chunk.length;
76
+ }
77
+ }
78
+ await client.uploadFiles(box.box_id, destDir ?? '/', data);
79
+ },
80
+ async downloadArchive(srcDir) {
81
+ return client.downloadFiles(box.box_id, srcDir ?? '/');
82
+ },
83
+ async sleep() {
84
+ await client.stopBox(box.box_id);
85
+ },
86
+ async wake() {
87
+ await client.startBox(box.box_id);
88
+ },
89
+ async createSnapshot(name) {
90
+ const snapshotName = name ?? `snap-${Date.now()}`;
91
+ await client.createSnapshot(box.box_id, snapshotName);
92
+ return { snapshotId: snapshotName };
93
+ },
94
+ async restoreSnapshot(snapshotId) {
95
+ await client.restoreSnapshot(box.box_id, snapshotId);
96
+ },
97
+ async exposePort(port) {
98
+ const hostPort = portMappings.get(port) ?? port;
99
+ const host = getApiHost(config.apiUrl);
100
+ return { url: `http://${host}:${hostPort}` };
101
+ },
102
+ async startTerminal(options) {
103
+ const port = 7681;
104
+ const shell = options?.shell ?? '/bin/bash';
105
+ const ttydUrl = 'https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.x86_64';
106
+ // 1. Ensure ttyd is available
107
+ const check = await client.exec(box.box_id, { command: 'which', args: ['ttyd'] });
108
+ if (check.exitCode !== 0) {
109
+ await client.exec(box.box_id, {
110
+ command: 'bash',
111
+ args: ['-c',
112
+ `command -v curl > /dev/null && curl -sL ${ttydUrl} -o /usr/local/bin/ttyd`
113
+ + ` || { command -v wget > /dev/null && wget -qO /usr/local/bin/ttyd ${ttydUrl}; }`
114
+ + ` || { apt-get update -qq && apt-get install -y -qq wget > /dev/null && wget -qO /usr/local/bin/ttyd ${ttydUrl}; }`,
115
+ ],
116
+ });
117
+ await client.exec(box.box_id, {
118
+ command: 'chmod',
119
+ args: ['+x', '/usr/local/bin/ttyd'],
120
+ });
121
+ }
122
+ // 2. Start ttyd in background (-W enables write)
123
+ await client.exec(box.box_id, {
124
+ command: 'bash',
125
+ args: ['-c', `nohup ttyd -W -p ${port} '${shell.replace(/'/g, "'\\''")}' > /dev/null 2>&1 &`],
126
+ });
127
+ // 3. Wait for ttyd to be ready
128
+ await client.exec(box.box_id, {
129
+ command: 'bash',
130
+ args: ['-c', `for i in $(seq 1 20); do pgrep -x ttyd > /dev/null && break || sleep 0.5; done`],
131
+ });
132
+ const hostPort = portMappings.get(port) ?? port;
133
+ const host = getApiHost(config.apiUrl);
134
+ return {
135
+ url: `ws://${host}:${hostPort}/ws`,
136
+ port,
137
+ };
138
+ },
139
+ };
140
+ }
141
+ /** Check if an error is a 404 "not found" */
142
+ function isNotFound(err) {
143
+ const msg = err instanceof Error ? err.message : String(err);
144
+ return msg.includes('404') || msg.includes('not found') || msg.includes('Not Found');
145
+ }
146
+ export class BoxLiteAdapter {
147
+ name = 'boxlite';
148
+ capabilities = new Set([
149
+ 'exec.stream',
150
+ 'terminal',
151
+ 'sleep',
152
+ 'snapshot',
153
+ 'port.expose',
154
+ ]);
155
+ client;
156
+ config;
157
+ /** Track port mappings per box: boxId → Map<guestPort, hostPort> */
158
+ portMaps = new Map();
159
+ constructor(config) {
160
+ this.config = config;
161
+ this.client = createBoxLiteClient(config);
162
+ }
163
+ async createSandbox(config) {
164
+ try {
165
+ const box = await this.client.createBox({
166
+ image: config.image,
167
+ cpus: config.resources?.cpu,
168
+ memory_mib: config.resources?.memory,
169
+ env: config.env,
170
+ auto_remove: false,
171
+ });
172
+ // Store port mappings if they were specified at creation
173
+ const portMap = new Map();
174
+ this.portMaps.set(box.box_id, portMap);
175
+ // Start the box if it was created in configured state
176
+ if (box.status === 'configured' || box.status === 'stopped') {
177
+ await this.client.startBox(box.box_id);
178
+ }
179
+ // Wait for box to be running (timeout is in seconds per CreateConfig docs)
180
+ const timeoutSec = config.timeout ?? 30;
181
+ const maxAttempts = Math.max(1, timeoutSec);
182
+ let current = box;
183
+ for (let i = 0; i < maxAttempts; i++) {
184
+ current = await this.client.getBox(box.box_id);
185
+ if (current.status === 'running')
186
+ break;
187
+ await new Promise(r => setTimeout(r, 1000));
188
+ }
189
+ if (current.status !== 'running') {
190
+ await this.client.deleteBox(box.box_id, true).catch(() => { });
191
+ this.portMaps.delete(box.box_id);
192
+ throw new ProviderError('boxlite', new Error(`Sandbox failed to start within ${timeoutSec}s (status: ${current.status})`));
193
+ }
194
+ return wrapBox(current, this.client, this.config, portMap);
195
+ }
196
+ catch (err) {
197
+ if (err instanceof ProviderError)
198
+ throw err;
199
+ throw new ProviderError('boxlite', err);
200
+ }
201
+ }
202
+ async getSandbox(id) {
203
+ try {
204
+ const box = await this.client.getBox(id);
205
+ const portMap = this.portMaps.get(id) ?? new Map();
206
+ return wrapBox(box, this.client, this.config, portMap);
207
+ }
208
+ catch (err) {
209
+ if (isNotFound(err))
210
+ throw new SandboxNotFoundError('boxlite', id);
211
+ throw new ProviderError('boxlite', err, id);
212
+ }
213
+ }
214
+ async listSandboxes(filter) {
215
+ try {
216
+ const boxes = await this.client.listBoxes();
217
+ let infos = boxes.map((b) => ({
218
+ id: b.box_id,
219
+ state: mapState(b.status),
220
+ createdAt: b.created_at,
221
+ image: b.image,
222
+ }));
223
+ if (filter?.state) {
224
+ const states = Array.isArray(filter.state) ? filter.state : [filter.state];
225
+ infos = infos.filter(s => states.includes(s.state));
226
+ }
227
+ if (filter?.limit) {
228
+ infos = infos.slice(0, filter.limit);
229
+ }
230
+ return infos;
231
+ }
232
+ catch (err) {
233
+ throw new ProviderError('boxlite', err);
234
+ }
235
+ }
236
+ async destroySandbox(id) {
237
+ try {
238
+ await this.client.deleteBox(id, true);
239
+ this.portMaps.delete(id);
240
+ }
241
+ catch (err) {
242
+ if (isNotFound(err))
243
+ return;
244
+ throw new ProviderError('boxlite', err, id);
245
+ }
246
+ }
247
+ }
@@ -0,0 +1,23 @@
1
+ import type { BoxLiteAdapterConfig, BoxLiteBox, BoxLiteCreateParams, BoxLiteExecRequest, BoxLiteSnapshot } from './types.js';
2
+ export declare function createBoxLiteClient(config: BoxLiteAdapterConfig): {
3
+ createBox(params: BoxLiteCreateParams): Promise<BoxLiteBox>;
4
+ getBox(boxId: string): Promise<BoxLiteBox>;
5
+ listBoxes(status?: string, pageSize?: number): Promise<BoxLiteBox[]>;
6
+ deleteBox(boxId: string, force?: boolean): Promise<void>;
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>;
23
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,oBAAoB,EACpB,UAAU,EACV,mBAAmB,EACnB,kBAAkB,EAElB,eAAe,EAEhB,MAAM,YAAY,CAAA;AAEnB,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,oBAAoB;sBA4HpC,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC;kBAO7C,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;uBAIvB,MAAM,aAAa,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;qBASnD,MAAM,oBAAkB,OAAO,CAAC,IAAI,CAAC;oBAMtC,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;mBAIvB,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;gBAOlC,MAAM,OACR,kBAAkB,GACtB,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;sBAwBvD,MAAM,OACR,kBAAkB,GACtB,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;uBAuEb,MAAM,QAAQ,MAAM,WAAW,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;yBAkBvD,MAAM,QAAQ,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;0BAwBzD,MAAM,QAAQ,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;2BAO9C,MAAM,QAAQ,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;yBAMtC,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;0BAIlC,MAAM,QAAQ,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;EAMnE;AAED,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAA"}
package/dist/client.js ADDED
@@ -0,0 +1,271 @@
1
+ export function createBoxLiteClient(config) {
2
+ const { apiUrl } = config;
3
+ const prefix = config.prefix ?? 'default';
4
+ const baseUrl = apiUrl.replace(/\/$/, '') + '/v1';
5
+ // --- Token management ---
6
+ let token = config.apiToken ?? '';
7
+ let tokenExpiresAt = 0;
8
+ async function ensureToken() {
9
+ // If a static token was provided, always use it
10
+ if (config.apiToken)
11
+ return config.apiToken;
12
+ // If we have a valid cached token, use it
13
+ if (token && Date.now() < tokenExpiresAt)
14
+ 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
+ const response = await fetch(`${baseUrl}/oauth/tokens`, {
20
+ method: 'POST',
21
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
22
+ body: new URLSearchParams({
23
+ grant_type: 'client_credentials',
24
+ client_id: config.clientId,
25
+ client_secret: config.clientSecret,
26
+ }),
27
+ });
28
+ if (!response.ok) {
29
+ const body = await response.text();
30
+ throw new Error(`BoxLite OAuth2 error ${response.status}: ${body}`);
31
+ }
32
+ const data = await response.json();
33
+ token = data.access_token;
34
+ // Refresh 60s before expiry
35
+ tokenExpiresAt = Date.now() + (data.expires_in - 60) * 1000;
36
+ return token;
37
+ }
38
+ async function request(path, options = {}, rawResponse = false) {
39
+ 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
+ });
49
+ if (rawResponse)
50
+ return response;
51
+ if (!response.ok) {
52
+ const body = await response.text();
53
+ throw new Error(`BoxLite API error ${response.status}: ${body}`);
54
+ }
55
+ const text = await response.text();
56
+ if (!text)
57
+ return {};
58
+ return JSON.parse(text);
59
+ }
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
+ return {
110
+ // --- Box lifecycle ---
111
+ async createBox(params) {
112
+ return request('/boxes', {
113
+ method: 'POST',
114
+ body: JSON.stringify(params),
115
+ });
116
+ },
117
+ async getBox(boxId) {
118
+ return request(`/boxes/${boxId}`);
119
+ },
120
+ async listBoxes(status, pageSize) {
121
+ const params = new URLSearchParams();
122
+ if (status)
123
+ params.set('status', status);
124
+ if (pageSize)
125
+ params.set('page_size', String(pageSize));
126
+ const qs = params.toString();
127
+ const data = await request(`/boxes${qs ? `?${qs}` : ''}`);
128
+ return data.boxes ?? [];
129
+ },
130
+ async deleteBox(boxId, force = false) {
131
+ await request(`/boxes/${boxId}${force ? '?force=true' : ''}`, {
132
+ method: 'DELETE',
133
+ });
134
+ },
135
+ async startBox(boxId) {
136
+ await request(`/boxes/${boxId}/start`, { method: 'POST' });
137
+ },
138
+ async stopBox(boxId) {
139
+ await request(`/boxes/${boxId}/stop`, { method: 'POST' });
140
+ },
141
+ // --- Exec ---
142
+ async exec(boxId, req) {
143
+ // 1. POST /exec to start execution
144
+ const execution = await request(`/boxes/${boxId}/exec`, {
145
+ method: 'POST',
146
+ body: JSON.stringify(req),
147
+ });
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}`);
153
+ }
154
+ const sseText = await response.text();
155
+ return parseSSE(sseText);
156
+ },
157
+ async execStream(boxId, req) {
158
+ // 1. POST /exec to start execution
159
+ const execution = await request(`/boxes/${boxId}/exec`, {
160
+ method: 'POST',
161
+ body: JSON.stringify(req),
162
+ });
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
+ }
192
+ }
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
+ }
208
+ }
209
+ }
210
+ }
211
+ controller.terminate();
212
+ },
213
+ }));
214
+ },
215
+ // --- Files (native tar API) ---
216
+ async uploadFiles(boxId, path, tarData) {
217
+ const bearerToken = await ensureToken();
218
+ const url = `${baseUrl}/${prefix}/boxes/${boxId}/files?path=${encodeURIComponent(path)}`;
219
+ const response = await fetch(url, {
220
+ method: 'PUT',
221
+ headers: {
222
+ 'Authorization': `Bearer ${bearerToken}`,
223
+ 'Content-Type': 'application/x-tar',
224
+ },
225
+ body: tarData.buffer.slice(tarData.byteOffset, tarData.byteOffset + tarData.byteLength),
226
+ });
227
+ if (!response.ok) {
228
+ const body = await response.text();
229
+ throw new Error(`BoxLite API error ${response.status}: ${body}`);
230
+ }
231
+ },
232
+ async downloadFiles(boxId, path) {
233
+ const bearerToken = await ensureToken();
234
+ const url = `${baseUrl}/${prefix}/boxes/${boxId}/files?path=${encodeURIComponent(path)}`;
235
+ const response = await fetch(url, {
236
+ headers: {
237
+ 'Authorization': `Bearer ${bearerToken}`,
238
+ 'Accept': 'application/x-tar',
239
+ },
240
+ });
241
+ if (!response.ok) {
242
+ const body = await response.text();
243
+ throw new Error(`BoxLite API error ${response.status}: ${body}`);
244
+ }
245
+ if (!response.body) {
246
+ throw new Error('BoxLite download: no response body');
247
+ }
248
+ return response.body;
249
+ },
250
+ // --- Snapshots ---
251
+ async createSnapshot(boxId, name) {
252
+ return request(`/boxes/${boxId}/snapshots`, {
253
+ method: 'POST',
254
+ body: JSON.stringify({ name }),
255
+ });
256
+ },
257
+ async restoreSnapshot(boxId, name) {
258
+ await request(`/boxes/${boxId}/snapshots/${encodeURIComponent(name)}/restore`, {
259
+ method: 'POST',
260
+ });
261
+ },
262
+ async listSnapshots(boxId) {
263
+ return request(`/boxes/${boxId}/snapshots`);
264
+ },
265
+ async deleteSnapshot(boxId, name) {
266
+ await request(`/boxes/${boxId}/snapshots/${encodeURIComponent(name)}`, {
267
+ method: 'DELETE',
268
+ });
269
+ },
270
+ };
271
+ }
@@ -0,0 +1,3 @@
1
+ export { BoxLiteAdapter } from './adapter.js';
2
+ export type { BoxLiteAdapterConfig } from './types.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ // @sandbank.dev/boxlite — BoxLite bare-metal sandbox adapter
2
+ export { BoxLiteAdapter } from './adapter.js';
@@ -0,0 +1,61 @@
1
+ /** BoxLite adapter configuration */
2
+ export interface BoxLiteAdapterConfig {
3
+ /** BoxLite API base URL, e.g. 'http://localhost:8080' */
4
+ apiUrl: string;
5
+ /** Multi-tenant prefix, defaults to 'default' */
6
+ prefix?: string;
7
+ /** Bearer token (if already obtained) */
8
+ apiToken?: string;
9
+ /** OAuth2 client ID (for automatic token acquisition) */
10
+ clientId?: string;
11
+ /** OAuth2 client secret (for automatic token acquisition) */
12
+ clientSecret?: string;
13
+ }
14
+ export interface BoxLiteBox {
15
+ box_id: string;
16
+ name: string | null;
17
+ status: BoxStatus;
18
+ created_at: string;
19
+ updated_at?: string;
20
+ image: string;
21
+ cpus: number;
22
+ memory_mib: number;
23
+ }
24
+ export type BoxStatus = 'configured' | 'running' | 'stopping' | 'stopped' | 'paused' | 'unknown';
25
+ export interface BoxLiteExecRequest {
26
+ command: string;
27
+ args?: string[];
28
+ env?: Record<string, string>;
29
+ timeout_seconds?: number;
30
+ working_dir?: string;
31
+ tty?: boolean;
32
+ }
33
+ export interface BoxLiteExecution {
34
+ execution_id: string;
35
+ }
36
+ export interface BoxLiteSnapshot {
37
+ id: string;
38
+ box_id: string;
39
+ name: string;
40
+ created_at: number;
41
+ size_bytes: number;
42
+ guest_disk_bytes?: number;
43
+ container_disk_bytes?: number;
44
+ }
45
+ export interface BoxLiteCreateParams {
46
+ image: string;
47
+ name?: string;
48
+ cpus?: number;
49
+ memory_mib?: number;
50
+ disk_size_gb?: number;
51
+ working_dir?: string;
52
+ env?: Record<string, string>;
53
+ auto_remove?: boolean;
54
+ security?: string;
55
+ }
56
+ export interface BoxLiteTokenResponse {
57
+ access_token: string;
58
+ token_type: string;
59
+ expires_in: number;
60
+ }
61
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,oCAAoC;AACpC,MAAM,WAAW,oBAAoB;IACnC,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAA;IACd,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,yDAAyD;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6DAA6D;IAC7D,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAID,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,MAAM,EAAE,SAAS,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,MAAM,SAAS,GACjB,YAAY,GACZ,SAAS,GACT,UAAU,GACV,SAAS,GACT,QAAQ,GACR,SAAS,CAAA;AAEb,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,GAAG,CAAC,EAAE,OAAO,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,oBAAoB,CAAC,EAAE,MAAM,CAAA;CAC9B;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;CACnB"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@sandbank.dev/boxlite",
3
+ "version": "0.1.0",
4
+ "description": "BoxLite bare-metal sandbox adapter for Sandbank",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "homepage": "https://sandbank.dev",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/chekusu/sandbank.git",
11
+ "directory": "packages/boxlite"
12
+ },
13
+ "keywords": [
14
+ "sandbox",
15
+ "ai-agent",
16
+ "boxlite",
17
+ "bare-metal",
18
+ "kvm"
19
+ ],
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "dependencies": {
30
+ "@sandbank.dev/core": "0.1.0"
31
+ },
32
+ "devDependencies": {
33
+ "typescript": "^5.7.3"
34
+ },
35
+ "scripts": {
36
+ "build": "tsc",
37
+ "typecheck": "tsc --noEmit",
38
+ "clean": "rm -rf dist"
39
+ }
40
+ }