@ricsam/r5d-browser 0.0.113 → 0.0.114
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/cjs/browser-input.cjs +59 -0
- package/dist/cjs/browser-runtime.cjs +5 -2
- package/dist/cjs/main.cjs +1 -0
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/password-safe-snapshot.cjs +150 -0
- package/dist/mjs/browser-input.mjs +35 -0
- package/dist/mjs/browser-runtime.mjs +5 -2
- package/dist/mjs/main.mjs +1 -0
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/password-safe-snapshot.mjs +125 -0
- package/dist/types/browser-input.d.ts +81 -0
- package/dist/types/password-safe-snapshot.d.ts +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,6 +22,8 @@ r5d-browser start --chrome-path "/Applications/Google Chrome.app/Contents/MacOS/
|
|
|
22
22
|
|
|
23
23
|
The profile is stored under `~/.r5d/browser/chrome-profile`. The previous Chrome for Testing profile at `~/.r5d/browser/profile` is preserved but is not migrated automatically, so sign in again the first time you use the new profile. Browser binaries previously installed under `~/.r5d/browser/browsers` are also left untouched, but new versions of `r5d-browser` do not use them. Completed downloads are retained under `~/.r5d/browser/downloads`; agents can list them and copy a selected file into the active session's artifacts. That artifact is then synchronized to every connected worker. Browser screenshots are session artifacts in r5d.dev and are not retained by the browser process.
|
|
24
24
|
|
|
25
|
+
Password input values are replaced with `[redacted]` in `browser_read_page` snapshots, including values used in accessibility labels and iframe content. Reading a snapshot preserves the actual form values.
|
|
26
|
+
|
|
25
27
|
Agents read pages through `browser_read_page`, an accessibility outline with stable element refs (`[ref=e12]`) on interactable elements and `[box=x,y,w,h]` viewport coordinates on everything visible. Refs feed the one-call interaction tools: `browser_mouse_click` clicks a ref (scrolled into view, with actionability checks) or an x/y point, `browser_form_input` sets values on text inputs, selects, checkboxes, and contenteditable elements with trusted events that controlled (React-style) components honor, and `browser_scroll` scrolls an element into view or dispatches trusted wheel input. Same-origin iframe content appears inline with frame-prefixed refs such as `f1e3`; cross-origin frame content may be omitted. Refs come from the most recent snapshot of a tab, so agents re-read the page after navigations or large re-renders. `browser_wait_for` waits for a selector, visible text, a URL glob, or a load state instead of sleeping, and `browser_keyboard_type` has a `keys` mode that presses each character as a real key event (up to 500 characters per call) for type-ahead widgets — characters without key codes, such as emoji, still fall back to text insertion. Screenshots capture the viewport by default; `fullPage: true` captures the scrollable page up to 4000 CSS px of height.
|
|
26
28
|
|
|
27
29
|
Agents running JavaScript in a page can save captured text straight into the active session's artifacts by calling `r5dCreateArtifact(filename, content)`, which takes two strings and must be called before the agent's code returns. Each captured file is written to the session artifacts in r5d.dev, synchronized to every connected worker, and reported back under `$R5D_ROOT/artifacts/$R5D_SESSION_ID`; a single call may create at most 32 files totalling 4 MiB. The function is passed into the agent's code rather than assigned to `window`, so pages can neither call it nor use it to detect automation.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var browser_input_exports = {};
|
|
20
|
+
__export(browser_input_exports, {
|
|
21
|
+
normalizeBrowserInput: () => normalizeBrowserInput
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(browser_input_exports);
|
|
24
|
+
const browserInputFields = {
|
|
25
|
+
list_tabs: { required: [], optional: [] },
|
|
26
|
+
list_downloads: { required: [], optional: [] },
|
|
27
|
+
get_download: { required: ["downloadId"], optional: ["offset", "maxBytes"] },
|
|
28
|
+
open_tab: { required: [], optional: ["url", "disposition"] },
|
|
29
|
+
close_tab: { required: ["tabId"], optional: [] },
|
|
30
|
+
navigate: { required: ["tabId", "url"], optional: [] },
|
|
31
|
+
read_page: { required: ["tabId"], optional: ["depth", "maxChars"] },
|
|
32
|
+
screenshot: { required: ["tabId"], optional: ["fullPage"] },
|
|
33
|
+
move_mouse: { required: ["tabId", "x", "y"], optional: [] },
|
|
34
|
+
mouse_click: { required: ["tabId"], optional: ["ref", "x", "y", "button", "clickCount"] },
|
|
35
|
+
scroll: { required: ["tabId"], optional: ["ref", "direction", "amount", "x", "y"] },
|
|
36
|
+
wait_for: { required: ["tabId"], optional: ["selector", "text", "url", "loadState", "state", "timeoutMs"] },
|
|
37
|
+
form_input: { required: ["tabId", "ref"], optional: ["value", "values", "checked"] },
|
|
38
|
+
keyboard: { required: ["tabId", "keys"], optional: ["modifiers"] },
|
|
39
|
+
keyboard_type: { required: ["tabId", "text"], optional: ["mode", "delayMs"] },
|
|
40
|
+
run_js: { required: ["tabId", "code"], optional: [] },
|
|
41
|
+
port_forward: { required: ["workerPort"], optional: ["browserPort"] },
|
|
42
|
+
list_port_forwards: { required: [], optional: [] },
|
|
43
|
+
stop_port_forward: { required: ["forwardId"], optional: [] }
|
|
44
|
+
};
|
|
45
|
+
function normalizeBrowserInput(action, input) {
|
|
46
|
+
const fields = browserInputFields[action];
|
|
47
|
+
for (const key of fields.required) {
|
|
48
|
+
if (input[key] === null || input[key] === void 0) throw new Error(`${action} requires ${key}.`);
|
|
49
|
+
}
|
|
50
|
+
const normalized = { ...input };
|
|
51
|
+
for (const key of fields.optional) {
|
|
52
|
+
if (normalized[key] === null) delete normalized[key];
|
|
53
|
+
}
|
|
54
|
+
return normalized;
|
|
55
|
+
}
|
|
56
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
57
|
+
0 && (module.exports = {
|
|
58
|
+
normalizeBrowserInput
|
|
59
|
+
});
|
|
@@ -33,6 +33,8 @@ __export(browser_runtime_exports, {
|
|
|
33
33
|
module.exports = __toCommonJS(browser_runtime_exports);
|
|
34
34
|
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
35
35
|
var import_node_path = __toESM(require("node:path"), 1);
|
|
36
|
+
var import_browser_input = require("./browser-input.cjs");
|
|
37
|
+
var import_password_safe_snapshot = require("./password-safe-snapshot.cjs");
|
|
36
38
|
const MAX_DOWNLOAD_CHUNK_BYTES = 1024 * 1024;
|
|
37
39
|
const MAX_RUN_JS_RESULT_BYTES = 1024 * 1024;
|
|
38
40
|
const MAX_RUN_JS_ARTIFACT_BYTES = 4 * 1024 * 1024;
|
|
@@ -234,6 +236,7 @@ class BrowserRuntime {
|
|
|
234
236
|
await this.downloadSaveQueue;
|
|
235
237
|
}
|
|
236
238
|
async execute(action, input) {
|
|
239
|
+
input = (0, import_browser_input.normalizeBrowserInput)(action, input);
|
|
237
240
|
if (action === "list_tabs") return { tabs: await this.listTabs() };
|
|
238
241
|
if (action === "list_downloads") return { downloads: await this.listDownloads() };
|
|
239
242
|
if (action === "get_download") return await this.getDownloadChunk(input);
|
|
@@ -278,12 +281,12 @@ class BrowserRuntime {
|
|
|
278
281
|
const options = { mode: "ai", boxes: true, timeout: READ_PAGE_TIMEOUT_MS, ...depth === void 0 ? {} : { depth } };
|
|
279
282
|
let snapshot;
|
|
280
283
|
try {
|
|
281
|
-
snapshot = await
|
|
284
|
+
snapshot = await (0, import_password_safe_snapshot.passwordSafeAriaSnapshot)(page, options);
|
|
282
285
|
} catch (error) {
|
|
283
286
|
const message = error instanceof Error ? error.message : String(error);
|
|
284
287
|
if (isDestroyedContext(message)) {
|
|
285
288
|
await page.waitForLoadState("domcontentloaded", { timeout: 5e3 }).catch(() => void 0);
|
|
286
|
-
snapshot = await
|
|
289
|
+
snapshot = await (0, import_password_safe_snapshot.passwordSafeAriaSnapshot)(page, options);
|
|
287
290
|
} else if (/Timeout/i.test(message)) {
|
|
288
291
|
throw new Error(
|
|
289
292
|
`read_page timed out after ${READ_PAGE_TIMEOUT_MS}ms building the outline. The page is very large or busy; retry with a depth limit (e.g. depth: 10).`
|
package/dist/cjs/main.cjs
CHANGED
package/dist/cjs/package.json
CHANGED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var password_safe_snapshot_exports = {};
|
|
20
|
+
__export(password_safe_snapshot_exports, {
|
|
21
|
+
passwordSafeAriaSnapshot: () => passwordSafeAriaSnapshot
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(password_safe_snapshot_exports);
|
|
24
|
+
var import_node_module = require("node:module");
|
|
25
|
+
const import_meta = {};
|
|
26
|
+
const requirePlaywright = (0, import_node_module.createRequire)(typeof __filename === "string" ? __filename : import_meta.url);
|
|
27
|
+
const SUPPORTED_PLAYWRIGHT_VERSION = "1.62.1";
|
|
28
|
+
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
29
|
+
const UNAVAILABLE = "Password-safe browser snapshots are unavailable with this Playwright runtime. Update r5d-browser before retrying.";
|
|
30
|
+
function isServerFrame(value) {
|
|
31
|
+
return !!value && typeof value.selectors?.callOnSelector === "function";
|
|
32
|
+
}
|
|
33
|
+
function captureInUtilityWorld({ elements, injected }, options) {
|
|
34
|
+
const root = elements[0];
|
|
35
|
+
const valueDescriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value");
|
|
36
|
+
const typeDescriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "type");
|
|
37
|
+
const attributeDescriptor = Object.getOwnPropertyDescriptor(Element.prototype, "getAttribute");
|
|
38
|
+
if (!root || typeof injected.ariaSnapshotWithRefs !== "function" || !valueDescriptor?.configurable || typeof valueDescriptor.get !== "function" || typeof typeDescriptor?.get !== "function" || !attributeDescriptor?.configurable || typeof attributeDescriptor.value !== "function") {
|
|
39
|
+
throw new Error("Password-safe snapshot utility APIs are unavailable.");
|
|
40
|
+
}
|
|
41
|
+
const readValue = valueDescriptor.get;
|
|
42
|
+
const readType = typeDescriptor.get;
|
|
43
|
+
const readAttribute = attributeDescriptor.value;
|
|
44
|
+
const isPassword = (element) => element instanceof HTMLInputElement && readType.call(element) === "password";
|
|
45
|
+
try {
|
|
46
|
+
Object.defineProperty(HTMLInputElement.prototype, "value", {
|
|
47
|
+
...valueDescriptor,
|
|
48
|
+
get() {
|
|
49
|
+
const value = readValue.call(this);
|
|
50
|
+
return value && isPassword(this) ? "[redacted]" : value;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
Object.defineProperty(Element.prototype, "getAttribute", {
|
|
54
|
+
...attributeDescriptor,
|
|
55
|
+
value(name) {
|
|
56
|
+
const value = readAttribute.call(this, name);
|
|
57
|
+
return value && String(name).toLowerCase() === "value" && isPassword(this) ? "[redacted]" : value;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
const snapshot = injected.ariaSnapshotWithRefs(root, options);
|
|
61
|
+
if (snapshot && typeof snapshot.then === "function") {
|
|
62
|
+
throw new Error("Password-safe snapshot generation must be synchronous.");
|
|
63
|
+
}
|
|
64
|
+
return snapshot;
|
|
65
|
+
} finally {
|
|
66
|
+
Object.defineProperty(Element.prototype, "getAttribute", attributeDescriptor);
|
|
67
|
+
Object.defineProperty(HTMLInputElement.prototype, "value", valueDescriptor);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function validateSnapshot(value) {
|
|
71
|
+
const snapshot = value;
|
|
72
|
+
if (!snapshot || typeof snapshot.text !== "string" || !Array.isArray(snapshot.iframeRefs) || !snapshot.iframeDepths || typeof snapshot.iframeDepths !== "object" || snapshot.iframeRefs.some((ref) => typeof ref !== "string" || !/^(f\d+)?e\d+$/.test(ref)) || Object.entries(snapshot.iframeDepths).some(([ref, depth]) => !/^(f\d+)?e\d+$/.test(ref) || !Number.isInteger(depth) || depth < 0)) {
|
|
73
|
+
throw new Error(UNAVAILABLE);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function passwordSafeAriaSnapshot(page, options = {}) {
|
|
77
|
+
const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
78
|
+
if (!Number.isFinite(timeout) || timeout <= 0) throw new Error("Password-safe snapshots require a finite positive timeout.");
|
|
79
|
+
let frame;
|
|
80
|
+
try {
|
|
81
|
+
if (requirePlaywright("playwright-core/package.json").version !== SUPPORTED_PLAYWRIGHT_VERSION) throw new Error(UNAVAILABLE);
|
|
82
|
+
const connection = page._connection;
|
|
83
|
+
if (typeof connection?.toImpl !== "function") throw new Error(UNAVAILABLE);
|
|
84
|
+
frame = connection.toImpl(page.mainFrame());
|
|
85
|
+
} catch {
|
|
86
|
+
throw new Error(UNAVAILABLE);
|
|
87
|
+
}
|
|
88
|
+
if (!isServerFrame(frame)) throw new Error(UNAVAILABLE);
|
|
89
|
+
const deadline = Date.now() + timeout;
|
|
90
|
+
const renderOptions = {
|
|
91
|
+
mode: "ai",
|
|
92
|
+
...options.depth === void 0 ? {} : { depth: options.depth },
|
|
93
|
+
boxes: options.boxes
|
|
94
|
+
};
|
|
95
|
+
const timeoutError = () => new Error(`Timeout ${timeout}ms exceeded building a password-safe browser snapshot.`);
|
|
96
|
+
const bounded = async (operation) => {
|
|
97
|
+
let timer;
|
|
98
|
+
try {
|
|
99
|
+
return await Promise.race([
|
|
100
|
+
operation,
|
|
101
|
+
new Promise((_, reject) => {
|
|
102
|
+
timer = setTimeout(() => reject(timeoutError()), Math.max(0, deadline - Date.now()));
|
|
103
|
+
})
|
|
104
|
+
]);
|
|
105
|
+
} finally {
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const captureFrame = async (currentFrame, selector, frameOptions, isMainFrame = false) => {
|
|
110
|
+
if (!isServerFrame(currentFrame)) throw new Error(UNAVAILABLE);
|
|
111
|
+
if (Date.now() >= deadline) throw timeoutError();
|
|
112
|
+
const captured = await bounded(currentFrame.selectors.callOnSelector(selector, { strict: false }, captureInUtilityWorld, frameOptions));
|
|
113
|
+
if (!captured) {
|
|
114
|
+
if (!isMainFrame) return [];
|
|
115
|
+
await bounded(new Promise((resolve) => setTimeout(resolve, 50)));
|
|
116
|
+
return captureFrame(currentFrame, selector, frameOptions, true);
|
|
117
|
+
}
|
|
118
|
+
if (captured.info?.world !== "utility" || !isServerFrame(captured.frame)) throw new Error(UNAVAILABLE);
|
|
119
|
+
validateSnapshot(captured.result);
|
|
120
|
+
const snapshot = captured.result;
|
|
121
|
+
const renderedRefs = snapshot.iframeRefs.filter((ref) => Object.hasOwn(snapshot.iframeDepths, ref));
|
|
122
|
+
const children = /* @__PURE__ */ new Map();
|
|
123
|
+
await Promise.all(
|
|
124
|
+
renderedRefs.map(async (ref) => {
|
|
125
|
+
const childDepth = frameOptions.depth === void 0 ? void 0 : frameOptions.depth - snapshot.iframeDepths[ref] - 1;
|
|
126
|
+
if (childDepth !== void 0 && childDepth <= 0) return;
|
|
127
|
+
children.set(
|
|
128
|
+
ref,
|
|
129
|
+
await captureFrame(captured.frame, `aria-ref=${ref} >> internal:control=enter-frame >> body,frameset`, {
|
|
130
|
+
...frameOptions,
|
|
131
|
+
...childDepth === void 0 ? {} : { depth: childDepth }
|
|
132
|
+
})
|
|
133
|
+
);
|
|
134
|
+
})
|
|
135
|
+
);
|
|
136
|
+
const lines = [];
|
|
137
|
+
for (const line of snapshot.text.split("\n")) {
|
|
138
|
+
const match = line.match(/^(\s*)- iframe (?:\[active\] )?\[ref=([^\]]*)\]/);
|
|
139
|
+
const child = match ? children.get(match[2]) : void 0;
|
|
140
|
+
lines.push(child?.length ? `${line}:` : line);
|
|
141
|
+
if (match && child) lines.push(...child.map((childLine) => `${match[1]} ${childLine}`));
|
|
142
|
+
}
|
|
143
|
+
return lines;
|
|
144
|
+
};
|
|
145
|
+
return (await captureFrame(frame, "body,frameset", renderOptions, true)).join("\n");
|
|
146
|
+
}
|
|
147
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
148
|
+
0 && (module.exports = {
|
|
149
|
+
passwordSafeAriaSnapshot
|
|
150
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const browserInputFields = {
|
|
2
|
+
list_tabs: { required: [], optional: [] },
|
|
3
|
+
list_downloads: { required: [], optional: [] },
|
|
4
|
+
get_download: { required: ["downloadId"], optional: ["offset", "maxBytes"] },
|
|
5
|
+
open_tab: { required: [], optional: ["url", "disposition"] },
|
|
6
|
+
close_tab: { required: ["tabId"], optional: [] },
|
|
7
|
+
navigate: { required: ["tabId", "url"], optional: [] },
|
|
8
|
+
read_page: { required: ["tabId"], optional: ["depth", "maxChars"] },
|
|
9
|
+
screenshot: { required: ["tabId"], optional: ["fullPage"] },
|
|
10
|
+
move_mouse: { required: ["tabId", "x", "y"], optional: [] },
|
|
11
|
+
mouse_click: { required: ["tabId"], optional: ["ref", "x", "y", "button", "clickCount"] },
|
|
12
|
+
scroll: { required: ["tabId"], optional: ["ref", "direction", "amount", "x", "y"] },
|
|
13
|
+
wait_for: { required: ["tabId"], optional: ["selector", "text", "url", "loadState", "state", "timeoutMs"] },
|
|
14
|
+
form_input: { required: ["tabId", "ref"], optional: ["value", "values", "checked"] },
|
|
15
|
+
keyboard: { required: ["tabId", "keys"], optional: ["modifiers"] },
|
|
16
|
+
keyboard_type: { required: ["tabId", "text"], optional: ["mode", "delayMs"] },
|
|
17
|
+
run_js: { required: ["tabId", "code"], optional: [] },
|
|
18
|
+
port_forward: { required: ["workerPort"], optional: ["browserPort"] },
|
|
19
|
+
list_port_forwards: { required: [], optional: [] },
|
|
20
|
+
stop_port_forward: { required: ["forwardId"], optional: [] }
|
|
21
|
+
};
|
|
22
|
+
function normalizeBrowserInput(action, input) {
|
|
23
|
+
const fields = browserInputFields[action];
|
|
24
|
+
for (const key of fields.required) {
|
|
25
|
+
if (input[key] === null || input[key] === void 0) throw new Error(`${action} requires ${key}.`);
|
|
26
|
+
}
|
|
27
|
+
const normalized = { ...input };
|
|
28
|
+
for (const key of fields.optional) {
|
|
29
|
+
if (normalized[key] === null) delete normalized[key];
|
|
30
|
+
}
|
|
31
|
+
return normalized;
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
normalizeBrowserInput
|
|
35
|
+
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { normalizeBrowserInput } from "./browser-input.mjs";
|
|
4
|
+
import { passwordSafeAriaSnapshot } from "./password-safe-snapshot.mjs";
|
|
3
5
|
const MAX_DOWNLOAD_CHUNK_BYTES = 1024 * 1024;
|
|
4
6
|
const MAX_RUN_JS_RESULT_BYTES = 1024 * 1024;
|
|
5
7
|
const MAX_RUN_JS_ARTIFACT_BYTES = 4 * 1024 * 1024;
|
|
@@ -201,6 +203,7 @@ class BrowserRuntime {
|
|
|
201
203
|
await this.downloadSaveQueue;
|
|
202
204
|
}
|
|
203
205
|
async execute(action, input) {
|
|
206
|
+
input = normalizeBrowserInput(action, input);
|
|
204
207
|
if (action === "list_tabs") return { tabs: await this.listTabs() };
|
|
205
208
|
if (action === "list_downloads") return { downloads: await this.listDownloads() };
|
|
206
209
|
if (action === "get_download") return await this.getDownloadChunk(input);
|
|
@@ -245,12 +248,12 @@ class BrowserRuntime {
|
|
|
245
248
|
const options = { mode: "ai", boxes: true, timeout: READ_PAGE_TIMEOUT_MS, ...depth === void 0 ? {} : { depth } };
|
|
246
249
|
let snapshot;
|
|
247
250
|
try {
|
|
248
|
-
snapshot = await page
|
|
251
|
+
snapshot = await passwordSafeAriaSnapshot(page, options);
|
|
249
252
|
} catch (error) {
|
|
250
253
|
const message = error instanceof Error ? error.message : String(error);
|
|
251
254
|
if (isDestroyedContext(message)) {
|
|
252
255
|
await page.waitForLoadState("domcontentloaded", { timeout: 5e3 }).catch(() => void 0);
|
|
253
|
-
snapshot = await page
|
|
256
|
+
snapshot = await passwordSafeAriaSnapshot(page, options);
|
|
254
257
|
} else if (/Timeout/i.test(message)) {
|
|
255
258
|
throw new Error(
|
|
256
259
|
`read_page timed out after ${READ_PAGE_TIMEOUT_MS}ms building the outline. The page is very large or busy; retry with a depth limit (e.g. depth: 10).`
|
package/dist/mjs/main.mjs
CHANGED
package/dist/mjs/package.json
CHANGED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
const requirePlaywright = createRequire(typeof __filename === "string" ? __filename : import.meta.url);
|
|
3
|
+
const SUPPORTED_PLAYWRIGHT_VERSION = "1.62.1";
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
5
|
+
const UNAVAILABLE = "Password-safe browser snapshots are unavailable with this Playwright runtime. Update r5d-browser before retrying.";
|
|
6
|
+
function isServerFrame(value) {
|
|
7
|
+
return !!value && typeof value.selectors?.callOnSelector === "function";
|
|
8
|
+
}
|
|
9
|
+
function captureInUtilityWorld({ elements, injected }, options) {
|
|
10
|
+
const root = elements[0];
|
|
11
|
+
const valueDescriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value");
|
|
12
|
+
const typeDescriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "type");
|
|
13
|
+
const attributeDescriptor = Object.getOwnPropertyDescriptor(Element.prototype, "getAttribute");
|
|
14
|
+
if (!root || typeof injected.ariaSnapshotWithRefs !== "function" || !valueDescriptor?.configurable || typeof valueDescriptor.get !== "function" || typeof typeDescriptor?.get !== "function" || !attributeDescriptor?.configurable || typeof attributeDescriptor.value !== "function") {
|
|
15
|
+
throw new Error("Password-safe snapshot utility APIs are unavailable.");
|
|
16
|
+
}
|
|
17
|
+
const readValue = valueDescriptor.get;
|
|
18
|
+
const readType = typeDescriptor.get;
|
|
19
|
+
const readAttribute = attributeDescriptor.value;
|
|
20
|
+
const isPassword = (element) => element instanceof HTMLInputElement && readType.call(element) === "password";
|
|
21
|
+
try {
|
|
22
|
+
Object.defineProperty(HTMLInputElement.prototype, "value", {
|
|
23
|
+
...valueDescriptor,
|
|
24
|
+
get() {
|
|
25
|
+
const value = readValue.call(this);
|
|
26
|
+
return value && isPassword(this) ? "[redacted]" : value;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
Object.defineProperty(Element.prototype, "getAttribute", {
|
|
30
|
+
...attributeDescriptor,
|
|
31
|
+
value(name) {
|
|
32
|
+
const value = readAttribute.call(this, name);
|
|
33
|
+
return value && String(name).toLowerCase() === "value" && isPassword(this) ? "[redacted]" : value;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
const snapshot = injected.ariaSnapshotWithRefs(root, options);
|
|
37
|
+
if (snapshot && typeof snapshot.then === "function") {
|
|
38
|
+
throw new Error("Password-safe snapshot generation must be synchronous.");
|
|
39
|
+
}
|
|
40
|
+
return snapshot;
|
|
41
|
+
} finally {
|
|
42
|
+
Object.defineProperty(Element.prototype, "getAttribute", attributeDescriptor);
|
|
43
|
+
Object.defineProperty(HTMLInputElement.prototype, "value", valueDescriptor);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function validateSnapshot(value) {
|
|
47
|
+
const snapshot = value;
|
|
48
|
+
if (!snapshot || typeof snapshot.text !== "string" || !Array.isArray(snapshot.iframeRefs) || !snapshot.iframeDepths || typeof snapshot.iframeDepths !== "object" || snapshot.iframeRefs.some((ref) => typeof ref !== "string" || !/^(f\d+)?e\d+$/.test(ref)) || Object.entries(snapshot.iframeDepths).some(([ref, depth]) => !/^(f\d+)?e\d+$/.test(ref) || !Number.isInteger(depth) || depth < 0)) {
|
|
49
|
+
throw new Error(UNAVAILABLE);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function passwordSafeAriaSnapshot(page, options = {}) {
|
|
53
|
+
const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
54
|
+
if (!Number.isFinite(timeout) || timeout <= 0) throw new Error("Password-safe snapshots require a finite positive timeout.");
|
|
55
|
+
let frame;
|
|
56
|
+
try {
|
|
57
|
+
if (requirePlaywright("playwright-core/package.json").version !== SUPPORTED_PLAYWRIGHT_VERSION) throw new Error(UNAVAILABLE);
|
|
58
|
+
const connection = page._connection;
|
|
59
|
+
if (typeof connection?.toImpl !== "function") throw new Error(UNAVAILABLE);
|
|
60
|
+
frame = connection.toImpl(page.mainFrame());
|
|
61
|
+
} catch {
|
|
62
|
+
throw new Error(UNAVAILABLE);
|
|
63
|
+
}
|
|
64
|
+
if (!isServerFrame(frame)) throw new Error(UNAVAILABLE);
|
|
65
|
+
const deadline = Date.now() + timeout;
|
|
66
|
+
const renderOptions = {
|
|
67
|
+
mode: "ai",
|
|
68
|
+
...options.depth === void 0 ? {} : { depth: options.depth },
|
|
69
|
+
boxes: options.boxes
|
|
70
|
+
};
|
|
71
|
+
const timeoutError = () => new Error(`Timeout ${timeout}ms exceeded building a password-safe browser snapshot.`);
|
|
72
|
+
const bounded = async (operation) => {
|
|
73
|
+
let timer;
|
|
74
|
+
try {
|
|
75
|
+
return await Promise.race([
|
|
76
|
+
operation,
|
|
77
|
+
new Promise((_, reject) => {
|
|
78
|
+
timer = setTimeout(() => reject(timeoutError()), Math.max(0, deadline - Date.now()));
|
|
79
|
+
})
|
|
80
|
+
]);
|
|
81
|
+
} finally {
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const captureFrame = async (currentFrame, selector, frameOptions, isMainFrame = false) => {
|
|
86
|
+
if (!isServerFrame(currentFrame)) throw new Error(UNAVAILABLE);
|
|
87
|
+
if (Date.now() >= deadline) throw timeoutError();
|
|
88
|
+
const captured = await bounded(currentFrame.selectors.callOnSelector(selector, { strict: false }, captureInUtilityWorld, frameOptions));
|
|
89
|
+
if (!captured) {
|
|
90
|
+
if (!isMainFrame) return [];
|
|
91
|
+
await bounded(new Promise((resolve) => setTimeout(resolve, 50)));
|
|
92
|
+
return captureFrame(currentFrame, selector, frameOptions, true);
|
|
93
|
+
}
|
|
94
|
+
if (captured.info?.world !== "utility" || !isServerFrame(captured.frame)) throw new Error(UNAVAILABLE);
|
|
95
|
+
validateSnapshot(captured.result);
|
|
96
|
+
const snapshot = captured.result;
|
|
97
|
+
const renderedRefs = snapshot.iframeRefs.filter((ref) => Object.hasOwn(snapshot.iframeDepths, ref));
|
|
98
|
+
const children = /* @__PURE__ */ new Map();
|
|
99
|
+
await Promise.all(
|
|
100
|
+
renderedRefs.map(async (ref) => {
|
|
101
|
+
const childDepth = frameOptions.depth === void 0 ? void 0 : frameOptions.depth - snapshot.iframeDepths[ref] - 1;
|
|
102
|
+
if (childDepth !== void 0 && childDepth <= 0) return;
|
|
103
|
+
children.set(
|
|
104
|
+
ref,
|
|
105
|
+
await captureFrame(captured.frame, `aria-ref=${ref} >> internal:control=enter-frame >> body,frameset`, {
|
|
106
|
+
...frameOptions,
|
|
107
|
+
...childDepth === void 0 ? {} : { depth: childDepth }
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
const lines = [];
|
|
113
|
+
for (const line of snapshot.text.split("\n")) {
|
|
114
|
+
const match = line.match(/^(\s*)- iframe (?:\[active\] )?\[ref=([^\]]*)\]/);
|
|
115
|
+
const child = match ? children.get(match[2]) : void 0;
|
|
116
|
+
lines.push(child?.length ? `${line}:` : line);
|
|
117
|
+
if (match && child) lines.push(...child.map((childLine) => `${match[1]} ${childLine}`));
|
|
118
|
+
}
|
|
119
|
+
return lines;
|
|
120
|
+
};
|
|
121
|
+
return (await captureFrame(frame, "body,frameset", renderOptions, true)).join("\n");
|
|
122
|
+
}
|
|
123
|
+
export {
|
|
124
|
+
passwordSafeAriaSnapshot
|
|
125
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
declare const browserInputFields: {
|
|
2
|
+
list_tabs: {
|
|
3
|
+
required: never[];
|
|
4
|
+
optional: never[];
|
|
5
|
+
};
|
|
6
|
+
list_downloads: {
|
|
7
|
+
required: never[];
|
|
8
|
+
optional: never[];
|
|
9
|
+
};
|
|
10
|
+
get_download: {
|
|
11
|
+
required: string[];
|
|
12
|
+
optional: string[];
|
|
13
|
+
};
|
|
14
|
+
open_tab: {
|
|
15
|
+
required: never[];
|
|
16
|
+
optional: string[];
|
|
17
|
+
};
|
|
18
|
+
close_tab: {
|
|
19
|
+
required: string[];
|
|
20
|
+
optional: never[];
|
|
21
|
+
};
|
|
22
|
+
navigate: {
|
|
23
|
+
required: string[];
|
|
24
|
+
optional: never[];
|
|
25
|
+
};
|
|
26
|
+
read_page: {
|
|
27
|
+
required: string[];
|
|
28
|
+
optional: string[];
|
|
29
|
+
};
|
|
30
|
+
screenshot: {
|
|
31
|
+
required: string[];
|
|
32
|
+
optional: string[];
|
|
33
|
+
};
|
|
34
|
+
move_mouse: {
|
|
35
|
+
required: string[];
|
|
36
|
+
optional: never[];
|
|
37
|
+
};
|
|
38
|
+
mouse_click: {
|
|
39
|
+
required: string[];
|
|
40
|
+
optional: string[];
|
|
41
|
+
};
|
|
42
|
+
scroll: {
|
|
43
|
+
required: string[];
|
|
44
|
+
optional: string[];
|
|
45
|
+
};
|
|
46
|
+
wait_for: {
|
|
47
|
+
required: string[];
|
|
48
|
+
optional: string[];
|
|
49
|
+
};
|
|
50
|
+
form_input: {
|
|
51
|
+
required: string[];
|
|
52
|
+
optional: string[];
|
|
53
|
+
};
|
|
54
|
+
keyboard: {
|
|
55
|
+
required: string[];
|
|
56
|
+
optional: string[];
|
|
57
|
+
};
|
|
58
|
+
keyboard_type: {
|
|
59
|
+
required: string[];
|
|
60
|
+
optional: string[];
|
|
61
|
+
};
|
|
62
|
+
run_js: {
|
|
63
|
+
required: string[];
|
|
64
|
+
optional: never[];
|
|
65
|
+
};
|
|
66
|
+
port_forward: {
|
|
67
|
+
required: string[];
|
|
68
|
+
optional: string[];
|
|
69
|
+
};
|
|
70
|
+
list_port_forwards: {
|
|
71
|
+
required: never[];
|
|
72
|
+
optional: never[];
|
|
73
|
+
};
|
|
74
|
+
stop_port_forward: {
|
|
75
|
+
required: string[];
|
|
76
|
+
optional: never[];
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
export type BrowserInputAction = keyof typeof browserInputFields;
|
|
80
|
+
export declare function normalizeBrowserInput(action: BrowserInputAction, input: Readonly<Record<string, unknown>>): Record<string, unknown>;
|
|
81
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Page } from "playwright-core";
|
|
2
|
+
export type PasswordSafeSnapshotOptions = {
|
|
3
|
+
mode?: "ai";
|
|
4
|
+
depth?: number;
|
|
5
|
+
boxes?: boolean;
|
|
6
|
+
timeout?: number;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Playwright 1.62.1 has no public snapshot redaction hook. This deliberately narrow
|
|
10
|
+
* adapter uses its in-process server Frame and the same utility-world selector,
|
|
11
|
+
* ref generation, and iframe stitching as ariaSnapshotForFrame in coreBundle.js.
|
|
12
|
+
* The exact version and private API shapes are checked; failure never falls back
|
|
13
|
+
* to page.ariaSnapshot(), which exposes password values in this pinned version.
|
|
14
|
+
*/
|
|
15
|
+
export declare function passwordSafeAriaSnapshot(page: Page, options?: PasswordSafeSnapshotOptions): Promise<string>;
|