@unotest/grounder-client 0.43.0 → 0.43.2
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 +38 -0
- package/dist/dom-walker/index.d.ts +9 -1
- package/dist/dom-walker/index.js +31 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.43.2] - 2026-09-30
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 70c37be: Screenshots and saved page HTML leave out what a page keeps secret.
|
|
8
|
+
|
|
9
|
+
- Password and one-time-code fields are painted over on every screenshot: the agent's screenshots, per-step captures, and the screenshot taken when a run fails.
|
|
10
|
+
- The `page.html` saved when a run fails no longer contains the content of `<script>` elements or the values of hidden form fields; each is replaced by a label with its length. The same applies to the element HTML written in debug mode.
|
|
11
|
+
- Text in elements that are not rendered, and in `noembed` and `noframes`, no longer reaches element names.
|
|
12
|
+
|
|
13
|
+
What changes for you: if you read `page.html` for script data or hidden field values, set `failureBundle.pageHtml: "full"` in your config to keep them. Password fields stay hidden either way.
|
|
14
|
+
|
|
15
|
+
- 0e15c53: An element's text, wherever the page is read for it — the locator a ref becomes in a recorded scenario, `check_locator`'s matches and the failure's semantic snapshot — no longer includes the content of `<noscript>`, `<script>`, `<style>` or `<template>`: with JavaScript on, a page could repeat a secret inside `<noscript>` and it went into the customer's scenario (or into the refusal the agent read) in a locator that, since Playwright's own text matching skips `<noscript>`, did not even find the element.
|
|
16
|
+
|
|
17
|
+
The same goes for a failed `assertText` (and `assert_text`): it still compares against the element's full text, as before, but the "got" it quotes — in the MCP reply, the run's output, `runtime.json`, `steps.jsonl` and `failure.json` — leaves those blocks out, and when they alone made the comparison fail it says so. **What changes for you:** a failure message may now read `got "API key"` where it used to show the text of a `<noscript>` as well, followed by a note that the comparison includes hidden text; very long texts are quoted up to 10,000 characters, with a note when cut.
|
|
18
|
+
|
|
19
|
+
## [0.43.1] - 2026-09-29
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- 5433bab: Secret field values no longer leak into failures and replies. An
|
|
24
|
+
`assertValue` failure on a field marked as a credential (`type="password"`,
|
|
25
|
+
or `autocomplete` of `current-password`, `new-password` or `one-time-code`)
|
|
26
|
+
reports only the lengths of the two values, for example
|
|
27
|
+
`expected ‹hidden, 12 chars›, got ‹hidden, 32 chars›`. The same fields are
|
|
28
|
+
no longer read by the grounding capture, and show as `‹hidden, N chars›` in
|
|
29
|
+
a failure's page HTML and in a fill recorded by the viewer. MCP replies now
|
|
30
|
+
mask registered secrets as `‹secret:NAME›`, as the session journal already
|
|
31
|
+
did: text, structured content and tool error messages.
|
|
32
|
+
|
|
33
|
+
**What to check if you ran 0.43.0 or earlier.** The value of a password or
|
|
34
|
+
one-time-code field, and the value of a secret from `unotest/.secrets`, could
|
|
35
|
+
appear in plain text in MCP tool replies (for example `assert_text`,
|
|
36
|
+
`check_locator`, `assert_value` and the `get_failure_*` tools), in the text
|
|
37
|
+
of an `assertValue` failure, and in the `page.html` and `failure.json` saved
|
|
38
|
+
with a failed run. Search your MCP client's transcripts and your saved run
|
|
39
|
+
failures for your secrets and credentials; if one is there, rotate it.
|
|
40
|
+
|
|
3
41
|
## [0.43.0] - 2026-09-27
|
|
4
42
|
|
|
5
43
|
## [0.42.0] - 2026-09-24
|
|
@@ -9,6 +9,10 @@ interface DomHelperOptions {
|
|
|
9
9
|
* subtrees, and the per-element ancestor climb would be O(depth²) on
|
|
10
10
|
* its hot path. Default "ancestors". */
|
|
11
11
|
ariaHidden?: "ancestors" | "self";
|
|
12
|
+
/** Elements whose content is not an element's text (`TEXT_HIDDEN_TAGS`,
|
|
13
|
+
* text-rules.ts). Passed in rather than written here: this factory is
|
|
14
|
+
* serialized into the page and cannot import the declaration. */
|
|
15
|
+
hiddenTextTags?: readonly string[];
|
|
12
16
|
}
|
|
13
17
|
/** Subset of accessibility state shared by the walker's `RawState` and
|
|
14
18
|
* outline capture in @unotest/web (`disabled` and `level` handling
|
|
@@ -30,6 +34,8 @@ declare function createDomHelpers(opts?: DomHelperOptions): {
|
|
|
30
34
|
cap: (s: string) => string;
|
|
31
35
|
cssEscape: (s: string) => string;
|
|
32
36
|
textContentVisible: (el: Element) => string;
|
|
37
|
+
isHiddenText: (el: Element) => boolean;
|
|
38
|
+
textWithoutHidden: (node: Node) => string;
|
|
33
39
|
interactiveRoles: Set<string>;
|
|
34
40
|
formFieldRole: (el: Element, tag: string) => string | null;
|
|
35
41
|
implicitRole: (el: Element) => string | null;
|
|
@@ -71,6 +77,8 @@ declare function composeInjectedScript(algorithm: (h: InjectedDomHelpers, ...res
|
|
|
71
77
|
|
|
72
78
|
declare function captureRawTree(h: InjectedDomHelpers): RawCapture;
|
|
73
79
|
|
|
80
|
+
declare const TEXT_HIDDEN_TAGS: readonly string[];
|
|
81
|
+
|
|
74
82
|
/** DOM attribute carrying an element's `eN` ref (interactive elements). */
|
|
75
83
|
declare const REF_ATTRIBUTE = "data-unotest-ref";
|
|
76
84
|
/** <html> attribute persisting the ref counter across captures. */
|
|
@@ -112,4 +120,4 @@ declare function highlightByNodeIdScript(nodeIds: number[], color?: string): str
|
|
|
112
120
|
/** Evaluate-ready script removing the highlight overlay, if any. */
|
|
113
121
|
declare function clearHighlightScript(): string;
|
|
114
122
|
|
|
115
|
-
export { type DomHelperOptions, type InjectedDomHelpers, type InjectedScriptOptions, NODE_REGISTRY_GLOBAL, REF_ATTRIBUTE, REF_COUNTER_ATTRIBUTE, RawCapture, captureRawTree, clearHighlightScript, composeInjectedScript, createDomHelpers, domWalkerScript, highlightByNodeIdScript, materializeRefScript, nodeByIdScript };
|
|
123
|
+
export { type DomHelperOptions, type InjectedDomHelpers, type InjectedScriptOptions, NODE_REGISTRY_GLOBAL, REF_ATTRIBUTE, REF_COUNTER_ATTRIBUTE, RawCapture, TEXT_HIDDEN_TAGS, captureRawTree, clearHighlightScript, composeInjectedScript, createDomHelpers, domWalkerScript, highlightByNodeIdScript, materializeRefScript, nodeByIdScript };
|
package/dist/dom-walker/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// src/dom-walker/text-rules.ts
|
|
2
|
+
var TEXT_HIDDEN_TAGS = ["noscript", "script", "style", "template", "noembed", "noframes"];
|
|
3
|
+
|
|
1
4
|
// src/dom-walker/injected-helpers.ts
|
|
2
5
|
function createDomHelpers(opts) {
|
|
3
6
|
const MAX_NAME = opts && opts.maxName || 80;
|
|
@@ -51,10 +54,23 @@ function createDomHelpers(opts) {
|
|
|
51
54
|
function cssEscape(s) {
|
|
52
55
|
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
53
56
|
}
|
|
57
|
+
const HIDDEN_TEXT_TAGS = new Set(opts && opts.hiddenTextTags || []);
|
|
58
|
+
function isHiddenText(el) {
|
|
59
|
+
return HIDDEN_TEXT_TAGS.has(el.tagName.toLowerCase());
|
|
60
|
+
}
|
|
61
|
+
function textWithoutHidden(node) {
|
|
62
|
+
if (node.nodeType === 1 && isHiddenText(node)) return "";
|
|
63
|
+
let out = "";
|
|
64
|
+
for (const child of Array.from(node.childNodes)) {
|
|
65
|
+
if (child.nodeType === 3) out += child.nodeValue ?? "";
|
|
66
|
+
else if (child.nodeType === 1) out += textWithoutHidden(child);
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
54
70
|
function textContentVisible(el) {
|
|
55
71
|
const ht = el;
|
|
56
|
-
if (typeof ht.innerText === "string") return ht.innerText;
|
|
57
|
-
return el
|
|
72
|
+
if (typeof ht.innerText === "string" && ht.checkVisibility?.() === true) return ht.innerText;
|
|
73
|
+
return textWithoutHidden(el);
|
|
58
74
|
}
|
|
59
75
|
const INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
60
76
|
"button",
|
|
@@ -457,6 +473,8 @@ function createDomHelpers(opts) {
|
|
|
457
473
|
cap,
|
|
458
474
|
cssEscape,
|
|
459
475
|
textContentVisible,
|
|
476
|
+
isHiddenText,
|
|
477
|
+
textWithoutHidden,
|
|
460
478
|
interactiveRoles: INTERACTIVE_ROLES,
|
|
461
479
|
formFieldRole,
|
|
462
480
|
implicitRole,
|
|
@@ -483,7 +501,7 @@ function createDomHelpers(opts) {
|
|
|
483
501
|
};
|
|
484
502
|
}
|
|
485
503
|
function composeInjectedScript(algorithm, opts = {}) {
|
|
486
|
-
const helperOpts = JSON.stringify(opts.helperOpts ?? {});
|
|
504
|
+
const helperOpts = JSON.stringify({ hiddenTextTags: TEXT_HIDDEN_TAGS, ...opts.helperOpts ?? {} });
|
|
487
505
|
const args = ["__h", ...opts.argLiterals ?? []].join(", ");
|
|
488
506
|
return `(() => { var __name = (fn) => fn; var __h = (${createDomHelpers.toString()})(${helperOpts}); return (${algorithm.toString()})(${args}); })()`;
|
|
489
507
|
}
|
|
@@ -514,11 +532,18 @@ function captureRawTree(h) {
|
|
|
514
532
|
}
|
|
515
533
|
return h.cap(phrase);
|
|
516
534
|
}
|
|
535
|
+
const SECRET_AUTOCOMPLETE = ["current-password", "new-password", "one-time-code"];
|
|
536
|
+
function isSecretField(el) {
|
|
537
|
+
if ((el.getAttribute("type") ?? "").trim().toLowerCase() === "password") return true;
|
|
538
|
+
const tokens = (el.getAttribute("autocomplete") ?? "").toLowerCase().split(/\s+/);
|
|
539
|
+
return tokens.some((token) => SECRET_AUTOCOMPLETE.includes(token));
|
|
540
|
+
}
|
|
517
541
|
function controlValue(el) {
|
|
518
542
|
const tag = el.tagName.toLowerCase();
|
|
543
|
+
if ((tag === "input" || tag === "textarea") && isSecretField(el)) return void 0;
|
|
519
544
|
if (tag === "input") {
|
|
520
545
|
const t = (el.type || "text").toLowerCase();
|
|
521
|
-
if (t === "checkbox" || t === "radio" || t === "button" || t === "submit" || t === "reset" || t === "image" || t === "
|
|
546
|
+
if (t === "checkbox" || t === "radio" || t === "button" || t === "submit" || t === "reset" || t === "image" || t === "hidden") {
|
|
522
547
|
return void 0;
|
|
523
548
|
}
|
|
524
549
|
const v = h.trim(el.value);
|
|
@@ -598,7 +623,7 @@ function captureRawTree(h) {
|
|
|
598
623
|
if (n.nodeType !== 1) return;
|
|
599
624
|
const el = n;
|
|
600
625
|
const tag = el.tagName.toLowerCase();
|
|
601
|
-
if (
|
|
626
|
+
if (h.isHiddenText(el)) return;
|
|
602
627
|
if (tag === "svg") return;
|
|
603
628
|
if (!h.isVisible(el)) return;
|
|
604
629
|
bucket.push(buildElement(el));
|
|
@@ -705,6 +730,7 @@ export {
|
|
|
705
730
|
NODE_REGISTRY_GLOBAL,
|
|
706
731
|
REF_ATTRIBUTE,
|
|
707
732
|
REF_COUNTER_ATTRIBUTE,
|
|
733
|
+
TEXT_HIDDEN_TAGS,
|
|
708
734
|
captureRawTree,
|
|
709
735
|
clearHighlightScript,
|
|
710
736
|
composeInjectedScript,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unotest/grounder-client",
|
|
3
|
-
"version": "0.43.
|
|
3
|
+
"version": "0.43.2",
|
|
4
4
|
"description": "Client contract for the unotest semantic UI grounder: typed intent/resolution types, the injectable DOM walker (RawCapture), the content hash, and the HTTP client + wire protocol of the grounding service. The grounding engine itself is a private service; this package is everything a consumer needs to capture a page and talk to it.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|