@anvia/browser 1.1.3 → 1.1.6
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 -2
- package/dist/automation-worker.js +4 -20
- package/dist/automation-worker.js.map +1 -1
- package/dist/chunk-XE5WKN5F.js +195 -0
- package/dist/chunk-XE5WKN5F.js.map +1 -0
- package/dist/index.js +41 -45
- package/dist/index.js.map +1 -1
- package/image/Dockerfile +2 -2
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -217,7 +217,10 @@ The legacy `not_ready` and `human_controlled` codes remain in the public union.
|
|
|
217
217
|
The tools use ARIA state and strict Playwright locators. They do not expose JavaScript evaluation, raw
|
|
218
218
|
CDP, coordinate input, shell access, hidden action retries, or automatic reconnection. The navigation
|
|
219
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.
|
|
220
|
+
redirects, popups, and direct navigation is checked consistently. Private and reserved IP literals
|
|
221
|
+
(loopback, link-local, RFC 1918, and the other IANA special-purpose ranges) are rejected in every
|
|
222
|
+
policy mode, including `origins` entries that list them. The guard matches the literal host, so a
|
|
223
|
+
public domain that resolves to a private address is not covered. The policy does not block third-party
|
|
221
224
|
subresources; Docker bridge networking remains outside that policy.
|
|
222
225
|
|
|
223
226
|
The image runs Chromium as a non-root user with Chromium sandboxing, the Playwright seccomp profile,
|
|
@@ -235,7 +238,7 @@ URLs, environment variables, or logs.
|
|
|
235
238
|
|
|
236
239
|
## Playwright and Node compatibility
|
|
237
240
|
|
|
238
|
-
The host package and browser image pin `playwright-core` 1.
|
|
241
|
+
The host package and browser image pin `playwright-core` 1.63.0 together. Playwright requires matching
|
|
239
242
|
browser/package releases for its bundled browser and declares Node `>=20`; its current system matrix is
|
|
240
243
|
the latest Node 22, 24, or 26. Anvia CI uses Node 24, and the isolation regression test also runs on the
|
|
241
244
|
active host Node version. The package keeps its existing Node `>=20.12` engine for source compatibility,
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
enforceNavigationPolicy
|
|
3
|
+
} from "./chunk-XE5WKN5F.js";
|
|
4
|
+
|
|
1
5
|
// src/automation-worker.ts
|
|
2
6
|
import { randomUUID } from "crypto";
|
|
3
7
|
import { chromium } from "playwright-core";
|
|
@@ -222,26 +226,6 @@ async function setNavigationPolicy(policy) {
|
|
|
222
226
|
assertBrowser().contexts().map((context) => context.route("**/*", (route) => enforceNavigationPolicy(route, policy)))
|
|
223
227
|
);
|
|
224
228
|
}
|
|
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
229
|
function locatorFor(page, target) {
|
|
246
230
|
switch (target.by) {
|
|
247
231
|
case "role": {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/automation-worker.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { Browser, Locator, Page, Route } from \"playwright-core\";\nimport { chromium } from \"playwright-core\";\nimport type {\n AutomationCancel,\n AutomationCommand,\n AutomationRequest,\n AutomationResponse,\n BrowserTarget,\n SerializedError,\n} from \"./automation-protocol\";\nimport type { BrowserNavigationPolicy, BrowserTab } from \"./types\";\n\ntype ActiveOperation = {\n cancelled: boolean;\n page?: Page;\n completed: Promise<void>;\n complete: () => void;\n};\n\nlet browser: Browser | undefined;\nlet selected: Page | undefined;\nconst tabIds = new WeakMap<Page, string>();\nconst pagesById = new Map<string, Page>();\nconst operations = new Map<number, ActiveOperation>();\nlet navigationPolicyKey: string | undefined;\n\nprocess.once(\"disconnect\", () => {\n void closeAfterParentExit();\n});\n\nprocess.on(\"message\", (message: AutomationRequest | AutomationCancel) => {\n if (message.kind === \"cancel\") {\n void cancelOperation(message);\n return;\n }\n void runRequest(message);\n});\n\nasync function runRequest(request: AutomationRequest): Promise<void> {\n let complete: (() => void) | undefined;\n const operation: ActiveOperation = {\n cancelled: false,\n completed: new Promise<void>((resolve) => {\n complete = resolve;\n }),\n complete: () => complete?.(),\n };\n operations.set(request.id, operation);\n try {\n const value = await execute(request, operation);\n send({ kind: \"response\", id: request.id, ok: true, value });\n } catch (error) {\n send({ kind: \"response\", id: request.id, ok: false, error: serializeError(error) });\n } finally {\n operation.complete();\n operations.delete(request.id);\n }\n}\n\nasync function cancelOperation(request: AutomationCancel): Promise<void> {\n const operation = operations.get(request.id);\n if (operation !== undefined) {\n operation.cancelled = true;\n await operation.page?.close().catch(() => undefined);\n await operation.completed;\n }\n send({ kind: \"cancelled\", id: request.id });\n}\n\nasync function execute(request: AutomationRequest, operation: ActiveOperation): Promise<unknown> {\n const command: AutomationCommand = request;\n switch (command.method) {\n case \"connect\": {\n if (browser !== undefined) throw new Error(\"Automation worker is already connected.\");\n const connected = await chromium.connectOverCDP(command.params.endpointUrl, {\n timeout: command.params.timeoutMs,\n });\n try {\n if (operation.cancelled) throw new Error(\"Connection attempt was cancelled.\");\n const context = connected.contexts()[0];\n if (context === undefined) {\n throw new Error(\"CDP connection did not expose a browser context.\");\n }\n const session = await connected.newBrowserCDPSession();\n try {\n await session.send(\"Browser.getVersion\");\n } finally {\n await session.detach().catch(() => undefined);\n }\n if (operation.cancelled) throw new Error(\"Connection attempt was cancelled.\");\n } catch (error) {\n await connected.close().catch(() => undefined);\n throw error;\n }\n browser = connected;\n selected = pages()[0];\n for (const page of pages()) idFor(page);\n connected.once(\"disconnected\", () => {\n browser = undefined;\n send({ kind: \"event\", event: \"disconnected\" });\n });\n return undefined;\n }\n case \"disconnect\": {\n const connected = browser;\n browser = undefined;\n selected = undefined;\n if (connected !== undefined) await connected.close();\n return undefined;\n }\n case \"listTabs\":\n return tabSummaries();\n case \"setNavigationPolicy\":\n await setNavigationPolicy(command.params.policy);\n return undefined;\n case \"openTab\": {\n const context = assertBrowser().contexts()[0];\n if (context === undefined) throw new Error(\"Browser has no CDP context.\");\n const page = await context.newPage();\n operation.page = page;\n if (operation.cancelled) {\n await page.close().catch(() => undefined);\n throw new Error(\"Open-tab operation was cancelled.\");\n }\n selected = page;\n return tabResult(page);\n }\n case \"selectTab\": {\n const page = pageFor(command.params.tabId);\n selected = page;\n return tabResult(page);\n }\n case \"closeTab\": {\n const page = pageFor(command.params.tabId);\n operation.page = page;\n await page.close();\n if (page === selected) selected = pages()[0];\n return { closedTabId: command.params.tabId, tabs: await tabSummaries() };\n }\n case \"navigate\": {\n const page = targetPage(command.params.tabId, operation);\n await page.goto(command.params.url, {\n timeout: command.params.timeoutMs,\n waitUntil: command.params.waitUntil,\n });\n return tabResult(page);\n }\n case \"snapshot\": {\n const page = targetPage(command.params.tabId, operation);\n const snapshot = await page.locator(\"body\").ariaSnapshot({\n timeout: command.params.timeoutMs,\n });\n const truncated = snapshot.length > command.params.maxChars;\n return {\n ...(await tabResult(page)),\n snapshot: truncated ? snapshot.slice(0, command.params.maxChars) : snapshot,\n truncated,\n };\n }\n case \"click\": {\n const page = targetPage(command.params.tabId, operation);\n await locatorFor(page, command.params.target).click({ timeout: command.params.timeoutMs });\n return tabResult(page);\n }\n case \"type\": {\n const page = targetPage(command.params.tabId, operation);\n await locatorFor(page, command.params.target).fill(command.params.text, {\n timeout: command.params.timeoutMs,\n });\n return tabResult(page);\n }\n case \"pressKey\": {\n const page = targetPage(command.params.tabId, operation);\n await page.keyboard.press(command.params.key);\n return tabResult(page);\n }\n case \"screenshot\": {\n const page = targetPage(command.params.tabId, operation);\n const png = await page.screenshot({\n type: \"png\",\n fullPage: false,\n timeout: command.params.timeoutMs,\n });\n return { metadata: await tabResult(page), pngBase64: png.toString(\"base64\") };\n }\n }\n}\n\nfunction assertBrowser(): Browser {\n if (browser === undefined || !browser.isConnected())\n throw new Error(\"Browser connection is closed.\");\n return browser;\n}\n\nfunction pages(): Page[] {\n return browser?.contexts().flatMap((context) => context.pages()) ?? [];\n}\n\nfunction idFor(page: Page): string {\n const existing = tabIds.get(page);\n if (existing !== undefined) return existing;\n const id = randomUUID();\n tabIds.set(page, id);\n pagesById.set(id, page);\n page.once(\"close\", () => pagesById.delete(id));\n return id;\n}\n\nfunction pageFor(id: string): Page {\n assertBrowser();\n const page = pagesById.get(id);\n if (page === undefined || page.isClosed() || !pages().includes(page)) {\n throw new Error(`Browser tab does not exist: ${id}`);\n }\n return page;\n}\n\nfunction targetPage(id: string, operation: ActiveOperation): Page {\n const page = pageFor(id);\n operation.page = page;\n return page;\n}\n\nasync function tabSummaries(): Promise<readonly BrowserTab[]> {\n assertBrowser();\n if (selected?.isClosed()) selected = pages()[0];\n return Promise.all(\n pages().map(async (page) => ({\n id: idFor(page),\n title: await page.title(),\n url: page.url(),\n selected: page === selected,\n })),\n );\n}\n\nasync function tabResult(page: Page) {\n return { tabId: idFor(page), title: await page.title(), url: page.url() };\n}\n\nasync function setNavigationPolicy(policy: BrowserNavigationPolicy): Promise<void> {\n const key = JSON.stringify(policy);\n if (navigationPolicyKey !== undefined) {\n if (navigationPolicyKey !== key) {\n throw new TypeError(\"Browser connection already has a different navigation policy.\");\n }\n return;\n }\n navigationPolicyKey = key;\n await Promise.all(\n assertBrowser()\n .contexts()\n .map((context) => context.route(\"**/*\", (route) => enforceNavigationPolicy(route, policy))),\n );\n}\n\nasync function enforceNavigationPolicy(\n route: Route,\n policy: BrowserNavigationPolicy,\n): Promise<void> {\n const request = route.request();\n const frame = request.frame();\n if (\n request.isNavigationRequest() &&\n frame === frame.page().mainFrame() &&\n !isNavigationAllowed(request.url(), policy)\n ) {\n await route.abort(\"blockedbyclient\");\n return;\n }\n await route.continue();\n}\n\nfunction isNavigationAllowed(value: string, policy: BrowserNavigationPolicy): boolean {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return false;\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return false;\n if (url.username.length > 0 || url.password.length > 0) return false;\n return policy.mode === \"allow-all-http\" || policy.origins.includes(url.origin);\n}\n\nfunction locatorFor(page: Page, target: BrowserTarget): Locator {\n switch (target.by) {\n case \"role\": {\n const options: { name?: string; exact?: boolean } = {};\n if (target.name !== undefined) options.name = target.name;\n if (target.exact !== undefined) options.exact = target.exact;\n return page.getByRole(target.role as never, options);\n }\n case \"text\": {\n const options: { exact?: boolean } = {};\n if (target.exact !== undefined) options.exact = target.exact;\n return page.getByText(target.text, options);\n }\n case \"label\": {\n const options: { exact?: boolean } = {};\n if (target.exact !== undefined) options.exact = target.exact;\n return page.getByLabel(target.label, options);\n }\n case \"placeholder\": {\n const options: { exact?: boolean } = {};\n if (target.exact !== undefined) options.exact = target.exact;\n return page.getByPlaceholder(target.placeholder, options);\n }\n case \"test-id\":\n return page.getByTestId(target.testId);\n case \"css\":\n return page.locator(target.selector);\n }\n}\n\nfunction serializeError(error: unknown): SerializedError {\n if (error instanceof Error) {\n const value: { name: string; message: string; stack?: string; code?: string } = {\n name: error.name,\n message: error.message,\n };\n if (error.stack !== undefined) value.stack = error.stack;\n if (\"code\" in error && typeof error.code === \"string\") value.code = error.code;\n return value;\n }\n return { name: \"Error\", message: String(error) };\n}\n\nfunction send(message: AutomationResponse): void {\n if (!process.connected || process.send === undefined) return;\n process.send(message, () => undefined);\n}\n\nasync function closeAfterParentExit(): Promise<void> {\n const forceExit = setTimeout(() => process.exit(1), 2_000);\n const connected = browser;\n browser = undefined;\n await connected?.close().catch(() => undefined);\n clearTimeout(forceExit);\n process.exit(0);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/automation-worker.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { Browser, Locator, Page } from \"playwright-core\";\nimport { chromium } from \"playwright-core\";\nimport type {\n AutomationCancel,\n AutomationCommand,\n AutomationRequest,\n AutomationResponse,\n BrowserTarget,\n SerializedError,\n} from \"./automation-protocol\";\nimport { enforceNavigationPolicy } from \"./internal/navigation-policy\";\nimport type { BrowserNavigationPolicy, BrowserTab } from \"./types\";\n\ntype ActiveOperation = {\n cancelled: boolean;\n page?: Page;\n completed: Promise<void>;\n complete: () => void;\n};\n\nlet browser: Browser | undefined;\nlet selected: Page | undefined;\nconst tabIds = new WeakMap<Page, string>();\nconst pagesById = new Map<string, Page>();\nconst operations = new Map<number, ActiveOperation>();\nlet navigationPolicyKey: string | undefined;\n\nprocess.once(\"disconnect\", () => {\n void closeAfterParentExit();\n});\n\nprocess.on(\"message\", (message: AutomationRequest | AutomationCancel) => {\n if (message.kind === \"cancel\") {\n void cancelOperation(message);\n return;\n }\n void runRequest(message);\n});\n\nasync function runRequest(request: AutomationRequest): Promise<void> {\n let complete: (() => void) | undefined;\n const operation: ActiveOperation = {\n cancelled: false,\n completed: new Promise<void>((resolve) => {\n complete = resolve;\n }),\n complete: () => complete?.(),\n };\n operations.set(request.id, operation);\n try {\n const value = await execute(request, operation);\n send({ kind: \"response\", id: request.id, ok: true, value });\n } catch (error) {\n send({ kind: \"response\", id: request.id, ok: false, error: serializeError(error) });\n } finally {\n operation.complete();\n operations.delete(request.id);\n }\n}\n\nasync function cancelOperation(request: AutomationCancel): Promise<void> {\n const operation = operations.get(request.id);\n if (operation !== undefined) {\n operation.cancelled = true;\n await operation.page?.close().catch(() => undefined);\n await operation.completed;\n }\n send({ kind: \"cancelled\", id: request.id });\n}\n\nasync function execute(request: AutomationRequest, operation: ActiveOperation): Promise<unknown> {\n const command: AutomationCommand = request;\n switch (command.method) {\n case \"connect\": {\n if (browser !== undefined) throw new Error(\"Automation worker is already connected.\");\n const connected = await chromium.connectOverCDP(command.params.endpointUrl, {\n timeout: command.params.timeoutMs,\n });\n try {\n if (operation.cancelled) throw new Error(\"Connection attempt was cancelled.\");\n const context = connected.contexts()[0];\n if (context === undefined) {\n throw new Error(\"CDP connection did not expose a browser context.\");\n }\n const session = await connected.newBrowserCDPSession();\n try {\n await session.send(\"Browser.getVersion\");\n } finally {\n await session.detach().catch(() => undefined);\n }\n if (operation.cancelled) throw new Error(\"Connection attempt was cancelled.\");\n } catch (error) {\n await connected.close().catch(() => undefined);\n throw error;\n }\n browser = connected;\n selected = pages()[0];\n for (const page of pages()) idFor(page);\n connected.once(\"disconnected\", () => {\n browser = undefined;\n send({ kind: \"event\", event: \"disconnected\" });\n });\n return undefined;\n }\n case \"disconnect\": {\n const connected = browser;\n browser = undefined;\n selected = undefined;\n if (connected !== undefined) await connected.close();\n return undefined;\n }\n case \"listTabs\":\n return tabSummaries();\n case \"setNavigationPolicy\":\n await setNavigationPolicy(command.params.policy);\n return undefined;\n case \"openTab\": {\n const context = assertBrowser().contexts()[0];\n if (context === undefined) throw new Error(\"Browser has no CDP context.\");\n const page = await context.newPage();\n operation.page = page;\n if (operation.cancelled) {\n await page.close().catch(() => undefined);\n throw new Error(\"Open-tab operation was cancelled.\");\n }\n selected = page;\n return tabResult(page);\n }\n case \"selectTab\": {\n const page = pageFor(command.params.tabId);\n selected = page;\n return tabResult(page);\n }\n case \"closeTab\": {\n const page = pageFor(command.params.tabId);\n operation.page = page;\n await page.close();\n if (page === selected) selected = pages()[0];\n return { closedTabId: command.params.tabId, tabs: await tabSummaries() };\n }\n case \"navigate\": {\n const page = targetPage(command.params.tabId, operation);\n await page.goto(command.params.url, {\n timeout: command.params.timeoutMs,\n waitUntil: command.params.waitUntil,\n });\n return tabResult(page);\n }\n case \"snapshot\": {\n const page = targetPage(command.params.tabId, operation);\n const snapshot = await page.locator(\"body\").ariaSnapshot({\n timeout: command.params.timeoutMs,\n });\n const truncated = snapshot.length > command.params.maxChars;\n return {\n ...(await tabResult(page)),\n snapshot: truncated ? snapshot.slice(0, command.params.maxChars) : snapshot,\n truncated,\n };\n }\n case \"click\": {\n const page = targetPage(command.params.tabId, operation);\n await locatorFor(page, command.params.target).click({ timeout: command.params.timeoutMs });\n return tabResult(page);\n }\n case \"type\": {\n const page = targetPage(command.params.tabId, operation);\n await locatorFor(page, command.params.target).fill(command.params.text, {\n timeout: command.params.timeoutMs,\n });\n return tabResult(page);\n }\n case \"pressKey\": {\n const page = targetPage(command.params.tabId, operation);\n await page.keyboard.press(command.params.key);\n return tabResult(page);\n }\n case \"screenshot\": {\n const page = targetPage(command.params.tabId, operation);\n const png = await page.screenshot({\n type: \"png\",\n fullPage: false,\n timeout: command.params.timeoutMs,\n });\n return { metadata: await tabResult(page), pngBase64: png.toString(\"base64\") };\n }\n }\n}\n\nfunction assertBrowser(): Browser {\n if (browser === undefined || !browser.isConnected())\n throw new Error(\"Browser connection is closed.\");\n return browser;\n}\n\nfunction pages(): Page[] {\n return browser?.contexts().flatMap((context) => context.pages()) ?? [];\n}\n\nfunction idFor(page: Page): string {\n const existing = tabIds.get(page);\n if (existing !== undefined) return existing;\n const id = randomUUID();\n tabIds.set(page, id);\n pagesById.set(id, page);\n page.once(\"close\", () => pagesById.delete(id));\n return id;\n}\n\nfunction pageFor(id: string): Page {\n assertBrowser();\n const page = pagesById.get(id);\n if (page === undefined || page.isClosed() || !pages().includes(page)) {\n throw new Error(`Browser tab does not exist: ${id}`);\n }\n return page;\n}\n\nfunction targetPage(id: string, operation: ActiveOperation): Page {\n const page = pageFor(id);\n operation.page = page;\n return page;\n}\n\nasync function tabSummaries(): Promise<readonly BrowserTab[]> {\n assertBrowser();\n if (selected?.isClosed()) selected = pages()[0];\n return Promise.all(\n pages().map(async (page) => ({\n id: idFor(page),\n title: await page.title(),\n url: page.url(),\n selected: page === selected,\n })),\n );\n}\n\nasync function tabResult(page: Page) {\n return { tabId: idFor(page), title: await page.title(), url: page.url() };\n}\n\nasync function setNavigationPolicy(policy: BrowserNavigationPolicy): Promise<void> {\n const key = JSON.stringify(policy);\n if (navigationPolicyKey !== undefined) {\n if (navigationPolicyKey !== key) {\n throw new TypeError(\"Browser connection already has a different navigation policy.\");\n }\n return;\n }\n navigationPolicyKey = key;\n await Promise.all(\n assertBrowser()\n .contexts()\n .map((context) => context.route(\"**/*\", (route) => enforceNavigationPolicy(route, policy))),\n );\n}\n\nfunction locatorFor(page: Page, target: BrowserTarget): Locator {\n switch (target.by) {\n case \"role\": {\n const options: { name?: string; exact?: boolean } = {};\n if (target.name !== undefined) options.name = target.name;\n if (target.exact !== undefined) options.exact = target.exact;\n return page.getByRole(target.role as never, options);\n }\n case \"text\": {\n const options: { exact?: boolean } = {};\n if (target.exact !== undefined) options.exact = target.exact;\n return page.getByText(target.text, options);\n }\n case \"label\": {\n const options: { exact?: boolean } = {};\n if (target.exact !== undefined) options.exact = target.exact;\n return page.getByLabel(target.label, options);\n }\n case \"placeholder\": {\n const options: { exact?: boolean } = {};\n if (target.exact !== undefined) options.exact = target.exact;\n return page.getByPlaceholder(target.placeholder, options);\n }\n case \"test-id\":\n return page.getByTestId(target.testId);\n case \"css\":\n return page.locator(target.selector);\n }\n}\n\nfunction serializeError(error: unknown): SerializedError {\n if (error instanceof Error) {\n const value: { name: string; message: string; stack?: string; code?: string } = {\n name: error.name,\n message: error.message,\n };\n if (error.stack !== undefined) value.stack = error.stack;\n if (\"code\" in error && typeof error.code === \"string\") value.code = error.code;\n return value;\n }\n return { name: \"Error\", message: String(error) };\n}\n\nfunction send(message: AutomationResponse): void {\n if (!process.connected || process.send === undefined) return;\n process.send(message, () => undefined);\n}\n\nasync function closeAfterParentExit(): Promise<void> {\n const forceExit = setTimeout(() => process.exit(1), 2_000);\n const connected = browser;\n browser = undefined;\n await connected?.close().catch(() => undefined);\n clearTimeout(forceExit);\n process.exit(0);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// src/internal/ip-check.ts
|
|
2
|
+
var BLOCKED_IPV4 = [
|
|
3
|
+
["0.0.0.0", 8],
|
|
4
|
+
// "this network"
|
|
5
|
+
["10.0.0.0", 8],
|
|
6
|
+
// private network
|
|
7
|
+
["100.64.0.0", 10],
|
|
8
|
+
// shared address space (carrier-grade NAT)
|
|
9
|
+
["127.0.0.0", 8],
|
|
10
|
+
// loopback
|
|
11
|
+
["169.254.0.0", 16],
|
|
12
|
+
// link-local, includes cloud metadata at 169.254.169.254
|
|
13
|
+
["172.16.0.0", 12],
|
|
14
|
+
// private network
|
|
15
|
+
["192.0.0.0", 24],
|
|
16
|
+
// IETF protocol assignments
|
|
17
|
+
["192.0.2.0", 24],
|
|
18
|
+
// documentation (TEST-NET-1)
|
|
19
|
+
["192.88.99.0", 24],
|
|
20
|
+
// 6to4 relay anycast (deprecated)
|
|
21
|
+
["192.168.0.0", 16],
|
|
22
|
+
// private network
|
|
23
|
+
["198.18.0.0", 15],
|
|
24
|
+
// benchmarking
|
|
25
|
+
["198.51.100.0", 24],
|
|
26
|
+
// documentation (TEST-NET-2)
|
|
27
|
+
["203.0.113.0", 24],
|
|
28
|
+
// documentation (TEST-NET-3)
|
|
29
|
+
["224.0.0.0", 4],
|
|
30
|
+
// multicast
|
|
31
|
+
["240.0.0.0", 4]
|
|
32
|
+
// reserved, includes 255.255.255.255
|
|
33
|
+
];
|
|
34
|
+
var BLOCKED_IPV6 = [
|
|
35
|
+
["::", 96],
|
|
36
|
+
// unspecified, loopback, and deprecated IPv4-compatible addresses
|
|
37
|
+
["::ffff:0:0", 96],
|
|
38
|
+
// IPv4-mapped (alternate spelling of an IPv4 destination)
|
|
39
|
+
["64:ff9b:1::", 48],
|
|
40
|
+
// NAT64 local-use prefix
|
|
41
|
+
["100::", 64],
|
|
42
|
+
// discard-only
|
|
43
|
+
["100:0:0:1::", 64],
|
|
44
|
+
// dummy IPv6 prefix
|
|
45
|
+
["2001::", 32],
|
|
46
|
+
// Teredo
|
|
47
|
+
["2001:2::", 48],
|
|
48
|
+
// benchmarking
|
|
49
|
+
["2001:10::", 28],
|
|
50
|
+
// ORCHID (deprecated)
|
|
51
|
+
["2001:db8::", 32],
|
|
52
|
+
// documentation
|
|
53
|
+
["2002::", 16],
|
|
54
|
+
// 6to4
|
|
55
|
+
["3fff::", 20],
|
|
56
|
+
// documentation
|
|
57
|
+
["5f00::", 16],
|
|
58
|
+
// segment routing SIDs
|
|
59
|
+
["fc00::", 7],
|
|
60
|
+
// unique local
|
|
61
|
+
["fe80::", 10],
|
|
62
|
+
// link-local
|
|
63
|
+
["fec0::", 10],
|
|
64
|
+
// site-local (deprecated)
|
|
65
|
+
["ff00::", 8]
|
|
66
|
+
// multicast
|
|
67
|
+
];
|
|
68
|
+
var GLOBALLY_REACHABLE_IPV4 = /* @__PURE__ */ new Set([3221225481, 3221225482]);
|
|
69
|
+
var BLOCKED_IPV4_RANGES = compileIpv4Ranges(BLOCKED_IPV4);
|
|
70
|
+
var BLOCKED_IPV6_RANGES = compileIpv6Ranges(BLOCKED_IPV6);
|
|
71
|
+
function isPrivateOrReservedHost(hostname) {
|
|
72
|
+
const host = stripBrackets(hostname).toLowerCase().replace(/\.+$/, "");
|
|
73
|
+
if (host === "localhost" || host.endsWith(".localhost")) return true;
|
|
74
|
+
const ipv4 = parseIpv4(host);
|
|
75
|
+
if (ipv4 !== null) return isBlockedIpv4(ipv4);
|
|
76
|
+
const ipv6 = parseIpv6(host);
|
|
77
|
+
if (ipv6 !== null) return isBlockedIpv6(ipv6);
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
function stripBrackets(hostname) {
|
|
81
|
+
return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
82
|
+
}
|
|
83
|
+
function isBlockedIpv4(value) {
|
|
84
|
+
if (GLOBALLY_REACHABLE_IPV4.has(value)) return false;
|
|
85
|
+
for (const [network, prefix] of BLOCKED_IPV4_RANGES) {
|
|
86
|
+
if (isInIpv4Range(value, network, prefix)) return true;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
function isBlockedIpv6(value) {
|
|
91
|
+
for (const [network, prefix] of BLOCKED_IPV6_RANGES) {
|
|
92
|
+
if (isInIpv6Range(value, network, prefix)) return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
function isInIpv4Range(value, network, prefix) {
|
|
97
|
+
if (prefix === 0) return true;
|
|
98
|
+
const shift = 32 - prefix;
|
|
99
|
+
return value >>> shift === network >>> shift;
|
|
100
|
+
}
|
|
101
|
+
function isInIpv6Range(value, network, prefix) {
|
|
102
|
+
if (prefix === 0) return true;
|
|
103
|
+
const shift = BigInt(128 - prefix);
|
|
104
|
+
return value >> shift === network >> shift;
|
|
105
|
+
}
|
|
106
|
+
function parseIpv4(value) {
|
|
107
|
+
const parts = value.split(".");
|
|
108
|
+
if (parts.length !== 4) return null;
|
|
109
|
+
let result = 0;
|
|
110
|
+
for (const part of parts) {
|
|
111
|
+
if (!/^\d{1,3}$/.test(part)) return null;
|
|
112
|
+
const octet = Number(part);
|
|
113
|
+
if (octet > 255) return null;
|
|
114
|
+
result = result * 256 + octet;
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
function parseIpv6(value) {
|
|
119
|
+
const zone = value.indexOf("%");
|
|
120
|
+
const address = zone === -1 ? value : value.slice(0, zone);
|
|
121
|
+
let text = address;
|
|
122
|
+
const embedded = /(\d{1,3}(?:\.\d{1,3}){3})$/.exec(address);
|
|
123
|
+
if (embedded !== null) {
|
|
124
|
+
const ipv4 = parseIpv4(embedded[1] ?? "");
|
|
125
|
+
if (ipv4 === null) return null;
|
|
126
|
+
const groups = `${(ipv4 >>> 16).toString(16)}:${(ipv4 & 65535).toString(16)}`;
|
|
127
|
+
text = `${address.slice(0, embedded.index)}${groups}`;
|
|
128
|
+
}
|
|
129
|
+
const halves = text.split("::");
|
|
130
|
+
if (halves.length > 2) return null;
|
|
131
|
+
if (halves.length === 1) {
|
|
132
|
+
const groups = text.split(":");
|
|
133
|
+
return groups.length === 8 ? foldHexGroups(groups) : null;
|
|
134
|
+
}
|
|
135
|
+
const [head = "", tail = ""] = halves;
|
|
136
|
+
const leading = head === "" ? [] : head.split(":");
|
|
137
|
+
const trailing = tail === "" ? [] : tail.split(":");
|
|
138
|
+
const missing = 8 - leading.length - trailing.length;
|
|
139
|
+
if (missing < 1) return null;
|
|
140
|
+
const foldedHead = foldHexGroups(leading);
|
|
141
|
+
const foldedTail = foldHexGroups(trailing);
|
|
142
|
+
if (foldedHead === null || foldedTail === null) return null;
|
|
143
|
+
return foldedHead << BigInt(16 * (missing + trailing.length)) | foldedTail;
|
|
144
|
+
}
|
|
145
|
+
function foldHexGroups(groups) {
|
|
146
|
+
let value = 0n;
|
|
147
|
+
for (const group of groups) {
|
|
148
|
+
if (!/^[0-9a-f]{1,4}$/.test(group)) return null;
|
|
149
|
+
value = value << 16n | BigInt(parseInt(group, 16));
|
|
150
|
+
}
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
function compileIpv4Ranges(ranges) {
|
|
154
|
+
return ranges.map(([network, prefix]) => {
|
|
155
|
+
const value = parseIpv4(network);
|
|
156
|
+
if (value === null) throw new TypeError(`Invalid IPv4 range: ${network}/${prefix}`);
|
|
157
|
+
return [value, prefix];
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function compileIpv6Ranges(ranges) {
|
|
161
|
+
return ranges.map(([network, prefix]) => {
|
|
162
|
+
const value = parseIpv6(network);
|
|
163
|
+
if (value === null) throw new TypeError(`Invalid IPv6 range: ${network}/${prefix}`);
|
|
164
|
+
return [value, prefix];
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/internal/navigation-policy.ts
|
|
169
|
+
function isNavigationAllowed(value, policy) {
|
|
170
|
+
let url;
|
|
171
|
+
try {
|
|
172
|
+
url = new URL(value);
|
|
173
|
+
} catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
177
|
+
if (url.username.length > 0 || url.password.length > 0) return false;
|
|
178
|
+
if (isPrivateOrReservedHost(url.hostname)) return false;
|
|
179
|
+
return policy.mode === "allow-all-http" || policy.origins.includes(url.origin);
|
|
180
|
+
}
|
|
181
|
+
async function enforceNavigationPolicy(route, policy) {
|
|
182
|
+
const request = route.request();
|
|
183
|
+
const frame = request.frame();
|
|
184
|
+
if (request.isNavigationRequest() && frame === frame.page().mainFrame() && !isNavigationAllowed(request.url(), policy)) {
|
|
185
|
+
await route.abort("blockedbyclient");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
await route.continue();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export {
|
|
192
|
+
isNavigationAllowed,
|
|
193
|
+
enforceNavigationPolicy
|
|
194
|
+
};
|
|
195
|
+
//# sourceMappingURL=chunk-XE5WKN5F.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/internal/ip-check.ts","../src/internal/navigation-policy.ts"],"sourcesContent":["/**\n * Shared SSRF guard: classify a hostname as private, reserved, or public.\n *\n * Called from the navigation policy checks in ./navigation-policy.ts, which run both\n * before the tool dispatches a navigation and inside the worker's route handler for\n * every top-level navigation request, including redirects.\n *\n * Blocked ranges follow the IANA IPv4/IPv6 special-purpose address registries.\n * Addresses that IANA marks globally reachable inside otherwise special-purpose space\n * stay reachable.\n *\n * IPv4 spellings are normalized by the WHATWG URL parser before this guard runs\n * (`127.1`, `2130706433`, `0x7f.0.0.1`, and `0177.0.0.1` all arrive as `127.0.0.1`),\n * and hosts that are not valid literals are left to DNS.\n */\n\ntype AddressRange = readonly [network: string, prefix: number];\n\n/** Non-global IPv4 ranges plus transition mechanisms that can encode another address. */\nconst BLOCKED_IPV4: readonly AddressRange[] = [\n [\"0.0.0.0\", 8], // \"this network\"\n [\"10.0.0.0\", 8], // private network\n [\"100.64.0.0\", 10], // shared address space (carrier-grade NAT)\n [\"127.0.0.0\", 8], // loopback\n [\"169.254.0.0\", 16], // link-local, includes cloud metadata at 169.254.169.254\n [\"172.16.0.0\", 12], // private network\n [\"192.0.0.0\", 24], // IETF protocol assignments\n [\"192.0.2.0\", 24], // documentation (TEST-NET-1)\n [\"192.88.99.0\", 24], // 6to4 relay anycast (deprecated)\n [\"192.168.0.0\", 16], // private network\n [\"198.18.0.0\", 15], // benchmarking\n [\"198.51.100.0\", 24], // documentation (TEST-NET-2)\n [\"203.0.113.0\", 24], // documentation (TEST-NET-3)\n [\"224.0.0.0\", 4], // multicast\n [\"240.0.0.0\", 4], // reserved, includes 255.255.255.255\n];\n\n/** Non-global IPv6 ranges plus transition mechanisms that can encode another address. */\nconst BLOCKED_IPV6: readonly AddressRange[] = [\n [\"::\", 96], // unspecified, loopback, and deprecated IPv4-compatible addresses\n [\"::ffff:0:0\", 96], // IPv4-mapped (alternate spelling of an IPv4 destination)\n [\"64:ff9b:1::\", 48], // NAT64 local-use prefix\n [\"100::\", 64], // discard-only\n [\"100:0:0:1::\", 64], // dummy IPv6 prefix\n [\"2001::\", 32], // Teredo\n [\"2001:2::\", 48], // benchmarking\n [\"2001:10::\", 28], // ORCHID (deprecated)\n [\"2001:db8::\", 32], // documentation\n [\"2002::\", 16], // 6to4\n [\"3fff::\", 20], // documentation\n [\"5f00::\", 16], // segment routing SIDs\n [\"fc00::\", 7], // unique local\n [\"fe80::\", 10], // link-local\n [\"fec0::\", 10], // site-local (deprecated)\n [\"ff00::\", 8], // multicast\n];\n\n/** IANA marks these two /32s globally reachable although 192.0.0.0/24 is not. */\nconst GLOBALLY_REACHABLE_IPV4: ReadonlySet<number> = new Set([0xc0000009, 0xc000000a]);\n\nconst BLOCKED_IPV4_RANGES = compileIpv4Ranges(BLOCKED_IPV4);\nconst BLOCKED_IPV6_RANGES = compileIpv6Ranges(BLOCKED_IPV6);\n\nexport function isPrivateOrReservedHost(hostname: string): boolean {\n const host = stripBrackets(hostname).toLowerCase().replace(/\\.+$/, \"\");\n\n if (host === \"localhost\" || host.endsWith(\".localhost\")) return true;\n\n const ipv4 = parseIpv4(host);\n if (ipv4 !== null) return isBlockedIpv4(ipv4);\n\n const ipv6 = parseIpv6(host);\n if (ipv6 !== null) return isBlockedIpv6(ipv6);\n\n return false;\n}\n\nfunction stripBrackets(hostname: string): string {\n return hostname.startsWith(\"[\") && hostname.endsWith(\"]\") ? hostname.slice(1, -1) : hostname;\n}\n\nfunction isBlockedIpv4(value: number): boolean {\n if (GLOBALLY_REACHABLE_IPV4.has(value)) return false;\n for (const [network, prefix] of BLOCKED_IPV4_RANGES) {\n if (isInIpv4Range(value, network, prefix)) return true;\n }\n return false;\n}\n\nfunction isBlockedIpv6(value: bigint): boolean {\n for (const [network, prefix] of BLOCKED_IPV6_RANGES) {\n if (isInIpv6Range(value, network, prefix)) return true;\n }\n return false;\n}\n\nfunction isInIpv4Range(value: number, network: number, prefix: number): boolean {\n if (prefix === 0) return true;\n const shift = 32 - prefix;\n return value >>> shift === network >>> shift;\n}\n\nfunction isInIpv6Range(value: bigint, network: bigint, prefix: number): boolean {\n if (prefix === 0) return true;\n const shift = BigInt(128 - prefix);\n return value >> shift === network >> shift;\n}\n\n/** Parse a dotted quad into its unsigned 32-bit value, or null when it is not one. */\nfunction parseIpv4(value: string): number | null {\n const parts = value.split(\".\");\n if (parts.length !== 4) return null;\n let result = 0;\n for (const part of parts) {\n if (!/^\\d{1,3}$/.test(part)) return null;\n const octet = Number(part);\n if (octet > 255) return null;\n result = result * 256 + octet;\n }\n return result;\n}\n\n/** Parse an IPv6 address (with optional embedded IPv4) into its 128-bit value. */\nfunction parseIpv6(value: string): bigint | null {\n // Zone identifiers (`fe80::1%eth0`) are invalid inside URLs; strip the zone so a\n // caller passing one directly still gets the address classified.\n const zone = value.indexOf(\"%\");\n const address = zone === -1 ? value : value.slice(0, zone);\n\n let text = address;\n const embedded = /(\\d{1,3}(?:\\.\\d{1,3}){3})$/.exec(address);\n if (embedded !== null) {\n const ipv4 = parseIpv4(embedded[1] ?? \"\");\n if (ipv4 === null) return null;\n const groups = `${(ipv4 >>> 16).toString(16)}:${(ipv4 & 0xffff).toString(16)}`;\n text = `${address.slice(0, embedded.index)}${groups}`;\n }\n\n const halves = text.split(\"::\");\n if (halves.length > 2) return null;\n if (halves.length === 1) {\n const groups = text.split(\":\");\n return groups.length === 8 ? foldHexGroups(groups) : null;\n }\n\n const [head = \"\", tail = \"\"] = halves;\n const leading = head === \"\" ? [] : head.split(\":\");\n const trailing = tail === \"\" ? [] : tail.split(\":\");\n const missing = 8 - leading.length - trailing.length;\n if (missing < 1) return null;\n const foldedHead = foldHexGroups(leading);\n const foldedTail = foldHexGroups(trailing);\n if (foldedHead === null || foldedTail === null) return null;\n return (foldedHead << BigInt(16 * (missing + trailing.length))) | foldedTail;\n}\n\nfunction foldHexGroups(groups: readonly string[]): bigint | null {\n let value = 0n;\n for (const group of groups) {\n if (!/^[0-9a-f]{1,4}$/.test(group)) return null;\n value = (value << 16n) | BigInt(parseInt(group, 16));\n }\n return value;\n}\n\nfunction compileIpv4Ranges(\n ranges: readonly AddressRange[],\n): ReadonlyArray<readonly [number, number]> {\n return ranges.map(([network, prefix]) => {\n const value = parseIpv4(network);\n if (value === null) throw new TypeError(`Invalid IPv4 range: ${network}/${prefix}`);\n return [value, prefix] as const;\n });\n}\n\nfunction compileIpv6Ranges(\n ranges: readonly AddressRange[],\n): ReadonlyArray<readonly [bigint, number]> {\n return ranges.map(([network, prefix]) => {\n const value = parseIpv6(network);\n if (value === null) throw new TypeError(`Invalid IPv6 range: ${network}/${prefix}`);\n return [value, prefix] as const;\n });\n}\n","/**\n * Shared navigation policy checks.\n *\n * Used by the tool layer (tools.ts) to reject a navigation before it is dispatched and\n * by the worker's route handler (automation-worker.ts) to re-check every top-level\n * navigation request, so a redirect cannot reach a destination the policy forbids.\n */\nimport type { Route } from \"playwright-core\";\nimport type { BrowserNavigationPolicy } from \"../types\";\nimport { isPrivateOrReservedHost } from \"./ip-check\";\n\nexport function isNavigationAllowed(value: string, policy: BrowserNavigationPolicy): boolean {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return false;\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return false;\n if (url.username.length > 0 || url.password.length > 0) return false;\n\n // Private and reserved literal destinations stay blocked under every policy mode.\n if (isPrivateOrReservedHost(url.hostname)) return false;\n\n return policy.mode === \"allow-all-http\" || policy.origins.includes(url.origin);\n}\n\n/**\n * Abort top-level navigation requests that the policy rejects, including each redirect\n * hop Chromium follows after an allowed initial navigation.\n */\nexport async function enforceNavigationPolicy(\n route: Route,\n policy: BrowserNavigationPolicy,\n): Promise<void> {\n const request = route.request();\n const frame = request.frame();\n if (\n request.isNavigationRequest() &&\n frame === frame.page().mainFrame() &&\n !isNavigationAllowed(request.url(), policy)\n ) {\n await route.abort(\"blockedbyclient\");\n return;\n }\n await route.continue();\n}\n"],"mappings":";AAmBA,IAAM,eAAwC;AAAA,EAC5C,CAAC,WAAW,CAAC;AAAA;AAAA,EACb,CAAC,YAAY,CAAC;AAAA;AAAA,EACd,CAAC,cAAc,EAAE;AAAA;AAAA,EACjB,CAAC,aAAa,CAAC;AAAA;AAAA,EACf,CAAC,eAAe,EAAE;AAAA;AAAA,EAClB,CAAC,cAAc,EAAE;AAAA;AAAA,EACjB,CAAC,aAAa,EAAE;AAAA;AAAA,EAChB,CAAC,aAAa,EAAE;AAAA;AAAA,EAChB,CAAC,eAAe,EAAE;AAAA;AAAA,EAClB,CAAC,eAAe,EAAE;AAAA;AAAA,EAClB,CAAC,cAAc,EAAE;AAAA;AAAA,EACjB,CAAC,gBAAgB,EAAE;AAAA;AAAA,EACnB,CAAC,eAAe,EAAE;AAAA;AAAA,EAClB,CAAC,aAAa,CAAC;AAAA;AAAA,EACf,CAAC,aAAa,CAAC;AAAA;AACjB;AAGA,IAAM,eAAwC;AAAA,EAC5C,CAAC,MAAM,EAAE;AAAA;AAAA,EACT,CAAC,cAAc,EAAE;AAAA;AAAA,EACjB,CAAC,eAAe,EAAE;AAAA;AAAA,EAClB,CAAC,SAAS,EAAE;AAAA;AAAA,EACZ,CAAC,eAAe,EAAE;AAAA;AAAA,EAClB,CAAC,UAAU,EAAE;AAAA;AAAA,EACb,CAAC,YAAY,EAAE;AAAA;AAAA,EACf,CAAC,aAAa,EAAE;AAAA;AAAA,EAChB,CAAC,cAAc,EAAE;AAAA;AAAA,EACjB,CAAC,UAAU,EAAE;AAAA;AAAA,EACb,CAAC,UAAU,EAAE;AAAA;AAAA,EACb,CAAC,UAAU,EAAE;AAAA;AAAA,EACb,CAAC,UAAU,CAAC;AAAA;AAAA,EACZ,CAAC,UAAU,EAAE;AAAA;AAAA,EACb,CAAC,UAAU,EAAE;AAAA;AAAA,EACb,CAAC,UAAU,CAAC;AAAA;AACd;AAGA,IAAM,0BAA+C,oBAAI,IAAI,CAAC,YAAY,UAAU,CAAC;AAErF,IAAM,sBAAsB,kBAAkB,YAAY;AAC1D,IAAM,sBAAsB,kBAAkB,YAAY;AAEnD,SAAS,wBAAwB,UAA2B;AACjE,QAAM,OAAO,cAAc,QAAQ,EAAE,YAAY,EAAE,QAAQ,QAAQ,EAAE;AAErE,MAAI,SAAS,eAAe,KAAK,SAAS,YAAY,EAAG,QAAO;AAEhE,QAAM,OAAO,UAAU,IAAI;AAC3B,MAAI,SAAS,KAAM,QAAO,cAAc,IAAI;AAE5C,QAAM,OAAO,UAAU,IAAI;AAC3B,MAAI,SAAS,KAAM,QAAO,cAAc,IAAI;AAE5C,SAAO;AACT;AAEA,SAAS,cAAc,UAA0B;AAC/C,SAAO,SAAS,WAAW,GAAG,KAAK,SAAS,SAAS,GAAG,IAAI,SAAS,MAAM,GAAG,EAAE,IAAI;AACtF;AAEA,SAAS,cAAc,OAAwB;AAC7C,MAAI,wBAAwB,IAAI,KAAK,EAAG,QAAO;AAC/C,aAAW,CAAC,SAAS,MAAM,KAAK,qBAAqB;AACnD,QAAI,cAAc,OAAO,SAAS,MAAM,EAAG,QAAO;AAAA,EACpD;AACA,SAAO;AACT;AAEA,SAAS,cAAc,OAAwB;AAC7C,aAAW,CAAC,SAAS,MAAM,KAAK,qBAAqB;AACnD,QAAI,cAAc,OAAO,SAAS,MAAM,EAAG,QAAO;AAAA,EACpD;AACA,SAAO;AACT;AAEA,SAAS,cAAc,OAAe,SAAiB,QAAyB;AAC9E,MAAI,WAAW,EAAG,QAAO;AACzB,QAAM,QAAQ,KAAK;AACnB,SAAO,UAAU,UAAU,YAAY;AACzC;AAEA,SAAS,cAAc,OAAe,SAAiB,QAAyB;AAC9E,MAAI,WAAW,EAAG,QAAO;AACzB,QAAM,QAAQ,OAAO,MAAM,MAAM;AACjC,SAAO,SAAS,UAAU,WAAW;AACvC;AAGA,SAAS,UAAU,OAA8B;AAC/C,QAAM,QAAQ,MAAM,MAAM,GAAG;AAC7B,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,MAAI,SAAS;AACb,aAAW,QAAQ,OAAO;AACxB,QAAI,CAAC,YAAY,KAAK,IAAI,EAAG,QAAO;AACpC,UAAM,QAAQ,OAAO,IAAI;AACzB,QAAI,QAAQ,IAAK,QAAO;AACxB,aAAS,SAAS,MAAM;AAAA,EAC1B;AACA,SAAO;AACT;AAGA,SAAS,UAAU,OAA8B;AAG/C,QAAM,OAAO,MAAM,QAAQ,GAAG;AAC9B,QAAM,UAAU,SAAS,KAAK,QAAQ,MAAM,MAAM,GAAG,IAAI;AAEzD,MAAI,OAAO;AACX,QAAM,WAAW,6BAA6B,KAAK,OAAO;AAC1D,MAAI,aAAa,MAAM;AACrB,UAAM,OAAO,UAAU,SAAS,CAAC,KAAK,EAAE;AACxC,QAAI,SAAS,KAAM,QAAO;AAC1B,UAAM,SAAS,IAAI,SAAS,IAAI,SAAS,EAAE,CAAC,KAAK,OAAO,OAAQ,SAAS,EAAE,CAAC;AAC5E,WAAO,GAAG,QAAQ,MAAM,GAAG,SAAS,KAAK,CAAC,GAAG,MAAM;AAAA,EACrD;AAEA,QAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,MAAI,OAAO,SAAS,EAAG,QAAO;AAC9B,MAAI,OAAO,WAAW,GAAG;AACvB,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,WAAO,OAAO,WAAW,IAAI,cAAc,MAAM,IAAI;AAAA,EACvD;AAEA,QAAM,CAAC,OAAO,IAAI,OAAO,EAAE,IAAI;AAC/B,QAAM,UAAU,SAAS,KAAK,CAAC,IAAI,KAAK,MAAM,GAAG;AACjD,QAAM,WAAW,SAAS,KAAK,CAAC,IAAI,KAAK,MAAM,GAAG;AAClD,QAAM,UAAU,IAAI,QAAQ,SAAS,SAAS;AAC9C,MAAI,UAAU,EAAG,QAAO;AACxB,QAAM,aAAa,cAAc,OAAO;AACxC,QAAM,aAAa,cAAc,QAAQ;AACzC,MAAI,eAAe,QAAQ,eAAe,KAAM,QAAO;AACvD,SAAQ,cAAc,OAAO,MAAM,UAAU,SAAS,OAAO,IAAK;AACpE;AAEA,SAAS,cAAc,QAA0C;AAC/D,MAAI,QAAQ;AACZ,aAAW,SAAS,QAAQ;AAC1B,QAAI,CAAC,kBAAkB,KAAK,KAAK,EAAG,QAAO;AAC3C,YAAS,SAAS,MAAO,OAAO,SAAS,OAAO,EAAE,CAAC;AAAA,EACrD;AACA,SAAO;AACT;AAEA,SAAS,kBACP,QAC0C;AAC1C,SAAO,OAAO,IAAI,CAAC,CAAC,SAAS,MAAM,MAAM;AACvC,UAAM,QAAQ,UAAU,OAAO;AAC/B,QAAI,UAAU,KAAM,OAAM,IAAI,UAAU,uBAAuB,OAAO,IAAI,MAAM,EAAE;AAClF,WAAO,CAAC,OAAO,MAAM;AAAA,EACvB,CAAC;AACH;AAEA,SAAS,kBACP,QAC0C;AAC1C,SAAO,OAAO,IAAI,CAAC,CAAC,SAAS,MAAM,MAAM;AACvC,UAAM,QAAQ,UAAU,OAAO;AAC/B,QAAI,UAAU,KAAM,OAAM,IAAI,UAAU,uBAAuB,OAAO,IAAI,MAAM,EAAE;AAClF,WAAO,CAAC,OAAO,MAAM;AAAA,EACvB,CAAC;AACH;;;AC5KO,SAAS,oBAAoB,OAAe,QAA0C;AAC3F,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,IAAI,KAAK;AAAA,EACrB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,IAAI,aAAa,WAAW,IAAI,aAAa,SAAU,QAAO;AAClE,MAAI,IAAI,SAAS,SAAS,KAAK,IAAI,SAAS,SAAS,EAAG,QAAO;AAG/D,MAAI,wBAAwB,IAAI,QAAQ,EAAG,QAAO;AAElD,SAAO,OAAO,SAAS,oBAAoB,OAAO,QAAQ,SAAS,IAAI,MAAM;AAC/E;AAMA,eAAsB,wBACpB,OACA,QACe;AACf,QAAM,UAAU,MAAM,QAAQ;AAC9B,QAAM,QAAQ,QAAQ,MAAM;AAC5B,MACE,QAAQ,oBAAoB,KAC5B,UAAU,MAAM,KAAK,EAAE,UAAU,KACjC,CAAC,oBAAoB,QAAQ,IAAI,GAAG,MAAM,GAC1C;AACA,UAAM,MAAM,MAAM,iBAAiB;AACnC;AAAA,EACF;AACA,QAAM,MAAM,SAAS;AACvB;","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isNavigationAllowed
|
|
3
|
+
} from "./chunk-XE5WKN5F.js";
|
|
4
|
+
|
|
1
5
|
// src/docker-browser-client.ts
|
|
2
6
|
import { fileURLToPath } from "url";
|
|
3
7
|
|
|
@@ -506,10 +510,10 @@ var ResourceScheduler = class {
|
|
|
506
510
|
controller: new AbortController(),
|
|
507
511
|
operation,
|
|
508
512
|
resolve,
|
|
509
|
-
reject
|
|
510
|
-
...abortSignal === void 0 ? {} : { abortSignal }
|
|
513
|
+
reject
|
|
511
514
|
};
|
|
512
515
|
if (abortSignal !== void 0) {
|
|
516
|
+
task.abortSignal = abortSignal;
|
|
513
517
|
task.abort = () => {
|
|
514
518
|
const current = this.resources.get(key);
|
|
515
519
|
const index = current?.tasks.indexOf(task) ?? -1;
|
|
@@ -619,10 +623,10 @@ var AsyncMutex = class {
|
|
|
619
623
|
return new Promise((resolve, reject) => {
|
|
620
624
|
const waiter = {
|
|
621
625
|
resolve,
|
|
622
|
-
reject
|
|
623
|
-
...abortSignal === void 0 ? {} : { abortSignal }
|
|
626
|
+
reject
|
|
624
627
|
};
|
|
625
628
|
if (abortSignal !== void 0) {
|
|
629
|
+
waiter.abortSignal = abortSignal;
|
|
626
630
|
waiter.abort = () => {
|
|
627
631
|
const index = this.waiters.indexOf(waiter);
|
|
628
632
|
if (index === -1) return;
|
|
@@ -666,18 +670,20 @@ async function connectPlaywrightBrowser(options) {
|
|
|
666
670
|
const timeoutMs = options.timeoutMs ?? defaultConnectionTimeoutMs;
|
|
667
671
|
assertPositiveSafeInteger2(timeoutMs, "timeoutMs");
|
|
668
672
|
options.abortSignal?.throwIfAborted();
|
|
669
|
-
const
|
|
673
|
+
const connectOptions = {
|
|
670
674
|
endpointUrl: options.endpointUrl,
|
|
671
|
-
timeoutMs
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
675
|
+
timeoutMs
|
|
676
|
+
};
|
|
677
|
+
if (options.abortSignal !== void 0) connectOptions.abortSignal = options.abortSignal;
|
|
678
|
+
if (options.workerFactory !== void 0) connectOptions.workerFactory = options.workerFactory;
|
|
679
|
+
const backend = await AutomationWorkerClient.connect(connectOptions);
|
|
680
|
+
const connectionOptions = {
|
|
676
681
|
backend,
|
|
677
682
|
control: options.control,
|
|
678
|
-
scheduling: options.scheduling
|
|
679
|
-
|
|
680
|
-
|
|
683
|
+
scheduling: options.scheduling
|
|
684
|
+
};
|
|
685
|
+
if (options.onClosed !== void 0) connectionOptions.onClosed = options.onClosed;
|
|
686
|
+
return new PlaywrightBrowserConnectionImpl(connectionOptions);
|
|
681
687
|
}
|
|
682
688
|
var PlaywrightBrowserConnectionImpl = class {
|
|
683
689
|
backend;
|
|
@@ -1130,17 +1136,18 @@ var BrowserControlState = class {
|
|
|
1130
1136
|
snapshot() {
|
|
1131
1137
|
const lease = this.activeLease;
|
|
1132
1138
|
const state = lease !== void 0 ? "human" : this.humanPending ? "human-pending" : this.activeAgentActions > 0 ? "agent-active" : "agent";
|
|
1133
|
-
|
|
1139
|
+
const snapshot = {
|
|
1134
1140
|
mode: lease === void 0 ? "agent" : "human",
|
|
1135
1141
|
state,
|
|
1136
1142
|
availability: this.availability,
|
|
1137
1143
|
activeAgentActions: this.activeAgentActions,
|
|
1138
|
-
humanPending: this.humanPending
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
}
|
|
1144
|
+
humanPending: this.humanPending
|
|
1145
|
+
};
|
|
1146
|
+
if (lease !== void 0) {
|
|
1147
|
+
snapshot.ownerId = lease.ownerId;
|
|
1148
|
+
snapshot.expiresAt = lease.expiresAt;
|
|
1149
|
+
}
|
|
1150
|
+
return Object.freeze(snapshot);
|
|
1144
1151
|
}
|
|
1145
1152
|
async acquireHumanControl(options) {
|
|
1146
1153
|
validateAcquireOptions(options);
|
|
@@ -1789,22 +1796,22 @@ var DockerBrowserHandle = class {
|
|
|
1789
1796
|
cleanupError = caught;
|
|
1790
1797
|
}
|
|
1791
1798
|
if (disconnectedDuringInitialization && this.handleState === "active" && this.state === "running") {
|
|
1799
|
+
const disconnectOptions = {};
|
|
1800
|
+
if (cleanupError !== void 0) disconnectOptions.cause = cleanupError;
|
|
1801
|
+
disconnectOptions.phase = "connect";
|
|
1792
1802
|
throw new BrowserError(
|
|
1793
1803
|
"Browser disconnected while automation was initializing.",
|
|
1794
1804
|
"connection_closed",
|
|
1795
|
-
|
|
1796
|
-
...cleanupError === void 0 ? {} : { cause: cleanupError },
|
|
1797
|
-
phase: "connect"
|
|
1798
|
-
}
|
|
1805
|
+
disconnectOptions
|
|
1799
1806
|
);
|
|
1800
1807
|
}
|
|
1801
1808
|
const lifecycleError = this.lifecycleError(
|
|
1802
1809
|
"Browser stopped while the connection was initializing."
|
|
1803
1810
|
);
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1811
|
+
if (cleanupError === void 0) throw lifecycleError;
|
|
1812
|
+
const cleanupOptions = { cause: cleanupError };
|
|
1813
|
+
if (lifecycleError.phase !== void 0) cleanupOptions.phase = lifecycleError.phase;
|
|
1814
|
+
throw new BrowserError(lifecycleError.message, lifecycleError.code, cleanupOptions);
|
|
1808
1815
|
}
|
|
1809
1816
|
return connection;
|
|
1810
1817
|
} catch (error) {
|
|
@@ -1960,15 +1967,13 @@ var DockerBrowserHandle = class {
|
|
|
1960
1967
|
return results.filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
1961
1968
|
}
|
|
1962
1969
|
setCapability(capability, state, error) {
|
|
1963
|
-
|
|
1970
|
+
const snapshot = {
|
|
1964
1971
|
capability,
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
})
|
|
1971
|
-
);
|
|
1972
|
+
state,
|
|
1973
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1974
|
+
};
|
|
1975
|
+
if (error !== void 0) snapshot.error = error;
|
|
1976
|
+
this.capabilityStates.set(capability, Object.freeze(snapshot));
|
|
1972
1977
|
}
|
|
1973
1978
|
updateControlAvailability() {
|
|
1974
1979
|
if (this.handleState !== "active" || this.state !== "running") {
|
|
@@ -2438,15 +2443,6 @@ function assertNavigationAllowed(value, policy) {
|
|
|
2438
2443
|
throw new BrowserError(`Browser navigation is blocked: ${value}`, "navigation_blocked");
|
|
2439
2444
|
}
|
|
2440
2445
|
}
|
|
2441
|
-
function isNavigationAllowed(value, policy) {
|
|
2442
|
-
let url;
|
|
2443
|
-
try {
|
|
2444
|
-
url = parseHttpUrl(value);
|
|
2445
|
-
} catch {
|
|
2446
|
-
return false;
|
|
2447
|
-
}
|
|
2448
|
-
return policy.mode === "allow-all-http" || policy.origins.includes(url.origin);
|
|
2449
|
-
}
|
|
2450
2446
|
function parseHttpUrl(value) {
|
|
2451
2447
|
const url = new URL(value);
|
|
2452
2448
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|