@mehmoodqureshi/chrome-mcp 0.6.7 → 0.7.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.
Files changed (36) hide show
  1. package/README.md +116 -0
  2. package/dist/shared/observers.d.ts +110 -0
  3. package/dist/shared/observers.js +127 -0
  4. package/dist/shared/page-fns.d.ts +46 -0
  5. package/dist/shared/page-fns.js +292 -0
  6. package/dist/shared/policy.d.ts +9 -0
  7. package/dist/shared/policy.js +16 -0
  8. package/dist/shared/protocol.d.ts +11 -2
  9. package/dist/shared/protocol.js +3 -0
  10. package/dist/shared/snapshot.d.ts +2 -0
  11. package/dist/shared/snapshot.js +8 -1
  12. package/dist/src/bridge/workspace.d.ts +6 -0
  13. package/dist/src/bridge/workspace.js +20 -0
  14. package/dist/src/config.js +31 -0
  15. package/dist/src/executor/extension-executor.d.ts +27 -13
  16. package/dist/src/executor/extension-executor.js +53 -12
  17. package/dist/src/executor/stub-executor.d.ts +19 -1
  18. package/dist/src/executor/stub-executor.js +22 -6
  19. package/dist/src/executor/types.d.ts +72 -13
  20. package/dist/src/mcp/audit.d.ts +39 -0
  21. package/dist/src/mcp/audit.js +60 -0
  22. package/dist/src/mcp/helpers.d.ts +4 -4
  23. package/dist/src/mcp/helpers.js +9 -4
  24. package/dist/src/mcp/locate.d.ts +45 -0
  25. package/dist/src/mcp/locate.js +105 -0
  26. package/dist/src/mcp/redact.d.ts +48 -0
  27. package/dist/src/mcp/redact.js +92 -0
  28. package/dist/src/mcp/snapdiff.d.ts +43 -0
  29. package/dist/src/mcp/snapdiff.js +92 -0
  30. package/dist/src/mcp/tools.js +429 -43
  31. package/dist/src/security/policy.d.ts +5 -0
  32. package/dist/src/security/policy.js +6 -0
  33. package/docs/BLUEPRINT.md +15 -1
  34. package/extension-dist/background.js +617 -214
  35. package/extension-dist/page-hook.js +215 -0
  36. package/package.json +1 -1
