@oya-ai/browser 1.0.96 → 1.0.99

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