@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 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 };
@@ -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.textContent ?? "";
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 === "password" || t === "hidden") {
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 (tag === "script" || tag === "style" || tag === "noscript" || tag === "template") return;
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.0",
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": {