@oya-ai/browser 1.0.110 → 1.0.111

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -310,7 +310,7 @@ await browser.goto('https://github.com/trending');
310
310
 
311
311
  // Analyze page: returns markdown and visible numbered elements
312
312
  const { markdown, elements } = await browser.analyze();
313
- console.log(markdown.slice(0, 300));
313
+ console.log((markdown ?? '').slice(0, 300));
314
314
 
315
315
  // Interact using numbered element IDs:
316
316
  const firstRepo = elements.find((el) => el.tag === 'a' && el.href?.includes('/stargazers'));
@@ -468,10 +468,10 @@ const oya = new Oya({
468
468
  | `remove(id)` | Delete persona and associated cookie jar |
469
469
  | `setMfa(id, config)` | Store TOTP secret (sealed at rest with AES-256-GCM) |
470
470
  | `clearMfa(id)` | Remove MFA secret from persona |
471
- | `cookies(id, format?)` | Export the persona's logins; `'playwright'` fits `addCookies()` |
472
- | `importCookies(id, cookies)` | Merge cookies into the jar (from a file, a script, anywhere) |
473
- | `copyCookies(from, to)` | Copy one persona's logins into another |
474
- | `clearCookies(id)` | Forget every cookie: signs the persona out everywhere |
471
+ | `cookies(id, format?)` | Export the persona's logins; `'playwright'` fits `addCookies()` |
472
+ | `importCookies(id, cookies)` | Merge cookies into the jar (from a file, a script, anywhere) |
473
+ | `copyCookies(from, to)` | Copy one persona's logins into another |
474
+ | `clearCookies(id)` | Forget every cookie: signs the persona out everywhere |
475
475
 
476
476
  ### Proxies (`oya.proxies`)
477
477
 
package/dist/index.cjs CHANGED
@@ -30,6 +30,47 @@ __export(index_exports, {
30
30
  });
31
31
  module.exports = __toCommonJS(index_exports);
32
32
 
33
+ // src/constants.ts
34
+ var DEFAULT_BASE_URL = "https://oyabrowser.com";
35
+ var DEFAULT_TIMEOUT_MS = 6e4;
36
+ var START_TIMEOUT_MS = 12e4;
37
+ var READY_TIMEOUT_MS = 12e4;
38
+ var READY_POLL_MS = 2e3;
39
+ var NAVIGATE_TIMEOUT_MS = 12e4;
40
+ var CHALLENGE_TIMEOUT_MS = 18e4;
41
+ var AGENT_TIMEOUT_MS = 6e5;
42
+ var PLAYBOOK_TIMEOUT_MS = 12e4;
43
+ var STOP_TIMEOUT_MS = 6e4;
44
+ var WAIT_FOR_DEFAULT_MS = 3e4;
45
+ var WAIT_FOR_GRACE_MS = 5e3;
46
+ var AIMED_SCROLL_AMOUNT = 500;
47
+ var RUN_POLL_MS = 2e3;
48
+ var MAX_RUN_POLL_ERRORS = 5;
49
+ var BYTES_PER_MB = 1048576;
50
+ var MAX_FILE_MB = 10;
51
+ var BASE64_CHUNK_BYTES = 8192;
52
+ var MS_PER_SECOND = 1e3;
53
+ var LATIN1_MAX = 255;
54
+ var SPACE = 32;
55
+ var TAB = 9;
56
+ var DEL = 127;
57
+ var HEX = 16;
58
+ var CODE_POINT_DIGITS = 4;
59
+ var Status = {
60
+ /** The caller passed something unusable, such as a non-numeric element id. */
61
+ BAD_REQUEST: 400,
62
+ /** The server does not know that session. */
63
+ NOT_FOUND: 404,
64
+ /** The browser is in a state that needs attention first. */
65
+ CONFLICT: 409,
66
+ /** A browser command ran and failed. */
67
+ UNPROCESSABLE: 422,
68
+ /** A run failed without saying why. */
69
+ SERVER_ERROR: 500,
70
+ /** A browser did not come up in time. */
71
+ GATEWAY_TIMEOUT: 504
72
+ };
73
+
33
74
  // src/errors.ts
34
75
  var OyaError = class extends Error {
35
76
  /** The HTTP status, or the SDK's own status for errors it raises itself. */
@@ -44,6 +85,10 @@ var OyaError = class extends Error {
44
85
  this.body = body;
45
86
  }
46
87
  };
88
+ function refusal(message, field, suggestion) {
89
+ const body = { error: message, code: "invalid_request", field, ...suggestion ? { suggestion } : {} };
90
+ return new OyaError(message, Status.BAD_REQUEST, body);
91
+ }
47
92
 
48
93
  // src/cli-config.ts
49
94
  var node = () => globalThis.process;
@@ -67,58 +112,26 @@ function savedConfig() {
67
112
  }
68
113
  }
69
114
 
70
- // src/constants.ts
71
- var DEFAULT_BASE_URL = "https://oyabrowser.com";
72
- var DEFAULT_TIMEOUT_MS = 6e4;
73
- var START_TIMEOUT_MS = 12e4;
74
- var READY_TIMEOUT_MS = 12e4;
75
- var READY_POLL_MS = 2e3;
76
- var NAVIGATE_TIMEOUT_MS = 12e4;
77
- var CHALLENGE_TIMEOUT_MS = 18e4;
78
- var AGENT_TIMEOUT_MS = 6e5;
79
- var PLAYBOOK_TIMEOUT_MS = 12e4;
80
- var STOP_TIMEOUT_MS = 6e4;
81
- var WAIT_FOR_DEFAULT_MS = 3e4;
82
- var WAIT_FOR_GRACE_MS = 5e3;
83
- var AIMED_SCROLL_AMOUNT = 500;
84
- var RUN_POLL_MS = 2e3;
85
- var MAX_RUN_POLL_ERRORS = 5;
86
- var BYTES_PER_MB = 1048576;
87
- var MAX_FILE_MB = 10;
88
- var BASE64_CHUNK_BYTES = 8192;
89
- var MS_PER_SECOND = 1e3;
90
- var Status = {
91
- /** The caller passed something unusable, such as a non-numeric element id. */
92
- BAD_REQUEST: 400,
93
- /** The server does not know that session. */
94
- NOT_FOUND: 404,
95
- /** The browser is in a state that needs attention first. */
96
- CONFLICT: 409,
97
- /** A browser command ran and failed. */
98
- UNPROCESSABLE: 422,
99
- /** A run failed without saying why. */
100
- SERVER_ERROR: 500,
101
- /** A browser did not come up in time. */
102
- GATEWAY_TIMEOUT: 504
103
- };
104
-
105
115
  // src/client.ts
116
+ var OPTIONS_ORIGIN = { apiKeyFrom: "the apiKey option", baseUrlFrom: "the baseUrl option", savedKey: false };
106
117
  var Http = class {
107
- /** Stores where to call, with which key, how long to wait and which fetch to use. */
108
- constructor(baseUrl, apiKey, timeoutMs, fetchImpl) {
118
+ /** Stores where to call, with which key, how long to wait, which fetch to use, and where the key and address came from. */
119
+ constructor(baseUrl, apiKey, timeoutMs, fetchImpl, origin = OPTIONS_ORIGIN) {
109
120
  this.baseUrl = baseUrl;
110
121
  this.apiKey = apiKey;
111
122
  this.timeoutMs = timeoutMs;
112
123
  this.fetchImpl = fetchImpl;
124
+ this.origin = origin;
113
125
  }
114
126
  baseUrl;
115
127
  apiKey;
116
128
  timeoutMs;
117
129
  fetchImpl;
130
+ origin;
118
131
  /** Sends one request and returns the parsed answer, or throws an OyaError when it failed. */
119
132
  async request(method, path, body, timeoutMs = this.timeoutMs, headers = {}) {
120
133
  const res = await this.send(`${this.baseUrl}${path}`, this.init(method, body, timeoutMs, headers), timeoutMs);
121
- return readAnswer(res, `${method} ${path}`, this.baseUrl);
134
+ return readAnswer(res, `${method} ${path}`, this);
122
135
  }
123
136
  /** The fetch options for one call, with this client's key on them. */
124
137
  init(method, body, timeoutMs, headers) {
@@ -133,23 +146,34 @@ var Http = class {
133
146
  try {
134
147
  return await this.fetchImpl(url, init);
135
148
  } catch (err) {
136
- throw new OyaError(unreachable(this.baseUrl, timeoutMs, err), 0, { error: String(err?.message) });
149
+ throw noAnswer(this, timeoutMs, err);
137
150
  }
138
151
  }
139
152
  };
140
- function unreachable(baseUrl, timeoutMs, err) {
141
- const cause = err?.name === "TimeoutError" || /abort/i.test(String(err?.message));
142
- if (cause) return `No answer from ${baseUrl} within ${timeoutMs}ms. Is it reachable, and is the call this slow?`;
143
- return `Could not reach ${baseUrl}. Is the server running, and is OYA_BASE_URL right?`;
153
+ var timedOut = (err) => err?.name === "TimeoutError" || /abort/i.test(String(err?.message));
154
+ function causeOf(err) {
155
+ const cause = err?.cause;
156
+ return cause?.code ?? cause?.errors?.[0]?.code;
157
+ }
158
+ function noAnswer(http, timeoutMs, err) {
159
+ const code = timedOut(err) ? "timeout" : "unreachable";
160
+ const body = { error: String(err?.message), code, ...causeOf(err) ? { cause: causeOf(err) } : {} };
161
+ return new OyaError(unreachable(http, timeoutMs, code), 0, body);
162
+ }
163
+ function unreachable({ baseUrl, origin }, timeoutMs, code) {
164
+ if (code === "timeout")
165
+ return `No answer from ${baseUrl} within ${timeoutMs}ms. Is it reachable, and is the call this slow?`;
166
+ if (origin.baseUrlFrom === "default") return `Could not reach ${baseUrl}. Check your connection.`;
167
+ return `Could not reach ${baseUrl}. Is the server running, and is ${origin.baseUrlFrom} right?`;
144
168
  }
145
169
  function requestInit(method, body, timeoutMs, headers) {
146
170
  const json = body === void 0 ? {} : { "Content-Type": "application/json" };
147
171
  const payload = body === void 0 ? void 0 : JSON.stringify(body);
148
172
  return { method, headers: { ...headers, ...json }, body: payload, signal: AbortSignal.timeout(timeoutMs) };
149
173
  }
150
- async function readAnswer(res, call2, baseUrl) {
174
+ async function readAnswer(res, call2, http) {
151
175
  const payload = parseBody(await res.text());
152
- if (!res.ok) throw failure(call2, res.status, payload, baseUrl);
176
+ if (!res.ok) throw failure(call2, res.status, payload, http);
153
177
  return payload;
154
178
  }
155
179
  function parseBody(text) {
@@ -159,22 +183,74 @@ function parseBody(text) {
159
183
  return text;
160
184
  }
161
185
  }
162
- function failure(call2, status, payload, baseUrl) {
163
- let message = payload?.error || `${call2} failed (${status})`;
164
- if (message === "Invalid API key") {
165
- message += ` for ${baseUrl}. Check OYA_API_KEY: a value exported in your shell beats .env.`;
166
- }
167
- return new OyaError(message, status, payload);
186
+ function failure(call2, status, payload, http) {
187
+ const message = payload?.error || `${call2} failed (${status})`;
188
+ return new OyaError(message === "Invalid API key" ? invalidKey(http) : message, status, payload);
189
+ }
190
+ function invalidKey({ baseUrl, origin }) {
191
+ const overrides = origin.apiKeyFrom === "OYA_API_KEY" && origin.savedKey;
192
+ const rest = overrides ? ", which overrides the one saved in ~/.oya/config.json" : "";
193
+ return `Invalid API key for ${baseUrl}. The key came from ${origin.apiKeyFrom}${rest}.`;
168
194
  }
169
195
  var env = (name) => globalThis.process?.env?.[name];
196
+ function firstOf(option, name, saved, optionName) {
197
+ if (option) return { value: option, from: `the ${optionName} option` };
198
+ if (env(name)) return { value: env(name), from: name };
199
+ return { value: saved, from: "~/.oya/config.json" };
200
+ }
201
+ function addressOf(options, saved) {
202
+ const found = firstOf(options.baseUrl, "OYA_BASE_URL", saved, "baseUrl");
203
+ return found.value ? found : { value: DEFAULT_BASE_URL, from: "default" };
204
+ }
205
+ function keyOf(options, saved) {
206
+ const found = firstOf(options.apiKey, "OYA_API_KEY", saved, "apiKey");
207
+ if (!found.value) throw new Error("No API key. Pass { apiKey }, set OYA_API_KEY, or run `npx @oya-ai/cli login`.");
208
+ return { ...found, value: headerSafe(found.value, "apiKey", "Copy the key again.") };
209
+ }
170
210
  function createHttp(options) {
171
211
  const saved = savedConfig();
172
- const apiKey = options.apiKey || env("OYA_API_KEY") || saved.apiKey;
173
- if (!apiKey) throw new Error("No API key. Pass { apiKey }, set OYA_API_KEY, or run `npx @oya-ai/cli login`.");
174
- const baseUrl = (options.baseUrl || env("OYA_BASE_URL") || saved.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
212
+ const key = keyOf(options, saved.apiKey);
213
+ const address = addressOf(options, saved.baseUrl);
175
214
  const fetchImpl = options.fetch || globalThis.fetch;
176
215
  if (!fetchImpl) throw new Error("No fetch available, pass { fetch } or use Node 18+.");
177
- return new Http(baseUrl, apiKey, options.timeoutMs ?? DEFAULT_TIMEOUT_MS, fetchImpl.bind(globalThis));
216
+ const origin = { apiKeyFrom: key.from, baseUrlFrom: address.from, savedKey: !!saved.apiKey };
217
+ const timeout = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
218
+ return new Http(webAddress(address), key.value, timeout, fetchImpl.bind(globalThis), origin);
219
+ }
220
+ var LOCAL_HOST = /^(localhost|127\.)/i;
221
+ function webAddress({ value, from }) {
222
+ const url = value.trim().replace(/\/+$/, "");
223
+ if (/^https?:\/\//i.test(url)) return url;
224
+ const name = from.startsWith("the ") ? "baseUrl" : from === "OYA_BASE_URL" ? from : `baseUrl in ${from}`;
225
+ const suggestion = suggestionFor(url);
226
+ const tryIt = suggestion ? ` Try ${suggestion}.` : "";
227
+ throw refusal(`${name} must start with http:// or https://, not "${url}".${tryIt}`, "baseUrl", suggestion);
228
+ }
229
+ function suggestionFor(url) {
230
+ if (/^[a-z][a-z0-9+.-]*:/i.test(url) && !/^[^:/]+:\d+(\/|$)/.test(url)) return void 0;
231
+ return `${LOCAL_HOST.test(url) ? "http" : "https"}://${url}`;
232
+ }
233
+ function notInAHeader(ch) {
234
+ const code = ch.codePointAt(0);
235
+ return code < SPACE && code !== TAB || code === DEL || code > LATIN1_MAX;
236
+ }
237
+ function headerSafe(value, field, ending) {
238
+ const trimmed = value.trim();
239
+ const chars = Array.from(trimmed);
240
+ const at = chars.findIndex(notInAHeader);
241
+ if (at < 0) return trimmed;
242
+ const leading = Array.from(value).length - Array.from(value.trimStart()).length;
243
+ const where = `position ${leading + at + 1} (${codePoint(chars[at])})`;
244
+ throw refusal(`${field} has a character HTTP headers cannot carry at ${where}. ${ending}`, field);
245
+ }
246
+ var codePoint = (ch) => `U+${ch.codePointAt(0).toString(HEX).toUpperCase().padStart(CODE_POINT_DIGITS, "0")}`;
247
+ var shown = (value) => typeof value === "string" ? JSON.stringify(value) : String(value);
248
+ function segment(value, field = "id") {
249
+ if (typeof value !== "string" || !value)
250
+ throw refusal(`${field} must be a non-empty string, not ${shown(value)}`, field);
251
+ if (value.includes("/") || value === "." || value === "..")
252
+ throw refusal(`${field} must be a single path segment (no "/" and not "." or ".."), not ${shown(value)}`, field);
253
+ return encodeURIComponent(value);
178
254
  }
179
255
 
180
256
  // src/run-watch.ts
@@ -413,7 +489,7 @@ var Browser = class {
413
489
  * Play `'<name>:draft'` to try a draft before promoting it.
414
490
  */
415
491
  async play(name, data = {}, { autoHeal = true } = {}) {
416
- const path = `/api/browsers/${this.id}/playbooks/${encodeURIComponent(name)}/play`;
492
+ const path = `/api/browsers/${this.id}/playbooks/${segment(name, "name")}/play`;
417
493
  return agentAnswer(
418
494
  await this.http.request("POST", path, { variables: data, autoHeal }, AGENT_TIMEOUT_MS)
419
495
  );
@@ -464,7 +540,7 @@ var Browser = class {
464
540
  }
465
541
  /** Revoke a link from `shareUrl()` before it expires, by the id it returned. */
466
542
  async revokeShare(id) {
467
- await this.http.request("DELETE", `/api/control/credentials/${encodeURIComponent(id)}`);
543
+ await this.http.request("DELETE", `/api/control/credentials/${segment(id)}`);
468
544
  }
469
545
  /** Counters, health and the last 50 things this browser did. */
470
546
  status() {
@@ -520,8 +596,9 @@ function startBody(options) {
520
596
  const { profile, persona, provider, wsUrl, name, queueMs, priority, budgetUsd, governed, policy } = options;
521
597
  return { profile: profile || persona, provider, wsUrl, name, queueMs, priority, budgetUsd, governed, policy };
522
598
  }
599
+ var idempotencyKeyOf = ({ idempotencyKey }) => idempotencyKey ? headerSafe(idempotencyKey, "idempotencyKey", 'Use letters, digits, "-" and "_".') : globalThis.crypto.randomUUID();
523
600
  async function start(http, wait, options) {
524
- const headers = { "Idempotency-Key": options.idempotencyKey || globalThis.crypto.randomUUID() };
601
+ const headers = { "Idempotency-Key": idempotencyKeyOf(options) };
525
602
  const path = "/api/browsers/start";
526
603
  const started = await http().request("POST", path, startBody(options), START_TIMEOUT_MS, headers);
527
604
  if (started.status === "starting") {
@@ -530,7 +607,7 @@ async function start(http, wait, options) {
530
607
  }
531
608
  return new Browser(http(), started, options.captcha === "auto");
532
609
  }
533
- var fetchBrowser = (http, id) => http().request("GET", `/api/browsers/${encodeURIComponent(id)}`);
610
+ var fetchBrowser = (http, id) => http().request("GET", `/api/browsers/${segment(id)}`);
534
611
  async function reattach(http, id) {
535
612
  const found = await fetchBrowser(http, id);
536
613
  return new Browser(http(), asStarted(found), false);
@@ -542,69 +619,79 @@ function asStarted(found) {
542
619
  var stopBrowsers = (http, ids) => http().request("POST", "/api/browsers/stop", ids === "all" ? { all: true } : { ids }, START_TIMEOUT_MS);
543
620
  var browserApi = (http, wait) => ({
544
621
  /** Start a browser and wait until it can take commands. */
545
- start: (options = {}) => start(http, wait, options),
622
+ start: async (options = {}) => start(http, wait, options),
546
623
  /** Reattach to a browser that is already running. */
547
- get: (id) => reattach(http, id),
624
+ get: async (id) => reattach(http, id),
548
625
  /** Every browser on this key. */
549
- list: () => http().request("GET", "/api/browsers"),
626
+ list: async () => http().request("GET", "/api/browsers"),
550
627
  /** Stop some (`ids`) or every browser on this key. Each reports separately. */
551
- stop: (ids) => stopBrowsers(http, ids),
628
+ stop: async (ids) => stopBrowsers(http, ids),
552
629
  /** Stop every browser on this key; returns how many stopped. */
553
630
  stopAll: async () => (await stopBrowsers(http, "all")).stopped
554
631
  });
555
632
 
556
633
  // src/api/control.ts
557
- var session = (id, action = "") => `/api/control/sessions/${encodeURIComponent(id)}${action}`;
558
- var item = (kind, id) => `/api/control/${kind}/${encodeURIComponent(id)}`;
634
+ var session = (id, action = "") => `/api/control/sessions/${segment(id)}${action}`;
635
+ var item = (kind, id) => `/api/control/${kind}/${segment(id)}`;
559
636
  var sessionCalls = (http) => ({
560
637
  /** Settings, sessions and recent events at a glance. */
561
- overview: () => http().request("GET", "/api/control"),
638
+ overview: async () => http().request("GET", "/api/control"),
562
639
  /** Every session, including disconnected and cleanup-pending ones. */
563
- sessions: () => http().request("GET", "/api/control/sessions"),
640
+ sessions: async () => http().request("GET", "/api/control/sessions"),
564
641
  /** One session. */
565
- session: (id) => http().request("GET", session(id)),
642
+ session: async (id) => http().request("GET", session(id)),
566
643
  /** Update limits, rate cards and retention. */
567
- settings: (changes) => http().request("PATCH", "/api/control/project", changes)
644
+ settings: async (changes) => http().request("PATCH", "/api/control/project", changes)
568
645
  });
569
646
  var lifecycleCalls = (http) => ({
570
- /** Cancel queued or provisioning work. */
571
- cancel: (id) => http().request("POST", session(id, "/cancel"), {}),
647
+ /** Stop a session in any state; answers its final state. */
648
+ cancel: async (id) => http().request("POST", session(id, "/cancel"), {}),
572
649
  /** Stop a session; `force` stops despite a profile-save error, or reconciles. */
573
- stop: (id, force = false) => http().request("POST", session(id, "/stop"), { force }),
650
+ stop: async (id, force = false) => http().request("POST", session(id, "/stop"), { force }),
574
651
  /** Acquire or release human control, or acknowledge the agent's resume. */
575
- takeover: (id, action) => http().request("POST", session(id, "/control"), { action }),
652
+ takeover: async (id, action) => http().request("POST", session(id, "/control"), { action }),
576
653
  /** Send one input as the human holding the control lease. */
577
- input: (id, action, params) => http().request("POST", session(id, "/input"), { action, params })
654
+ input: async (id, action, params) => http().request("POST", session(id, "/input"), { action, params })
578
655
  });
656
+ function recoverBody(options) {
657
+ const { replace = false, wsUrl } = typeof options === "boolean" ? { replace: options } : options ?? {};
658
+ if (wsUrl && !replace)
659
+ throw refusal("wsUrl is only used with replace: true; recovering in place keeps the endpoint it had.", "wsUrl");
660
+ return wsUrl ? { replace, wsUrl } : { replace };
661
+ }
579
662
  var recoveryCalls = (http) => ({
580
- /** Explicitly recover a session, or replace it with a fresh one. */
581
- recover: (id, replace = false) => http().request("POST", session(id, "/recover"), { replace }),
663
+ /**
664
+ * Explicitly recover a session, or replace it with a fresh one. A cdp
665
+ * session's replacement needs the Chrome it runs on: `{ replace: true, wsUrl }`.
666
+ * `recover(id, true)` still means replace.
667
+ */
668
+ recover: async (id, options = {}) => http().request("POST", session(id, "/recover"), recoverBody(options)),
582
669
  /** A single-use ticket for the live stream. */
583
- ticket: (id) => http().request("POST", session(id, "/ticket"), {}),
670
+ ticket: async (id) => http().request("POST", session(id, "/ticket"), {}),
584
671
  /** Durable lifecycle events after a cursor. */
585
- events: (after = 0) => http().request("GET", `/api/control/events?after=${after}`)
672
+ events: async (after = 0) => http().request("GET", `/api/control/events?after=${after}`)
586
673
  });
587
674
  var credentialCalls = (http) => ({
588
675
  /** Mint a service credential. Its token is returned this once. */
589
- createCredential: (options) => http().request("POST", "/api/control/credentials", options),
676
+ createCredential: async (options) => http().request("POST", "/api/control/credentials", options),
590
677
  /** Revoke a service credential. */
591
- revokeCredential: (id) => http().request("DELETE", item("credentials", id))
678
+ revokeCredential: async (id) => http().request("DELETE", item("credentials", id))
592
679
  });
593
680
  var memberCalls = (http) => ({
594
681
  /** The owner and every member. */
595
- members: () => http().request("GET", "/api/control/members"),
682
+ members: async () => http().request("GET", "/api/control/members"),
596
683
  /** An invitation code for a new member. */
597
- inviteMember: (role = "operator") => http().request("POST", "/api/control/members/invite", { role }),
684
+ inviteMember: async (role = "operator") => http().request("POST", "/api/control/members/invite", { role }),
598
685
  /** Remove a member. */
599
- removeMember: (userId) => http().request("DELETE", item("members", userId))
686
+ removeMember: async (userId) => http().request("DELETE", item("members", userId))
600
687
  });
601
688
  var webhookCalls = (http) => ({
602
689
  /** Register a webhook; `types` limits which events it receives. */
603
- createWebhook: (url, types = []) => http().request("POST", "/api/control/webhooks", { url, types }),
690
+ createWebhook: async (url, types = []) => http().request("POST", "/api/control/webhooks", { url, types }),
604
691
  /** Disable a webhook. */
605
- removeWebhook: (id) => http().request("DELETE", item("webhooks", id)),
692
+ removeWebhook: async (id) => http().request("DELETE", item("webhooks", id)),
606
693
  /** Send a delivery again. */
607
- replayDelivery: (id) => http().request("POST", `${item("deliveries", id)}/replay`, {})
694
+ replayDelivery: async (id) => http().request("POST", `${item("deliveries", id)}/replay`, {})
608
695
  });
609
696
  var controlApi = (http) => ({
610
697
  ...sessionCalls(http),
@@ -616,7 +703,7 @@ var controlApi = (http) => ({
616
703
  });
617
704
 
618
705
  // src/api/playbooks.ts
619
- var playbook = (name) => `/api/playbooks/${encodeURIComponent(name)}`;
706
+ var playbook = (name) => `/api/playbooks/${segment(name, "name")}`;
620
707
  var playbookApi = (http) => ({
621
708
  /** Every saved playbook, with any draft waiting on it. */
622
709
  list: async () => (await http().request("GET", "/api/playbooks")).playbooks,
@@ -625,7 +712,7 @@ var playbookApi = (http) => ({
625
712
  await http().request("DELETE", playbook(name));
626
713
  },
627
714
  /** Replace a playbook with the draft a healed replay saved. Try it first with `browser.play('<name>:draft')`. */
628
- promote: (name) => http().request("POST", `${playbook(name)}/promote`, {})
715
+ promote: async (name) => http().request("POST", `${playbook(name)}/promote`, {})
629
716
  });
630
717
 
631
718
  // src/api/proxies.ts
@@ -633,10 +720,10 @@ var proxyApi = (http) => ({
633
720
  /** Every proxy this key can use, shared ones included. */
634
721
  list: async () => (await http().request("GET", "/api/proxies")).proxies,
635
722
  /** Add a proxy. Its credentials are never read back. */
636
- create: (proxy) => http().request("POST", "/api/proxies", proxy),
723
+ create: async (proxy) => http().request("POST", "/api/proxies", proxy),
637
724
  /** Remove one of this key's proxies. */
638
725
  remove: async (id) => {
639
- await http().request("DELETE", `/api/proxies/${encodeURIComponent(id)}`);
726
+ await http().request("DELETE", `/api/proxies/${segment(id)}`);
640
727
  },
641
728
  /** Dial each proxy and learn its real exit IP. Failing ones cool down and are skipped. */
642
729
  check: async () => (await http().request("POST", "/api/proxies/check", {})).results
@@ -647,35 +734,38 @@ var identityCalls = (http) => ({
647
734
  /** Every persona on this key. */
648
735
  list: async () => (await http().request("GET", "/api/personas")).personas,
649
736
  /** One persona. */
650
- get: (id) => http().request("GET", `/api/personas/${id}`),
737
+ get: async (id) => http().request("GET", `/api/personas/${segment(id)}`),
651
738
  /**
652
739
  * Create an identity. The device, platform, timezone, locale, is chosen
653
740
  * here and fixed for its life; `preview()` shows what a choice produces.
654
741
  */
655
- create: (options = {}) => http().request("POST", "/api/personas", options),
742
+ create: async (options = {}) => http().request("POST", "/api/personas", options),
656
743
  /** Name, concurrency cap and proxy hint. Never the device, clone for that. */
657
- update: (id, changes) => http().request("PUT", `/api/personas/${id}`, changes),
744
+ update: async (id, changes) => http().request("PUT", `/api/personas/${segment(id)}`, changes),
658
745
  /** A new persona of the same kind of device: same choices, fresh identity, empty jar. */
659
- clone: (id, options = {}) => http().request("POST", `/api/personas/${id}/clone`, options)
746
+ clone: async (id, options = {}) => http().request("POST", `/api/personas/${segment(id)}/clone`, options)
660
747
  });
661
748
  var deviceCalls = (http) => ({
662
749
  /** The fingerprint these choices would produce. Persists nothing. */
663
750
  preview: async (prefs = {}) => (await http().request("POST", "/api/personas/preview", { prefs })).fingerprint,
664
751
  /** Platforms, and the timezones and locales each may coherently claim. */
665
- options: () => http().request("GET", "/api/personas/options"),
752
+ options: async () => http().request("GET", "/api/personas/options"),
666
753
  /** Pin the persona to one of your proxies, or `null` to let assignment happen at connect. */
667
- pinProxy: (id, proxyId) => http().request("PUT", `/api/personas/${id}/proxy`, { proxyId }),
754
+ pinProxy: async (id, proxyId) => http().request("PUT", `/api/personas/${segment(id)}/proxy`, { proxyId }),
668
755
  /** Delete a persona. */
669
756
  remove: async (id) => {
670
- await http().request("DELETE", `/api/personas/${id}`);
757
+ await http().request("DELETE", `/api/personas/${segment(id)}`);
671
758
  }
672
759
  });
673
760
  var mfaCalls = (http) => ({
674
761
  /** Store the second factor for this identity. Sealed at rest, never read back. */
675
- setMfa: (id, config) => http().request("PUT", `/api/personas/${id}/mfa`, config),
762
+ setMfa: async (id, config) => http().request("PUT", `/api/personas/${segment(id)}/mfa`, config),
676
763
  /** Remove the persona-wide factor, or the one filed against `domain`. */
677
764
  clearMfa: async (id, domain) => {
678
- await http().request("DELETE", `/api/personas/${id}/mfa${domain ? `?domain=${encodeURIComponent(domain)}` : ""}`);
765
+ await http().request(
766
+ "DELETE",
767
+ `/api/personas/${segment(id)}/mfa${domain ? `?domain=${encodeURIComponent(domain)}` : ""}`
768
+ );
679
769
  }
680
770
  });
681
771
  var loginCalls = (http) => ({
@@ -686,20 +776,20 @@ var loginCalls = (http) => ({
686
776
  * better path. This is for portals that expire a session server-side
687
777
  * between runs, where an unattended run has nothing else to recover with.
688
778
  */
689
- setCredentials: (id, config) => http().request("PUT", `/api/personas/${id}/credentials`, config),
779
+ setCredentials: async (id, config) => http().request("PUT", `/api/personas/${segment(id)}/credentials`, config),
690
780
  /** Which sites this identity can sign in to. Usernames only. */
691
- credentials: (id) => http().request("GET", `/api/personas/${id}/credentials`),
781
+ credentials: async (id) => http().request("GET", `/api/personas/${segment(id)}/credentials`),
692
782
  /** Remove the login stored for one site. */
693
783
  clearCredentials: async (id, domain) => {
694
- await http().request("DELETE", `/api/personas/${id}/credentials?domain=${encodeURIComponent(domain)}`);
784
+ await http().request("DELETE", `/api/personas/${segment(id)}/credentials?domain=${encodeURIComponent(domain)}`);
695
785
  }
696
786
  });
697
- var jarPath = (id) => `/api/pool/cookies?persona=${encodeURIComponent(id)}`;
787
+ var jarPath = (id) => `/api/pool/cookies?persona=${segment(id)}`;
698
788
  var jarCalls = (http) => ({
699
789
  /** Every cookie in the jar; `format: 'playwright'` is ready for `context.addCookies()`. */
700
- cookies: async (id, format = "json") => (await http().request("GET", `${jarPath(id)}&format=${format}`)).cookies,
790
+ cookies: async (id, format = "json") => (await http().request("GET", `${jarPath(id)}&format=${encodeURIComponent(format)}`)).cookies,
701
791
  /** Merge cookies into the jar. The persona's browsers pick them up on their next visit to each site. */
702
- importCookies: (id, list) => http().request("PUT", jarPath(id), { cookies: list })
792
+ importCookies: async (id, list) => http().request("PUT", jarPath(id), { cookies: list })
703
793
  });
704
794
  var cookieCalls = (http) => ({
705
795
  ...jarCalls(http),
package/dist/index.d.cts CHANGED
@@ -255,6 +255,8 @@ interface BrowserInfo {
255
255
  }
256
256
  /** A browser plus what it has been doing, from `browser.status()`. */
257
257
  interface BrowserDetail extends BrowserInfo {
258
+ /** The actions it does, sorted, one spelling each: an action not listed answers action_unsupported or action_unknown. */
259
+ actions: string[];
258
260
  /** Its most recent commands, newest first. */
259
261
  activity: Activity[];
260
262
  }
@@ -342,10 +344,6 @@ interface Config extends Omit<ConfigUpdate, 'llm_provider' | 'browser_provider'
342
344
  }>;
343
345
  }
344
346
 
345
- /**
346
- * OyaError: the one error type the SDK throws for a failed call, carrying the
347
- * HTTP status and the server's answer so a caller can branch on either.
348
- */
349
347
  /** A failed API call or browser command. */
350
348
  declare class OyaError extends Error {
351
349
  /** The HTTP status, or the SDK's own status for errors it raises itself. */
@@ -721,6 +719,13 @@ type CookieFormat = 'json' | 'playwright';
721
719
  */
722
720
  /** What a member or credential may do: read, operate browsers, or also administer the project. */
723
721
  type ControlRole = 'viewer' | 'operator' | 'administrator';
722
+ /** How to recover a session: in place (the default), or replaced by a fresh one. */
723
+ interface RecoverOptions {
724
+ /** Start a fresh session in its place. */
725
+ replace?: boolean;
726
+ /** cdp only: the Chrome the replacement runs on. */
727
+ wsUrl?: string;
728
+ }
724
729
  /** What a human holding the control lease may send. Mirrors the server's allowlist. */
725
730
  type HumanInputAction = 'click' | 'type' | 'press_key' | 'scroll' | 'click_coordinates' | 'double_click' | 'drag' | 'mouse_move' | 'scroll_at' | 'type_text' | 'keyboard_type' | 'navigate' | 'back' | 'forward' | 'reload' | 'screenshot' | 'analyze' | 'read_page';
726
731
  /** A browser session as the control plane records it, including ones no longer connected. */
@@ -834,14 +839,24 @@ interface OyaOptions {
834
839
  fetch?: typeof globalThis.fetch;
835
840
  }
836
841
 
842
+ /** Where the key and the address came from, so a message names the one to fix. */
843
+ interface Origin {
844
+ /** The key's source, as a message names it: "the apiKey option", "OYA_API_KEY" or "~/.oya/config.json". */
845
+ apiKeyFrom: string;
846
+ /** The address's source, named the same way, or "default" for the hosted one. */
847
+ baseUrlFrom: string;
848
+ /** Whether a key is also saved by `oya login`, which an environment key overrides. */
849
+ savedKey: boolean;
850
+ }
837
851
  /** The one HTTP path. Everything else in this package is a wrapper over it. */
838
852
  declare class Http {
839
853
  readonly baseUrl: string;
840
854
  readonly apiKey: string;
841
855
  private readonly timeoutMs;
842
856
  private readonly fetchImpl;
843
- /** Stores where to call, with which key, how long to wait and which fetch to use. */
844
- constructor(baseUrl: string, apiKey: string, timeoutMs: number, fetchImpl: typeof globalThis.fetch);
857
+ readonly origin: Origin;
858
+ /** Stores where to call, with which key, how long to wait, which fetch to use, and where the key and address came from. */
859
+ constructor(baseUrl: string, apiKey: string, timeoutMs: number, fetchImpl: typeof globalThis.fetch, origin?: Origin);
845
860
  /** Sends one request and returns the parsed answer, or throws an OyaError when it failed. */
846
861
  request<T>(method: string, path: string, body?: unknown, timeoutMs?: number, headers?: Record<string, string>): Promise<T>;
847
862
  /** The fetch options for one call, with this client's key on them. */
@@ -1322,7 +1337,7 @@ declare class Oya {
1322
1337
  removeMember: (userId: string) => Promise<Ack>;
1323
1338
  createCredential: (options: CredentialRequest) => Promise<NewCredential>;
1324
1339
  revokeCredential: (id: string) => Promise<Ack>;
1325
- recover: (id: string, replace?: boolean) => Promise<unknown>;
1340
+ recover: (id: string, options?: boolean | RecoverOptions) => Promise<unknown>;
1326
1341
  ticket: (id: string) => Promise<Ticket>;
1327
1342
  events: (after?: number) => Promise<EventPage>;
1328
1343
  cancel: (id: string) => Promise<ControlSession>;
@@ -1413,4 +1428,4 @@ declare class Oya {
1413
1428
  private waitUntilConnected;
1414
1429
  }
1415
1430
 
1416
- export { type Activity, type Analysis, type AnalyzeOptions, type AttentionRequest, type Block, Browser, type BrowserDetail, type BrowserInfo, type CaptchaResult, type CaptchaSolver, type Config, type ConfigUpdate, type ControlCredential, type ControlEvent, type ControlOverview, type ControlRole, type ControlSession, type Cookie, type CookieFormat, type Element, type FileValue, type Fingerprint, type Health, type HumanInputAction, type LlmProvider, MAX_FILE_BYTES, type MfaConfig, type MfaResult, Oya, OyaError, type OyaOptions, type PageFormat, type PersonaInfo, type PersonaPrefs, type PlayResult, type Playbook, type PlaybookSummary, type ProjectSettings, type Provider, type ProxyCreate, type ProxyInfo, Run, type RunData, type RunInfo, type RunResult, type SiteCredentials, type StartOptions, type StartResult, type StopResult, type SubmitOptions, Oya as default, file };
1431
+ export { type Activity, type Analysis, type AnalyzeOptions, type AttentionRequest, type Block, Browser, type BrowserDetail, type BrowserInfo, type CaptchaResult, type CaptchaSolver, type Config, type ConfigUpdate, type ControlCredential, type ControlEvent, type ControlOverview, type ControlRole, type ControlSession, type Cookie, type CookieFormat, type Element, type FileValue, type Fingerprint, type Health, type HumanInputAction, type LlmProvider, MAX_FILE_BYTES, type MfaConfig, type MfaResult, Oya, OyaError, type OyaOptions, type PageFormat, type PersonaInfo, type PersonaPrefs, type PlayResult, type Playbook, type PlaybookSummary, type ProjectSettings, type Provider, type ProxyCreate, type ProxyInfo, type RecoverOptions, Run, type RunData, type RunInfo, type RunResult, type SiteCredentials, type StartOptions, type StartResult, type StopResult, type SubmitOptions, Oya as default, file };
package/dist/index.d.ts CHANGED
@@ -255,6 +255,8 @@ interface BrowserInfo {
255
255
  }
256
256
  /** A browser plus what it has been doing, from `browser.status()`. */
257
257
  interface BrowserDetail extends BrowserInfo {
258
+ /** The actions it does, sorted, one spelling each: an action not listed answers action_unsupported or action_unknown. */
259
+ actions: string[];
258
260
  /** Its most recent commands, newest first. */
259
261
  activity: Activity[];
260
262
  }
@@ -342,10 +344,6 @@ interface Config extends Omit<ConfigUpdate, 'llm_provider' | 'browser_provider'
342
344
  }>;
343
345
  }
344
346
 
345
- /**
346
- * OyaError: the one error type the SDK throws for a failed call, carrying the
347
- * HTTP status and the server's answer so a caller can branch on either.
348
- */
349
347
  /** A failed API call or browser command. */
350
348
  declare class OyaError extends Error {
351
349
  /** The HTTP status, or the SDK's own status for errors it raises itself. */
@@ -721,6 +719,13 @@ type CookieFormat = 'json' | 'playwright';
721
719
  */
722
720
  /** What a member or credential may do: read, operate browsers, or also administer the project. */
723
721
  type ControlRole = 'viewer' | 'operator' | 'administrator';
722
+ /** How to recover a session: in place (the default), or replaced by a fresh one. */
723
+ interface RecoverOptions {
724
+ /** Start a fresh session in its place. */
725
+ replace?: boolean;
726
+ /** cdp only: the Chrome the replacement runs on. */
727
+ wsUrl?: string;
728
+ }
724
729
  /** What a human holding the control lease may send. Mirrors the server's allowlist. */
725
730
  type HumanInputAction = 'click' | 'type' | 'press_key' | 'scroll' | 'click_coordinates' | 'double_click' | 'drag' | 'mouse_move' | 'scroll_at' | 'type_text' | 'keyboard_type' | 'navigate' | 'back' | 'forward' | 'reload' | 'screenshot' | 'analyze' | 'read_page';
726
731
  /** A browser session as the control plane records it, including ones no longer connected. */
@@ -834,14 +839,24 @@ interface OyaOptions {
834
839
  fetch?: typeof globalThis.fetch;
835
840
  }
836
841
 
842
+ /** Where the key and the address came from, so a message names the one to fix. */
843
+ interface Origin {
844
+ /** The key's source, as a message names it: "the apiKey option", "OYA_API_KEY" or "~/.oya/config.json". */
845
+ apiKeyFrom: string;
846
+ /** The address's source, named the same way, or "default" for the hosted one. */
847
+ baseUrlFrom: string;
848
+ /** Whether a key is also saved by `oya login`, which an environment key overrides. */
849
+ savedKey: boolean;
850
+ }
837
851
  /** The one HTTP path. Everything else in this package is a wrapper over it. */
838
852
  declare class Http {
839
853
  readonly baseUrl: string;
840
854
  readonly apiKey: string;
841
855
  private readonly timeoutMs;
842
856
  private readonly fetchImpl;
843
- /** Stores where to call, with which key, how long to wait and which fetch to use. */
844
- constructor(baseUrl: string, apiKey: string, timeoutMs: number, fetchImpl: typeof globalThis.fetch);
857
+ readonly origin: Origin;
858
+ /** Stores where to call, with which key, how long to wait, which fetch to use, and where the key and address came from. */
859
+ constructor(baseUrl: string, apiKey: string, timeoutMs: number, fetchImpl: typeof globalThis.fetch, origin?: Origin);
845
860
  /** Sends one request and returns the parsed answer, or throws an OyaError when it failed. */
846
861
  request<T>(method: string, path: string, body?: unknown, timeoutMs?: number, headers?: Record<string, string>): Promise<T>;
847
862
  /** The fetch options for one call, with this client's key on them. */
@@ -1322,7 +1337,7 @@ declare class Oya {
1322
1337
  removeMember: (userId: string) => Promise<Ack>;
1323
1338
  createCredential: (options: CredentialRequest) => Promise<NewCredential>;
1324
1339
  revokeCredential: (id: string) => Promise<Ack>;
1325
- recover: (id: string, replace?: boolean) => Promise<unknown>;
1340
+ recover: (id: string, options?: boolean | RecoverOptions) => Promise<unknown>;
1326
1341
  ticket: (id: string) => Promise<Ticket>;
1327
1342
  events: (after?: number) => Promise<EventPage>;
1328
1343
  cancel: (id: string) => Promise<ControlSession>;
@@ -1413,4 +1428,4 @@ declare class Oya {
1413
1428
  private waitUntilConnected;
1414
1429
  }
1415
1430
 
1416
- export { type Activity, type Analysis, type AnalyzeOptions, type AttentionRequest, type Block, Browser, type BrowserDetail, type BrowserInfo, type CaptchaResult, type CaptchaSolver, type Config, type ConfigUpdate, type ControlCredential, type ControlEvent, type ControlOverview, type ControlRole, type ControlSession, type Cookie, type CookieFormat, type Element, type FileValue, type Fingerprint, type Health, type HumanInputAction, type LlmProvider, MAX_FILE_BYTES, type MfaConfig, type MfaResult, Oya, OyaError, type OyaOptions, type PageFormat, type PersonaInfo, type PersonaPrefs, type PlayResult, type Playbook, type PlaybookSummary, type ProjectSettings, type Provider, type ProxyCreate, type ProxyInfo, Run, type RunData, type RunInfo, type RunResult, type SiteCredentials, type StartOptions, type StartResult, type StopResult, type SubmitOptions, Oya as default, file };
1431
+ export { type Activity, type Analysis, type AnalyzeOptions, type AttentionRequest, type Block, Browser, type BrowserDetail, type BrowserInfo, type CaptchaResult, type CaptchaSolver, type Config, type ConfigUpdate, type ControlCredential, type ControlEvent, type ControlOverview, type ControlRole, type ControlSession, type Cookie, type CookieFormat, type Element, type FileValue, type Fingerprint, type Health, type HumanInputAction, type LlmProvider, MAX_FILE_BYTES, type MfaConfig, type MfaResult, Oya, OyaError, type OyaOptions, type PageFormat, type PersonaInfo, type PersonaPrefs, type PlayResult, type Playbook, type PlaybookSummary, type ProjectSettings, type Provider, type ProxyCreate, type ProxyInfo, type RecoverOptions, Run, type RunData, type RunInfo, type RunResult, type SiteCredentials, type StartOptions, type StartResult, type StopResult, type SubmitOptions, Oya as default, file };
package/dist/index.js CHANGED
@@ -1,3 +1,44 @@
1
+ // src/constants.ts
2
+ var DEFAULT_BASE_URL = "https://oyabrowser.com";
3
+ var DEFAULT_TIMEOUT_MS = 6e4;
4
+ var START_TIMEOUT_MS = 12e4;
5
+ var READY_TIMEOUT_MS = 12e4;
6
+ var READY_POLL_MS = 2e3;
7
+ var NAVIGATE_TIMEOUT_MS = 12e4;
8
+ var CHALLENGE_TIMEOUT_MS = 18e4;
9
+ var AGENT_TIMEOUT_MS = 6e5;
10
+ var PLAYBOOK_TIMEOUT_MS = 12e4;
11
+ var STOP_TIMEOUT_MS = 6e4;
12
+ var WAIT_FOR_DEFAULT_MS = 3e4;
13
+ var WAIT_FOR_GRACE_MS = 5e3;
14
+ var AIMED_SCROLL_AMOUNT = 500;
15
+ var RUN_POLL_MS = 2e3;
16
+ var MAX_RUN_POLL_ERRORS = 5;
17
+ var BYTES_PER_MB = 1048576;
18
+ var MAX_FILE_MB = 10;
19
+ var BASE64_CHUNK_BYTES = 8192;
20
+ var MS_PER_SECOND = 1e3;
21
+ var LATIN1_MAX = 255;
22
+ var SPACE = 32;
23
+ var TAB = 9;
24
+ var DEL = 127;
25
+ var HEX = 16;
26
+ var CODE_POINT_DIGITS = 4;
27
+ var Status = {
28
+ /** The caller passed something unusable, such as a non-numeric element id. */
29
+ BAD_REQUEST: 400,
30
+ /** The server does not know that session. */
31
+ NOT_FOUND: 404,
32
+ /** The browser is in a state that needs attention first. */
33
+ CONFLICT: 409,
34
+ /** A browser command ran and failed. */
35
+ UNPROCESSABLE: 422,
36
+ /** A run failed without saying why. */
37
+ SERVER_ERROR: 500,
38
+ /** A browser did not come up in time. */
39
+ GATEWAY_TIMEOUT: 504
40
+ };
41
+
1
42
  // src/errors.ts
2
43
  var OyaError = class extends Error {
3
44
  /** The HTTP status, or the SDK's own status for errors it raises itself. */
@@ -12,6 +53,10 @@ var OyaError = class extends Error {
12
53
  this.body = body;
13
54
  }
14
55
  };
56
+ function refusal(message, field, suggestion) {
57
+ const body = { error: message, code: "invalid_request", field, ...suggestion ? { suggestion } : {} };
58
+ return new OyaError(message, Status.BAD_REQUEST, body);
59
+ }
15
60
 
16
61
  // src/cli-config.ts
17
62
  var node = () => globalThis.process;
@@ -35,58 +80,26 @@ function savedConfig() {
35
80
  }
36
81
  }
37
82
 
38
- // src/constants.ts
39
- var DEFAULT_BASE_URL = "https://oyabrowser.com";
40
- var DEFAULT_TIMEOUT_MS = 6e4;
41
- var START_TIMEOUT_MS = 12e4;
42
- var READY_TIMEOUT_MS = 12e4;
43
- var READY_POLL_MS = 2e3;
44
- var NAVIGATE_TIMEOUT_MS = 12e4;
45
- var CHALLENGE_TIMEOUT_MS = 18e4;
46
- var AGENT_TIMEOUT_MS = 6e5;
47
- var PLAYBOOK_TIMEOUT_MS = 12e4;
48
- var STOP_TIMEOUT_MS = 6e4;
49
- var WAIT_FOR_DEFAULT_MS = 3e4;
50
- var WAIT_FOR_GRACE_MS = 5e3;
51
- var AIMED_SCROLL_AMOUNT = 500;
52
- var RUN_POLL_MS = 2e3;
53
- var MAX_RUN_POLL_ERRORS = 5;
54
- var BYTES_PER_MB = 1048576;
55
- var MAX_FILE_MB = 10;
56
- var BASE64_CHUNK_BYTES = 8192;
57
- var MS_PER_SECOND = 1e3;
58
- var Status = {
59
- /** The caller passed something unusable, such as a non-numeric element id. */
60
- BAD_REQUEST: 400,
61
- /** The server does not know that session. */
62
- NOT_FOUND: 404,
63
- /** The browser is in a state that needs attention first. */
64
- CONFLICT: 409,
65
- /** A browser command ran and failed. */
66
- UNPROCESSABLE: 422,
67
- /** A run failed without saying why. */
68
- SERVER_ERROR: 500,
69
- /** A browser did not come up in time. */
70
- GATEWAY_TIMEOUT: 504
71
- };
72
-
73
83
  // src/client.ts
84
+ var OPTIONS_ORIGIN = { apiKeyFrom: "the apiKey option", baseUrlFrom: "the baseUrl option", savedKey: false };
74
85
  var Http = class {
75
- /** Stores where to call, with which key, how long to wait and which fetch to use. */
76
- constructor(baseUrl, apiKey, timeoutMs, fetchImpl) {
86
+ /** Stores where to call, with which key, how long to wait, which fetch to use, and where the key and address came from. */
87
+ constructor(baseUrl, apiKey, timeoutMs, fetchImpl, origin = OPTIONS_ORIGIN) {
77
88
  this.baseUrl = baseUrl;
78
89
  this.apiKey = apiKey;
79
90
  this.timeoutMs = timeoutMs;
80
91
  this.fetchImpl = fetchImpl;
92
+ this.origin = origin;
81
93
  }
82
94
  baseUrl;
83
95
  apiKey;
84
96
  timeoutMs;
85
97
  fetchImpl;
98
+ origin;
86
99
  /** Sends one request and returns the parsed answer, or throws an OyaError when it failed. */
87
100
  async request(method, path, body, timeoutMs = this.timeoutMs, headers = {}) {
88
101
  const res = await this.send(`${this.baseUrl}${path}`, this.init(method, body, timeoutMs, headers), timeoutMs);
89
- return readAnswer(res, `${method} ${path}`, this.baseUrl);
102
+ return readAnswer(res, `${method} ${path}`, this);
90
103
  }
91
104
  /** The fetch options for one call, with this client's key on them. */
92
105
  init(method, body, timeoutMs, headers) {
@@ -101,23 +114,34 @@ var Http = class {
101
114
  try {
102
115
  return await this.fetchImpl(url, init);
103
116
  } catch (err) {
104
- throw new OyaError(unreachable(this.baseUrl, timeoutMs, err), 0, { error: String(err?.message) });
117
+ throw noAnswer(this, timeoutMs, err);
105
118
  }
106
119
  }
107
120
  };
108
- function unreachable(baseUrl, timeoutMs, err) {
109
- const cause = err?.name === "TimeoutError" || /abort/i.test(String(err?.message));
110
- if (cause) return `No answer from ${baseUrl} within ${timeoutMs}ms. Is it reachable, and is the call this slow?`;
111
- return `Could not reach ${baseUrl}. Is the server running, and is OYA_BASE_URL right?`;
121
+ var timedOut = (err) => err?.name === "TimeoutError" || /abort/i.test(String(err?.message));
122
+ function causeOf(err) {
123
+ const cause = err?.cause;
124
+ return cause?.code ?? cause?.errors?.[0]?.code;
125
+ }
126
+ function noAnswer(http, timeoutMs, err) {
127
+ const code = timedOut(err) ? "timeout" : "unreachable";
128
+ const body = { error: String(err?.message), code, ...causeOf(err) ? { cause: causeOf(err) } : {} };
129
+ return new OyaError(unreachable(http, timeoutMs, code), 0, body);
130
+ }
131
+ function unreachable({ baseUrl, origin }, timeoutMs, code) {
132
+ if (code === "timeout")
133
+ return `No answer from ${baseUrl} within ${timeoutMs}ms. Is it reachable, and is the call this slow?`;
134
+ if (origin.baseUrlFrom === "default") return `Could not reach ${baseUrl}. Check your connection.`;
135
+ return `Could not reach ${baseUrl}. Is the server running, and is ${origin.baseUrlFrom} right?`;
112
136
  }
113
137
  function requestInit(method, body, timeoutMs, headers) {
114
138
  const json = body === void 0 ? {} : { "Content-Type": "application/json" };
115
139
  const payload = body === void 0 ? void 0 : JSON.stringify(body);
116
140
  return { method, headers: { ...headers, ...json }, body: payload, signal: AbortSignal.timeout(timeoutMs) };
117
141
  }
118
- async function readAnswer(res, call2, baseUrl) {
142
+ async function readAnswer(res, call2, http) {
119
143
  const payload = parseBody(await res.text());
120
- if (!res.ok) throw failure(call2, res.status, payload, baseUrl);
144
+ if (!res.ok) throw failure(call2, res.status, payload, http);
121
145
  return payload;
122
146
  }
123
147
  function parseBody(text) {
@@ -127,22 +151,74 @@ function parseBody(text) {
127
151
  return text;
128
152
  }
129
153
  }
130
- function failure(call2, status, payload, baseUrl) {
131
- let message = payload?.error || `${call2} failed (${status})`;
132
- if (message === "Invalid API key") {
133
- message += ` for ${baseUrl}. Check OYA_API_KEY: a value exported in your shell beats .env.`;
134
- }
135
- return new OyaError(message, status, payload);
154
+ function failure(call2, status, payload, http) {
155
+ const message = payload?.error || `${call2} failed (${status})`;
156
+ return new OyaError(message === "Invalid API key" ? invalidKey(http) : message, status, payload);
157
+ }
158
+ function invalidKey({ baseUrl, origin }) {
159
+ const overrides = origin.apiKeyFrom === "OYA_API_KEY" && origin.savedKey;
160
+ const rest = overrides ? ", which overrides the one saved in ~/.oya/config.json" : "";
161
+ return `Invalid API key for ${baseUrl}. The key came from ${origin.apiKeyFrom}${rest}.`;
136
162
  }
137
163
  var env = (name) => globalThis.process?.env?.[name];
164
+ function firstOf(option, name, saved, optionName) {
165
+ if (option) return { value: option, from: `the ${optionName} option` };
166
+ if (env(name)) return { value: env(name), from: name };
167
+ return { value: saved, from: "~/.oya/config.json" };
168
+ }
169
+ function addressOf(options, saved) {
170
+ const found = firstOf(options.baseUrl, "OYA_BASE_URL", saved, "baseUrl");
171
+ return found.value ? found : { value: DEFAULT_BASE_URL, from: "default" };
172
+ }
173
+ function keyOf(options, saved) {
174
+ const found = firstOf(options.apiKey, "OYA_API_KEY", saved, "apiKey");
175
+ if (!found.value) throw new Error("No API key. Pass { apiKey }, set OYA_API_KEY, or run `npx @oya-ai/cli login`.");
176
+ return { ...found, value: headerSafe(found.value, "apiKey", "Copy the key again.") };
177
+ }
138
178
  function createHttp(options) {
139
179
  const saved = savedConfig();
140
- const apiKey = options.apiKey || env("OYA_API_KEY") || saved.apiKey;
141
- if (!apiKey) throw new Error("No API key. Pass { apiKey }, set OYA_API_KEY, or run `npx @oya-ai/cli login`.");
142
- const baseUrl = (options.baseUrl || env("OYA_BASE_URL") || saved.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
180
+ const key = keyOf(options, saved.apiKey);
181
+ const address = addressOf(options, saved.baseUrl);
143
182
  const fetchImpl = options.fetch || globalThis.fetch;
144
183
  if (!fetchImpl) throw new Error("No fetch available, pass { fetch } or use Node 18+.");
145
- return new Http(baseUrl, apiKey, options.timeoutMs ?? DEFAULT_TIMEOUT_MS, fetchImpl.bind(globalThis));
184
+ const origin = { apiKeyFrom: key.from, baseUrlFrom: address.from, savedKey: !!saved.apiKey };
185
+ const timeout = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
186
+ return new Http(webAddress(address), key.value, timeout, fetchImpl.bind(globalThis), origin);
187
+ }
188
+ var LOCAL_HOST = /^(localhost|127\.)/i;
189
+ function webAddress({ value, from }) {
190
+ const url = value.trim().replace(/\/+$/, "");
191
+ if (/^https?:\/\//i.test(url)) return url;
192
+ const name = from.startsWith("the ") ? "baseUrl" : from === "OYA_BASE_URL" ? from : `baseUrl in ${from}`;
193
+ const suggestion = suggestionFor(url);
194
+ const tryIt = suggestion ? ` Try ${suggestion}.` : "";
195
+ throw refusal(`${name} must start with http:// or https://, not "${url}".${tryIt}`, "baseUrl", suggestion);
196
+ }
197
+ function suggestionFor(url) {
198
+ if (/^[a-z][a-z0-9+.-]*:/i.test(url) && !/^[^:/]+:\d+(\/|$)/.test(url)) return void 0;
199
+ return `${LOCAL_HOST.test(url) ? "http" : "https"}://${url}`;
200
+ }
201
+ function notInAHeader(ch) {
202
+ const code = ch.codePointAt(0);
203
+ return code < SPACE && code !== TAB || code === DEL || code > LATIN1_MAX;
204
+ }
205
+ function headerSafe(value, field, ending) {
206
+ const trimmed = value.trim();
207
+ const chars = Array.from(trimmed);
208
+ const at = chars.findIndex(notInAHeader);
209
+ if (at < 0) return trimmed;
210
+ const leading = Array.from(value).length - Array.from(value.trimStart()).length;
211
+ const where = `position ${leading + at + 1} (${codePoint(chars[at])})`;
212
+ throw refusal(`${field} has a character HTTP headers cannot carry at ${where}. ${ending}`, field);
213
+ }
214
+ var codePoint = (ch) => `U+${ch.codePointAt(0).toString(HEX).toUpperCase().padStart(CODE_POINT_DIGITS, "0")}`;
215
+ var shown = (value) => typeof value === "string" ? JSON.stringify(value) : String(value);
216
+ function segment(value, field = "id") {
217
+ if (typeof value !== "string" || !value)
218
+ throw refusal(`${field} must be a non-empty string, not ${shown(value)}`, field);
219
+ if (value.includes("/") || value === "." || value === "..")
220
+ throw refusal(`${field} must be a single path segment (no "/" and not "." or ".."), not ${shown(value)}`, field);
221
+ return encodeURIComponent(value);
146
222
  }
147
223
 
148
224
  // src/run-watch.ts
@@ -381,7 +457,7 @@ var Browser = class {
381
457
  * Play `'<name>:draft'` to try a draft before promoting it.
382
458
  */
383
459
  async play(name, data = {}, { autoHeal = true } = {}) {
384
- const path = `/api/browsers/${this.id}/playbooks/${encodeURIComponent(name)}/play`;
460
+ const path = `/api/browsers/${this.id}/playbooks/${segment(name, "name")}/play`;
385
461
  return agentAnswer(
386
462
  await this.http.request("POST", path, { variables: data, autoHeal }, AGENT_TIMEOUT_MS)
387
463
  );
@@ -432,7 +508,7 @@ var Browser = class {
432
508
  }
433
509
  /** Revoke a link from `shareUrl()` before it expires, by the id it returned. */
434
510
  async revokeShare(id) {
435
- await this.http.request("DELETE", `/api/control/credentials/${encodeURIComponent(id)}`);
511
+ await this.http.request("DELETE", `/api/control/credentials/${segment(id)}`);
436
512
  }
437
513
  /** Counters, health and the last 50 things this browser did. */
438
514
  status() {
@@ -488,8 +564,9 @@ function startBody(options) {
488
564
  const { profile, persona, provider, wsUrl, name, queueMs, priority, budgetUsd, governed, policy } = options;
489
565
  return { profile: profile || persona, provider, wsUrl, name, queueMs, priority, budgetUsd, governed, policy };
490
566
  }
567
+ var idempotencyKeyOf = ({ idempotencyKey }) => idempotencyKey ? headerSafe(idempotencyKey, "idempotencyKey", 'Use letters, digits, "-" and "_".') : globalThis.crypto.randomUUID();
491
568
  async function start(http, wait, options) {
492
- const headers = { "Idempotency-Key": options.idempotencyKey || globalThis.crypto.randomUUID() };
569
+ const headers = { "Idempotency-Key": idempotencyKeyOf(options) };
493
570
  const path = "/api/browsers/start";
494
571
  const started = await http().request("POST", path, startBody(options), START_TIMEOUT_MS, headers);
495
572
  if (started.status === "starting") {
@@ -498,7 +575,7 @@ async function start(http, wait, options) {
498
575
  }
499
576
  return new Browser(http(), started, options.captcha === "auto");
500
577
  }
501
- var fetchBrowser = (http, id) => http().request("GET", `/api/browsers/${encodeURIComponent(id)}`);
578
+ var fetchBrowser = (http, id) => http().request("GET", `/api/browsers/${segment(id)}`);
502
579
  async function reattach(http, id) {
503
580
  const found = await fetchBrowser(http, id);
504
581
  return new Browser(http(), asStarted(found), false);
@@ -510,69 +587,79 @@ function asStarted(found) {
510
587
  var stopBrowsers = (http, ids) => http().request("POST", "/api/browsers/stop", ids === "all" ? { all: true } : { ids }, START_TIMEOUT_MS);
511
588
  var browserApi = (http, wait) => ({
512
589
  /** Start a browser and wait until it can take commands. */
513
- start: (options = {}) => start(http, wait, options),
590
+ start: async (options = {}) => start(http, wait, options),
514
591
  /** Reattach to a browser that is already running. */
515
- get: (id) => reattach(http, id),
592
+ get: async (id) => reattach(http, id),
516
593
  /** Every browser on this key. */
517
- list: () => http().request("GET", "/api/browsers"),
594
+ list: async () => http().request("GET", "/api/browsers"),
518
595
  /** Stop some (`ids`) or every browser on this key. Each reports separately. */
519
- stop: (ids) => stopBrowsers(http, ids),
596
+ stop: async (ids) => stopBrowsers(http, ids),
520
597
  /** Stop every browser on this key; returns how many stopped. */
521
598
  stopAll: async () => (await stopBrowsers(http, "all")).stopped
522
599
  });
523
600
 
524
601
  // src/api/control.ts
525
- var session = (id, action = "") => `/api/control/sessions/${encodeURIComponent(id)}${action}`;
526
- var item = (kind, id) => `/api/control/${kind}/${encodeURIComponent(id)}`;
602
+ var session = (id, action = "") => `/api/control/sessions/${segment(id)}${action}`;
603
+ var item = (kind, id) => `/api/control/${kind}/${segment(id)}`;
527
604
  var sessionCalls = (http) => ({
528
605
  /** Settings, sessions and recent events at a glance. */
529
- overview: () => http().request("GET", "/api/control"),
606
+ overview: async () => http().request("GET", "/api/control"),
530
607
  /** Every session, including disconnected and cleanup-pending ones. */
531
- sessions: () => http().request("GET", "/api/control/sessions"),
608
+ sessions: async () => http().request("GET", "/api/control/sessions"),
532
609
  /** One session. */
533
- session: (id) => http().request("GET", session(id)),
610
+ session: async (id) => http().request("GET", session(id)),
534
611
  /** Update limits, rate cards and retention. */
535
- settings: (changes) => http().request("PATCH", "/api/control/project", changes)
612
+ settings: async (changes) => http().request("PATCH", "/api/control/project", changes)
536
613
  });
537
614
  var lifecycleCalls = (http) => ({
538
- /** Cancel queued or provisioning work. */
539
- cancel: (id) => http().request("POST", session(id, "/cancel"), {}),
615
+ /** Stop a session in any state; answers its final state. */
616
+ cancel: async (id) => http().request("POST", session(id, "/cancel"), {}),
540
617
  /** Stop a session; `force` stops despite a profile-save error, or reconciles. */
541
- stop: (id, force = false) => http().request("POST", session(id, "/stop"), { force }),
618
+ stop: async (id, force = false) => http().request("POST", session(id, "/stop"), { force }),
542
619
  /** Acquire or release human control, or acknowledge the agent's resume. */
543
- takeover: (id, action) => http().request("POST", session(id, "/control"), { action }),
620
+ takeover: async (id, action) => http().request("POST", session(id, "/control"), { action }),
544
621
  /** Send one input as the human holding the control lease. */
545
- input: (id, action, params) => http().request("POST", session(id, "/input"), { action, params })
622
+ input: async (id, action, params) => http().request("POST", session(id, "/input"), { action, params })
546
623
  });
624
+ function recoverBody(options) {
625
+ const { replace = false, wsUrl } = typeof options === "boolean" ? { replace: options } : options ?? {};
626
+ if (wsUrl && !replace)
627
+ throw refusal("wsUrl is only used with replace: true; recovering in place keeps the endpoint it had.", "wsUrl");
628
+ return wsUrl ? { replace, wsUrl } : { replace };
629
+ }
547
630
  var recoveryCalls = (http) => ({
548
- /** Explicitly recover a session, or replace it with a fresh one. */
549
- recover: (id, replace = false) => http().request("POST", session(id, "/recover"), { replace }),
631
+ /**
632
+ * Explicitly recover a session, or replace it with a fresh one. A cdp
633
+ * session's replacement needs the Chrome it runs on: `{ replace: true, wsUrl }`.
634
+ * `recover(id, true)` still means replace.
635
+ */
636
+ recover: async (id, options = {}) => http().request("POST", session(id, "/recover"), recoverBody(options)),
550
637
  /** A single-use ticket for the live stream. */
551
- ticket: (id) => http().request("POST", session(id, "/ticket"), {}),
638
+ ticket: async (id) => http().request("POST", session(id, "/ticket"), {}),
552
639
  /** Durable lifecycle events after a cursor. */
553
- events: (after = 0) => http().request("GET", `/api/control/events?after=${after}`)
640
+ events: async (after = 0) => http().request("GET", `/api/control/events?after=${after}`)
554
641
  });
555
642
  var credentialCalls = (http) => ({
556
643
  /** Mint a service credential. Its token is returned this once. */
557
- createCredential: (options) => http().request("POST", "/api/control/credentials", options),
644
+ createCredential: async (options) => http().request("POST", "/api/control/credentials", options),
558
645
  /** Revoke a service credential. */
559
- revokeCredential: (id) => http().request("DELETE", item("credentials", id))
646
+ revokeCredential: async (id) => http().request("DELETE", item("credentials", id))
560
647
  });
561
648
  var memberCalls = (http) => ({
562
649
  /** The owner and every member. */
563
- members: () => http().request("GET", "/api/control/members"),
650
+ members: async () => http().request("GET", "/api/control/members"),
564
651
  /** An invitation code for a new member. */
565
- inviteMember: (role = "operator") => http().request("POST", "/api/control/members/invite", { role }),
652
+ inviteMember: async (role = "operator") => http().request("POST", "/api/control/members/invite", { role }),
566
653
  /** Remove a member. */
567
- removeMember: (userId) => http().request("DELETE", item("members", userId))
654
+ removeMember: async (userId) => http().request("DELETE", item("members", userId))
568
655
  });
569
656
  var webhookCalls = (http) => ({
570
657
  /** Register a webhook; `types` limits which events it receives. */
571
- createWebhook: (url, types = []) => http().request("POST", "/api/control/webhooks", { url, types }),
658
+ createWebhook: async (url, types = []) => http().request("POST", "/api/control/webhooks", { url, types }),
572
659
  /** Disable a webhook. */
573
- removeWebhook: (id) => http().request("DELETE", item("webhooks", id)),
660
+ removeWebhook: async (id) => http().request("DELETE", item("webhooks", id)),
574
661
  /** Send a delivery again. */
575
- replayDelivery: (id) => http().request("POST", `${item("deliveries", id)}/replay`, {})
662
+ replayDelivery: async (id) => http().request("POST", `${item("deliveries", id)}/replay`, {})
576
663
  });
577
664
  var controlApi = (http) => ({
578
665
  ...sessionCalls(http),
@@ -584,7 +671,7 @@ var controlApi = (http) => ({
584
671
  });
585
672
 
586
673
  // src/api/playbooks.ts
587
- var playbook = (name) => `/api/playbooks/${encodeURIComponent(name)}`;
674
+ var playbook = (name) => `/api/playbooks/${segment(name, "name")}`;
588
675
  var playbookApi = (http) => ({
589
676
  /** Every saved playbook, with any draft waiting on it. */
590
677
  list: async () => (await http().request("GET", "/api/playbooks")).playbooks,
@@ -593,7 +680,7 @@ var playbookApi = (http) => ({
593
680
  await http().request("DELETE", playbook(name));
594
681
  },
595
682
  /** Replace a playbook with the draft a healed replay saved. Try it first with `browser.play('<name>:draft')`. */
596
- promote: (name) => http().request("POST", `${playbook(name)}/promote`, {})
683
+ promote: async (name) => http().request("POST", `${playbook(name)}/promote`, {})
597
684
  });
598
685
 
599
686
  // src/api/proxies.ts
@@ -601,10 +688,10 @@ var proxyApi = (http) => ({
601
688
  /** Every proxy this key can use, shared ones included. */
602
689
  list: async () => (await http().request("GET", "/api/proxies")).proxies,
603
690
  /** Add a proxy. Its credentials are never read back. */
604
- create: (proxy) => http().request("POST", "/api/proxies", proxy),
691
+ create: async (proxy) => http().request("POST", "/api/proxies", proxy),
605
692
  /** Remove one of this key's proxies. */
606
693
  remove: async (id) => {
607
- await http().request("DELETE", `/api/proxies/${encodeURIComponent(id)}`);
694
+ await http().request("DELETE", `/api/proxies/${segment(id)}`);
608
695
  },
609
696
  /** Dial each proxy and learn its real exit IP. Failing ones cool down and are skipped. */
610
697
  check: async () => (await http().request("POST", "/api/proxies/check", {})).results
@@ -615,35 +702,38 @@ var identityCalls = (http) => ({
615
702
  /** Every persona on this key. */
616
703
  list: async () => (await http().request("GET", "/api/personas")).personas,
617
704
  /** One persona. */
618
- get: (id) => http().request("GET", `/api/personas/${id}`),
705
+ get: async (id) => http().request("GET", `/api/personas/${segment(id)}`),
619
706
  /**
620
707
  * Create an identity. The device, platform, timezone, locale, is chosen
621
708
  * here and fixed for its life; `preview()` shows what a choice produces.
622
709
  */
623
- create: (options = {}) => http().request("POST", "/api/personas", options),
710
+ create: async (options = {}) => http().request("POST", "/api/personas", options),
624
711
  /** Name, concurrency cap and proxy hint. Never the device, clone for that. */
625
- update: (id, changes) => http().request("PUT", `/api/personas/${id}`, changes),
712
+ update: async (id, changes) => http().request("PUT", `/api/personas/${segment(id)}`, changes),
626
713
  /** A new persona of the same kind of device: same choices, fresh identity, empty jar. */
627
- clone: (id, options = {}) => http().request("POST", `/api/personas/${id}/clone`, options)
714
+ clone: async (id, options = {}) => http().request("POST", `/api/personas/${segment(id)}/clone`, options)
628
715
  });
629
716
  var deviceCalls = (http) => ({
630
717
  /** The fingerprint these choices would produce. Persists nothing. */
631
718
  preview: async (prefs = {}) => (await http().request("POST", "/api/personas/preview", { prefs })).fingerprint,
632
719
  /** Platforms, and the timezones and locales each may coherently claim. */
633
- options: () => http().request("GET", "/api/personas/options"),
720
+ options: async () => http().request("GET", "/api/personas/options"),
634
721
  /** Pin the persona to one of your proxies, or `null` to let assignment happen at connect. */
635
- pinProxy: (id, proxyId) => http().request("PUT", `/api/personas/${id}/proxy`, { proxyId }),
722
+ pinProxy: async (id, proxyId) => http().request("PUT", `/api/personas/${segment(id)}/proxy`, { proxyId }),
636
723
  /** Delete a persona. */
637
724
  remove: async (id) => {
638
- await http().request("DELETE", `/api/personas/${id}`);
725
+ await http().request("DELETE", `/api/personas/${segment(id)}`);
639
726
  }
640
727
  });
641
728
  var mfaCalls = (http) => ({
642
729
  /** Store the second factor for this identity. Sealed at rest, never read back. */
643
- setMfa: (id, config) => http().request("PUT", `/api/personas/${id}/mfa`, config),
730
+ setMfa: async (id, config) => http().request("PUT", `/api/personas/${segment(id)}/mfa`, config),
644
731
  /** Remove the persona-wide factor, or the one filed against `domain`. */
645
732
  clearMfa: async (id, domain) => {
646
- await http().request("DELETE", `/api/personas/${id}/mfa${domain ? `?domain=${encodeURIComponent(domain)}` : ""}`);
733
+ await http().request(
734
+ "DELETE",
735
+ `/api/personas/${segment(id)}/mfa${domain ? `?domain=${encodeURIComponent(domain)}` : ""}`
736
+ );
647
737
  }
648
738
  });
649
739
  var loginCalls = (http) => ({
@@ -654,20 +744,20 @@ var loginCalls = (http) => ({
654
744
  * better path. This is for portals that expire a session server-side
655
745
  * between runs, where an unattended run has nothing else to recover with.
656
746
  */
657
- setCredentials: (id, config) => http().request("PUT", `/api/personas/${id}/credentials`, config),
747
+ setCredentials: async (id, config) => http().request("PUT", `/api/personas/${segment(id)}/credentials`, config),
658
748
  /** Which sites this identity can sign in to. Usernames only. */
659
- credentials: (id) => http().request("GET", `/api/personas/${id}/credentials`),
749
+ credentials: async (id) => http().request("GET", `/api/personas/${segment(id)}/credentials`),
660
750
  /** Remove the login stored for one site. */
661
751
  clearCredentials: async (id, domain) => {
662
- await http().request("DELETE", `/api/personas/${id}/credentials?domain=${encodeURIComponent(domain)}`);
752
+ await http().request("DELETE", `/api/personas/${segment(id)}/credentials?domain=${encodeURIComponent(domain)}`);
663
753
  }
664
754
  });
665
- var jarPath = (id) => `/api/pool/cookies?persona=${encodeURIComponent(id)}`;
755
+ var jarPath = (id) => `/api/pool/cookies?persona=${segment(id)}`;
666
756
  var jarCalls = (http) => ({
667
757
  /** Every cookie in the jar; `format: 'playwright'` is ready for `context.addCookies()`. */
668
- cookies: async (id, format = "json") => (await http().request("GET", `${jarPath(id)}&format=${format}`)).cookies,
758
+ cookies: async (id, format = "json") => (await http().request("GET", `${jarPath(id)}&format=${encodeURIComponent(format)}`)).cookies,
669
759
  /** Merge cookies into the jar. The persona's browsers pick them up on their next visit to each site. */
670
- importCookies: (id, list) => http().request("PUT", jarPath(id), { cookies: list })
760
+ importCookies: async (id, list) => http().request("PUT", jarPath(id), { cookies: list })
671
761
  });
672
762
  var cookieCalls = (http) => ({
673
763
  ...jarCalls(http),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oya-ai/browser",
3
- "version": "1.0.110",
3
+ "version": "1.0.111",
4
4
  "description": "Rotate thousands of browsers behind one API, personas, proxies, stealth, CAPTCHA and MFA.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://oyabrowser.com",