kiro-mobile-bridge 1.0.8 → 1.0.11

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.
@@ -1,27 +1,186 @@
1
1
  /**
2
2
  * Click service - handles UI element clicks via CDP
3
3
  */
4
+ import { MODEL_NAMES } from '../utils/constants.js';
4
5
 
5
6
  /**
6
- * Click an element in the Kiro UI via CDP
7
- * @param {CDPConnection} cdp - CDP connection
8
- * @param {object} clickInfo - Element identification info
9
- * @returns {Promise<{success: boolean, matchMethod?: string, error?: string}>}
7
+ * Build the click script for CDP evaluation
8
+ * This is separated for maintainability and testing
9
+ * @param {object} clickInfo - Element identification info (already sanitized)
10
+ * @returns {string} - JavaScript expression
10
11
  */
11
- export async function clickElement(cdp, clickInfo) {
12
- const script = `(function() {
12
+ function buildClickScript(clickInfo) {
13
+ // Escape the clickInfo for safe inclusion in the script
14
+ const safeClickInfo = JSON.stringify(clickInfo);
15
+ const modelNamesJson = JSON.stringify(MODEL_NAMES);
16
+
17
+ return `(function() {
13
18
  let targetDoc = document;
14
19
  const activeFrame = document.getElementById('active-frame');
15
20
  if (activeFrame && activeFrame.contentDocument) targetDoc = activeFrame.contentDocument;
16
21
 
17
- const info = ${JSON.stringify(clickInfo)};
22
+ const info = ${safeClickInfo};
23
+ const modelNames = ${modelNamesJson};
18
24
  let element = null;
19
25
  let matchMethod = '';
26
+
27
+ // Helper functions
28
+ const isVisible = (el) => el && el.offsetParent !== null;
29
+ const findModelName = (text) => {
30
+ const lowerText = text.toLowerCase();
31
+ for (const m of modelNames) {
32
+ if (lowerText.includes(m)) return m;
33
+ }
34
+ return null;
35
+ };
36
+
37
+ // Determine element type
20
38
  const isTabClick = info.isTab || info.role === 'tab';
21
39
  const isCloseButton = info.isCloseButton || (info.ariaLabel && info.ariaLabel.toLowerCase() === 'close');
22
40
  const isToggle = info.isToggle || info.role === 'switch';
41
+ const isModelSelector = info.isModelSelector;
42
+ const isModelOption = info.isModelOption;
43
+
44
+ // =========================================================================
45
+ // Model Selector Button Click
46
+ // =========================================================================
47
+ if (isModelSelector && !element) {
48
+ // Strategy 1: Find Kiro's specific dropdown trigger
49
+ const kiroDropdownTrigger = targetDoc.querySelector('button.kiro-dropdown-trigger[aria-haspopup="true"]');
50
+ if (kiroDropdownTrigger && isVisible(kiroDropdownTrigger)) {
51
+ const triggerText = (kiroDropdownTrigger.textContent || '').toLowerCase();
52
+ if (modelNames.some(m => triggerText.includes(m))) {
53
+ element = kiroDropdownTrigger;
54
+ matchMethod = 'kiro-dropdown-trigger';
55
+ }
56
+ }
57
+
58
+ // Strategy 2: Find by aria-haspopup with model text
59
+ if (!element) {
60
+ const hasPopupButtons = targetDoc.querySelectorAll('button[aria-haspopup="true"], button[aria-haspopup="listbox"], button[aria-haspopup="menu"]');
61
+ for (const btn of hasPopupButtons) {
62
+ if (!isVisible(btn)) continue;
63
+ const btnText = (btn.textContent || '').toLowerCase();
64
+ if (modelNames.some(m => btnText.includes(m))) {
65
+ element = btn;
66
+ matchMethod = 'aria-haspopup-model';
67
+ break;
68
+ }
69
+ }
70
+ }
71
+
72
+ // Strategy 3: Find by class patterns
73
+ if (!element) {
74
+ const modelSelectors = [
75
+ '.kiro-dropdown-trigger',
76
+ '[class*="model-selector"]', '[class*="modelSelector"]',
77
+ '[class*="model-dropdown"]', '[class*="modelDropdown"]',
78
+ 'button[class*="dropdown-trigger"]'
79
+ ];
80
+
81
+ for (const sel of modelSelectors) {
82
+ try {
83
+ const candidates = targetDoc.querySelectorAll(sel);
84
+ for (const c of candidates) {
85
+ if (isVisible(c)) {
86
+ element = c;
87
+ matchMethod = 'model-selector-class';
88
+ break;
89
+ }
90
+ }
91
+ if (element) break;
92
+ } catch(e) {}
93
+ }
94
+ }
95
+ }
23
96
 
24
- // Handle send button
97
+ // =========================================================================
98
+ // Model Option Selection (from dropdown menu)
99
+ // =========================================================================
100
+ if (isModelOption && !element) {
101
+ const searchText = (info.text || '').trim().toLowerCase();
102
+ const searchModelName = findModelName(searchText);
103
+
104
+ // Check if dropdown is currently open
105
+ const openDropdown = targetDoc.querySelector('.kiro-dropdown-menu, [class*="dropdown-menu"][class*="open"], [role="listbox"], [role="menu"]');
106
+ const isDropdownOpen = openDropdown && isVisible(openDropdown);
107
+
108
+ // If dropdown is NOT open, we need to open it first
109
+ if (!isDropdownOpen) {
110
+ const dropdownTrigger = targetDoc.querySelector('.kiro-dropdown-trigger[aria-haspopup="true"], button[aria-haspopup="true"], button[aria-haspopup="listbox"]');
111
+ if (dropdownTrigger && isVisible(dropdownTrigger)) {
112
+ const triggerText = (dropdownTrigger.textContent || '').toLowerCase();
113
+ if (modelNames.some(m => triggerText.includes(m))) {
114
+ dropdownTrigger.click();
115
+ return { found: true, clicked: true, needsRetry: true, matchMethod: 'dropdown-opened-for-option' };
116
+ }
117
+ }
118
+ }
119
+
120
+ // Strategy 1: Find Kiro's specific dropdown items
121
+ const kiroDropdownItems = targetDoc.querySelectorAll('.kiro-dropdown-item, .kiro-dropdown-menu > div');
122
+ for (const item of kiroDropdownItems) {
123
+ if (!isVisible(item)) continue;
124
+ const itemText = (item.textContent || '').trim().toLowerCase();
125
+ const itemModelName = findModelName(itemText);
126
+
127
+ if (searchModelName && itemModelName && searchModelName === itemModelName) {
128
+ const searchVersion = searchText.match(/\\d+\\.?\\d*/)?.[0];
129
+ const itemVersion = itemText.match(/\\d+\\.?\\d*/)?.[0];
130
+
131
+ if (!searchVersion || !itemVersion || searchVersion === itemVersion) {
132
+ element = item;
133
+ matchMethod = 'kiro-dropdown-item';
134
+ break;
135
+ }
136
+ }
137
+ }
138
+
139
+ // Strategy 2: Find by role selectors
140
+ if (!element) {
141
+ const optionSelectors = ['[role="option"]', '[role="menuitem"]', '[class*="dropdown-item"]', '[class*="menu-item"]'];
142
+
143
+ for (const sel of optionSelectors) {
144
+ try {
145
+ const options = targetDoc.querySelectorAll(sel);
146
+ for (const opt of options) {
147
+ if (!isVisible(opt)) continue;
148
+ const optText = (opt.textContent || '').trim().toLowerCase();
149
+ if (searchText && (optText.includes(searchText) || searchText.includes(optText.substring(0, 20)))) {
150
+ element = opt;
151
+ matchMethod = 'model-option-role';
152
+ break;
153
+ }
154
+ }
155
+ if (element) break;
156
+ } catch(e) {}
157
+ }
158
+ }
159
+
160
+ // Strategy 3: Find any clickable element in dropdown with matching model name
161
+ if (!element && searchText && searchModelName) {
162
+ const dropdownMenu = targetDoc.querySelector('.kiro-dropdown-menu, [class*="dropdown-menu"], [role="listbox"], [role="menu"]');
163
+ if (dropdownMenu) {
164
+ const allItems = dropdownMenu.querySelectorAll('*');
165
+ for (const item of allItems) {
166
+ if (item.children.length > 5) continue;
167
+ if (!isVisible(item)) continue;
168
+ const itemText = (item.textContent || '').trim().toLowerCase();
169
+ const itemModelName = findModelName(itemText);
170
+
171
+ if (itemModelName && searchModelName === itemModelName) {
172
+ element = item;
173
+ matchMethod = 'dropdown-menu-item';
174
+ break;
175
+ }
176
+ }
177
+ }
178
+ }
179
+ }
180
+
181
+ // =========================================================================
182
+ // Send Button
183
+ // =========================================================================
25
184
  if (info.isSendButton && !element) {
26
185
  const sendSelectors = ['button[data-variant="submit"]', 'svg.lucide-arrow-right', 'button[type="submit"]', 'button[aria-label*="send" i]'];
27
186
  for (const sel of sendSelectors) {
@@ -35,7 +194,9 @@ export async function clickElement(cdp, clickInfo) {
35
194
  }
36
195
  }
37
196
 
38
- // Handle toggle/switch
197
+ // =========================================================================
198
+ // Toggle/Switch
199
+ // =========================================================================
39
200
  if (isToggle && !element) {
40
201
  if (info.toggleId) {
41
202
  element = targetDoc.getElementById(info.toggleId);
@@ -54,7 +215,95 @@ export async function clickElement(cdp, clickInfo) {
54
215
  }
55
216
  }
56
217
 
57
- // Handle close button
218
+ // =========================================================================
219
+ // Notification Banner Buttons
220
+ // =========================================================================
221
+ if (info.isNotificationButton && !element) {
222
+ const searchText = (info.text || '').trim().toLowerCase();
223
+ const searchAriaLabel = (info.ariaLabel || '').trim().toLowerCase();
224
+
225
+ const snackbarButtons = targetDoc.querySelectorAll('.kiro-snackbar button, .kiro-snackbar-actions button, .kiro-snackbar-header button');
226
+
227
+ // Strategy 1: Find by text
228
+ for (const btn of snackbarButtons) {
229
+ if (!isVisible(btn)) continue;
230
+ const btnText = (btn.textContent || '').trim().toLowerCase();
231
+ if (searchText && btnText && (btnText.includes(searchText) || searchText.includes(btnText))) {
232
+ element = btn;
233
+ matchMethod = 'snackbar-button-text';
234
+ break;
235
+ }
236
+ }
237
+
238
+ // Strategy 2: Find X/close/dismiss icon button
239
+ if (!element && (searchText === 'close' || searchText === 'dismiss' || searchText === 'x' ||
240
+ searchText === 'icon-button' || searchText === '' || !searchText ||
241
+ searchAriaLabel.includes('close') || searchAriaLabel.includes('dismiss'))) {
242
+ for (const btn of snackbarButtons) {
243
+ if (!isVisible(btn)) continue;
244
+ const btnAriaLabel = (btn.getAttribute('aria-label') || '').toLowerCase();
245
+ const isExpandBtn = btnAriaLabel.includes('expand') || btn.classList.contains('kiro-snackbar-expand');
246
+
247
+ if (isExpandBtn) continue;
248
+
249
+ if (btnAriaLabel.includes('close') || btnAriaLabel.includes('dismiss') ||
250
+ btn.querySelector('.codicon-close, .codicon-x, [class*="close"]')) {
251
+ element = btn;
252
+ matchMethod = 'snackbar-dismiss-button';
253
+ break;
254
+ }
255
+
256
+ const isIconBtn = btn.classList.contains('kiro-icon-button');
257
+ const btnText = (btn.textContent || '').trim();
258
+ if (isIconBtn && !btnText && !isExpandBtn) {
259
+ element = btn;
260
+ matchMethod = 'snackbar-icon-dismiss';
261
+ break;
262
+ }
263
+ }
264
+ }
265
+
266
+ // Strategy 3: Find by aria-label
267
+ if (!element && searchAriaLabel && !searchAriaLabel.includes('expand')) {
268
+ for (const btn of snackbarButtons) {
269
+ if (!isVisible(btn)) continue;
270
+ const btnAriaLabel = (btn.getAttribute('aria-label') || '').toLowerCase();
271
+ if (btnAriaLabel.includes('expand')) continue;
272
+ if (btnAriaLabel && (btnAriaLabel.includes(searchAriaLabel) || searchAriaLabel.includes(btnAriaLabel))) {
273
+ element = btn;
274
+ matchMethod = 'snackbar-button-aria';
275
+ break;
276
+ }
277
+ }
278
+ }
279
+
280
+ // Strategy 4: Find in agent-outcome notifications
281
+ if (!element) {
282
+ const outcomeButtons = targetDoc.querySelectorAll('.agent-outcome button, .agent-outcome-notification button, [class*="outcome"] button');
283
+ for (const btn of outcomeButtons) {
284
+ if (!isVisible(btn)) continue;
285
+ const btnText = (btn.textContent || '').trim().toLowerCase();
286
+ if (searchText && (btnText.includes(searchText) || searchText.includes(btnText))) {
287
+ element = btn;
288
+ matchMethod = 'outcome-button';
289
+ break;
290
+ }
291
+ }
292
+ }
293
+
294
+ // Strategy 5: Find expand/collapse arrow (only if explicitly requested)
295
+ if (!element && (searchText.includes('expand') || searchAriaLabel.includes('expand'))) {
296
+ const expandArrow = targetDoc.querySelector('.kiro-snackbar [class*="expand"], .kiro-snackbar [class*="arrow"], .kiro-snackbar [class*="chevron"], .kiro-snackbar button[aria-label*="expand" i]');
297
+ if (expandArrow && isVisible(expandArrow)) {
298
+ element = expandArrow;
299
+ matchMethod = 'snackbar-expand';
300
+ }
301
+ }
302
+ }
303
+
304
+ // =========================================================================
305
+ // Close Button
306
+ // =========================================================================
58
307
  if (isCloseButton && !element) {
59
308
  const closeButtons = targetDoc.querySelectorAll('[aria-label="close"], .kiro-tabs-item-close, [class*="close"]');
60
309
  if (info.parentTabLabel) {
@@ -84,7 +333,9 @@ export async function clickElement(cdp, clickInfo) {
84
333
  }
85
334
  }
86
335
 
87
- // Handle tab click
336
+ // =========================================================================
337
+ // Tab Click
338
+ // =========================================================================
88
339
  if (isTabClick && !element) {
89
340
  const allTabs = targetDoc.querySelectorAll('[role="tab"]');
90
341
  const searchText = (info.tabLabel || info.text || '').trim().toLowerCase();
@@ -99,28 +350,35 @@ export async function clickElement(cdp, clickInfo) {
99
350
  }
100
351
  }
101
352
 
102
- // Handle file link
353
+ // =========================================================================
354
+ // File Link
355
+ // =========================================================================
103
356
  if (info.isFileLink && info.filePath && !element) {
104
357
  const fileName = info.filePath.split('/').pop().split('\\\\').pop();
105
358
  const fileSelectors = ['a[href*="' + fileName + '"]', '[data-path*="' + fileName + '"]', 'code', 'span', '[class*="file"]'];
106
359
  for (const selector of fileSelectors) {
107
- const candidates = targetDoc.querySelectorAll(selector);
108
- for (const el of candidates) {
109
- const text = (el.textContent || '').trim();
110
- if (text.includes(info.filePath) || text.includes(fileName)) {
111
- element = el;
112
- matchMethod = 'file-link';
113
- break;
360
+ try {
361
+ const candidates = targetDoc.querySelectorAll(selector);
362
+ for (const el of candidates) {
363
+ const text = (el.textContent || '').trim();
364
+ if (text.includes(info.filePath) || text.includes(fileName)) {
365
+ element = el;
366
+ matchMethod = 'file-link';
367
+ break;
368
+ }
114
369
  }
115
- }
116
- if (element) break;
370
+ if (element) break;
371
+ } catch(e) {}
117
372
  }
118
373
  }
119
374
 
120
- // Try by aria-label
375
+ // =========================================================================
376
+ // Aria Label Match
377
+ // =========================================================================
121
378
  if (info.ariaLabel && !element && !isCloseButton) {
122
379
  try {
123
- const candidates = targetDoc.querySelectorAll('[aria-label="' + info.ariaLabel.replace(/"/g, '\\\\"') + '"]');
380
+ const escapedLabel = info.ariaLabel.replace(/"/g, '\\\\"');
381
+ const candidates = targetDoc.querySelectorAll('[aria-label="' + escapedLabel + '"]');
124
382
  for (const c of candidates) {
125
383
  const label = (c.getAttribute('aria-label') || '').toLowerCase();
126
384
  if (!label.includes('close')) { element = c; matchMethod = 'aria-label'; break; }
@@ -128,24 +386,23 @@ export async function clickElement(cdp, clickInfo) {
128
386
  } catch(e) {}
129
387
  }
130
388
 
131
- // Handle history/session list items
389
+ // =========================================================================
390
+ // History/Session List Items
391
+ // =========================================================================
132
392
  if (info.isHistoryItem && !element) {
133
393
  const searchText = (info.text || '').trim().toLowerCase();
134
-
135
- // Strategy 1: Find all items that look like history entries (contain dates)
136
394
  const datePattern = /\\d{1,2}\\/\\d{1,2}\\/\\d{4}|\\d{1,2}:\\d{2}:\\d{2}/;
137
395
  const allDivs = targetDoc.querySelectorAll('div, li, article');
138
396
  const historyItems = [];
139
397
 
140
398
  for (const item of allDivs) {
141
- if (item.children.length > 15) continue; // Skip large containers
399
+ if (item.children.length > 15) continue;
142
400
  const text = item.textContent || '';
143
401
  if (datePattern.test(text) && text.length > 20 && text.length < 500) {
144
402
  historyItems.push(item);
145
403
  }
146
404
  }
147
405
 
148
- // Find the one matching our search text
149
406
  for (const item of historyItems) {
150
407
  const itemText = (item.textContent || '').trim().toLowerCase();
151
408
  if (searchText && (itemText.includes(searchText) || searchText.includes(itemText.substring(0, 50)))) {
@@ -155,17 +412,8 @@ export async function clickElement(cdp, clickInfo) {
155
412
  }
156
413
  }
157
414
 
158
- // Strategy 2: If not found by text, try standard selectors
159
415
  if (!element) {
160
- const historySelectors = [
161
- '[role="listitem"]',
162
- '[role="option"]',
163
- '[class*="history"] > *',
164
- '[class*="session"] > *',
165
- '[class*="conversation"] > *',
166
- '[class*="list-item"]'
167
- ];
168
-
416
+ const historySelectors = ['[role="listitem"]', '[role="option"]', '[class*="history"] > *', '[class*="session"] > *'];
169
417
  for (const selector of historySelectors) {
170
418
  try {
171
419
  const items = targetDoc.querySelectorAll(selector);
@@ -181,27 +429,11 @@ export async function clickElement(cdp, clickInfo) {
181
429
  } catch(e) {}
182
430
  }
183
431
  }
184
-
185
- // Strategy 3: Find ANY element with matching text that has cursor:pointer
186
- if (!element && searchText) {
187
- const allElements = targetDoc.querySelectorAll('*');
188
- for (const item of allElements) {
189
- if (item.children.length > 10) continue;
190
- const itemText = (item.textContent || '').trim().toLowerCase();
191
- const firstLine = itemText.split('\\n')[0];
192
- if (firstLine.includes(searchText) || searchText.includes(firstLine.substring(0, 30))) {
193
- const style = window.getComputedStyle(item);
194
- if (style.cursor === 'pointer') {
195
- element = item;
196
- matchMethod = 'history-item-pointer';
197
- break;
198
- }
199
- }
200
- }
201
- }
202
432
  }
203
433
 
204
- // Try by text content
434
+ // =========================================================================
435
+ // Text Content Match (fallback)
436
+ // =========================================================================
205
437
  if (info.text && info.text.trim() && !element) {
206
438
  const searchText = info.text.trim();
207
439
  const allElements = targetDoc.querySelectorAll('button, [role="button"], [role="tab"], [role="menuitem"], [role="option"], [role="listitem"], a, [tabindex="0"], [class*="cursor-pointer"]');
@@ -219,22 +451,27 @@ export async function clickElement(cdp, clickInfo) {
219
451
  }
220
452
  }
221
453
 
454
+ // =========================================================================
455
+ // Execute Click
456
+ // =========================================================================
222
457
  if (!element) return { found: false, error: 'Element not found' };
223
458
 
224
- // For history items, click the item itself - NOT child buttons (which might be delete buttons!)
225
- let clickTarget = element;
226
-
227
- // DO NOT click child buttons for history items - they are likely delete/close buttons
228
- // Just click the main item element directly
459
+ const elementInfo = {
460
+ tag: element.tagName,
461
+ className: element.className?.substring?.(0, 100) || '',
462
+ role: element.getAttribute('role'),
463
+ ariaHaspopup: element.getAttribute('aria-haspopup'),
464
+ ariaExpanded: element.getAttribute('aria-expanded'),
465
+ dataState: element.getAttribute('data-state'),
466
+ textContent: (element.textContent || '').substring(0, 50)
467
+ };
229
468
 
230
469
  try {
231
- // Try standard click first
232
- clickTarget.click();
233
- return { found: true, clicked: true, matchMethod };
470
+ element.click();
471
+ return { found: true, clicked: true, matchMethod, elementInfo };
234
472
  } catch (e) {
235
473
  try {
236
- // Try full mouse event sequence
237
- const rect = clickTarget.getBoundingClientRect();
474
+ const rect = element.getBoundingClientRect();
238
475
  const centerX = rect.left + rect.width / 2;
239
476
  const centerY = rect.top + rect.height / 2;
240
477
 
@@ -246,16 +483,26 @@ export async function clickElement(cdp, clickInfo) {
246
483
  clientY: centerY
247
484
  };
248
485
 
249
- clickTarget.dispatchEvent(new MouseEvent('mousedown', mouseOpts));
250
- clickTarget.dispatchEvent(new MouseEvent('mouseup', mouseOpts));
251
- clickTarget.dispatchEvent(new MouseEvent('click', mouseOpts));
486
+ element.dispatchEvent(new MouseEvent('mousedown', mouseOpts));
487
+ element.dispatchEvent(new MouseEvent('mouseup', mouseOpts));
488
+ element.dispatchEvent(new MouseEvent('click', mouseOpts));
252
489
 
253
- return { found: true, clicked: true, matchMethod: matchMethod + '-dispatch' };
490
+ return { found: true, clicked: true, matchMethod: matchMethod + '-dispatch', elementInfo };
254
491
  } catch (e2) {
255
492
  return { found: true, clicked: false, error: 'Click failed: ' + e2.message };
256
493
  }
257
494
  }
258
495
  })()`;
496
+ }
497
+
498
+ /**
499
+ * Click an element in the Kiro UI via CDP
500
+ * @param {CDPConnection} cdp - CDP connection
501
+ * @param {object} clickInfo - Element identification info (should be sanitized)
502
+ * @returns {Promise<{success: boolean, matchMethod?: string, error?: string}>}
503
+ */
504
+ export async function clickElement(cdp, clickInfo) {
505
+ const script = buildClickScript(clickInfo);
259
506
 
260
507
  try {
261
508
  const result = await cdp.call('Runtime.evaluate', {
@@ -265,14 +512,18 @@ export async function clickElement(cdp, clickInfo) {
265
512
  });
266
513
 
267
514
  const elementInfo = result.result?.value;
515
+
268
516
  if (!elementInfo?.found) {
269
- console.log('[Click] Element not found:', clickInfo.ariaLabel || clickInfo.text);
517
+ console.log('[Click] Element not found:', clickInfo.ariaLabel || clickInfo.text || 'unknown');
270
518
  return { success: false, error: 'Element not found' };
271
519
  }
272
520
 
273
521
  if (elementInfo.clicked) {
274
- console.log('[Click] Clicked via', elementInfo.matchMethod);
275
- return { success: true, matchMethod: elementInfo.matchMethod };
522
+ return {
523
+ success: true,
524
+ matchMethod: elementInfo.matchMethod,
525
+ needsRetry: elementInfo.needsRetry
526
+ };
276
527
  }
277
528
  return { success: false, error: elementInfo.error || 'Click failed' };
278
529
  } catch (err) {
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Message injection service - sends messages to Kiro chat via CDP
3
3
  */
4
+ import { escapeForJavaScript, validateMessage } from '../utils/security.js';
4
5
 
5
6
  /**
6
7
  * Create script to inject message into chat input
@@ -8,7 +9,8 @@
8
9
  * @returns {string} - JavaScript expression
9
10
  */
10
11
  function createInjectScript(messageText) {
11
- const escaped = messageText.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n').replace(/\r/g, '\\r');
12
+ // Use proper escaping to prevent XSS and injection attacks
13
+ const escaped = escapeForJavaScript(messageText);
12
14
 
13
15
  return `(async () => {
14
16
  const text = '${escaped}';
@@ -177,6 +179,12 @@ function createInjectScript(messageText) {
177
179
  * @returns {Promise<{success: boolean, method?: string, error?: string}>}
178
180
  */
179
181
  export async function injectMessage(cdp, message) {
182
+ // Validate message before processing
183
+ const validation = validateMessage(message);
184
+ if (!validation.valid) {
185
+ return { success: false, error: validation.error };
186
+ }
187
+
180
188
  if (!cdp.rootContextId) {
181
189
  return { success: false, error: 'No execution context available' };
182
190
  }