@flrande/bak-extension 0.3.8 → 0.6.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.
@@ -33,11 +33,24 @@
33
33
  // src/privacy.ts
34
34
  var MAX_SAFE_TEXT_LENGTH = 120;
35
35
  var MAX_DEBUG_TEXT_LENGTH = 320;
36
+ var REDACTION_MARKER = "[REDACTED]";
36
37
  var EMAIL_PATTERN = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
37
38
  var LONG_DIGIT_PATTERN = /(?:\d[ -]?){13,19}/g;
38
39
  var OTP_PATTERN = /^\d{4,8}$/;
39
40
  var SECRET_QUERY_PARAM_PATTERN = /(token|secret|password|passwd|otp|code|session|auth)=/i;
40
41
  var HIGH_ENTROPY_TOKEN_PATTERN = /^(?=.*\d)(?=.*[a-zA-Z])[A-Za-z0-9~!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`]{16,}$/;
42
+ var TRANSPORT_SECRET_KEY_SOURCE = "(?:api[_-]?key|authorization|auth|cookie|csrf(?:token)?|nonce|password|passwd|secret|session(?:id)?|token|xsrf(?:token)?)";
43
+ var TRANSPORT_SECRET_PAIR_PATTERN = new RegExp(`((?:^|[?&;,\\s])${TRANSPORT_SECRET_KEY_SOURCE}=)[^&\\r\\n"'>]*`, "gi");
44
+ var JSON_SECRET_VALUE_PATTERN = new RegExp(
45
+ `((?:"|')${TRANSPORT_SECRET_KEY_SOURCE}(?:"|')\\s*:\\s*)(?:"[^"]*"|'[^']*'|true|false|null|-?\\d+(?:\\.\\d+)?)`,
46
+ "gi"
47
+ );
48
+ var ASSIGNMENT_SECRET_VALUE_PATTERN = new RegExp(
49
+ `((?:^|[\\s,{;])${TRANSPORT_SECRET_KEY_SOURCE}\\s*[:=]\\s*)([^,&;}"'\\r\\n]+)`,
50
+ "gi"
51
+ );
52
+ var AUTHORIZATION_VALUE_PATTERN = /\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+\b/gi;
53
+ var SENSITIVE_ATTRIBUTE_PATTERN = /(?:api[_-]?key|authorization|auth|cookie|csrf|nonce|password|passwd|secret|session|token|xsrf)/i;
41
54
  var INPUT_TEXT_ENTRY_TYPES = /* @__PURE__ */ new Set([
42
55
  "text",
43
56
  "search",
@@ -73,11 +86,31 @@
73
86
  if (OTP_PATTERN.test(output)) {
74
87
  return "[REDACTED:otp]";
75
88
  }
76
- if (HIGH_ENTROPY_TOKEN_PATTERN.test(output) && !output.includes(" ")) {
89
+ if (HIGH_ENTROPY_TOKEN_PATTERN.test(output) && !/[ =&:]/.test(output) && !output.includes("[REDACTED")) {
77
90
  return "[REDACTED:secret]";
78
91
  }
79
92
  return output;
80
93
  }
94
+ function redactTransportSecrets(text) {
95
+ let output = text;
96
+ output = output.replace(AUTHORIZATION_VALUE_PATTERN, "$1 [REDACTED]");
97
+ output = output.replace(TRANSPORT_SECRET_PAIR_PATTERN, "$1[REDACTED]");
98
+ output = output.replace(JSON_SECRET_VALUE_PATTERN, '$1"[REDACTED]"');
99
+ output = output.replace(ASSIGNMENT_SECRET_VALUE_PATTERN, "$1[REDACTED]");
100
+ if (HIGH_ENTROPY_TOKEN_PATTERN.test(output) && !/[ =&:]/.test(output) && !output.includes("[REDACTED")) {
101
+ return "[REDACTED:secret]";
102
+ }
103
+ return output;
104
+ }
105
+ function redactAttributeValue(name, value) {
106
+ if (!value) {
107
+ return value;
108
+ }
109
+ if (name === "value") {
110
+ return REDACTION_MARKER;
111
+ }
112
+ return redactTransportText(value);
113
+ }
81
114
  function redactElementText(raw, options = {}) {
82
115
  if (!raw) {
83
116
  return "";
@@ -89,6 +122,44 @@
89
122
  const redacted = redactByPattern(normalized);
90
123
  return clamp(redacted, options);
91
124
  }
125
+ function redactTransportText(raw) {
126
+ if (!raw) {
127
+ return "";
128
+ }
129
+ return redactTransportSecrets(String(raw));
130
+ }
131
+ function redactHtmlSnapshot(root) {
132
+ if (!root || !("cloneNode" in root)) {
133
+ return "";
134
+ }
135
+ const clone = root.cloneNode(true);
136
+ const elements = [clone, ...Array.from(clone.querySelectorAll("*"))];
137
+ for (const element of elements) {
138
+ const tagName = element.tagName.toLowerCase();
139
+ if (tagName === "script" && !element.getAttribute("src")) {
140
+ element.textContent = "[REDACTED:script]";
141
+ }
142
+ if (tagName === "textarea" && (element.textContent ?? "").trim().length > 0) {
143
+ element.textContent = REDACTION_MARKER;
144
+ }
145
+ for (const attribute of Array.from(element.attributes)) {
146
+ const name = attribute.name;
147
+ const value = attribute.value;
148
+ const shouldRedactValue = name === "value" && (tagName === "input" || tagName === "textarea" || tagName === "option") || SENSITIVE_ATTRIBUTE_PATTERN.test(name);
149
+ if (shouldRedactValue) {
150
+ element.setAttribute(name, redactAttributeValue(name, value));
151
+ continue;
152
+ }
153
+ if (name === "href" || name === "src" || name === "action" || name === "content" || name.startsWith("data-")) {
154
+ const redacted = redactAttributeValue(name, value);
155
+ if (redacted !== value) {
156
+ element.setAttribute(name, redacted);
157
+ }
158
+ }
159
+ }
160
+ }
161
+ return "outerHTML" in clone ? clone.outerHTML : "";
162
+ }
92
163
  function isTextEntryField(candidates) {
93
164
  const tag = candidates.tag.toLowerCase();
94
165
  const role = (candidates.role ?? "").toLowerCase();
@@ -163,6 +234,8 @@
163
234
  var longTaskCount = 0;
164
235
  var longTaskDurationMs = 0;
165
236
  var performanceBaselineMs = 0;
237
+ var pageLoadedAt = Math.round(performance.timeOrigin || Date.now());
238
+ var lastMutationAt = Date.now();
166
239
  function isHtmlElement(node) {
167
240
  if (!node) {
168
241
  return false;
@@ -710,6 +783,28 @@
710
783
  }
711
784
  return collected;
712
785
  }
786
+ function evaluateXPath(root, expression) {
787
+ try {
788
+ const ownerDocument = isDocumentNode(root) ? root : root.ownerDocument ?? document;
789
+ const iterator = ownerDocument.evaluate(
790
+ expression,
791
+ root,
792
+ null,
793
+ XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
794
+ null
795
+ );
796
+ const matches = [];
797
+ for (let index = 0; index < iterator.snapshotLength; index += 1) {
798
+ const node = iterator.snapshotItem(index);
799
+ if (isHtmlElement(node)) {
800
+ matches.push(node);
801
+ }
802
+ }
803
+ return matches;
804
+ } catch {
805
+ return [];
806
+ }
807
+ }
713
808
  function resolveFrameDocument(framePath) {
714
809
  let currentDocument = document;
715
810
  for (const selector of framePath) {
@@ -782,6 +877,7 @@
782
877
  const eid = buildEid(element);
783
878
  const selectors = {
784
879
  css: toCssSelector(element),
880
+ xpath: null,
785
881
  text: text || name ? (text || name).slice(0, 80) : null,
786
882
  aria: role && name ? `${role}:${name.slice(0, 80)}` : null
787
883
  };
@@ -793,6 +889,8 @@
793
889
  role,
794
890
  name,
795
891
  text,
892
+ visible: isElementVisible(element),
893
+ enabled: !(element.disabled || element.getAttribute("aria-disabled") === "true"),
796
894
  bbox: {
797
895
  x: rect.x,
798
896
  y: rect.y,
@@ -903,6 +1001,13 @@
903
1001
  currentRoot = found.shadowRoot;
904
1002
  }
905
1003
  }
1004
+ if (locator.xpath) {
1005
+ const matches = evaluateXPath(root, locator.xpath).filter((item) => isElementVisible(item));
1006
+ const found = indexedCandidate(matches, locator);
1007
+ if (found) {
1008
+ return found;
1009
+ }
1010
+ }
906
1011
  if (locator.name) {
907
1012
  const fallback = indexedCandidate(
908
1013
  interactive.filter((element) => inferName(element).toLowerCase().includes(locator.name.toLowerCase())),
@@ -914,6 +1019,61 @@
914
1019
  }
915
1020
  return null;
916
1021
  }
1022
+ function matchedElementsForLocator(locator) {
1023
+ if (!locator) {
1024
+ return [];
1025
+ }
1026
+ const rootResult = resolveRootForLocator(locator);
1027
+ if (!rootResult.ok) {
1028
+ return [];
1029
+ }
1030
+ const root = rootResult.root;
1031
+ const interactive = getInteractiveElements(root, locator.shadow !== "none");
1032
+ if (locator.css) {
1033
+ const parts = splitShadowSelector(locator.css);
1034
+ let currentRoot = root;
1035
+ for (let index = 0; index < parts.length; index += 1) {
1036
+ const selector = parts[index];
1037
+ const found = locator.shadow === "none" ? querySelectorInTree(currentRoot, selector) : querySelectorAcrossOpenShadow(currentRoot, selector);
1038
+ if (!found) {
1039
+ return [];
1040
+ }
1041
+ if (index === parts.length - 1) {
1042
+ return (locator.shadow === "none" ? Array.from(currentRoot.querySelectorAll(selector)) : querySelectorAllAcrossOpenShadow(currentRoot, selector)).filter((item) => isElementVisible(item));
1043
+ }
1044
+ if (!found.shadowRoot) {
1045
+ return [];
1046
+ }
1047
+ currentRoot = found.shadowRoot;
1048
+ }
1049
+ }
1050
+ if (locator.xpath) {
1051
+ return evaluateXPath(root, locator.xpath).filter((item) => isElementVisible(item));
1052
+ }
1053
+ if (locator.role || locator.name || locator.text) {
1054
+ return interactive.filter((element) => {
1055
+ if (locator.role && inferRole(element).toLowerCase() !== locator.role.toLowerCase()) {
1056
+ return false;
1057
+ }
1058
+ if (locator.name && !inferName(element).toLowerCase().includes(locator.name.toLowerCase())) {
1059
+ return false;
1060
+ }
1061
+ if (locator.text) {
1062
+ const text = (element.innerText || element.textContent || "").toLowerCase();
1063
+ if (!text.includes(locator.text.toLowerCase())) {
1064
+ return false;
1065
+ }
1066
+ }
1067
+ return true;
1068
+ });
1069
+ }
1070
+ if (locator.eid) {
1071
+ collectElements({}, locator);
1072
+ const fromCache = elementCache.get(locator.eid);
1073
+ return fromCache ? [fromCache] : [];
1074
+ }
1075
+ return [];
1076
+ }
917
1077
  function ensureOverlayRoot() {
918
1078
  let root = document.getElementById("bak-overlay-root");
919
1079
  if (!root) {
@@ -1387,6 +1547,30 @@
1387
1547
  return true;
1388
1548
  }).slice(-limit).reverse();
1389
1549
  }
1550
+ function filterNetworkEntrySections(entry, include) {
1551
+ if (!Array.isArray(include)) {
1552
+ return entry;
1553
+ }
1554
+ const sections = new Set(
1555
+ include.map(String).filter((section) => section === "request" || section === "response")
1556
+ );
1557
+ if (sections.size === 0 || sections.size === 2) {
1558
+ return entry;
1559
+ }
1560
+ const clone = { ...entry };
1561
+ if (!sections.has("request")) {
1562
+ delete clone.requestHeaders;
1563
+ delete clone.requestBodyPreview;
1564
+ delete clone.requestBodyTruncated;
1565
+ }
1566
+ if (!sections.has("response")) {
1567
+ delete clone.responseHeaders;
1568
+ delete clone.responseBodyPreview;
1569
+ delete clone.responseBodyTruncated;
1570
+ delete clone.binary;
1571
+ }
1572
+ return clone;
1573
+ }
1390
1574
  async function waitForNetwork(params) {
1391
1575
  const timeoutMs = typeof params.timeoutMs === "number" ? Math.max(1, params.timeoutMs) : 5e3;
1392
1576
  const sinceTs = typeof params.sinceTs === "number" ? params.sinceTs : Date.now() - timeoutMs;
@@ -1400,6 +1584,559 @@
1400
1584
  }
1401
1585
  throw { code: "E_TIMEOUT", message: "network.waitFor timeout" };
1402
1586
  }
1587
+ function trackMutations() {
1588
+ const observer = new MutationObserver(() => {
1589
+ lastMutationAt = Date.now();
1590
+ });
1591
+ const root = document.documentElement ?? document.body;
1592
+ if (!root) {
1593
+ return;
1594
+ }
1595
+ observer.observe(root, {
1596
+ childList: true,
1597
+ subtree: true,
1598
+ attributes: true,
1599
+ characterData: true
1600
+ });
1601
+ }
1602
+ function currentContextSnapshot() {
1603
+ return {
1604
+ tabId: null,
1605
+ framePath: [...contextState.framePath],
1606
+ shadowPath: [...contextState.shadowPath]
1607
+ };
1608
+ }
1609
+ function timestampCandidatesFromText(text, patterns) {
1610
+ const regexes = (patterns ?? [String.raw`\b20\d{2}-\d{2}-\d{2}\b`, String.raw`\b20\d{2}\/\d{2}\/\d{2}\b`]).map(
1611
+ (pattern) => new RegExp(pattern, "gi")
1612
+ );
1613
+ const collected = /* @__PURE__ */ new Set();
1614
+ for (const regex of regexes) {
1615
+ for (const match of text.matchAll(regex)) {
1616
+ if (match[0]) {
1617
+ collected.add(match[0]);
1618
+ }
1619
+ }
1620
+ }
1621
+ return [...collected];
1622
+ }
1623
+ function listInlineScripts() {
1624
+ return Array.from(document.scripts).filter((script) => !script.src).map((script) => {
1625
+ const content = script.textContent ?? "";
1626
+ const suspectedVars = [...content.matchAll(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*(?:data|table|json|state|store)[A-Za-z_$\w]*)/gi)].map(
1627
+ (match) => match[1] ?? ""
1628
+ ).filter(Boolean);
1629
+ return { content, suspectedVars };
1630
+ });
1631
+ }
1632
+ function cookieMetadata() {
1633
+ return document.cookie.split(";").map((item) => item.trim()).filter(Boolean).map((item) => ({ name: item.split("=")[0] ?? "" })).filter((item) => item.name.length > 0);
1634
+ }
1635
+ function storageMetadata() {
1636
+ return {
1637
+ localStorageKeys: Object.keys(localStorage),
1638
+ sessionStorageKeys: Object.keys(sessionStorage)
1639
+ };
1640
+ }
1641
+ function collectFrames(baseDocument = document, framePath = []) {
1642
+ const frames = [];
1643
+ const frameNodes = Array.from(baseDocument.querySelectorAll("iframe,frame"));
1644
+ for (const frame of frameNodes) {
1645
+ if (!isFrameElement(frame)) {
1646
+ continue;
1647
+ }
1648
+ const selector = frame.id ? `#${frame.id}` : `${frame.tagName.toLowerCase()}:nth-of-type(${frameNodes.indexOf(frame) + 1})`;
1649
+ try {
1650
+ const childDocument = frame.contentDocument;
1651
+ if (!childDocument) {
1652
+ continue;
1653
+ }
1654
+ const nextPath = [...framePath, selector];
1655
+ frames.push({
1656
+ framePath: nextPath,
1657
+ url: childDocument.location.href
1658
+ });
1659
+ frames.push(...collectFrames(childDocument, nextPath));
1660
+ } catch {
1661
+ continue;
1662
+ }
1663
+ }
1664
+ return frames;
1665
+ }
1666
+ function buildTableId(kind, index) {
1667
+ return `${kind}:${index + 1}`;
1668
+ }
1669
+ function describeTables() {
1670
+ const rootResult = resolveRootForLocator();
1671
+ if (!rootResult.ok) {
1672
+ return [];
1673
+ }
1674
+ const root = rootResult.root;
1675
+ const tables = [];
1676
+ const htmlTables = Array.from(root.querySelectorAll("table"));
1677
+ for (const [index, table] of htmlTables.entries()) {
1678
+ tables.push({
1679
+ id: buildTableId(table.closest(".dataTables_wrapper") ? "dataTables" : "html", index),
1680
+ name: (table.getAttribute("aria-label") || table.getAttribute("data-testid") || table.id || `table-${index + 1}`).trim(),
1681
+ kind: table.closest(".dataTables_wrapper") ? "dataTables" : "html",
1682
+ selector: table.id ? `#${table.id}` : void 0,
1683
+ rowCount: table.querySelectorAll("tbody tr").length || table.querySelectorAll("tr").length,
1684
+ columnCount: table.querySelectorAll("thead th").length || table.querySelectorAll("tr:first-child th, tr:first-child td").length
1685
+ });
1686
+ }
1687
+ const gridRoots = Array.from(root.querySelectorAll('[role="grid"], [role="table"], .ag-root, .ag-root-wrapper'));
1688
+ for (const [index, grid] of gridRoots.entries()) {
1689
+ const kind = grid.className.includes("ag-") ? "ag-grid" : "aria-grid";
1690
+ tables.push({
1691
+ id: buildTableId(kind, index),
1692
+ name: (grid.getAttribute("aria-label") || grid.getAttribute("data-testid") || grid.id || `grid-${index + 1}`).trim(),
1693
+ kind,
1694
+ selector: grid.id ? `#${grid.id}` : void 0,
1695
+ rowCount: grid.querySelectorAll('[role="row"]').length,
1696
+ columnCount: grid.querySelectorAll('[role="columnheader"]').length
1697
+ });
1698
+ }
1699
+ return tables;
1700
+ }
1701
+ function resolveTable(handleId) {
1702
+ const tables = describeTables();
1703
+ const handle = tables.find((candidate) => candidate.id === handleId);
1704
+ if (!handle) {
1705
+ return null;
1706
+ }
1707
+ const rootResult = resolveRootForLocator();
1708
+ if (!rootResult.ok) {
1709
+ return null;
1710
+ }
1711
+ const root = rootResult.root;
1712
+ if (!handle.selector) {
1713
+ if (handle.kind === "html" || handle.kind === "dataTables") {
1714
+ const index2 = Number(handle.id.split(":")[1] ?? "1") - 1;
1715
+ return { table: handle, element: root.querySelectorAll("table")[index2] ?? null };
1716
+ }
1717
+ const candidates = root.querySelectorAll('[role="grid"], [role="table"], .ag-root, .ag-root-wrapper');
1718
+ const index = Number(handle.id.split(":")[1] ?? "1") - 1;
1719
+ return { table: handle, element: candidates[index] ?? null };
1720
+ }
1721
+ return { table: handle, element: root.querySelector(handle.selector) };
1722
+ }
1723
+ function htmlTableSchema(table) {
1724
+ const headers = Array.from(table.querySelectorAll("thead th"));
1725
+ if (headers.length > 0) {
1726
+ return headers.map((cell, index) => ({
1727
+ key: `col_${index + 1}`,
1728
+ label: (cell.textContent ?? "").trim() || `Column ${index + 1}`
1729
+ }));
1730
+ }
1731
+ const firstRow = table.querySelector("tr");
1732
+ return Array.from(firstRow?.querySelectorAll("th,td") ?? []).map((cell, index) => ({
1733
+ key: `col_${index + 1}`,
1734
+ label: (cell.textContent ?? "").trim() || `Column ${index + 1}`
1735
+ }));
1736
+ }
1737
+ function htmlTableRows(table, limit) {
1738
+ const columns = htmlTableSchema(table);
1739
+ const rowNodes = Array.from(table.querySelectorAll("tbody tr")).length > 0 ? Array.from(table.querySelectorAll("tbody tr")) : Array.from(table.querySelectorAll("tr")).slice(1);
1740
+ return rowNodes.slice(0, limit).map((row) => {
1741
+ const record = {};
1742
+ Array.from(row.querySelectorAll("th,td")).forEach((cell, index) => {
1743
+ const key = columns[index]?.label ?? `Column ${index + 1}`;
1744
+ record[key] = (cell.textContent ?? "").trim();
1745
+ });
1746
+ return record;
1747
+ });
1748
+ }
1749
+ function gridSchema(grid) {
1750
+ return Array.from(grid.querySelectorAll('[role="columnheader"]')).map((cell, index) => ({
1751
+ key: `col_${index + 1}`,
1752
+ label: (cell.textContent ?? "").trim() || `Column ${index + 1}`
1753
+ }));
1754
+ }
1755
+ function gridRows(grid, limit) {
1756
+ const columns = gridSchema(grid);
1757
+ return Array.from(grid.querySelectorAll('[role="row"]')).slice(0, limit).map((row) => {
1758
+ const record = {};
1759
+ Array.from(row.querySelectorAll('[role="gridcell"], [role="cell"], [role="columnheader"]')).forEach((cell, index) => {
1760
+ const key = columns[index]?.label ?? `Column ${index + 1}`;
1761
+ record[key] = (cell.textContent ?? "").trim();
1762
+ });
1763
+ return record;
1764
+ }).filter((row) => Object.values(row).some((value) => String(value).trim().length > 0));
1765
+ }
1766
+ function runPageWorldRequest(action, payload) {
1767
+ const requestId = `bak_page_world_${Date.now()}_${Math.random().toString(16).slice(2)}`;
1768
+ return new Promise((resolve, reject) => {
1769
+ const timeoutMs = typeof payload.timeoutMs === "number" && Number.isFinite(payload.timeoutMs) && payload.timeoutMs > 0 ? Math.max(1e3, Math.floor(payload.timeoutMs) + 1e3) : 15e3;
1770
+ const responseNode = document.createElement("div");
1771
+ responseNode.setAttribute("data-bak-page-world-response", requestId);
1772
+ responseNode.hidden = true;
1773
+ (document.documentElement ?? document.head ?? document.body).appendChild(responseNode);
1774
+ const cleanup = () => {
1775
+ clearTimeout(timer);
1776
+ observer.disconnect();
1777
+ responseNode.remove();
1778
+ };
1779
+ const tryResolveFromNode = () => {
1780
+ if (responseNode.getAttribute("data-ready") !== "1") {
1781
+ return;
1782
+ }
1783
+ const payloadText = responseNode.getAttribute("data-payload");
1784
+ const detail = payloadText ? JSON.parse(payloadText) : null;
1785
+ if (!detail) {
1786
+ cleanup();
1787
+ reject(failAction("E_EXECUTION", `${action} returned no detail`));
1788
+ return;
1789
+ }
1790
+ cleanup();
1791
+ if (detail.ok) {
1792
+ resolve(detail.result);
1793
+ return;
1794
+ }
1795
+ reject(detail.error ?? failAction("E_EXECUTION", `${action} failed`));
1796
+ };
1797
+ const observer = new MutationObserver(() => {
1798
+ try {
1799
+ tryResolveFromNode();
1800
+ } catch (error) {
1801
+ cleanup();
1802
+ reject(error instanceof Error ? error : new Error(String(error)));
1803
+ }
1804
+ });
1805
+ observer.observe(responseNode, {
1806
+ attributes: true,
1807
+ childList: true,
1808
+ characterData: true,
1809
+ subtree: true
1810
+ });
1811
+ const timer = window.setTimeout(() => {
1812
+ cleanup();
1813
+ reject(failAction("E_TIMEOUT", `${action} timed out`));
1814
+ }, timeoutMs);
1815
+ const injector = document.createElement("script");
1816
+ injector.textContent = `
1817
+ (() => {
1818
+ const requestId = ${JSON.stringify(requestId)};
1819
+ const action = ${JSON.stringify(action)};
1820
+ const payload = ${JSON.stringify(payload)};
1821
+ const responseNode = document.querySelector('div[data-bak-page-world-response="' + requestId + '"]');
1822
+ const emit = (detail) => {
1823
+ if (!responseNode) {
1824
+ return;
1825
+ }
1826
+ responseNode.setAttribute('data-payload', JSON.stringify(detail));
1827
+ responseNode.setAttribute('data-ready', '1');
1828
+ };
1829
+ const serializeValue = (value, maxBytes) => {
1830
+ let cloned;
1831
+ try {
1832
+ cloned = typeof structuredClone === 'function' ? structuredClone(value) : JSON.parse(JSON.stringify(value));
1833
+ } catch (error) {
1834
+ throw { code: 'E_NOT_SERIALIZABLE', message: error instanceof Error ? error.message : String(error) };
1835
+ }
1836
+ const json = JSON.stringify(cloned);
1837
+ if (typeof maxBytes === 'number' && maxBytes > 0 && json.length > maxBytes) {
1838
+ throw { code: 'E_BODY_TOO_LARGE', message: 'serialized value exceeds max-bytes', details: { bytes: json.length, maxBytes } };
1839
+ }
1840
+ return { value: cloned, bytes: json.length };
1841
+ };
1842
+ const resolveFrameWindow = (framePath) => {
1843
+ let currentWindow = window;
1844
+ let currentDocument = document;
1845
+ for (const selector of framePath || []) {
1846
+ const frame = currentDocument.querySelector(selector);
1847
+ if (!frame || !('contentWindow' in frame)) {
1848
+ throw { code: 'E_NOT_FOUND', message: 'frame not found: ' + selector };
1849
+ }
1850
+ const nextWindow = frame.contentWindow;
1851
+ if (!nextWindow) {
1852
+ throw { code: 'E_NOT_READY', message: 'frame window unavailable: ' + selector };
1853
+ }
1854
+ try {
1855
+ currentDocument = nextWindow.document;
1856
+ } catch {
1857
+ throw { code: 'E_CROSS_ORIGIN_BLOCKED', message: 'cross-origin frame is not accessible: ' + selector };
1858
+ }
1859
+ currentWindow = nextWindow;
1860
+ }
1861
+ return currentWindow;
1862
+ };
1863
+ const collectFrames = (rootWindow, framePath) => {
1864
+ const collected = [{
1865
+ url: rootWindow.location.href,
1866
+ framePath,
1867
+ targetWindow: rootWindow
1868
+ }];
1869
+ const frames = Array.from(rootWindow.document.querySelectorAll('iframe,frame'));
1870
+ frames.forEach((frame, index) => {
1871
+ const selector = frame.id ? '#' + frame.id : frame.tagName.toLowerCase() + ':nth-of-type(' + (index + 1) + ')';
1872
+ try {
1873
+ if (!frame.contentWindow || !frame.contentWindow.document) {
1874
+ return;
1875
+ }
1876
+ collected.push(...collectFrames(frame.contentWindow, [...framePath, selector]));
1877
+ } catch {
1878
+ // Skip cross-origin frames.
1879
+ }
1880
+ });
1881
+ return collected;
1882
+ };
1883
+ const parsePath = (path) => {
1884
+ if (typeof path !== 'string' || !path.trim()) {
1885
+ throw { code: 'E_INVALID_PARAMS', message: 'path is required' };
1886
+ }
1887
+ const normalized = path.replace(/^globalThis\\.?/, '').replace(/^window\\.?/, '').trim();
1888
+ if (!normalized) {
1889
+ return [];
1890
+ }
1891
+ const segments = [];
1892
+ let index = 0;
1893
+ while (index < normalized.length) {
1894
+ if (normalized[index] === '.') {
1895
+ index += 1;
1896
+ continue;
1897
+ }
1898
+ if (normalized[index] === '[') {
1899
+ const bracket = normalized.slice(index).match(/^\\[(\\d+)\\]/);
1900
+ if (!bracket) {
1901
+ throw { code: 'E_INVALID_PARAMS', message: 'Only numeric bracket paths are supported' };
1902
+ }
1903
+ segments.push(Number(bracket[1]));
1904
+ index += bracket[0].length;
1905
+ continue;
1906
+ }
1907
+ const identifier = normalized.slice(index).match(/^[A-Za-z_$][\\w$]*/);
1908
+ if (!identifier) {
1909
+ throw { code: 'E_INVALID_PARAMS', message: 'Unsupported path token near: ' + normalized.slice(index, index + 16) };
1910
+ }
1911
+ segments.push(identifier[0]);
1912
+ index += identifier[0].length;
1913
+ }
1914
+ return segments;
1915
+ };
1916
+ const readPath = (targetWindow, path) => {
1917
+ const segments = parsePath(path);
1918
+ let current = targetWindow;
1919
+ for (const segment of segments) {
1920
+ if (current == null || !(segment in current)) {
1921
+ throw { code: 'E_NOT_FOUND', message: 'path not found: ' + path };
1922
+ }
1923
+ current = current[segment];
1924
+ }
1925
+ return current;
1926
+ };
1927
+ const buildScopeTargets = () => {
1928
+ if (payload.scope === 'main') {
1929
+ return [{
1930
+ url: window.location.href,
1931
+ framePath: [],
1932
+ targetWindow: window
1933
+ }];
1934
+ }
1935
+ if (payload.scope === 'all-frames') {
1936
+ return collectFrames(window, []);
1937
+ }
1938
+ return [{
1939
+ url: resolveFrameWindow(payload.framePath || []).location.href,
1940
+ framePath: payload.framePath || [],
1941
+ targetWindow: resolveFrameWindow(payload.framePath || [])
1942
+ }];
1943
+ };
1944
+ const toResult = async (target) => {
1945
+ if (action === 'globals') {
1946
+ const keys = Object.keys(target.targetWindow).filter((key) => /(data|table|json|state|store)/i.test(key)).slice(0, 50);
1947
+ return { url: target.url, framePath: target.framePath, value: keys };
1948
+ }
1949
+ if (action === 'eval') {
1950
+ const serialized = serializeValue(target.targetWindow.eval(payload.expr), payload.maxBytes);
1951
+ return { url: target.url, framePath: target.framePath, value: serialized.value, bytes: serialized.bytes };
1952
+ }
1953
+ if (action === 'extract') {
1954
+ const serialized = serializeValue(readPath(target.targetWindow, payload.path), payload.maxBytes);
1955
+ return { url: target.url, framePath: target.framePath, value: serialized.value, bytes: serialized.bytes };
1956
+ }
1957
+ if (action === 'fetch') {
1958
+ const headers = { ...(payload.headers || {}) };
1959
+ if (payload.contentType && !headers['Content-Type']) {
1960
+ headers['Content-Type'] = payload.contentType;
1961
+ }
1962
+ const controller = typeof AbortController === 'function' ? new AbortController() : null;
1963
+ const timeoutId =
1964
+ controller && typeof payload.timeoutMs === 'number' && payload.timeoutMs > 0
1965
+ ? window.setTimeout(() => controller.abort(new Error('fetch timeout')), payload.timeoutMs)
1966
+ : null;
1967
+ let response;
1968
+ try {
1969
+ response = await target.targetWindow.fetch(payload.url, {
1970
+ method: payload.method || 'GET',
1971
+ headers,
1972
+ body: typeof payload.body === 'string' ? payload.body : undefined,
1973
+ signal: controller ? controller.signal : undefined
1974
+ });
1975
+ } finally {
1976
+ if (timeoutId !== null) {
1977
+ window.clearTimeout(timeoutId);
1978
+ }
1979
+ }
1980
+ const headerMap = {};
1981
+ response.headers.forEach((value, key) => {
1982
+ headerMap[key] = value;
1983
+ });
1984
+ const bodyText = await response.text();
1985
+ const encoder = typeof TextEncoder === 'function' ? new TextEncoder() : null;
1986
+ const decoder = typeof TextDecoder === 'function' ? new TextDecoder() : null;
1987
+ const previewLimit = typeof payload.maxBytes === 'number' && payload.maxBytes > 0 ? payload.maxBytes : 8192;
1988
+ const encodedBody = encoder ? encoder.encode(bodyText) : null;
1989
+ const bodyBytes = encodedBody ? encodedBody.byteLength : bodyText.length;
1990
+ const truncated = bodyBytes > previewLimit;
1991
+ const finalBodyText =
1992
+ encodedBody && decoder
1993
+ ? decoder.decode(encodedBody.subarray(0, Math.min(encodedBody.byteLength, previewLimit)))
1994
+ : truncated
1995
+ ? bodyText.slice(0, previewLimit)
1996
+ : bodyText;
1997
+ if (payload.mode === 'json' && truncated) {
1998
+ throw {
1999
+ code: 'E_BODY_TOO_LARGE',
2000
+ message: 'JSON response exceeds max-bytes',
2001
+ details: {
2002
+ bytes: bodyBytes,
2003
+ maxBytes: previewLimit
2004
+ }
2005
+ };
2006
+ }
2007
+ const result = {
2008
+ url: response.url,
2009
+ status: response.status,
2010
+ ok: response.ok,
2011
+ headers: headerMap,
2012
+ contentType: response.headers.get('content-type') || undefined,
2013
+ bodyText: payload.mode === 'json' ? undefined : finalBodyText,
2014
+ json: payload.mode === 'json' && bodyText ? JSON.parse(bodyText) : undefined,
2015
+ bytes: bodyBytes,
2016
+ truncated
2017
+ };
2018
+ return { url: target.url, framePath: target.framePath, value: result };
2019
+ }
2020
+ throw { code: 'E_NOT_FOUND', message: 'Unsupported page-world action: ' + action };
2021
+ };
2022
+ Promise.resolve()
2023
+ .then(async () => {
2024
+ const targets = buildScopeTargets();
2025
+ const results = [];
2026
+ for (const target of targets) {
2027
+ try {
2028
+ results.push(await toResult(target));
2029
+ } catch (error) {
2030
+ results.push({
2031
+ url: target.url,
2032
+ framePath: target.framePath,
2033
+ error: typeof error === 'object' && error !== null && 'code' in error
2034
+ ? error
2035
+ : { code: 'E_EXECUTION', message: error instanceof Error ? error.message : String(error) }
2036
+ });
2037
+ }
2038
+ }
2039
+ if (payload.scope === 'all-frames') {
2040
+ emit({ ok: true, result: { scope: payload.scope, results } });
2041
+ return;
2042
+ }
2043
+ emit({ ok: true, result: { scope: payload.scope || 'current', result: results[0] } });
2044
+ })
2045
+ .catch((error) => {
2046
+ emit({
2047
+ ok: false,
2048
+ error: typeof error === 'object' && error !== null && 'code' in error
2049
+ ? error
2050
+ : { code: 'E_EXECUTION', message: error instanceof Error ? error.message : String(error) }
2051
+ });
2052
+ });
2053
+ })();
2054
+ `;
2055
+ (document.documentElement ?? document.head ?? document.body).appendChild(injector);
2056
+ injector.remove();
2057
+ });
2058
+ }
2059
+ function resolveScope(params) {
2060
+ const scope = typeof params.scope === "string" ? params.scope : "current";
2061
+ return scope === "main" || scope === "all-frames" ? scope : "current";
2062
+ }
2063
+ function pageWorldFramePath() {
2064
+ return [...contextState.framePath];
2065
+ }
2066
+ function pageEval(expr, params) {
2067
+ return runPageWorldRequest("eval", {
2068
+ expr,
2069
+ scope: resolveScope(params),
2070
+ maxBytes: typeof params.maxBytes === "number" ? params.maxBytes : void 0,
2071
+ framePath: pageWorldFramePath()
2072
+ });
2073
+ }
2074
+ function pageExtract(path, params) {
2075
+ return runPageWorldRequest("extract", {
2076
+ path,
2077
+ scope: resolveScope(params),
2078
+ maxBytes: typeof params.maxBytes === "number" ? params.maxBytes : void 0,
2079
+ framePath: pageWorldFramePath()
2080
+ });
2081
+ }
2082
+ function pageFetch(params) {
2083
+ return runPageWorldRequest("fetch", {
2084
+ url: String(params.url ?? ""),
2085
+ method: typeof params.method === "string" ? params.method : "GET",
2086
+ headers: typeof params.headers === "object" && params.headers !== null ? params.headers : void 0,
2087
+ body: typeof params.body === "string" ? params.body : void 0,
2088
+ contentType: typeof params.contentType === "string" ? params.contentType : void 0,
2089
+ mode: params.mode === "json" ? "json" : "raw",
2090
+ maxBytes: typeof params.maxBytes === "number" ? params.maxBytes : void 0,
2091
+ timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : void 0,
2092
+ scope: resolveScope(params),
2093
+ framePath: pageWorldFramePath()
2094
+ });
2095
+ }
2096
+ async function globalsPreview() {
2097
+ return Object.keys(window).filter((key) => /(data|table|json|state|store)/i.test(key)).slice(0, 50);
2098
+ }
2099
+ async function collectInspectionState(params = {}) {
2100
+ const rootResult = resolveRootForLocator();
2101
+ if (!rootResult.ok) {
2102
+ throw rootResult.error;
2103
+ }
2104
+ const root = rootResult.root;
2105
+ const metadata = documentMetadata(root, document);
2106
+ const visibleText = pageTextChunks(root, 40, 320);
2107
+ const combinedText = visibleText.map((chunk) => chunk.text).join("\n");
2108
+ const scripts = listInlineScripts();
2109
+ const visibleTimestamps = timestampCandidatesFromText(combinedText, Array.isArray(params.patterns) ? params.patterns.map(String) : void 0);
2110
+ const inlineTimestamps = timestampCandidatesFromText(
2111
+ scripts.map((script) => script.content).join("\n"),
2112
+ Array.isArray(params.patterns) ? params.patterns.map(String) : void 0
2113
+ );
2114
+ return {
2115
+ url: metadata.url,
2116
+ title: metadata.title,
2117
+ html: redactHtmlSnapshot(document.documentElement),
2118
+ visibleText,
2119
+ visibleTimestamps,
2120
+ inlineTimestamps,
2121
+ suspiciousGlobals: [...new Set(scripts.flatMap((script) => script.suspectedVars))].slice(0, 50),
2122
+ globalsPreview: await globalsPreview(),
2123
+ scripts: {
2124
+ inlineCount: scripts.length,
2125
+ suspectedDataVars: [...new Set(scripts.flatMap((script) => script.suspectedVars))].slice(0, 50)
2126
+ },
2127
+ storage: storageMetadata(),
2128
+ cookies: cookieMetadata(),
2129
+ frames: collectFrames(),
2130
+ tables: describeTables(),
2131
+ timers: {
2132
+ timeouts: 0,
2133
+ intervals: 0
2134
+ },
2135
+ pageLoadedAt,
2136
+ lastMutationAt,
2137
+ context: currentContextSnapshot()
2138
+ };
2139
+ }
1403
2140
  function performDoubleClick(target, point) {
1404
2141
  performClick(target, point);
1405
2142
  performClick(target, point);
@@ -1667,6 +2404,12 @@
1667
2404
  )
1668
2405
  };
1669
2406
  }
2407
+ case "page.eval":
2408
+ return await pageEval(String(params.expr ?? ""), params);
2409
+ case "page.extract":
2410
+ return await pageExtract(String(params.path ?? ""), params);
2411
+ case "page.fetch":
2412
+ return await pageFetch(params);
1670
2413
  case "page.dom": {
1671
2414
  const rootResult = resolveRootForLocator();
1672
2415
  if (!rootResult.ok) {
@@ -1818,6 +2561,7 @@
1818
2561
  if (!target) {
1819
2562
  throw notFoundForLocator(params.locator, "element.get target not found");
1820
2563
  }
2564
+ const matches = matchedElementsForLocator(params.locator);
1821
2565
  const elements = collectElements({}, params.locator);
1822
2566
  const eid = buildEid(target);
1823
2567
  const element = elements.find((item) => item.eid === eid);
@@ -1830,6 +2574,10 @@
1830
2574
  }
1831
2575
  return {
1832
2576
  element,
2577
+ matchedCount: matches.length,
2578
+ visible: isElementVisible(target),
2579
+ enabled: !(target.disabled || target.getAttribute("aria-disabled") === "true"),
2580
+ textPreview: (target.innerText || target.textContent || "").replace(/\s+/g, " ").trim().slice(0, 160),
1833
2581
  value: isEditable(target) ? target.value : target.isContentEditable ? target.textContent ?? "" : void 0,
1834
2582
  checked: isInputElement(target) ? target.checked : void 0,
1835
2583
  attributes
@@ -1962,6 +2710,35 @@
1962
2710
  dispatchInputEvents(target);
1963
2711
  return { ok: true, fileCount: transfer.files.length };
1964
2712
  }
2713
+ case "context.get":
2714
+ return {
2715
+ ok: true,
2716
+ frameDepth: contextState.framePath.length,
2717
+ shadowDepth: contextState.shadowPath.length,
2718
+ framePath: [...contextState.framePath],
2719
+ shadowPath: [...contextState.shadowPath]
2720
+ };
2721
+ case "context.set": {
2722
+ const framePath = Array.isArray(params.framePath) ? params.framePath.map(String) : [];
2723
+ const shadowPath = Array.isArray(params.shadowPath) ? params.shadowPath.map(String) : [];
2724
+ const frameResult = resolveFrameDocument(framePath);
2725
+ if (!frameResult.ok) {
2726
+ throw frameResult.error;
2727
+ }
2728
+ const shadowResult = resolveShadowRoot(frameResult.document, shadowPath);
2729
+ if (!shadowResult.ok) {
2730
+ throw shadowResult.error;
2731
+ }
2732
+ contextState.framePath = framePath;
2733
+ contextState.shadowPath = shadowPath;
2734
+ return {
2735
+ ok: true,
2736
+ frameDepth: contextState.framePath.length,
2737
+ shadowDepth: contextState.shadowPath.length,
2738
+ framePath: [...contextState.framePath],
2739
+ shadowPath: [...contextState.shadowPath]
2740
+ };
2741
+ }
1965
2742
  case "context.enterFrame": {
1966
2743
  if (params.reset === true) {
1967
2744
  contextState.framePath = [];
@@ -2034,7 +2811,7 @@
2034
2811
  if (!found) {
2035
2812
  throw { code: "E_NOT_FOUND", message: `network entry not found: ${id}` };
2036
2813
  }
2037
- return { entry: found };
2814
+ return { entry: filterNetworkEntrySections(found, params.include) };
2038
2815
  }
2039
2816
  case "network.waitFor":
2040
2817
  return { entry: await waitForNetwork(params) };
@@ -2042,6 +2819,34 @@
2042
2819
  networkEntries.length = 0;
2043
2820
  performanceBaselineMs = performance.now();
2044
2821
  return { ok: true };
2822
+ case "table.list":
2823
+ return { tables: describeTables() };
2824
+ case "table.schema": {
2825
+ const resolved = resolveTable(String(params.table ?? ""));
2826
+ if (!resolved || !(resolved.element instanceof Element)) {
2827
+ throw { code: "E_NOT_FOUND", message: `table not found: ${String(params.table ?? "")}` };
2828
+ }
2829
+ const schema = resolved.element instanceof HTMLTableElement ? { columns: htmlTableSchema(resolved.element) } : { columns: gridSchema(resolved.element) };
2830
+ return {
2831
+ table: resolved.table,
2832
+ schema
2833
+ };
2834
+ }
2835
+ case "table.rows":
2836
+ case "table.export": {
2837
+ const resolved = resolveTable(String(params.table ?? ""));
2838
+ if (!resolved || !(resolved.element instanceof Element)) {
2839
+ throw { code: "E_NOT_FOUND", message: `table not found: ${String(params.table ?? "")}` };
2840
+ }
2841
+ const requestedLimit = params.all === true ? typeof params.maxRows === "number" ? Math.max(1, Math.floor(params.maxRows)) : 1e4 : typeof params.limit === "number" ? Math.max(1, Math.floor(params.limit)) : 100;
2842
+ const extractionMode = resolved.table.kind === "html" || resolved.table.kind === "dataTables" ? "dataSource" : "visibleOnly";
2843
+ const rows = resolved.element instanceof HTMLTableElement ? htmlTableRows(resolved.element, requestedLimit) : gridRows(resolved.element, requestedLimit);
2844
+ return {
2845
+ table: resolved.table,
2846
+ extractionMode,
2847
+ rows
2848
+ };
2849
+ }
2045
2850
  case "debug.dumpState": {
2046
2851
  const consoleLimit = typeof params.consoleLimit === "number" ? Math.max(1, Math.floor(params.consoleLimit)) : 80;
2047
2852
  const networkLimit = typeof params.networkLimit === "number" ? Math.max(1, Math.floor(params.networkLimit)) : 80;
@@ -2051,6 +2856,7 @@
2051
2856
  throw rootResult.error;
2052
2857
  }
2053
2858
  const metadata = documentMetadata(rootResult.root, document);
2859
+ const inspection = await collectInspectionState(params);
2054
2860
  return {
2055
2861
  url: metadata.url,
2056
2862
  title: metadata.title,
@@ -2069,9 +2875,19 @@
2069
2875
  },
2070
2876
  console: consoleEntries.slice(-consoleLimit),
2071
2877
  network: filterNetworkEntries({ limit: networkLimit }),
2072
- accessibility: includeAccessibility ? pageAccessibility(rootResult.root, 200) : void 0
2878
+ accessibility: includeAccessibility ? pageAccessibility(rootResult.root, 200) : void 0,
2879
+ scripts: inspection.scripts,
2880
+ globalsPreview: inspection.globalsPreview,
2881
+ storage: inspection.storage,
2882
+ frames: inspection.frames,
2883
+ networkSummary: {
2884
+ total: filterNetworkEntries({ limit: networkLimit }).length,
2885
+ recent: filterNetworkEntries({ limit: Math.min(networkLimit, 10) })
2886
+ }
2073
2887
  };
2074
2888
  }
2889
+ case "bak.internal.inspectState":
2890
+ return await collectInspectionState(params);
2075
2891
  default:
2076
2892
  throw { code: "E_NOT_FOUND", message: `Unsupported content RPC method: ${method}` };
2077
2893
  }
@@ -2124,4 +2940,5 @@
2124
2940
  return false;
2125
2941
  });
2126
2942
  ensureOverlayRoot();
2943
+ trackMutations();
2127
2944
  })();