@@ -0,0 +1,292 @@
1
+ "use strict";
2
+ /**
3
+ * shared/page-fns.ts — the ONE function that runs inside a page for every
4
+ * DOM-touching command.
5
+ *
6
+ * MUST be self-contained: `chrome.scripting.executeScript` serializes it to
7
+ * source, so it may not close over anything from this module. That constraint is
8
+ * exactly why every op lives in one function instead of ten — the shared
9
+ * helpers below (`deepQuery` above all) can then be written once and are
10
+ * automatically used by every op.
11
+ *
12
+ * `deepQuery` is the reason this file exists. `snapshot` deliberately walks open
13
+ * shadow roots and stamps `data-mcp-ref` on what it finds there, but every
14
+ * action used to resolve that ref with a plain `document.querySelector`, which
15
+ * cannot cross a shadow boundary. So the snapshot advertised elements that no
16
+ * click could ever reach — a structural dead end on every web-component site.
17
+ * One resolver, used by every op, is what closes it.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.pageOp = pageOp;
21
+ /**
22
+ * Runs IN THE PAGE (or in one frame of it). Returns a plain JSON-able object;
23
+ * `found: false` means the selector matched nothing, which the caller renders as
24
+ * SELECTOR_NOT_FOUND. Never throws across the boundary — a page that blows up
25
+ * inside an op is reported as `{ ok: false, error }`.
26
+ */
27
+ function pageOp(a) {
28
+ // -- shared helpers (must stay INSIDE: this function is serialized alone) --
29
+ /** querySelector that descends into open shadow roots, breadth-ish first. */
30
+ const deepQuery = (sel) => {
31
+ const roots = [document];
32
+ const seen = new Set();
33
+ while (roots.length > 0) {
34
+ const root = roots.shift();
35
+ if (seen.has(root))
36
+ continue;
37
+ seen.add(root);
38
+ let hit = null;
39
+ try {
40
+ hit = root.querySelector(sel);
41
+ }
42
+ catch {
43
+ return null; // malformed selector — same answer for every root
44
+ }
45
+ if (hit)
46
+ return hit;
47
+ let hosts = [];
48
+ try {
49
+ hosts = Array.from(root.querySelectorAll('*'));
50
+ }
51
+ catch {
52
+ hosts = [];
53
+ }
54
+ for (const host of hosts) {
55
+ const sr = host.shadowRoot;
56
+ if (sr && !seen.has(sr))
57
+ roots.push(sr);
58
+ }
59
+ }
60
+ return null;
61
+ };
62
+ /**
63
+ * Where this frame sits inside the TOP document, so a CDP mouse event (which
64
+ * is dispatched in top-viewport coordinates) lands on an element that lives in
65
+ * an iframe. `window.frameElement` is readable only when the parent is
66
+ * same-origin, so a cross-origin ancestor yields `exact: false` and the caller
67
+ * falls back to a synthetic click rather than clicking the wrong pixel.
68
+ */
69
+ const frameOffset = () => {
70
+ let dx = 0;
71
+ let dy = 0;
72
+ let win = window;
73
+ try {
74
+ while (win.parent && win.parent !== win) {
75
+ const fe = win.frameElement;
76
+ if (!fe)
77
+ return { dx, dy, exact: false, scrollX: window.scrollX, scrollY: window.scrollY };
78
+ const r = fe.getBoundingClientRect();
79
+ dx += r.left;
80
+ dy += r.top;
81
+ win = win.parent;
82
+ }
83
+ }
84
+ catch {
85
+ return { dx, dy, exact: false, scrollX: window.scrollX, scrollY: window.scrollY };
86
+ }
87
+ return { dx, dy, exact: true, scrollX: win.scrollX, scrollY: win.scrollY };
88
+ };
89
+ const sel = typeof a.selector === 'string' && a.selector.length > 0 ? a.selector : null;
90
+ const el = sel ? deepQuery(sel) : null;
91
+ const missing = sel !== null && el === null;
92
+ /** Set a value the way React/Vue see it (they patch the instance setter). */
93
+ const setValue = (node, next) => {
94
+ const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), 'value')?.set;
95
+ if (setter)
96
+ setter.call(node, next);
97
+ else
98
+ node.value = next;
99
+ node.dispatchEvent(new Event('input', { bubbles: true }));
100
+ };
101
+ switch (a.op) {
102
+ case 'probe':
103
+ return { found: true, url: location.href, title: document.title };
104
+ case 'text': {
105
+ if (missing)
106
+ return { found: false };
107
+ const root = el ?? document.body;
108
+ return { found: true, text: root ? root.innerText ?? '' : '' };
109
+ }
110
+ case 'html': {
111
+ if (missing)
112
+ return { found: false };
113
+ const root = el ?? document.documentElement;
114
+ if (!root)
115
+ return { found: true, html: '' };
116
+ const outer = a.outer === true || !sel;
117
+ return { found: true, html: outer ? root.outerHTML : root.innerHTML };
118
+ }
119
+ case 'click': {
120
+ if (!el)
121
+ return { found: false };
122
+ el.scrollIntoView({ block: 'center' });
123
+ el.click();
124
+ return { found: true };
125
+ }
126
+ case 'type': {
127
+ const node = el;
128
+ if (!node)
129
+ return { found: false };
130
+ node.focus();
131
+ const next = (a.clear ? '' : node.value ?? '') + (a.text ?? '');
132
+ setValue(node, next);
133
+ node.dispatchEvent(new Event('change', { bubbles: true }));
134
+ return { found: true };
135
+ }
136
+ case 'focus': {
137
+ const node = el;
138
+ if (!node)
139
+ return { found: false };
140
+ node.focus();
141
+ if (a.clear)
142
+ setValue(node, '');
143
+ return { found: true };
144
+ }
145
+ case 'point': {
146
+ if (!el)
147
+ return { found: false };
148
+ el.scrollIntoView({ block: 'center', inline: 'center' });
149
+ const r = el.getBoundingClientRect();
150
+ const off = frameOffset();
151
+ return {
152
+ found: true,
153
+ x: r.left + r.width / 2 + off.dx,
154
+ y: r.top + r.height / 2 + off.dy,
155
+ // false => this frame's coordinates cannot be mapped to the top viewport.
156
+ exact: off.exact,
157
+ };
158
+ }
159
+ case 'hover': {
160
+ if (!el)
161
+ return { found: false };
162
+ el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
163
+ el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
164
+ return { found: true };
165
+ }
166
+ case 'select': {
167
+ const node = el;
168
+ if (!node || !node.options)
169
+ return { found: false };
170
+ const want = new Set(a.values ?? []);
171
+ let matched = false;
172
+ for (const opt of Array.from(node.options)) {
173
+ const on = want.has(opt.value) || want.has(opt.label) || want.has(opt.text);
174
+ opt.selected = on;
175
+ if (on)
176
+ matched = true;
177
+ }
178
+ node.dispatchEvent(new Event('input', { bubbles: true }));
179
+ node.dispatchEvent(new Event('change', { bubbles: true }));
180
+ return { found: true, matched };
181
+ }
182
+ case 'measure': {
183
+ const d = document.documentElement;
184
+ const dims = {
185
+ w: window.innerWidth,
186
+ h: window.innerHeight,
187
+ fullW: Math.max(d.scrollWidth, d.clientWidth),
188
+ fullH: Math.max(d.scrollHeight, d.clientHeight),
189
+ };
190
+ if (!sel)
191
+ return { found: true, dims, element: null, missing: false };
192
+ if (!el)
193
+ return { found: true, dims, element: null, missing: true };
194
+ el.scrollIntoView({ block: 'center', inline: 'center' });
195
+ const r = el.getBoundingClientRect();
196
+ const off = frameOffset();
197
+ // viewport rect + this frame's offset + the TOP document's scroll ->
198
+ // document coordinates of the page the screenshot actually captures.
199
+ return {
200
+ found: true,
201
+ dims,
202
+ element: {
203
+ x: r.left + off.dx + off.scrollX,
204
+ y: r.top + off.dy + off.scrollY,
205
+ w: r.width,
206
+ h: r.height,
207
+ },
208
+ missing: false,
209
+ exact: off.exact,
210
+ };
211
+ }
212
+ case 'scroll': {
213
+ if (el)
214
+ el.scrollIntoView({ block: 'center' });
215
+ else if (a.x != null || a.y != null)
216
+ window.scrollTo(a.x ?? 0, a.y ?? 0);
217
+ else
218
+ window.scrollBy(a.deltaX ?? 0, a.deltaY ?? 0);
219
+ return { found: !missing };
220
+ }
221
+ case 'storage': {
222
+ const store = a.session ? window.sessionStorage : window.localStorage;
223
+ const op = a.storageOp;
224
+ if (op === 'set') {
225
+ store.setItem(String(a.key), String(a.value ?? ''));
226
+ return { found: true, ok: true };
227
+ }
228
+ if (op === 'remove') {
229
+ store.removeItem(String(a.key));
230
+ return { found: true, ok: true };
231
+ }
232
+ if (op === 'clear') {
233
+ store.clear();
234
+ return { found: true, ok: true };
235
+ }
236
+ if (a.key)
237
+ return { found: true, ok: true, value: store.getItem(a.key) };
238
+ const entries = {};
239
+ for (let i = 0; i < store.length; i++) {
240
+ const k = store.key(i);
241
+ if (k)
242
+ entries[k] = store.getItem(k) ?? '';
243
+ }
244
+ return { found: true, ok: true, entries };
245
+ }
246
+ // -- the two polling ops: one injection that resolves in-page, rather than
247
+ // one executeScript round-trip per tick --
248
+ case 'waitSelector': {
249
+ const deadline = Date.now() + (a.timeoutMs ?? 5_000);
250
+ const every = a.interval ?? 120;
251
+ return new Promise((resolve) => {
252
+ const tick = () => {
253
+ if (sel && deepQuery(sel))
254
+ return resolve({ found: true });
255
+ if (Date.now() > deadline)
256
+ return resolve({ found: false });
257
+ setTimeout(tick, every);
258
+ };
259
+ tick();
260
+ });
261
+ }
262
+ case 'waitFor': {
263
+ const deadline = Date.now() + (a.timeoutMs ?? 30_000);
264
+ const every = a.interval ?? 150;
265
+ const want = typeof a.textContains === 'string' && a.textContains.length > 0 ? a.textContains : null;
266
+ const gone = a.gone === true;
267
+ return new Promise((resolve) => {
268
+ const hit = () => {
269
+ let present;
270
+ if (sel)
271
+ present = !!deepQuery(sel);
272
+ else if (want)
273
+ present = (document.body?.innerText ?? '').includes(want);
274
+ else
275
+ present = true;
276
+ return gone ? !present : present;
277
+ };
278
+ const tick = () => {
279
+ if (hit())
280
+ return resolve({ found: true, matched: true });
281
+ if (Date.now() > deadline)
282
+ return resolve({ found: true, matched: false });
283
+ setTimeout(tick, every);
284
+ };
285
+ tick();
286
+ });
287
+ }
288
+ default:
289
+ return { found: false, error: `unknown page op: ${String(a.op)}` };
290
+ }
291
+ }
292
+ //# sourceMappingURL=page-fns.js.map
@@ -17,6 +17,15 @@ export declare function isMutatingMethod(method: WireMethod): boolean;
17
17
  export declare function isUrlGated(method: WireMethod): boolean;
