@rynx-ai/browser-cdp 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +120 -0
- package/dist/client.js +788 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +30 -0
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export type BrowserAutomationEndpointKind = "browser" | "page";
|
|
2
|
+
export interface BrowserAutomationWebSocket {
|
|
3
|
+
readonly readyState: number;
|
|
4
|
+
onopen: ((event: unknown) => void) | null;
|
|
5
|
+
onmessage: ((event: {
|
|
6
|
+
data: unknown;
|
|
7
|
+
}) => void) | null;
|
|
8
|
+
onerror: ((event: unknown) => void) | null;
|
|
9
|
+
onclose: ((event: {
|
|
10
|
+
code?: number;
|
|
11
|
+
reason?: string;
|
|
12
|
+
}) => void) | null;
|
|
13
|
+
send(data: string): void;
|
|
14
|
+
close(code?: number, reason?: string): void;
|
|
15
|
+
}
|
|
16
|
+
export type BrowserAutomationWebSocketFactory = (endpoint: string) => BrowserAutomationWebSocket;
|
|
17
|
+
export interface BrowserAutomationClientOptions {
|
|
18
|
+
/** A page-level or browser-level loopback CDP WebSocket endpoint. */
|
|
19
|
+
endpoint: string;
|
|
20
|
+
/** Override endpoint inference when a non-standard CDP path is unavoidable. */
|
|
21
|
+
endpointKind?: BrowserAutomationEndpointKind;
|
|
22
|
+
/** Required only when a browser endpoint contains more than one Page. */
|
|
23
|
+
pageTargetId?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Stable secret for the lifetime of the owning Rynx Runtime. It lets refs
|
|
26
|
+
* cross short-lived client processes while making modified refs fail closed.
|
|
27
|
+
*/
|
|
28
|
+
referenceKey: string | Uint8Array;
|
|
29
|
+
commandTimeoutMs?: number;
|
|
30
|
+
createWebSocket?: BrowserAutomationWebSocketFactory;
|
|
31
|
+
}
|
|
32
|
+
export interface BrowserAutomationSnapshotNode {
|
|
33
|
+
nodeId: string;
|
|
34
|
+
parentId?: string;
|
|
35
|
+
childIds: string[];
|
|
36
|
+
role: string;
|
|
37
|
+
name: string;
|
|
38
|
+
value?: string;
|
|
39
|
+
description?: string;
|
|
40
|
+
ignored: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Opaque, process-independent reference valid only for this endpoint's Page
|
|
43
|
+
* target and current document. A navigation or Browser restart invalidates it.
|
|
44
|
+
*/
|
|
45
|
+
ref?: string;
|
|
46
|
+
properties: Readonly<Record<string, string | number | boolean>>;
|
|
47
|
+
}
|
|
48
|
+
export interface BrowserAutomationSnapshot {
|
|
49
|
+
documentGeneration: number;
|
|
50
|
+
nodes: BrowserAutomationSnapshotNode[];
|
|
51
|
+
}
|
|
52
|
+
export interface BrowserAutomationNavigateResult {
|
|
53
|
+
frameId?: string;
|
|
54
|
+
loaderId?: string;
|
|
55
|
+
}
|
|
56
|
+
export type BrowserAutomationElementTarget = {
|
|
57
|
+
ref: string;
|
|
58
|
+
} | {
|
|
59
|
+
selector: string;
|
|
60
|
+
};
|
|
61
|
+
export type BrowserAutomationClickTarget = BrowserAutomationElementTarget | {
|
|
62
|
+
x: number;
|
|
63
|
+
y: number;
|
|
64
|
+
};
|
|
65
|
+
export interface BrowserAutomationScreenshotOptions {
|
|
66
|
+
format?: "png" | "jpeg" | "webp";
|
|
67
|
+
quality?: number;
|
|
68
|
+
}
|
|
69
|
+
export interface BrowserAutomationScreenshot {
|
|
70
|
+
format: "png" | "jpeg" | "webp";
|
|
71
|
+
mimeType: "image/png" | "image/jpeg" | "image/webp";
|
|
72
|
+
/** Canonical base64 returned by CDP, without a data-URL prefix. */
|
|
73
|
+
data: string;
|
|
74
|
+
}
|
|
75
|
+
export declare class BrowserAutomationError extends Error {
|
|
76
|
+
readonly code: "connection" | "invalid_input" | "protocol" | "target_not_found" | "timeout";
|
|
77
|
+
readonly name = "BrowserAutomationError";
|
|
78
|
+
constructor(message: string, code: "connection" | "invalid_input" | "protocol" | "target_not_found" | "timeout");
|
|
79
|
+
}
|
|
80
|
+
export declare class BrowserAutomationStaleReferenceError extends Error {
|
|
81
|
+
readonly ref: string;
|
|
82
|
+
readonly name = "BrowserAutomationStaleReferenceError";
|
|
83
|
+
constructor(ref: string);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* A deliberately small automation facade over CDP.
|
|
87
|
+
*
|
|
88
|
+
* It does not infer selectors or wait for application-specific readiness.
|
|
89
|
+
* Snapshot refs can cross short-lived CLI processes, but are bound to the
|
|
90
|
+
* endpoint, Page target, and current main-document loader.
|
|
91
|
+
*/
|
|
92
|
+
export declare class BrowserAutomationClient {
|
|
93
|
+
private readonly connection;
|
|
94
|
+
private readonly pageSessionId;
|
|
95
|
+
private readonly referenceScope;
|
|
96
|
+
private readonly referenceKey;
|
|
97
|
+
private documentGenerationValue;
|
|
98
|
+
private accessibilityEnabled;
|
|
99
|
+
private domEnabled;
|
|
100
|
+
private closed;
|
|
101
|
+
private constructor();
|
|
102
|
+
static connect(options: BrowserAutomationClientOptions): Promise<BrowserAutomationClient>;
|
|
103
|
+
get documentGeneration(): number;
|
|
104
|
+
snapshot(): Promise<BrowserAutomationSnapshot>;
|
|
105
|
+
navigate(url: string): Promise<BrowserAutomationNavigateResult>;
|
|
106
|
+
click(target: BrowserAutomationClickTarget): Promise<void>;
|
|
107
|
+
type(target: BrowserAutomationElementTarget, text: string): Promise<void>;
|
|
108
|
+
screenshot(options?: BrowserAutomationScreenshotOptions): Promise<BrowserAutomationScreenshot>;
|
|
109
|
+
close(): void;
|
|
110
|
+
private pageCall;
|
|
111
|
+
private ensureDomEnabled;
|
|
112
|
+
private snapshotNode;
|
|
113
|
+
private resolveElement;
|
|
114
|
+
private resolveObjectId;
|
|
115
|
+
private currentDocumentIdentity;
|
|
116
|
+
private elementCenter;
|
|
117
|
+
private invalidateDocument;
|
|
118
|
+
private assertOpen;
|
|
119
|
+
}
|
|
120
|
+
export declare function connectBrowserAutomation(options: BrowserAutomationClientOptions): Promise<BrowserAutomationClient>;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
const DEFAULT_COMMAND_TIMEOUT_MS = 10_000;
|
|
3
|
+
const MAX_COMMAND_TIMEOUT_MS = 120_000;
|
|
4
|
+
const MAX_PENDING_COMMANDS = 128;
|
|
5
|
+
const MAX_MESSAGE_BYTES = 16 * 1024 * 1024;
|
|
6
|
+
const MAX_SNAPSHOT_NODES = 10_000;
|
|
7
|
+
const MAX_SELECTOR_CHARS = 4_096;
|
|
8
|
+
const MAX_INPUT_TEXT_BYTES = 64 * 1024;
|
|
9
|
+
const WEB_SOCKET_OPEN = 1;
|
|
10
|
+
const REFERENCE_VERSION = 1;
|
|
11
|
+
const REFERENCE_DOCUMENT_FINGERPRINT_BYTES = 12;
|
|
12
|
+
const REFERENCE_BACKEND_NODE_BYTES = 8;
|
|
13
|
+
const REFERENCE_TAG_BYTES = 16;
|
|
14
|
+
const REFERENCE_PAYLOAD_BYTES = 1 + REFERENCE_DOCUMENT_FINGERPRINT_BYTES + REFERENCE_BACKEND_NODE_BYTES;
|
|
15
|
+
const REFERENCE_BYTES = REFERENCE_PAYLOAD_BYTES + REFERENCE_TAG_BYTES;
|
|
16
|
+
const REFERENCE_DOMAIN = Buffer.from("rynx-browser-cdp-ref-v1\0", "utf8");
|
|
17
|
+
export class BrowserAutomationError extends Error {
|
|
18
|
+
code;
|
|
19
|
+
name = "BrowserAutomationError";
|
|
20
|
+
constructor(message, code) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.code = code;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class BrowserAutomationStaleReferenceError extends Error {
|
|
26
|
+
ref;
|
|
27
|
+
name = "BrowserAutomationStaleReferenceError";
|
|
28
|
+
constructor(ref) {
|
|
29
|
+
super(`Browser reference ${ref} is stale; take a new snapshot`);
|
|
30
|
+
this.ref = ref;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A deliberately small automation facade over CDP.
|
|
35
|
+
*
|
|
36
|
+
* It does not infer selectors or wait for application-specific readiness.
|
|
37
|
+
* Snapshot refs can cross short-lived CLI processes, but are bound to the
|
|
38
|
+
* endpoint, Page target, and current main-document loader.
|
|
39
|
+
*/
|
|
40
|
+
export class BrowserAutomationClient {
|
|
41
|
+
connection;
|
|
42
|
+
pageSessionId;
|
|
43
|
+
referenceScope;
|
|
44
|
+
referenceKey;
|
|
45
|
+
documentGenerationValue = 1;
|
|
46
|
+
accessibilityEnabled = false;
|
|
47
|
+
domEnabled = false;
|
|
48
|
+
closed = false;
|
|
49
|
+
constructor(connection, referenceScope, referenceKey, pageSessionId) {
|
|
50
|
+
this.connection = connection;
|
|
51
|
+
this.referenceScope = referenceScope;
|
|
52
|
+
this.referenceKey = referenceKey;
|
|
53
|
+
this.pageSessionId = pageSessionId;
|
|
54
|
+
this.connection.onEvent((method, params, sessionId) => {
|
|
55
|
+
if (this.pageSessionId && sessionId && sessionId !== this.pageSessionId)
|
|
56
|
+
return;
|
|
57
|
+
if (method === "DOM.documentUpdated") {
|
|
58
|
+
this.invalidateDocument();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (method === "Page.frameNavigated") {
|
|
62
|
+
const frame = recordAt(params, "frame");
|
|
63
|
+
if (frame && typeof frame.parentId !== "string")
|
|
64
|
+
this.invalidateDocument();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (method === "Inspector.detached" ||
|
|
68
|
+
(method === "Target.detachedFromTarget" &&
|
|
69
|
+
stringAt(params, "sessionId") === this.pageSessionId)) {
|
|
70
|
+
this.invalidateDocument();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
static async connect(options) {
|
|
75
|
+
const endpoint = parseEndpoint(options.endpoint);
|
|
76
|
+
const endpointKind = options.endpointKind ?? inferEndpointKind(endpoint);
|
|
77
|
+
const timeoutMs = boundedInteger(options.commandTimeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS, 10, MAX_COMMAND_TIMEOUT_MS, "commandTimeoutMs");
|
|
78
|
+
const referenceKey = parseReferenceKey(options.referenceKey);
|
|
79
|
+
const connection = await CdpConnection.connect(endpoint.href, timeoutMs, options.createWebSocket ?? defaultWebSocketFactory);
|
|
80
|
+
try {
|
|
81
|
+
if (endpointKind === "page") {
|
|
82
|
+
return new BrowserAutomationClient(connection, endpoint.href, referenceKey);
|
|
83
|
+
}
|
|
84
|
+
const targetId = await selectPageTarget(connection, options.pageTargetId);
|
|
85
|
+
const attached = requireRecord(await connection.call("Target.attachToTarget", {
|
|
86
|
+
targetId,
|
|
87
|
+
flatten: true,
|
|
88
|
+
}), "Target.attachToTarget result");
|
|
89
|
+
const sessionId = requiredString(attached.sessionId, "Target.attachToTarget sessionId");
|
|
90
|
+
return new BrowserAutomationClient(connection, `${endpoint.href}\0${targetId}`, referenceKey, sessionId);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
connection.close();
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
get documentGeneration() {
|
|
98
|
+
return this.documentGenerationValue;
|
|
99
|
+
}
|
|
100
|
+
async snapshot() {
|
|
101
|
+
this.assertOpen();
|
|
102
|
+
// Enable DOM before minting refs. Chromium may announce an initial
|
|
103
|
+
// DOM.documentUpdated while the domain comes online; any such event must
|
|
104
|
+
// precede, rather than immediately invalidate, this snapshot's refs.
|
|
105
|
+
await this.ensureDomEnabled();
|
|
106
|
+
if (!this.accessibilityEnabled) {
|
|
107
|
+
await this.pageCall("Accessibility.enable");
|
|
108
|
+
this.accessibilityEnabled = true;
|
|
109
|
+
}
|
|
110
|
+
const documentIdentity = await this.currentDocumentIdentity();
|
|
111
|
+
const result = requireRecord(await this.pageCall("Accessibility.getFullAXTree"), "Accessibility.getFullAXTree result");
|
|
112
|
+
if (!Array.isArray(result.nodes)) {
|
|
113
|
+
throw protocolError("Accessibility.getFullAXTree result nodes must be an array");
|
|
114
|
+
}
|
|
115
|
+
if (result.nodes.length > MAX_SNAPSHOT_NODES) {
|
|
116
|
+
throw protocolError(`Accessibility snapshot exceeds ${MAX_SNAPSHOT_NODES} nodes`);
|
|
117
|
+
}
|
|
118
|
+
if ((await this.currentDocumentIdentity()) !== documentIdentity) {
|
|
119
|
+
throw protocolError("Browser document changed while taking the accessibility snapshot");
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
documentGeneration: this.documentGenerationValue,
|
|
123
|
+
nodes: result.nodes.map((value, index) => this.snapshotNode(value, index, documentIdentity)),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
async navigate(url) {
|
|
127
|
+
this.assertOpen();
|
|
128
|
+
const parsed = parseNavigationUrl(url);
|
|
129
|
+
// Refs must fail immediately, not after Chrome eventually emits a lifecycle event.
|
|
130
|
+
this.invalidateDocument();
|
|
131
|
+
const result = requireRecord(await this.pageCall("Page.navigate", { url: parsed }), "Page.navigate result");
|
|
132
|
+
const errorText = optionalString(result.errorText);
|
|
133
|
+
if (errorText)
|
|
134
|
+
throw protocolError(`Page.navigate failed: ${errorText}`);
|
|
135
|
+
return {
|
|
136
|
+
...(optionalString(result.frameId)
|
|
137
|
+
? { frameId: optionalString(result.frameId) }
|
|
138
|
+
: {}),
|
|
139
|
+
...(optionalString(result.loaderId)
|
|
140
|
+
? { loaderId: optionalString(result.loaderId) }
|
|
141
|
+
: {}),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
async click(target) {
|
|
145
|
+
this.assertOpen();
|
|
146
|
+
const element = "x" in target ? undefined : await this.resolveElement(target);
|
|
147
|
+
try {
|
|
148
|
+
const point = element
|
|
149
|
+
? await this.elementCenter(element)
|
|
150
|
+
: coordinateTarget(target);
|
|
151
|
+
await this.pageCall("Input.dispatchMouseEvent", {
|
|
152
|
+
type: "mouseMoved",
|
|
153
|
+
x: point.x,
|
|
154
|
+
y: point.y,
|
|
155
|
+
button: "none",
|
|
156
|
+
buttons: 0,
|
|
157
|
+
});
|
|
158
|
+
await this.pageCall("Input.dispatchMouseEvent", {
|
|
159
|
+
type: "mousePressed",
|
|
160
|
+
x: point.x,
|
|
161
|
+
y: point.y,
|
|
162
|
+
button: "left",
|
|
163
|
+
buttons: 1,
|
|
164
|
+
clickCount: 1,
|
|
165
|
+
});
|
|
166
|
+
await this.pageCall("Input.dispatchMouseEvent", {
|
|
167
|
+
type: "mouseReleased",
|
|
168
|
+
x: point.x,
|
|
169
|
+
y: point.y,
|
|
170
|
+
button: "left",
|
|
171
|
+
buttons: 0,
|
|
172
|
+
clickCount: 1,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
if (element?.objectId) {
|
|
177
|
+
await this.pageCall("Runtime.releaseObject", {
|
|
178
|
+
objectId: element.objectId,
|
|
179
|
+
}).catch(() => undefined);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async type(target, text) {
|
|
184
|
+
this.assertOpen();
|
|
185
|
+
if (typeof text !== "string" ||
|
|
186
|
+
Buffer.byteLength(text, "utf8") > MAX_INPUT_TEXT_BYTES) {
|
|
187
|
+
throw invalidInput(`text must be at most ${MAX_INPUT_TEXT_BYTES} UTF-8 bytes`);
|
|
188
|
+
}
|
|
189
|
+
const element = await this.resolveElement(target);
|
|
190
|
+
await this.ensureDomEnabled();
|
|
191
|
+
const objectId = element.objectId ?? await this.resolveObjectId(element);
|
|
192
|
+
try {
|
|
193
|
+
const focused = requireRecord(await this.pageCall("Runtime.callFunctionOn", {
|
|
194
|
+
objectId,
|
|
195
|
+
functionDeclaration: "function() { this.focus(); }",
|
|
196
|
+
returnByValue: true,
|
|
197
|
+
}), "Runtime.callFunctionOn result");
|
|
198
|
+
if (focused.exceptionDetails !== undefined) {
|
|
199
|
+
throw protocolError("Browser element could not be focused");
|
|
200
|
+
}
|
|
201
|
+
await this.pageCall("Input.insertText", { text });
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
await this.pageCall("Runtime.releaseObject", { objectId }).catch(() => undefined);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async screenshot(options = {}) {
|
|
208
|
+
this.assertOpen();
|
|
209
|
+
const format = options.format ?? "png";
|
|
210
|
+
if (format !== "png" && format !== "jpeg" && format !== "webp") {
|
|
211
|
+
throw invalidInput("screenshot format must be png, jpeg, or webp");
|
|
212
|
+
}
|
|
213
|
+
if (options.quality !== undefined &&
|
|
214
|
+
(!Number.isInteger(options.quality) ||
|
|
215
|
+
options.quality < 0 ||
|
|
216
|
+
options.quality > 100)) {
|
|
217
|
+
throw invalidInput("screenshot quality must be an integer from 0 to 100");
|
|
218
|
+
}
|
|
219
|
+
if (format === "png" && options.quality !== undefined) {
|
|
220
|
+
throw invalidInput("screenshot quality is unsupported for png");
|
|
221
|
+
}
|
|
222
|
+
const result = requireRecord(await this.pageCall("Page.captureScreenshot", {
|
|
223
|
+
format,
|
|
224
|
+
...(options.quality === undefined ? {} : { quality: options.quality }),
|
|
225
|
+
}), "Page.captureScreenshot result");
|
|
226
|
+
const data = requiredString(result.data, "Page.captureScreenshot data");
|
|
227
|
+
if (!canonicalBase64(data)) {
|
|
228
|
+
throw protocolError("Page.captureScreenshot returned invalid base64");
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
format,
|
|
232
|
+
mimeType: format === "png"
|
|
233
|
+
? "image/png"
|
|
234
|
+
: format === "jpeg"
|
|
235
|
+
? "image/jpeg"
|
|
236
|
+
: "image/webp",
|
|
237
|
+
data,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
close() {
|
|
241
|
+
if (this.closed)
|
|
242
|
+
return;
|
|
243
|
+
this.closed = true;
|
|
244
|
+
this.invalidateDocument();
|
|
245
|
+
this.connection.close();
|
|
246
|
+
}
|
|
247
|
+
async pageCall(method, params = {}) {
|
|
248
|
+
return await this.connection.call(method, params, this.pageSessionId);
|
|
249
|
+
}
|
|
250
|
+
async ensureDomEnabled() {
|
|
251
|
+
if (this.domEnabled)
|
|
252
|
+
return;
|
|
253
|
+
await this.pageCall("DOM.enable");
|
|
254
|
+
this.domEnabled = true;
|
|
255
|
+
}
|
|
256
|
+
snapshotNode(value, index, documentIdentity) {
|
|
257
|
+
const node = requireRecord(value, `Accessibility node ${index}`);
|
|
258
|
+
const nodeId = requiredString(node.nodeId, `Accessibility node ${index} nodeId`);
|
|
259
|
+
const backendNodeId = positiveInteger(node.backendDOMNodeId);
|
|
260
|
+
const ignored = node.ignored === true;
|
|
261
|
+
const role = axString(node.role);
|
|
262
|
+
const name = axString(node.name);
|
|
263
|
+
const rawChildIds = node.childIds;
|
|
264
|
+
const childIds = rawChildIds === undefined
|
|
265
|
+
? []
|
|
266
|
+
: Array.isArray(rawChildIds)
|
|
267
|
+
? rawChildIds.map((child, childIndex) => requiredString(child, `Accessibility node ${index} childIds[${childIndex}]`))
|
|
268
|
+
: (() => {
|
|
269
|
+
throw protocolError(`Accessibility node ${index} childIds must be an array`);
|
|
270
|
+
})();
|
|
271
|
+
const ref = !ignored && backendNodeId !== undefined
|
|
272
|
+
? encodeElementReference(this.referenceScope, documentIdentity, backendNodeId, this.referenceKey)
|
|
273
|
+
: undefined;
|
|
274
|
+
return {
|
|
275
|
+
nodeId,
|
|
276
|
+
...(optionalString(node.parentId)
|
|
277
|
+
? { parentId: optionalString(node.parentId) }
|
|
278
|
+
: {}),
|
|
279
|
+
childIds,
|
|
280
|
+
role,
|
|
281
|
+
name,
|
|
282
|
+
...(axOptionalString(node.value)
|
|
283
|
+
? { value: axOptionalString(node.value) }
|
|
284
|
+
: {}),
|
|
285
|
+
...(axOptionalString(node.description)
|
|
286
|
+
? { description: axOptionalString(node.description) }
|
|
287
|
+
: {}),
|
|
288
|
+
ignored,
|
|
289
|
+
...(ref ? { ref } : {}),
|
|
290
|
+
properties: axProperties(node.properties),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
async resolveElement(target) {
|
|
294
|
+
await this.ensureDomEnabled();
|
|
295
|
+
if ("ref" in target) {
|
|
296
|
+
if (typeof target.ref !== "string")
|
|
297
|
+
throw invalidInput("ref must be a string");
|
|
298
|
+
const documentIdentity = await this.currentDocumentIdentity();
|
|
299
|
+
const backendNodeId = decodeElementReference(target.ref, this.referenceScope, documentIdentity, this.referenceKey);
|
|
300
|
+
try {
|
|
301
|
+
return {
|
|
302
|
+
backendNodeId,
|
|
303
|
+
objectId: await this.resolveObjectId({ backendNodeId }),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
if (error instanceof BrowserAutomationError && error.code === "protocol") {
|
|
308
|
+
throw new BrowserAutomationStaleReferenceError(target.ref);
|
|
309
|
+
}
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (typeof target.selector !== "string" ||
|
|
314
|
+
target.selector.length === 0 ||
|
|
315
|
+
target.selector.length > MAX_SELECTOR_CHARS) {
|
|
316
|
+
throw invalidInput(`selector must be a non-empty string of at most ${MAX_SELECTOR_CHARS} characters`);
|
|
317
|
+
}
|
|
318
|
+
const document = requireRecord(await this.pageCall("DOM.getDocument", { depth: 0 }), "DOM.getDocument result");
|
|
319
|
+
const root = requireRecord(document.root, "DOM.getDocument root");
|
|
320
|
+
const rootNodeId = positiveInteger(root.nodeId);
|
|
321
|
+
if (rootNodeId === undefined) {
|
|
322
|
+
throw protocolError("DOM.getDocument root nodeId is invalid");
|
|
323
|
+
}
|
|
324
|
+
const query = requireRecord(await this.pageCall("DOM.querySelector", {
|
|
325
|
+
nodeId: rootNodeId,
|
|
326
|
+
selector: target.selector,
|
|
327
|
+
}), "DOM.querySelector result");
|
|
328
|
+
const nodeId = positiveInteger(query.nodeId);
|
|
329
|
+
if (nodeId === undefined) {
|
|
330
|
+
throw new BrowserAutomationError(`No element matches selector ${JSON.stringify(target.selector)}`, "target_not_found");
|
|
331
|
+
}
|
|
332
|
+
return { nodeId };
|
|
333
|
+
}
|
|
334
|
+
async resolveObjectId(element) {
|
|
335
|
+
const resolved = requireRecord(await this.pageCall("DOM.resolveNode", domLocator(element)), "DOM.resolveNode result");
|
|
336
|
+
const object = requireRecord(resolved.object, "DOM.resolveNode object");
|
|
337
|
+
return requiredString(object.objectId, "DOM.resolveNode objectId");
|
|
338
|
+
}
|
|
339
|
+
async currentDocumentIdentity() {
|
|
340
|
+
const result = requireRecord(await this.pageCall("Page.getFrameTree"), "Page.getFrameTree result");
|
|
341
|
+
const frameTree = requireRecord(result.frameTree, "Page.getFrameTree frameTree");
|
|
342
|
+
const frame = requireRecord(frameTree.frame, "Page.getFrameTree frame");
|
|
343
|
+
return requiredString(frame.loaderId, "Page.getFrameTree loaderId");
|
|
344
|
+
}
|
|
345
|
+
async elementCenter(element) {
|
|
346
|
+
await this.ensureDomEnabled();
|
|
347
|
+
const locator = domLocator(element);
|
|
348
|
+
await this.pageCall("DOM.scrollIntoViewIfNeeded", locator);
|
|
349
|
+
const result = requireRecord(await this.pageCall("DOM.getBoxModel", locator), "DOM.getBoxModel result");
|
|
350
|
+
const model = requireRecord(result.model, "DOM.getBoxModel model");
|
|
351
|
+
const quad = numericQuad(model.content ?? model.border);
|
|
352
|
+
return {
|
|
353
|
+
x: (quad[0] + quad[2] + quad[4] + quad[6]) / 4,
|
|
354
|
+
y: (quad[1] + quad[3] + quad[5] + quad[7]) / 4,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
invalidateDocument() {
|
|
358
|
+
this.documentGenerationValue += 1;
|
|
359
|
+
}
|
|
360
|
+
assertOpen() {
|
|
361
|
+
if (this.closed) {
|
|
362
|
+
throw new BrowserAutomationError("Browser automation client is closed", "connection");
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
export async function connectBrowserAutomation(options) {
|
|
367
|
+
return await BrowserAutomationClient.connect(options);
|
|
368
|
+
}
|
|
369
|
+
class CdpConnection {
|
|
370
|
+
socket;
|
|
371
|
+
timeoutMs;
|
|
372
|
+
nextId = 1;
|
|
373
|
+
closed = false;
|
|
374
|
+
pending = new Map();
|
|
375
|
+
eventListeners = new Set();
|
|
376
|
+
constructor(socket, timeoutMs) {
|
|
377
|
+
this.socket = socket;
|
|
378
|
+
this.timeoutMs = timeoutMs;
|
|
379
|
+
socket.onmessage = (event) => this.receive(event.data);
|
|
380
|
+
socket.onerror = () => this.failConnection("CDP WebSocket failed");
|
|
381
|
+
socket.onclose = (event) => {
|
|
382
|
+
const detail = event.reason
|
|
383
|
+
? `: ${event.reason}`
|
|
384
|
+
: event.code
|
|
385
|
+
? ` (code ${event.code})`
|
|
386
|
+
: "";
|
|
387
|
+
this.failConnection(`CDP WebSocket closed${detail}`);
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
static async connect(endpoint, timeoutMs, createWebSocket) {
|
|
391
|
+
let socket;
|
|
392
|
+
try {
|
|
393
|
+
socket = createWebSocket(endpoint);
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
throw new BrowserAutomationError(`Could not create CDP WebSocket: ${errorMessage(error)}`, "connection");
|
|
397
|
+
}
|
|
398
|
+
await new Promise((resolve, reject) => {
|
|
399
|
+
const timer = setTimeout(() => {
|
|
400
|
+
socket.close(1000, "connection timeout");
|
|
401
|
+
reject(new BrowserAutomationError(`CDP WebSocket did not open within ${timeoutMs}ms`, "timeout"));
|
|
402
|
+
}, timeoutMs);
|
|
403
|
+
socket.onopen = () => {
|
|
404
|
+
clearTimeout(timer);
|
|
405
|
+
resolve();
|
|
406
|
+
};
|
|
407
|
+
socket.onerror = () => {
|
|
408
|
+
clearTimeout(timer);
|
|
409
|
+
reject(new BrowserAutomationError("CDP WebSocket failed to open", "connection"));
|
|
410
|
+
};
|
|
411
|
+
socket.onclose = () => {
|
|
412
|
+
clearTimeout(timer);
|
|
413
|
+
reject(new BrowserAutomationError("CDP WebSocket closed before opening", "connection"));
|
|
414
|
+
};
|
|
415
|
+
});
|
|
416
|
+
return new CdpConnection(socket, timeoutMs);
|
|
417
|
+
}
|
|
418
|
+
onEvent(listener) {
|
|
419
|
+
this.eventListeners.add(listener);
|
|
420
|
+
}
|
|
421
|
+
async call(method, params = {}, sessionId) {
|
|
422
|
+
if (this.closed || this.socket.readyState !== WEB_SOCKET_OPEN) {
|
|
423
|
+
throw new BrowserAutomationError("CDP WebSocket is not open", "connection");
|
|
424
|
+
}
|
|
425
|
+
if (this.pending.size >= MAX_PENDING_COMMANDS) {
|
|
426
|
+
throw new BrowserAutomationError(`CDP has ${MAX_PENDING_COMMANDS} pending commands`, "connection");
|
|
427
|
+
}
|
|
428
|
+
const id = this.nextId++;
|
|
429
|
+
const request = {
|
|
430
|
+
id,
|
|
431
|
+
method,
|
|
432
|
+
params,
|
|
433
|
+
...(sessionId ? { sessionId } : {}),
|
|
434
|
+
};
|
|
435
|
+
return await new Promise((resolve, reject) => {
|
|
436
|
+
const timer = setTimeout(() => {
|
|
437
|
+
this.pending.delete(id);
|
|
438
|
+
reject(new BrowserAutomationError(`${method} timed out after ${this.timeoutMs}ms`, "timeout"));
|
|
439
|
+
}, this.timeoutMs);
|
|
440
|
+
this.pending.set(id, { method, resolve, reject, timer });
|
|
441
|
+
try {
|
|
442
|
+
this.socket.send(JSON.stringify(request));
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
clearTimeout(timer);
|
|
446
|
+
this.pending.delete(id);
|
|
447
|
+
reject(new BrowserAutomationError(`${method} could not be sent: ${errorMessage(error)}`, "connection"));
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
close() {
|
|
452
|
+
if (this.closed)
|
|
453
|
+
return;
|
|
454
|
+
this.closed = true;
|
|
455
|
+
this.rejectPending(new BrowserAutomationError("CDP WebSocket was closed", "connection"));
|
|
456
|
+
if (this.socket.readyState <= WEB_SOCKET_OPEN) {
|
|
457
|
+
this.socket.close(1000, "automation client closed");
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
receive(data) {
|
|
461
|
+
let raw;
|
|
462
|
+
if (typeof data === "string") {
|
|
463
|
+
raw = data;
|
|
464
|
+
}
|
|
465
|
+
else if (data instanceof ArrayBuffer) {
|
|
466
|
+
if (data.byteLength > MAX_MESSAGE_BYTES) {
|
|
467
|
+
this.failConnection("CDP message exceeded its size limit");
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
raw = new TextDecoder().decode(data);
|
|
471
|
+
}
|
|
472
|
+
else if (ArrayBuffer.isView(data)) {
|
|
473
|
+
if (data.byteLength > MAX_MESSAGE_BYTES) {
|
|
474
|
+
this.failConnection("CDP message exceeded its size limit");
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
raw = new TextDecoder().decode(data);
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
this.failConnection("CDP returned a non-text message");
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (Buffer.byteLength(raw, "utf8") > MAX_MESSAGE_BYTES) {
|
|
484
|
+
this.failConnection("CDP message exceeded its size limit");
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
let message;
|
|
488
|
+
try {
|
|
489
|
+
message = JSON.parse(raw);
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
this.failConnection("CDP returned invalid JSON");
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (!message || typeof message !== "object" || Array.isArray(message)) {
|
|
496
|
+
this.failConnection("CDP returned an invalid message");
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
if (typeof message.id === "number" && Number.isSafeInteger(message.id)) {
|
|
500
|
+
const pending = this.pending.get(message.id);
|
|
501
|
+
if (!pending)
|
|
502
|
+
return;
|
|
503
|
+
clearTimeout(pending.timer);
|
|
504
|
+
this.pending.delete(message.id);
|
|
505
|
+
if (message.error !== undefined) {
|
|
506
|
+
const error = isRecord(message.error) ? message.error : {};
|
|
507
|
+
const detail = optionalString(error.message) ?? "unknown protocol error";
|
|
508
|
+
pending.reject(protocolError(`${pending.method} failed: ${detail}`));
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
pending.resolve(message.result ?? {});
|
|
512
|
+
}
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (typeof message.method === "string") {
|
|
516
|
+
const params = isRecord(message.params) ? message.params : {};
|
|
517
|
+
const sessionId = optionalString(message.sessionId);
|
|
518
|
+
for (const listener of this.eventListeners) {
|
|
519
|
+
try {
|
|
520
|
+
listener(message.method, params, sessionId);
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
// Observers cannot corrupt command transport.
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
this.failConnection("CDP returned an unrecognized message");
|
|
529
|
+
}
|
|
530
|
+
failConnection(message) {
|
|
531
|
+
if (this.closed)
|
|
532
|
+
return;
|
|
533
|
+
this.closed = true;
|
|
534
|
+
this.rejectPending(new BrowserAutomationError(message, "connection"));
|
|
535
|
+
if (this.socket.readyState <= WEB_SOCKET_OPEN) {
|
|
536
|
+
this.socket.close(1002, "invalid CDP connection");
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
rejectPending(error) {
|
|
540
|
+
for (const pending of this.pending.values()) {
|
|
541
|
+
clearTimeout(pending.timer);
|
|
542
|
+
pending.reject(error);
|
|
543
|
+
}
|
|
544
|
+
this.pending.clear();
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
async function selectPageTarget(connection, requestedTargetId) {
|
|
548
|
+
const result = requireRecord(await connection.call("Target.getTargets"), "Target.getTargets result");
|
|
549
|
+
if (!Array.isArray(result.targetInfos)) {
|
|
550
|
+
throw protocolError("Target.getTargets result targetInfos must be an array");
|
|
551
|
+
}
|
|
552
|
+
const pages = result.targetInfos
|
|
553
|
+
.map((value) => (isRecord(value) ? value : undefined))
|
|
554
|
+
.filter((value) => value !== undefined &&
|
|
555
|
+
value.type === "page" &&
|
|
556
|
+
typeof value.targetId === "string");
|
|
557
|
+
const selected = requestedTargetId
|
|
558
|
+
? pages.find((target) => target.targetId === requestedTargetId)
|
|
559
|
+
: pages[0];
|
|
560
|
+
if (!selected) {
|
|
561
|
+
throw new BrowserAutomationError(requestedTargetId
|
|
562
|
+
? `CDP Page target ${requestedTargetId} was not found`
|
|
563
|
+
: "CDP endpoint has no Page target", "target_not_found");
|
|
564
|
+
}
|
|
565
|
+
return selected.targetId;
|
|
566
|
+
}
|
|
567
|
+
function defaultWebSocketFactory(endpoint) {
|
|
568
|
+
const Constructor = globalThis.WebSocket;
|
|
569
|
+
if (typeof Constructor !== "function") {
|
|
570
|
+
throw new BrowserAutomationError("This runtime does not provide a WebSocket client", "connection");
|
|
571
|
+
}
|
|
572
|
+
return new Constructor(endpoint);
|
|
573
|
+
}
|
|
574
|
+
function encodeElementReference(referenceScope, documentIdentity, backendNodeId, referenceKey) {
|
|
575
|
+
const payload = Buffer.alloc(REFERENCE_PAYLOAD_BYTES);
|
|
576
|
+
payload[0] = REFERENCE_VERSION;
|
|
577
|
+
documentFingerprint(referenceScope, documentIdentity).copy(payload, 1);
|
|
578
|
+
payload.writeBigUInt64BE(BigInt(backendNodeId), 1 + REFERENCE_DOCUMENT_FINGERPRINT_BYTES);
|
|
579
|
+
const tag = referenceTag(payload, referenceKey);
|
|
580
|
+
return Buffer.concat([payload, tag]).toString("base64url");
|
|
581
|
+
}
|
|
582
|
+
function decodeElementReference(ref, referenceScope, documentIdentity, referenceKey) {
|
|
583
|
+
if (typeof ref !== "string" ||
|
|
584
|
+
ref.length === 0 ||
|
|
585
|
+
!/^[A-Za-z0-9_-]+$/.test(ref)) {
|
|
586
|
+
throw new BrowserAutomationStaleReferenceError(String(ref));
|
|
587
|
+
}
|
|
588
|
+
let encoded;
|
|
589
|
+
try {
|
|
590
|
+
encoded = Buffer.from(ref, "base64url");
|
|
591
|
+
}
|
|
592
|
+
catch {
|
|
593
|
+
throw new BrowserAutomationStaleReferenceError(ref);
|
|
594
|
+
}
|
|
595
|
+
if (encoded.length !== REFERENCE_BYTES ||
|
|
596
|
+
encoded.toString("base64url") !== ref ||
|
|
597
|
+
encoded[0] !== REFERENCE_VERSION) {
|
|
598
|
+
throw new BrowserAutomationStaleReferenceError(ref);
|
|
599
|
+
}
|
|
600
|
+
const payload = encoded.subarray(0, REFERENCE_PAYLOAD_BYTES);
|
|
601
|
+
const actualTag = encoded.subarray(REFERENCE_PAYLOAD_BYTES);
|
|
602
|
+
if (!timingSafeEqual(actualTag, referenceTag(payload, referenceKey))) {
|
|
603
|
+
throw new BrowserAutomationStaleReferenceError(ref);
|
|
604
|
+
}
|
|
605
|
+
const expectedDocument = documentFingerprint(referenceScope, documentIdentity);
|
|
606
|
+
const actualDocument = payload.subarray(1, 1 + REFERENCE_DOCUMENT_FINGERPRINT_BYTES);
|
|
607
|
+
if (!timingSafeEqual(actualDocument, expectedDocument)) {
|
|
608
|
+
throw new BrowserAutomationStaleReferenceError(ref);
|
|
609
|
+
}
|
|
610
|
+
const rawNodeId = payload.readBigUInt64BE(1 + REFERENCE_DOCUMENT_FINGERPRINT_BYTES);
|
|
611
|
+
if (rawNodeId < 1n || rawNodeId > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
612
|
+
throw new BrowserAutomationStaleReferenceError(ref);
|
|
613
|
+
}
|
|
614
|
+
return Number(rawNodeId);
|
|
615
|
+
}
|
|
616
|
+
function documentFingerprint(referenceScope, documentIdentity) {
|
|
617
|
+
return createHash("sha256")
|
|
618
|
+
.update("rynx-browser-cdp-document-v1\0")
|
|
619
|
+
.update(referenceScope)
|
|
620
|
+
.update("\0")
|
|
621
|
+
.update(documentIdentity)
|
|
622
|
+
.digest()
|
|
623
|
+
.subarray(0, REFERENCE_DOCUMENT_FINGERPRINT_BYTES);
|
|
624
|
+
}
|
|
625
|
+
function referenceTag(payload, referenceKey) {
|
|
626
|
+
return createHmac("sha256", referenceKey)
|
|
627
|
+
.update(REFERENCE_DOMAIN)
|
|
628
|
+
.update(payload)
|
|
629
|
+
.digest()
|
|
630
|
+
.subarray(0, REFERENCE_TAG_BYTES);
|
|
631
|
+
}
|
|
632
|
+
function parseReferenceKey(value) {
|
|
633
|
+
const key = typeof value === "string" ? Buffer.from(value, "utf8") : Buffer.from(value);
|
|
634
|
+
if (key.length < 32 || key.length > 256) {
|
|
635
|
+
throw invalidInput("referenceKey must contain between 32 and 256 bytes");
|
|
636
|
+
}
|
|
637
|
+
return key;
|
|
638
|
+
}
|
|
639
|
+
function domLocator(element) {
|
|
640
|
+
if (element.objectId)
|
|
641
|
+
return { objectId: element.objectId };
|
|
642
|
+
if (element.backendNodeId !== undefined) {
|
|
643
|
+
return { backendNodeId: element.backendNodeId };
|
|
644
|
+
}
|
|
645
|
+
if (element.nodeId !== undefined)
|
|
646
|
+
return { nodeId: element.nodeId };
|
|
647
|
+
throw protocolError("Browser element has no CDP identity");
|
|
648
|
+
}
|
|
649
|
+
function parseEndpoint(endpoint) {
|
|
650
|
+
if (typeof endpoint !== "string" || endpoint.length === 0 || endpoint.length > 4_096) {
|
|
651
|
+
throw invalidInput("endpoint must be a non-empty bounded string");
|
|
652
|
+
}
|
|
653
|
+
let url;
|
|
654
|
+
try {
|
|
655
|
+
url = new URL(endpoint);
|
|
656
|
+
}
|
|
657
|
+
catch {
|
|
658
|
+
throw invalidInput("endpoint must be a WebSocket URL");
|
|
659
|
+
}
|
|
660
|
+
if (url.protocol !== "ws:" ||
|
|
661
|
+
url.hostname !== "127.0.0.1" ||
|
|
662
|
+
url.port.length === 0 ||
|
|
663
|
+
url.username.length > 0 ||
|
|
664
|
+
url.password.length > 0 ||
|
|
665
|
+
url.pathname === "/" ||
|
|
666
|
+
url.search.length > 0 ||
|
|
667
|
+
url.hash.length > 0) {
|
|
668
|
+
throw invalidInput("endpoint must be a canonical 127.0.0.1 CDP WebSocket URL");
|
|
669
|
+
}
|
|
670
|
+
return url;
|
|
671
|
+
}
|
|
672
|
+
function inferEndpointKind(endpoint) {
|
|
673
|
+
if (endpoint.pathname.startsWith("/devtools/browser/"))
|
|
674
|
+
return "browser";
|
|
675
|
+
if (endpoint.pathname.startsWith("/devtools/page/"))
|
|
676
|
+
return "page";
|
|
677
|
+
throw invalidInput("endpointKind is required for a non-standard CDP WebSocket path");
|
|
678
|
+
}
|
|
679
|
+
function parseNavigationUrl(value) {
|
|
680
|
+
if (value === "about:blank")
|
|
681
|
+
return value;
|
|
682
|
+
let url;
|
|
683
|
+
try {
|
|
684
|
+
url = new URL(value);
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
throw invalidInput("navigation URL must be absolute");
|
|
688
|
+
}
|
|
689
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
690
|
+
throw invalidInput("navigation URL must use http or https");
|
|
691
|
+
}
|
|
692
|
+
return url.href;
|
|
693
|
+
}
|
|
694
|
+
function coordinateTarget(value) {
|
|
695
|
+
if (!Number.isFinite(value.x) || !Number.isFinite(value.y)) {
|
|
696
|
+
throw invalidInput("click coordinates must be finite");
|
|
697
|
+
}
|
|
698
|
+
return { x: value.x, y: value.y };
|
|
699
|
+
}
|
|
700
|
+
function numericQuad(value) {
|
|
701
|
+
if (!Array.isArray(value) ||
|
|
702
|
+
value.length !== 8 ||
|
|
703
|
+
value.some((coordinate) => typeof coordinate !== "number" || !Number.isFinite(coordinate))) {
|
|
704
|
+
throw protocolError("DOM.getBoxModel returned an invalid content quad");
|
|
705
|
+
}
|
|
706
|
+
return value;
|
|
707
|
+
}
|
|
708
|
+
function axString(value) {
|
|
709
|
+
return axOptionalString(value) ?? "";
|
|
710
|
+
}
|
|
711
|
+
function axOptionalString(value) {
|
|
712
|
+
if (!isRecord(value))
|
|
713
|
+
return undefined;
|
|
714
|
+
const candidate = value.value;
|
|
715
|
+
if (typeof candidate === "string" ||
|
|
716
|
+
typeof candidate === "number" ||
|
|
717
|
+
typeof candidate === "boolean") {
|
|
718
|
+
return String(candidate);
|
|
719
|
+
}
|
|
720
|
+
return undefined;
|
|
721
|
+
}
|
|
722
|
+
function axProperties(value) {
|
|
723
|
+
if (!Array.isArray(value))
|
|
724
|
+
return Object.freeze({});
|
|
725
|
+
const properties = {};
|
|
726
|
+
for (const entry of value) {
|
|
727
|
+
if (!isRecord(entry) || typeof entry.name !== "string")
|
|
728
|
+
continue;
|
|
729
|
+
const property = isRecord(entry.value) ? entry.value.value : undefined;
|
|
730
|
+
if (typeof property === "string" ||
|
|
731
|
+
typeof property === "number" ||
|
|
732
|
+
typeof property === "boolean") {
|
|
733
|
+
properties[entry.name] = property;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
return Object.freeze(properties);
|
|
737
|
+
}
|
|
738
|
+
function canonicalBase64(value) {
|
|
739
|
+
if (value.length === 0 || value.length > MAX_MESSAGE_BYTES)
|
|
740
|
+
return false;
|
|
741
|
+
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) {
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
return Buffer.from(value, "base64").toString("base64") === value;
|
|
745
|
+
}
|
|
746
|
+
function boundedInteger(value, minimum, maximum, field) {
|
|
747
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
748
|
+
throw invalidInput(`${field} must be an integer from ${minimum} to ${maximum}`);
|
|
749
|
+
}
|
|
750
|
+
return value;
|
|
751
|
+
}
|
|
752
|
+
function positiveInteger(value) {
|
|
753
|
+
return Number.isSafeInteger(value) && value > 0
|
|
754
|
+
? value
|
|
755
|
+
: undefined;
|
|
756
|
+
}
|
|
757
|
+
function recordAt(value, key) {
|
|
758
|
+
return isRecord(value[key]) ? value[key] : undefined;
|
|
759
|
+
}
|
|
760
|
+
function stringAt(value, key) {
|
|
761
|
+
return optionalString(value[key]);
|
|
762
|
+
}
|
|
763
|
+
function optionalString(value) {
|
|
764
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
765
|
+
}
|
|
766
|
+
function requiredString(value, field) {
|
|
767
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
768
|
+
throw protocolError(`${field} must be a non-empty string`);
|
|
769
|
+
}
|
|
770
|
+
return value;
|
|
771
|
+
}
|
|
772
|
+
function isRecord(value) {
|
|
773
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
774
|
+
}
|
|
775
|
+
function requireRecord(value, field) {
|
|
776
|
+
if (!isRecord(value))
|
|
777
|
+
throw protocolError(`${field} must be an object`);
|
|
778
|
+
return value;
|
|
779
|
+
}
|
|
780
|
+
function invalidInput(message) {
|
|
781
|
+
return new BrowserAutomationError(message, "invalid_input");
|
|
782
|
+
}
|
|
783
|
+
function protocolError(message) {
|
|
784
|
+
return new BrowserAutomationError(message, "protocol");
|
|
785
|
+
}
|
|
786
|
+
function errorMessage(error) {
|
|
787
|
+
return error instanceof Error ? error.message : String(error);
|
|
788
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { BrowserAutomationClient, BrowserAutomationError, BrowserAutomationStaleReferenceError, connectBrowserAutomation, type BrowserAutomationClientOptions, type BrowserAutomationClickTarget, type BrowserAutomationElementTarget, type BrowserAutomationEndpointKind, type BrowserAutomationNavigateResult, type BrowserAutomationScreenshot, type BrowserAutomationScreenshotOptions, type BrowserAutomationSnapshot, type BrowserAutomationSnapshotNode, type BrowserAutomationWebSocket, type BrowserAutomationWebSocketFactory, } from "./client.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { BrowserAutomationClient, BrowserAutomationError, BrowserAutomationStaleReferenceError, connectBrowserAutomation, } from "./client.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rynx-ai/browser-cdp",
|
|
3
|
+
"version": "0.1.10",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/rynx-ai/rynx.git",
|
|
7
|
+
"directory": "packages/browser-cdp"
|
|
8
|
+
},
|
|
9
|
+
"description": "Dependency-free CDP client for Rynx Browser automation.",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"registry": "https://registry.npmjs.org/",
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "rm -rf dist && tsc -p tsconfig.json"
|
|
29
|
+
}
|
|
30
|
+
}
|