@oya-ai/browser 1.0.107 → 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 +5 -1
- package/dist/index.cjs +210 -103
- package/dist/index.d.cts +67 -8
- package/dist/index.d.ts +67 -8
- package/dist/index.js +210 -103
- package/package.json +1 -1
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,6 +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
475
|
|
|
472
476
|
### Proxies (`oya.proxies`)
|
|
473
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
|
|
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
|
|
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
|
|
149
|
+
throw noAnswer(this, timeoutMs, err);
|
|
137
150
|
}
|
|
138
151
|
}
|
|
139
152
|
};
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return
|
|
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,
|
|
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,
|
|
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,
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
173
|
-
|
|
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
|
-
|
|
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/${
|
|
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/${
|
|
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
|
|
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/${
|
|
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/${
|
|
558
|
-
var item = (kind, id) => `/api/control/${kind}/${
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
581
|
-
|
|
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/${
|
|
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/${
|
|
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(
|
|
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,19 +776,36 @@ 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)}`);
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
var jarPath = (id) => `/api/pool/cookies?persona=${segment(id)}`;
|
|
788
|
+
var jarCalls = (http) => ({
|
|
789
|
+
/** Every cookie in the jar; `format: 'playwright'` is ready for `context.addCookies()`. */
|
|
790
|
+
cookies: async (id, format = "json") => (await http().request("GET", `${jarPath(id)}&format=${encodeURIComponent(format)}`)).cookies,
|
|
791
|
+
/** Merge cookies into the jar. The persona's browsers pick them up on their next visit to each site. */
|
|
792
|
+
importCookies: async (id, list) => http().request("PUT", jarPath(id), { cookies: list })
|
|
793
|
+
});
|
|
794
|
+
var cookieCalls = (http) => ({
|
|
795
|
+
...jarCalls(http),
|
|
796
|
+
/** Copy every login of persona `from` into persona `to`, which keeps its own device. */
|
|
797
|
+
copyCookies: async (from, to) => jarCalls(http).importCookies(to, await jarCalls(http).cookies(from)),
|
|
798
|
+
/** Forget every cookie in the jar: signs the persona out everywhere. */
|
|
799
|
+
clearCookies: async (id) => {
|
|
800
|
+
await http().request("DELETE", jarPath(id));
|
|
695
801
|
}
|
|
696
802
|
});
|
|
697
803
|
var personaApi = (http) => ({
|
|
698
804
|
...identityCalls(http),
|
|
699
805
|
...deviceCalls(http),
|
|
700
806
|
...mfaCalls(http),
|
|
701
|
-
...loginCalls(http)
|
|
807
|
+
...loginCalls(http),
|
|
808
|
+
...cookieCalls(http)
|
|
702
809
|
});
|
|
703
810
|
|
|
704
811
|
// src/api/config.ts
|