@anvia/browser 1.0.7 → 1.0.10
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/MIGRATION.md +68 -0
- package/README.md +202 -26
- package/dist/automation-worker.d.ts +2 -0
- package/dist/automation-worker.js +298 -0
- package/dist/automation-worker.js.map +1 -0
- package/dist/index.d.ts +71 -21
- package/dist/index.js +2006 -572
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
package/MIGRATION.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# `@anvia/browser` migration notes
|
|
2
|
+
|
|
3
|
+
## Selected-tab tools to explicit tabs
|
|
4
|
+
|
|
5
|
+
Existing tools remain source compatible and connections still default to serial scheduling. Calls that
|
|
6
|
+
omit `tabId` continue using the selected tab.
|
|
7
|
+
|
|
8
|
+
To opt into independent-tab concurrency:
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
const connection = await browser.connect({
|
|
12
|
+
scheduling: { mode: "per-tab", maxConcurrentTabs: 8 },
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const [{ id: first }, { id: second }] = await connection.listTabs();
|
|
16
|
+
|
|
17
|
+
await Promise.all([
|
|
18
|
+
snapshot.call({ tabId: first }),
|
|
19
|
+
navigate.call({ tabId: second, url: "https://example.com" }),
|
|
20
|
+
]);
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Add `tabId` to `browser_navigate`, `browser_snapshot`, `browser_click`, `browser_type`,
|
|
24
|
+
`browser_press_key`, and `browser_screenshot`. `browser_open_tab` returns the new stable ID. Selection is
|
|
25
|
+
still useful for human-oriented or legacy serial workflows, but should not be shared mutable targeting
|
|
26
|
+
state for concurrent agents. IDs are scoped to one connection; call `browser_list_tabs` after reconnect.
|
|
27
|
+
|
|
28
|
+
A browser handle now allows one active or pending automation connection. Share that connection between
|
|
29
|
+
agents that use the same browser, and disconnect it before reconnecting. This is required so one
|
|
30
|
+
resource scheduler and navigation policy arbitrate the shared Chromium context deterministically.
|
|
31
|
+
|
|
32
|
+
## All-or-nothing readiness to capabilities
|
|
33
|
+
|
|
34
|
+
`waitUntilReady({ timeoutMs })` still waits for all runtime, browser, automation, and desktop
|
|
35
|
+
capabilities. Consumers that do not require every capability should migrate to:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
await browser.waitForCapabilities({
|
|
39
|
+
capabilities: ["desktop"],
|
|
40
|
+
timeoutMs: 10_000,
|
|
41
|
+
abortSignal,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const { capabilities } = browser.readiness();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Handle `BrowserError.capability` and preserve healthy capabilities when another is degraded.
|
|
48
|
+
|
|
49
|
+
## Cancellation and errors
|
|
50
|
+
|
|
51
|
+
Connection cancellation now terminates and joins an isolated Playwright worker. Cancelling an active
|
|
52
|
+
page tool closes that tab because Playwright page actions do not accept `AbortSignal`; obtain a fresh tab
|
|
53
|
+
ID before retrying. If page cleanup cannot finish, the entire connection closes and must be recreated.
|
|
54
|
+
|
|
55
|
+
`destroy({ timeoutMs, abortSignal })` starts an irreversible terminal transition. If Docker cleanup
|
|
56
|
+
cannot be cancelled, the caller now stops waiting at its own deadline while the handle remains
|
|
57
|
+
`destroying` and retains ownership of the shared cleanup. A later `destroy()` call joins that operation;
|
|
58
|
+
control cannot become active again while cleanup finishes.
|
|
59
|
+
|
|
60
|
+
Prefer `error.retryable` and `error.recovery` over a closed switch on `error.code`. Legacy
|
|
61
|
+
`human_controlled` remains active for an acquired lease; pending acquisition now uses
|
|
62
|
+
`human_control_conflict`. Legacy `not_ready` remains available, while bounded connection and readiness
|
|
63
|
+
operations use the more specific `connection_timeout` and `readiness_timeout` codes.
|
|
64
|
+
|
|
65
|
+
`BrowserControlSnapshot` now includes required `state`, `availability`, `activeAgentActions`, and
|
|
66
|
+
`humanPending` fields in addition to the existing mode and optional lease metadata. Consumers that
|
|
67
|
+
provide a structural browser-control test double should add those fields; consumers that only read the
|
|
68
|
+
snapshot remain source compatible.
|
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# `@anvia/browser`
|
|
2
2
|
|
|
3
|
-
Visible Chromium ownership and semantic browser tools for Anvia agents
|
|
4
|
-
owned by `@anvia/sandbox`; this package owns the browser workload
|
|
3
|
+
Visible Chromium ownership and semantic browser tools for concurrent Anvia agents and human viewers.
|
|
4
|
+
Docker infrastructure remains owned by `@anvia/sandbox`; this package owns the browser workload,
|
|
5
|
+
capability readiness, isolated Playwright connection, action scheduling, and control arbitration.
|
|
5
6
|
|
|
6
7
|
```ts
|
|
7
8
|
import { DockerBrowserClient, createBrowserTools } from "@anvia/browser";
|
|
@@ -13,7 +14,7 @@ const browserClient = new DockerBrowserClient({
|
|
|
13
14
|
image: "ghcr.io/anvia-hq/browser@sha256:...",
|
|
14
15
|
});
|
|
15
16
|
|
|
16
|
-
await browserClient.pullImage();
|
|
17
|
+
await browserClient.pullImage({ timeoutMs: 120_000 });
|
|
17
18
|
await using browser = await browserClient.createBrowser({
|
|
18
19
|
workspace: { type: "ephemeral" },
|
|
19
20
|
network: { mode: "bridge" },
|
|
@@ -24,8 +25,19 @@ await using browser = await browserClient.createBrowser({
|
|
|
24
25
|
},
|
|
25
26
|
});
|
|
26
27
|
|
|
27
|
-
await browser.
|
|
28
|
-
|
|
28
|
+
await browser.waitForCapabilities({
|
|
29
|
+
capabilities: ["automation", "desktop"],
|
|
30
|
+
timeoutMs: 30_000,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
await using connection = await browser.connect({
|
|
34
|
+
timeoutMs: 30_000,
|
|
35
|
+
scheduling: {
|
|
36
|
+
mode: "per-tab",
|
|
37
|
+
maxConcurrentTabs: 8,
|
|
38
|
+
maxQueuedActions: 1_000,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
29
41
|
|
|
30
42
|
const tools = createBrowserTools({
|
|
31
43
|
connection,
|
|
@@ -47,34 +59,196 @@ const tools = createBrowserTools({
|
|
|
47
59
|
const agent = new Agent({ id: "browser-agent", model, tools });
|
|
48
60
|
```
|
|
49
61
|
|
|
50
|
-
The client constructor performs no I/O.
|
|
51
|
-
|
|
52
|
-
|
|
62
|
+
The client constructor performs no I/O. Image pull, create/resume, readiness, and CDP connection are
|
|
63
|
+
separate bounded operations. `DockerBrowser` owns the sandbox; a `PlaywrightBrowserConnection` owns
|
|
64
|
+
only its automation worker and CDP connection. Disconnecting automation never destroys Chromium.
|
|
65
|
+
|
|
66
|
+
## Lifecycle and cancellation
|
|
67
|
+
|
|
68
|
+
Playwright and its CDP protocol state run in a supervised child process, one per connection. This is an
|
|
69
|
+
intentional fault-containment boundary: Playwright 1.62.1 normally aborts its progress scope and closes
|
|
70
|
+
the transport on timeout, but an internal exception raised by a late protocol message is outside the
|
|
71
|
+
rejected `connectOverCDP()` promise. An in-process wrapper cannot guarantee host survival in that case.
|
|
72
|
+
The child boundary contains that exception without installing process-global `uncaughtException` or
|
|
73
|
+
`unhandledRejection` handlers.
|
|
74
|
+
|
|
75
|
+
`connect()` defaults to 30 seconds and passes the remaining budget to Playwright. Caller cancellation,
|
|
76
|
+
timeout, stop, and destroy terminate and join the worker before the attempt rejects. A worker response
|
|
77
|
+
that loses the cancellation race is ignored. A failed attempt owns no state in `DockerBrowser`, so a
|
|
78
|
+
later call starts a clean attempt.
|
|
79
|
+
|
|
80
|
+
Image pull, create, resume, stop, and destroy accept `timeoutMs` and `abortSignal`; their default budget
|
|
81
|
+
is 120 seconds. `disconnect()` defaults to 10 seconds. Docker sandbox destruction is irreversible and
|
|
82
|
+
cannot currently be cancelled. Once it starts, timeout or cancellation bounds only that caller's wait:
|
|
83
|
+
the browser remains visibly `destroying`, owns the eventual completion, and makes later `destroy()`
|
|
84
|
+
calls join the same cleanup. Late completion can only transition the terminal handle to `destroyed` or
|
|
85
|
+
`error`; it cannot restore agent or human control.
|
|
86
|
+
Timeout values are integer milliseconds from 1 through `2_147_483_647`, matching Node's timer range.
|
|
87
|
+
|
|
88
|
+
`stop()` first cancels pending readiness/connect work, disconnects all automation workers, and then
|
|
89
|
+
stops the sandbox. It also cancels pending human acquisition and releases an active lease, while
|
|
90
|
+
preserving the container. `resumeBrowser({ id })` returns a new handle and requires new readiness and
|
|
91
|
+
automation connections. `destroy()` is idempotent, joins pending work, invalidates human-control leases,
|
|
92
|
+
and removes the sandbox. Destroy remains available after a failed stop.
|
|
93
|
+
Concurrent stop, destroy, or disconnect calls join the first shared transition. A later caller's own
|
|
94
|
+
timeout or cancellation bounds its wait without rolling back that already-visible `stopping`,
|
|
95
|
+
`destroying`, or closed transition.
|
|
96
|
+
|
|
97
|
+
## Capability readiness
|
|
98
|
+
|
|
99
|
+
Readiness is not all-or-nothing:
|
|
100
|
+
|
|
101
|
+
| Capability | What is proven |
|
|
102
|
+
| ------------ | --------------------------------------------------------------------------------------------------------- |
|
|
103
|
+
| `runtime` | The Docker browser handle and sandbox are running. |
|
|
104
|
+
| `browser` | Chromium's CDP port is reachable and `/json/version` exposes a WebSocket debugger URL. |
|
|
105
|
+
| `automation` | An isolated Playwright CDP connection initializes, exposes a context, and completes `Browser.getVersion`. |
|
|
106
|
+
| `desktop` | The noVNC port and HTTP client page respond successfully. |
|
|
107
|
+
|
|
108
|
+
Use `waitForCapabilities()` to wait only for required capabilities and receive a
|
|
109
|
+
`BrowserReadinessSnapshot`. `waitUntilReady()` remains available and defaults to all four capabilities.
|
|
110
|
+
Both accept a required timeout and optional `AbortSignal`. `readiness()` is synchronous and reports
|
|
111
|
+
`unknown`, `checking`, `partial`, `ready`, `degraded`, `failed`, `stopped`, or `destroyed` state
|
|
112
|
+
without probing. `partial` means some capabilities are ready while others have not been checked;
|
|
113
|
+
`degraded` means at least one checked capability failed while another remains usable.
|
|
114
|
+
|
|
115
|
+
Desktop probing never establishes Playwright. A failed automation probe can therefore leave desktop
|
|
116
|
+
ready and the runtime degraded. Retrying readiness replaces the requested capabilities' prior failed
|
|
117
|
+
state. Caller cancellation restores their previous state. Stop and destroy abort and join every probe.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
await browser.waitForCapabilities({ capabilities: ["desktop"], timeoutMs: 10_000 });
|
|
121
|
+
const snapshot = browser.readiness();
|
|
122
|
+
|
|
123
|
+
if (snapshot.capabilities.automation.state === "failed") {
|
|
124
|
+
// The desktop may still be offered to a human while automation is retried or restarted.
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Concurrency guarantees
|
|
129
|
+
|
|
130
|
+
The default `{ mode: "serial" }` scheduling preserves the original selected-tab behavior: every tool
|
|
131
|
+
call on the connection executes in one bounded FIFO queue. This is the compatibility mode.
|
|
132
|
+
|
|
133
|
+
`{ mode: "per-tab" }` enables resource-scoped scheduling:
|
|
134
|
+
|
|
135
|
+
- Mutating operations for the same tab are FIFO and never overlap.
|
|
136
|
+
- Operations for different explicit tab IDs may overlap up to `maxConcurrentTabs` (default 8).
|
|
137
|
+
- Tab creation, listing, selection, and closure use a browser-context lock where necessary.
|
|
138
|
+
- A close marks its tab as closing before it queues. Work already queued runs first; later work is
|
|
139
|
+
rejected with `invalid_state`.
|
|
140
|
+
- One failed operation releases its queue. Cancelling one tab closes that page to stop uncancellable
|
|
141
|
+
Playwright work, then releases only that tab queue.
|
|
142
|
+
- If page cleanup itself cannot settle within five seconds, the worker is terminated and consumers
|
|
143
|
+
reconnect. This fail-closed fallback can interrupt other tabs on that connection.
|
|
144
|
+
- Queue admission is bounded by `maxQueuedActions` (default 1,000) and ready tab queues are scheduled
|
|
145
|
+
FIFO, one operation per turn, to avoid starvation.
|
|
146
|
+
- Disconnect, stop, or destroy reject queued work with a structured lifecycle error.
|
|
147
|
+
|
|
148
|
+
Concurrent tool dispatch does not imply concurrent execution when calls contend for the same tab,
|
|
149
|
+
browser-context state, selected-tab compatibility lock, or human-control gate.
|
|
150
|
+
|
|
151
|
+
Page tools now accept an optional `tabId`: `browser_navigate`, `browser_snapshot`, `browser_click`,
|
|
152
|
+
`browser_type`, `browser_press_key`, and `browser_screenshot`. Omitting it uses selected-tab compatibility
|
|
153
|
+
behavior. Obtain stable IDs from `browser_list_tabs` or `browser_open_tab`.
|
|
154
|
+
Tab IDs are stable for one automation connection; reconnecting creates a fresh ID namespace, so list
|
|
155
|
+
tabs again after reconnect.
|
|
156
|
+
|
|
157
|
+
The scheduling domain is one connection, and a `DockerBrowser` handle permits one active or pending
|
|
158
|
+
automation connection at a time. A second `connect()` rejects with `agent_action_busy`; share the first
|
|
159
|
+
connection among agents and dispatch explicit-tab work through it. This prevents independent workers,
|
|
160
|
+
ID namespaces, navigation-policy installations, and queues from racing over the same Chromium context.
|
|
161
|
+
Disconnect before creating a replacement connection. Automation readiness may use a short-lived,
|
|
162
|
+
non-mutating probe connection. Human control remains runtime-wide.
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
await Promise.all([
|
|
166
|
+
snapshotTool.call({ tabId: researchTabId }),
|
|
167
|
+
navigateTool.call({ tabId: monitoringTabId, url: "https://status.example.com" }),
|
|
168
|
+
]);
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Human and agent control
|
|
172
|
+
|
|
173
|
+
Human control is browser-wide and exclusive. Acquisition defaults to a 30-second wait and also accepts
|
|
174
|
+
`timeoutMs` and `abortSignal`. The arbitration policy is:
|
|
175
|
+
|
|
176
|
+
- Acquisition waits for active agent operations to finish.
|
|
177
|
+
- Pending acquisition rejects work that has not entered the control gate, including queued and newly
|
|
178
|
+
dispatched work, with `human_control_conflict`.
|
|
179
|
+
- An active lease rejects agent work with the backward-compatible `human_controlled` code.
|
|
180
|
+
- A second acquisition is rejected; there is never more than one pending waiter or active lease.
|
|
181
|
+
- Cancellation/timeout removes pending state before rejecting.
|
|
182
|
+
- Renewal replaces the expiration timer atomically. Release, expiration, and destroy are idempotent.
|
|
183
|
+
- Agent work resumes after release or expiration.
|
|
184
|
+
|
|
185
|
+
`browser.desktop.control.snapshot()` reports `state` (`agent`, `agent-active`, `human-pending`, or
|
|
186
|
+
`human`), `activeAgentActions`, `humanPending`, lease data, and availability (`available`, `degraded`,
|
|
187
|
+
`disconnected`, or `destroyed`).
|
|
53
188
|
|
|
54
|
-
|
|
55
|
-
a new readiness check and CDP connection. A named Docker volume preserves Chromium profile state even
|
|
56
|
-
when the browser container is destroyed and later recreated with that volume.
|
|
189
|
+
## Errors and recovery
|
|
57
190
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
from links, forms, redirects, popups, and direct navigation is checked consistently. It does not block
|
|
63
|
-
third-party subresources; Docker bridge networking remains outside that policy.
|
|
191
|
+
Operational failures from public browser lifecycle/tool wrappers reject with `BrowserError`. Invalid
|
|
192
|
+
API arguments still throw `TypeError` or `RangeError`. The original operational error is retained as
|
|
193
|
+
`cause`; `code`, `retryable`, `recovery`, `phase`, and optional readiness `capability` support policy
|
|
194
|
+
decisions.
|
|
64
195
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
196
|
+
`retryable` describes whether the runtime has a documented recovery path; it does not make a mutating
|
|
197
|
+
tool call idempotent. After a timeout or transport loss, inspect or refresh tab state before deciding
|
|
198
|
+
whether replaying navigation, typing, clicking, or key input is safe.
|
|
199
|
+
|
|
200
|
+
| Codes | Typical recovery |
|
|
201
|
+
| ----------------------------------------------------------------------- | ------------------------------------------------------------------- |
|
|
202
|
+
| `cancelled`, `action_timeout`, `lifecycle_timeout`, `agent_action_busy` | Retry the operation when appropriate. |
|
|
203
|
+
| `human_control_conflict`, `human_controlled` | Wait for acquisition/lease release or expiration. |
|
|
204
|
+
| `readiness_timeout`, `not_ready` | Retry the failed capability; inspect `capability`. |
|
|
205
|
+
| `connection_timeout`, `connection_closed`, `transport_failure` | Disconnect if needed, then create a new connection. |
|
|
206
|
+
| `tool_failed` | Retry the tool if its inputs and tab are still valid. |
|
|
207
|
+
| `invalid_state` | Refresh tabs/state; restart when the runtime is stopped or errored. |
|
|
208
|
+
| `navigation_blocked` | Do not retry unchanged input; update the explicit policy or URL. |
|
|
209
|
+
| `startup_failed` | Recreate the runtime after fixing image/runtime configuration. |
|
|
210
|
+
| `runtime_destroyed` | Create or resume a new runtime handle. |
|
|
211
|
+
|
|
212
|
+
The legacy `not_ready` and `human_controlled` codes remain in the public union. New code should use
|
|
213
|
+
`retryable` and `recovery` rather than hard-coding all recovery policy.
|
|
214
|
+
|
|
215
|
+
## Browser tools and security
|
|
216
|
+
|
|
217
|
+
The tools use ARIA state and strict Playwright locators. They do not expose JavaScript evaluation, raw
|
|
218
|
+
CDP, coordinate input, shell access, hidden action retries, or automatic reconnection. The navigation
|
|
219
|
+
policy is installed across the default browser context, so top-level navigation from links, forms,
|
|
220
|
+
redirects, popups, and direct navigation is checked consistently. It does not block third-party
|
|
221
|
+
subresources; Docker bridge networking remains outside that policy.
|
|
222
|
+
|
|
223
|
+
The image runs Chromium as a non-root user with Chromium sandboxing, the Playwright seccomp profile,
|
|
224
|
+
every Linux capability dropped except `SYS_CHROOT`, no-new-privileges, and private shared memory.
|
|
225
|
+
Startup fails rather than switching Chromium to `--no-sandbox`. Docker bridge networking is not an SSRF
|
|
226
|
+
boundary; use infrastructure network policy for untrusted destinations.
|
|
227
|
+
|
|
228
|
+
The automation child process is a crash-containment boundary, not a privilege or tenant-isolation
|
|
229
|
+
boundary. It inherits the host application's OS identity; the Docker browser sandbox and application
|
|
230
|
+
authorization remain the security boundaries.
|
|
70
231
|
|
|
71
232
|
The VNC protocol uses exactly eight printable ASCII password characters. noVNC is published only on a
|
|
72
233
|
host-loopback Docker port, raw VNC is not published, and the password is not placed in image metadata,
|
|
73
234
|
URLs, environment variables, or logs.
|
|
74
235
|
|
|
236
|
+
## Playwright and Node compatibility
|
|
237
|
+
|
|
238
|
+
The host package and browser image pin `playwright-core` 1.62.1 together. Playwright requires matching
|
|
239
|
+
browser/package releases for its bundled browser and declares Node `>=20`; its current system matrix is
|
|
240
|
+
the latest Node 22, 24, or 26. Anvia CI uses Node 24, and the isolation regression test also runs on the
|
|
241
|
+
active host Node version. The package keeps its existing Node `>=20.12` engine for source compatibility,
|
|
242
|
+
but production deployments should use a currently supported Node 22, 24, or 26 release. Keep the image
|
|
243
|
+
and package pin aligned when upgrading.
|
|
244
|
+
|
|
245
|
+
The CDP connection is lower fidelity than Playwright's native protocol, but a native Playwright server
|
|
246
|
+
would conflict with this runtime's persistent, human-visible Chromium ownership. CDP remains the
|
|
247
|
+
appropriate protocol; isolation contains its in-process failure modes.
|
|
248
|
+
|
|
75
249
|
## Studio desktop and takeover
|
|
76
250
|
|
|
77
|
-
`browser.desktop`
|
|
251
|
+
`browser.desktop` remains structurally compatible with Studio without a package dependency:
|
|
78
252
|
|
|
79
253
|
```ts
|
|
80
254
|
const studio = new Studio([agent], {
|
|
@@ -99,8 +273,10 @@ const studio = new Studio([agent], {
|
|
|
99
273
|
|
|
100
274
|
Use `{ mode: "authorize", authorize }` when Studio is reachable remotely. The callback is invoked for
|
|
101
275
|
the viewer connection, WebSocket upgrade, and every control operation. When the registered agent uses a
|
|
102
|
-
matching browser tool, Studio opens its
|
|
276
|
+
matching browser tool, Studio opens its programmatic noVNC viewer in a resizable Playground panel;
|
|
103
277
|
there is no stock noVNC toolbar, splash, or password prompt. Closing the panel restores Sessions and an
|
|
104
|
-
**Open browser** action restores the current desktop. Studio human takeover waits for
|
|
105
|
-
|
|
278
|
+
**Open browser** action restores the current desktop. Studio human takeover waits for active agent
|
|
279
|
+
actions, blocks new browser tool actions, and expires unless the viewer renews its lease. Takeover
|
|
106
280
|
coordinates trusted viewers; application authorization remains the security boundary.
|
|
281
|
+
|
|
282
|
+
See [MIGRATION.md](./MIGRATION.md) for selected-tab and readiness migration examples.
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// src/automation-worker.ts
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import { chromium } from "playwright-core";
|
|
4
|
+
var browser;
|
|
5
|
+
var selected;
|
|
6
|
+
var tabIds = /* @__PURE__ */ new WeakMap();
|
|
7
|
+
var pagesById = /* @__PURE__ */ new Map();
|
|
8
|
+
var operations = /* @__PURE__ */ new Map();
|
|
9
|
+
var navigationPolicyKey;
|
|
10
|
+
process.once("disconnect", () => {
|
|
11
|
+
void closeAfterParentExit();
|
|
12
|
+
});
|
|
13
|
+
process.on("message", (message) => {
|
|
14
|
+
if (message.kind === "cancel") {
|
|
15
|
+
void cancelOperation(message);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
void runRequest(message);
|
|
19
|
+
});
|
|
20
|
+
async function runRequest(request) {
|
|
21
|
+
let complete;
|
|
22
|
+
const operation = {
|
|
23
|
+
cancelled: false,
|
|
24
|
+
completed: new Promise((resolve) => {
|
|
25
|
+
complete = resolve;
|
|
26
|
+
}),
|
|
27
|
+
complete: () => complete?.()
|
|
28
|
+
};
|
|
29
|
+
operations.set(request.id, operation);
|
|
30
|
+
try {
|
|
31
|
+
const value = await execute(request, operation);
|
|
32
|
+
send({ kind: "response", id: request.id, ok: true, value });
|
|
33
|
+
} catch (error) {
|
|
34
|
+
send({ kind: "response", id: request.id, ok: false, error: serializeError(error) });
|
|
35
|
+
} finally {
|
|
36
|
+
operation.complete();
|
|
37
|
+
operations.delete(request.id);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function cancelOperation(request) {
|
|
41
|
+
const operation = operations.get(request.id);
|
|
42
|
+
if (operation !== void 0) {
|
|
43
|
+
operation.cancelled = true;
|
|
44
|
+
await operation.page?.close().catch(() => void 0);
|
|
45
|
+
await operation.completed;
|
|
46
|
+
}
|
|
47
|
+
send({ kind: "cancelled", id: request.id });
|
|
48
|
+
}
|
|
49
|
+
async function execute(request, operation) {
|
|
50
|
+
const command = request;
|
|
51
|
+
switch (command.method) {
|
|
52
|
+
case "connect": {
|
|
53
|
+
if (browser !== void 0) throw new Error("Automation worker is already connected.");
|
|
54
|
+
const connected = await chromium.connectOverCDP(command.params.endpointUrl, {
|
|
55
|
+
timeout: command.params.timeoutMs
|
|
56
|
+
});
|
|
57
|
+
try {
|
|
58
|
+
if (operation.cancelled) throw new Error("Connection attempt was cancelled.");
|
|
59
|
+
const context = connected.contexts()[0];
|
|
60
|
+
if (context === void 0) {
|
|
61
|
+
throw new Error("CDP connection did not expose a browser context.");
|
|
62
|
+
}
|
|
63
|
+
const session = await connected.newBrowserCDPSession();
|
|
64
|
+
try {
|
|
65
|
+
await session.send("Browser.getVersion");
|
|
66
|
+
} finally {
|
|
67
|
+
await session.detach().catch(() => void 0);
|
|
68
|
+
}
|
|
69
|
+
if (operation.cancelled) throw new Error("Connection attempt was cancelled.");
|
|
70
|
+
} catch (error) {
|
|
71
|
+
await connected.close().catch(() => void 0);
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
browser = connected;
|
|
75
|
+
selected = pages()[0];
|
|
76
|
+
for (const page of pages()) idFor(page);
|
|
77
|
+
connected.once("disconnected", () => {
|
|
78
|
+
browser = void 0;
|
|
79
|
+
send({ kind: "event", event: "disconnected" });
|
|
80
|
+
});
|
|
81
|
+
return void 0;
|
|
82
|
+
}
|
|
83
|
+
case "disconnect": {
|
|
84
|
+
const connected = browser;
|
|
85
|
+
browser = void 0;
|
|
86
|
+
selected = void 0;
|
|
87
|
+
if (connected !== void 0) await connected.close();
|
|
88
|
+
return void 0;
|
|
89
|
+
}
|
|
90
|
+
case "listTabs":
|
|
91
|
+
return tabSummaries();
|
|
92
|
+
case "setNavigationPolicy":
|
|
93
|
+
await setNavigationPolicy(command.params.policy);
|
|
94
|
+
return void 0;
|
|
95
|
+
case "openTab": {
|
|
96
|
+
const context = assertBrowser().contexts()[0];
|
|
97
|
+
if (context === void 0) throw new Error("Browser has no CDP context.");
|
|
98
|
+
const page = await context.newPage();
|
|
99
|
+
operation.page = page;
|
|
100
|
+
if (operation.cancelled) {
|
|
101
|
+
await page.close().catch(() => void 0);
|
|
102
|
+
throw new Error("Open-tab operation was cancelled.");
|
|
103
|
+
}
|
|
104
|
+
selected = page;
|
|
105
|
+
return tabResult(page);
|
|
106
|
+
}
|
|
107
|
+
case "selectTab": {
|
|
108
|
+
const page = pageFor(command.params.tabId);
|
|
109
|
+
selected = page;
|
|
110
|
+
return tabResult(page);
|
|
111
|
+
}
|
|
112
|
+
case "closeTab": {
|
|
113
|
+
const page = pageFor(command.params.tabId);
|
|
114
|
+
operation.page = page;
|
|
115
|
+
await page.close();
|
|
116
|
+
if (page === selected) selected = pages()[0];
|
|
117
|
+
return { closedTabId: command.params.tabId, tabs: await tabSummaries() };
|
|
118
|
+
}
|
|
119
|
+
case "navigate": {
|
|
120
|
+
const page = targetPage(command.params.tabId, operation);
|
|
121
|
+
await page.goto(command.params.url, {
|
|
122
|
+
timeout: command.params.timeoutMs,
|
|
123
|
+
waitUntil: command.params.waitUntil
|
|
124
|
+
});
|
|
125
|
+
return tabResult(page);
|
|
126
|
+
}
|
|
127
|
+
case "snapshot": {
|
|
128
|
+
const page = targetPage(command.params.tabId, operation);
|
|
129
|
+
const snapshot = await page.locator("body").ariaSnapshot({
|
|
130
|
+
timeout: command.params.timeoutMs
|
|
131
|
+
});
|
|
132
|
+
const truncated = snapshot.length > command.params.maxChars;
|
|
133
|
+
return {
|
|
134
|
+
...await tabResult(page),
|
|
135
|
+
snapshot: truncated ? snapshot.slice(0, command.params.maxChars) : snapshot,
|
|
136
|
+
truncated
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
case "click": {
|
|
140
|
+
const page = targetPage(command.params.tabId, operation);
|
|
141
|
+
await locatorFor(page, command.params.target).click({ timeout: command.params.timeoutMs });
|
|
142
|
+
return tabResult(page);
|
|
143
|
+
}
|
|
144
|
+
case "type": {
|
|
145
|
+
const page = targetPage(command.params.tabId, operation);
|
|
146
|
+
await locatorFor(page, command.params.target).fill(command.params.text, {
|
|
147
|
+
timeout: command.params.timeoutMs
|
|
148
|
+
});
|
|
149
|
+
return tabResult(page);
|
|
150
|
+
}
|
|
151
|
+
case "pressKey": {
|
|
152
|
+
const page = targetPage(command.params.tabId, operation);
|
|
153
|
+
await page.keyboard.press(command.params.key);
|
|
154
|
+
return tabResult(page);
|
|
155
|
+
}
|
|
156
|
+
case "screenshot": {
|
|
157
|
+
const page = targetPage(command.params.tabId, operation);
|
|
158
|
+
const png = await page.screenshot({
|
|
159
|
+
type: "png",
|
|
160
|
+
fullPage: false,
|
|
161
|
+
timeout: command.params.timeoutMs
|
|
162
|
+
});
|
|
163
|
+
return { metadata: await tabResult(page), pngBase64: png.toString("base64") };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function assertBrowser() {
|
|
168
|
+
if (browser === void 0 || !browser.isConnected())
|
|
169
|
+
throw new Error("Browser connection is closed.");
|
|
170
|
+
return browser;
|
|
171
|
+
}
|
|
172
|
+
function pages() {
|
|
173
|
+
return browser?.contexts().flatMap((context) => context.pages()) ?? [];
|
|
174
|
+
}
|
|
175
|
+
function idFor(page) {
|
|
176
|
+
const existing = tabIds.get(page);
|
|
177
|
+
if (existing !== void 0) return existing;
|
|
178
|
+
const id = randomUUID();
|
|
179
|
+
tabIds.set(page, id);
|
|
180
|
+
pagesById.set(id, page);
|
|
181
|
+
page.once("close", () => pagesById.delete(id));
|
|
182
|
+
return id;
|
|
183
|
+
}
|
|
184
|
+
function pageFor(id) {
|
|
185
|
+
assertBrowser();
|
|
186
|
+
const page = pagesById.get(id);
|
|
187
|
+
if (page === void 0 || page.isClosed() || !pages().includes(page)) {
|
|
188
|
+
throw new Error(`Browser tab does not exist: ${id}`);
|
|
189
|
+
}
|
|
190
|
+
return page;
|
|
191
|
+
}
|
|
192
|
+
function targetPage(id, operation) {
|
|
193
|
+
const page = pageFor(id);
|
|
194
|
+
operation.page = page;
|
|
195
|
+
return page;
|
|
196
|
+
}
|
|
197
|
+
async function tabSummaries() {
|
|
198
|
+
assertBrowser();
|
|
199
|
+
if (selected?.isClosed()) selected = pages()[0];
|
|
200
|
+
return Promise.all(
|
|
201
|
+
pages().map(async (page) => ({
|
|
202
|
+
id: idFor(page),
|
|
203
|
+
title: await page.title(),
|
|
204
|
+
url: page.url(),
|
|
205
|
+
selected: page === selected
|
|
206
|
+
}))
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
async function tabResult(page) {
|
|
210
|
+
return { tabId: idFor(page), title: await page.title(), url: page.url() };
|
|
211
|
+
}
|
|
212
|
+
async function setNavigationPolicy(policy) {
|
|
213
|
+
const key = JSON.stringify(policy);
|
|
214
|
+
if (navigationPolicyKey !== void 0) {
|
|
215
|
+
if (navigationPolicyKey !== key) {
|
|
216
|
+
throw new TypeError("Browser connection already has a different navigation policy.");
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
navigationPolicyKey = key;
|
|
221
|
+
await Promise.all(
|
|
222
|
+
assertBrowser().contexts().map((context) => context.route("**/*", (route) => enforceNavigationPolicy(route, policy)))
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
async function enforceNavigationPolicy(route, policy) {
|
|
226
|
+
const request = route.request();
|
|
227
|
+
const frame = request.frame();
|
|
228
|
+
if (request.isNavigationRequest() && frame === frame.page().mainFrame() && !isNavigationAllowed(request.url(), policy)) {
|
|
229
|
+
await route.abort("blockedbyclient");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
await route.continue();
|
|
233
|
+
}
|
|
234
|
+
function isNavigationAllowed(value, policy) {
|
|
235
|
+
let url;
|
|
236
|
+
try {
|
|
237
|
+
url = new URL(value);
|
|
238
|
+
} catch {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
242
|
+
if (url.username.length > 0 || url.password.length > 0) return false;
|
|
243
|
+
return policy.mode === "allow-all-http" || policy.origins.includes(url.origin);
|
|
244
|
+
}
|
|
245
|
+
function locatorFor(page, target) {
|
|
246
|
+
switch (target.by) {
|
|
247
|
+
case "role": {
|
|
248
|
+
const options = {};
|
|
249
|
+
if (target.name !== void 0) options.name = target.name;
|
|
250
|
+
if (target.exact !== void 0) options.exact = target.exact;
|
|
251
|
+
return page.getByRole(target.role, options);
|
|
252
|
+
}
|
|
253
|
+
case "text": {
|
|
254
|
+
const options = {};
|
|
255
|
+
if (target.exact !== void 0) options.exact = target.exact;
|
|
256
|
+
return page.getByText(target.text, options);
|
|
257
|
+
}
|
|
258
|
+
case "label": {
|
|
259
|
+
const options = {};
|
|
260
|
+
if (target.exact !== void 0) options.exact = target.exact;
|
|
261
|
+
return page.getByLabel(target.label, options);
|
|
262
|
+
}
|
|
263
|
+
case "placeholder": {
|
|
264
|
+
const options = {};
|
|
265
|
+
if (target.exact !== void 0) options.exact = target.exact;
|
|
266
|
+
return page.getByPlaceholder(target.placeholder, options);
|
|
267
|
+
}
|
|
268
|
+
case "test-id":
|
|
269
|
+
return page.getByTestId(target.testId);
|
|
270
|
+
case "css":
|
|
271
|
+
return page.locator(target.selector);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function serializeError(error) {
|
|
275
|
+
if (error instanceof Error) {
|
|
276
|
+
const value = {
|
|
277
|
+
name: error.name,
|
|
278
|
+
message: error.message
|
|
279
|
+
};
|
|
280
|
+
if (error.stack !== void 0) value.stack = error.stack;
|
|
281
|
+
if ("code" in error && typeof error.code === "string") value.code = error.code;
|
|
282
|
+
return value;
|
|
283
|
+
}
|
|
284
|
+
return { name: "Error", message: String(error) };
|
|
285
|
+
}
|
|
286
|
+
function send(message) {
|
|
287
|
+
if (!process.connected || process.send === void 0) return;
|
|
288
|
+
process.send(message, () => void 0);
|
|
289
|
+
}
|
|
290
|
+
async function closeAfterParentExit() {
|
|
291
|
+
const forceExit = setTimeout(() => process.exit(1), 2e3);
|
|
292
|
+
const connected = browser;
|
|
293
|
+
browser = void 0;
|
|
294
|
+
await connected?.close().catch(() => void 0);
|
|
295
|
+
clearTimeout(forceExit);
|
|
296
|
+
process.exit(0);
|
|
297
|
+
}
|
|
298
|
+
//# sourceMappingURL=automation-worker.js.map
|