instavm 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/README.md +795 -0
- package/dist/index.d.mts +424 -0
- package/dist/index.d.ts +424 -0
- package/dist/index.js +986 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +935 -0
- package/dist/index.mjs.map +1 -0
- package/dist/integrations/openai/index.d.mts +16 -0
- package/dist/integrations/openai/index.d.ts +16 -0
- package/dist/integrations/openai/index.js +39 -0
- package/dist/integrations/openai/index.js.map +1 -0
- package/dist/integrations/openai/index.mjs +14 -0
- package/dist/integrations/openai/index.mjs.map +1 -0
- package/package.json +80 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,935 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// src/client/HTTPClient.ts
|
|
4
|
+
import axios from "axios";
|
|
5
|
+
|
|
6
|
+
// src/errors/BaseError.ts
|
|
7
|
+
var InstaVMError = class extends Error {
|
|
8
|
+
constructor(message, options) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = this.constructor.name;
|
|
11
|
+
this.code = options?.code;
|
|
12
|
+
this.statusCode = options?.statusCode;
|
|
13
|
+
this.response = options?.response;
|
|
14
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
15
|
+
if (Error.captureStackTrace) {
|
|
16
|
+
Error.captureStackTrace(this, this.constructor);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var AuthenticationError = class extends InstaVMError {
|
|
21
|
+
constructor(message = "Authentication failed", options) {
|
|
22
|
+
super(message, { ...options, statusCode: 401 });
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var RateLimitError = class extends InstaVMError {
|
|
26
|
+
constructor(message = "Rate limit exceeded", retryAfter, options) {
|
|
27
|
+
super(message, { ...options, statusCode: 429 });
|
|
28
|
+
this.retryAfter = retryAfter;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
var QuotaExceededError = class extends InstaVMError {
|
|
32
|
+
constructor(message = "Usage quota exceeded", options) {
|
|
33
|
+
super(message, { ...options, statusCode: 402 });
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var NetworkError = class extends InstaVMError {
|
|
37
|
+
constructor(message = "Network error", options) {
|
|
38
|
+
super(message, options);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var ExecutionError = class extends InstaVMError {
|
|
42
|
+
constructor(message, executionOutput, executionTime, options) {
|
|
43
|
+
super(message, options);
|
|
44
|
+
this.executionOutput = executionOutput;
|
|
45
|
+
this.executionTime = executionTime;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var SessionError = class extends InstaVMError {
|
|
49
|
+
constructor(message = "Session error", options) {
|
|
50
|
+
super(message, options);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var BrowserError = class extends InstaVMError {
|
|
54
|
+
constructor(message = "Browser error", options) {
|
|
55
|
+
super(message, options);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var BrowserSessionError = class extends BrowserError {
|
|
59
|
+
constructor(message = "Browser session error", options) {
|
|
60
|
+
super(message, options);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var BrowserInteractionError = class extends BrowserError {
|
|
64
|
+
constructor(message = "Browser interaction error", options) {
|
|
65
|
+
super(message, options);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
var BrowserTimeoutError = class extends BrowserError {
|
|
69
|
+
constructor(message = "Browser operation timed out", options) {
|
|
70
|
+
super(message, options);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var BrowserNavigationError = class extends BrowserError {
|
|
74
|
+
constructor(message = "Browser navigation error", options) {
|
|
75
|
+
super(message, options);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
var ElementNotFoundError = class extends BrowserError {
|
|
79
|
+
constructor(message = "Element not found", selector, options) {
|
|
80
|
+
super(message, options);
|
|
81
|
+
this.selector = selector;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// src/utils/retry.ts
|
|
86
|
+
function defaultRetryCondition(error) {
|
|
87
|
+
if (error instanceof NetworkError) return true;
|
|
88
|
+
if (error instanceof RateLimitError) return true;
|
|
89
|
+
if (error.response?.status >= 500) return true;
|
|
90
|
+
if (error.code === "ECONNABORTED" || error.code === "ETIMEDOUT") return true;
|
|
91
|
+
if (error.code === "ECONNRESET" || error.code === "ENOTFOUND") return true;
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
function calculateRetryDelay(attempt, baseDelay, maxDelay = 3e4) {
|
|
95
|
+
const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);
|
|
96
|
+
const jitteredDelay = exponentialDelay * (0.5 + Math.random() * 0.5);
|
|
97
|
+
return Math.min(jitteredDelay, maxDelay);
|
|
98
|
+
}
|
|
99
|
+
async function withRetry(fn, options) {
|
|
100
|
+
const {
|
|
101
|
+
retries,
|
|
102
|
+
retryDelay,
|
|
103
|
+
maxRetryDelay = 3e4,
|
|
104
|
+
retryCondition = defaultRetryCondition
|
|
105
|
+
} = options;
|
|
106
|
+
let lastError;
|
|
107
|
+
for (let attempt = 1; attempt <= retries + 1; attempt++) {
|
|
108
|
+
try {
|
|
109
|
+
return await fn();
|
|
110
|
+
} catch (error) {
|
|
111
|
+
lastError = error;
|
|
112
|
+
if (attempt === retries + 1) {
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
if (!retryCondition(error)) {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
const delay = calculateRetryDelay(attempt, retryDelay, maxRetryDelay);
|
|
119
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
throw lastError;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/client/HTTPClient.ts
|
|
126
|
+
var HTTPClient = class {
|
|
127
|
+
get apiKey() {
|
|
128
|
+
return this.config.apiKey;
|
|
129
|
+
}
|
|
130
|
+
constructor(config) {
|
|
131
|
+
this.config = config;
|
|
132
|
+
this.client = axios.create({
|
|
133
|
+
baseURL: config.baseURL,
|
|
134
|
+
timeout: config.timeout,
|
|
135
|
+
headers: {
|
|
136
|
+
"Content-Type": "application/json",
|
|
137
|
+
"User-Agent": "instavm-js-sdk/0.1.0"
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
this.setupInterceptors();
|
|
141
|
+
}
|
|
142
|
+
setupInterceptors() {
|
|
143
|
+
this.client.interceptors.request.use(
|
|
144
|
+
(config) => {
|
|
145
|
+
if (config.url?.includes("/browser/")) {
|
|
146
|
+
config.headers["X-API-Key"] = this.config.apiKey;
|
|
147
|
+
}
|
|
148
|
+
return config;
|
|
149
|
+
},
|
|
150
|
+
(error) => Promise.reject(error)
|
|
151
|
+
);
|
|
152
|
+
this.client.interceptors.response.use(
|
|
153
|
+
(response) => response,
|
|
154
|
+
(error) => {
|
|
155
|
+
const axiosError = error;
|
|
156
|
+
const status = axiosError.response?.status;
|
|
157
|
+
const data = axiosError.response?.data;
|
|
158
|
+
const message = data?.message || data?.error || axiosError.message;
|
|
159
|
+
switch (status) {
|
|
160
|
+
case 401:
|
|
161
|
+
throw new AuthenticationError(message, {
|
|
162
|
+
statusCode: status,
|
|
163
|
+
response: data
|
|
164
|
+
});
|
|
165
|
+
case 402:
|
|
166
|
+
throw new QuotaExceededError(message, {
|
|
167
|
+
statusCode: status,
|
|
168
|
+
response: data
|
|
169
|
+
});
|
|
170
|
+
case 429:
|
|
171
|
+
const retryAfter = parseInt(
|
|
172
|
+
axiosError.response?.headers["retry-after"] || "60"
|
|
173
|
+
);
|
|
174
|
+
throw new RateLimitError(message, retryAfter, {
|
|
175
|
+
statusCode: status,
|
|
176
|
+
response: data
|
|
177
|
+
});
|
|
178
|
+
case 500:
|
|
179
|
+
case 502:
|
|
180
|
+
case 503:
|
|
181
|
+
case 504:
|
|
182
|
+
throw new NetworkError(message, {
|
|
183
|
+
statusCode: status,
|
|
184
|
+
response: data
|
|
185
|
+
});
|
|
186
|
+
default:
|
|
187
|
+
if (axiosError.code === "ECONNABORTED") {
|
|
188
|
+
throw new NetworkError("Request timeout", {
|
|
189
|
+
code: axiosError.code
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
throw new InstaVMError(message, {
|
|
193
|
+
statusCode: status,
|
|
194
|
+
response: data,
|
|
195
|
+
code: axiosError.code
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Make an HTTP request with retry logic
|
|
203
|
+
*/
|
|
204
|
+
async request(requestConfig) {
|
|
205
|
+
const axiosConfig = {
|
|
206
|
+
method: requestConfig.method,
|
|
207
|
+
url: requestConfig.url,
|
|
208
|
+
headers: requestConfig.headers,
|
|
209
|
+
data: requestConfig.data,
|
|
210
|
+
params: requestConfig.params,
|
|
211
|
+
timeout: requestConfig.timeout || this.config.timeout
|
|
212
|
+
};
|
|
213
|
+
const makeRequest = async () => {
|
|
214
|
+
const response = await this.client.request(axiosConfig);
|
|
215
|
+
return response.data;
|
|
216
|
+
};
|
|
217
|
+
return withRetry(makeRequest, {
|
|
218
|
+
retries: this.config.maxRetries,
|
|
219
|
+
retryDelay: this.config.retryDelay
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* POST request with JSON body
|
|
224
|
+
*/
|
|
225
|
+
async post(url, data, headers) {
|
|
226
|
+
return this.request({
|
|
227
|
+
method: "POST",
|
|
228
|
+
url,
|
|
229
|
+
data,
|
|
230
|
+
headers
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* POST request for code execution (uses X-API-Key header like Python client)
|
|
235
|
+
*/
|
|
236
|
+
async postExecution(url, data, headers) {
|
|
237
|
+
const requestHeaders = {
|
|
238
|
+
"X-API-Key": this.config.apiKey,
|
|
239
|
+
...headers
|
|
240
|
+
};
|
|
241
|
+
return this.request({
|
|
242
|
+
method: "POST",
|
|
243
|
+
url,
|
|
244
|
+
data,
|
|
245
|
+
headers: requestHeaders
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* POST request with form data (for file uploads)
|
|
250
|
+
*/
|
|
251
|
+
async postFormData(url, formData, headers) {
|
|
252
|
+
const requestHeaders = {
|
|
253
|
+
...formData.getHeaders(),
|
|
254
|
+
...headers
|
|
255
|
+
};
|
|
256
|
+
return this.request({
|
|
257
|
+
method: "POST",
|
|
258
|
+
url,
|
|
259
|
+
data: formData,
|
|
260
|
+
headers: requestHeaders
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* GET request
|
|
265
|
+
*/
|
|
266
|
+
async get(url, params, headers) {
|
|
267
|
+
return this.request({
|
|
268
|
+
method: "GET",
|
|
269
|
+
url,
|
|
270
|
+
params,
|
|
271
|
+
headers
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* DELETE request
|
|
276
|
+
*/
|
|
277
|
+
async delete(url, headers) {
|
|
278
|
+
return this.request({
|
|
279
|
+
method: "DELETE",
|
|
280
|
+
url,
|
|
281
|
+
headers
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// src/client/BrowserSession.ts
|
|
287
|
+
import { EventEmitter } from "eventemitter3";
|
|
288
|
+
function getErrorMessage(error) {
|
|
289
|
+
return error instanceof Error ? error.message : String(error);
|
|
290
|
+
}
|
|
291
|
+
var BrowserSession = class extends EventEmitter {
|
|
292
|
+
constructor(sessionId, httpClient) {
|
|
293
|
+
super();
|
|
294
|
+
this._isActive = true;
|
|
295
|
+
this.sessionId = sessionId;
|
|
296
|
+
this.httpClient = httpClient;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Navigate to a URL
|
|
300
|
+
*/
|
|
301
|
+
async navigate(url, options = {}) {
|
|
302
|
+
this.ensureActive();
|
|
303
|
+
const requestData = {
|
|
304
|
+
url,
|
|
305
|
+
session_id: this.sessionId,
|
|
306
|
+
wait_timeout: options.waitTimeout || 3e4,
|
|
307
|
+
wait_until: options.waitUntil || "load"
|
|
308
|
+
};
|
|
309
|
+
try {
|
|
310
|
+
const response = await this.httpClient.post(
|
|
311
|
+
"/v1/browser/interactions/navigate",
|
|
312
|
+
requestData
|
|
313
|
+
);
|
|
314
|
+
const result = {
|
|
315
|
+
success: response.success !== false,
|
|
316
|
+
url: response.url || url,
|
|
317
|
+
title: response.title,
|
|
318
|
+
status: response.status
|
|
319
|
+
};
|
|
320
|
+
this.emit("navigation", result);
|
|
321
|
+
return result;
|
|
322
|
+
} catch (error) {
|
|
323
|
+
const navigationError = new BrowserNavigationError(
|
|
324
|
+
`Navigation failed: ${getErrorMessage(error)}`,
|
|
325
|
+
{ cause: error }
|
|
326
|
+
);
|
|
327
|
+
this.emit("error", navigationError);
|
|
328
|
+
throw navigationError;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Click an element
|
|
333
|
+
*/
|
|
334
|
+
async click(selector, options = {}) {
|
|
335
|
+
this.ensureActive();
|
|
336
|
+
const requestData = {
|
|
337
|
+
selector,
|
|
338
|
+
session_id: this.sessionId,
|
|
339
|
+
timeout: options.timeout || 1e4,
|
|
340
|
+
button: options.button || "left",
|
|
341
|
+
click_count: options.clickCount || 1,
|
|
342
|
+
force: options.force || false
|
|
343
|
+
};
|
|
344
|
+
try {
|
|
345
|
+
await this.httpClient.post(
|
|
346
|
+
"/v1/browser/interactions/click",
|
|
347
|
+
requestData
|
|
348
|
+
);
|
|
349
|
+
} catch (error) {
|
|
350
|
+
const errorMessage = getErrorMessage(error);
|
|
351
|
+
if (errorMessage.includes("not found") || errorMessage.includes("selector")) {
|
|
352
|
+
throw new ElementNotFoundError(
|
|
353
|
+
`Element not found: ${selector}`,
|
|
354
|
+
selector,
|
|
355
|
+
{ cause: error }
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
if (errorMessage.includes("timeout")) {
|
|
359
|
+
throw new BrowserTimeoutError(
|
|
360
|
+
`Click timeout: ${selector}`,
|
|
361
|
+
{ cause: error }
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
throw new BrowserInteractionError(
|
|
365
|
+
`Click failed: ${errorMessage}`,
|
|
366
|
+
{ cause: error }
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Type text into an element
|
|
372
|
+
*/
|
|
373
|
+
async type(selector, text, options = {}) {
|
|
374
|
+
this.ensureActive();
|
|
375
|
+
const requestData = {
|
|
376
|
+
selector,
|
|
377
|
+
text,
|
|
378
|
+
session_id: this.sessionId,
|
|
379
|
+
timeout: options.timeout || 1e4,
|
|
380
|
+
delay: options.delay || 0,
|
|
381
|
+
clear: options.clear !== false
|
|
382
|
+
};
|
|
383
|
+
try {
|
|
384
|
+
await this.httpClient.post(
|
|
385
|
+
"/v1/browser/interactions/type",
|
|
386
|
+
requestData
|
|
387
|
+
);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
const errorMessage = getErrorMessage(error);
|
|
390
|
+
if (errorMessage.includes("not found") || errorMessage.includes("selector")) {
|
|
391
|
+
throw new ElementNotFoundError(
|
|
392
|
+
`Element not found: ${selector}`,
|
|
393
|
+
selector,
|
|
394
|
+
{ cause: error }
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
throw new BrowserInteractionError(
|
|
398
|
+
`Type failed: ${errorMessage}`,
|
|
399
|
+
{ cause: error }
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Fill a form field
|
|
405
|
+
*/
|
|
406
|
+
async fill(selector, value, options = {}) {
|
|
407
|
+
this.ensureActive();
|
|
408
|
+
const requestData = {
|
|
409
|
+
selector,
|
|
410
|
+
value,
|
|
411
|
+
session_id: this.sessionId,
|
|
412
|
+
timeout: options.timeout || 1e4,
|
|
413
|
+
force: options.force || false
|
|
414
|
+
};
|
|
415
|
+
try {
|
|
416
|
+
await this.httpClient.post(
|
|
417
|
+
"/v1/browser/interactions/fill",
|
|
418
|
+
requestData
|
|
419
|
+
);
|
|
420
|
+
} catch (error) {
|
|
421
|
+
const errorMessage = getErrorMessage(error);
|
|
422
|
+
if (errorMessage.includes("not found") || errorMessage.includes("selector")) {
|
|
423
|
+
throw new ElementNotFoundError(
|
|
424
|
+
`Element not found: ${selector}`,
|
|
425
|
+
selector,
|
|
426
|
+
{ cause: error }
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
throw new BrowserInteractionError(
|
|
430
|
+
`Fill failed: ${errorMessage}`,
|
|
431
|
+
{ cause: error }
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Scroll the page
|
|
437
|
+
*/
|
|
438
|
+
async scroll(options = {}) {
|
|
439
|
+
this.ensureActive();
|
|
440
|
+
const requestData = {
|
|
441
|
+
session_id: this.sessionId,
|
|
442
|
+
x: options.x || 0,
|
|
443
|
+
y: options.y || 500,
|
|
444
|
+
behavior: options.behavior || "auto"
|
|
445
|
+
};
|
|
446
|
+
try {
|
|
447
|
+
await this.httpClient.post(
|
|
448
|
+
"/v1/browser/interactions/scroll",
|
|
449
|
+
requestData
|
|
450
|
+
);
|
|
451
|
+
} catch (error) {
|
|
452
|
+
throw new BrowserInteractionError(
|
|
453
|
+
`Scroll failed: ${getErrorMessage(error)}`,
|
|
454
|
+
{ cause: error }
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Take a screenshot
|
|
460
|
+
*/
|
|
461
|
+
async screenshot(options = {}) {
|
|
462
|
+
this.ensureActive();
|
|
463
|
+
const requestData = {
|
|
464
|
+
session_id: this.sessionId,
|
|
465
|
+
full_page: options.fullPage !== false,
|
|
466
|
+
format: options.format || "png",
|
|
467
|
+
quality: options.quality || 90,
|
|
468
|
+
clip: options.clip
|
|
469
|
+
};
|
|
470
|
+
try {
|
|
471
|
+
const response = await this.httpClient.post(
|
|
472
|
+
"/v1/browser/interactions/screenshot",
|
|
473
|
+
requestData
|
|
474
|
+
);
|
|
475
|
+
if (!response.screenshot) {
|
|
476
|
+
throw new BrowserError("Screenshot data not returned");
|
|
477
|
+
}
|
|
478
|
+
return response.screenshot;
|
|
479
|
+
} catch (error) {
|
|
480
|
+
throw new BrowserInteractionError(
|
|
481
|
+
`Screenshot failed: ${getErrorMessage(error)}`,
|
|
482
|
+
{ cause: error }
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Extract elements from the page
|
|
488
|
+
*/
|
|
489
|
+
async extractElements(selector, attributes = ["text"], options = {}) {
|
|
490
|
+
this.ensureActive();
|
|
491
|
+
const requestData = {
|
|
492
|
+
selector,
|
|
493
|
+
attributes,
|
|
494
|
+
session_id: this.sessionId,
|
|
495
|
+
max_results: options.maxResults || 100
|
|
496
|
+
};
|
|
497
|
+
try {
|
|
498
|
+
const response = await this.httpClient.post(
|
|
499
|
+
"/v1/browser/interactions/extract",
|
|
500
|
+
requestData
|
|
501
|
+
);
|
|
502
|
+
return response.elements || [];
|
|
503
|
+
} catch (error) {
|
|
504
|
+
throw new BrowserInteractionError(
|
|
505
|
+
`Element extraction failed: ${getErrorMessage(error)}`,
|
|
506
|
+
{ cause: error }
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Wait for a condition
|
|
512
|
+
*/
|
|
513
|
+
async wait(condition, timeout = 1e4) {
|
|
514
|
+
this.ensureActive();
|
|
515
|
+
let requestData = {
|
|
516
|
+
session_id: this.sessionId,
|
|
517
|
+
timeout
|
|
518
|
+
};
|
|
519
|
+
if (condition.type === "timeout") {
|
|
520
|
+
await new Promise((resolve) => setTimeout(resolve, condition.ms));
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
requestData = {
|
|
524
|
+
...requestData,
|
|
525
|
+
condition: condition.type,
|
|
526
|
+
selector: "selector" in condition ? condition.selector : void 0
|
|
527
|
+
};
|
|
528
|
+
try {
|
|
529
|
+
await this.httpClient.post(
|
|
530
|
+
"/v1/browser/interactions/wait",
|
|
531
|
+
requestData
|
|
532
|
+
);
|
|
533
|
+
} catch (error) {
|
|
534
|
+
const errorMessage = getErrorMessage(error);
|
|
535
|
+
if (errorMessage.includes("timeout")) {
|
|
536
|
+
throw new BrowserTimeoutError(
|
|
537
|
+
`Wait condition timeout: ${condition.type}`,
|
|
538
|
+
{ cause: error }
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
throw new BrowserInteractionError(
|
|
542
|
+
`Wait failed: ${errorMessage}`,
|
|
543
|
+
{ cause: error }
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Close the browser session
|
|
549
|
+
*/
|
|
550
|
+
async close() {
|
|
551
|
+
if (!this._isActive) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
try {
|
|
555
|
+
await this.httpClient.delete(`/v1/browser/sessions/${this.sessionId}`);
|
|
556
|
+
} catch (error) {
|
|
557
|
+
console.warn(`Failed to close browser session ${this.sessionId}:`, getErrorMessage(error));
|
|
558
|
+
} finally {
|
|
559
|
+
this._isActive = false;
|
|
560
|
+
this.emit("close");
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Check if session is active
|
|
565
|
+
*/
|
|
566
|
+
get isActive() {
|
|
567
|
+
return this._isActive;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Ensure session is active before operations
|
|
571
|
+
*/
|
|
572
|
+
ensureActive() {
|
|
573
|
+
if (!this._isActive) {
|
|
574
|
+
throw new BrowserError("Browser session is not active");
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
// src/client/BrowserManager.ts
|
|
580
|
+
var BrowserManager = class {
|
|
581
|
+
constructor(httpClient) {
|
|
582
|
+
this.activeSessions = /* @__PURE__ */ new Map();
|
|
583
|
+
this.httpClient = httpClient;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Create a new browser session
|
|
587
|
+
*/
|
|
588
|
+
async createSession(options = {}) {
|
|
589
|
+
const requestData = {
|
|
590
|
+
viewport_width: options.viewportWidth || 1920,
|
|
591
|
+
viewport_height: options.viewportHeight || 1080,
|
|
592
|
+
user_agent: options.userAgent
|
|
593
|
+
};
|
|
594
|
+
try {
|
|
595
|
+
const response = await this.httpClient.post(
|
|
596
|
+
"/v1/browser/sessions/",
|
|
597
|
+
requestData
|
|
598
|
+
);
|
|
599
|
+
if (!response.session_id) {
|
|
600
|
+
throw new BrowserSessionError("No session ID returned from server");
|
|
601
|
+
}
|
|
602
|
+
const session = new BrowserSession(response.session_id, this.httpClient);
|
|
603
|
+
this.activeSessions.set(response.session_id, session);
|
|
604
|
+
session.on("close", () => {
|
|
605
|
+
this.activeSessions.delete(response.session_id);
|
|
606
|
+
});
|
|
607
|
+
return session;
|
|
608
|
+
} catch (error) {
|
|
609
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
610
|
+
throw new BrowserSessionError(
|
|
611
|
+
`Failed to create browser session: ${errorMessage}`,
|
|
612
|
+
{ cause: error }
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Get information about a specific session
|
|
618
|
+
*/
|
|
619
|
+
async getSession(sessionId) {
|
|
620
|
+
try {
|
|
621
|
+
const response = await this.httpClient.get(
|
|
622
|
+
`/v1/browser/sessions/${sessionId}`
|
|
623
|
+
);
|
|
624
|
+
return {
|
|
625
|
+
sessionId: response.session_id,
|
|
626
|
+
status: response.status || "active",
|
|
627
|
+
createdAt: response.created_at,
|
|
628
|
+
viewportWidth: response.viewport_width,
|
|
629
|
+
viewportHeight: response.viewport_height,
|
|
630
|
+
userAgent: response.user_agent
|
|
631
|
+
};
|
|
632
|
+
} catch (error) {
|
|
633
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
634
|
+
throw new BrowserSessionError(
|
|
635
|
+
`Failed to get session info: ${errorMessage}`,
|
|
636
|
+
{ cause: error }
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* List all browser sessions
|
|
642
|
+
*/
|
|
643
|
+
async listSessions() {
|
|
644
|
+
try {
|
|
645
|
+
const response = await this.httpClient.get("/v1/browser/sessions/");
|
|
646
|
+
if (!Array.isArray(response.sessions)) {
|
|
647
|
+
return [];
|
|
648
|
+
}
|
|
649
|
+
return response.sessions.map((session) => ({
|
|
650
|
+
sessionId: session.session_id,
|
|
651
|
+
status: session.status || "active",
|
|
652
|
+
createdAt: session.created_at,
|
|
653
|
+
viewportWidth: session.viewport_width,
|
|
654
|
+
viewportHeight: session.viewport_height,
|
|
655
|
+
userAgent: session.user_agent
|
|
656
|
+
}));
|
|
657
|
+
} catch (error) {
|
|
658
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
659
|
+
throw new BrowserSessionError(
|
|
660
|
+
`Failed to list sessions: ${errorMessage}`,
|
|
661
|
+
{ cause: error }
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Get a locally tracked session
|
|
667
|
+
*/
|
|
668
|
+
getLocalSession(sessionId) {
|
|
669
|
+
return this.activeSessions.get(sessionId);
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Get all locally tracked sessions
|
|
673
|
+
*/
|
|
674
|
+
getLocalSessions() {
|
|
675
|
+
return Array.from(this.activeSessions.values());
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Close all active sessions
|
|
679
|
+
*/
|
|
680
|
+
async closeAllSessions() {
|
|
681
|
+
const sessions = Array.from(this.activeSessions.values());
|
|
682
|
+
await Promise.allSettled(
|
|
683
|
+
sessions.map((session) => session.close())
|
|
684
|
+
);
|
|
685
|
+
this.activeSessions.clear();
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Clean up resources
|
|
689
|
+
*/
|
|
690
|
+
async dispose() {
|
|
691
|
+
await this.closeAllSessions();
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
// src/client/InstaVM.ts
|
|
696
|
+
import FormData from "form-data";
|
|
697
|
+
var InstaVM = class {
|
|
698
|
+
constructor(apiKey, options = {}) {
|
|
699
|
+
this._sessionId = null;
|
|
700
|
+
if (!apiKey) {
|
|
701
|
+
throw new Error("API key is required");
|
|
702
|
+
}
|
|
703
|
+
const config = {
|
|
704
|
+
baseURL: options.baseURL || "https://api.instavm.io",
|
|
705
|
+
timeout: options.timeout || 3e5,
|
|
706
|
+
// 5 minutes
|
|
707
|
+
maxRetries: options.maxRetries || 3,
|
|
708
|
+
retryDelay: options.retryDelay || 1e3,
|
|
709
|
+
apiKey
|
|
710
|
+
};
|
|
711
|
+
this.httpClient = new HTTPClient(config);
|
|
712
|
+
this.browser = new BrowserManager(this.httpClient);
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Execute code synchronously
|
|
716
|
+
*/
|
|
717
|
+
async execute(command, options = {}) {
|
|
718
|
+
let sessionId = options.sessionId || this._sessionId;
|
|
719
|
+
if (!sessionId) {
|
|
720
|
+
sessionId = await this.createSession();
|
|
721
|
+
}
|
|
722
|
+
const requestData = {
|
|
723
|
+
command,
|
|
724
|
+
language: options.language || "python",
|
|
725
|
+
timeout: options.timeout || 15,
|
|
726
|
+
session_id: sessionId
|
|
727
|
+
};
|
|
728
|
+
try {
|
|
729
|
+
const response = await this.httpClient.postExecution(
|
|
730
|
+
"/execute",
|
|
731
|
+
requestData
|
|
732
|
+
);
|
|
733
|
+
if (response.session_id) {
|
|
734
|
+
this._sessionId = response.session_id;
|
|
735
|
+
}
|
|
736
|
+
return {
|
|
737
|
+
output: response.stdout || response.output || "",
|
|
738
|
+
success: response.success !== false,
|
|
739
|
+
executionTime: response.execution_time || 0,
|
|
740
|
+
sessionId: response.session_id,
|
|
741
|
+
error: response.error
|
|
742
|
+
};
|
|
743
|
+
} catch (error) {
|
|
744
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
745
|
+
throw new ExecutionError(
|
|
746
|
+
`Code execution failed: ${errorMessage}`,
|
|
747
|
+
void 0,
|
|
748
|
+
void 0,
|
|
749
|
+
{ cause: error }
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Execute code asynchronously
|
|
755
|
+
*/
|
|
756
|
+
async executeAsync(command, options = {}) {
|
|
757
|
+
let sessionId = options.sessionId || this._sessionId;
|
|
758
|
+
if (!sessionId) {
|
|
759
|
+
sessionId = await this.createSession();
|
|
760
|
+
}
|
|
761
|
+
const requestData = {
|
|
762
|
+
command,
|
|
763
|
+
language: options.language || "python",
|
|
764
|
+
timeout: options.timeout || 15,
|
|
765
|
+
session_id: sessionId
|
|
766
|
+
};
|
|
767
|
+
try {
|
|
768
|
+
const response = await this.httpClient.postExecution(
|
|
769
|
+
"/execute_async",
|
|
770
|
+
requestData
|
|
771
|
+
);
|
|
772
|
+
if (response.session_id) {
|
|
773
|
+
this._sessionId = response.session_id;
|
|
774
|
+
}
|
|
775
|
+
return {
|
|
776
|
+
taskId: response.task_id,
|
|
777
|
+
status: response.status || "pending",
|
|
778
|
+
output: response.stdout || response.output,
|
|
779
|
+
success: response.success,
|
|
780
|
+
executionTime: response.execution_time,
|
|
781
|
+
sessionId: response.session_id,
|
|
782
|
+
error: response.error
|
|
783
|
+
};
|
|
784
|
+
} catch (error) {
|
|
785
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
786
|
+
throw new ExecutionError(
|
|
787
|
+
`Async code execution failed: ${errorMessage}`,
|
|
788
|
+
void 0,
|
|
789
|
+
void 0,
|
|
790
|
+
{ cause: error }
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Upload files to the execution environment
|
|
796
|
+
*/
|
|
797
|
+
async upload(files, options = {}) {
|
|
798
|
+
const formData = new FormData();
|
|
799
|
+
for (const file of files) {
|
|
800
|
+
if (Buffer.isBuffer(file.content)) {
|
|
801
|
+
formData.append("files", file.content, file.name);
|
|
802
|
+
} else {
|
|
803
|
+
formData.append("files", Buffer.from(file.content), file.name);
|
|
804
|
+
}
|
|
805
|
+
if (file.path) {
|
|
806
|
+
formData.append("paths", file.path);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
if (options.sessionId || this._sessionId) {
|
|
810
|
+
formData.append("session_id", options.sessionId || this._sessionId);
|
|
811
|
+
}
|
|
812
|
+
if (options.remotePath) {
|
|
813
|
+
formData.append("remote_path", options.remotePath);
|
|
814
|
+
}
|
|
815
|
+
if (options.recursive !== void 0) {
|
|
816
|
+
formData.append("recursive", String(options.recursive));
|
|
817
|
+
}
|
|
818
|
+
try {
|
|
819
|
+
const response = await this.httpClient.postFormData(
|
|
820
|
+
"/upload",
|
|
821
|
+
formData
|
|
822
|
+
);
|
|
823
|
+
return {
|
|
824
|
+
success: response.success !== false,
|
|
825
|
+
files: response.files || [],
|
|
826
|
+
message: response.message
|
|
827
|
+
};
|
|
828
|
+
} catch (error) {
|
|
829
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
830
|
+
throw new SessionError(
|
|
831
|
+
`File upload failed: ${errorMessage}`,
|
|
832
|
+
{ cause: error }
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Create a new execution session
|
|
838
|
+
*/
|
|
839
|
+
async createSession() {
|
|
840
|
+
try {
|
|
841
|
+
const response = await this.httpClient.post("/v1/sessions/session", {
|
|
842
|
+
api_key: this.httpClient.apiKey
|
|
843
|
+
});
|
|
844
|
+
if (response.session_id) {
|
|
845
|
+
this._sessionId = response.session_id;
|
|
846
|
+
return response.session_id;
|
|
847
|
+
}
|
|
848
|
+
throw new SessionError("Session creation failed: No session ID returned");
|
|
849
|
+
} catch (error) {
|
|
850
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
851
|
+
throw new SessionError(
|
|
852
|
+
`Session creation failed: ${errorMessage}`,
|
|
853
|
+
{ cause: error }
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Close a session
|
|
859
|
+
*/
|
|
860
|
+
async closeSession(sessionId) {
|
|
861
|
+
const targetSessionId = sessionId || this._sessionId;
|
|
862
|
+
if (!targetSessionId) {
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
try {
|
|
866
|
+
await this.httpClient.delete(`/v1/sessions/${targetSessionId}`);
|
|
867
|
+
if (targetSessionId === this._sessionId) {
|
|
868
|
+
this._sessionId = null;
|
|
869
|
+
}
|
|
870
|
+
} catch (error) {
|
|
871
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
872
|
+
console.warn(`Failed to close session ${targetSessionId}:`, errorMessage);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Get usage statistics for a session
|
|
877
|
+
*/
|
|
878
|
+
async getUsage(sessionId) {
|
|
879
|
+
const targetSessionId = sessionId || this._sessionId;
|
|
880
|
+
if (!targetSessionId) {
|
|
881
|
+
throw new SessionError("No active session to get usage for");
|
|
882
|
+
}
|
|
883
|
+
try {
|
|
884
|
+
const response = await this.httpClient.get(
|
|
885
|
+
`/v1/sessions/usage/${targetSessionId}`
|
|
886
|
+
);
|
|
887
|
+
return {
|
|
888
|
+
sessionsUsed: response.sessions_used || 0,
|
|
889
|
+
executionTime: response.execution_time || 0,
|
|
890
|
+
quotaRemaining: response.quota_remaining || 0,
|
|
891
|
+
quotaLimit: response.quota_limit || 0
|
|
892
|
+
};
|
|
893
|
+
} catch (error) {
|
|
894
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
895
|
+
throw new SessionError(
|
|
896
|
+
`Failed to get usage stats: ${errorMessage}`,
|
|
897
|
+
{ cause: error }
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Get the current session ID
|
|
903
|
+
*/
|
|
904
|
+
get sessionId() {
|
|
905
|
+
return this._sessionId;
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Clean up resources
|
|
909
|
+
*/
|
|
910
|
+
async dispose() {
|
|
911
|
+
if (this._sessionId) {
|
|
912
|
+
await this.closeSession();
|
|
913
|
+
}
|
|
914
|
+
await this.browser.dispose();
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
export {
|
|
918
|
+
AuthenticationError,
|
|
919
|
+
BrowserError,
|
|
920
|
+
BrowserInteractionError,
|
|
921
|
+
BrowserManager,
|
|
922
|
+
BrowserNavigationError,
|
|
923
|
+
BrowserSession,
|
|
924
|
+
BrowserSessionError,
|
|
925
|
+
BrowserTimeoutError,
|
|
926
|
+
ElementNotFoundError,
|
|
927
|
+
ExecutionError,
|
|
928
|
+
InstaVM,
|
|
929
|
+
InstaVMError,
|
|
930
|
+
NetworkError,
|
|
931
|
+
QuotaExceededError,
|
|
932
|
+
RateLimitError,
|
|
933
|
+
SessionError
|
|
934
|
+
};
|
|
935
|
+
//# sourceMappingURL=index.mjs.map
|