@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 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. Docker infrastructure remains
4
- owned by `@anvia/sandbox`; this package owns the browser workload running inside that sandbox.
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.waitUntilReady({ timeoutMs: 30_000 });
28
- await using connection = await browser.connect();
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. `pullImage()`, `createBrowser()`, readiness, and CDP
51
- connection are separate operations. `DockerBrowser` owns the underlying sandbox. A
52
- `PlaywrightBrowserConnection` owns only its CDP connection and never destroys the browser.
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
- `stop()` preserves the container. `resumeBrowser({ id })` starts a fresh browser service and requires
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
- The browser tools use ARIA state and strict Playwright locators. They do not expose JavaScript
59
- evaluation, raw CDP, coordinate input, shell access, hidden retries, or automatic reconnection.
60
- Aborting an action that Playwright cannot cancel closes the CDP connection and leaves the browser
61
- running. The selected navigation policy is installed across the connection, so top-level navigation
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
- The image runs Chromium as a non-root user with Chromium sandboxing, the pinned Playwright seccomp
66
- profile, every Linux capability dropped except the explicit `SYS_CHROOT` capability required by the
67
- namespace sandbox, no-new-privileges, and private shared memory. Startup fails rather than silently
68
- switching Chromium to `--no-sandbox`. Docker bridge networking is not an SSRF boundary; use
69
- infrastructure network policy where browsing untrusted destinations requires isolation.
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` is structurally compatible with Studio without creating a package dependency:
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 clean programmatic noVNC viewer in a resizable Playground panel;
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 an active agent
105
- action, blocks new browser tool actions, and expires unless the Studio viewer renews its lease. Takeover
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,2 @@
1
+
2
+ export { }
@@ -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