@flrande/bak-extension 0.5.0 → 0.6.1

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,9 @@
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();
239
+ var DISCOVERY_GLOBAL_PATTERN = /(data|table|json|state|store|market|quote|flow|row|timestamp|snapshot|book|signal)/i;
166
240
  function isHtmlElement(node) {
167
241
  if (!node) {
168
242
  return false;
@@ -710,6 +784,28 @@
710
784
  }
711
785
  return collected;
712
786
  }
787
+ function evaluateXPath(root, expression) {
788
+ try {
789
+ const ownerDocument = isDocumentNode(root) ? root : root.ownerDocument ?? document;
790
+ const iterator = ownerDocument.evaluate(
791
+ expression,
792
+ root,
793
+ null,
794
+ XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
795
+ null
796
+ );
797
+ const matches = [];
798
+ for (let index = 0; index < iterator.snapshotLength; index += 1) {
799
+ const node = iterator.snapshotItem(index);
800
+ if (isHtmlElement(node)) {
801
+ matches.push(node);
802
+ }
803
+ }
804
+ return matches;
805
+ } catch {
806
+ return [];
807
+ }
808
+ }
713
809
  function resolveFrameDocument(framePath) {
714
810
  let currentDocument = document;
715
811
  for (const selector of framePath) {
@@ -782,6 +878,7 @@
782
878
  const eid = buildEid(element);
783
879
  const selectors = {
784
880
  css: toCssSelector(element),
881
+ xpath: null,
785
882
  text: text || name ? (text || name).slice(0, 80) : null,
786
883
  aria: role && name ? `${role}:${name.slice(0, 80)}` : null
787
884
  };
@@ -793,6 +890,8 @@
793
890
  role,
794
891
  name,
795
892
  text,
893
+ visible: isElementVisible(element),
894
+ enabled: !(element.disabled || element.getAttribute("aria-disabled") === "true"),
796
895
  bbox: {
797
896
  x: rect.x,
798
897
  y: rect.y,
@@ -903,6 +1002,13 @@
903
1002
  currentRoot = found.shadowRoot;
904
1003
  }
905
1004
  }
1005
+ if (locator.xpath) {
1006
+ const matches = evaluateXPath(root, locator.xpath).filter((item) => isElementVisible(item));
1007
+ const found = indexedCandidate(matches, locator);
1008
+ if (found) {
1009
+ return found;
1010
+ }
1011
+ }
906
1012
  if (locator.name) {
907
1013
  const fallback = indexedCandidate(
908
1014
  interactive.filter((element) => inferName(element).toLowerCase().includes(locator.name.toLowerCase())),
@@ -914,6 +1020,61 @@
914
1020
  }
915
1021
  return null;
916
1022
  }
1023
+ function matchedElementsForLocator(locator) {
1024
+ if (!locator) {
1025
+ return [];
1026
+ }
1027
+ const rootResult = resolveRootForLocator(locator);
1028
+ if (!rootResult.ok) {
1029
+ return [];
1030
+ }
1031
+ const root = rootResult.root;
1032
+ const interactive = getInteractiveElements(root, locator.shadow !== "none");
1033
+ if (locator.css) {
1034
+ const parts = splitShadowSelector(locator.css);
1035
+ let currentRoot = root;
1036
+ for (let index = 0; index < parts.length; index += 1) {
1037
+ const selector = parts[index];
1038
+ const found = locator.shadow === "none" ? querySelectorInTree(currentRoot, selector) : querySelectorAcrossOpenShadow(currentRoot, selector);
1039
+ if (!found) {
1040
+ return [];
1041
+ }
1042
+ if (index === parts.length - 1) {
1043
+ return (locator.shadow === "none" ? Array.from(currentRoot.querySelectorAll(selector)) : querySelectorAllAcrossOpenShadow(currentRoot, selector)).filter((item) => isElementVisible(item));
1044
+ }
1045
+ if (!found.shadowRoot) {
1046
+ return [];
1047
+ }
1048
+ currentRoot = found.shadowRoot;
1049
+ }
1050
+ }
1051
+ if (locator.xpath) {
1052
+ return evaluateXPath(root, locator.xpath).filter((item) => isElementVisible(item));
1053
+ }
1054
+ if (locator.role || locator.name || locator.text) {
1055
+ return interactive.filter((element) => {
1056
+ if (locator.role && inferRole(element).toLowerCase() !== locator.role.toLowerCase()) {
1057
+ return false;
1058
+ }
1059
+ if (locator.name && !inferName(element).toLowerCase().includes(locator.name.toLowerCase())) {
1060
+ return false;
1061
+ }
1062
+ if (locator.text) {
1063
+ const text = (element.innerText || element.textContent || "").toLowerCase();
1064
+ if (!text.includes(locator.text.toLowerCase())) {
1065
+ return false;
1066
+ }
1067
+ }
1068
+ return true;
1069
+ });
1070
+ }
1071
+ if (locator.eid) {
1072
+ collectElements({}, locator);
1073
+ const fromCache = elementCache.get(locator.eid);
1074
+ return fromCache ? [fromCache] : [];
1075
+ }
1076
+ return [];
1077
+ }
917
1078
  function ensureOverlayRoot() {
918
1079
  let root = document.getElementById("bak-overlay-root");
919
1080
  if (!root) {
@@ -1387,6 +1548,30 @@
1387
1548
  return true;
1388
1549
  }).slice(-limit).reverse();
1389
1550
  }
