@banata-boxes/sdk 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.
package/dist/index.js ADDED
@@ -0,0 +1,476 @@
1
+ // src/index.ts
2
+ class BanataError extends Error {
3
+ status;
4
+ code;
5
+ requiredPlan;
6
+ currentPlan;
7
+ constructor(message, status, details) {
8
+ super(message);
9
+ this.name = "BanataError";
10
+ this.status = status;
11
+ this.code = details?.code;
12
+ this.requiredPlan = details?.requiredPlan;
13
+ this.currentPlan = details?.currentPlan;
14
+ }
15
+ }
16
+ var BrowserServiceError = BanataError;
17
+ function isRetryableStatus(status) {
18
+ return status >= 500 || status === 429;
19
+ }
20
+ function sleep(ms) {
21
+ return new Promise((r) => setTimeout(r, ms));
22
+ }
23
+
24
+ class BrowserCloud {
25
+ apiKey;
26
+ baseUrl;
27
+ retryConfig;
28
+ constructor(config) {
29
+ if (!config.apiKey)
30
+ throw new Error("API key is required");
31
+ this.apiKey = config.apiKey;
32
+ this.baseUrl = config.baseUrl ?? "https://api.banata.dev";
33
+ this.retryConfig = {
34
+ maxRetries: config.retry?.maxRetries ?? 3,
35
+ baseDelayMs: config.retry?.baseDelayMs ?? 500,
36
+ maxDelayMs: config.retry?.maxDelayMs ?? 1e4
37
+ };
38
+ }
39
+ get headers() {
40
+ return {
41
+ Authorization: `Bearer ${this.apiKey}`,
42
+ "Content-Type": "application/json"
43
+ };
44
+ }
45
+ async request(path, options = {}) {
46
+ const { maxRetries, baseDelayMs, maxDelayMs } = this.retryConfig;
47
+ let lastError;
48
+ for (let attempt = 0;attempt <= maxRetries; attempt++) {
49
+ try {
50
+ const res = await fetch(`${this.baseUrl}${path}`, {
51
+ ...options,
52
+ headers: { ...this.headers, ...options.headers }
53
+ });
54
+ if (!res.ok) {
55
+ const body = await res.text();
56
+ let message;
57
+ let parsed = {};
58
+ try {
59
+ parsed = JSON.parse(body);
60
+ message = parsed.error ?? body;
61
+ } catch {
62
+ message = body;
63
+ }
64
+ const error = new BanataError(message, res.status, {
65
+ code: parsed.code,
66
+ requiredPlan: parsed.requiredPlan,
67
+ currentPlan: parsed.currentPlan
68
+ });
69
+ if (isRetryableStatus(res.status) && attempt < maxRetries) {
70
+ lastError = error;
71
+ const retryAfterHeader = res.headers.get("Retry-After");
72
+ let delay;
73
+ if (retryAfterHeader) {
74
+ delay = Math.min(parseInt(retryAfterHeader, 10) * 1000, maxDelayMs);
75
+ } else {
76
+ delay = Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
77
+ }
78
+ delay += Math.random() * delay * 0.25;
79
+ await sleep(delay);
80
+ continue;
81
+ }
82
+ throw error;
83
+ }
84
+ return res.json();
85
+ } catch (err) {
86
+ if (err instanceof BanataError) {
87
+ throw err;
88
+ }
89
+ lastError = err instanceof Error ? err : new Error(String(err));
90
+ if (attempt < maxRetries) {
91
+ const delay = Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
92
+ await sleep(delay + Math.random() * delay * 0.25);
93
+ continue;
94
+ }
95
+ throw new BanataError(`Network error after ${maxRetries + 1} attempts: ${lastError.message}`, 0);
96
+ }
97
+ }
98
+ throw lastError ?? new Error("Request failed");
99
+ }
100
+ async createBrowser(config = {}) {
101
+ const { timeout, waitTimeoutMs, ...rest } = config;
102
+ return this.request("/v1/browsers", {
103
+ method: "POST",
104
+ body: JSON.stringify({
105
+ ...rest,
106
+ waitTimeoutMs: waitTimeoutMs ?? timeout
107
+ })
108
+ });
109
+ }
110
+ async getBrowser(sessionId) {
111
+ return this.request(`/v1/browsers?id=${encodeURIComponent(sessionId)}`);
112
+ }
113
+ async closeBrowser(sessionId) {
114
+ await this.request(`/v1/browsers?id=${encodeURIComponent(sessionId)}`, {
115
+ method: "DELETE"
116
+ });
117
+ }
118
+ async waitForReady(sessionId, timeoutMs = 30000) {
119
+ const start = Date.now();
120
+ while (Date.now() - start < timeoutMs) {
121
+ const session = await this.getBrowser(sessionId);
122
+ if ((session.status === "ready" || session.status === "active") && session.cdpUrl) {
123
+ return session.cdpUrl;
124
+ }
125
+ if (session.status === "failed") {
126
+ throw new BanataError("Session failed to start", 500);
127
+ }
128
+ if (session.status === "ended" || session.status === "ending") {
129
+ throw new BanataError("Session ended before becoming ready", 410);
130
+ }
131
+ await sleep(500);
132
+ }
133
+ throw new BanataError(`Session ${sessionId} not ready within ${timeoutMs}ms`, 408);
134
+ }
135
+ async launch(config = {}) {
136
+ const session = await this.createBrowser(config);
137
+ let cdpUrl;
138
+ try {
139
+ cdpUrl = await this.waitForReady(session.id, config.timeout ?? 30000);
140
+ } catch (err) {
141
+ try {
142
+ await this.closeBrowser(session.id);
143
+ } catch {}
144
+ throw err;
145
+ }
146
+ return {
147
+ cdpUrl,
148
+ sessionId: session.id,
149
+ close: () => this.closeBrowser(session.id)
150
+ };
151
+ }
152
+ async getUsage() {
153
+ return this.request("/v1/usage");
154
+ }
155
+ async getBilling() {
156
+ return this.request("/v1/billing");
157
+ }
158
+ async createCheckout(params) {
159
+ return this.request("/v1/billing/checkout", {
160
+ method: "POST",
161
+ body: JSON.stringify(params)
162
+ });
163
+ }
164
+ async listWebhooks() {
165
+ const response = await this.request("/v1/webhooks");
166
+ return response.data;
167
+ }
168
+ async createWebhook(params) {
169
+ return this.request("/v1/webhooks", {
170
+ method: "POST",
171
+ body: JSON.stringify(params)
172
+ });
173
+ }
174
+ async deleteWebhook(id) {
175
+ await this.request(`/v1/webhooks?id=${encodeURIComponent(id)}`, {
176
+ method: "DELETE"
177
+ });
178
+ }
179
+ async listWebhookDeliveries(limit = 50) {
180
+ const response = await this.request(`/v1/webhooks/deliveries?limit=${encodeURIComponent(String(limit))}`);
181
+ return response.data;
182
+ }
183
+ async testWebhook(id) {
184
+ return this.request("/v1/webhooks/test", {
185
+ method: "POST",
186
+ body: JSON.stringify({ id })
187
+ });
188
+ }
189
+ }
190
+
191
+ class BanataSandbox {
192
+ apiKey;
193
+ baseUrl;
194
+ retryConfig;
195
+ constructor(config) {
196
+ if (!config.apiKey)
197
+ throw new Error("API key is required");
198
+ this.apiKey = config.apiKey;
199
+ this.baseUrl = config.baseUrl ?? "https://api.banata.dev";
200
+ this.retryConfig = {
201
+ maxRetries: config.retry?.maxRetries ?? 3,
202
+ baseDelayMs: config.retry?.baseDelayMs ?? 500,
203
+ maxDelayMs: config.retry?.maxDelayMs ?? 1e4
204
+ };
205
+ }
206
+ get headers() {
207
+ return {
208
+ Authorization: `Bearer ${this.apiKey}`,
209
+ "Content-Type": "application/json"
210
+ };
211
+ }
212
+ async request(path, options = {}) {
213
+ const { maxRetries, baseDelayMs, maxDelayMs } = this.retryConfig;
214
+ let lastError;
215
+ for (let attempt = 0;attempt <= maxRetries; attempt++) {
216
+ try {
217
+ const res = await fetch(`${this.baseUrl}${path}`, {
218
+ ...options,
219
+ headers: { ...this.headers, ...options.headers }
220
+ });
221
+ if (!res.ok) {
222
+ const body = await res.text();
223
+ let message;
224
+ let parsed = {};
225
+ try {
226
+ parsed = JSON.parse(body);
227
+ message = parsed.error ?? body;
228
+ } catch {
229
+ message = body;
230
+ }
231
+ const error = new BanataError(message, res.status, {
232
+ code: parsed.code,
233
+ requiredPlan: parsed.requiredPlan,
234
+ currentPlan: parsed.currentPlan
235
+ });
236
+ if (isRetryableStatus(res.status) && attempt < maxRetries) {
237
+ lastError = error;
238
+ const retryAfterHeader = res.headers.get("Retry-After");
239
+ let delay;
240
+ if (retryAfterHeader) {
241
+ delay = Math.min(parseInt(retryAfterHeader, 10) * 1000, maxDelayMs);
242
+ } else {
243
+ delay = Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
244
+ }
245
+ delay += Math.random() * delay * 0.25;
246
+ await sleep(delay);
247
+ continue;
248
+ }
249
+ throw error;
250
+ }
251
+ if (res.status === 204) {
252
+ return;
253
+ }
254
+ return res.json();
255
+ } catch (err) {
256
+ if (err instanceof BanataError) {
257
+ throw err;
258
+ }
259
+ lastError = err instanceof Error ? err : new Error(String(err));
260
+ if (attempt < maxRetries) {
261
+ const delay = Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
262
+ await sleep(delay + Math.random() * delay * 0.25);
263
+ continue;
264
+ }
265
+ throw new BanataError(`Network error after ${maxRetries + 1} attempts: ${lastError.message}`, 0);
266
+ }
267
+ }
268
+ throw lastError ?? new Error("Request failed");
269
+ }
270
+ async create(config = {}) {
271
+ const { timeout, waitTimeoutMs, ...rest } = config;
272
+ return this.request("/v1/sandboxes", {
273
+ method: "POST",
274
+ body: JSON.stringify({
275
+ ...rest,
276
+ waitTimeoutMs: waitTimeoutMs ?? timeout
277
+ })
278
+ });
279
+ }
280
+ async get(id) {
281
+ return this.request(`/v1/sandboxes?id=${encodeURIComponent(id)}`);
282
+ }
283
+ async list() {
284
+ const response = await this.request("/v1/sandboxes");
285
+ return response.items;
286
+ }
287
+ async kill(id) {
288
+ await this.request(`/v1/sandboxes?id=${encodeURIComponent(id)}`, {
289
+ method: "DELETE"
290
+ });
291
+ }
292
+ async waitForReady(id, timeoutMs = 30000) {
293
+ const start = Date.now();
294
+ while (Date.now() - start < timeoutMs) {
295
+ const session = await this.get(id);
296
+ if (session.status === "ready" || session.status === "active") {
297
+ return session;
298
+ }
299
+ if (session.status === "failed") {
300
+ throw new BanataError("Sandbox failed to start", 500);
301
+ }
302
+ if (session.status === "ended" || session.status === "ending") {
303
+ throw new BanataError("Sandbox ended before becoming ready", 410);
304
+ }
305
+ await sleep(500);
306
+ }
307
+ throw new BanataError(`Sandbox ${id} not ready within ${timeoutMs}ms`, 408);
308
+ }
309
+ async exec(id, command, args) {
310
+ return this.request("/v1/sandboxes/exec", {
311
+ method: "POST",
312
+ body: JSON.stringify({ id, command, args })
313
+ });
314
+ }
315
+ async runCode(id, code) {
316
+ return this.request("/v1/sandboxes/code", {
317
+ method: "POST",
318
+ body: JSON.stringify({ id, code })
319
+ });
320
+ }
321
+ async pause(id) {
322
+ await this.request("/v1/sandboxes/pause", {
323
+ method: "POST",
324
+ body: JSON.stringify({ id })
325
+ });
326
+ }
327
+ async resume(id) {
328
+ await this.request("/v1/sandboxes/resume", {
329
+ method: "POST",
330
+ body: JSON.stringify({ id })
331
+ });
332
+ }
333
+ fs = {
334
+ read: async (id, path) => {
335
+ const result = await this.request(`/v1/sandboxes/fs/read?id=${encodeURIComponent(id)}&path=${encodeURIComponent(path)}`);
336
+ return result.content;
337
+ },
338
+ write: async (id, path, content) => {
339
+ await this.request("/v1/sandboxes/fs/write", {
340
+ method: "POST",
341
+ body: JSON.stringify({ id, path, content })
342
+ });
343
+ },
344
+ list: async (id, path) => {
345
+ return this.request(`/v1/sandboxes/fs/list?id=${encodeURIComponent(id)}&path=${encodeURIComponent(path ?? "/workspace")}`);
346
+ }
347
+ };
348
+ async terminal(id) {
349
+ return this.request(`/v1/sandboxes/terminal?id=${encodeURIComponent(id)}`);
350
+ }
351
+ async getRuntime(id) {
352
+ return this.request(`/v1/sandboxes/runtime?id=${encodeURIComponent(id)}`);
353
+ }
354
+ async prompt(id, prompt, options = {}) {
355
+ return this.request("/v1/sandboxes/opencode/prompt", {
356
+ method: "POST",
357
+ body: JSON.stringify({
358
+ id,
359
+ prompt,
360
+ agent: options.agent,
361
+ sessionId: options.sessionId,
362
+ noReply: options.noReply
363
+ })
364
+ });
365
+ }
366
+ async checkpoint(id) {
367
+ return this.request("/v1/sandboxes/checkpoint", {
368
+ method: "POST",
369
+ body: JSON.stringify({ id })
370
+ });
371
+ }
372
+ async getArtifacts(id) {
373
+ return this.request(`/v1/sandboxes/artifacts?id=${encodeURIComponent(id)}`);
374
+ }
375
+ async getArtifactDownloadUrl(id, key, expiresInSeconds = 3600) {
376
+ return this.request(`/v1/sandboxes/artifacts/download?id=${encodeURIComponent(id)}&key=${encodeURIComponent(key)}&expiresIn=${encodeURIComponent(String(expiresInSeconds))}`);
377
+ }
378
+ async getPreview(id) {
379
+ return this.request(`/v1/sandboxes/browser-preview?id=${encodeURIComponent(id)}`);
380
+ }
381
+ async getHandoff(id) {
382
+ return this.request(`/v1/sandboxes/handoff?id=${encodeURIComponent(id)}`);
383
+ }
384
+ async requestHumanHandoff(id, options) {
385
+ return this.request("/v1/sandboxes/handoff/request", {
386
+ method: "POST",
387
+ body: JSON.stringify({
388
+ id,
389
+ reason: options.reason,
390
+ message: options.message,
391
+ resumePrompt: options.resumePrompt,
392
+ expiresInMs: options.expiresInMs
393
+ })
394
+ });
395
+ }
396
+ async acceptHumanHandoff(id, options) {
397
+ return this.request("/v1/sandboxes/handoff/accept", {
398
+ method: "POST",
399
+ body: JSON.stringify({
400
+ id,
401
+ controller: options.controller,
402
+ leaseMs: options.leaseMs
403
+ })
404
+ });
405
+ }
406
+ async completeHumanHandoff(id, options = {}) {
407
+ return this.request("/v1/sandboxes/handoff/complete", {
408
+ method: "POST",
409
+ body: JSON.stringify({
410
+ id,
411
+ controller: options.controller,
412
+ note: options.note,
413
+ returnControlTo: options.returnControlTo,
414
+ runResumePrompt: options.runResumePrompt
415
+ })
416
+ });
417
+ }
418
+ async setControl(id, mode, options = {}) {
419
+ return this.request("/v1/sandboxes/browser-preview/control", {
420
+ method: "POST",
421
+ body: JSON.stringify({
422
+ id,
423
+ mode,
424
+ controller: options.controller,
425
+ leaseMs: options.leaseMs
426
+ })
427
+ });
428
+ }
429
+ async launch(config = {}) {
430
+ const created = await this.create(config);
431
+ let session;
432
+ try {
433
+ session = await this.waitForReady(created.id, config.timeout ?? 30000);
434
+ } catch (err) {
435
+ try {
436
+ await this.kill(created.id);
437
+ } catch {}
438
+ throw err;
439
+ }
440
+ const sessionId = session.id;
441
+ const terminalUrl = session.terminalUrl ?? "";
442
+ const browserPreviewUrl = session.browserPreview?.publicUrl ?? session.pairedBrowser?.previewUrl ?? null;
443
+ return {
444
+ sessionId,
445
+ terminalUrl,
446
+ browserPreviewUrl,
447
+ exec: (command, args) => this.exec(sessionId, command, args),
448
+ runCode: (code) => this.runCode(sessionId, code),
449
+ prompt: (prompt, options) => this.prompt(sessionId, prompt, options),
450
+ checkpoint: () => this.checkpoint(sessionId),
451
+ getRuntime: () => this.getRuntime(sessionId),
452
+ getPreview: () => this.getPreview(sessionId),
453
+ getHandoff: () => this.getHandoff(sessionId),
454
+ setControl: (mode, options) => this.setControl(sessionId, mode, options),
455
+ takeControl: (options) => this.setControl(sessionId, "human", options),
456
+ returnControl: (options) => this.setControl(sessionId, "ai", options),
457
+ requestHumanHandoff: (options) => this.requestHumanHandoff(sessionId, options),
458
+ acceptHumanHandoff: (options) => this.acceptHumanHandoff(sessionId, options),
459
+ completeHumanHandoff: (options) => this.completeHumanHandoff(sessionId, options),
460
+ fs: {
461
+ read: (path) => this.fs.read(sessionId, path),
462
+ write: (path, content) => this.fs.write(sessionId, path, content),
463
+ list: (path) => this.fs.list(sessionId, path)
464
+ },
465
+ kill: () => this.kill(sessionId)
466
+ };
467
+ }
468
+ }
469
+ var src_default = BrowserCloud;
470
+ export {
471
+ src_default as default,
472
+ BrowserServiceError,
473
+ BrowserCloud,
474
+ BanataSandbox,
475
+ BanataError
476
+ };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@banata-boxes/sdk",
3
+ "version": "0.1.0",
4
+ "description": "TypeScript SDK for Banata browser sessions and code sandboxes",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ },
13
+ "./sdk": {
14
+ "import": "./dist/index.js",
15
+ "types": "./dist/index.d.ts"
16
+ }
17
+ },
18
+ "scripts": {
19
+ "build": "bun build src/index.ts --outdir dist --target node && bun x tsc --emitDeclarationOnly --declaration --outDir dist",
20
+ "prepublishOnly": "bun run build",
21
+ "typecheck": "tsc --noEmit",
22
+ "clean": "rm -rf dist"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "sideEffects": false,
28
+ "devDependencies": {
29
+ "typescript": "^5.4.0"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "src"
34
+ ],
35
+ "keywords": [
36
+ "banata",
37
+ "browser",
38
+ "sandbox",
39
+ "automation",
40
+ "puppeteer",
41
+ "cdp",
42
+ "headless"
43
+ ],
44
+ "license": "MIT",
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/Banata-Labs/banata-sandbox.git",
48
+ "directory": "packages/sdk"
49
+ },
50
+ "homepage": "https://docs-boxes.banata.dev"
51
+ }