@oya-ai/browser 1.0.97 → 1.0.101
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.cjs +516 -326
- package/dist/index.d.cts +834 -434
- package/dist/index.d.ts +834 -434
- package/dist/index.js +518 -328
- package/package.json +14 -6
package/dist/index.cjs
CHANGED
|
@@ -30,10 +30,13 @@ __export(index_exports, {
|
|
|
30
30
|
});
|
|
31
31
|
module.exports = __toCommonJS(index_exports);
|
|
32
32
|
|
|
33
|
-
// src/
|
|
33
|
+
// src/errors.ts
|
|
34
34
|
var OyaError = class extends Error {
|
|
35
|
+
/** The HTTP status, or the SDK's own status for errors it raises itself. */
|
|
35
36
|
status;
|
|
37
|
+
/** The server's response body, parsed when it was JSON. */
|
|
36
38
|
body;
|
|
39
|
+
/** Records the message, status and body. */
|
|
37
40
|
constructor(message, status, body) {
|
|
38
41
|
super(message);
|
|
39
42
|
this.name = "OyaError";
|
|
@@ -42,8 +45,44 @@ var OyaError = class extends Error {
|
|
|
42
45
|
}
|
|
43
46
|
};
|
|
44
47
|
|
|
48
|
+
// src/constants.ts
|
|
49
|
+
var DEFAULT_BASE_URL = "https://browser.getoya.ai";
|
|
50
|
+
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
51
|
+
var START_TIMEOUT_MS = 12e4;
|
|
52
|
+
var READY_TIMEOUT_MS = 12e4;
|
|
53
|
+
var READY_POLL_MS = 2e3;
|
|
54
|
+
var NAVIGATE_TIMEOUT_MS = 12e4;
|
|
55
|
+
var CHALLENGE_TIMEOUT_MS = 18e4;
|
|
56
|
+
var AGENT_TIMEOUT_MS = 6e5;
|
|
57
|
+
var PLAYBOOK_TIMEOUT_MS = 12e4;
|
|
58
|
+
var STOP_TIMEOUT_MS = 6e4;
|
|
59
|
+
var WAIT_FOR_DEFAULT_MS = 3e4;
|
|
60
|
+
var WAIT_FOR_GRACE_MS = 5e3;
|
|
61
|
+
var AIMED_SCROLL_AMOUNT = 500;
|
|
62
|
+
var RUN_POLL_MS = 2e3;
|
|
63
|
+
var MAX_RUN_POLL_ERRORS = 5;
|
|
64
|
+
var BYTES_PER_MB = 1048576;
|
|
65
|
+
var MAX_FILE_MB = 10;
|
|
66
|
+
var BASE64_CHUNK_BYTES = 8192;
|
|
67
|
+
var MS_PER_SECOND = 1e3;
|
|
68
|
+
var Status = {
|
|
69
|
+
/** The caller passed something unusable, such as a non-numeric element id. */
|
|
70
|
+
BAD_REQUEST: 400,
|
|
71
|
+
/** The server does not know that session. */
|
|
72
|
+
NOT_FOUND: 404,
|
|
73
|
+
/** The browser is in a state that needs attention first. */
|
|
74
|
+
CONFLICT: 409,
|
|
75
|
+
/** A browser command ran and failed. */
|
|
76
|
+
UNPROCESSABLE: 422,
|
|
77
|
+
/** A run failed without saying why. */
|
|
78
|
+
SERVER_ERROR: 500,
|
|
79
|
+
/** A browser did not come up in time. */
|
|
80
|
+
GATEWAY_TIMEOUT: 504
|
|
81
|
+
};
|
|
82
|
+
|
|
45
83
|
// src/client.ts
|
|
46
84
|
var Http = class {
|
|
85
|
+
/** Stores where to call, with which key, how long to wait and which fetch to use. */
|
|
47
86
|
constructor(baseUrl, apiKey, timeoutMs, fetchImpl) {
|
|
48
87
|
this.baseUrl = baseUrl;
|
|
49
88
|
this.apiKey = apiKey;
|
|
@@ -54,36 +93,141 @@ var Http = class {
|
|
|
54
93
|
apiKey;
|
|
55
94
|
timeoutMs;
|
|
56
95
|
fetchImpl;
|
|
96
|
+
/** Sends one request and returns the parsed answer, or throws an OyaError when it failed. */
|
|
57
97
|
async request(method, path, body, timeoutMs = this.timeoutMs, headers = {}) {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
98
|
+
const init = requestInit(method, body, timeoutMs, { Authorization: `Bearer ${this.apiKey}`, ...headers });
|
|
99
|
+
return readAnswer(await this.fetchImpl(`${this.baseUrl}${path}`, init), `${method} ${path}`, this.baseUrl);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
function requestInit(method, body, timeoutMs, headers) {
|
|
103
|
+
const json = body === void 0 ? {} : { "Content-Type": "application/json" };
|
|
104
|
+
const payload = body === void 0 ? void 0 : JSON.stringify(body);
|
|
105
|
+
return { method, headers: { ...headers, ...json }, body: payload, signal: AbortSignal.timeout(timeoutMs) };
|
|
106
|
+
}
|
|
107
|
+
async function readAnswer(res, call2, baseUrl) {
|
|
108
|
+
const payload = parseBody(await res.text());
|
|
109
|
+
if (!res.ok) throw failure(call2, res.status, payload, baseUrl);
|
|
110
|
+
return payload;
|
|
111
|
+
}
|
|
112
|
+
function parseBody(text) {
|
|
113
|
+
try {
|
|
114
|
+
return text ? JSON.parse(text) : null;
|
|
115
|
+
} catch {
|
|
116
|
+
return text;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function failure(call2, status, payload, baseUrl) {
|
|
120
|
+
let message = payload?.error || `${call2} failed (${status})`;
|
|
121
|
+
if (message === "Invalid API key") {
|
|
122
|
+
message += ` for ${baseUrl}. Check OYA_API_KEY: a value exported in your shell beats .env.`;
|
|
123
|
+
}
|
|
124
|
+
return new OyaError(message, status, payload);
|
|
125
|
+
}
|
|
126
|
+
var env = (name) => globalThis.process?.env?.[name];
|
|
127
|
+
function createHttp(options) {
|
|
128
|
+
const apiKey = options.apiKey || env("OYA_API_KEY");
|
|
129
|
+
if (!apiKey) {
|
|
130
|
+
throw new Error("No API key. Pass { apiKey } or set OYA_API_KEY \u2014 run `oya login` to get one.");
|
|
131
|
+
}
|
|
132
|
+
const baseUrl = (options.baseUrl || env("OYA_BASE_URL") || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
133
|
+
const fetchImpl = options.fetch || globalThis.fetch;
|
|
134
|
+
if (!fetchImpl) throw new Error("No fetch available \u2014 pass { fetch } or use Node 18+.");
|
|
135
|
+
return new Http(baseUrl, apiKey, options.timeoutMs ?? DEFAULT_TIMEOUT_MS, fetchImpl.bind(globalThis));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/run-watch.ts
|
|
139
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
140
|
+
async function call(fn) {
|
|
141
|
+
try {
|
|
142
|
+
await fn();
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.error("[oya] run callback threw:", err);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function watchRun(run, baseUrl, cb, pollMs) {
|
|
148
|
+
const watch = { run, baseUrl, cb, errors: 0 };
|
|
149
|
+
for (; ; ) {
|
|
150
|
+
const result = await step(watch);
|
|
151
|
+
if (result) return result;
|
|
152
|
+
await sleep(pollMs);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function step(watch) {
|
|
156
|
+
const info = await poll(watch);
|
|
157
|
+
if (!info) return void 0;
|
|
158
|
+
noticeAttention(watch, info);
|
|
159
|
+
if (info.status === "succeeded") return succeed(watch.cb, info);
|
|
160
|
+
if (info.status === "failed") return fail(watch.cb, info);
|
|
161
|
+
return void 0;
|
|
162
|
+
}
|
|
163
|
+
async function poll(watch) {
|
|
164
|
+
try {
|
|
165
|
+
const info = await watch.run.status();
|
|
166
|
+
watch.errors = 0;
|
|
167
|
+
return info;
|
|
168
|
+
} catch (err) {
|
|
169
|
+
if (++watch.errors < MAX_RUN_POLL_ERRORS) return null;
|
|
170
|
+
return giveUp(watch.cb, err);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function giveUp(cb, err) {
|
|
174
|
+
const failure2 = err instanceof OyaError ? err : new OyaError(String(err), 0, null);
|
|
175
|
+
await call(() => cb.onFailure?.(failure2));
|
|
176
|
+
throw failure2;
|
|
177
|
+
}
|
|
178
|
+
function noticeAttention(watch, info) {
|
|
179
|
+
if (info.status !== "needs_attention" || !info.attention || info.attention.id === watch.seen) return;
|
|
180
|
+
watch.seen = info.attention.id;
|
|
181
|
+
const request = {
|
|
182
|
+
...info.attention,
|
|
183
|
+
liveViewUrl: info.attention.liveViewUrl && new URL(info.attention.liveViewUrl, watch.baseUrl).href,
|
|
184
|
+
respond: (response) => watch.run.respond(response)
|
|
185
|
+
};
|
|
186
|
+
void call(() => watch.cb.onHumanAttention?.(request));
|
|
187
|
+
}
|
|
188
|
+
async function succeed(cb, info) {
|
|
189
|
+
const result = info.result || {};
|
|
190
|
+
if (result.healed) await call(() => cb.onHealed?.(result));
|
|
191
|
+
await call(() => cb.onSuccess?.(result));
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
async function fail(cb, info) {
|
|
195
|
+
const failure2 = new OyaError(info.error || "Run failed", info.errorStatus ?? Status.SERVER_ERROR, info);
|
|
196
|
+
await call(() => cb.onFailure?.(failure2));
|
|
197
|
+
throw failure2;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/run.ts
|
|
201
|
+
var Run = class {
|
|
202
|
+
/** Starts watching the run straight away. */
|
|
203
|
+
constructor(http, id, callbacks, pollMs = RUN_POLL_MS) {
|
|
204
|
+
this.http = http;
|
|
205
|
+
this.id = id;
|
|
206
|
+
this.done = this.watch(callbacks, pollMs);
|
|
207
|
+
this.done.catch(() => {
|
|
67
208
|
});
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
209
|
+
}
|
|
210
|
+
http;
|
|
211
|
+
id;
|
|
212
|
+
/** Settles with the result when the run succeeds, or rejects when it fails. */
|
|
213
|
+
done;
|
|
214
|
+
/** The run's current state. */
|
|
215
|
+
status() {
|
|
216
|
+
return this.http.request("GET", `/api/runs/${encodeURIComponent(this.id)}`);
|
|
217
|
+
}
|
|
218
|
+
/** Answer the open attention request: `'done'` after handling it by hand, or your reply to the agent. */
|
|
219
|
+
async respond(response = "done") {
|
|
220
|
+
await this.http.request("POST", `/api/runs/${encodeURIComponent(this.id)}/respond`, { response });
|
|
221
|
+
}
|
|
222
|
+
/** Polls until the run ends, firing callbacks on the way. */
|
|
223
|
+
watch(cb, pollMs) {
|
|
224
|
+
return watchRun(this, this.http.baseUrl, cb, pollMs);
|
|
81
225
|
}
|
|
82
226
|
};
|
|
83
227
|
|
|
84
228
|
// src/browser.ts
|
|
85
|
-
var NAVIGATE_TIMEOUT_MS = 12e4;
|
|
86
229
|
var Browser = class {
|
|
230
|
+
/** Wraps a started browser; `autoCaptcha` solves CAPTCHAs after every `goto()`. */
|
|
87
231
|
constructor(http, info, autoCaptcha) {
|
|
88
232
|
this.http = http;
|
|
89
233
|
this.autoCaptcha = autoCaptcha;
|
|
@@ -94,51 +238,52 @@ var Browser = class {
|
|
|
94
238
|
}
|
|
95
239
|
http;
|
|
96
240
|
autoCaptcha;
|
|
241
|
+
/** The browser's id. */
|
|
97
242
|
id;
|
|
243
|
+
/** Which provider runs it. */
|
|
98
244
|
provider;
|
|
245
|
+
/** Which persona it runs as. */
|
|
99
246
|
persona;
|
|
100
247
|
/** Point Playwright, Puppeteer or browser-use here. */
|
|
101
248
|
cdpUrl;
|
|
249
|
+
/** Runs one browser command; a command that ran and failed throws. */
|
|
102
250
|
async command(action, params = {}, timeoutMs) {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
{ action, params },
|
|
107
|
-
timeoutMs
|
|
108
|
-
);
|
|
109
|
-
if (result.ok === false) throw new OyaError(result.error || `${action} failed`, 422, result);
|
|
251
|
+
const path = `/api/browsers/${this.id}/command`;
|
|
252
|
+
const result = await this.http.request("POST", path, { action, params }, timeoutMs);
|
|
253
|
+
if (result.ok === false) throw new OyaError(result.error || `${action} failed`, Status.UNPROCESSABLE, result);
|
|
110
254
|
return result.data;
|
|
111
255
|
}
|
|
256
|
+
/** Navigates, then clears any CAPTCHA when `captcha: 'auto'` was asked for. */
|
|
112
257
|
async goto(url) {
|
|
113
258
|
await this.command("navigate", { url }, NAVIGATE_TIMEOUT_MS);
|
|
114
|
-
if (this.autoCaptcha)
|
|
115
|
-
const result = await this.solveCaptcha();
|
|
116
|
-
if (result.present && !result.solved && !result.invisible) throw new OyaError(result.error || "CAPTCHA needs attention. Call solveCaptcha() again or open the live view.", 409, result);
|
|
117
|
-
}
|
|
259
|
+
if (this.autoCaptcha) assertCaptchaCleared(await this.solveCaptcha());
|
|
118
260
|
}
|
|
119
|
-
/**
|
|
120
|
-
|
|
121
|
-
|
|
261
|
+
/**
|
|
262
|
+
* The page, written as markdown (the default), TOON or JSONL, plus its blocks and
|
|
263
|
+
* the numbered elements to act on.
|
|
264
|
+
*/
|
|
265
|
+
async analyze(options = {}) {
|
|
266
|
+
return this.command("analyze", options.format ? { format: options.format } : {});
|
|
122
267
|
}
|
|
123
268
|
/** Only the visible elements, which is what an agent almost always wants. */
|
|
124
269
|
async elements() {
|
|
125
270
|
return (await this.analyze()).elements.filter((e) => e.visible);
|
|
126
271
|
}
|
|
272
|
+
/** Clicks an element by its id from `analyze()`. */
|
|
127
273
|
async click(elementId) {
|
|
128
274
|
const id = this.elementId(elementId);
|
|
129
275
|
await this.command("click", { element_id: id, selector: `[data-ac-id="${id}"]` });
|
|
130
276
|
}
|
|
277
|
+
/** Types into an element by its id from `analyze()`. */
|
|
131
278
|
async type(elementId, text) {
|
|
132
279
|
const id = this.elementId(elementId);
|
|
133
280
|
return this.command("type", { element_id: id, selector: `[data-ac-id="${id}"]`, text });
|
|
134
281
|
}
|
|
282
|
+
/** A valid element id, or an OyaError saying where ids come from. */
|
|
135
283
|
elementId(value) {
|
|
136
|
-
|
|
137
|
-
if (typeof value !== "number" && typeof value !== "string" || value === "" || !Number.isInteger(id) || id < 0) {
|
|
138
|
-
throw new OyaError("Use a numeric element id from browser.analyze().", 400, null);
|
|
139
|
-
}
|
|
140
|
-
return id;
|
|
284
|
+
return toElementId(value);
|
|
141
285
|
}
|
|
286
|
+
/** Presses one key, such as Enter or Escape. */
|
|
142
287
|
async pressKey(key) {
|
|
143
288
|
await this.command("press_key", { key });
|
|
144
289
|
}
|
|
@@ -152,47 +297,49 @@ var Browser = class {
|
|
|
152
297
|
}
|
|
153
298
|
/** `at` aims the wheel at an inner scroller (a results panel, a chat pane) instead of the page. */
|
|
154
299
|
async scroll(direction, amount, at) {
|
|
155
|
-
await this.command("scroll",
|
|
300
|
+
await this.command("scroll", scrollParams(direction, amount, at));
|
|
156
301
|
}
|
|
157
|
-
|
|
158
|
-
|
|
302
|
+
/** Waits until `selector` matches, up to `timeout` milliseconds. */
|
|
303
|
+
async waitFor(selector, timeout = WAIT_FOR_DEFAULT_MS) {
|
|
304
|
+
await this.command("wait", { selector, timeout }, timeout + WAIT_FOR_GRACE_MS);
|
|
159
305
|
}
|
|
160
306
|
/** A `data:image/…;base64,` URL. PNG or JPEG depending on the driver. */
|
|
161
307
|
async screenshot() {
|
|
162
|
-
|
|
163
|
-
return data.screenshot;
|
|
308
|
+
return (await this.command("screenshot")).screenshot;
|
|
164
309
|
}
|
|
310
|
+
/** The active tab's URL, or empty when there is none. */
|
|
165
311
|
async url() {
|
|
166
|
-
|
|
167
|
-
return tabs.find((t) => t.active)?.url || "";
|
|
312
|
+
return (await this.tabs()).find((t) => t.active)?.url || "";
|
|
168
313
|
}
|
|
314
|
+
/** Every open tab. */
|
|
169
315
|
async tabs() {
|
|
170
|
-
|
|
171
|
-
return data.tabs || [];
|
|
316
|
+
return (await this.command("list_tabs")).tabs || [];
|
|
172
317
|
}
|
|
318
|
+
/** Opens a tab, optionally at `url`, and returns its id. */
|
|
173
319
|
async openTab(url) {
|
|
174
|
-
|
|
175
|
-
return data.tab_id;
|
|
320
|
+
return (await this.command("open_tab", { url }, NAVIGATE_TIMEOUT_MS)).tab_id;
|
|
176
321
|
}
|
|
322
|
+
/** Makes a tab the one commands act on. */
|
|
177
323
|
async switchTab(tabId) {
|
|
178
324
|
await this.command("switch_tab", { tab_id: tabId });
|
|
179
325
|
}
|
|
326
|
+
/** Closes a tab. */
|
|
180
327
|
async closeTab(tabId) {
|
|
181
328
|
await this.command("close_tab", { tab_id: tabId });
|
|
182
329
|
}
|
|
183
330
|
/**
|
|
184
|
-
|
|
331
|
+
* Detect and clear a CAPTCHA. Providers that solve natively are left to do
|
|
185
332
|
* it; everything else goes to the configured solver.
|
|
186
333
|
*/
|
|
187
334
|
solveCaptcha() {
|
|
188
|
-
return this.http.request("POST", `/api/browsers/${this.id}/captcha`, {},
|
|
335
|
+
return this.http.request("POST", `/api/browsers/${this.id}/captcha`, {}, CHALLENGE_TIMEOUT_MS);
|
|
189
336
|
}
|
|
190
337
|
/**
|
|
191
338
|
* Answer an MFA prompt with the persona's configured factor. When nothing can
|
|
192
339
|
* answer it, `liveViewUrl` is where a person finishes by hand.
|
|
193
340
|
*/
|
|
194
341
|
async completeMfa() {
|
|
195
|
-
const result = await this.http.request("POST", `/api/browsers/${this.id}/mfa`, {},
|
|
342
|
+
const result = await this.http.request("POST", `/api/browsers/${this.id}/mfa`, {}, CHALLENGE_TIMEOUT_MS);
|
|
196
343
|
if (result.liveViewUrl) result.liveViewUrl = new URL(result.liveViewUrl, this.http.baseUrl).href;
|
|
197
344
|
return result;
|
|
198
345
|
}
|
|
@@ -203,14 +350,9 @@ var Browser = class {
|
|
|
203
350
|
* with filters like `{{name|first}}`, so a playbook saved from the run stores no values.
|
|
204
351
|
*/
|
|
205
352
|
async ask(prompt, { data, secrets } = {}) {
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
{ messages: [{ role: "user", content: prompt }], data, secrets },
|
|
210
|
-
6e5
|
|
211
|
-
);
|
|
212
|
-
if (res.error) throw new OyaError(res.error, res.status ?? 500, res);
|
|
213
|
-
return res.text;
|
|
353
|
+
const body = { messages: [{ role: "user", content: prompt }], data, secrets };
|
|
354
|
+
const path = `/api/browsers/${this.id}/chat`;
|
|
355
|
+
return agentAnswer(await this.http.request("POST", path, body, AGENT_TIMEOUT_MS)).text;
|
|
214
356
|
}
|
|
215
357
|
/**
|
|
216
358
|
* Save the last `ask()` on this browser as a named playbook. Every value that was
|
|
@@ -219,7 +361,7 @@ var Browser = class {
|
|
|
219
361
|
* swapped. `code` is the same flow as a Playwright module, to read or run yourself.
|
|
220
362
|
*/
|
|
221
363
|
toPlaybook(name) {
|
|
222
|
-
return this.http.request("POST", `/api/browsers/${this.id}/playbooks`, { name },
|
|
364
|
+
return this.http.request("POST", `/api/browsers/${this.id}/playbooks`, { name }, PLAYBOOK_TIMEOUT_MS);
|
|
223
365
|
}
|
|
224
366
|
/**
|
|
225
367
|
* Replay a playbook with no LLM in the loop. Variables left out reuse the
|
|
@@ -229,14 +371,10 @@ var Browser = class {
|
|
|
229
371
|
* Play `'<name>:draft'` to try a draft before promoting it.
|
|
230
372
|
*/
|
|
231
373
|
async play(name, data = {}, { autoHeal = true } = {}) {
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
{ variables: data, autoHeal },
|
|
236
|
-
6e5
|
|
374
|
+
const path = `/api/browsers/${this.id}/playbooks/${encodeURIComponent(name)}/play`;
|
|
375
|
+
return agentAnswer(
|
|
376
|
+
await this.http.request("POST", path, { variables: data, autoHeal }, AGENT_TIMEOUT_MS)
|
|
237
377
|
);
|
|
238
|
-
if (res.error) throw new OyaError(res.error, res.status ?? 500, res);
|
|
239
|
-
return res;
|
|
240
378
|
}
|
|
241
379
|
/**
|
|
242
380
|
* Start a prompt or playbook in the background and hear back through callbacks.
|
|
@@ -250,19 +388,11 @@ var Browser = class {
|
|
|
250
388
|
return new Run(this.http, started.id, { onSuccess, onFailure, onHumanAttention, onHealed }, pollMs);
|
|
251
389
|
}
|
|
252
390
|
/**
|
|
253
|
-
* Watch it work: the console, opened on this browser.
|
|
254
|
-
*
|
|
255
|
-
* This used to return the raw frame stream with the project's API key in the
|
|
256
|
-
* query string — a permanent credential in browser history, Referer headers
|
|
257
|
-
* and every proxy log on the way, and a URL that renders as a wall of
|
|
258
|
-
* text/event-stream if a person actually opens it. It is the console deep
|
|
259
|
-
* link now, the same one the server hands back from `completeMfa()`, and it
|
|
260
|
-
* carries no credential at all.
|
|
261
|
-
*
|
|
262
|
-
* For the frames themselves, use `liveStreamUrl()`.
|
|
391
|
+
* Watch it work: the console, opened on this browser. It carries no
|
|
392
|
+
* credential. For the frames themselves, use `liveStreamUrl()`.
|
|
263
393
|
*/
|
|
264
394
|
liveViewUrl() {
|
|
265
|
-
return
|
|
395
|
+
return consoleLink(this.http.baseUrl, this.id);
|
|
266
396
|
}
|
|
267
397
|
/**
|
|
268
398
|
* The SSE stream of JPEG frames, for embedding in your own UI. EventSource
|
|
@@ -270,11 +400,8 @@ var Browser = class {
|
|
|
270
400
|
* 60 seconds. Mint one per viewer — the first connection spends it.
|
|
271
401
|
*/
|
|
272
402
|
async liveStreamUrl() {
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
`/api/control/sessions/${encodeURIComponent(this.id)}/ticket`,
|
|
276
|
-
{}
|
|
277
|
-
);
|
|
403
|
+
const path = `/api/control/sessions/${encodeURIComponent(this.id)}/ticket`;
|
|
404
|
+
const { ticket } = await this.http.request("POST", path, {});
|
|
278
405
|
return `${this.http.baseUrl}/api/live/${this.id}?ticket=${encodeURIComponent(ticket)}`;
|
|
279
406
|
}
|
|
280
407
|
/**
|
|
@@ -289,12 +416,9 @@ var Browser = class {
|
|
|
289
416
|
* expires or you revoke it, so treat it like a password.
|
|
290
417
|
*/
|
|
291
418
|
async shareUrl({ control = false, expiresInSeconds = 3600 } = {}) {
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
{ control, expiresIn: expiresInSeconds }
|
|
296
|
-
);
|
|
297
|
-
return { url: `${this.http.baseUrl}/live/${encodeURIComponent(this.id)}#t=${encodeURIComponent(c.token)}`, id: c.id, expiresAt: c.expiresAt };
|
|
419
|
+
const path = `/api/control/sessions/${encodeURIComponent(this.id)}/share`;
|
|
420
|
+
const c = await this.http.request("POST", path, { control, expiresIn: expiresInSeconds });
|
|
421
|
+
return { url: shareLink(this.http.baseUrl, this.id, c.token), id: c.id, expiresAt: c.expiresAt };
|
|
298
422
|
}
|
|
299
423
|
/** Revoke a link from `shareUrl()` before it expires, by the id it returned. */
|
|
300
424
|
async revokeShare(id) {
|
|
@@ -309,7 +433,7 @@ var Browser = class {
|
|
|
309
433
|
* CDP session is handed back to its provider, a desktop browser disconnects.
|
|
310
434
|
*/
|
|
311
435
|
stop() {
|
|
312
|
-
return this.http.request("POST", `/api/browsers/${this.id}/stop`, {},
|
|
436
|
+
return this.http.request("POST", `/api/browsers/${this.id}/stop`, {}, STOP_TIMEOUT_MS);
|
|
313
437
|
}
|
|
314
438
|
/** `await using browser = await oya.browser.start()` stops it however the block exits, errors included. */
|
|
315
439
|
async [Symbol.asyncDispose]() {
|
|
@@ -320,75 +444,258 @@ var Browser = class {
|
|
|
320
444
|
await this.stop();
|
|
321
445
|
}
|
|
322
446
|
};
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
447
|
+
function assertCaptchaCleared(result) {
|
|
448
|
+
if (!result.present || result.solved || result.invisible) return;
|
|
449
|
+
const message = result.error || "CAPTCHA needs attention. Call solveCaptcha() again or open the live view.";
|
|
450
|
+
throw new OyaError(message, Status.CONFLICT, result);
|
|
451
|
+
}
|
|
452
|
+
function toElementId(value) {
|
|
453
|
+
const id = Number(value);
|
|
454
|
+
const wrongType = typeof value !== "number" && typeof value !== "string";
|
|
455
|
+
if (wrongType || value === "" || !Number.isInteger(id) || id < 0) {
|
|
456
|
+
throw new OyaError("Use a numeric element id from browser.analyze().", Status.BAD_REQUEST, null);
|
|
331
457
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
458
|
+
return id;
|
|
459
|
+
}
|
|
460
|
+
function scrollParams(direction, amount, at) {
|
|
461
|
+
if (!at) return { direction, amount };
|
|
462
|
+
return { direction, amount: amount ?? AIMED_SCROLL_AMOUNT, ...at, smooth: false };
|
|
463
|
+
}
|
|
464
|
+
function agentAnswer(res) {
|
|
465
|
+
if (res.error) throw new OyaError(res.error, res.status ?? Status.SERVER_ERROR, res);
|
|
466
|
+
return res;
|
|
467
|
+
}
|
|
468
|
+
function consoleLink(baseUrl, id) {
|
|
469
|
+
return `${baseUrl}/dashboard/?browser=${encodeURIComponent(id)}`;
|
|
470
|
+
}
|
|
471
|
+
function shareLink(baseUrl, id, token) {
|
|
472
|
+
return `${baseUrl}/live/${encodeURIComponent(id)}#t=${encodeURIComponent(token)}`;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/api/browsers.ts
|
|
476
|
+
function startBody(options) {
|
|
477
|
+
const { profile, persona, provider, wsUrl, name, queueMs, priority, budgetUsd, governed, policy } = options;
|
|
478
|
+
return { profile: profile || persona, provider, wsUrl, name, queueMs, priority, budgetUsd, governed, policy };
|
|
479
|
+
}
|
|
480
|
+
async function start(http, wait, options) {
|
|
481
|
+
const headers = { "Idempotency-Key": options.idempotencyKey || globalThis.crypto.randomUUID() };
|
|
482
|
+
const path = "/api/browsers/start";
|
|
483
|
+
const started = await http().request("POST", path, startBody(options), START_TIMEOUT_MS, headers);
|
|
484
|
+
if (started.status === "starting") {
|
|
485
|
+
await wait(started.id, options.readyTimeoutMs ?? READY_TIMEOUT_MS + (options.queueMs || 0));
|
|
486
|
+
started.cdpUrl = (await fetchBrowser(http, started.id)).cdpUrl;
|
|
487
|
+
}
|
|
488
|
+
return new Browser(http(), started, options.captcha === "auto");
|
|
489
|
+
}
|
|
490
|
+
var fetchBrowser = (http, id) => http().request("GET", `/api/browsers/${encodeURIComponent(id)}`);
|
|
491
|
+
async function reattach(http, id) {
|
|
492
|
+
const found = await fetchBrowser(http, id);
|
|
493
|
+
return new Browser(http(), asStarted(found), false);
|
|
494
|
+
}
|
|
495
|
+
function asStarted(found) {
|
|
496
|
+
const provider = found.provider || "cdp";
|
|
497
|
+
return { id: found.id, provider, persona: found.persona || "default", status: "ready", cdpUrl: found.cdpUrl };
|
|
498
|
+
}
|
|
499
|
+
var stopBrowsers = (http, ids) => http().request("POST", "/api/browsers/stop", ids === "all" ? { all: true } : { ids }, START_TIMEOUT_MS);
|
|
500
|
+
var browserApi = (http, wait) => ({
|
|
501
|
+
/** Start a browser and wait until it can take commands. */
|
|
502
|
+
start: (options = {}) => start(http, wait, options),
|
|
503
|
+
/** Reattach to a browser that is already running. */
|
|
504
|
+
get: (id) => reattach(http, id),
|
|
505
|
+
/** Every browser on this key. */
|
|
506
|
+
list: () => http().request("GET", "/api/browsers"),
|
|
507
|
+
/** Stop some (`ids`) or every browser on this key. Each reports separately. */
|
|
508
|
+
stop: (ids) => stopBrowsers(http, ids),
|
|
509
|
+
/** Stop every browser on this key; returns how many stopped. */
|
|
510
|
+
stopAll: async () => (await stopBrowsers(http, "all")).stopped
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// src/api/control.ts
|
|
514
|
+
var session = (id, action = "") => `/api/control/sessions/${encodeURIComponent(id)}${action}`;
|
|
515
|
+
var item = (kind, id) => `/api/control/${kind}/${encodeURIComponent(id)}`;
|
|
516
|
+
var sessionCalls = (http) => ({
|
|
517
|
+
/** Settings, sessions and recent events at a glance. */
|
|
518
|
+
overview: () => http().request("GET", "/api/control"),
|
|
519
|
+
/** Every session, including disconnected and cleanup-pending ones. */
|
|
520
|
+
sessions: () => http().request("GET", "/api/control/sessions"),
|
|
521
|
+
/** One session. */
|
|
522
|
+
session: (id) => http().request("GET", session(id)),
|
|
523
|
+
/** Update limits, rate cards and retention. */
|
|
524
|
+
settings: (changes) => http().request("PATCH", "/api/control/project", changes)
|
|
525
|
+
});
|
|
526
|
+
var lifecycleCalls = (http) => ({
|
|
527
|
+
/** Cancel queued or provisioning work. */
|
|
528
|
+
cancel: (id) => http().request("POST", session(id, "/cancel"), {}),
|
|
529
|
+
/** Stop a session; `force` stops despite a profile-save error, or reconciles. */
|
|
530
|
+
stop: (id, force = false) => http().request("POST", session(id, "/stop"), { force }),
|
|
531
|
+
/** Acquire or release human control, or acknowledge the agent's resume. */
|
|
532
|
+
takeover: (id, action) => http().request("POST", session(id, "/control"), { action }),
|
|
533
|
+
/** Send one input as the human holding the control lease. */
|
|
534
|
+
input: (id, action, params) => http().request("POST", session(id, "/input"), { action, params })
|
|
535
|
+
});
|
|
536
|
+
var recoveryCalls = (http) => ({
|
|
537
|
+
/** Explicitly recover a session, or replace it with a fresh one. */
|
|
538
|
+
recover: (id, replace = false) => http().request("POST", session(id, "/recover"), { replace }),
|
|
539
|
+
/** A single-use ticket for the live stream. */
|
|
540
|
+
ticket: (id) => http().request("POST", session(id, "/ticket"), {}),
|
|
541
|
+
/** Durable lifecycle events after a cursor. */
|
|
542
|
+
events: (after = 0) => http().request("GET", `/api/control/events?after=${after}`)
|
|
543
|
+
});
|
|
544
|
+
var credentialCalls = (http) => ({
|
|
545
|
+
/** Mint a service credential. Its token is returned this once. */
|
|
546
|
+
createCredential: (options) => http().request("POST", "/api/control/credentials", options),
|
|
547
|
+
/** Revoke a service credential. */
|
|
548
|
+
revokeCredential: (id) => http().request("DELETE", item("credentials", id))
|
|
549
|
+
});
|
|
550
|
+
var memberCalls = (http) => ({
|
|
551
|
+
/** The owner and every member. */
|
|
552
|
+
members: () => http().request("GET", "/api/control/members"),
|
|
553
|
+
/** An invitation code for a new member. */
|
|
554
|
+
inviteMember: (role = "operator") => http().request("POST", "/api/control/members/invite", { role }),
|
|
555
|
+
/** Remove a member. */
|
|
556
|
+
removeMember: (userId) => http().request("DELETE", item("members", userId))
|
|
557
|
+
});
|
|
558
|
+
var webhookCalls = (http) => ({
|
|
559
|
+
/** Register a webhook; `types` limits which events it receives. */
|
|
560
|
+
createWebhook: (url, types = []) => http().request("POST", "/api/control/webhooks", { url, types }),
|
|
561
|
+
/** Disable a webhook. */
|
|
562
|
+
removeWebhook: (id) => http().request("DELETE", item("webhooks", id)),
|
|
563
|
+
/** Send a delivery again. */
|
|
564
|
+
replayDelivery: (id) => http().request("POST", `${item("deliveries", id)}/replay`, {})
|
|
565
|
+
});
|
|
566
|
+
var controlApi = (http) => ({
|
|
567
|
+
...sessionCalls(http),
|
|
568
|
+
...lifecycleCalls(http),
|
|
569
|
+
...recoveryCalls(http),
|
|
570
|
+
...credentialCalls(http),
|
|
571
|
+
...memberCalls(http),
|
|
572
|
+
...webhookCalls(http)
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// src/api/playbooks.ts
|
|
576
|
+
var playbook = (name) => `/api/playbooks/${encodeURIComponent(name)}`;
|
|
577
|
+
var playbookApi = (http) => ({
|
|
578
|
+
/** Every saved playbook, with any draft waiting on it. */
|
|
579
|
+
list: async () => (await http().request("GET", "/api/playbooks")).playbooks,
|
|
580
|
+
/** Delete a playbook and its draft, or only the draft with `'<name>:draft'`. */
|
|
581
|
+
remove: async (name) => {
|
|
582
|
+
await http().request("DELETE", playbook(name));
|
|
583
|
+
},
|
|
584
|
+
/** Replace a playbook with the draft a healed replay saved. Try it first with `browser.play('<name>:draft')`. */
|
|
585
|
+
promote: (name) => http().request("POST", `${playbook(name)}/promote`, {})
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// src/api/proxies.ts
|
|
589
|
+
var proxyApi = (http) => ({
|
|
590
|
+
/** Every proxy this key can use, shared ones included. */
|
|
591
|
+
list: async () => (await http().request("GET", "/api/proxies")).proxies,
|
|
592
|
+
/** Add a proxy. Its credentials are never read back. */
|
|
593
|
+
create: (proxy) => http().request("POST", "/api/proxies", proxy),
|
|
594
|
+
/** Remove one of this key's proxies. */
|
|
595
|
+
remove: async (id) => {
|
|
596
|
+
await http().request("DELETE", `/api/proxies/${encodeURIComponent(id)}`);
|
|
597
|
+
},
|
|
598
|
+
/** Dial each proxy and learn its real exit IP. Failing ones cool down and are skipped. */
|
|
599
|
+
check: async () => (await http().request("POST", "/api/proxies/check", {})).results
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// src/api/personas.ts
|
|
603
|
+
var identityCalls = (http) => ({
|
|
604
|
+
/** Every persona on this key. */
|
|
605
|
+
list: async () => (await http().request("GET", "/api/personas")).personas,
|
|
606
|
+
/** One persona. */
|
|
607
|
+
get: (id) => http().request("GET", `/api/personas/${id}`),
|
|
608
|
+
/**
|
|
609
|
+
* Create an identity. The device — platform, timezone, locale — is chosen
|
|
610
|
+
* here and fixed for its life; `preview()` shows what a choice produces.
|
|
611
|
+
*/
|
|
612
|
+
create: (options = {}) => http().request("POST", "/api/personas", options),
|
|
613
|
+
/** Name, concurrency cap and proxy hint. Never the device — clone for that. */
|
|
614
|
+
update: (id, changes) => http().request("PUT", `/api/personas/${id}`, changes),
|
|
615
|
+
/** A new persona of the same kind of device: same choices, fresh identity, empty jar. */
|
|
616
|
+
clone: (id, options = {}) => http().request("POST", `/api/personas/${id}/clone`, options)
|
|
617
|
+
});
|
|
618
|
+
var deviceCalls = (http) => ({
|
|
619
|
+
/** The fingerprint these choices would produce. Persists nothing. */
|
|
620
|
+
preview: async (prefs = {}) => (await http().request("POST", "/api/personas/preview", { prefs })).fingerprint,
|
|
621
|
+
/** Platforms, and the timezones and locales each may coherently claim. */
|
|
622
|
+
options: () => http().request("GET", "/api/personas/options"),
|
|
623
|
+
/** Pin the persona to one of your proxies, or `null` to let assignment happen at connect. */
|
|
624
|
+
pinProxy: (id, proxyId) => http().request("PUT", `/api/personas/${id}/proxy`, { proxyId }),
|
|
625
|
+
/** Delete a persona. */
|
|
626
|
+
remove: async (id) => {
|
|
627
|
+
await http().request("DELETE", `/api/personas/${id}`);
|
|
337
628
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
629
|
+
});
|
|
630
|
+
var mfaCalls = (http) => ({
|
|
631
|
+
/** Store the second factor for this identity. Sealed at rest, never read back. */
|
|
632
|
+
setMfa: (id, config) => http().request("PUT", `/api/personas/${id}/mfa`, config),
|
|
633
|
+
/** Remove the persona-wide factor, or the one filed against `domain`. */
|
|
634
|
+
clearMfa: async (id, domain) => {
|
|
635
|
+
await http().request("DELETE", `/api/personas/${id}/mfa${domain ? `?domain=${encodeURIComponent(domain)}` : ""}`);
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
var loginCalls = (http) => ({
|
|
639
|
+
/**
|
|
640
|
+
* Store a site login for this identity. Sealed at rest, never read back.
|
|
641
|
+
*
|
|
642
|
+
* Signing in once on the desktop and inheriting the cookies is still the
|
|
643
|
+
* better path. This is for portals that expire a session server-side
|
|
644
|
+
* between runs, where an unattended run has nothing else to recover with.
|
|
645
|
+
*/
|
|
646
|
+
setCredentials: (id, config) => http().request("PUT", `/api/personas/${id}/credentials`, config),
|
|
647
|
+
/** Which sites this identity can sign in to. Usernames only. */
|
|
648
|
+
credentials: (id) => http().request("GET", `/api/personas/${id}/credentials`),
|
|
649
|
+
/** Remove the login stored for one site. */
|
|
650
|
+
clearCredentials: async (id, domain) => {
|
|
651
|
+
await http().request("DELETE", `/api/personas/${id}/credentials?domain=${encodeURIComponent(domain)}`);
|
|
341
652
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
if (run.status === "failed") {
|
|
381
|
-
const failure = new OyaError(run.error || "Run failed", run.errorStatus ?? 500, run);
|
|
382
|
-
await call(() => cb.onFailure?.(failure));
|
|
383
|
-
throw failure;
|
|
384
|
-
}
|
|
385
|
-
await sleep(pollMs);
|
|
653
|
+
});
|
|
654
|
+
var personaApi = (http) => ({
|
|
655
|
+
...identityCalls(http),
|
|
656
|
+
...deviceCalls(http),
|
|
657
|
+
...mfaCalls(http),
|
|
658
|
+
...loginCalls(http)
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// src/api/config.ts
|
|
662
|
+
var configApi = (http) => ({
|
|
663
|
+
/** This key's settings. Secrets read back masked. */
|
|
664
|
+
get: () => http().request("GET", "/api/config"),
|
|
665
|
+
/** Change settings; `null` clears a field back to the deployment default. */
|
|
666
|
+
set: (values) => http().request("POST", "/api/config", values)
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
// src/api/ready.ts
|
|
670
|
+
var ENDED = ["failed", "stopped", "unknown_outcome"];
|
|
671
|
+
async function waitUntilConnected(checks, id, timeoutMs) {
|
|
672
|
+
const deadline = Date.now() + timeoutMs;
|
|
673
|
+
while (Date.now() < deadline) {
|
|
674
|
+
if (await isUp(checks, id)) return;
|
|
675
|
+
await new Promise((r) => setTimeout(r, READY_POLL_MS));
|
|
676
|
+
}
|
|
677
|
+
const seconds = Math.round(timeoutMs / MS_PER_SECOND);
|
|
678
|
+
throw new OyaError(`Browser ${id} did not come up within ${seconds}s`, Status.GATEWAY_TIMEOUT, null);
|
|
679
|
+
}
|
|
680
|
+
async function isUp(checks, id) {
|
|
681
|
+
const all = await checks.list();
|
|
682
|
+
if (all.some((b) => b.id === id && b.health !== "dead")) return true;
|
|
683
|
+
await assertNotEnded(checks, id);
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
async function assertNotEnded(checks, id) {
|
|
687
|
+
try {
|
|
688
|
+
const session2 = await checks.session(id);
|
|
689
|
+
if (ENDED.includes(session2.state)) {
|
|
690
|
+
throw new OyaError(`Browser creation ended in ${session2.state}`, Status.CONFLICT, session2);
|
|
386
691
|
}
|
|
692
|
+
} catch (e) {
|
|
693
|
+
if (!(e instanceof OyaError) || e.status !== Status.NOT_FOUND) throw e;
|
|
387
694
|
}
|
|
388
|
-
}
|
|
695
|
+
}
|
|
389
696
|
|
|
390
697
|
// src/file.ts
|
|
391
|
-
var MAX_FILE_BYTES =
|
|
698
|
+
var MAX_FILE_BYTES = MAX_FILE_MB * BYTES_PER_MB;
|
|
392
699
|
var NODE_FS = "node:fs/promises";
|
|
393
700
|
var MIME = {
|
|
394
701
|
pdf: "application/pdf",
|
|
@@ -416,172 +723,67 @@ var base64 = (bytes) => {
|
|
|
416
723
|
const buffer = globalThis.Buffer;
|
|
417
724
|
if (buffer) return buffer.from(bytes).toString("base64");
|
|
418
725
|
let binary = "";
|
|
419
|
-
for (let i = 0; i < bytes.length; i +=
|
|
726
|
+
for (let i = 0; i < bytes.length; i += BASE64_CHUNK_BYTES) {
|
|
727
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + BASE64_CHUNK_BYTES));
|
|
728
|
+
}
|
|
420
729
|
return btoa(binary);
|
|
421
730
|
};
|
|
422
|
-
async function
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
731
|
+
async function readPath(path) {
|
|
732
|
+
const fs = await import(
|
|
733
|
+
/* webpackIgnore: true */
|
|
734
|
+
/* @vite-ignore */
|
|
735
|
+
NODE_FS
|
|
736
|
+
);
|
|
737
|
+
return { bytes: new Uint8Array(await fs.readFile(path)), name: path.split(/[\\/]/).pop() || "file" };
|
|
738
|
+
}
|
|
739
|
+
async function readSource(source) {
|
|
740
|
+
if (typeof source === "string") return readPath(source);
|
|
741
|
+
if (source instanceof Uint8Array) return { bytes: source };
|
|
742
|
+
return { bytes: new Uint8Array(await source.arrayBuffer()), name: source.name };
|
|
743
|
+
}
|
|
744
|
+
function checkSize(name, bytes) {
|
|
745
|
+
if (bytes.length <= MAX_FILE_BYTES) return;
|
|
746
|
+
throw new Error(
|
|
747
|
+
`${name} is ${Math.round(bytes.length / BYTES_PER_MB)}MB; the limit for a task file is ${MAX_FILE_BYTES / BYTES_PER_MB}MB.`
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
function mimeFor(name) {
|
|
443
751
|
const ext = name.includes(".") ? name.split(".").pop().toLowerCase() : "";
|
|
444
|
-
return
|
|
752
|
+
return MIME[ext] || "application/octet-stream";
|
|
753
|
+
}
|
|
754
|
+
function fileValue(name, bytes, type) {
|
|
755
|
+
checkSize(name, bytes);
|
|
756
|
+
return { file: name, type: type || mimeFor(name), b64: base64(bytes) };
|
|
757
|
+
}
|
|
758
|
+
async function file(source, options = {}) {
|
|
759
|
+
const read = await readSource(source);
|
|
760
|
+
return fileValue(options.name || read.name || "file", read.bytes, options.type);
|
|
445
761
|
}
|
|
446
762
|
|
|
447
763
|
// src/index.ts
|
|
448
|
-
var DEFAULT_BASE_URL = "https://browser.getoya.ai";
|
|
449
|
-
var READY_POLL_MS = 2e3;
|
|
450
|
-
var env = (name) => globalThis.process?.env?.[name];
|
|
451
764
|
var Oya = class {
|
|
765
|
+
/** The one HTTP path every call goes through. */
|
|
452
766
|
http;
|
|
767
|
+
/** Reads the key and URL from `options`, then OYA_API_KEY and OYA_BASE_URL. Throws without a key. */
|
|
453
768
|
constructor(options = {}) {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
this.http = new Http(baseUrl, apiKey, options.timeoutMs ?? 6e4, fetchImpl.bind(globalThis));
|
|
462
|
-
}
|
|
463
|
-
browser = {
|
|
464
|
-
/** Start a browser and wait until it can take commands. */
|
|
465
|
-
start: async (options = {}) => {
|
|
466
|
-
const started = await this.http.request("POST", "/api/browsers/start", {
|
|
467
|
-
profile: options.profile || options.persona,
|
|
468
|
-
provider: options.provider,
|
|
469
|
-
wsUrl: options.wsUrl,
|
|
470
|
-
name: options.name,
|
|
471
|
-
queueMs: options.queueMs,
|
|
472
|
-
priority: options.priority,
|
|
473
|
-
budgetUsd: options.budgetUsd,
|
|
474
|
-
governed: options.governed,
|
|
475
|
-
policy: options.policy
|
|
476
|
-
}, 12e4, { "Idempotency-Key": options.idempotencyKey || globalThis.crypto.randomUUID() });
|
|
477
|
-
if (started.status === "starting") {
|
|
478
|
-
await this.waitUntilConnected(started.id, options.readyTimeoutMs ?? 12e4 + (options.queueMs || 0));
|
|
479
|
-
const connected = await this.http.request("GET", `/api/browsers/${encodeURIComponent(started.id)}`);
|
|
480
|
-
started.cdpUrl = connected.cdpUrl;
|
|
481
|
-
}
|
|
482
|
-
return new Browser(this.http, started, options.captcha === "auto");
|
|
483
|
-
},
|
|
484
|
-
/** Reattach to a browser that is already running. */
|
|
485
|
-
get: async (id) => {
|
|
486
|
-
const found = await this.http.request("GET", `/api/browsers/${encodeURIComponent(id)}`);
|
|
487
|
-
return new Browser(this.http, {
|
|
488
|
-
id: found.id,
|
|
489
|
-
provider: found.provider || "cdp",
|
|
490
|
-
persona: found.persona || "default",
|
|
491
|
-
status: "ready",
|
|
492
|
-
cdpUrl: found.cdpUrl
|
|
493
|
-
}, false);
|
|
494
|
-
},
|
|
495
|
-
list: () => this.http.request("GET", "/api/browsers"),
|
|
496
|
-
/** Stop some (`ids`) or every browser on this key. Each reports separately. */
|
|
497
|
-
stop: (ids) => this.http.request("POST", "/api/browsers/stop", ids === "all" ? { all: true } : { ids }, 12e4),
|
|
498
|
-
stopAll: async () => (await this.browser.stop("all")).stopped
|
|
499
|
-
};
|
|
769
|
+
this.http = createHttp(options);
|
|
770
|
+
}
|
|
771
|
+
/** Start, reattach to, list and stop browsers. */
|
|
772
|
+
browser = browserApi(
|
|
773
|
+
() => this.http,
|
|
774
|
+
(id, timeoutMs) => this.waitUntilConnected(id, timeoutMs)
|
|
775
|
+
);
|
|
500
776
|
/** Durable operational controls, including disconnected and cleanup-pending sessions. */
|
|
501
|
-
control =
|
|
502
|
-
overview: () => this.http.request("GET", "/api/control"),
|
|
503
|
-
sessions: () => this.http.request("GET", "/api/control/sessions"),
|
|
504
|
-
session: (id) => this.http.request("GET", `/api/control/sessions/${encodeURIComponent(id)}`),
|
|
505
|
-
settings: (changes) => this.http.request("PATCH", "/api/control/project", changes),
|
|
506
|
-
cancel: (id) => this.http.request("POST", `/api/control/sessions/${encodeURIComponent(id)}/cancel`, {}),
|
|
507
|
-
stop: (id, force = false) => this.http.request("POST", `/api/control/sessions/${encodeURIComponent(id)}/stop`, { force }),
|
|
508
|
-
takeover: (id, action) => this.http.request("POST", `/api/control/sessions/${encodeURIComponent(id)}/control`, { action }),
|
|
509
|
-
input: (id, action, params) => this.http.request("POST", `/api/control/sessions/${encodeURIComponent(id)}/input`, { action, params }),
|
|
510
|
-
recover: (id, replace = false) => this.http.request("POST", `/api/control/sessions/${encodeURIComponent(id)}/recover`, { replace }),
|
|
511
|
-
ticket: (id) => this.http.request("POST", `/api/control/sessions/${encodeURIComponent(id)}/ticket`, {}),
|
|
512
|
-
events: (after = 0) => this.http.request("GET", `/api/control/events?after=${after}`),
|
|
513
|
-
createCredential: (options) => this.http.request("POST", "/api/control/credentials", options),
|
|
514
|
-
revokeCredential: (id) => this.http.request("DELETE", `/api/control/credentials/${encodeURIComponent(id)}`),
|
|
515
|
-
members: () => this.http.request("GET", "/api/control/members"),
|
|
516
|
-
inviteMember: (role = "operator") => this.http.request("POST", "/api/control/members/invite", { role }),
|
|
517
|
-
removeMember: (userId) => this.http.request("DELETE", `/api/control/members/${encodeURIComponent(userId)}`),
|
|
518
|
-
createWebhook: (url, types = []) => this.http.request("POST", "/api/control/webhooks", { url, types }),
|
|
519
|
-
removeWebhook: (id) => this.http.request("DELETE", `/api/control/webhooks/${encodeURIComponent(id)}`),
|
|
520
|
-
replayDelivery: (id) => this.http.request("POST", `/api/control/deliveries/${encodeURIComponent(id)}/replay`, {})
|
|
521
|
-
};
|
|
777
|
+
control = controlApi(() => this.http);
|
|
522
778
|
/** Playbooks saved with `browser.toPlaybook()`. */
|
|
523
|
-
playbooks =
|
|
524
|
-
list: async () => (await this.http.request("GET", "/api/playbooks")).playbooks,
|
|
525
|
-
/** Delete a playbook and its draft, or only the draft with `'<name>:draft'`. */
|
|
526
|
-
remove: async (name) => {
|
|
527
|
-
await this.http.request("DELETE", `/api/playbooks/${encodeURIComponent(name)}`);
|
|
528
|
-
},
|
|
529
|
-
/** Replace a playbook with the draft a healed replay saved. Try it first with `browser.play('<name>:draft')`. */
|
|
530
|
-
promote: (name) => this.http.request("POST", `/api/playbooks/${encodeURIComponent(name)}/promote`, {})
|
|
531
|
-
};
|
|
779
|
+
playbooks = playbookApi(() => this.http);
|
|
532
780
|
/**
|
|
533
781
|
* Proxy exits for your personas. A persona takes one at first connect (by its
|
|
534
782
|
* geo hint) or by `personas.pinProxy`, and keeps it.
|
|
535
783
|
*/
|
|
536
|
-
proxies =
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
remove: async (id) => {
|
|
540
|
-
await this.http.request("DELETE", `/api/proxies/${encodeURIComponent(id)}`);
|
|
541
|
-
},
|
|
542
|
-
/** Dial each proxy and learn its real exit IP. Failing ones cool down and are skipped. */
|
|
543
|
-
check: async () => (await this.http.request("POST", "/api/proxies/check", {})).results
|
|
544
|
-
};
|
|
545
|
-
personas = {
|
|
546
|
-
list: async () => (await this.http.request("GET", "/api/personas")).personas,
|
|
547
|
-
get: (id) => this.http.request("GET", `/api/personas/${id}`),
|
|
548
|
-
/**
|
|
549
|
-
* Create an identity. The device — platform, timezone, locale — is chosen
|
|
550
|
-
* here and fixed for its life; `preview()` shows what a choice produces.
|
|
551
|
-
*/
|
|
552
|
-
create: (options = {}) => this.http.request("POST", "/api/personas", options),
|
|
553
|
-
/** Name, concurrency cap and proxy hint. Never the device — clone for that. */
|
|
554
|
-
update: (id, changes) => this.http.request("PUT", `/api/personas/${id}`, changes),
|
|
555
|
-
/** A new persona of the same kind of device: same choices, fresh identity, empty jar. */
|
|
556
|
-
clone: (id, options = {}) => this.http.request("POST", `/api/personas/${id}/clone`, options),
|
|
557
|
-
/** The fingerprint these choices would produce. Persists nothing. */
|
|
558
|
-
preview: async (prefs = {}) => (await this.http.request("POST", "/api/personas/preview", { prefs })).fingerprint,
|
|
559
|
-
/** Platforms, and the timezones and locales each may coherently claim. */
|
|
560
|
-
options: () => this.http.request("GET", "/api/personas/options"),
|
|
561
|
-
/** Pin the persona to one of your proxies, or `null` to let assignment happen at connect. */
|
|
562
|
-
pinProxy: (id, proxyId) => this.http.request("PUT", `/api/personas/${id}/proxy`, { proxyId }),
|
|
563
|
-
remove: async (id) => {
|
|
564
|
-
await this.http.request("DELETE", `/api/personas/${id}`);
|
|
565
|
-
},
|
|
566
|
-
/** Store the second factor for this identity. Sealed at rest, never read back. */
|
|
567
|
-
setMfa: (id, config) => this.http.request("PUT", `/api/personas/${id}/mfa`, config),
|
|
568
|
-
clearMfa: async (id, domain) => {
|
|
569
|
-
await this.http.request("DELETE", `/api/personas/${id}/mfa${domain ? `?domain=${encodeURIComponent(domain)}` : ""}`);
|
|
570
|
-
},
|
|
571
|
-
/**
|
|
572
|
-
* Store a site login for this identity. Sealed at rest, never read back.
|
|
573
|
-
*
|
|
574
|
-
* Signing in once on the desktop and inheriting the cookies is still the
|
|
575
|
-
* better path. This is for portals that expire a session server-side
|
|
576
|
-
* between runs, where an unattended run has nothing else to recover with.
|
|
577
|
-
*/
|
|
578
|
-
setCredentials: (id, config) => this.http.request("PUT", `/api/personas/${id}/credentials`, config),
|
|
579
|
-
/** Which sites this identity can sign in to. Usernames only. */
|
|
580
|
-
credentials: (id) => this.http.request("GET", `/api/personas/${id}/credentials`),
|
|
581
|
-
clearCredentials: async (id, domain) => {
|
|
582
|
-
await this.http.request("DELETE", `/api/personas/${id}/credentials?domain=${encodeURIComponent(domain)}`);
|
|
583
|
-
}
|
|
584
|
-
};
|
|
784
|
+
proxies = proxyApi(() => this.http);
|
|
785
|
+
/** Identities: fingerprint, cookie jar and proxy bound together, plus their stored factors and logins. */
|
|
786
|
+
personas = personaApi(() => this.http);
|
|
585
787
|
/**
|
|
586
788
|
* This key's settings: LLM credentials, browser provider, solver.
|
|
587
789
|
*
|
|
@@ -593,29 +795,17 @@ var Oya = class {
|
|
|
593
795
|
* project-scoped endpoint, set `openai_base_url` to `.../endpoints/openapi` and pass an
|
|
594
796
|
* OAuth access token as `openai_api_key`. See {@link ConfigUpdate} for every field.
|
|
595
797
|
*/
|
|
596
|
-
config =
|
|
597
|
-
get: () => this.http.request("GET", "/api/config"),
|
|
598
|
-
set: (values) => this.http.request("POST", "/api/config", values)
|
|
599
|
-
};
|
|
798
|
+
config = configApi(() => this.http);
|
|
600
799
|
/** Saved profiles. `personas` is retained as an alias for existing clients. */
|
|
601
800
|
profiles = this.personas;
|
|
801
|
+
/** What this key has spent. */
|
|
602
802
|
usage() {
|
|
603
803
|
return this.http.request("GET", "/api/usage");
|
|
604
804
|
}
|
|
805
|
+
/** Waits for a starting browser to dial in, through this client's own list and session calls. */
|
|
605
806
|
async waitUntilConnected(id, timeoutMs) {
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
const all = await this.browser.list();
|
|
609
|
-
if (all.some((b) => b.id === id && b.health !== "dead")) return;
|
|
610
|
-
try {
|
|
611
|
-
const session = await this.control.session(id);
|
|
612
|
-
if (["failed", "stopped", "unknown_outcome"].includes(session.state)) throw new OyaError(`Browser creation ended in ${session.state}`, 409, session);
|
|
613
|
-
} catch (e) {
|
|
614
|
-
if (!(e instanceof OyaError) || e.status !== 404) throw e;
|
|
615
|
-
}
|
|
616
|
-
await new Promise((r) => setTimeout(r, READY_POLL_MS));
|
|
617
|
-
}
|
|
618
|
-
throw new OyaError(`Browser ${id} did not come up within ${Math.round(timeoutMs / 1e3)}s`, 504, null);
|
|
807
|
+
const checks = { list: () => this.browser.list(), session: (sid) => this.control.session(sid) };
|
|
808
|
+
await waitUntilConnected(checks, id, timeoutMs);
|
|
619
809
|
}
|
|
620
810
|
};
|
|
621
811
|
var index_default = Oya;
|