1551
+ function filterNetworkEntrySections(entry, include) {
1552
+ if (!Array.isArray(include)) {
1553
+ return entry;
1554
+ }
1555
+ const sections = new Set(
1556
+ include.map(String).filter((section) => section === "request" || section === "response")
1557
+ );
1558
+ if (sections.size === 0 || sections.size === 2) {
1559
+ return entry;
1560
+ }
1561
+ const clone = { ...entry };
1562
+ if (!sections.has("request")) {
1563
+ delete clone.requestHeaders;
1564
+ delete clone.requestBodyPreview;
1565
+ delete clone.requestBodyTruncated;
1566
+ }
1567
+ if (!sections.has("response")) {
1568
+ delete clone.responseHeaders;
1569
+ delete clone.responseBodyPreview;
1570
+ delete clone.responseBodyTruncated;
1571
+ delete clone.binary;
1572
+ }
1573
+ return clone;
1574
+ }
1390
1575
  async function waitForNetwork(params) {
1391
1576
  const timeoutMs = typeof params.timeoutMs === "number" ? Math.max(1, params.timeoutMs) : 5e3;
1392
1577
  const sinceTs = typeof params.sinceTs === "number" ? params.sinceTs : Date.now() - timeoutMs;
@@ -1400,6 +1585,619 @@
1400
1585
  }
1401
1586
  throw { code: "E_TIMEOUT", message: "network.waitFor timeout" };
1402
1587
  }
