@oya-ai/browser 1.0.103 → 1.0.105
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 +200 -193
- package/dist/index.cjs +60 -17
- package/dist/index.d.cts +29 -12
- package/dist/index.d.ts +29 -12
- package/dist/index.js +60 -17
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,49 +1,54 @@
|
|
|
1
1
|
# @oya-ai/browser
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<strong>
|
|
4
|
+
<strong>A real browser your agents drive from the inside. Record a run once, replay it with no model in the loop.</strong>
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<a href="https://www.npmjs.com/package/@oya-ai/browser"><img src="https://img.shields.io/npm/v/@oya-ai/browser?color=39ed35&label=@oya-ai/browser&logo=npm" alt="NPM Version"></a>
|
|
9
9
|
<a href="https://github.com/OyadotAI/oya-browser/blob/main/packages/sdk/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg?color=39ed35" alt="License: MIT"></a>
|
|
10
10
|
<a href="https://www.typescriptlang.org"><img src="https://img.shields.io/badge/TypeScript-Ready-3178C6.svg?logo=typescript&logoColor=white" alt="TypeScript"></a>
|
|
11
|
-
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%
|
|
11
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D20.0.0-brightgreen.svg" alt="Node Version"></a>
|
|
12
12
|
<a href="https://bundlephobia.com/package/@oya-ai/browser"><img src="https://img.shields.io/bundlephobia/minzip/@oya-ai/browser?color=39ed35" alt="Bundle Size"></a>
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Oya does for browser vendors what OpenRouter does for LLM providers. The vendor behind a browser is a setting on your API key, or one `provider` parameter, so switching vendors never means rewriting your agent.
|
|
17
|
+
## From nothing to an answer
|
|
20
18
|
|
|
21
19
|
```bash
|
|
22
20
|
npm install @oya-ai/browser
|
|
23
21
|
```
|
|
24
22
|
|
|
25
|
-
|
|
23
|
+
```js
|
|
24
|
+
import { Oya } from '@oya-ai/browser';
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
const browser = await new Oya().browser.start();
|
|
27
|
+
await browser.goto('https://news.ycombinator.com');
|
|
28
|
+
console.log(await browser.ask('What are the top 3 stories?'));
|
|
29
|
+
```
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
Two things to have first: an API key from [oyabrowser.com](https://oyabrowser.com)
|
|
32
|
+
(or your own deployment) in `OYA_API_KEY`, and a browser to drive.
|
|
30
33
|
|
|
31
|
-
|
|
34
|
+
**The fastest browser to have is one you already have.** Open the
|
|
35
|
+
[Oya desktop browser](https://oyabrowser.com) and sign in with the same key: `start()`
|
|
36
|
+
hands over the browser that is already connected when no provider is configured, so the
|
|
37
|
+
five lines above work with nothing else set up. `stop()` leaves a browser you borrowed
|
|
38
|
+
alone, because you did not start it.
|
|
32
39
|
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
When you would rather it start one for you, run `npx @oya-ai/cli init` and pick where
|
|
41
|
+
browsers run: Oya Cloud, your own Docker, Browserbase, Steel, Anchor, Browser Use, or a
|
|
42
|
+
Chrome of your own over CDP. Your code does not change.
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
console.log(await browser.ask("What is the main heading on this page?"));
|
|
41
|
-
} finally {
|
|
42
|
-
await browser.stop();
|
|
43
|
-
}
|
|
44
|
-
```
|
|
44
|
+
**Where the key comes from**, in order: `new Oya({ apiKey })`, then `OYA_API_KEY`, then the
|
|
45
|
+
file `oya login` wrote (`~/.oya/config.json`, or `OYA_CONFIG_HOME`), which is read on Node
|
|
46
|
+
22.3 and newer. `baseUrl` resolves the same way. A CI job that sets neither fails loudly
|
|
47
|
+
rather than borrowing whatever is on the machine.
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
Node.js 20 or newer; the examples are ES modules. `OYA_BASE_URL` points at a self-hosted
|
|
50
|
+
control plane. On Node.js 24+, `await using browser = await oya.browser.start()` stops it
|
|
51
|
+
when the scope exits, errors included.
|
|
47
52
|
|
|
48
53
|
## Portal automation: record once, replay with new inputs
|
|
49
54
|
|
|
@@ -52,8 +57,8 @@ This example adapts the portal-automation project's workflow: reuse a persona, a
|
|
|
52
57
|
Save as `portal.mjs` and run `node portal.mjs` after setting `OYA_API_KEY`, `PORTAL_URL`, `PORTAL_USERNAME`, and `PORTAL_PASSWORD`. Set `OYA_BROWSER_ID` only to reuse an already running browser.
|
|
53
58
|
|
|
54
59
|
```js
|
|
55
|
-
import { createInterface } from
|
|
56
|
-
import { Oya } from
|
|
60
|
+
import { createInterface } from 'node:readline/promises';
|
|
61
|
+
import { Oya } from '@oya-ai/browser';
|
|
57
62
|
|
|
58
63
|
function requiredEnv(name) {
|
|
59
64
|
const value = process.env[name];
|
|
@@ -61,79 +66,76 @@ function requiredEnv(name) {
|
|
|
61
66
|
return value;
|
|
62
67
|
}
|
|
63
68
|
|
|
64
|
-
const oya = new Oya({ apiKey: requiredEnv(
|
|
65
|
-
const portalUrl = requiredEnv(
|
|
66
|
-
const playbookName =
|
|
69
|
+
const oya = new Oya({ apiKey: requiredEnv('OYA_API_KEY') });
|
|
70
|
+
const portalUrl = requiredEnv('PORTAL_URL');
|
|
71
|
+
const playbookName = 'portal-request-review';
|
|
67
72
|
const secrets = {
|
|
68
|
-
username: requiredEnv(
|
|
69
|
-
password: requiredEnv(
|
|
73
|
+
username: requiredEnv('PORTAL_USERNAME'),
|
|
74
|
+
password: requiredEnv('PORTAL_PASSWORD'),
|
|
70
75
|
};
|
|
71
76
|
// Fictional test inputs. data is visible to the agent.
|
|
72
77
|
const data = {
|
|
73
|
-
customerName:
|
|
74
|
-
requestId:
|
|
75
|
-
requestedDate:
|
|
78
|
+
customerName: 'Alex Example',
|
|
79
|
+
requestId: 'DEMO-0001',
|
|
80
|
+
requestedDate: '2030-01-15',
|
|
76
81
|
};
|
|
77
82
|
const task = [
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
].join(
|
|
83
|
+
'If not logged in, log in with {{username}} and {{password}}.',
|
|
84
|
+
'Open New Request and enter {{requestId}} as the reference.',
|
|
85
|
+
'Fill first name {{customerName|first}} and last name {{customerName|last}}.',
|
|
86
|
+
'Set the requested date to {{requestedDate|date:MM/DD/YYYY}}.',
|
|
87
|
+
'If a field is already correct, do not type its value again.',
|
|
88
|
+
'If an action times out, inspect the page before retrying it.',
|
|
89
|
+
'If information is missing, ask the person instead of guessing.',
|
|
90
|
+
'Stop on the review page. Do not submit the request.',
|
|
91
|
+
].join('\n');
|
|
87
92
|
|
|
88
93
|
const existingId = process.env.OYA_BROWSER_ID;
|
|
89
94
|
let browser;
|
|
90
95
|
if (existingId) {
|
|
91
96
|
browser = await oya.browser.get(existingId);
|
|
92
97
|
} else {
|
|
93
|
-
const persona =
|
|
94
|
-
.find((p) => p.name ===
|
|
95
|
-
|
|
96
|
-
browser = await oya.browser.start({ persona: persona.id, captcha:
|
|
98
|
+
const persona =
|
|
99
|
+
(await oya.personas.list()).find((p) => p.name === 'portal-demo') ??
|
|
100
|
+
(await oya.personas.create({ name: 'portal-demo' }));
|
|
101
|
+
browser = await oya.browser.start({ persona: persona.id, captcha: 'auto' });
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
try {
|
|
100
|
-
console.log(
|
|
105
|
+
console.log('Watch in your Oya dashboard:', browser.liveViewUrl());
|
|
101
106
|
await browser.goto(portalUrl);
|
|
102
107
|
const exists = (await oya.playbooks.list()).some((p) => p.name === playbookName);
|
|
103
|
-
const run = await browser.submit(
|
|
104
|
-
|
|
105
|
-
{
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
terminal.close();
|
|
125
|
-
}
|
|
126
|
-
},
|
|
108
|
+
const run = await browser.submit(exists ? { playbook: playbookName } : { prompt: task }, {
|
|
109
|
+
// Replay accepts all variables in data; the playbook remembers secret names.
|
|
110
|
+
...(exists ? { data: { ...data, ...secrets } } : { data, secrets }),
|
|
111
|
+
onSuccess: () => console.log('Run succeeded.'),
|
|
112
|
+
onFailure: (error) => console.error('Run failed with status:', error.status),
|
|
113
|
+
onHealed: (result) => {
|
|
114
|
+
console.log('A repair draft is ready for review:', result.draft);
|
|
115
|
+
},
|
|
116
|
+
onHumanAttention: async (request) => {
|
|
117
|
+
console.log('Attention needed:', request.reason);
|
|
118
|
+
console.log(request.message);
|
|
119
|
+
console.log('Open:', request.liveViewUrl ?? browser.liveViewUrl());
|
|
120
|
+
const terminal = createInterface({ input: process.stdin, output: process.stdout });
|
|
121
|
+
try {
|
|
122
|
+
const answer = await terminal.question(
|
|
123
|
+
request.reason === 'agent' ? 'Answer the agent: ' : 'Handle this in the live view, then press Enter: ',
|
|
124
|
+
);
|
|
125
|
+
await request.respond(answer || 'done');
|
|
126
|
+
} finally {
|
|
127
|
+
terminal.close();
|
|
128
|
+
}
|
|
127
129
|
},
|
|
128
|
-
);
|
|
130
|
+
});
|
|
129
131
|
|
|
130
132
|
await run.done; // Rejects on failure; do not save a failed run as a playbook.
|
|
131
133
|
const info = await run.status();
|
|
132
|
-
console.log(
|
|
134
|
+
console.log('Run status:', info.status);
|
|
133
135
|
if (!exists) {
|
|
134
136
|
const playbook = await browser.toPlaybook(playbookName);
|
|
135
|
-
console.log(
|
|
136
|
-
console.log(
|
|
137
|
+
console.log('Saved:', playbook.name, 'Steps:', playbook.steps);
|
|
138
|
+
console.log('Variables:', playbook.variables);
|
|
137
139
|
// playbook.code contains the flow as an exported Playwright module.
|
|
138
140
|
}
|
|
139
141
|
} finally {
|
|
@@ -152,22 +154,22 @@ For prompt runs, pass credentials in `secrets`. For replay, pass all variables i
|
|
|
152
154
|
|
|
153
155
|
### Files
|
|
154
156
|
|
|
155
|
-
`file()` puts a file in `data`. The agent attaches it with its upload tool: hand it the element id of whatever you can see
|
|
157
|
+
`file()` puts a file in `data`. The agent attaches it with its upload tool: hand it the element id of whatever you can see (the "Choose file" button, the drop zone, the field itself) and the real `<input type="file">` is found from there, including the hidden ones most upload widgets use.
|
|
156
158
|
|
|
157
159
|
```js
|
|
158
|
-
import { Oya, file } from
|
|
160
|
+
import { Oya, file } from '@oya-ai/browser';
|
|
159
161
|
|
|
160
|
-
await browser.ask(
|
|
161
|
-
data: { name:
|
|
162
|
+
await browser.ask('Attach my resume to the application and submit it', {
|
|
163
|
+
data: { name: 'Ada Lovelace', resume: await file('./cv.pdf') },
|
|
162
164
|
});
|
|
163
165
|
```
|
|
164
166
|
|
|
165
|
-
A string argument is a path on disk (Node only); a `Blob`, a `File`, or a `Uint8Array` works anywhere. `name` sets the filename the site sees and `type` overrides the MIME guessed from the extension. The ceiling is 10MB per file, and the bytes travel inline with the run
|
|
167
|
+
A string argument is a path on disk (Node only); a `Blob`, a `File`, or a `Uint8Array` works anywhere. `name` sets the filename the site sees and `type` overrides the MIME guessed from the extension. The ceiling is 10MB per file, and the bytes travel inline with the run. Nothing is stored server-side after it ends.
|
|
166
168
|
|
|
167
169
|
Files work in `data` for `ask()`, `submit()`, and `play()`; `secrets` rejects them, because a file is never typed through a placeholder. A run recorded with `toPlaybook()` keeps the upload as a variable, so the replay takes a different file:
|
|
168
170
|
|
|
169
171
|
```js
|
|
170
|
-
await browser.play(
|
|
172
|
+
await browser.play('job-application', { name: 'Ada Lovelace', resume: await file('./other.pdf') });
|
|
171
173
|
```
|
|
172
174
|
|
|
173
175
|
The generated Playwright module calls `setInputFiles`, where the same variable is a plain path rather than a `file()` value.
|
|
@@ -179,8 +181,7 @@ Replay normally runs recorded steps without an LLM. With `autoHeal: true` (the d
|
|
|
179
181
|
The following continues with an active `browser` and the inputs above. Replaying a draft performs its actions, so review its code and use test inputs before promoting it.
|
|
180
182
|
|
|
181
183
|
```js
|
|
182
|
-
const saved = (await oya.playbooks.list())
|
|
183
|
-
.find((p) => p.name === playbookName);
|
|
184
|
+
const saved = (await oya.playbooks.list()).find((p) => p.name === playbookName);
|
|
184
185
|
if (saved?.draft) {
|
|
185
186
|
// Review saved.draft.code before executing it.
|
|
186
187
|
await browser.play(`${playbookName}:draft`, { ...data, ...secrets }, { autoHeal: false });
|
|
@@ -195,13 +196,13 @@ Use `autoHeal: false` with `play()` or `submit({ playbook: name }, options)` to
|
|
|
195
196
|
As in the portal-automation project's model setup script, configure the model on your Oya API key once for subsequent agent runs:
|
|
196
197
|
|
|
197
198
|
```js
|
|
198
|
-
import { Oya } from
|
|
199
|
+
import { Oya } from '@oya-ai/browser';
|
|
199
200
|
|
|
200
201
|
const modelKey = process.env.GEMINI_API_KEY;
|
|
201
|
-
if (!modelKey) throw new Error(
|
|
202
|
+
if (!modelKey) throw new Error('Set GEMINI_API_KEY first.');
|
|
202
203
|
const oya = new Oya();
|
|
203
204
|
await oya.config.set({
|
|
204
|
-
llm_provider:
|
|
205
|
+
llm_provider: 'gemini', // LlmProvider: "openai" | "anthropic" | "gemini" | "vertex"
|
|
205
206
|
openai_api_key: modelKey, // Shared field name for every supported provider.
|
|
206
207
|
// chat_model: process.env.OYA_CHAT_MODEL, // Optional provider model override.
|
|
207
208
|
});
|
|
@@ -210,13 +211,13 @@ await oya.config.set({
|
|
|
210
211
|
`config.set` takes `ConfigUpdate` and `config.get()` returns `Config`, so an editor offers the valid providers and a typo fails to compile rather than silently falling back:
|
|
211
212
|
|
|
212
213
|
```ts
|
|
213
|
-
import { Oya, type LlmProvider } from
|
|
214
|
+
import { Oya, type LlmProvider } from '@oya-ai/browser';
|
|
214
215
|
|
|
215
|
-
await oya.config.set({ llm_provider:
|
|
216
|
+
await oya.config.set({ llm_provider: 'vertx' });
|
|
216
217
|
// ~~~~~~~ Type '"vertx"' is not assignable to type
|
|
217
218
|
// 'LlmProvider'. Did you mean '"vertex"'?
|
|
218
219
|
|
|
219
|
-
const provider: LlmProvider =
|
|
220
|
+
const provider: LlmProvider = 'vertex'; // for your own config plumbing
|
|
220
221
|
const { effective } = await oya.config.get();
|
|
221
222
|
console.log(effective.baseUrl, effective.model, effective.hasLlmKey);
|
|
222
223
|
```
|
|
@@ -228,17 +229,18 @@ console.log(effective.baseUrl, effective.model, effective.hasLlmKey);
|
|
|
228
229
|
`llm_provider: "vertex"` targets express mode, whose API keys work against a global endpoint with no GCP project or location:
|
|
229
230
|
|
|
230
231
|
```js
|
|
231
|
-
await oya.config.set({ llm_provider:
|
|
232
|
+
await oya.config.set({ llm_provider: 'vertex', openai_api_key: process.env.VERTEX_EXPRESS_KEY });
|
|
232
233
|
```
|
|
233
234
|
|
|
234
235
|
For an enterprise project instead, point at its OpenAI-compatible endpoint. That path authenticates with a Google OAuth access token rather than an API key, and the token expires after about an hour, so it suits a one-off run rather than a long-lived deployment:
|
|
235
236
|
|
|
236
237
|
```js
|
|
237
238
|
await oya.config.set({
|
|
238
|
-
llm_provider:
|
|
239
|
-
openai_base_url:
|
|
239
|
+
llm_provider: 'vertex',
|
|
240
|
+
openai_base_url:
|
|
241
|
+
'https://us-central1-aiplatform.googleapis.com/v1/projects/PROJECT/locations/us-central1/endpoints/openapi',
|
|
240
242
|
openai_api_key: accessToken, // gcloud auth print-access-token
|
|
241
|
-
chat_model:
|
|
243
|
+
chat_model: 'google/gemini-2.5-flash', // this endpoint prefixes model ids
|
|
242
244
|
});
|
|
243
245
|
```
|
|
244
246
|
|
|
@@ -270,21 +272,21 @@ For an embedded SSE stream of JPEG frames, use `await browser.liveStreamUrl()`.
|
|
|
270
272
|
A persona groups a stable device fingerprint, saved login cookies, and a proxy assignment for reuse across browser sessions.
|
|
271
273
|
|
|
272
274
|
```ts
|
|
273
|
-
import { Oya } from
|
|
275
|
+
import { Oya } from '@oya-ai/browser';
|
|
274
276
|
|
|
275
277
|
const oya = new Oya();
|
|
276
278
|
|
|
277
279
|
// Create a persistent persona
|
|
278
280
|
const persona = await oya.personas.create({
|
|
279
|
-
name:
|
|
280
|
-
prefs: { platform:
|
|
281
|
-
proxy: { geo:
|
|
281
|
+
name: 'us-shopper',
|
|
282
|
+
prefs: { platform: 'MacIntel', timezone: 'America/New_York', locale: 'en-US' },
|
|
283
|
+
proxy: { geo: 'US' },
|
|
282
284
|
maxConcurrent: 2, // Limit simultaneous sessions for this identity
|
|
283
285
|
});
|
|
284
286
|
|
|
285
287
|
// Launch a browser with this persona (or persona: 'auto' for least-recently-used)
|
|
286
288
|
await using browser = await oya.browser.start({ persona: persona.id });
|
|
287
|
-
await browser.goto(
|
|
289
|
+
await browser.goto('https://www.amazon.com');
|
|
288
290
|
|
|
289
291
|
// Hardware attributes remain byte-identical on subsequent sessions
|
|
290
292
|
console.log(persona.fingerprint.platform, persona.fingerprint.timezone);
|
|
@@ -300,18 +302,18 @@ console.log(persona.fingerprint.platform, persona.fingerprint.timezone);
|
|
|
300
302
|
Skip messy DOM traversal. Get clean markdown and numbered interactive elements:
|
|
301
303
|
|
|
302
304
|
```ts
|
|
303
|
-
import { Oya } from
|
|
305
|
+
import { Oya } from '@oya-ai/browser';
|
|
304
306
|
|
|
305
307
|
const oya = new Oya();
|
|
306
308
|
await using browser = await oya.browser.start();
|
|
307
|
-
await browser.goto(
|
|
309
|
+
await browser.goto('https://github.com/trending');
|
|
308
310
|
|
|
309
311
|
// Analyze page: returns markdown and visible numbered elements
|
|
310
312
|
const { markdown, elements } = await browser.analyze();
|
|
311
313
|
console.log(markdown.slice(0, 300));
|
|
312
314
|
|
|
313
315
|
// Interact using numbered element IDs:
|
|
314
|
-
const firstRepo = elements.find((el) => el.tag ===
|
|
316
|
+
const firstRepo = elements.find((el) => el.tag === 'a' && el.href?.includes('/stargazers'));
|
|
315
317
|
if (firstRepo) {
|
|
316
318
|
await browser.click(firstRepo.id); // clicks [data-ac-id="firstRepo.id"]
|
|
317
319
|
}
|
|
@@ -322,23 +324,23 @@ if (firstRepo) {
|
|
|
322
324
|
## 🧩 Challenge Handling: Automated CAPTCHA & Sealed MFA
|
|
323
325
|
|
|
324
326
|
```ts
|
|
325
|
-
import { Oya } from
|
|
327
|
+
import { Oya } from '@oya-ai/browser';
|
|
326
328
|
|
|
327
329
|
const oya = new Oya();
|
|
328
330
|
|
|
329
331
|
// 1. Seal a TOTP secret on a persona (encrypted with AES-256-GCM at rest, never exposed over API)
|
|
330
|
-
const persona = await oya.personas.create({ name:
|
|
332
|
+
const persona = await oya.personas.create({ name: 'finance-admin' });
|
|
331
333
|
await oya.personas.setMfa(persona.id, {
|
|
332
|
-
type:
|
|
334
|
+
type: 'totp',
|
|
333
335
|
secret: process.env.TOTP_SECRET!,
|
|
334
336
|
});
|
|
335
337
|
|
|
336
338
|
await using browser = await oya.browser.start({ persona: persona.id });
|
|
337
339
|
|
|
338
340
|
// 2. Clear CAPTCHAs automatically (uses vendor solver or CapSolver/2Captcha fallback)
|
|
339
|
-
await browser.goto(
|
|
341
|
+
await browser.goto('https://www.google.com/recaptcha/api2/demo');
|
|
340
342
|
const captcha = await browser.solveCaptcha();
|
|
341
|
-
console.log(
|
|
343
|
+
console.log('CAPTCHA Solved:', captcha.solved, 'via', captcha.method);
|
|
342
344
|
|
|
343
345
|
// 3. Complete Two-Factor Authentication
|
|
344
346
|
await browser.goto(process.env.MFA_LOGIN_URL!);
|
|
@@ -346,7 +348,7 @@ const mfa = await browser.completeMfa();
|
|
|
346
348
|
|
|
347
349
|
if (!mfa.completed && mfa.liveViewUrl) {
|
|
348
350
|
// Hand off to human operator if interactive push notification or WebAuthn is needed
|
|
349
|
-
console.log(
|
|
351
|
+
console.log('Interactive handoff required at:', mfa.liveViewUrl);
|
|
350
352
|
}
|
|
351
353
|
```
|
|
352
354
|
|
|
@@ -357,20 +359,20 @@ if (!mfa.completed && mfa.liveViewUrl) {
|
|
|
357
359
|
Every browser exposes an authenticated `browser.cdpUrl` routed through Oya's gateway. Connect standard Playwright, Puppeteer, or Stagehand:
|
|
358
360
|
|
|
359
361
|
```ts
|
|
360
|
-
import { chromium } from
|
|
361
|
-
import { Oya } from
|
|
362
|
+
import { chromium } from 'playwright-core';
|
|
363
|
+
import { Oya } from '@oya-ai/browser';
|
|
362
364
|
|
|
363
365
|
const oya = new Oya();
|
|
364
366
|
|
|
365
367
|
// Run on any underlying provider: browserbase, steel, anchor, browseruse, or oya-cloud
|
|
366
|
-
await using browser = await oya.browser.start({ provider:
|
|
368
|
+
await using browser = await oya.browser.start({ provider: 'browserbase' });
|
|
367
369
|
|
|
368
370
|
// Connect Playwright directly over Oya's gateway
|
|
369
371
|
const context = (await chromium.connectOverCDP(browser.cdpUrl!)).contexts()[0];
|
|
370
372
|
const page = context.pages()[0] ?? (await context.newPage());
|
|
371
373
|
|
|
372
|
-
await page.goto(
|
|
373
|
-
console.log(
|
|
374
|
+
await page.goto('https://news.ycombinator.com');
|
|
375
|
+
console.log('Page Title:', await page.title());
|
|
374
376
|
```
|
|
375
377
|
|
|
376
378
|
---
|
|
@@ -380,120 +382,125 @@ console.log("Page Title:", await page.title());
|
|
|
380
382
|
### Initialization
|
|
381
383
|
|
|
382
384
|
```ts
|
|
383
|
-
import { Oya } from
|
|
385
|
+
import { Oya } from '@oya-ai/browser';
|
|
384
386
|
|
|
385
387
|
const oya = new Oya({
|
|
386
|
-
apiKey:
|
|
387
|
-
baseUrl:
|
|
388
|
-
timeoutMs: 60_000,
|
|
388
|
+
apiKey: 'oya_...', // default: process.env.OYA_API_KEY
|
|
389
|
+
baseUrl: 'https://oyabrowser.com', // default: OYA_BASE_URL, then the hosted service
|
|
390
|
+
timeoutMs: 60_000, // per request
|
|
389
391
|
// fetch: customFetch, // any fetch-compatible implementation
|
|
390
392
|
});
|
|
391
393
|
```
|
|
392
394
|
|
|
393
395
|
### Browser Operations (`oya.browser`)
|
|
394
396
|
|
|
395
|
-
| Method
|
|
396
|
-
|
|
397
|
-
| `start(options)`
|
|
398
|
-
| `get(id)`
|
|
399
|
-
| `list()`
|
|
400
|
-
| `stop(ids \| 'all')` | `(ids: string[] \| 'all') => Promise<{ stopped: number; results: StopResult[] }>` | Stop target browsers or all browsers
|
|
401
|
-
| `stopAll()`
|
|
397
|
+
| Method | Signature | Description |
|
|
398
|
+
| :------------------- | :-------------------------------------------------------------------------------- | :------------------------------------------------------ |
|
|
399
|
+
| `start(options)` | `(options?: StartOptions) => Promise<Browser>` | Start a browser and wait until it is ready for commands |
|
|
400
|
+
| `get(id)` | `(id: string) => Promise<Browser>` | Reattach to an existing running browser |
|
|
401
|
+
| `list()` | `() => Promise<BrowserInfo[]>` | List all active running browser sessions |
|
|
402
|
+
| `stop(ids \| 'all')` | `(ids: string[] \| 'all') => Promise<{ stopped: number; results: StopResult[] }>` | Stop target browsers or all browsers |
|
|
403
|
+
| `stopAll()` | `() => Promise<number>` | Terminate all active browser sessions |
|
|
402
404
|
|
|
403
405
|
#### `StartOptions`
|
|
404
406
|
|
|
405
|
-
- `persona?: 'default' | 'auto' | string
|
|
407
|
+
- `persona?: 'default' | 'auto' | string`, Assign persistent identity
|
|
406
408
|
- `provider?: 'oya-cloud' | 'oya-selfhosted' | 'browserbase' | 'steel' | 'anchor' | 'browseruse' | 'cdp'`
|
|
407
|
-
- `wsUrl?: string`: required only for the `'cdp'` provider
|
|
409
|
+
- `wsUrl?: string`: required only for the `'cdp'` provider. Either the WebSocket URL, or the
|
|
410
|
+
plain `http://localhost:9222` that Chrome prints for `--remote-debugging-port`, which is
|
|
411
|
+
resolved through Chrome's own `/json/version`
|
|
408
412
|
- `name?: string`: display name in the console and `oya ls`
|
|
409
|
-
- `captcha?: 'auto' | 'off'
|
|
410
|
-
- `queueMs?: number
|
|
411
|
-
- `budgetUsd?: number
|
|
412
|
-
- `idempotencyKey?: string
|
|
413
|
-
- `governed?: boolean
|
|
414
|
-
- `profile?: string
|
|
415
|
-
- `priority?: 'low' | 'normal' | 'high'
|
|
416
|
-
- `policy?: { allowedHosts?, humanHosts?, region?, redactRecording? }
|
|
417
|
-
- `readyTimeoutMs?: number
|
|
413
|
+
- `captcha?: 'auto' | 'off'`, Automatically solve CAPTCHAs on navigation
|
|
414
|
+
- `queueMs?: number`, Wait duration for fleet capacity (ms)
|
|
415
|
+
- `budgetUsd?: number`, Enforce budget limit for session
|
|
416
|
+
- `idempotencyKey?: string`, Safe retry token
|
|
417
|
+
- `governed?: boolean`, Enable governed session controls
|
|
418
|
+
- `profile?: string`, Saved login profile (takes precedence over `persona`)
|
|
419
|
+
- `priority?: 'low' | 'normal' | 'high'`, Queue priority
|
|
420
|
+
- `policy?: { allowedHosts?, humanHosts?, region?, redactRecording? }`, Session policy
|
|
421
|
+
- `readyTimeoutMs?: number`, Wait budget for a starting browser to connect
|
|
418
422
|
|
|
419
423
|
### Browser Instance Methods (`browser.*`)
|
|
420
424
|
|
|
421
|
-
| Method
|
|
422
|
-
|
|
423
|
-
| `goto(url)`
|
|
424
|
-
| `ask(prompt, { data?, secrets? }?)` | `Promise<string>`
|
|
425
|
-
| `analyze()`
|
|
426
|
-
| `elements()`
|
|
427
|
-
| `click(elementId)`
|
|
428
|
-
| `type(elementId, text)`
|
|
429
|
-
| `pressKey(key)`
|
|
430
|
-
| `scroll(dir, amount?, at?)`
|
|
431
|
-
| `waitFor(selector, timeout?)
|
|
432
|
-
| `screenshot()`
|
|
433
|
-
| `url()`
|
|
434
|
-
| `tabs()`
|
|
435
|
-
| `openTab(url?)`
|
|
436
|
-
| `switchTab(tabId)`
|
|
437
|
-
| `closeTab(tabId)`
|
|
438
|
-
| `solveCaptcha()`
|
|
439
|
-
| `completeMfa()`
|
|
440
|
-
| `liveViewUrl()`
|
|
441
|
-
| `liveStreamUrl()`
|
|
442
|
-
| `shareUrl(options?)`
|
|
443
|
-
| `revokeShare(id)`
|
|
444
|
-
| `submit(task, options?)`
|
|
445
|
-
| `toPlaybook(name)`
|
|
446
|
-
| `play(name, data?, { autoHeal? }?)` | `Promise<PlayResult>`
|
|
447
|
-
| `status()`
|
|
448
|
-
| `stop()`
|
|
425
|
+
| Method | Returns | Description |
|
|
426
|
+
| :---------------------------------- | :------------------------------------------- | :------------------------------------------------------- |
|
|
427
|
+
| `goto(url)` | `Promise<void>` | Navigate to URL (with optional auto-CAPTCHA) |
|
|
428
|
+
| `ask(prompt, { data?, secrets? }?)` | `Promise<string>` | Natural-language AI driving using key's configured model |
|
|
429
|
+
| `analyze()` | `Promise<Analysis>` | Returns markdown representation and numbered elements |
|
|
430
|
+
| `elements()` | `Promise<Element[]>` | Returns only visible interactable elements |
|
|
431
|
+
| `click(elementId)` | `Promise<void>` | Click element by numeric ID from `analyze()` |
|
|
432
|
+
| `type(elementId, text)` | `Promise<{ suggestions_visible?: boolean }>` | Type text into specified element |
|
|
433
|
+
| `pressKey(key)` | `Promise<void>` | Dispatch keyboard key event (e.g. `'Enter'`) |
|
|
434
|
+
| `scroll(dir, amount?, at?)` | `Promise<void>` | Scroll `'up' \| 'down' \| 'top' \| 'bottom'` |
|
|
435
|
+
| `waitFor(selector, timeout?)` | `Promise<void>` | Wait for DOM selector |
|
|
436
|
+
| `screenshot()` | `Promise<string>` | Capture page as base64 image data URL |
|
|
437
|
+
| `url()` | `Promise<string>` | Current active tab URL |
|
|
438
|
+
| `tabs()` | `Promise<Tab[]>` | List open tabs |
|
|
439
|
+
| `openTab(url?)` | `Promise<string>` | Open a new tab |
|
|
440
|
+
| `switchTab(tabId)` | `Promise<void>` | Switch active tab |
|
|
441
|
+
| `closeTab(tabId)` | `Promise<void>` | Close target tab |
|
|
442
|
+
| `solveCaptcha()` | `Promise<CaptchaResult>` | Detect and solve on-screen CAPTCHA |
|
|
443
|
+
| `completeMfa()` | `Promise<MfaResult>` | Resolve TOTP/SMS MFA or return `liveViewUrl` |
|
|
444
|
+
| `liveViewUrl()` | `string` | Dashboard link for this browser |
|
|
445
|
+
| `liveStreamUrl()` | `Promise<string>` | SSE frame stream URL with a single-use ticket |
|
|
446
|
+
| `shareUrl(options?)` | `Promise<{ url, id, expiresAt }>` | Expiring browser share link; optional control access |
|
|
447
|
+
| `revokeShare(id)` | `Promise<void>` | Revoke a share link |
|
|
448
|
+
| `submit(task, options?)` | `Promise<Run>` | Background prompt or playbook with callbacks |
|
|
449
|
+
| `toPlaybook(name)` | `Promise<Playbook>` | Save the latest agent flow and export Playwright code |
|
|
450
|
+
| `play(name, data?, { autoHeal? }?)` | `Promise<PlayResult>` | Replay a saved flow |
|
|
451
|
+
| `status()` | `Promise<BrowserDetail>` | Instance metrics, health, and recent activity log |
|
|
452
|
+
| `stop()` | `Promise<StopResult>` | Tear down sandbox and release CDP session |
|
|
449
453
|
|
|
450
454
|
### Profile and persona management (`oya.profiles`, `oya.personas`)
|
|
451
455
|
|
|
452
456
|
`oya.profiles` exposes the same methods as `oya.personas`; the persona name remains available for existing integrations.
|
|
453
457
|
|
|
454
|
-
| Method
|
|
455
|
-
|
|
456
|
-
| `create({ name?, prefs?, proxy?, maxConcurrent? })` | Create new deterministic device identity
|
|
457
|
-
| `list()`
|
|
458
|
-
| `get(id)`
|
|
459
|
-
| `update(id, changes)`
|
|
460
|
-
| `clone(id, options)`
|
|
461
|
-
| `preview(prefs)`
|
|
462
|
-
| `options()`
|
|
463
|
-
| `pinProxy(id, proxyId)`
|
|
464
|
-
| `remove(id)`
|
|
465
|
-
| `setMfa(id, config)`
|
|
466
|
-
| `clearMfa(id)`
|
|
458
|
+
| Method | Description |
|
|
459
|
+
| :-------------------------------------------------- | :---------------------------------------------------------------- |
|
|
460
|
+
| `create({ name?, prefs?, proxy?, maxConcurrent? })` | Create new deterministic device identity |
|
|
461
|
+
| `list()` | List all saved personas and active concurrency |
|
|
462
|
+
| `get(id)` | Get persona profile details |
|
|
463
|
+
| `update(id, changes)` | Update name, concurrency limit, or proxy geo |
|
|
464
|
+
| `clone(id, options)` | Create fresh persona with same device traits but empty cookie jar |
|
|
465
|
+
| `preview(prefs)` | Preview generated hardware fingerprint before creating |
|
|
466
|
+
| `options()` | Available platforms, timezones, and valid locales |
|
|
467
|
+
| `pinProxy(id, proxyId)` | Bind persona permanently to a residential proxy exit node |
|
|
468
|
+
| `remove(id)` | Delete persona and associated cookie jar |
|
|
469
|
+
| `setMfa(id, config)` | Store TOTP secret (sealed at rest with AES-256-GCM) |
|
|
470
|
+
| `clearMfa(id)` | Remove MFA secret from persona |
|
|
467
471
|
|
|
468
472
|
### Proxies (`oya.proxies`)
|
|
469
473
|
|
|
470
|
-
| Method
|
|
471
|
-
|
|
472
|
-
| `create({ url, label?, geo?, kind?, maxPersonas? })` | Add a proxy from your vendor. Credentials are encrypted and never returned
|
|
473
|
-
| `list()`
|
|
474
|
-
| `check()`
|
|
475
|
-
| `remove(id)`
|
|
474
|
+
| Method | Description |
|
|
475
|
+
| :--------------------------------------------------- | :-------------------------------------------------------------------------------- |
|
|
476
|
+
| `create({ url, label?, geo?, kind?, maxPersonas? })` | Add a proxy from your vendor. Credentials are encrypted and never returned |
|
|
477
|
+
| `list()` | Your proxies and shared ones, with exit IP, health and how many personas use each |
|
|
478
|
+
| `check()` | Dial every proxy and record its real exit IP |
|
|
479
|
+
| `remove(id)` | Delete a proxy and unpin the personas on it |
|
|
476
480
|
|
|
477
481
|
```ts
|
|
478
482
|
const proxy = await oya.proxies.create({
|
|
479
|
-
url:
|
|
480
|
-
label:
|
|
483
|
+
url: 'http://user:pass_session-shopper1@gate.vendor.com:7000', // one sticky session per persona
|
|
484
|
+
label: 'us-shopper-1',
|
|
485
|
+
geo: 'US',
|
|
486
|
+
kind: 'residential',
|
|
487
|
+
maxPersonas: 1,
|
|
481
488
|
});
|
|
482
489
|
await oya.personas.pinProxy(persona.id, proxy.id);
|
|
483
490
|
```
|
|
484
491
|
|
|
485
492
|
### Durable Governance & Control (`oya.control`)
|
|
486
493
|
|
|
487
|
-
| Method
|
|
488
|
-
|
|
489
|
-
| `overview()`
|
|
490
|
-
| `sessions()`
|
|
491
|
-
| `session(id)`
|
|
492
|
-
| `takeover(id, 'acquire' \| 'release' \| 'resume')` | Manage human control leases
|
|
493
|
-
| `ticket(id)`
|
|
494
|
-
| `events(after?)`
|
|
495
|
-
| `createCredential(options)`
|
|
496
|
-
| `createWebhook(url, types)`
|
|
494
|
+
| Method | Description |
|
|
495
|
+
| :------------------------------------------------- | :----------------------------------------------------------------------- |
|
|
496
|
+
| `overview()` | Fleet overview, spend, sessions, and active rate cards |
|
|
497
|
+
| `sessions()` | List all durable sessions (including cleanup-pending) |
|
|
498
|
+
| `session(id)` | Get detailed session execution state |
|
|
499
|
+
| `takeover(id, 'acquire' \| 'release' \| 'resume')` | Manage human control leases |
|
|
500
|
+
| `ticket(id)` | Generate single-use connection ticket for secure handoff |
|
|
501
|
+
| `events(after?)` | Read audit events and a pagination cursor |
|
|
502
|
+
| `createCredential(options)` | Mint scoped service credential (`viewer` / `operator` / `administrator`) |
|
|
503
|
+
| `createWebhook(url, types)` | Register HMAC-signed webhook for fleet lifecycle events |
|
|
497
504
|
|
|
498
505
|
---
|
|
499
506
|
|
|
@@ -502,11 +509,11 @@ await oya.personas.pinProxy(persona.id, proxy.id);
|
|
|
502
509
|
API error responses and failed browser commands throw `OyaError`. Network failures, request timeouts, and configuration errors may throw other error types:
|
|
503
510
|
|
|
504
511
|
```ts
|
|
505
|
-
import { Oya, OyaError } from
|
|
512
|
+
import { Oya, OyaError } from '@oya-ai/browser';
|
|
506
513
|
|
|
507
514
|
try {
|
|
508
515
|
const oya = new Oya();
|
|
509
|
-
await oya.browser.start({ persona:
|
|
516
|
+
await oya.browser.start({ persona: 'invalid-id' });
|
|
510
517
|
} catch (err) {
|
|
511
518
|
if (err instanceof OyaError) {
|
|
512
519
|
console.error(`Oya API Error (${err.status}):`, err.message);
|
package/dist/index.cjs
CHANGED
|
@@ -45,8 +45,30 @@ var OyaError = class extends Error {
|
|
|
45
45
|
}
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
// src/cli-config.ts
|
|
49
|
+
var node = () => globalThis.process;
|
|
50
|
+
function builtin(name) {
|
|
51
|
+
const get = node()?.getBuiltinModule;
|
|
52
|
+
return typeof get === "function" ? get.call(node(), name) ?? null : null;
|
|
53
|
+
}
|
|
54
|
+
function configFile() {
|
|
55
|
+
const os = builtin("node:os");
|
|
56
|
+
const home = node()?.env?.OYA_CONFIG_HOME || (os ? `${os.homedir()}/.oya` : null);
|
|
57
|
+
return home ? `${home}/config.json` : null;
|
|
58
|
+
}
|
|
59
|
+
function savedConfig() {
|
|
60
|
+
try {
|
|
61
|
+
const fs = builtin("node:fs");
|
|
62
|
+
const file2 = configFile();
|
|
63
|
+
if (!fs || !file2 || !fs.existsSync(file2)) return {};
|
|
64
|
+
return JSON.parse(fs.readFileSync(file2, "utf8"));
|
|
65
|
+
} catch {
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
48
70
|
// src/constants.ts
|
|
49
|
-
var DEFAULT_BASE_URL = "https://
|
|
71
|
+
var DEFAULT_BASE_URL = "https://oyabrowser.com";
|
|
50
72
|
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
51
73
|
var START_TIMEOUT_MS = 12e4;
|
|
52
74
|
var READY_TIMEOUT_MS = 12e4;
|
|
@@ -95,10 +117,31 @@ var Http = class {
|
|
|
95
117
|
fetchImpl;
|
|
96
118
|
/** Sends one request and returns the parsed answer, or throws an OyaError when it failed. */
|
|
97
119
|
async request(method, path, body, timeoutMs = this.timeoutMs, headers = {}) {
|
|
98
|
-
const
|
|
99
|
-
return readAnswer(
|
|
120
|
+
const res = await this.send(`${this.baseUrl}${path}`, this.init(method, body, timeoutMs, headers), timeoutMs);
|
|
121
|
+
return readAnswer(res, `${method} ${path}`, this.baseUrl);
|
|
122
|
+
}
|
|
123
|
+
/** The fetch options for one call, with this client's key on them. */
|
|
124
|
+
init(method, body, timeoutMs, headers) {
|
|
125
|
+
return requestInit(method, body, timeoutMs, { Authorization: `Bearer ${this.apiKey}`, ...headers });
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* fetch's own failures say only "fetch failed" or "The operation was
|
|
129
|
+
* aborted", which leaves a reader guessing at the address, the port and
|
|
130
|
+
* whether anything is listening. Say which it was.
|
|
131
|
+
*/
|
|
132
|
+
async send(url, init, timeoutMs) {
|
|
133
|
+
try {
|
|
134
|
+
return await this.fetchImpl(url, init);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
throw new OyaError(unreachable(this.baseUrl, timeoutMs, err), 0, { error: String(err?.message) });
|
|
137
|
+
}
|
|
100
138
|
}
|
|
101
139
|
};
|
|
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?`;
|
|
144
|
+
}
|
|
102
145
|
function requestInit(method, body, timeoutMs, headers) {
|
|
103
146
|
const json = body === void 0 ? {} : { "Content-Type": "application/json" };
|
|
104
147
|
const payload = body === void 0 ? void 0 : JSON.stringify(body);
|
|
@@ -125,13 +168,12 @@ function failure(call2, status, payload, baseUrl) {
|
|
|
125
168
|
}
|
|
126
169
|
var env = (name) => globalThis.process?.env?.[name];
|
|
127
170
|
function createHttp(options) {
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const baseUrl = (options.baseUrl || env("OYA_BASE_URL") || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
171
|
+
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(/\/+$/, "");
|
|
133
175
|
const fetchImpl = options.fetch || globalThis.fetch;
|
|
134
|
-
if (!fetchImpl) throw new Error("No fetch available
|
|
176
|
+
if (!fetchImpl) throw new Error("No fetch available, pass { fetch } or use Node 18+.");
|
|
135
177
|
return new Http(baseUrl, apiKey, options.timeoutMs ?? DEFAULT_TIMEOUT_MS, fetchImpl.bind(globalThis));
|
|
136
178
|
}
|
|
137
179
|
|
|
@@ -231,10 +273,8 @@ var Browser = class {
|
|
|
231
273
|
constructor(http, info, autoCaptcha) {
|
|
232
274
|
this.http = http;
|
|
233
275
|
this.autoCaptcha = autoCaptcha;
|
|
234
|
-
this.id = info
|
|
235
|
-
this.
|
|
236
|
-
this.persona = info.persona;
|
|
237
|
-
this.cdpUrl = info.cdpUrl;
|
|
276
|
+
({ id: this.id, provider: this.provider, persona: this.persona, cdpUrl: this.cdpUrl } = info);
|
|
277
|
+
this.reused = info.reused === true;
|
|
238
278
|
}
|
|
239
279
|
http;
|
|
240
280
|
autoCaptcha;
|
|
@@ -246,6 +286,8 @@ var Browser = class {
|
|
|
246
286
|
persona;
|
|
247
287
|
/** Point Playwright, Puppeteer or browser-use here. */
|
|
248
288
|
cdpUrl;
|
|
289
|
+
/** True when `start()` handed over a browser that was already running, such as the desktop app. */
|
|
290
|
+
reused;
|
|
249
291
|
/** Runs one browser command; a command that ran and failed throws. */
|
|
250
292
|
async command(action, params = {}, timeoutMs) {
|
|
251
293
|
const path = `/api/browsers/${this.id}/command`;
|
|
@@ -397,7 +439,7 @@ var Browser = class {
|
|
|
397
439
|
/**
|
|
398
440
|
* The SSE stream of JPEG frames, for embedding in your own UI. EventSource
|
|
399
441
|
* cannot set headers, so the URL carries a connection ticket: single use,
|
|
400
|
-
* 60 seconds. Mint one per viewer
|
|
442
|
+
* 60 seconds. Mint one per viewer, the first connection spends it.
|
|
401
443
|
*/
|
|
402
444
|
async liveStreamUrl() {
|
|
403
445
|
const path = `/api/control/sessions/${encodeURIComponent(this.id)}/ticket`;
|
|
@@ -433,13 +475,14 @@ var Browser = class {
|
|
|
433
475
|
* CDP session is handed back to its provider, a desktop browser disconnects.
|
|
434
476
|
*/
|
|
435
477
|
stop() {
|
|
478
|
+
if (this.reused) return Promise.resolve({ id: this.id, ok: true, reused: true });
|
|
436
479
|
return this.http.request("POST", `/api/browsers/${this.id}/stop`, {}, STOP_TIMEOUT_MS);
|
|
437
480
|
}
|
|
438
481
|
/** `await using browser = await oya.browser.start()` stops it however the block exits, errors included. */
|
|
439
482
|
async [Symbol.asyncDispose]() {
|
|
440
483
|
await this.stop();
|
|
441
484
|
}
|
|
442
|
-
/** @deprecated use stop()
|
|
485
|
+
/** @deprecated use stop(), close() only dropped the socket, and a cloud browser redialled. */
|
|
443
486
|
async close() {
|
|
444
487
|
await this.stop();
|
|
445
488
|
}
|
|
@@ -606,11 +649,11 @@ var identityCalls = (http) => ({
|
|
|
606
649
|
/** One persona. */
|
|
607
650
|
get: (id) => http().request("GET", `/api/personas/${id}`),
|
|
608
651
|
/**
|
|
609
|
-
* Create an identity. The device
|
|
652
|
+
* Create an identity. The device, platform, timezone, locale, is chosen
|
|
610
653
|
* here and fixed for its life; `preview()` shows what a choice produces.
|
|
611
654
|
*/
|
|
612
655
|
create: (options = {}) => http().request("POST", "/api/personas", options),
|
|
613
|
-
/** Name, concurrency cap and proxy hint. Never the device
|
|
656
|
+
/** Name, concurrency cap and proxy hint. Never the device, clone for that. */
|
|
614
657
|
update: (id, changes) => http().request("PUT", `/api/personas/${id}`, changes),
|
|
615
658
|
/** A new persona of the same kind of device: same choices, fresh identity, empty jar. */
|
|
616
659
|
clone: (id, options = {}) => http().request("POST", `/api/personas/${id}/clone`, options)
|
package/dist/index.d.cts
CHANGED
|
@@ -32,16 +32,19 @@ interface StartOptions {
|
|
|
32
32
|
/**
|
|
33
33
|
* Which identity to run as. A persona is one device: fingerprint, cookie jar
|
|
34
34
|
* and proxy bound together and stable for its life.
|
|
35
|
-
* 'default'
|
|
36
|
-
* 'auto'
|
|
37
|
-
* <id>
|
|
35
|
+
* 'default', this key's own persona (the default)
|
|
36
|
+
* 'auto' , the least recently used persona under its concurrency cap
|
|
37
|
+
* <id> , a specific persona
|
|
38
38
|
*/
|
|
39
39
|
persona?: 'default' | 'auto' | (string & {});
|
|
40
40
|
/** Solve CAPTCHAs as they appear rather than waiting to be asked. */
|
|
41
41
|
captcha?: 'auto' | 'off';
|
|
42
42
|
/** Override the key's configured provider for this browser only. */
|
|
43
43
|
provider?: Provider;
|
|
44
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Required only for the 'cdp' provider: the CDP WebSocket URL, or the plain
|
|
46
|
+
* `http://localhost:9222` Chrome prints for `--remote-debugging-port`.
|
|
47
|
+
*/
|
|
45
48
|
wsUrl?: string;
|
|
46
49
|
/** A label for the fleet listing. */
|
|
47
50
|
name?: string;
|
|
@@ -62,6 +65,8 @@ interface StartResult {
|
|
|
62
65
|
cdpUrl?: string;
|
|
63
66
|
/** Anything the server wants the caller to know about this start. */
|
|
64
67
|
note?: string;
|
|
68
|
+
/** True when this browser was already running and was handed over rather than started. */
|
|
69
|
+
reused?: boolean;
|
|
65
70
|
}
|
|
66
71
|
/** One element on the page, numbered by `analyze()`. */
|
|
67
72
|
interface Element {
|
|
@@ -212,6 +217,8 @@ interface StopResult {
|
|
|
212
217
|
sandboxRemoved?: boolean | null;
|
|
213
218
|
/** Why it did not stop. */
|
|
214
219
|
error?: string;
|
|
220
|
+
/** True when nothing was stopped because the browser was only borrowed. */
|
|
221
|
+
reused?: boolean;
|
|
215
222
|
}
|
|
216
223
|
/** One browser in the fleet listing. */
|
|
217
224
|
interface BrowserInfo {
|
|
@@ -274,7 +281,7 @@ type CaptchaSolver = 'capsolver' | '2captcha' | '';
|
|
|
274
281
|
interface ConfigUpdate {
|
|
275
282
|
/** Which LLM vendor `ask()` and the chat API call. */
|
|
276
283
|
llm_provider?: LlmProvider | null;
|
|
277
|
-
/** The credential for whichever `llm_provider` is set
|
|
284
|
+
/** The credential for whichever `llm_provider` is set, the field name is shared. */
|
|
278
285
|
openai_api_key?: string | null;
|
|
279
286
|
/** Only honoured alongside this key's own `openai_api_key`. */
|
|
280
287
|
openai_base_url?: string | null;
|
|
@@ -360,7 +367,7 @@ interface Playbook {
|
|
|
360
367
|
name: string;
|
|
361
368
|
/** Inputs `play()` accepts; any left out reuse the recorded value. */
|
|
362
369
|
variables: string[];
|
|
363
|
-
/** What each variable was recorded with. A secret has none
|
|
370
|
+
/** What each variable was recorded with. A secret has none, it never left the page. */
|
|
364
371
|
defaults: Record<string, string>;
|
|
365
372
|
/** How many steps it replays. */
|
|
366
373
|
steps: number;
|
|
@@ -629,7 +636,7 @@ interface PersonaInfo {
|
|
|
629
636
|
* persona-wide default.
|
|
630
637
|
*
|
|
631
638
|
* `gmail` and `graph` read the code straight out of a mailbox. `email` and `sms`
|
|
632
|
-
* poll an endpoint you host
|
|
639
|
+
* poll an endpoint you host, set `x-oya-received-at` on its response (epoch ms)
|
|
633
640
|
* and a code from a previous run will never be reused.
|
|
634
641
|
*
|
|
635
642
|
* The code is pulled out of the message by your own configured LLM, because
|
|
@@ -794,7 +801,7 @@ interface ControlOverview {
|
|
|
794
801
|
interface OyaOptions {
|
|
795
802
|
/** Defaults to OYA_API_KEY. */
|
|
796
803
|
apiKey?: string;
|
|
797
|
-
/** Defaults to OYA_BASE_URL, then https://
|
|
804
|
+
/** Defaults to OYA_BASE_URL, then https://oyabrowser.com. */
|
|
798
805
|
baseUrl?: string;
|
|
799
806
|
/** Per-request timeout. Navigation gets its own, longer budget. */
|
|
800
807
|
timeoutMs?: number;
|
|
@@ -812,6 +819,14 @@ declare class Http {
|
|
|
812
819
|
constructor(baseUrl: string, apiKey: string, timeoutMs: number, fetchImpl: typeof globalThis.fetch);
|
|
813
820
|
/** Sends one request and returns the parsed answer, or throws an OyaError when it failed. */
|
|
814
821
|
request<T>(method: string, path: string, body?: unknown, timeoutMs?: number, headers?: Record<string, string>): Promise<T>;
|
|
822
|
+
/** The fetch options for one call, with this client's key on them. */
|
|
823
|
+
private init;
|
|
824
|
+
/**
|
|
825
|
+
* fetch's own failures say only "fetch failed" or "The operation was
|
|
826
|
+
* aborted", which leaves a reader guessing at the address, the port and
|
|
827
|
+
* whether anything is listening. Say which it was.
|
|
828
|
+
*/
|
|
829
|
+
private send;
|
|
815
830
|
}
|
|
816
831
|
|
|
817
832
|
/**
|
|
@@ -1086,7 +1101,7 @@ declare class Run {
|
|
|
1086
1101
|
/**
|
|
1087
1102
|
* One running browser.
|
|
1088
1103
|
*
|
|
1089
|
-
* Element ids come from `analyze()` and are only valid until the page changes
|
|
1104
|
+
* Element ids come from `analyze()` and are only valid until the page changes,
|
|
1090
1105
|
* the same contract the agent tools use. `click(13)` after a navigation is a
|
|
1091
1106
|
* bug; call `analyze()` again.
|
|
1092
1107
|
*/
|
|
@@ -1101,6 +1116,8 @@ declare class Browser {
|
|
|
1101
1116
|
readonly persona: string;
|
|
1102
1117
|
/** Point Playwright, Puppeteer or browser-use here. */
|
|
1103
1118
|
readonly cdpUrl?: string;
|
|
1119
|
+
/** True when `start()` handed over a browser that was already running, such as the desktop app. */
|
|
1120
|
+
readonly reused: boolean;
|
|
1104
1121
|
/** Wraps a started browser; `autoCaptcha` solves CAPTCHAs after every `goto()`. */
|
|
1105
1122
|
constructor(http: Http, info: StartResult, autoCaptcha: boolean);
|
|
1106
1123
|
/** Runs one browser command; a command that ran and failed throws. */
|
|
@@ -1191,7 +1208,7 @@ declare class Browser {
|
|
|
1191
1208
|
/**
|
|
1192
1209
|
* The SSE stream of JPEG frames, for embedding in your own UI. EventSource
|
|
1193
1210
|
* cannot set headers, so the URL carries a connection ticket: single use,
|
|
1194
|
-
* 60 seconds. Mint one per viewer
|
|
1211
|
+
* 60 seconds. Mint one per viewer, the first connection spends it.
|
|
1195
1212
|
*/
|
|
1196
1213
|
liveStreamUrl(): Promise<string>;
|
|
1197
1214
|
/**
|
|
@@ -1217,7 +1234,7 @@ declare class Browser {
|
|
|
1217
1234
|
stop(): Promise<StopResult>;
|
|
1218
1235
|
/** `await using browser = await oya.browser.start()` stops it however the block exits, errors included. */
|
|
1219
1236
|
[Symbol.asyncDispose](): Promise<void>;
|
|
1220
|
-
/** @deprecated use stop()
|
|
1237
|
+
/** @deprecated use stop(), close() only dropped the socket, and a cloud browser redialled. */
|
|
1221
1238
|
close(): Promise<void>;
|
|
1222
1239
|
}
|
|
1223
1240
|
|
|
@@ -1227,7 +1244,7 @@ declare class Browser {
|
|
|
1227
1244
|
* await browser.ask('Attach my resume', { data: { resume: await file('./cv.pdf') } });
|
|
1228
1245
|
*
|
|
1229
1246
|
* The bytes ride inline in the run request, so the agent's `upload_file` tool can put
|
|
1230
|
-
* them into a page's file input. `secrets` cannot hold one
|
|
1247
|
+
* them into a page's file input. `secrets` cannot hold one, a file is never typed
|
|
1231
1248
|
* through a placeholder, so there is nothing to hide.
|
|
1232
1249
|
*/
|
|
1233
1250
|
|
package/dist/index.d.ts
CHANGED
|
@@ -32,16 +32,19 @@ interface StartOptions {
|
|
|
32
32
|
/**
|
|
33
33
|
* Which identity to run as. A persona is one device: fingerprint, cookie jar
|
|
34
34
|
* and proxy bound together and stable for its life.
|
|
35
|
-
* 'default'
|
|
36
|
-
* 'auto'
|
|
37
|
-
* <id>
|
|
35
|
+
* 'default', this key's own persona (the default)
|
|
36
|
+
* 'auto' , the least recently used persona under its concurrency cap
|
|
37
|
+
* <id> , a specific persona
|
|
38
38
|
*/
|
|
39
39
|
persona?: 'default' | 'auto' | (string & {});
|
|
40
40
|
/** Solve CAPTCHAs as they appear rather than waiting to be asked. */
|
|
41
41
|
captcha?: 'auto' | 'off';
|
|
42
42
|
/** Override the key's configured provider for this browser only. */
|
|
43
43
|
provider?: Provider;
|
|
44
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Required only for the 'cdp' provider: the CDP WebSocket URL, or the plain
|
|
46
|
+
* `http://localhost:9222` Chrome prints for `--remote-debugging-port`.
|
|
47
|
+
*/
|
|
45
48
|
wsUrl?: string;
|
|
46
49
|
/** A label for the fleet listing. */
|
|
47
50
|
name?: string;
|
|
@@ -62,6 +65,8 @@ interface StartResult {
|
|
|
62
65
|
cdpUrl?: string;
|
|
63
66
|
/** Anything the server wants the caller to know about this start. */
|
|
64
67
|
note?: string;
|
|
68
|
+
/** True when this browser was already running and was handed over rather than started. */
|
|
69
|
+
reused?: boolean;
|
|
65
70
|
}
|
|
66
71
|
/** One element on the page, numbered by `analyze()`. */
|
|
67
72
|
interface Element {
|
|
@@ -212,6 +217,8 @@ interface StopResult {
|
|
|
212
217
|
sandboxRemoved?: boolean | null;
|
|
213
218
|
/** Why it did not stop. */
|
|
214
219
|
error?: string;
|
|
220
|
+
/** True when nothing was stopped because the browser was only borrowed. */
|
|
221
|
+
reused?: boolean;
|
|
215
222
|
}
|
|
216
223
|
/** One browser in the fleet listing. */
|
|
217
224
|
interface BrowserInfo {
|
|
@@ -274,7 +281,7 @@ type CaptchaSolver = 'capsolver' | '2captcha' | '';
|
|
|
274
281
|
interface ConfigUpdate {
|
|
275
282
|
/** Which LLM vendor `ask()` and the chat API call. */
|
|
276
283
|
llm_provider?: LlmProvider | null;
|
|
277
|
-
/** The credential for whichever `llm_provider` is set
|
|
284
|
+
/** The credential for whichever `llm_provider` is set, the field name is shared. */
|
|
278
285
|
openai_api_key?: string | null;
|
|
279
286
|
/** Only honoured alongside this key's own `openai_api_key`. */
|
|
280
287
|
openai_base_url?: string | null;
|
|
@@ -360,7 +367,7 @@ interface Playbook {
|
|
|
360
367
|
name: string;
|
|
361
368
|
/** Inputs `play()` accepts; any left out reuse the recorded value. */
|
|
362
369
|
variables: string[];
|
|
363
|
-
/** What each variable was recorded with. A secret has none
|
|
370
|
+
/** What each variable was recorded with. A secret has none, it never left the page. */
|
|
364
371
|
defaults: Record<string, string>;
|
|
365
372
|
/** How many steps it replays. */
|
|
366
373
|
steps: number;
|
|
@@ -629,7 +636,7 @@ interface PersonaInfo {
|
|
|
629
636
|
* persona-wide default.
|
|
630
637
|
*
|
|
631
638
|
* `gmail` and `graph` read the code straight out of a mailbox. `email` and `sms`
|
|
632
|
-
* poll an endpoint you host
|
|
639
|
+
* poll an endpoint you host, set `x-oya-received-at` on its response (epoch ms)
|
|
633
640
|
* and a code from a previous run will never be reused.
|
|
634
641
|
*
|
|
635
642
|
* The code is pulled out of the message by your own configured LLM, because
|
|
@@ -794,7 +801,7 @@ interface ControlOverview {
|
|
|
794
801
|
interface OyaOptions {
|
|
795
802
|
/** Defaults to OYA_API_KEY. */
|
|
796
803
|
apiKey?: string;
|
|
797
|
-
/** Defaults to OYA_BASE_URL, then https://
|
|
804
|
+
/** Defaults to OYA_BASE_URL, then https://oyabrowser.com. */
|
|
798
805
|
baseUrl?: string;
|
|
799
806
|
/** Per-request timeout. Navigation gets its own, longer budget. */
|
|
800
807
|
timeoutMs?: number;
|
|
@@ -812,6 +819,14 @@ declare class Http {
|
|
|
812
819
|
constructor(baseUrl: string, apiKey: string, timeoutMs: number, fetchImpl: typeof globalThis.fetch);
|
|
813
820
|
/** Sends one request and returns the parsed answer, or throws an OyaError when it failed. */
|
|
814
821
|
request<T>(method: string, path: string, body?: unknown, timeoutMs?: number, headers?: Record<string, string>): Promise<T>;
|
|
822
|
+
/** The fetch options for one call, with this client's key on them. */
|
|
823
|
+
private init;
|
|
824
|
+
/**
|
|
825
|
+
* fetch's own failures say only "fetch failed" or "The operation was
|
|
826
|
+
* aborted", which leaves a reader guessing at the address, the port and
|
|
827
|
+
* whether anything is listening. Say which it was.
|
|
828
|
+
*/
|
|
829
|
+
private send;
|
|
815
830
|
}
|
|
816
831
|
|
|
817
832
|
/**
|
|
@@ -1086,7 +1101,7 @@ declare class Run {
|
|
|
1086
1101
|
/**
|
|
1087
1102
|
* One running browser.
|
|
1088
1103
|
*
|
|
1089
|
-
* Element ids come from `analyze()` and are only valid until the page changes
|
|
1104
|
+
* Element ids come from `analyze()` and are only valid until the page changes,
|
|
1090
1105
|
* the same contract the agent tools use. `click(13)` after a navigation is a
|
|
1091
1106
|
* bug; call `analyze()` again.
|
|
1092
1107
|
*/
|
|
@@ -1101,6 +1116,8 @@ declare class Browser {
|
|
|
1101
1116
|
readonly persona: string;
|
|
1102
1117
|
/** Point Playwright, Puppeteer or browser-use here. */
|
|
1103
1118
|
readonly cdpUrl?: string;
|
|
1119
|
+
/** True when `start()` handed over a browser that was already running, such as the desktop app. */
|
|
1120
|
+
readonly reused: boolean;
|
|
1104
1121
|
/** Wraps a started browser; `autoCaptcha` solves CAPTCHAs after every `goto()`. */
|
|
1105
1122
|
constructor(http: Http, info: StartResult, autoCaptcha: boolean);
|
|
1106
1123
|
/** Runs one browser command; a command that ran and failed throws. */
|
|
@@ -1191,7 +1208,7 @@ declare class Browser {
|
|
|
1191
1208
|
/**
|
|
1192
1209
|
* The SSE stream of JPEG frames, for embedding in your own UI. EventSource
|
|
1193
1210
|
* cannot set headers, so the URL carries a connection ticket: single use,
|
|
1194
|
-
* 60 seconds. Mint one per viewer
|
|
1211
|
+
* 60 seconds. Mint one per viewer, the first connection spends it.
|
|
1195
1212
|
*/
|
|
1196
1213
|
liveStreamUrl(): Promise<string>;
|
|
1197
1214
|
/**
|
|
@@ -1217,7 +1234,7 @@ declare class Browser {
|
|
|
1217
1234
|
stop(): Promise<StopResult>;
|
|
1218
1235
|
/** `await using browser = await oya.browser.start()` stops it however the block exits, errors included. */
|
|
1219
1236
|
[Symbol.asyncDispose](): Promise<void>;
|
|
1220
|
-
/** @deprecated use stop()
|
|
1237
|
+
/** @deprecated use stop(), close() only dropped the socket, and a cloud browser redialled. */
|
|
1221
1238
|
close(): Promise<void>;
|
|
1222
1239
|
}
|
|
1223
1240
|
|
|
@@ -1227,7 +1244,7 @@ declare class Browser {
|
|
|
1227
1244
|
* await browser.ask('Attach my resume', { data: { resume: await file('./cv.pdf') } });
|
|
1228
1245
|
*
|
|
1229
1246
|
* The bytes ride inline in the run request, so the agent's `upload_file` tool can put
|
|
1230
|
-
* them into a page's file input. `secrets` cannot hold one
|
|
1247
|
+
* them into a page's file input. `secrets` cannot hold one, a file is never typed
|
|
1231
1248
|
* through a placeholder, so there is nothing to hide.
|
|
1232
1249
|
*/
|
|
1233
1250
|
|
package/dist/index.js
CHANGED
|
@@ -13,8 +13,30 @@ var OyaError = class extends Error {
|
|
|
13
13
|
}
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
// src/cli-config.ts
|
|
17
|
+
var node = () => globalThis.process;
|
|
18
|
+
function builtin(name) {
|
|
19
|
+
const get = node()?.getBuiltinModule;
|
|
20
|
+
return typeof get === "function" ? get.call(node(), name) ?? null : null;
|
|
21
|
+
}
|
|
22
|
+
function configFile() {
|
|
23
|
+
const os = builtin("node:os");
|
|
24
|
+
const home = node()?.env?.OYA_CONFIG_HOME || (os ? `${os.homedir()}/.oya` : null);
|
|
25
|
+
return home ? `${home}/config.json` : null;
|
|
26
|
+
}
|
|
27
|
+
function savedConfig() {
|
|
28
|
+
try {
|
|
29
|
+
const fs = builtin("node:fs");
|
|
30
|
+
const file2 = configFile();
|
|
31
|
+
if (!fs || !file2 || !fs.existsSync(file2)) return {};
|
|
32
|
+
return JSON.parse(fs.readFileSync(file2, "utf8"));
|
|
33
|
+
} catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
16
38
|
// src/constants.ts
|
|
17
|
-
var DEFAULT_BASE_URL = "https://
|
|
39
|
+
var DEFAULT_BASE_URL = "https://oyabrowser.com";
|
|
18
40
|
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
19
41
|
var START_TIMEOUT_MS = 12e4;
|
|
20
42
|
var READY_TIMEOUT_MS = 12e4;
|
|
@@ -63,10 +85,31 @@ var Http = class {
|
|
|
63
85
|
fetchImpl;
|
|
64
86
|
/** Sends one request and returns the parsed answer, or throws an OyaError when it failed. */
|
|
65
87
|
async request(method, path, body, timeoutMs = this.timeoutMs, headers = {}) {
|
|
66
|
-
const
|
|
67
|
-
return readAnswer(
|
|
88
|
+
const res = await this.send(`${this.baseUrl}${path}`, this.init(method, body, timeoutMs, headers), timeoutMs);
|
|
89
|
+
return readAnswer(res, `${method} ${path}`, this.baseUrl);
|
|
90
|
+
}
|
|
91
|
+
/** The fetch options for one call, with this client's key on them. */
|
|
92
|
+
init(method, body, timeoutMs, headers) {
|
|
93
|
+
return requestInit(method, body, timeoutMs, { Authorization: `Bearer ${this.apiKey}`, ...headers });
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* fetch's own failures say only "fetch failed" or "The operation was
|
|
97
|
+
* aborted", which leaves a reader guessing at the address, the port and
|
|
98
|
+
* whether anything is listening. Say which it was.
|
|
99
|
+
*/
|
|
100
|
+
async send(url, init, timeoutMs) {
|
|
101
|
+
try {
|
|
102
|
+
return await this.fetchImpl(url, init);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
throw new OyaError(unreachable(this.baseUrl, timeoutMs, err), 0, { error: String(err?.message) });
|
|
105
|
+
}
|
|
68
106
|
}
|
|
69
107
|
};
|
|
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?`;
|
|
112
|
+
}
|
|
70
113
|
function requestInit(method, body, timeoutMs, headers) {
|
|
71
114
|
const json = body === void 0 ? {} : { "Content-Type": "application/json" };
|
|
72
115
|
const payload = body === void 0 ? void 0 : JSON.stringify(body);
|
|
@@ -93,13 +136,12 @@ function failure(call2, status, payload, baseUrl) {
|
|
|
93
136
|
}
|
|
94
137
|
var env = (name) => globalThis.process?.env?.[name];
|
|
95
138
|
function createHttp(options) {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const baseUrl = (options.baseUrl || env("OYA_BASE_URL") || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
139
|
+
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(/\/+$/, "");
|
|
101
143
|
const fetchImpl = options.fetch || globalThis.fetch;
|
|
102
|
-
if (!fetchImpl) throw new Error("No fetch available
|
|
144
|
+
if (!fetchImpl) throw new Error("No fetch available, pass { fetch } or use Node 18+.");
|
|
103
145
|
return new Http(baseUrl, apiKey, options.timeoutMs ?? DEFAULT_TIMEOUT_MS, fetchImpl.bind(globalThis));
|
|
104
146
|
}
|
|
105
147
|
|
|
@@ -199,10 +241,8 @@ var Browser = class {
|
|
|
199
241
|
constructor(http, info, autoCaptcha) {
|
|
200
242
|
this.http = http;
|
|
201
243
|
this.autoCaptcha = autoCaptcha;
|
|
202
|
-
this.id = info
|
|
203
|
-
this.
|
|
204
|
-
this.persona = info.persona;
|
|
205
|
-
this.cdpUrl = info.cdpUrl;
|
|
244
|
+
({ id: this.id, provider: this.provider, persona: this.persona, cdpUrl: this.cdpUrl } = info);
|
|
245
|
+
this.reused = info.reused === true;
|
|
206
246
|
}
|
|
207
247
|
http;
|
|
208
248
|
autoCaptcha;
|
|
@@ -214,6 +254,8 @@ var Browser = class {
|
|
|
214
254
|
persona;
|
|
215
255
|
/** Point Playwright, Puppeteer or browser-use here. */
|
|
216
256
|
cdpUrl;
|
|
257
|
+
/** True when `start()` handed over a browser that was already running, such as the desktop app. */
|
|
258
|
+
reused;
|
|
217
259
|
/** Runs one browser command; a command that ran and failed throws. */
|
|
218
260
|
async command(action, params = {}, timeoutMs) {
|
|
219
261
|
const path = `/api/browsers/${this.id}/command`;
|
|
@@ -365,7 +407,7 @@ var Browser = class {
|
|
|
365
407
|
/**
|
|
366
408
|
* The SSE stream of JPEG frames, for embedding in your own UI. EventSource
|
|
367
409
|
* cannot set headers, so the URL carries a connection ticket: single use,
|
|
368
|
-
* 60 seconds. Mint one per viewer
|
|
410
|
+
* 60 seconds. Mint one per viewer, the first connection spends it.
|
|
369
411
|
*/
|
|
370
412
|
async liveStreamUrl() {
|
|
371
413
|
const path = `/api/control/sessions/${encodeURIComponent(this.id)}/ticket`;
|
|
@@ -401,13 +443,14 @@ var Browser = class {
|
|
|
401
443
|
* CDP session is handed back to its provider, a desktop browser disconnects.
|
|
402
444
|
*/
|
|
403
445
|
stop() {
|
|
446
|
+
if (this.reused) return Promise.resolve({ id: this.id, ok: true, reused: true });
|
|
404
447
|
return this.http.request("POST", `/api/browsers/${this.id}/stop`, {}, STOP_TIMEOUT_MS);
|
|
405
448
|
}
|
|
406
449
|
/** `await using browser = await oya.browser.start()` stops it however the block exits, errors included. */
|
|
407
450
|
async [Symbol.asyncDispose]() {
|
|
408
451
|
await this.stop();
|
|
409
452
|
}
|
|
410
|
-
/** @deprecated use stop()
|
|
453
|
+
/** @deprecated use stop(), close() only dropped the socket, and a cloud browser redialled. */
|
|
411
454
|
async close() {
|
|
412
455
|
await this.stop();
|
|
413
456
|
}
|
|
@@ -574,11 +617,11 @@ var identityCalls = (http) => ({
|
|
|
574
617
|
/** One persona. */
|
|
575
618
|
get: (id) => http().request("GET", `/api/personas/${id}`),
|
|
576
619
|
/**
|
|
577
|
-
* Create an identity. The device
|
|
620
|
+
* Create an identity. The device, platform, timezone, locale, is chosen
|
|
578
621
|
* here and fixed for its life; `preview()` shows what a choice produces.
|
|
579
622
|
*/
|
|
580
623
|
create: (options = {}) => http().request("POST", "/api/personas", options),
|
|
581
|
-
/** Name, concurrency cap and proxy hint. Never the device
|
|
624
|
+
/** Name, concurrency cap and proxy hint. Never the device, clone for that. */
|
|
582
625
|
update: (id, changes) => http().request("PUT", `/api/personas/${id}`, changes),
|
|
583
626
|
/** A new persona of the same kind of device: same choices, fresh identity, empty jar. */
|
|
584
627
|
clone: (id, options = {}) => http().request("POST", `/api/personas/${id}/clone`, options)
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oya-ai/browser",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Rotate thousands of browsers behind one API
|
|
3
|
+
"version": "1.0.105",
|
|
4
|
+
"description": "Rotate thousands of browsers behind one API, personas, proxies, stealth, CAPTCHA and MFA.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"homepage": "https://
|
|
6
|
+
"homepage": "https://oyabrowser.com",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/OyadotAI/oya-browser.git",
|