@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.
- package/LICENSE +21 -0
- package/README.md +212 -0
- package/dist/config.d.ts +44 -0
- package/dist/config.js +74 -0
- package/dist/core/cdp.d.ts +124 -0
- package/dist/core/cdp.helpers.d.ts +14 -0
- package/dist/core/cdp.helpers.js +148 -0
- package/dist/core/cdp.js +309 -0
- package/dist/core/chrome.d.ts +21 -0
- package/dist/core/chrome.executables.d.ts +10 -0
- package/dist/core/chrome.executables.js +559 -0
- package/dist/core/chrome.js +257 -0
- package/dist/core/chrome.profile-decoration.d.ts +11 -0
- package/dist/core/chrome.profile-decoration.js +148 -0
- package/dist/core/constants.d.ts +9 -0
- package/dist/core/constants.js +9 -0
- package/dist/core/profiles.d.ts +31 -0
- package/dist/core/profiles.js +99 -0
- package/dist/core/target-id.d.ts +12 -0
- package/dist/core/target-id.js +21 -0
- package/dist/data-dir.d.ts +2 -0
- package/dist/data-dir.js +6 -0
- package/dist/logger.d.ts +16 -0
- package/dist/logger.js +125 -0
- package/dist/playwright/pw-role-snapshot.d.ts +32 -0
- package/dist/playwright/pw-role-snapshot.js +337 -0
- package/dist/playwright/pw-session.d.ts +119 -0
- package/dist/playwright/pw-session.js +530 -0
- package/dist/playwright/pw-tools-core.activity.d.ts +22 -0
- package/dist/playwright/pw-tools-core.activity.js +47 -0
- package/dist/playwright/pw-tools-core.d.ts +9 -0
- package/dist/playwright/pw-tools-core.downloads.d.ts +35 -0
- package/dist/playwright/pw-tools-core.downloads.js +186 -0
- package/dist/playwright/pw-tools-core.interactions.d.ts +104 -0
- package/dist/playwright/pw-tools-core.interactions.js +404 -0
- package/dist/playwright/pw-tools-core.js +9 -0
- package/dist/playwright/pw-tools-core.responses.d.ts +14 -0
- package/dist/playwright/pw-tools-core.responses.js +91 -0
- package/dist/playwright/pw-tools-core.shared.d.ts +7 -0
- package/dist/playwright/pw-tools-core.shared.js +50 -0
- package/dist/playwright/pw-tools-core.snapshot.d.ts +65 -0
- package/dist/playwright/pw-tools-core.snapshot.js +144 -0
- package/dist/playwright/pw-tools-core.state.d.ts +47 -0
- package/dist/playwright/pw-tools-core.state.js +154 -0
- package/dist/playwright/pw-tools-core.storage.d.ts +48 -0
- package/dist/playwright/pw-tools-core.storage.js +76 -0
- package/dist/playwright/pw-tools-core.trace.d.ts +13 -0
- package/dist/playwright/pw-tools-core.trace.js +26 -0
- package/dist/server/browser-context.d.ts +29 -0
- package/dist/server/browser-context.js +137 -0
- package/dist/server/browser-server.d.ts +7 -0
- package/dist/server/browser-server.js +49 -0
- package/dist/server/routes/act.d.ts +4 -0
- package/dist/server/routes/act.js +176 -0
- package/dist/server/routes/basic.d.ts +4 -0
- package/dist/server/routes/basic.js +36 -0
- package/dist/server/routes/index.d.ts +4 -0
- package/dist/server/routes/index.js +16 -0
- package/dist/server/routes/snapshot.d.ts +4 -0
- package/dist/server/routes/snapshot.js +143 -0
- package/dist/server/routes/storage.d.ts +4 -0
- package/dist/server/routes/storage.js +117 -0
- package/dist/server/routes/tabs.d.ts +4 -0
- package/dist/server/routes/tabs.js +51 -0
- package/dist/server/standalone.d.ts +9 -0
- package/dist/server/standalone.js +42 -0
- package/dist/utils.d.ts +18 -0
- package/dist/utils.js +58 -0
- 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
|
package/dist/config.d.ts
ADDED
|
@@ -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
|