18
18
  /** Parse a host out of a URL string; '' if it has none (about:blank, data:, …). */
19
19
  export declare function hostOf(url: string): string;
20
+ /**
21
+ * Reduce an allowlist entry to the bare host it constrains. Users routinely paste
22
+ * a full URL ("https://example.com/app") or a "host:port/path" instead of a bare
23
+ * host; those forms would never equal a hostname and so silently match nothing.
24
+ * We strip the scheme, userinfo, port, and path/query/fragment, preserving a
25
+ * leading "*." wildcard and the two catch-all forms. Returns '' for a pattern
26
+ * that carries no host (which then matches nothing).
27
+ */
28
+ export declare function normalizeDomainPattern(pattern: string): string;
20
29
  export declare function isDomainAllowed(url: string, policy: WirePolicy): boolean;
21
30
  export type PolicyVerdict = {
22
31
  ok: true;
@@ -15,6 +15,7 @@ exports.isReadMethod = isReadMethod;
15
15
  exports.isMutatingMethod = isMutatingMethod;
16
16
  exports.isUrlGated = isUrlGated;
17
17
  exports.hostOf = hostOf;
18
+ exports.normalizeDomainPattern = normalizeDomainPattern;
18
19
  exports.isDomainAllowed = isDomainAllowed;
19
20
  exports.evaluatePolicy = evaluatePolicy;
20
21
  exports.blockedDomainMessage = blockedDomainMessage;
@@ -27,6 +28,12 @@ const READ_CONTENT = new Set([
27
28
  'get_html',
28
29
  'screenshot',
29
30
  'wait_for',
31
+ // A frame list leaks the URLs a page embeds; console/network buffers are the
32
+ // page's own traffic and error text; a PDF is a screenshot by another name.
33
+ // All three are page CONTENT and gate exactly like the reads above.
34
+ 'frames_list',
35
+ 'observers',
36
+ 'print_pdf',
30
37
  ]);
31
38
  /** Content-mutating actions — URL-gated AND mutation-gated. */
32
39
  const MUTATE_CONTENT = new Set([
@@ -132,6 +139,14 @@ function evaluatePolicy(url, method, policy) {
132
139
  if (method === 'download_file' && !policy.allowDownloads) {
133
140
  return { ok: false, reason: 'downloads are disabled. Pass --enable-downloads or set allowDownloads.' };
134
141
  }
142
+ if (method === 'observers' && !policy.allowObservers) {
143
+ return {
144
+ ok: false,
145
+ reason: 'the in-page observers (console_logs / network_log / dialogs) are disabled. They patch console, ' +
146
+ 'fetch, XMLHttpRequest and the dialog functions on every allowlisted page in your real browser, ' +
147
+ 'so they are opt-in: pass --enable-observers or set allowObservers.',
148
+ };
149
+ }
135
150
  if (method === 'upload_file' && !policy.allowUploads) {
136
151
  return {
137
152
  ok: false,
@@ -182,5 +197,6 @@ exports.DENY_ALL_WIRE_POLICY = {
182
197
  allowUploads: false,
183
198
  allowAllTabs: false,
184
199
  enableMutations: false,
200
+ allowObservers: false,
185
201
  };
186
202
  //# sourceMappingURL=policy.js.map
@@ -44,10 +44,10 @@ export declare const WIRE_CAP_TAB_URL: "tab-url";
44
44
  * `download_file` (privileged, executor-owned) and `ping_probe` (a short-deadline
45
45
  * responsiveness check used to detect a dead-but-not-yet-reconnected worker).
46
46
  */
47
- export type WireMethod = 'tabs_list' | 'tab_select' | 'tab_new' | 'tab_close' | 'navigate' | 'back' | 'forward' | 'reload' | 'click' | 'type' | 'press' | 'hover' | 'scroll' | 'screenshot' | 'get_text' | 'get_html' | 'snapshot' | 'select_option' | 'get_cookies' | 'storage' | 'eval' | 'wait_for' | 'download_file' | 'upload_file' | 'ping_probe';
47
+ export type WireMethod = 'tabs_list' | 'tab_select' | 'tab_new' | 'tab_close' | 'navigate' | 'back' | 'forward' | 'reload' | 'click' | 'type' | 'press' | 'hover' | 'scroll' | 'screenshot' | 'get_text' | 'get_html' | 'snapshot' | 'select_option' | 'get_cookies' | 'storage' | 'eval' | 'wait_for' | 'download_file' | 'upload_file' | 'frames_list' | 'observers' | 'print_pdf' | 'ping_probe';
48
48
  /** Runtime list of every WireMethod, for boot-time drift assertions on both ends. */
49
49
  export declare const WIRE_METHODS: readonly WireMethod[];
50
- export type ExecutorErrorCode = 'NO_TARGET' | 'TARGET_GONE' | 'DETACHED' | 'DEVTOOLS_OPEN' | 'SELECTOR_NOT_FOUND' | 'REF_EXPIRED' | 'EVAL_THREW' | 'TIMEOUT' | 'BAD_ARGS' | 'CDP_ERROR' | 'POLICY_DENIED' | 'DOWNLOAD_FAILED' | 'UPLOAD_FAILED' | 'UNKNOWN_METHOD';
50
+ export type ExecutorErrorCode = 'NO_TARGET' | 'TARGET_GONE' | 'DETACHED' | 'DEVTOOLS_OPEN' | 'SELECTOR_NOT_FOUND' | 'REF_EXPIRED' | 'EVAL_THREW' | 'TIMEOUT' | 'BAD_ARGS' | 'CDP_ERROR' | 'POLICY_DENIED' | 'DOWNLOAD_FAILED' | 'UPLOAD_FAILED' | 'FRAME_NOT_FOUND' | 'OBSERVERS_DISABLED' | 'UNKNOWN_METHOD';
51
51
  export interface BaseFrame {
52
52
  type: string;
53
53
  v: ProtocolVersion;
@@ -80,6 +80,15 @@ export interface WirePolicy {
80
80
  allowUploads: boolean;
81
81
  allowAllTabs: boolean;
82
82
  enableMutations: boolean;
83
+ /**
84
+ * Whether the in-page observer hook (console / network / dialog capture) may
85
+ * be installed. Off by default: it patches `console`, `fetch`,
86
+ * `XMLHttpRequest` and the dialog functions on every allowlisted page in the
87
+ * user's real browser, which is too invasive to turn on for someone silently.
88
+ * Optional so an older extension deserializing a newer welcome frame reads it
89
+ * as undefined -> falsy -> the safe answer.
90
+ */
91
+ allowObservers?: boolean;
83
92
  }
84
93
  export interface WelcomeFrame extends BaseFrame {
85
94
  type: 'welcome';
@@ -67,6 +67,9 @@ exports.WIRE_METHODS = [
67
67
  'wait_for',
68
68
  'download_file',
69
69
  'upload_file',
70
+ 'frames_list',
71
+ 'observers',
72
+ 'print_pdf',
70
73
  'ping_probe',
71
74
  ];
72
75
  //# sourceMappingURL=protocol.js.map
@@ -15,6 +15,8 @@ export interface RawSnapshotNode {
15
15
  value?: string;
16
16
  disabled?: boolean;
17
17
  checked?: boolean;
18
+ /** A password field. `value` is never populated for one. */
19
+ secret?: boolean;
18
20
  }
19
21
  export interface RawSnapshot {
20
22
  url: string;
@@ -140,8 +140,15 @@ function collectSnapshot(interactiveOnly = true, max = 200) {
140
140
  const ref = `e${++n}`;
141
141
  el.setAttribute('data-mcp-ref', ref);
142
142
  const node = { ref, role: roleOf(el), name: accName(el), tag: el.tagName.toLowerCase() };
143
+ // A password field's characters never leave the page. The node still
144
+ // appears (so the model can target it) and is flagged `secret`, but the
145
+ // value is not something a caller has a use for and every caller would
146
+ // otherwise get it by default.
147
+ const isSecret = el.tagName === 'INPUT' && el.type === 'password';
148
+ if (isSecret)
149
+ node.secret = true;
143
150
  const v = el.value;
144
- if (typeof v === 'string' && v)
151
+ if (!isSecret && typeof v === 'string' && v)
145
152
  node.value = v.slice(0, 200);
146
153
  if (el.disabled)
147
154
  node.disabled = true;
@@ -37,6 +37,12 @@ export declare function switchWorkspace(opts: {
37
37
  * Returns the path written, or null if persisted nowhere (no workspace / error).
38
38
  */
39
39
  export declare function saveResult(tool: string, ext: string, body: string): string | null;
40
+ /**
41
+ * Save a binary artifact (a PDF, today) into the active task's results dir.
42
+ * Same contract as `saveResult`: best-effort, returns the path or null, and a
43
+ * failure here never fails the tool call that produced the bytes.
44
+ */
45
+ export declare function saveBinary(tool: string, ext: string, bytes: Buffer): string | null;
40
46
  /** Save a screenshot PNG (base64) into the active task's `screenshots/`. */
41
47
  export declare function saveScreenshot(dataBase64: string): string | null;
42
48
  /**
@@ -19,6 +19,7 @@ exports.peekActiveWorkspace = peekActiveWorkspace;
19
19
  exports.resetActiveWorkspaceForTesting = resetActiveWorkspaceForTesting;
20
20
  exports.switchWorkspace = switchWorkspace;
21
21
  exports.saveResult = saveResult;
22
+ exports.saveBinary = saveBinary;
22
23
  exports.saveScreenshot = saveScreenshot;
23
24
  exports.captureDownload = captureDownload;
24
25
  exports.appendHistory = appendHistory;
@@ -103,6 +104,25 @@ function saveResult(tool, ext, body) {
103
104
  return null;
104
105
  }
105
106
  }
107
+ /**
108
+ * Save a binary artifact (a PDF, today) into the active task's results dir.
109
+ * Same contract as `saveResult`: best-effort, returns the path or null, and a
110
+ * failure here never fails the tool call that produced the bytes.
111
+ */
112
+ function saveBinary(tool, ext, bytes) {
113
+ const w = peekActiveWorkspace();
114
+ if (!w)
115
+ return null;
116
+ try {
117
+ const path = (0, node_path_1.join)(w.resultsDir, `${stem(tool)}.${ext}`);
118
+ (0, node_fs_1.writeFileSync)(path, bytes, { mode: 0o600 });
119
+ return path;
120
+ }
121
+ catch (err) {
122
+ logErr(`results save failed: ${err instanceof Error ? err.message : String(err)}`);
123
+ return null;
124
+ }
125
+ }
106
126
  /** Save a screenshot PNG (base64) into the active task's `screenshots/`. */
107
127
  function saveScreenshot(dataBase64) {
108
128
  const w = peekActiveWorkspace();
@@ -134,6 +134,18 @@ function parseArgs(argv) {
134
134
  case '--allow-all-tabs':
135
135
  policyFlags.allowAllTabs = true;
136
136
  break;
137
+ case '--enable-observers':
138
+ policyFlags.allowObservers = true;
139
+ break;
140
+ case '--redact':
141
+ policyFlags.redact = true;
142
+ break;
143
+ case '--redact-pattern':
144
+ // A custom pattern only means anything with redaction on, so asking for
145
+ // one turns it on rather than being silently ignored.
146
+ policyFlags.redact = true;
147
+ (policyFlags.redactPatterns ??= []).push(requireValue(argv[++i], '--redact-pattern'));
148
+ break;
137
149
  case '--cdp-fallback':
138
150
  cdpFallback = true;
139
151
  break;
@@ -177,6 +189,17 @@ function parseArgs(argv) {
177
189
  if (policy.allowUploads && !policy.uploadsDir) {
178
190
  throw new Error('--enable-uploads requires --uploads-dir <path> (uploads must be confined to a directory)');
179
191
  }
192
+ // Fail at startup, not on the first read, if a redaction pattern is malformed:
193
+ // a pattern the user believes is scrubbing secrets but that never compiled is
194
+ // the worst of both worlds.
195
+ for (const source of policy.redactPatterns ?? []) {
196
+ try {
197
+ new RegExp(source);
198
+ }
199
+ catch (err) {
200
+ throw new Error(`--redact-pattern ${JSON.stringify(source)} is not a valid regular expression: ${String(err)}`);
201
+ }
202
+ }
180
203
  return {
181
204
  wsPort,
182
205
  dataDir: resolveDataDir(),
@@ -269,6 +292,14 @@ Security (default: deny-all safe mode):
269
292
  --enable-uploads Enable upload_file — sends local files to a page (off by default)
270
293
  --uploads-dir <path> Restrict upload_file to files inside <path> (recommended with --enable-uploads)
271
294
  --allow-all-tabs Relax tab list/select to all tabs
295
+ --enable-observers Enable console_logs / network_log / dialogs. Installs an
296
+ in-page hook on allowlisted sites that records console
297
+ output, fetch/XHR traffic, and intercepts alert/confirm/
298
+ prompt (off by default: it patches page globals).
299
+ --redact Scrub secret-shaped strings (JWTs, cloud keys, bearer
300
+ tokens, private keys) out of page reads. Password field
301
+ values are always suppressed, with or without this.
302
+ --redact-pattern <re> Add a redaction regex (repeatable; implies --redact)
272
303
 
273
304
  Misc:
274
305
  --log-level <lvl> silent | info | debug (default info)
@@ -7,7 +7,7 @@
7
7
  * method-specific arguments travel in `params`. Results are trusted shapes
8
8
  * produced by the extension router (validated there).
9
9
  */
10
- import { type ActionOk, type BackendKind, type CookieItem, type DownloadResult, type EvalResult, type Executor, type ExecutorStatus, type KeyModifier, type MouseButton, type NavResult, type ScreenshotResult, type SnapshotResult, type StorageOp, type StorageResult, type TabId, type TabInfo, type Target, type WaitResult, type WaitUntil } from './types';
10
+ import { type ActionOk, type BackendKind, type CookieItem, type DownloadResult, type EvalResult, type Executor, type ExecutorStatus, type FrameInfo, type FrameOpts, type ObserverArgs, type ObserverReadResult, type PdfResult, type KeyModifier, type MouseButton, type NavResult, type ScreenshotResult, type SnapshotResult, type StorageOp, type StorageResult, type TabId, type TabInfo, type Target, type WaitResult, type WaitUntil } from './types';
11
11
  import type { BridgeServer } from '../bridge/server';
12
12
  export declare class ExtensionExecutor implements Executor {
13
13
  private readonly bridge;
@@ -49,27 +49,27 @@ export declare class ExtensionExecutor implements Executor {
49
49
  button?: MouseButton;
50
50
  clickCount?: number;
51
51
  trusted?: boolean;
52
- }): Promise<ActionOk>;
52
+ } & FrameOpts): Promise<ActionOk>;
53
53
  type(t: Target, text: string, opts?: {
54
54
  tabId?: TabId;
55
55
  clear?: boolean;
56
56
  pressEnter?: boolean;
57
57
  keyEvents?: boolean;
58
58
  trusted?: boolean;
59
- }): Promise<ActionOk>;
59
+ } & FrameOpts): Promise<ActionOk>;
60
60
  selectOption(t: Target, values: string[], opts?: {
61
61
  tabId?: TabId;
62
- }): Promise<ActionOk>;
62
+ } & FrameOpts): Promise<ActionOk>;
63
63
  fill(t: Target, value: string, opts?: {
64
64
  tabId?: TabId;
65
- }): Promise<ActionOk>;
65
+ } & FrameOpts): Promise<ActionOk>;
66
66
  press(key: string, opts?: {
67
67
  tabId?: TabId;
68
68
  modifiers?: KeyModifier[];
69
69
  }): Promise<ActionOk>;
70
70
  hover(t: Target, opts?: {
71
71
  tabId?: TabId;
72
- }): Promise<ActionOk>;
72
+ } & FrameOpts): Promise<ActionOk>;
73
73
  scroll(opts: {
74
74
  tabId?: TabId;
75
75
  x?: number;
@@ -77,24 +77,24 @@ export declare class ExtensionExecutor implements Executor {
77
77
  deltaX?: number;
78
78
  deltaY?: number;
79
79
  target?: Target;
80
- }): Promise<ActionOk>;
80
+ } & FrameOpts): Promise<ActionOk>;
81
81
  getText(t?: Target, opts?: {
82
82
  tabId?: TabId;
83
- }): Promise<{
83
+ } & FrameOpts): Promise<{
84
84
  text: string;
85
85
  ref?: string;
86
86
  }>;
87
87
  getHtml(t?: Target, opts?: {
88
88
  tabId?: TabId;
89
89
  outer?: boolean;
90
- }): Promise<{
90
+ } & FrameOpts): Promise<{
91
91
  html: string;
92
92
  }>;
93
93
  snapshot(opts?: {
94
94
  tabId?: TabId;
95
95
  interactiveOnly?: boolean;
96
96
  max?: number;
97
- }): Promise<SnapshotResult>;
97
+ } & FrameOpts): Promise<SnapshotResult>;
98
98
  getCookies(opts?: {
99
99
  tabId?: TabId;
100
100
  url?: string;
@@ -112,18 +112,18 @@ export declare class ExtensionExecutor implements Executor {
112
112
  tabId?: TabId;
113
113
  fullPage?: boolean;
114
114
  target?: Target;
115
- }): Promise<ScreenshotResult>;
115
+ } & FrameOpts): Promise<ScreenshotResult>;
116
116
  eval(expression: string, opts?: {
117
117
  tabId?: TabId;
118
118
  awaitPromise?: boolean;
119
- }): Promise<EvalResult>;
119
+ } & FrameOpts): Promise<EvalResult>;
120
120
  waitFor(opts: {
121
121
  tabId?: TabId;
122
122
  selector?: string;
123
123
  textContains?: string;
124
124
  gone?: boolean;
125
125
  timeoutMs?: number;
126
- }): Promise<WaitResult>;
126
+ } & FrameOpts): Promise<WaitResult>;
127
127
  download(args: {
128
128
  url?: string;
129
129
  target?: Target;
@@ -133,4 +133,18 @@ export declare class ExtensionExecutor implements Executor {
133
133
  uploadFile(t: Target, files: string[], opts?: {
134
134
  tabId?: TabId;
135
135
  }): Promise<ActionOk>;
136
+ framesList(opts?: {
137
+ tabId?: TabId;
138
+ }): Promise<FrameInfo[]>;
139
+ observers(args: ObserverArgs): Promise<ObserverReadResult>;
140
+ printPdf(opts?: {
141
+ tabId?: TabId;
142
+ landscape?: boolean;
143
+ printBackground?: boolean;
144
+ scale?: number;
145
+ paperWidth?: number;
146
+ paperHeight?: number;
147
+ pageRanges?: string;
148
+ preferCSSPageSize?: boolean;
149
+ }): Promise<PdfResult>;
136
150
  }