machine-bridge-mcp 0.14.0 → 0.15.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/CHANGELOG.md +19 -0
- package/README.md +6 -6
- package/SECURITY.md +3 -3
- package/browser-extension/browser-operations.js +515 -0
- package/browser-extension/devtools-input.js +17 -2
- package/browser-extension/manifest.json +2 -2
- package/browser-extension/page-automation.js +245 -60
- package/browser-extension/pairing.js +1 -1
- package/browser-extension/service-worker.js +151 -354
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/AUDIT.md +13 -1
- package/docs/LOCAL_AUTOMATION.md +10 -10
- package/docs/OPERATIONS.md +5 -4
- package/docs/TESTING.md +2 -2
- package/package.json +5 -3
- package/src/local/browser-bridge.mjs +64 -8
- package/src/local/cli.mjs +16 -2
- package/src/local/service.mjs +7 -3
- package/src/shared/tool-catalog.json +7 -7
- package/src/worker/index.ts +1 -1
|
@@ -1,30 +1,94 @@
|
|
|
1
1
|
(() => {
|
|
2
2
|
if (globalThis.__machineBridgePageAutomation) return;
|
|
3
3
|
|
|
4
|
-
const INTERACTIVE_SELECTOR = "a,button,input,select,textarea,[role],[contenteditable='
|
|
4
|
+
const INTERACTIVE_SELECTOR = "a,button,input,select,textarea,[role],[contenteditable]:not([contenteditable='false']),summary";
|
|
5
5
|
const MAX_SHADOW_ROOTS = 200;
|
|
6
|
+
const MAX_SCAN_NODES = 100000;
|
|
7
|
+
const MAX_QUERY_MATCHES = 10001;
|
|
8
|
+
const MAX_PAGE_FIELD_CHARS = 2000;
|
|
9
|
+
const MAX_PAGE_URL_CHARS = 8192;
|
|
10
|
+
const MAX_WAIT_TEXT_CHARS = 2 * 1024 * 1024;
|
|
11
|
+
const MAX_ELEMENT_REFS = 10000;
|
|
6
12
|
const elementRefs = new WeakMap();
|
|
7
13
|
const refElements = new Map();
|
|
8
14
|
let nextRef = 1;
|
|
15
|
+
let evictedRefs = 0;
|
|
9
16
|
|
|
10
|
-
function
|
|
17
|
+
function scanPageElements(maxNodes = MAX_SCAN_NODES) {
|
|
11
18
|
const roots = [document];
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
const seenRoots = new Set(roots);
|
|
20
|
+
const entries = [];
|
|
21
|
+
let visitedNodes = 0;
|
|
22
|
+
let truncated = false;
|
|
23
|
+
for (let rootIndex = 0; rootIndex < roots.length; rootIndex += 1) {
|
|
24
|
+
const root = roots[rootIndex];
|
|
25
|
+
const elements = [];
|
|
26
|
+
const source = iterateElements(root);
|
|
27
|
+
for (const element of source) {
|
|
28
|
+
if (visitedNodes >= maxNodes) {
|
|
29
|
+
truncated = true;
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
visitedNodes += 1;
|
|
33
|
+
elements.push(element);
|
|
34
|
+
if (element.shadowRoot && !seenRoots.has(element.shadowRoot)) {
|
|
35
|
+
if (roots.length >= MAX_SHADOW_ROOTS + 1) truncated = true;
|
|
36
|
+
else {
|
|
37
|
+
seenRoots.add(element.shadowRoot);
|
|
38
|
+
roots.push(element.shadowRoot);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
17
41
|
}
|
|
42
|
+
entries.push({ root, elements });
|
|
43
|
+
if (visitedNodes >= maxNodes) break;
|
|
18
44
|
}
|
|
19
|
-
return roots;
|
|
45
|
+
return { roots, entries, visitedNodes, truncated };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function iterateElements(root) {
|
|
49
|
+
if (typeof document.createTreeWalker === "function") {
|
|
50
|
+
const walker = document.createTreeWalker(root, globalThis.NodeFilter?.SHOW_ELEMENT || 1);
|
|
51
|
+
return {
|
|
52
|
+
[Symbol.iterator]() {
|
|
53
|
+
return {
|
|
54
|
+
next() {
|
|
55
|
+
const value = walker.nextNode();
|
|
56
|
+
return value ? { value, done: false } : { value: undefined, done: true };
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return root.querySelectorAll("*");
|
|
20
63
|
}
|
|
21
64
|
|
|
22
|
-
function deepQuerySelectorAll(selector) {
|
|
65
|
+
function deepQuerySelectorAll(selector, limit = MAX_QUERY_MATCHES) {
|
|
66
|
+
if (!selector || limit < 1) return [];
|
|
67
|
+
const scan = scanPageElements();
|
|
23
68
|
const results = [];
|
|
24
|
-
|
|
69
|
+
try {
|
|
70
|
+
if (typeof document.documentElement?.matches === "function") document.documentElement.matches(selector);
|
|
71
|
+
for (const entry of scan.entries) {
|
|
72
|
+
for (const element of entry.elements) {
|
|
73
|
+
if (element.matches?.(selector)) {
|
|
74
|
+
results.push(element);
|
|
75
|
+
if (results.length >= limit) return results;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw new Error(`invalid CSS selector: ${String(error?.message || error).slice(0, 300)}`);
|
|
81
|
+
}
|
|
25
82
|
return results;
|
|
26
83
|
}
|
|
27
84
|
|
|
85
|
+
function isInteractiveElement(element) {
|
|
86
|
+
const tag = String(element.tagName || "").toLowerCase();
|
|
87
|
+
return ["a", "button", "input", "select", "textarea", "summary"].includes(tag)
|
|
88
|
+
|| element.hasAttribute?.("role")
|
|
89
|
+
|| (element.hasAttribute?.("contenteditable") && String(element.getAttribute?.("contenteditable") || "").toLowerCase() !== "false");
|
|
90
|
+
}
|
|
91
|
+
|
|
28
92
|
function rootById(element, id) {
|
|
29
93
|
const root = element.getRootNode?.();
|
|
30
94
|
if (root && typeof root.getElementById === "function") return root.getElementById(id);
|
|
@@ -33,16 +97,25 @@
|
|
|
33
97
|
|
|
34
98
|
function refFor(element) {
|
|
35
99
|
let ref = elementRefs.get(element);
|
|
36
|
-
if (ref) {
|
|
37
|
-
|
|
38
|
-
|
|
100
|
+
if (!ref) {
|
|
101
|
+
ref = `e${nextRef++}`;
|
|
102
|
+
elementRefs.set(element, ref);
|
|
39
103
|
}
|
|
40
|
-
ref
|
|
41
|
-
elementRefs.set(element, ref);
|
|
42
|
-
refElements.set(ref, element);
|
|
104
|
+
rememberRef(ref, element);
|
|
43
105
|
return ref;
|
|
44
106
|
}
|
|
45
107
|
|
|
108
|
+
function rememberRef(ref, element) {
|
|
109
|
+
if (refElements.has(ref)) refElements.delete(ref);
|
|
110
|
+
while (refElements.size >= MAX_ELEMENT_REFS) {
|
|
111
|
+
const oldest = refElements.keys().next().value;
|
|
112
|
+
if (!oldest) break;
|
|
113
|
+
refElements.delete(oldest);
|
|
114
|
+
evictedRefs += 1;
|
|
115
|
+
}
|
|
116
|
+
refElements.set(ref, element);
|
|
117
|
+
}
|
|
118
|
+
|
|
46
119
|
function pruneDetachedRefs() {
|
|
47
120
|
for (const [ref, element] of refElements) {
|
|
48
121
|
if (!element?.isConnected) refElements.delete(ref);
|
|
@@ -51,22 +124,39 @@
|
|
|
51
124
|
|
|
52
125
|
function inspect(params) {
|
|
53
126
|
pruneDetachedRefs();
|
|
54
|
-
const maxElements = Number(params.maxElements) ||
|
|
127
|
+
const maxElements = Math.max(0, Number(params.maxElements) || 0);
|
|
55
128
|
const includeValues = params.includeValues === true;
|
|
56
|
-
const
|
|
57
|
-
const candidates =
|
|
129
|
+
const scan = scanPageElements();
|
|
130
|
+
const candidates = [];
|
|
131
|
+
let interactiveCount = 0;
|
|
132
|
+
let formCount = 0;
|
|
133
|
+
for (const entry of scan.entries) {
|
|
134
|
+
for (const element of entry.elements) {
|
|
135
|
+
if (String(element.tagName || "").toLowerCase() === "form") formCount += 1;
|
|
136
|
+
if (!isInteractiveElement(element)) continue;
|
|
137
|
+
interactiveCount += 1;
|
|
138
|
+
if (candidates.length < maxElements) candidates.push(element);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const describedElements = candidates.map((element, index) => describeElement(element, index, includeValues));
|
|
58
142
|
return {
|
|
59
|
-
snapshot_version:
|
|
143
|
+
snapshot_version: 2,
|
|
60
144
|
document: {
|
|
61
|
-
title: document.title,
|
|
62
|
-
url: location.href,
|
|
63
|
-
language: document.documentElement.lang
|
|
145
|
+
title: boundedPageText(document.title, 1000),
|
|
146
|
+
url: safePageUrl(location.href),
|
|
147
|
+
language: boundedPageText(document.documentElement.lang, 100),
|
|
64
148
|
ready_state: document.readyState,
|
|
65
|
-
forms:
|
|
66
|
-
open_shadow_roots: Math.max(0,
|
|
149
|
+
forms: formCount,
|
|
150
|
+
open_shadow_roots: Math.max(0, scan.roots.length - 1),
|
|
151
|
+
scanned_nodes: scan.visitedNodes,
|
|
152
|
+
scan_limit: MAX_SCAN_NODES,
|
|
153
|
+
scan_truncated: scan.truncated,
|
|
154
|
+
tracked_refs: refElements.size,
|
|
155
|
+
ref_limit: MAX_ELEMENT_REFS,
|
|
156
|
+
refs_evicted: evictedRefs,
|
|
67
157
|
},
|
|
68
|
-
elements:
|
|
69
|
-
truncated:
|
|
158
|
+
elements: describedElements,
|
|
159
|
+
truncated: scan.truncated || interactiveCount > describedElements.length,
|
|
70
160
|
};
|
|
71
161
|
}
|
|
72
162
|
|
|
@@ -101,19 +191,30 @@
|
|
|
101
191
|
const timeoutMs = Number(params.elementTimeoutMs) || 10000;
|
|
102
192
|
for (let index = 0; index < params.fields.length; index += 1) {
|
|
103
193
|
const field = params.fields[index];
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
194
|
+
try {
|
|
195
|
+
const element = await waitForActionable(field.selector, field.action, timeoutMs);
|
|
196
|
+
await applyOne(element, field.action, field.value, "");
|
|
197
|
+
results.push({ index, action: field.action, sensitive: field.sensitive === true, element: describeElement(element, index, false) });
|
|
198
|
+
} catch (error) {
|
|
199
|
+
const prefix = index > 0
|
|
200
|
+
? `form field ${index} (${field.action}) failed after ${index} earlier field(s) may have changed`
|
|
201
|
+
: `form field 0 (${field.action}) failed before any earlier field changed`;
|
|
202
|
+
throw new Error(`${prefix}: ${boundedPageText(error?.message || error, 500)}`);
|
|
203
|
+
}
|
|
107
204
|
}
|
|
108
205
|
if (params.submit) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
206
|
+
try {
|
|
207
|
+
const submitter = params.submitSelector ? await waitForActionable(params.submitSelector, "click", timeoutMs) : deepQuerySelectorAll("button[type='submit'],input[type='submit']", 1)[0];
|
|
208
|
+
if (submitter) submitter.click();
|
|
209
|
+
else {
|
|
210
|
+
const field = params.fields.length ? findOne(params.fields[0].selector) : null;
|
|
211
|
+
const form = field?.form || field?.closest?.("form") || deepQuerySelectorAll("form", 1)[0];
|
|
212
|
+
if (!form) throw new Error("no form or submit control found");
|
|
213
|
+
if (typeof form.requestSubmit === "function") form.requestSubmit();
|
|
214
|
+
else form.submit();
|
|
215
|
+
}
|
|
216
|
+
} catch (error) {
|
|
217
|
+
throw new Error(`form submission failed after ${results.length} field(s) changed: ${boundedPageText(error?.message || error, 500)}`);
|
|
117
218
|
}
|
|
118
219
|
}
|
|
119
220
|
return { ok: true, fields: results, submitted: params.submit === true, values_exposed: false };
|
|
@@ -148,8 +249,8 @@
|
|
|
148
249
|
|| (params.state === "editable" && editable.length > 0)
|
|
149
250
|
|| (params.state === "checked" && elements.some((element) => "checked" in element && element.checked === true))
|
|
150
251
|
|| (params.state === "unchecked" && elements.some((element) => "checked" in element && element.checked === false));
|
|
151
|
-
const
|
|
152
|
-
const textMatched =
|
|
252
|
+
const textSearch = params.text ? pageContainsText(params.text) : { found: true, truncated: false, scanned_chars: 0 };
|
|
253
|
+
const textMatched = textSearch.found;
|
|
153
254
|
const loadMatched = !params.loadState
|
|
154
255
|
|| (params.loadState === "domcontentloaded" && ["interactive", "complete"].includes(document.readyState))
|
|
155
256
|
|| (params.loadState === "complete" && document.readyState === "complete");
|
|
@@ -159,13 +260,15 @@
|
|
|
159
260
|
visible_count: visible.length,
|
|
160
261
|
state: params.state || "",
|
|
161
262
|
text_found: textMatched,
|
|
263
|
+
text_scan_truncated: textSearch.truncated,
|
|
264
|
+
text_scanned_chars: textSearch.scanned_chars,
|
|
162
265
|
ready_state: document.readyState,
|
|
163
266
|
};
|
|
164
267
|
}
|
|
165
268
|
|
|
166
269
|
function describeElement(element, index, includeValues) {
|
|
167
|
-
const tag = element.tagName.toLowerCase();
|
|
168
|
-
const type =
|
|
270
|
+
const tag = String(element.tagName || "").toLowerCase();
|
|
271
|
+
const type = boundedPageText(element.getAttribute("type"), 100).toLowerCase();
|
|
169
272
|
const sensitive = isSensitiveElement(element, tag, type);
|
|
170
273
|
const visible = isVisible(element);
|
|
171
274
|
const item = {
|
|
@@ -173,33 +276,36 @@
|
|
|
173
276
|
index,
|
|
174
277
|
tag,
|
|
175
278
|
type,
|
|
176
|
-
role: element.getAttribute("role") || implicitRole(element),
|
|
279
|
+
role: boundedPageText(element.getAttribute("role") || implicitRole(element), 200),
|
|
177
280
|
name: accessibleName(element),
|
|
178
|
-
id: element.id
|
|
179
|
-
field_name: element.getAttribute("name")
|
|
281
|
+
id: boundedPageText(element.id, MAX_PAGE_FIELD_CHARS),
|
|
282
|
+
field_name: boundedPageText(element.getAttribute("name"), MAX_PAGE_FIELD_CHARS),
|
|
180
283
|
label: labelText(element),
|
|
181
|
-
placeholder: element.getAttribute("placeholder")
|
|
284
|
+
placeholder: boundedPageText(element.getAttribute("placeholder"), MAX_PAGE_FIELD_CHARS),
|
|
182
285
|
visible,
|
|
183
286
|
enabled: isEnabled(element),
|
|
184
287
|
editable: isEditable(element),
|
|
185
288
|
disabled: Boolean(element.disabled),
|
|
186
289
|
checked: "checked" in element ? Boolean(element.checked) : null,
|
|
187
|
-
selected: tag === "select" ? element.value : null,
|
|
188
|
-
href: tag === "a" ? element.href : "",
|
|
290
|
+
selected: tag === "select" ? boundedPageText(element.value, MAX_PAGE_FIELD_CHARS) : null,
|
|
291
|
+
href: tag === "a" ? safePageUrl(element.href) : "",
|
|
189
292
|
sensitive,
|
|
190
293
|
in_shadow_dom: element.getRootNode?.() instanceof ShadowRoot,
|
|
191
294
|
bounding_box: visible ? boundingBox(element) : null,
|
|
192
295
|
};
|
|
193
|
-
if (includeValues && !sensitive && "value" in element) item.value =
|
|
296
|
+
if (includeValues && !sensitive && "value" in element) item.value = boundedPageText(element.value, MAX_PAGE_FIELD_CHARS);
|
|
194
297
|
return item;
|
|
195
298
|
}
|
|
196
299
|
|
|
197
300
|
function isSensitiveElement(element, tag, type) {
|
|
198
|
-
if (tag !== "input" && tag !== "textarea") return false;
|
|
301
|
+
if (tag !== "input" && tag !== "textarea" && !element.isContentEditable) return false;
|
|
199
302
|
if (["password", "hidden"].includes(type)) return true;
|
|
200
303
|
const autocomplete = String(element.getAttribute("autocomplete") || "").toLowerCase();
|
|
201
304
|
if (/(?:password|one-time-code|cc-number|cc-csc|cc-exp)/.test(autocomplete)) return true;
|
|
202
|
-
const identity =
|
|
305
|
+
const identity = [element.id, element.getAttribute("name"), element.getAttribute("aria-label"), element.getAttribute("placeholder")]
|
|
306
|
+
.map((value) => boundedPageText(value, 500))
|
|
307
|
+
.join(" ")
|
|
308
|
+
.toLowerCase();
|
|
203
309
|
return /(?:password|passwd|secret|token|api[-_ ]?key|otp|one[-_ ]?time|verification|cvc|cvv|security[-_ ]?code|card[-_ ]?number)/.test(identity);
|
|
204
310
|
}
|
|
205
311
|
|
|
@@ -312,13 +418,14 @@
|
|
|
312
418
|
if (operation === "check" || operation === "uncheck") {
|
|
313
419
|
const wanted = operation === "check";
|
|
314
420
|
if (Boolean(element.checked) !== wanted) element.click();
|
|
421
|
+
if (Boolean(element.checked) !== wanted) throw new Error(`checkable control did not reach the requested ${wanted ? "checked" : "unchecked"} state`);
|
|
315
422
|
return;
|
|
316
423
|
}
|
|
317
424
|
if (operation === "select") {
|
|
318
425
|
const text = String(value ?? "");
|
|
319
426
|
const option = [...element.options].find((item) => item.value === text || item.text.trim() === text);
|
|
320
427
|
if (!option) throw new Error("select option was not found");
|
|
321
|
-
element
|
|
428
|
+
setNativeValue(element, option.value);
|
|
322
429
|
dispatchValueEvents(element);
|
|
323
430
|
return;
|
|
324
431
|
}
|
|
@@ -418,7 +525,7 @@
|
|
|
418
525
|
elements = elements.filter((element) => {
|
|
419
526
|
if (selector.name && element.getAttribute("name") !== selector.name) return false;
|
|
420
527
|
if (selector.label && !sameText(labelText(element), selector.label)) return false;
|
|
421
|
-
if (selector.text && !sameText(element
|
|
528
|
+
if (selector.text && !sameText(boundedNodeText(element, MAX_PAGE_FIELD_CHARS + 1), selector.text)) return false;
|
|
422
529
|
if (selector.role && !sameText(element.getAttribute("role") || implicitRole(element), selector.role)) return false;
|
|
423
530
|
if (selector.placeholder && !sameText(element.getAttribute("placeholder") || "", selector.placeholder)) return false;
|
|
424
531
|
return true;
|
|
@@ -478,7 +585,7 @@
|
|
|
478
585
|
}
|
|
479
586
|
|
|
480
587
|
function normalizedText(value) {
|
|
481
|
-
return
|
|
588
|
+
return boundedPageText(value, MAX_WAIT_TEXT_CHARS).trim().replace(/\s+/g, " ").toLowerCase();
|
|
482
589
|
}
|
|
483
590
|
|
|
484
591
|
function sameText(left, right) {
|
|
@@ -486,17 +593,95 @@
|
|
|
486
593
|
}
|
|
487
594
|
|
|
488
595
|
function labelText(element) {
|
|
489
|
-
if (element.labels?.length)
|
|
490
|
-
|
|
491
|
-
|
|
596
|
+
if (element.labels?.length) {
|
|
597
|
+
let text = "";
|
|
598
|
+
for (const label of [...element.labels].slice(0, 100)) {
|
|
599
|
+
text += ` ${boundedNodeText(label, Math.max(0, MAX_PAGE_FIELD_CHARS - text.length))}`;
|
|
600
|
+
if (text.length >= MAX_PAGE_FIELD_CHARS) break;
|
|
601
|
+
}
|
|
602
|
+
return boundedPageText(text.trim(), MAX_PAGE_FIELD_CHARS);
|
|
603
|
+
}
|
|
604
|
+
const parent = element.closest?.("label");
|
|
605
|
+
if (parent) return boundedNodeText(parent, MAX_PAGE_FIELD_CHARS);
|
|
492
606
|
const labelledBy = element.getAttribute("aria-labelledby");
|
|
493
|
-
if (labelledBy)
|
|
607
|
+
if (labelledBy) {
|
|
608
|
+
let text = "";
|
|
609
|
+
for (const id of labelledBy.split(/\s+/).slice(0, 100)) {
|
|
610
|
+
const labelled = rootById(element, id);
|
|
611
|
+
text += ` ${boundedNodeText(labelled, Math.max(0, MAX_PAGE_FIELD_CHARS - text.length))}`;
|
|
612
|
+
if (text.length >= MAX_PAGE_FIELD_CHARS) break;
|
|
613
|
+
}
|
|
614
|
+
return boundedPageText(text.trim(), MAX_PAGE_FIELD_CHARS);
|
|
615
|
+
}
|
|
494
616
|
return "";
|
|
495
617
|
}
|
|
496
618
|
|
|
497
619
|
function accessibleName(element) {
|
|
498
|
-
|
|
499
|
-
|
|
620
|
+
const direct = element.getAttribute("aria-label") || labelText(element) || element.getAttribute("title") || element.getAttribute("alt");
|
|
621
|
+
return boundedPageText(direct || boundedNodeText(element, 500), 500).trim().replace(/\s+/g, " ");
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function boundedNodeText(node, maxChars) {
|
|
625
|
+
const limit = Math.max(0, Number(maxChars) || 0);
|
|
626
|
+
if (!node || limit === 0) return "";
|
|
627
|
+
if (typeof document.createTreeWalker !== "function") return boundedPageText(node.textContent || node.innerText, limit);
|
|
628
|
+
const walker = document.createTreeWalker(node, globalThis.NodeFilter?.SHOW_TEXT || 4);
|
|
629
|
+
let text = "";
|
|
630
|
+
let current;
|
|
631
|
+
while ((current = walker.nextNode()) && text.length < limit) {
|
|
632
|
+
const remaining = limit - text.length;
|
|
633
|
+
text += boundedPageText(current.data, remaining);
|
|
634
|
+
}
|
|
635
|
+
return text;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function pageContainsText(value) {
|
|
639
|
+
const needle = normalizedText(value);
|
|
640
|
+
if (!needle) return { found: true, truncated: false, scanned_chars: 0 };
|
|
641
|
+
const root = document.body || document.documentElement;
|
|
642
|
+
if (!root) return { found: false, truncated: false, scanned_chars: 0 };
|
|
643
|
+
if (typeof document.createTreeWalker !== "function") {
|
|
644
|
+
const text = boundedPageText(root.textContent || root.innerText, MAX_WAIT_TEXT_CHARS);
|
|
645
|
+
return { found: normalizedText(text).includes(needle), truncated: String(root.textContent || root.innerText || "").length > text.length, scanned_chars: text.length };
|
|
646
|
+
}
|
|
647
|
+
const walker = document.createTreeWalker(root, globalThis.NodeFilter?.SHOW_TEXT || 4);
|
|
648
|
+
let haystack = "";
|
|
649
|
+
let truncated = false;
|
|
650
|
+
let scannedChars = 0;
|
|
651
|
+
let node;
|
|
652
|
+
while ((node = walker.nextNode())) {
|
|
653
|
+
const remaining = MAX_WAIT_TEXT_CHARS - scannedChars;
|
|
654
|
+
if (remaining <= 0) { truncated = true; break; }
|
|
655
|
+
const raw = String(node.data || "");
|
|
656
|
+
const boundedRaw = raw.slice(0, remaining);
|
|
657
|
+
scannedChars += boundedRaw.length;
|
|
658
|
+
const chunk = normalizedText(boundedRaw);
|
|
659
|
+
if (chunk) {
|
|
660
|
+
haystack = haystack ? `${haystack} ${chunk}` : chunk;
|
|
661
|
+
if (haystack.includes(needle)) return { found: true, truncated: raw.length > boundedRaw.length, scanned_chars: scannedChars };
|
|
662
|
+
if (haystack.length > needle.length * 2 + 4096) haystack = haystack.slice(-(needle.length + 4096));
|
|
663
|
+
}
|
|
664
|
+
if (raw.length > boundedRaw.length) { truncated = true; break; }
|
|
665
|
+
}
|
|
666
|
+
return { found: haystack.includes(needle), truncated, scanned_chars: Math.min(MAX_WAIT_TEXT_CHARS, scannedChars) };
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function boundedPageText(value, maxChars) {
|
|
670
|
+
return String(value || "").slice(0, Math.max(0, maxChars));
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function safePageUrl(value) {
|
|
674
|
+
const text = boundedPageText(value, MAX_PAGE_URL_CHARS);
|
|
675
|
+
try {
|
|
676
|
+
const parsed = new URL(text, location.href);
|
|
677
|
+
if (parsed.username || parsed.password) {
|
|
678
|
+
parsed.username = "";
|
|
679
|
+
parsed.password = "";
|
|
680
|
+
}
|
|
681
|
+
return boundedPageText(parsed.href, MAX_PAGE_URL_CHARS);
|
|
682
|
+
} catch {
|
|
683
|
+
return text;
|
|
684
|
+
}
|
|
500
685
|
}
|
|
501
686
|
|
|
502
687
|
function implicitRole(element) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const marker = document.querySelector('meta[name="machine-bridge-browser-pair"]');
|
|
3
3
|
const portMeta = document.querySelector('meta[name="machine-bridge-browser-port"]');
|
|
4
4
|
const tokenMeta = document.querySelector('meta[name="machine-bridge-browser-token"]');
|
|
5
|
-
if (
|
|
5
|
+
if (marker?.content !== "1" || !portMeta || !tokenMeta) return;
|
|
6
6
|
const token = tokenMeta.content;
|
|
7
7
|
const port = Number(portMeta.content);
|
|
8
8
|
if (!/^[A-Za-z0-9_-]{32,100}$/.test(token) || !Number.isInteger(port) || port < 1024 || port > 65535) return;
|