1588
+ function trackMutations() {
1589
+ const observer = new MutationObserver(() => {
1590
+ lastMutationAt = Date.now();
1591
+ });
1592
+ const root = document.documentElement ?? document.body;
1593
+ if (!root) {
1594
+ return;
1595
+ }
1596
+ observer.observe(root, {
1597
+ childList: true,
1598
+ subtree: true,
1599
+ attributes: true,
1600
+ characterData: true
1601
+ });
1602
+ }
1603
+ function currentContextSnapshot() {
1604
+ return {
1605
+ tabId: null,
1606
+ framePath: [...contextState.framePath],
1607
+ shadowPath: [...contextState.shadowPath]
1608
+ };
1609
+ }
1610
+ function timestampCandidateMatchesFromText(text, patterns) {
1611
+ const regexes = (patterns ?? [
1612
+ String.raw`\b20\d{2}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?\b`,
1613
+ String.raw`\b20\d{2}-\d{2}-\d{2}\b`,
1614
+ String.raw`\b20\d{2}\/\d{2}\/\d{2}\b`
1615
+ ]).map((pattern) => new RegExp(pattern, "gi"));
1616
+ const collected = /* @__PURE__ */ new Map();
1617
+ for (const regex of regexes) {
1618
+ for (const match of text.matchAll(regex)) {
1619
+ const value = match[0];
1620
+ if (!value) {
1621
+ continue;
1622
+ }
1623
+ const index = match.index ?? text.indexOf(value);
1624
+ const start = Math.max(0, index - 28);
1625
+ const end = Math.min(text.length, index + value.length + 28);
1626
+ const context = text.slice(start, end).replace(/\s+/g, " ").trim();
1627
+ const key = `${value}::${context}`;
1628
+ if (!collected.has(key)) {
1629
+ collected.set(key, { value, context });
1630
+ }
1631
+ }
1632
+ }
1633
+ return [...collected.values()];
1634
+ }
1635
+ function listInlineScripts() {
1636
+ return Array.from(document.scripts).filter((script) => !script.src).map((script) => {
1637
+ const content = script.textContent ?? "";
1638
+ const suspectedVars = [...content.matchAll(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)/g)].map((match) => match[1] ?? "").filter((candidate) => DISCOVERY_GLOBAL_PATTERN.test(candidate));
1639
+ return { content, suspectedVars };
1640
+ });
1641
+ }
1642
+ function cookieMetadata() {
1643
+ return document.cookie.split(";").map((item) => item.trim()).filter(Boolean).map((item) => ({ name: item.split("=")[0] ?? "" })).filter((item) => item.name.length > 0);
1644
+ }
1645
+ function storageMetadata() {
1646
+ return {
1647
+ localStorageKeys: Object.keys(localStorage),
1648
+ sessionStorageKeys: Object.keys(sessionStorage)
1649
+ };
1650
+ }
1651
+ function collectFrames(baseDocument = document, framePath = []) {
1652
+ const frames = [];
1653
+ const frameNodes = Array.from(baseDocument.querySelectorAll("iframe,frame"));
1654
+ for (const frame of frameNodes) {
1655
+ if (!isFrameElement(frame)) {
1656
+ continue;
1657
+ }
1658
+ const selector = frame.id ? `#${frame.id}` : `${frame.tagName.toLowerCase()}:nth-of-type(${frameNodes.indexOf(frame) + 1})`;
1659
+ try {
1660
+ const childDocument = frame.contentDocument;
1661
+ if (!childDocument) {
1662
+ continue;
1663
+ }
1664
+ const nextPath = [...framePath, selector];
1665
+ frames.push({
1666
+ framePath: nextPath,
1667
+ url: childDocument.location.href
1668
+ });
1669
+ frames.push(...collectFrames(childDocument, nextPath));
1670
+ } catch {
1671
+ continue;
1672
+ }
1673
+ }
1674
+ return frames;
1675
+ }
1676
+ function buildTableId(kind, index) {
1677
+ return `${kind}:${index + 1}`;
1678
+ }
1679
+ function describeTables() {
1680
+ const rootResult = resolveRootForLocator();
1681
+ if (!rootResult.ok) {
1682
+ return [];
1683
+ }
1684
+ const root = rootResult.root;
1685
+ const tables = [];
1686
+ const htmlTables = Array.from(root.querySelectorAll("table"));
1687
+ for (const [index, table] of htmlTables.entries()) {
1688
+ tables.push({
1689
+ id: buildTableId(table.closest(".dataTables_wrapper") ? "dataTables" : "html", index),
1690
+ name: (table.getAttribute("aria-label") || table.getAttribute("data-testid") || table.id || `table-${index + 1}`).trim(),
1691
+ kind: table.closest(".dataTables_wrapper") ? "dataTables" : "html",
1692
+ selector: table.id ? `#${table.id}` : void 0,
1693
+ rowCount: table.querySelectorAll("tbody tr").length || table.querySelectorAll("tr").length,
1694
+ columnCount: table.querySelectorAll("thead th").length || table.querySelectorAll("tr:first-child th, tr:first-child td").length
1695
+ });
1696
+ }
1697
+ const gridRoots = Array.from(root.querySelectorAll('[role="grid"], [role="table"], .ag-root, .ag-root-wrapper'));
1698
+ for (const [index, grid] of gridRoots.entries()) {
1699
+ const kind = grid.className.includes("ag-") ? "ag-grid" : "aria-grid";
1700
+ tables.push({
1701
+ id: buildTableId(kind, index),
1702
+ name: (grid.getAttribute("aria-label") || grid.getAttribute("data-testid") || grid.id || `grid-${index + 1}`).trim(),
1703
+ kind,
1704
+ selector: grid.id ? `#${grid.id}` : void 0,
1705
+ rowCount: grid.querySelectorAll('[role="row"]').length,
1706
+ columnCount: grid.querySelectorAll('[role="columnheader"]').length
1707
+ });
1708
+ }
1709
+ return tables;
1710
+ }
1711
+ function resolveTable(handleId) {
1712
+ const tables = describeTables();
1713
+ const handle = tables.find((candidate) => candidate.id === handleId);
1714
+ if (!handle) {
1715
+ return null;
1716
+ }
1717
+ const rootResult = resolveRootForLocator();
1718
+ if (!rootResult.ok) {
1719
+ return null;
1720
+ }
1721
+ const root = rootResult.root;
1722
+ if (!handle.selector) {
1723
+ if (handle.kind === "html" || handle.kind === "dataTables") {
1724
+ const index2 = Number(handle.id.split(":")[1] ?? "1") - 1;
1725
+ return { table: handle, element: root.querySelectorAll("table")[index2] ?? null };
1726
+ }
1727
+ const candidates = root.querySelectorAll('[role="grid"], [role="table"], .ag-root, .ag-root-wrapper');
1728
+ const index = Number(handle.id.split(":")[1] ?? "1") - 1;
1729
+ return { table: handle, element: candidates[index] ?? null };
1730
+ }
1731
+ return { table: handle, element: root.querySelector(handle.selector) };
1732
+ }
1733
+ function htmlTableSchema(table) {
1734
+ const headers = Array.from(table.querySelectorAll("thead th"));
1735
+ if (headers.length > 0) {
1736
+ return headers.map((cell, index) => ({
1737
+ key: `col_${index + 1}`,
1738
+ label: (cell.textContent ?? "").trim() || `Column ${index + 1}`
1739
+ }));
1740
+ }
1741
+ const firstRow = table.querySelector("tr");
1742
+ return Array.from(firstRow?.querySelectorAll("th,td") ?? []).map((cell, index) => ({
1743
+ key: `col_${index + 1}`,
1744
+ label: (cell.textContent ?? "").trim() || `Column ${index + 1}`
1745
+ }));
1746
+ }
1747
+ function htmlTableRows(table, limit) {
1748
+ const columns = htmlTableSchema(table);
1749
+ const rowNodes = Array.from(table.querySelectorAll("tbody tr")).length > 0 ? Array.from(table.querySelectorAll("tbody tr")) : Array.from(table.querySelectorAll("tr")).slice(1);
1750
+ return rowNodes.slice(0, limit).map((row) => {
1751
+ const record = {};
1752
+ Array.from(row.querySelectorAll("th,td")).forEach((cell, index) => {
1753
+ const key = columns[index]?.label ?? `Column ${index + 1}`;
1754
+ record[key] = (cell.textContent ?? "").trim();
1755
+ });
1756
+ return record;
1757
+ });
1758
+ }
1759
+ function gridSchema(grid) {
1760
+ return Array.from(grid.querySelectorAll('[role="columnheader"]')).map((cell, index) => ({
1761
+ key: `col_${index + 1}`,
1762
+ label: (cell.textContent ?? "").trim() || `Column ${index + 1}`
1763
+ }));
1764
+ }
1765
+ function gridRows(grid, limit) {
1766
+ const columns = gridSchema(grid);
1767
+ return Array.from(grid.querySelectorAll('[role="row"]')).slice(0, limit).map((row) => {
1768
+ const record = {};
1769
+ Array.from(row.querySelectorAll('[role="gridcell"], [role="cell"], [role="columnheader"]')).forEach((cell, index) => {
1770
+ const key = columns[index]?.label ?? `Column ${index + 1}`;
1771
+ record[key] = (cell.textContent ?? "").trim();
1772
+ });
1773
+ return record;
1774
+ }).filter((row) => Object.values(row).some((value) => String(value).trim().length > 0));
1775
+ }
1776
+ function runPageWorldRequest(action, payload) {
1777
+ const requestId = `bak_page_world_${Date.now()}_${Math.random().toString(16).slice(2)}`;
1778
+ return new Promise((resolve, reject) => {
1779
+ const timeoutMs = typeof payload.timeoutMs === "number" && Number.isFinite(payload.timeoutMs) && payload.timeoutMs > 0 ? Math.max(1e3, Math.floor(payload.timeoutMs) + 1e3) : 15e3;
1780
+ const responseNode = document.createElement("div");
1781
+ responseNode.setAttribute("data-bak-page-world-response", requestId);
1782
+ responseNode.hidden = true;
1783
+ (document.documentElement ?? document.head ?? document.body).appendChild(responseNode);
1784
+ const cleanup = () => {
1785
+ clearTimeout(timer);
1786
+ observer.disconnect();
1787
+ responseNode.remove();
1788
+ };
1789
+ const tryResolveFromNode = () => {
1790
+ if (responseNode.getAttribute("data-ready") !== "1") {
1791
+ return;
1792
+ }
1793
+ const payloadText = responseNode.getAttribute("data-payload");
1794
+ const detail = payloadText ? JSON.parse(payloadText) : null;
1795
+ if (!detail) {
1796
+ cleanup();
1797
+ reject(failAction("E_EXECUTION", `${action} returned no detail`));
1798
+ return;
1799
+ }
1800
+ cleanup();
1801
+ if (detail.ok) {
1802
+ resolve(detail.result);
1803
+ return;
1804
+ }
1805
+ reject(detail.error ?? failAction("E_EXECUTION", `${action} failed`));
1806
+ };
1807
+ const observer = new MutationObserver(() => {
1808
+ try {
1809
+ tryResolveFromNode();
1810
+ } catch (error) {
1811
+ cleanup();
1812
+ reject(error instanceof Error ? error : new Error(String(error)));
1813
+ }
1814
+ });
1815
+ observer.observe(responseNode, {
1816
+ attributes: true,
1817
+ childList: true,
1818
+ characterData: true,
1819
+ subtree: true
1820
+ });
1821
+ const timer = window.setTimeout(() => {
1822
+ cleanup();
1823
+ reject(failAction("E_TIMEOUT", `${action} timed out`));
1824
+ }, timeoutMs);
1825
+ const injector = document.createElement("script");
1826
+ injector.textContent = `
1827
+ (() => {
1828
+ const requestId = ${JSON.stringify(requestId)};
1829
+ const action = ${JSON.stringify(action)};
1830
+ const payload = ${JSON.stringify(payload)};
1831
+ const responseNode = document.querySelector('div[data-bak-page-world-response="' + requestId + '"]');
1832
+ const emit = (detail) => {
1833
+ if (!responseNode) {
1834
+ return;
1835
+ }
1836
+ responseNode.setAttribute('data-payload', JSON.stringify(detail));
1837
+ responseNode.setAttribute('data-ready', '1');
1838
+ };
1839
+ const serializeValue = (value, maxBytes) => {
1840
+ let cloned;
1841
+ try {
1842
+ cloned = typeof structuredClone === 'function' ? structuredClone(value) : JSON.parse(JSON.stringify(value));
1843
+ } catch (error) {
1844
+ throw { code: 'E_NOT_SERIALIZABLE', message: error instanceof Error ? error.message : String(error) };
1845
+ }
1846
+ const json = JSON.stringify(cloned);
1847
+ if (typeof maxBytes === 'number' && maxBytes > 0 && json.length > maxBytes) {
1848
+ throw { code: 'E_BODY_TOO_LARGE', message: 'serialized value exceeds max-bytes', details: { bytes: json.length, maxBytes } };
1849
+ }
1850
+ return { value: cloned, bytes: json.length };
1851
+ };
1852
+ const resolveFrameWindow = (framePath) => {
1853
+ let currentWindow = window;
1854
+ let currentDocument = document;
1855
+ for (const selector of framePath || []) {
1856
+ const frame = currentDocument.querySelector(selector);
1857
+ if (!frame || !('contentWindow' in frame)) {
1858
+ throw { code: 'E_NOT_FOUND', message: 'frame not found: ' + selector };
1859
+ }
1860
+ const nextWindow = frame.contentWindow;
1861
+ if (!nextWindow) {
1862
+ throw { code: 'E_NOT_READY', message: 'frame window unavailable: ' + selector };
1863
+ }
1864
+ try {
1865
+ currentDocument = nextWindow.document;
1866
+ } catch {
1867
+ throw { code: 'E_CROSS_ORIGIN_BLOCKED', message: 'cross-origin frame is not accessible: ' + selector };
1868
+ }
1869
+ currentWindow = nextWindow;
1870
+ }
1871
+ return currentWindow;
1872
+ };
1873
+ const collectFrames = (rootWindow, framePath) => {
1874
+ const collected = [{
1875
+ url: rootWindow.location.href,
1876
+ framePath,
1877
+ targetWindow: rootWindow
1878
+ }];
1879
+ const frames = Array.from(rootWindow.document.querySelectorAll('iframe,frame'));
1880
+ frames.forEach((frame, index) => {
1881
+ const selector = frame.id ? '#' + frame.id : frame.tagName.toLowerCase() + ':nth-of-type(' + (index + 1) + ')';
1882
+ try {
1883
+ if (!frame.contentWindow || !frame.contentWindow.document) {
1884
+ return;
1885
+ }
1886
+ collected.push(...collectFrames(frame.contentWindow, [...framePath, selector]));
1887
+ } catch {
1888
+ // Skip cross-origin frames.
1889
+ }
1890
+ });
1891
+ return collected;
1892
+ };
1893
+ const parsePath = (path) => {
1894
+ if (typeof path !== 'string' || !path.trim()) {
1895
+ throw { code: 'E_INVALID_PARAMS', message: 'path is required' };
1896
+ }
1897
+ const normalized = path.replace(/^globalThis\\.?/, '').replace(/^window\\.?/, '').trim();
1898
+ if (!normalized) {
1899
+ return [];
1900
+ }
1901
+ const segments = [];
1902
+ let index = 0;
1903
+ while (index < normalized.length) {
1904
+ if (normalized[index] === '.') {
1905
+ index += 1;
1906
+ continue;
1907
+ }
1908
+ if (normalized[index] === '[') {
1909
+ const bracket = normalized.slice(index).match(/^\\[(\\d+)\\]/);
1910
+ if (!bracket) {
1911
+ throw { code: 'E_INVALID_PARAMS', message: 'Only numeric bracket paths are supported' };
1912
+ }
1913
+ segments.push(Number(bracket[1]));
1914
+ index += bracket[0].length;
1915
+ continue;
1916
+ }
1917
+ const identifier = normalized.slice(index).match(/^[A-Za-z_$][\\w$]*/);
1918
+ if (!identifier) {
1919
+ throw { code: 'E_INVALID_PARAMS', message: 'Unsupported path token near: ' + normalized.slice(index, index + 16) };
1920
+ }
1921
+ segments.push(identifier[0]);
1922
+ index += identifier[0].length;
1923
+ }
1924
+ return segments;
1925
+ };
1926
+ const buildPathExpression = (path) => {
1927
+ const segments = parsePath(path);
1928
+ return segments
1929
+ .map((segment, index) => {
1930
+ if (typeof segment === 'number') {
1931
+ return '[' + segment + ']';
1932
+ }
1933
+ if (index === 0) {
1934
+ return segment;
1935
+ }
1936
+ return '.' + segment;
1937
+ })
1938
+ .join('');
1939
+ };
1940
+ const readPath = (targetWindow, path) => {
1941
+ const segments = parsePath(path);
1942
+ let current = targetWindow;
1943
+ for (const segment of segments) {
1944
+ if (current == null || !(segment in current)) {
1945
+ throw { code: 'E_NOT_FOUND', message: 'path not found: ' + path };
1946
+ }
1947
+ current = current[segment];
1948
+ }
1949
+ return current;
1950
+ };
1951
+ const resolveExtractValue = (targetWindow, path, resolver) => {
1952
+ const strategy = resolver === 'globalThis' || resolver === 'lexical' ? resolver : 'auto';
1953
+ const lexicalExpression = buildPathExpression(path);
1954
+ const readLexical = () => {
1955
+ try {
1956
+ return targetWindow.eval(lexicalExpression);
1957
+ } catch (error) {
1958
+ if (error instanceof ReferenceError) {
1959
+ throw { code: 'E_NOT_FOUND', message: 'path not found: ' + path };
1960
+ }
1961
+ throw error;
1962
+ }
1963
+ };
1964
+ if (strategy === 'globalThis') {
1965
+ return { resolver: 'globalThis', value: readPath(targetWindow, path) };
1966
+ }
1967
+ if (strategy === 'lexical') {
1968
+ return { resolver: 'lexical', value: readLexical() };
1969
+ }
1970
+ try {
1971
+ return { resolver: 'globalThis', value: readPath(targetWindow, path) };
1972
+ } catch (error) {
1973
+ if (!error || typeof error !== 'object' || error.code !== 'E_NOT_FOUND') {
1974
+ throw error;
1975
+ }
1976
+ }
1977
+ return { resolver: 'lexical', value: readLexical() };
1978
+ };
1979
+ const buildScopeTargets = () => {
1980
+ if (payload.scope === 'main') {
1981
+ return [{
1982
+ url: window.location.href,
1983
+ framePath: [],
1984
+ targetWindow: window
1985
+ }];
1986
+ }
1987
+ if (payload.scope === 'all-frames') {
1988
+ return collectFrames(window, []);
1989
+ }
1990
+ return [{
1991
+ url: resolveFrameWindow(payload.framePath || []).location.href,
1992
+ framePath: payload.framePath || [],
1993
+ targetWindow: resolveFrameWindow(payload.framePath || [])
1994
+ }];
1995
+ };
1996
+ const toResult = async (target) => {
1997
+ if (action === 'globals') {
1998
+ const keys = Object.keys(target.targetWindow).filter((key) => ${DISCOVERY_GLOBAL_PATTERN}.test(key)).slice(0, 50);
1999
+ return { url: target.url, framePath: target.framePath, value: keys };
2000
+ }
2001
+ if (action === 'eval') {
2002
+ const serialized = serializeValue(target.targetWindow.eval(payload.expr), payload.maxBytes);
2003
+ return { url: target.url, framePath: target.framePath, value: serialized.value, bytes: serialized.bytes };
2004
+ }
2005
+ if (action === 'extract') {
2006
+ const extracted = resolveExtractValue(target.targetWindow, payload.path, payload.resolver);
2007
+ const serialized = serializeValue(extracted.value, payload.maxBytes);
2008
+ return { url: target.url, framePath: target.framePath, value: serialized.value, bytes: serialized.bytes, resolver: extracted.resolver };
2009
+ }
2010
+ if (action === 'fetch') {
2011
+ const headers = { ...(payload.headers || {}) };
2012
+ if (payload.contentType && !headers['Content-Type']) {
2013
+ headers['Content-Type'] = payload.contentType;
2014
+ }
2015
+ const controller = typeof AbortController === 'function' ? new AbortController() : null;
2016
+ const timeoutId =
2017
+ controller && typeof payload.timeoutMs === 'number' && payload.timeoutMs > 0
2018
+ ? window.setTimeout(() => controller.abort(new Error('fetch timeout')), payload.timeoutMs)
2019
+ : null;
2020
+ let response;
2021
+ try {
2022
+ response = await target.targetWindow.fetch(payload.url, {
2023
+ method: payload.method || 'GET',
2024
+ headers,
2025
+ body: typeof payload.body === 'string' ? payload.body : undefined,
2026
+ signal: controller ? controller.signal : undefined
2027
+ });
2028
+ } finally {
2029
+ if (timeoutId !== null) {
2030
+ window.clearTimeout(timeoutId);
2031
+ }
2032
+ }
2033
+ const headerMap = {};
2034
+ response.headers.forEach((value, key) => {
2035
+ headerMap[key] = value;
2036
+ });
2037
+ const bodyText = await response.text();
2038
+ const encoder = typeof TextEncoder === 'function' ? new TextEncoder() : null;
2039
+ const decoder = typeof TextDecoder === 'function' ? new TextDecoder() : null;
2040
+ const previewLimit = typeof payload.maxBytes === 'number' && payload.maxBytes > 0 ? payload.maxBytes : 8192;
2041
+ const encodedBody = encoder ? encoder.encode(bodyText) : null;
2042
+ const bodyBytes = encodedBody ? encodedBody.byteLength : bodyText.length;
2043
+ const truncated = bodyBytes > previewLimit;
2044
+ const finalBodyText =
2045
+ encodedBody && decoder
2046
+ ? decoder.decode(encodedBody.subarray(0, Math.min(encodedBody.byteLength, previewLimit)))
2047
+ : truncated
2048
+ ? bodyText.slice(0, previewLimit)
2049
+ : bodyText;
2050
+ if (payload.mode === 'json' && truncated) {
2051
+ throw {
2052
+ code: 'E_BODY_TOO_LARGE',
2053
+ message: 'JSON response exceeds max-bytes',
2054
+ details: {
2055
+ bytes: bodyBytes,
2056
+ maxBytes: previewLimit
2057
+ }
2058
+ };
2059
+ }
2060
+ const result = {
2061
+ url: response.url,
2062
+ status: response.status,
2063
+ ok: response.ok,
2064
+ headers: headerMap,
2065
+ contentType: response.headers.get('content-type') || undefined,
2066
+ bodyText: payload.mode === 'json' ? undefined : finalBodyText,
2067
+ json: payload.mode === 'json' && bodyText ? JSON.parse(bodyText) : undefined,
2068
+ bytes: bodyBytes,
2069
+ truncated
2070
+ };
2071
+ return { url: target.url, framePath: target.framePath, value: result };
2072
+ }
2073
+ throw { code: 'E_NOT_FOUND', message: 'Unsupported page-world action: ' + action };
2074
+ };
2075
+ Promise.resolve()
2076
+ .then(async () => {
2077
+ const targets = buildScopeTargets();
2078
+ const results = [];
2079
+ for (const target of targets) {
2080
+ try {
2081
+ results.push(await toResult(target));
2082
+ } catch (error) {
2083
+ results.push({
2084
+ url: target.url,
2085
+ framePath: target.framePath,
2086
+ error: typeof error === 'object' && error !== null && 'code' in error
2087
+ ? error
2088
+ : { code: 'E_EXECUTION', message: error instanceof Error ? error.message : String(error) }
2089
+ });
2090
+ }
2091
+ }
2092
+ if (payload.scope === 'all-frames') {
2093
+ emit({ ok: true, result: { scope: payload.scope, results } });
2094
+ return;
2095
+ }
2096
+ emit({ ok: true, result: { scope: payload.scope || 'current', result: results[0] } });
2097
+ })
2098
+ .catch((error) => {
2099
+ emit({
2100
+ ok: false,
2101
+ error: typeof error === 'object' && error !== null && 'code' in error
2102
+ ? error
2103
+ : { code: 'E_EXECUTION', message: error instanceof Error ? error.message : String(error) }
2104
+ });
2105
+ });
2106
+ })();
2107
+ `;
2108
+ (document.documentElement ?? document.head ?? document.body).appendChild(injector);
2109
+ injector.remove();
2110
+ });
2111
+ }
2112
+ function resolveScope(params) {
2113
+ const scope = typeof params.scope === "string" ? params.scope : "current";
2114
+ return scope === "main" || scope === "all-frames" ? scope : "current";
2115
+ }
2116
+ function pageWorldFramePath() {
2117
+ return [...contextState.framePath];
2118
+ }
2119
+ function pageEval(expr, params) {
2120
+ return runPageWorldRequest("eval", {
2121
+ expr,
2122
+ scope: resolveScope(params),
2123
+ maxBytes: typeof params.maxBytes === "number" ? params.maxBytes : void 0,
2124
+ framePath: pageWorldFramePath()
2125
+ });
2126
+ }
2127
+ function pageExtract(path, params) {
2128
+ return runPageWorldRequest("extract", {
2129
+ path,
2130
+ resolver: typeof params.resolver === "string" ? params.resolver : void 0,
2131
+ scope: resolveScope(params),
2132
+ maxBytes: typeof params.maxBytes === "number" ? params.maxBytes : void 0,
2133
+ framePath: pageWorldFramePath()
2134
+ });
2135
+ }
2136
+ function pageFetch(params) {
2137
+ return runPageWorldRequest("fetch", {
2138
+ url: String(params.url ?? ""),
2139
+ method: typeof params.method === "string" ? params.method : "GET",
2140
+ headers: typeof params.headers === "object" && params.headers !== null ? params.headers : void 0,
2141
+ body: typeof params.body === "string" ? params.body : void 0,
2142
+ contentType: typeof params.contentType === "string" ? params.contentType : void 0,
2143
+ mode: params.mode === "json" ? "json" : "raw",
2144
+ maxBytes: typeof params.maxBytes === "number" ? params.maxBytes : void 0,
2145
+ timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : void 0,
2146
+ scope: resolveScope(params),
2147
+ framePath: pageWorldFramePath()
2148
+ });
2149
+ }
2150
+ async function globalsPreview() {
2151
+ return Object.keys(window).filter((key) => DISCOVERY_GLOBAL_PATTERN.test(key)).slice(0, 50);
2152
+ }
2153
+ async function collectInspectionState(params = {}) {
2154
+ const rootResult = resolveRootForLocator();
2155
+ if (!rootResult.ok) {
2156
+ throw rootResult.error;
2157
+ }
2158
+ const root = rootResult.root;
2159
+ const metadata = documentMetadata(root, document);
2160
+ const visibleText = pageTextChunks(root, 40, 320);
2161
+ const combinedText = visibleText.map((chunk) => chunk.text).join("\n");
2162
+ const scripts = listInlineScripts();
2163
+ const visibleTimestampCandidates = timestampCandidateMatchesFromText(
2164
+ combinedText,
2165
+ Array.isArray(params.patterns) ? params.patterns.map(String) : void 0
2166
+ );
2167
+ const inlineTimestampCandidates = timestampCandidateMatchesFromText(
2168
+ scripts.map((script) => script.content).join("\n"),
2169
+ Array.isArray(params.patterns) ? params.patterns.map(String) : void 0
2170
+ );
2171
+ const previewGlobals = await globalsPreview();
2172
+ const suspiciousGlobals = [...new Set(scripts.flatMap((script) => script.suspectedVars))].slice(0, 50);
2173
+ return {
2174
+ url: metadata.url,
2175
+ title: metadata.title,
2176
+ html: redactHtmlSnapshot(document.documentElement),
2177
+ visibleText,
2178
+ visibleTimestamps: visibleTimestampCandidates.map((candidate) => candidate.value),
2179
+ visibleTimestampCandidates,
2180
+ inlineTimestamps: inlineTimestampCandidates.map((candidate) => candidate.value),
2181
+ inlineTimestampCandidates,
2182
+ suspiciousGlobals,
2183
+ globalsPreview: previewGlobals,
2184
+ scripts: {
2185
+ inlineCount: scripts.length,
2186
+ suspectedDataVars: suspiciousGlobals
2187
+ },
2188
+ storage: storageMetadata(),
2189
+ cookies: cookieMetadata(),
2190
+ frames: collectFrames(),
2191
+ tables: describeTables(),
2192
+ timers: {
2193
+ timeouts: 0,
2194
+ intervals: 0
2195
+ },
2196
+ pageLoadedAt,
2197
+ lastMutationAt,
2198
+ context: currentContextSnapshot()
2199
+ };
2200
+ }
1403
2201
  function performDoubleClick(target, point) {
1404
2202
  performClick(target, point);
1405
2203
  performClick(target, point);
@@ -1667,6 +2465,12 @@
1667
2465
  )
