@hera-al/browser-server 1.0.0

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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +212 -0
  3. package/dist/config.d.ts +44 -0
  4. package/dist/config.js +74 -0
  5. package/dist/core/cdp.d.ts +124 -0
  6. package/dist/core/cdp.helpers.d.ts +14 -0
  7. package/dist/core/cdp.helpers.js +148 -0
  8. package/dist/core/cdp.js +309 -0
  9. package/dist/core/chrome.d.ts +21 -0
  10. package/dist/core/chrome.executables.d.ts +10 -0
  11. package/dist/core/chrome.executables.js +559 -0
  12. package/dist/core/chrome.js +257 -0
  13. package/dist/core/chrome.profile-decoration.d.ts +11 -0
  14. package/dist/core/chrome.profile-decoration.js +148 -0
  15. package/dist/core/constants.d.ts +9 -0
  16. package/dist/core/constants.js +9 -0
  17. package/dist/core/profiles.d.ts +31 -0
  18. package/dist/core/profiles.js +99 -0
  19. package/dist/core/target-id.d.ts +12 -0
  20. package/dist/core/target-id.js +21 -0
  21. package/dist/data-dir.d.ts +2 -0
  22. package/dist/data-dir.js +6 -0
  23. package/dist/logger.d.ts +16 -0
  24. package/dist/logger.js +125 -0
  25. package/dist/playwright/pw-role-snapshot.d.ts +32 -0
  26. package/dist/playwright/pw-role-snapshot.js +337 -0
  27. package/dist/playwright/pw-session.d.ts +119 -0
  28. package/dist/playwright/pw-session.js +530 -0
  29. package/dist/playwright/pw-tools-core.activity.d.ts +22 -0
  30. package/dist/playwright/pw-tools-core.activity.js +47 -0
  31. package/dist/playwright/pw-tools-core.d.ts +9 -0
  32. package/dist/playwright/pw-tools-core.downloads.d.ts +35 -0
  33. package/dist/playwright/pw-tools-core.downloads.js +186 -0
  34. package/dist/playwright/pw-tools-core.interactions.d.ts +104 -0
  35. package/dist/playwright/pw-tools-core.interactions.js +404 -0
  36. package/dist/playwright/pw-tools-core.js +9 -0
  37. package/dist/playwright/pw-tools-core.responses.d.ts +14 -0
  38. package/dist/playwright/pw-tools-core.responses.js +91 -0
  39. package/dist/playwright/pw-tools-core.shared.d.ts +7 -0
  40. package/dist/playwright/pw-tools-core.shared.js +50 -0
  41. package/dist/playwright/pw-tools-core.snapshot.d.ts +65 -0
  42. package/dist/playwright/pw-tools-core.snapshot.js +144 -0
  43. package/dist/playwright/pw-tools-core.state.d.ts +47 -0
  44. package/dist/playwright/pw-tools-core.state.js +154 -0
  45. package/dist/playwright/pw-tools-core.storage.d.ts +48 -0
  46. package/dist/playwright/pw-tools-core.storage.js +76 -0
  47. package/dist/playwright/pw-tools-core.trace.d.ts +13 -0
  48. package/dist/playwright/pw-tools-core.trace.js +26 -0
  49. package/dist/server/browser-context.d.ts +29 -0
  50. package/dist/server/browser-context.js +137 -0
  51. package/dist/server/browser-server.d.ts +7 -0
  52. package/dist/server/browser-server.js +49 -0
  53. package/dist/server/routes/act.d.ts +4 -0
  54. package/dist/server/routes/act.js +176 -0
  55. package/dist/server/routes/basic.d.ts +4 -0
  56. package/dist/server/routes/basic.js +36 -0
  57. package/dist/server/routes/index.d.ts +4 -0
  58. package/dist/server/routes/index.js +16 -0
  59. package/dist/server/routes/snapshot.d.ts +4 -0
  60. package/dist/server/routes/snapshot.js +143 -0
  61. package/dist/server/routes/storage.d.ts +4 -0
  62. package/dist/server/routes/storage.js +117 -0
  63. package/dist/server/routes/tabs.d.ts +4 -0
  64. package/dist/server/routes/tabs.js +51 -0
  65. package/dist/server/standalone.d.ts +9 -0
  66. package/dist/server/standalone.js +42 -0
  67. package/dist/utils.d.ts +18 -0
  68. package/dist/utils.js +58 -0
  69. package/package.json +66 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TGP / Hera Artificial Life
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,212 @@
1
+ # @hera-al/browser-server
2
+
3
+ > **Part of [Hera Artificial Life](https://github.com/ltoscano/hera_al)** — an opinionated AI assistant platform that runs locally on your machine. This package provides the browser automation layer used by Hera agents to interact with the web. It can also be used independently in any Node.js project.
4
+
5
+ Local browser automation server powered by [Playwright](https://playwright.dev/) and the Chrome DevTools Protocol (CDP).
6
+
7
+ Exposes a lightweight HTTP API on `127.0.0.1` that lets AI agents — or any local process — launch, control, and inspect browser sessions without embedding a full browser SDK.
8
+
9
+ ## Features
10
+
11
+ - **Launch or attach** to a local Chrome/Chromium instance via CDP
12
+ - **Multi-profile** support (separate CDP ports, colors, isolated sessions)
13
+ - **Tab management** — open, list, focus, close tabs
14
+ - **Page actions** — click, type, hover, scroll, drag, select, fill forms, press keys, navigate
15
+ - **Snapshots** — role-based (Playwright), ARIA, DOM, text/HTML extraction, CSS query, AI-digest
16
+ - **Screenshots & PDF** — full-page or element-level capture
17
+ - **Storage** — read/write cookies, localStorage, sessionStorage
18
+ - **JavaScript evaluation** — run arbitrary JS in page context
19
+ - **Wait conditions** — wait for text, selector, URL, load state, or custom function
20
+ - **Standalone or embedded** — run as a standalone process or import into your own Node.js app
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ npm install @hera-al/browser-server
26
+ ```
27
+
28
+ > **Peer dependency:** Requires `playwright-core` ≥ 1.50 and a Chromium-based browser available on the system.
29
+
30
+ ## Quick start
31
+
32
+ ### Standalone (CLI)
33
+
34
+ ```bash
35
+ npx hera-browser --port 3002 --headless false
36
+ ```
37
+
38
+ All CLI flags:
39
+
40
+ | Flag | Default | Description |
41
+ | ------------------ | ------- | ---------------------------------------- |
42
+ | `--port` | `3002` | HTTP control port |
43
+ | `--headless` | `false` | Run Chrome headless |
44
+ | `--noSandbox` | `false` | Disable Chrome sandbox (CI/Docker) |
45
+ | `--attachOnly` | `false` | Never launch Chrome, only attach via CDP |
46
+ | `--executablePath` | — | Custom Chrome/Chromium binary path |
47
+
48
+ ### Programmatic
49
+
50
+ ```ts
51
+ import { startBrowserServer, stopBrowserServer } from "@hera-al/browser-server";
52
+ import { resolveBrowserConfig, BrowserConfigSchema } from "@hera-al/browser-server/config";
53
+
54
+ const config = resolveBrowserConfig(
55
+ BrowserConfigSchema.parse({
56
+ enabled: true,
57
+ controlPort: 3002,
58
+ headless: false,
59
+ })
60
+ );
61
+
62
+ await startBrowserServer(config);
63
+
64
+ // ... your app logic ...
65
+
66
+ await stopBrowserServer();
67
+ ```
68
+
69
+ ## HTTP API Reference
70
+
71
+ All endpoints listen on `http://127.0.0.1:{port}`. Responses are JSON with an `ok` boolean.
72
+
73
+ ### Status & lifecycle
74
+
75
+ | Method | Path | Description |
76
+ | ------ | -------- | --------------------------------- |
77
+ | GET | `/` | Check browser status (`running` / `stopped`) |
78
+ | POST | `/start` | Launch or attach to the browser |
79
+ | POST | `/stop` | Stop the browser |
80
+
81
+ ### Tabs
82
+
83
+ | Method | Path | Description |
84
+ | ------ | ------------- | ------------- |
85
+ | GET | `/tabs` | List all open tabs |
86
+ | POST | `/tabs/open` | Open a new tab (`{ url }`) |
87
+ | POST | `/tabs/focus` | Focus a tab (`{ id }`) |
88
+ | DELETE | `/tabs/:id` | Close a tab |
89
+
90
+ ### Actions (`POST /act`)
91
+
92
+ Send `{ kind, ...params }` to perform a page action:
93
+
94
+ | Kind | Key params |
95
+ | ------------ | ------------------------------------------------------------- |
96
+ | `navigate` | `url`, `timeoutMs?` |
97
+ | `click` | `ref`, `doubleClick?`, `button?`, `modifiers?` |
98
+ | `type` | `ref`, `text`, `submit?`, `slowly?` |
99
+ | `press` | `key`, `delayMs?` |
100
+ | `hover` | `ref` |
101
+ | `scroll` | `ref` |
102
+ | `drag` | `startRef`, `endRef` |
103
+ | `select` | `ref`, `values` |
104
+ | `fill_form` | `fields: [{ ref, type, value }]` |
105
+ | `screenshot` | `ref?`, `element?`, `fullPage?`, `type?` (`png` \| `jpeg`) |
106
+ | `evaluate` | `fn` (JS string), `ref?` |
107
+ | `wait` | `timeMs?`, `text?`, `textGone?`, `selector?`, `url?`, `loadState?`, `fn?` |
108
+
109
+ All actions accept optional `profile`, `targetId`, and `timeoutMs`.
110
+
111
+ ### Snapshots (`GET /snapshot`)
112
+
113
+ Query params:
114
+
115
+ | Param | Values | Description |
116
+ | --------------- | ------------------------------------------- | ---------------------------------- |
117
+ | `mode` | `role` (default), `aria`, `dom`, `text`, `html`, `ai`, `query` | Snapshot format |
118
+ | `selector` | CSS selector string | Scope to element (for `text`, `html`, `query`) |
119
+ | `frameSelector` | CSS selector string | Target iframe |
120
+ | `interactive` | `true` / `false` | Include interactive elements only |
121
+ | `compact` | `true` / `false` | Compact output |
122
+
123
+ ### Screenshots & PDF
124
+
125
+ | Method | Path | Description |
126
+ | ------ | ------------- | -------------------------------------- |
127
+ | POST | `/screenshot` | Capture screenshot (`{ fullPage?, type?, ref?, element? }`) |
128
+ | POST | `/pdf` | Export page as PDF |
129
+
130
+ ### Storage
131
+
132
+ | Method | Path | Description |
133
+ | ------ | ----------------- | ----------------------------- |
134
+ | GET | `/cookies` | Get all cookies |
135
+ | POST | `/cookies` | Set a cookie (`{ cookie }`) |
136
+ | DELETE | `/cookies` | Clear all cookies |
137
+ | GET | `/storage/:kind` | Get localStorage or sessionStorage (`kind` = `local` \| `session`) |
138
+ | POST | `/storage/:kind` | Set a key (`{ key, value }`) |
139
+ | DELETE | `/storage/:kind` | Clear storage |
140
+
141
+ ## Configuration
142
+
143
+ When using the programmatic API, the full config schema is:
144
+
145
+ ```ts
146
+ {
147
+ enabled: boolean; // default: false
148
+ controlPort: number; // default: 3002
149
+ headless: boolean; // default: false
150
+ noSandbox: boolean; // default: false
151
+ attachOnly: boolean; // default: false
152
+ executablePath?: string; // custom Chrome path
153
+ remoteCdpTimeoutMs: number; // default: 1500
154
+ profiles: {
155
+ [name: string]: {
156
+ cdpPort?: number; // default: 9222
157
+ cdpUrl?: string; // overrides cdpPort
158
+ color?: string; // hex color, default: "#FF4500"
159
+ }
160
+ }
161
+ }
162
+ ```
163
+
164
+ ### Profiles
165
+
166
+ Profiles allow managing multiple isolated browser sessions on different CDP ports. The `default` profile is always available.
167
+
168
+ ```ts
169
+ const config = resolveBrowserConfig(
170
+ BrowserConfigSchema.parse({
171
+ enabled: true,
172
+ profiles: {
173
+ default: { cdpPort: 9222 },
174
+ social: { cdpPort: 9223, color: "#1DA1F2" },
175
+ },
176
+ })
177
+ );
178
+ ```
179
+
180
+ Pass `?profile=social` (GET) or `{ "profile": "social" }` (POST) to target a specific profile.
181
+
182
+ ## Architecture
183
+
184
+ ```
185
+ ┌─────────────────────────────────────────┐
186
+ │ HTTP API (Hono) │
187
+ │ basic · tabs · act · snapshot · storage│
188
+ └────────────────┬────────────────────────┘
189
+
190
+ ┌───────┴───────┐
191
+ │ BrowserContext │ ← multi-profile manager
192
+ └───────┬───────┘
193
+
194
+ ┌────────────┼────────────┐
195
+ │ │ │
196
+ Chrome Playwright CDP
197
+ launcher session mgr helpers
198
+ ```
199
+
200
+ - **Hono** — lightweight HTTP framework (< 15 KB)
201
+ - **Playwright** — used for page interactions, snapshots, screenshots (connects to existing Chrome via CDP)
202
+ - **CDP direct** — used for ARIA/DOM snapshots, tab management, WebSocket resolution
203
+
204
+ ## Requirements
205
+
206
+ - Node.js ≥ 18
207
+ - Chrome or Chromium installed locally (or provide `executablePath`)
208
+ - macOS, Linux, or WSL
209
+
210
+ ## License
211
+
212
+ [MIT](./LICENSE) — © 2026 TGP / Hera Artificial Life
@@ -0,0 +1,44 @@
1
+ import { z } from "zod";
2
+ export declare const BrowserProfileSchema: z.ZodObject<{
3
+ cdpPort: z.ZodOptional<z.ZodNumber>;
4
+ cdpUrl: z.ZodOptional<z.ZodString>;
5
+ color: z.ZodDefault<z.ZodString>;
6
+ }, z.core.$strip>;
7
+ export declare const BrowserConfigSchema: z.ZodObject<{
8
+ enabled: z.ZodDefault<z.ZodBoolean>;
9
+ controlPort: z.ZodDefault<z.ZodNumber>;
10
+ headless: z.ZodDefault<z.ZodBoolean>;
11
+ noSandbox: z.ZodDefault<z.ZodBoolean>;
12
+ attachOnly: z.ZodDefault<z.ZodBoolean>;
13
+ executablePath: z.ZodOptional<z.ZodString>;
14
+ remoteCdpTimeoutMs: z.ZodDefault<z.ZodNumber>;
15
+ profiles: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
16
+ cdpPort: z.ZodOptional<z.ZodNumber>;
17
+ cdpUrl: z.ZodOptional<z.ZodString>;
18
+ color: z.ZodDefault<z.ZodString>;
19
+ }, z.core.$strip>>>;
20
+ }, z.core.$strip>;
21
+ export type BrowserConfig = z.infer<typeof BrowserConfigSchema>;
22
+ export type BrowserProfileConfig = z.infer<typeof BrowserProfileSchema>;
23
+ export type ResolvedBrowserConfig = {
24
+ enabled: boolean;
25
+ controlPort: number;
26
+ remoteCdpTimeoutMs: number;
27
+ color: string;
28
+ executablePath?: string;
29
+ headless: boolean;
30
+ noSandbox: boolean;
31
+ attachOnly: boolean;
32
+ profiles: Record<string, BrowserProfileConfig>;
33
+ };
34
+ export type ResolvedBrowserProfile = {
35
+ name: string;
36
+ cdpPort: number;
37
+ cdpUrl: string;
38
+ cdpHost: string;
39
+ cdpIsLoopback: boolean;
40
+ color: string;
41
+ };
42
+ export declare function resolveBrowserConfig(cfg: BrowserConfig | undefined): ResolvedBrowserConfig;
43
+ export declare function resolveProfile(resolved: ResolvedBrowserConfig, profileName: string): ResolvedBrowserProfile | null;
44
+ //# sourceMappingURL=config.d.ts.map
package/dist/config.js ADDED
@@ -0,0 +1,74 @@
1
+ import { z } from "zod";
2
+ import { isLoopbackHost } from "./utils.js";
3
+ // --- Zod Schemas ---
4
+ export const BrowserProfileSchema = z.object({
5
+ cdpPort: z.number().optional(),
6
+ cdpUrl: z.string().optional(),
7
+ color: z.string().default("#FF4500"),
8
+ });
9
+ export const BrowserConfigSchema = z.object({
10
+ enabled: z.boolean().default(false),
11
+ controlPort: z.number().default(3002),
12
+ headless: z.boolean().default(false),
13
+ noSandbox: z.boolean().default(false),
14
+ attachOnly: z.boolean().default(false),
15
+ executablePath: z.string().optional(),
16
+ remoteCdpTimeoutMs: z.number().default(1500),
17
+ profiles: z.record(z.string(), BrowserProfileSchema).default({
18
+ default: { cdpPort: 9222, color: "#FF4500" },
19
+ }),
20
+ });
21
+ // --- Resolution ---
22
+ function normalizeHexColor(raw) {
23
+ const value = (raw ?? "").trim();
24
+ if (!value)
25
+ return "#FF4500";
26
+ const normalized = value.startsWith("#") ? value : `#${value}`;
27
+ if (!/^#[0-9a-fA-F]{6}$/.test(normalized))
28
+ return "#FF4500";
29
+ return normalized.toUpperCase();
30
+ }
31
+ export function resolveBrowserConfig(cfg) {
32
+ const defaults = BrowserConfigSchema.parse(cfg ?? {});
33
+ return {
34
+ enabled: defaults.enabled,
35
+ controlPort: defaults.controlPort,
36
+ remoteCdpTimeoutMs: defaults.remoteCdpTimeoutMs,
37
+ color: normalizeHexColor(Object.values(defaults.profiles)[0]?.color),
38
+ executablePath: defaults.executablePath?.trim() || undefined,
39
+ headless: defaults.headless,
40
+ noSandbox: defaults.noSandbox,
41
+ attachOnly: defaults.attachOnly,
42
+ profiles: defaults.profiles,
43
+ };
44
+ }
45
+ export function resolveProfile(resolved, profileName) {
46
+ const profile = resolved.profiles[profileName];
47
+ if (!profile)
48
+ return null;
49
+ const rawUrl = profile.cdpUrl?.trim() ?? "";
50
+ let cdpHost = "127.0.0.1";
51
+ let cdpPort = profile.cdpPort ?? 0;
52
+ let cdpUrl = "";
53
+ if (rawUrl) {
54
+ const parsed = new URL(rawUrl);
55
+ cdpHost = parsed.hostname;
56
+ cdpPort = parsed.port ? parseInt(parsed.port, 10) : (parsed.protocol === "https:" ? 443 : 80);
57
+ cdpUrl = parsed.toString().replace(/\/$/, "");
58
+ }
59
+ else if (cdpPort) {
60
+ cdpUrl = `http://127.0.0.1:${cdpPort}`;
61
+ }
62
+ else {
63
+ return null;
64
+ }
65
+ return {
66
+ name: profileName,
67
+ cdpPort,
68
+ cdpUrl,
69
+ cdpHost,
70
+ cdpIsLoopback: isLoopbackHost(cdpHost),
71
+ color: normalizeHexColor(profile.color),
72
+ };
73
+ }
74
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,124 @@
1
+ export { appendCdpPath, fetchJson, fetchOk, getHeadersWithAuth } from "./cdp.helpers.js";
2
+ export declare function normalizeCdpWsUrl(wsUrl: string, cdpUrl: string): string;
3
+ export declare function captureScreenshotPng(opts: {
4
+ wsUrl: string;
5
+ fullPage?: boolean;
6
+ }): Promise<Buffer>;
7
+ export declare function captureScreenshot(opts: {
8
+ wsUrl: string;
9
+ fullPage?: boolean;
10
+ format?: "png" | "jpeg";
11
+ quality?: number;
12
+ }): Promise<Buffer>;
13
+ export declare function createTargetViaCdp(opts: {
14
+ cdpUrl: string;
15
+ url: string;
16
+ }): Promise<{
17
+ targetId: string;
18
+ }>;
19
+ export type CdpRemoteObject = {
20
+ type: string;
21
+ subtype?: string;
22
+ value?: unknown;
23
+ description?: string;
24
+ unserializableValue?: string;
25
+ preview?: unknown;
26
+ };
27
+ export type CdpExceptionDetails = {
28
+ text?: string;
29
+ lineNumber?: number;
30
+ columnNumber?: number;
31
+ exception?: CdpRemoteObject;
32
+ stackTrace?: unknown;
33
+ };
34
+ export declare function evaluateJavaScript(opts: {
35
+ wsUrl: string;
36
+ expression: string;
37
+ awaitPromise?: boolean;
38
+ returnByValue?: boolean;
39
+ }): Promise<{
40
+ result: CdpRemoteObject;
41
+ exceptionDetails?: CdpExceptionDetails;
42
+ }>;
43
+ export type AriaSnapshotNode = {
44
+ ref: string;
45
+ role: string;
46
+ name: string;
47
+ value?: string;
48
+ description?: string;
49
+ backendDOMNodeId?: number;
50
+ depth: number;
51
+ };
52
+ export type RawAXNode = {
53
+ nodeId?: string;
54
+ role?: {
55
+ value?: string;
56
+ };
57
+ name?: {
58
+ value?: string;
59
+ };
60
+ value?: {
61
+ value?: string;
62
+ };
63
+ description?: {
64
+ value?: string;
65
+ };
66
+ childIds?: string[];
67
+ backendDOMNodeId?: number;
68
+ };
69
+ export declare function formatAriaSnapshot(nodes: RawAXNode[], limit: number): AriaSnapshotNode[];
70
+ export declare function snapshotAria(opts: {
71
+ wsUrl: string;
72
+ limit?: number;
73
+ }): Promise<{
74
+ nodes: AriaSnapshotNode[];
75
+ }>;
76
+ export declare function snapshotDom(opts: {
77
+ wsUrl: string;
78
+ limit?: number;
79
+ maxTextChars?: number;
80
+ }): Promise<{
81
+ nodes: DomSnapshotNode[];
82
+ }>;
83
+ export type DomSnapshotNode = {
84
+ ref: string;
85
+ parentRef: string | null;
86
+ depth: number;
87
+ tag: string;
88
+ id?: string;
89
+ className?: string;
90
+ role?: string;
91
+ name?: string;
92
+ text?: string;
93
+ href?: string;
94
+ type?: string;
95
+ value?: string;
96
+ };
97
+ export declare function getDomText(opts: {
98
+ wsUrl: string;
99
+ format: "html" | "text";
100
+ maxChars?: number;
101
+ selector?: string;
102
+ }): Promise<{
103
+ text: string;
104
+ }>;
105
+ export declare function querySelector(opts: {
106
+ wsUrl: string;
107
+ selector: string;
108
+ limit?: number;
109
+ maxTextChars?: number;
110
+ maxHtmlChars?: number;
111
+ }): Promise<{
112
+ matches: QueryMatch[];
113
+ }>;
114
+ export type QueryMatch = {
115
+ index: number;
116
+ tag: string;
117
+ id?: string;
118
+ className?: string;
119
+ text?: string;
120
+ value?: string;
121
+ href?: string;
122
+ outerHTML?: string;
123
+ };
124
+ //# sourceMappingURL=cdp.d.ts.map
@@ -0,0 +1,14 @@
1
+ import { isLoopbackHost } from "../utils.js";
2
+ export { isLoopbackHost };
3
+ export type CdpSendFn = (method: string, params?: Record<string, unknown>, sessionId?: string) => Promise<unknown>;
4
+ export declare function getHeadersWithAuth(url: string, headers?: Record<string, string>): {
5
+ [x: string]: string;
6
+ };
7
+ export declare function appendCdpPath(cdpUrl: string, path: string): string;
8
+ export declare function fetchJson<T>(url: string, timeoutMs?: number, init?: RequestInit): Promise<T>;
9
+ export declare function fetchOk(url: string, timeoutMs?: number, init?: RequestInit): Promise<void>;
10
+ export declare function withCdpSocket<T>(wsUrl: string, fn: (send: CdpSendFn) => Promise<T>, opts?: {
11
+ headers?: Record<string, string>;
12
+ handshakeTimeoutMs?: number;
13
+ }): Promise<T>;
14
+ //# sourceMappingURL=cdp.helpers.d.ts.map
@@ -0,0 +1,148 @@
1
+ import WebSocket from "ws";
2
+ import { isLoopbackHost, rawDataToString } from "../utils.js";
3
+ export { isLoopbackHost };
4
+ export function getHeadersWithAuth(url, headers = {}) {
5
+ const mergedHeaders = { ...headers };
6
+ try {
7
+ const parsed = new URL(url);
8
+ const hasAuthHeader = Object.keys(mergedHeaders).some((key) => key.toLowerCase() === "authorization");
9
+ if (hasAuthHeader) {
10
+ return mergedHeaders;
11
+ }
12
+ if (parsed.username || parsed.password) {
13
+ const auth = Buffer.from(`${parsed.username}:${parsed.password}`).toString("base64");
14
+ return { ...mergedHeaders, Authorization: `Basic ${auth}` };
15
+ }
16
+ }
17
+ catch {
18
+ // ignore
19
+ }
20
+ return mergedHeaders;
21
+ }
22
+ export function appendCdpPath(cdpUrl, path) {
23
+ const url = new URL(cdpUrl);
24
+ const basePath = url.pathname.replace(/\/$/, "");
25
+ const suffix = path.startsWith("/") ? path : `/${path}`;
26
+ url.pathname = `${basePath}${suffix}`;
27
+ return url.toString();
28
+ }
29
+ function createCdpSender(ws) {
30
+ let nextId = 1;
31
+ const pending = new Map();
32
+ const send = (method, params, sessionId) => {
33
+ const id = nextId++;
34
+ const msg = { id, method, params, sessionId };
35
+ ws.send(JSON.stringify(msg));
36
+ return new Promise((resolve, reject) => {
37
+ pending.set(id, { resolve, reject });
38
+ });
39
+ };
40
+ const closeWithError = (err) => {
41
+ for (const [, p] of pending) {
42
+ p.reject(err);
43
+ }
44
+ pending.clear();
45
+ try {
46
+ ws.close();
47
+ }
48
+ catch {
49
+ // ignore
50
+ }
51
+ };
52
+ ws.on("error", (err) => {
53
+ closeWithError(err instanceof Error ? err : new Error(String(err)));
54
+ });
55
+ ws.on("message", (data) => {
56
+ try {
57
+ const parsed = JSON.parse(rawDataToString(data));
58
+ if (typeof parsed.id !== "number") {
59
+ return;
60
+ }
61
+ const p = pending.get(parsed.id);
62
+ if (!p) {
63
+ return;
64
+ }
65
+ pending.delete(parsed.id);
66
+ if (parsed.error?.message) {
67
+ p.reject(new Error(parsed.error.message));
68
+ return;
69
+ }
70
+ p.resolve(parsed.result);
71
+ }
72
+ catch {
73
+ // ignore
74
+ }
75
+ });
76
+ ws.on("close", () => {
77
+ closeWithError(new Error("CDP socket closed"));
78
+ });
79
+ return { send, closeWithError };
80
+ }
81
+ export async function fetchJson(url, timeoutMs = 1500, init) {
82
+ const ctrl = new AbortController();
83
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
84
+ try {
85
+ const headers = getHeadersWithAuth(url, init?.headers || {});
86
+ const res = await fetch(url, { ...init, headers, signal: ctrl.signal });
87
+ if (!res.ok) {
88
+ throw new Error(`HTTP ${res.status}`);
89
+ }
90
+ return (await res.json());
91
+ }
92
+ finally {
93
+ clearTimeout(t);
94
+ }
95
+ }
96
+ export async function fetchOk(url, timeoutMs = 1500, init) {
97
+ const ctrl = new AbortController();
98
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
99
+ try {
100
+ const headers = getHeadersWithAuth(url, init?.headers || {});
101
+ const res = await fetch(url, { ...init, headers, signal: ctrl.signal });
102
+ if (!res.ok) {
103
+ throw new Error(`HTTP ${res.status}`);
104
+ }
105
+ }
106
+ finally {
107
+ clearTimeout(t);
108
+ }
109
+ }
110
+ export async function withCdpSocket(wsUrl, fn, opts) {
111
+ const headers = getHeadersWithAuth(wsUrl, opts?.headers ?? {});
112
+ const handshakeTimeoutMs = typeof opts?.handshakeTimeoutMs === "number" && Number.isFinite(opts.handshakeTimeoutMs)
113
+ ? Math.max(1, Math.floor(opts.handshakeTimeoutMs))
114
+ : 5000;
115
+ const ws = new WebSocket(wsUrl, {
116
+ handshakeTimeout: handshakeTimeoutMs,
117
+ ...(Object.keys(headers).length ? { headers } : {}),
118
+ });
119
+ const { send, closeWithError } = createCdpSender(ws);
120
+ const openPromise = new Promise((resolve, reject) => {
121
+ ws.once("open", () => resolve());
122
+ ws.once("error", (err) => reject(err));
123
+ ws.once("close", () => reject(new Error("CDP socket closed")));
124
+ });
125
+ try {
126
+ await openPromise;
127
+ }
128
+ catch (err) {
129
+ closeWithError(err instanceof Error ? err : new Error(String(err)));
130
+ throw err;
131
+ }
132
+ try {
133
+ return await fn(send);
134
+ }
135
+ catch (err) {
136
+ closeWithError(err instanceof Error ? err : new Error(String(err)));
137
+ throw err;
138
+ }
139
+ finally {
140
+ try {
141
+ ws.close();
142
+ }
143
+ catch {
144
+ // ignore
145
+ }
146
+ }
147
+ }
148
+ //# sourceMappingURL=cdp.helpers.js.map