1668
2466
  };
1669
2467
  }
2468
+ case "page.eval":
2469
+ return await pageEval(String(params.expr ?? ""), params);
2470
+ case "page.extract":
2471
+ return await pageExtract(String(params.path ?? ""), params);
2472
+ case "page.fetch":
2473
+ return await pageFetch(params);
1670
2474
  case "page.dom": {
1671
2475
  const rootResult = resolveRootForLocator();
1672
2476
  if (!rootResult.ok) {
@@ -1818,6 +2622,7 @@
1818
2622
  if (!target) {
1819
2623
  throw notFoundForLocator(params.locator, "element.get target not found");
1820
2624
  }
2625
+ const matches = matchedElementsForLocator(params.locator);
1821
2626
  const elements = collectElements({}, params.locator);
1822
2627
  const eid = buildEid(target);
1823
2628
  const element = elements.find((item) => item.eid === eid);
@@ -1830,6 +2635,10 @@
1830
2635
  }
1831
2636
  return {
1832
2637
  element,
2638
+ matchedCount: matches.length,
2639
+ visible: isElementVisible(target),
2640
+ enabled: !(target.disabled || target.getAttribute("aria-disabled") === "true"),
2641
+ textPreview: (target.innerText || target.textContent || "").replace(/\s+/g, " ").trim().slice(0, 160),
1833
2642
  value: isEditable(target) ? target.value : target.isContentEditable ? target.textContent ?? "" : void 0,
1834
2643
  checked: isInputElement(target) ? target.checked : void 0,
1835
2644
  attributes
@@ -2063,7 +2872,7 @@
2063
2872
  if (!found) {
2064
2873
  throw { code: "E_NOT_FOUND", message: `network entry not found: ${id}` };
2065
2874
  }
2066
- return { entry: found };
2875
+ return { entry: filterNetworkEntrySections(found, params.include) };
2067
2876
  }
2068
2877
  case "network.waitFor":
2069
2878
  return { entry: await waitForNetwork(params) };
@@ -2071,6 +2880,34 @@
2071
2880
  networkEntries.length = 0;
2072
2881
  performanceBaselineMs = performance.now();
2073
2882
  return { ok: true };
2883
+ case "table.list":
2884
+ return { tables: describeTables() };
2885
+ case "table.schema": {
2886
+ const resolved = resolveTable(String(params.table ?? ""));
2887
+ if (!resolved || !(resolved.element instanceof Element)) {
2888
+ throw { code: "E_NOT_FOUND", message: `table not found: ${String(params.table ?? "")}` };
2889
+ }
2890
+ const schema = resolved.element instanceof HTMLTableElement ? { columns: htmlTableSchema(resolved.element) } : { columns: gridSchema(resolved.element) };
2891
+ return {
2892
+ table: resolved.table,
2893
+ schema
2894
+ };
2895
+ }
2896
+ case "table.rows":
2897
+ case "table.export": {
2898
+ const resolved = resolveTable(String(params.table ?? ""));
2899
+ if (!resolved || !(resolved.element instanceof Element)) {
2900
+ throw { code: "E_NOT_FOUND", message: `table not found: ${String(params.table ?? "")}` };
2901
+ }
2902
+ 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;
2903
+ const extractionMode = resolved.table.kind === "html" || resolved.table.kind === "dataTables" ? "dataSource" : "visibleOnly";
2904
+ const rows = resolved.element instanceof HTMLTableElement ? htmlTableRows(resolved.element, requestedLimit) : gridRows(resolved.element, requestedLimit);
2905
+ return {
2906
+ table: resolved.table,
2907
+ extractionMode,
2908
+ rows
2909
+ };
2910
+ }
2074
2911
  case "debug.dumpState": {
2075
2912
  const consoleLimit = typeof params.consoleLimit === "number" ? Math.max(1, Math.floor(params.consoleLimit)) : 80;
2076
2913
  const networkLimit = typeof params.networkLimit === "number" ? Math.max(1, Math.floor(params.networkLimit)) : 80;
@@ -2080,6 +2917,7 @@
2080
2917
  throw rootResult.error;
2081
2918
  }
2082
2919
  const metadata = documentMetadata(rootResult.root, document);
2920
+ const inspection = await collectInspectionState(params);
2083
2921
  return {
2084
2922
  url: metadata.url,
2085
2923
  title: metadata.title,
@@ -2098,9 +2936,19 @@
2098
2936
  },
2099
2937
  console: consoleEntries.slice(-consoleLimit),
2100
2938
  network: filterNetworkEntries({ limit: networkLimit }),
2101
- accessibility: includeAccessibility ? pageAccessibility(rootResult.root, 200) : void 0
2939
+ accessibility: includeAccessibility ? pageAccessibility(rootResult.root, 200) : void 0,
2940
+ scripts: inspection.scripts,
2941
+ globalsPreview: inspection.globalsPreview,
2942
+ storage: inspection.storage,
2943
+ frames: inspection.frames,
2944
+ networkSummary: {
2945
+ total: filterNetworkEntries({ limit: networkLimit }).length,
2946
+ recent: filterNetworkEntries({ limit: Math.min(networkLimit, 10) })
2947
+ }
2102
2948
  };
2103
2949
  }
2950
+ case "bak.internal.inspectState":
2951
+ return await collectInspectionState(params);
2104
2952
  default:
2105
2953
  throw { code: "E_NOT_FOUND", message: `Unsupported content RPC method: ${method}` };
2106
2954
  }
@@ -2153,4 +3001,5 @@
2153
3001
  return false;
2154
3002
  });
2155
3003
  ensureOverlayRoot();
3004
+ trackMutations();
2156
3005
  })();