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,6 +1,7 @@
1
1
  /**
2
2
  * Snapshot capture service - captures DOM snapshots from Kiro via CDP
3
3
  */
4
+ import { getLanguageFromExtension } from '../utils/constants.js';
4
5
 
5
6
  /**
6
7
  * Capture chat metadata (title, active state)
@@ -60,7 +61,9 @@ export async function captureCSS(cdp) {
60
61
  const allProps = [];
61
62
  for (let i = 0; i < rootStyles.length; i++) allProps.push(rootStyles[i]);
62
63
 
63
- for (const sheet of targetDoc.styleSheets) {
64
+ // Safely iterate stylesheets with null check
65
+ const styleSheets = targetDoc.styleSheets || [];
66
+ for (const sheet of styleSheets) {
64
67
  try {
65
68
  if (sheet.cssRules) {
66
69
  for (const rule of sheet.cssRules) {
@@ -72,7 +75,9 @@ export async function captureCSS(cdp) {
72
75
  }
73
76
  }
74
77
  }
75
- } catch (e) {}
78
+ } catch (e) {
79
+ // CORS restriction on external stylesheets, skip
80
+ }
76
81
  }
77
82
 
78
83
  let cssVars = ':root {\\n';
@@ -85,12 +90,14 @@ export async function captureCSS(cdp) {
85
90
  cssVars += '}\\n\\n';
86
91
  css += cssVars;
87
92
 
88
- for (const sheet of targetDoc.styleSheets) {
93
+ for (const sheet of styleSheets) {
89
94
  try {
90
95
  if (sheet.cssRules) {
91
96
  for (const rule of sheet.cssRules) css += rule.cssText + '\\n';
92
97
  }
93
- } catch (e) {}
98
+ } catch (e) {
99
+ // CORS restriction, skip
100
+ }
94
101
  }
95
102
 
96
103
  const styleTags = targetDoc.querySelectorAll('style');
@@ -144,45 +151,68 @@ export async function captureSnapshot(cdp) {
144
151
 
145
152
  for (const container of scrollContainers) {
146
153
  if (container.scrollHeight > container.clientHeight) {
147
- // Only check class names for history detection - NOT date patterns
148
154
  const isHistoryPanel = container.matches('[class*="history"], [class*="History"], [class*="session-list"], [class*="SessionList"]') ||
149
155
  container.closest('[class*="history"], [class*="History"], [class*="session-list"], [class*="SessionList"]');
150
156
 
151
157
  if (isHistoryPanel) {
152
- container.scrollTop = 0; // Scroll to TOP for history panels
158
+ container.scrollTop = 0;
153
159
  } else {
154
- container.scrollTop = container.scrollHeight; // Scroll to BOTTOM for chat messages
160
+ container.scrollTop = container.scrollHeight;
155
161
  }
156
162
  }
157
163
  }
158
164
 
159
165
  const clone = targetBody.cloneNode(true);
160
166
 
161
- // Remove tooltips, popovers, overlays from clone
162
- const elementsToRemove = [
163
- '[role="tooltip"]', '[data-tooltip]', '[class*="tooltip"]:not(button)', '[class*="Tooltip"]:not(button)',
164
- '[class*="popover"]:not(button)', '[class*="Popover"]:not(button)', '[class*="dropdown-menu"]',
165
- '[class*="dropdownMenu"]', '[class*="modal"]', '[class*="Modal"]',
166
- '[style*="position: fixed"]:not(button):not([class*="input"]):not([class*="chat"])'
167
+ // Capture any portal/overlay content that might be outside the body
168
+ // Radix UI and similar libraries render dropdowns in portals at document root
169
+ const portals = targetDoc.querySelectorAll('[data-radix-portal], [data-radix-popper-content-wrapper], [class*="portal"], [class*="Portal"]');
170
+ portals.forEach(portal => {
171
+ try {
172
+ const portalClone = portal.cloneNode(true);
173
+ clone.appendChild(portalClone);
174
+ } catch(e) {
175
+ // Clone failed, skip
176
+ }
177
+ });
178
+
179
+ // Also check for any floating/overlay elements at document level
180
+ const floatingSelectors = [
181
+ 'body > [role="listbox"]',
182
+ 'body > [role="menu"]',
183
+ 'body > [data-state="open"]',
184
+ 'body > [class*="dropdown"]',
185
+ 'body > div[style*="position: absolute"]',
186
+ 'body > div[style*="position: fixed"]'
167
187
  ];
168
188
 
169
- elementsToRemove.forEach(selector => {
189
+ floatingSelectors.forEach(sel => {
170
190
  try {
171
- clone.querySelectorAll(selector).forEach(el => {
172
- const isTooltip = el.matches('[role="tooltip"], [class*="tooltip"], [class*="Tooltip"]');
173
- const isImportantUI = el.matches('[class*="model"], [class*="context"], [class*="input"], button, [role="button"]');
174
- if (isTooltip || !isImportantUI) el.remove();
191
+ targetDoc.querySelectorAll(sel).forEach(el => {
192
+ const elClone = el.cloneNode(true);
193
+ clone.appendChild(elClone);
175
194
  });
176
- } catch(e) {}
195
+ } catch(e) {
196
+ // Selector failed, skip
197
+ }
177
198
  });
178
199
 
200
+ // Remove ONLY tooltips from clone - preserve everything else
201
+ try {
202
+ clone.querySelectorAll('[role="tooltip"]').forEach(el => el.remove());
203
+ } catch(e) {
204
+ // Removal failed, continue
205
+ }
206
+
179
207
  // Fix SVG currentColor
180
208
  clone.querySelectorAll('svg').forEach(svg => {
181
209
  try {
182
210
  const computedColor = '#cccccc';
183
211
  svg.querySelectorAll('[fill="currentColor"]').forEach(el => el.setAttribute('fill', computedColor));
184
212
  svg.querySelectorAll('[stroke="currentColor"]').forEach(el => el.setAttribute('stroke', computedColor));
185
- } catch(e) {}
213
+ } catch(e) {
214
+ // SVG fix failed, continue
215
+ }
186
216
  });
187
217
 
188
218
  // Remove placeholder text
@@ -205,7 +235,9 @@ export async function captureSnapshot(cdp) {
205
235
  if (!el.querySelector('[contenteditable], [data-lexical-editor], textarea, input')) el.remove();
206
236
  }
207
237
  });
208
- } catch(e) {}
238
+ } catch(e) {
239
+ // Placeholder removal failed, continue
240
+ }
209
241
 
210
242
  return { html: clone.outerHTML, bodyBg, bodyColor };
211
243
  })()`;
@@ -216,6 +248,7 @@ export async function captureSnapshot(cdp) {
216
248
  contextId: cdp.rootContextId,
217
249
  returnByValue: true
218
250
  });
251
+
219
252
  return result.result?.value || null;
220
253
  } catch (err) {
221
254
  console.error('[Snapshot] Failed to capture HTML:', err.message);
@@ -247,7 +280,9 @@ export async function captureEditor(cdp) {
247
280
  result.fileName = tab.textContent.trim().split('\\n')[0].trim();
248
281
  if (result.fileName) break;
249
282
  }
250
- } catch(e) {}
283
+ } catch(e) {
284
+ // Selector failed, continue
285
+ }
251
286
  }
252
287
 
253
288
  // Try Monaco API
@@ -255,7 +290,7 @@ export async function captureEditor(cdp) {
255
290
  const monacoEditors = targetDoc.querySelectorAll('.monaco-editor');
256
291
  for (const editorEl of monacoEditors) {
257
292
  const editorInstance = editorEl.__vscode_editor__ || editorEl._editor ||
258
- (window.monaco && window.monaco.editor.getEditors && window.monaco.editor.getEditors()[0]);
293
+ (window.monaco && window.monaco.editor && window.monaco.editor.getEditors && window.monaco.editor.getEditors()[0]);
259
294
  if (editorInstance && editorInstance.getModel) {
260
295
  const model = editorInstance.getModel();
261
296
  if (model) {
@@ -267,7 +302,9 @@ export async function captureEditor(cdp) {
267
302
  }
268
303
  }
269
304
  }
270
- } catch(e) {}
305
+ } catch(e) {
306
+ // Monaco API not available, continue
307
+ }
271
308
 
272
309
  // Fallback: Extract from view-lines
273
310
  if (!result.content) {
@@ -309,7 +346,9 @@ export async function captureEditor(cdp) {
309
346
  const extMap = {
310
347
  'ts': 'typescript', 'tsx': 'typescript', 'js': 'javascript', 'jsx': 'javascript',
311
348
  'py': 'python', 'java': 'java', 'html': 'html', 'css': 'css', 'json': 'json',
312
- 'md': 'markdown', 'yaml': 'yaml', 'yml': 'yaml', 'go': 'go', 'rs': 'rust'
349
+ 'md': 'markdown', 'yaml': 'yaml', 'yml': 'yaml', 'go': 'go', 'rs': 'rust',
350
+ 'c': 'c', 'cpp': 'cpp', 'h': 'c', 'cs': 'csharp', 'rb': 'ruby', 'php': 'php',
351
+ 'sql': 'sql', 'sh': 'bash', 'vue': 'vue', 'svelte': 'svelte'
313
352
  };
314
353
  result.language = extMap[ext] || ext || '';
315
354
  }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Shared constants used across the application
3
+ * Centralizes configuration to eliminate duplication and improve maintainability
4
+ */
5
+
6
+ /**
7
+ * CDP ports to scan for Kiro instances
8
+ * @type {number[]}
9
+ */
10
+ export const CDP_PORTS = [9000, 9001, 9002, 9003, 9222, 9229];
11
+
12
+ /**
13
+ * Model names for AI model detection and matching
14
+ * Order matters: check specific names (opus, sonnet, haiku) BEFORE generic (claude)
15
+ * @type {string[]}
16
+ */
17
+ export const MODEL_NAMES = ['auto', 'opus', 'sonnet', 'haiku', 'gpt', 'claude', 'gemini', 'llama'];
18
+
19
+ /**
20
+ * Code file extensions for workspace file filtering
21
+ * @type {Set<string>}
22
+ */
23
+ export const CODE_EXTENSIONS = new Set([
24
+ '.ts', '.tsx', '.js', '.jsx', '.py', '.java', '.go', '.rs',
25
+ '.html', '.css', '.scss', '.json', '.yaml', '.yml', '.md',
26
+ '.sql', '.sh', '.c', '.cpp', '.h', '.cs', '.vue', '.svelte', '.rb', '.php'
27
+ ]);
28
+
29
+ /**
30
+ * File extension to language mapping for syntax highlighting
31
+ * @type {Object<string, string>}
32
+ */
33
+ export const EXTENSION_TO_LANGUAGE = {
34
+ '.ts': 'typescript',
35
+ '.tsx': 'typescript',
36
+ '.js': 'javascript',
37
+ '.jsx': 'javascript',
38
+ '.py': 'python',
39
+ '.html': 'html',
40
+ '.css': 'css',
41
+ '.scss': 'scss',
42
+ '.json': 'json',
43
+ '.md': 'markdown',
44
+ '.yaml': 'yaml',
45
+ '.yml': 'yaml',
46
+ '.go': 'go',
47
+ '.rs': 'rust',
48
+ '.java': 'java',
49
+ '.c': 'c',
50
+ '.cpp': 'cpp',
51
+ '.h': 'c',
52
+ '.cs': 'csharp',
53
+ '.rb': 'ruby',
54
+ '.php': 'php',
55
+ '.sql': 'sql',
56
+ '.sh': 'bash',
57
+ '.vue': 'vue',
58
+ '.svelte': 'svelte'
59
+ };
60
+
61
+ /**
62
+ * Get language from file extension
63
+ * @param {string} filename - File name or path
64
+ * @returns {string} - Language identifier or extension without dot
65
+ */
66
+ export function getLanguageFromExtension(filename) {
67
+ const ext = filename.includes('.') ? '.' + filename.split('.').pop().toLowerCase() : '';
68
+ return EXTENSION_TO_LANGUAGE[ext] || ext.slice(1) || 'text';
69
+ }
70
+
71
+ /**
72
+ * Check if a file has a code extension
73
+ * @param {string} filename - File name or path
74
+ * @returns {boolean}
75
+ */
76
+ export function isCodeFile(filename) {
77
+ const ext = filename.includes('.') ? '.' + filename.split('.').pop().toLowerCase() : '';
78
+ return CODE_EXTENSIONS.has(ext);
79
+ }
80
+
81
+ /**
82
+ * CDP call timeout in milliseconds
83
+ * @type {number}
84
+ */
85
+ export const CDP_CALL_TIMEOUT = 10000;
86
+
87
+ /**
88
+ * HTTP request timeout in milliseconds
89
+ * @type {number}
90
+ */
91
+ export const HTTP_TIMEOUT = 2000;
92
+
93
+ /**
94
+ * Discovery polling intervals
95
+ */
96
+ export const DISCOVERY_INTERVAL_ACTIVE = 10000; // 10 seconds when changes detected
97
+ export const DISCOVERY_INTERVAL_STABLE = 30000; // 30 seconds when stable
98
+
99
+ /**
100
+ * Snapshot polling intervals
101
+ */
102
+ export const SNAPSHOT_INTERVAL_ACTIVE = 1000; // 1 second when active
103
+ export const SNAPSHOT_INTERVAL_IDLE = 3000; // 3 seconds when idle
104
+ export const SNAPSHOT_IDLE_THRESHOLD = 10000; // 10 seconds before considered idle
105
+
106
+ /**
107
+ * Maximum depth for recursive file search
108
+ * @type {number}
109
+ */
110
+ export const MAX_FILE_SEARCH_DEPTH = 4;
111
+
112
+ /**
113
+ * Maximum depth for workspace file collection
114
+ * @type {number}
115
+ */
116
+ export const MAX_WORKSPACE_DEPTH = 5;
package/src/utils/hash.js CHANGED
@@ -1,22 +1,34 @@
1
1
  /**
2
2
  * Hash utilities for content change detection
3
+ *
4
+ * NOTE: MD5 is used here for change detection only, NOT for security purposes.
5
+ * MD5 is fast and sufficient for detecting content changes in snapshots.
6
+ * Do NOT use these functions for password hashing, authentication, or any security-sensitive operations.
3
7
  */
4
8
  import crypto from 'crypto';
5
9
 
6
10
  /**
7
11
  * Generate a unique ID from a string (e.g., WebSocket URL)
12
+ * Used for cascade identification, not security
8
13
  * @param {string} input - String to hash
9
14
  * @returns {string} - 8-character hash ID
10
15
  */
11
16
  export function generateId(input) {
17
+ if (typeof input !== 'string' || !input) {
18
+ return crypto.randomBytes(4).toString('hex');
19
+ }
12
20
  return crypto.createHash('md5').update(input).digest('hex').substring(0, 8);
13
21
  }
14
22
 
15
23
  /**
16
24
  * Compute MD5 hash for change detection
25
+ * Used to detect content changes in snapshots, not for security
17
26
  * @param {string} content - Content to hash
18
27
  * @returns {string} - Full MD5 hash
19
28
  */
20
29
  export function computeHash(content) {
30
+ if (typeof content !== 'string') {
31
+ return '';
32
+ }
21
33
  return crypto.createHash('md5').update(content).digest('hex');
22
34
  }
@@ -3,18 +3,88 @@
3
3
  */
4
4
  import { networkInterfaces } from 'os';
5
5
 
6
+ /**
7
+ * Check if interface is IPv4
8
+ * Handles both string ('IPv4') and number (4) family values
9
+ * @param {object} iface - Network interface object
10
+ * @returns {boolean}
11
+ */
12
+ function isIPv4(iface) {
13
+ return iface.family === 'IPv4' || iface.family === 4;
14
+ }
15
+
6
16
  /**
7
17
  * Get local IP address for LAN access
8
- * @returns {string} - Local IP or 'localhost'
18
+ * Returns the first non-internal IPv4 address found.
19
+ *
20
+ * NOTE: On systems with multiple network interfaces, this returns the first one found.
21
+ * For more control, consider using environment variables or configuration.
22
+ *
23
+ * @returns {string} - Local IP or 'localhost' if no suitable interface found
9
24
  */
10
25
  export function getLocalIP() {
11
26
  const interfaces = networkInterfaces();
27
+
28
+ // Prioritize common interface names (Linux, macOS, Windows)
29
+ const priorityInterfaces = ['Ethernet', 'Wi-Fi', 'eth0', 'en0', 'wlan0'];
30
+
31
+ // First, try priority interfaces
32
+ for (const name of priorityInterfaces) {
33
+ const ifaces = interfaces[name];
34
+ if (ifaces) {
35
+ for (const iface of ifaces) {
36
+ if (isIPv4(iface) && !iface.internal) {
37
+ return iface.address;
38
+ }
39
+ }
40
+ }
41
+ }
42
+
43
+ // Fallback: return first non-internal IPv4 (skip virtual/WSL interfaces)
44
+ for (const name of Object.keys(interfaces)) {
45
+ // Skip virtual interfaces (WSL, Docker, VPN, etc.)
46
+ if (name.toLowerCase().includes('vethernet') ||
47
+ name.toLowerCase().includes('docker') ||
48
+ name.toLowerCase().includes('vmware') ||
49
+ name.toLowerCase().includes('virtualbox')) {
50
+ continue;
51
+ }
52
+ for (const iface of interfaces[name]) {
53
+ if (isIPv4(iface) && !iface.internal) {
54
+ return iface.address;
55
+ }
56
+ }
57
+ }
58
+
59
+ // Last resort: any non-internal IPv4
12
60
  for (const name of Object.keys(interfaces)) {
13
61
  for (const iface of interfaces[name]) {
14
- if (iface.family === 'IPv4' && !iface.internal) {
62
+ if (isIPv4(iface) && !iface.internal) {
15
63
  return iface.address;
16
64
  }
17
65
  }
18
66
  }
67
+
19
68
  return 'localhost';
20
69
  }
70
+
71
+ /**
72
+ * Get all available local IP addresses
73
+ * Useful for debugging or when user needs to choose interface
74
+ *
75
+ * @returns {Array<{name: string, address: string}>} - Array of interface names and addresses
76
+ */
77
+ export function getAllLocalIPs() {
78
+ const interfaces = networkInterfaces();
79
+ const results = [];
80
+
81
+ for (const name of Object.keys(interfaces)) {
82
+ for (const iface of interfaces[name]) {
83
+ if (isIPv4(iface) && !iface.internal) {
84
+ results.push({ name, address: iface.address });
85
+ }
86
+ }
87
+ }
88
+
89
+ return results;
90
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Security utilities for input validation and sanitization
3
+ * Prevents path traversal, XSS, and other security vulnerabilities
4
+ */
5
+ import path from 'path';
6
+
7
+ /**
8
+ * Validate that a file path resolves within an allowed root directory
9
+ * Prevents path traversal attacks (e.g., ../../etc/passwd)
10
+ *
11
+ * @param {string} filePath - The file path to validate (can be relative or absolute)
12
+ * @param {string} rootDir - The allowed root directory
13
+ * @returns {{valid: boolean, resolvedPath: string|null, error: string|null}}
14
+ */
15
+ export function validatePathWithinRoot(filePath, rootDir) {
16
+ if (!filePath || typeof filePath !== 'string') {
17
+ return { valid: false, resolvedPath: null, error: 'Invalid file path' };
18
+ }
19
+
20
+ if (!rootDir || typeof rootDir !== 'string') {
21
+ return { valid: false, resolvedPath: null, error: 'Invalid root directory' };
22
+ }
23
+
24
+ try {
25
+ // Normalize and resolve both paths to absolute paths
26
+ const normalizedRoot = path.resolve(rootDir);
27
+ const resolvedPath = path.resolve(rootDir, filePath);
28
+
29
+ // Ensure the resolved path starts with the root directory
30
+ // Add path.sep to prevent matching partial directory names
31
+ // e.g., /home/user vs /home/username
32
+ const rootWithSep = normalizedRoot.endsWith(path.sep)
33
+ ? normalizedRoot
34
+ : normalizedRoot + path.sep;
35
+
36
+ if (!resolvedPath.startsWith(rootWithSep) && resolvedPath !== normalizedRoot) {
37
+ return {
38
+ valid: false,
39
+ resolvedPath: null,
40
+ error: 'Path traversal detected: path resolves outside allowed directory'
41
+ };
42
+ }
43
+
44
+ return { valid: true, resolvedPath, error: null };
45
+ } catch (err) {
46
+ return { valid: false, resolvedPath: null, error: `Path validation error: ${err.message}` };
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Escape a string for safe inclusion in JavaScript code
52
+ * Handles all special characters that could break string literals or enable injection
53
+ *
54
+ * @param {string} str - The string to escape
55
+ * @returns {string} - Escaped string safe for JS inclusion
56
+ */
57
+ export function escapeForJavaScript(str) {
58
+ if (typeof str !== 'string') {
59
+ return '';
60
+ }
61
+
62
+ return str
63
+ .replace(/\\/g, '\\\\') // Backslashes first (must be first!)
64
+ .replace(/'/g, "\\'") // Single quotes
65
+ .replace(/"/g, '\\"') // Double quotes
66
+ .replace(/`/g, '\\`') // Backticks (template literals)
67
+ .replace(/\$/g, '\\$') // Dollar signs (template literal interpolation)
68
+ .replace(/\n/g, '\\n') // Newlines
69
+ .replace(/\r/g, '\\r') // Carriage returns
70
+ .replace(/\t/g, '\\t') // Tabs
71
+ .replace(/\0/g, '\\0') // Null bytes
72
+ .replace(/\u2028/g, '\\u2028') // Line separator
73
+ .replace(/\u2029/g, '\\u2029'); // Paragraph separator
74
+ }
75
+
76
+ /**
77
+ * Validate and sanitize click info object
78
+ * Ensures all properties are of expected types and within reasonable limits
79
+ *
80
+ * @param {object} clickInfo - The click info object to validate
81
+ * @returns {{valid: boolean, sanitized: object|null, error: string|null}}
82
+ */
83
+ export function sanitizeClickInfo(clickInfo) {
84
+ if (!clickInfo || typeof clickInfo !== 'object') {
85
+ return { valid: false, sanitized: null, error: 'Click info must be an object' };
86
+ }
87
+
88
+ const sanitized = {};
89
+
90
+ // String properties with max length
91
+ const stringProps = [
92
+ { name: 'tag', maxLength: 50 },
93
+ { name: 'text', maxLength: 200 },
94
+ { name: 'ariaLabel', maxLength: 200 },
95
+ { name: 'role', maxLength: 50 },
96
+ { name: 'className', maxLength: 500 },
97
+ { name: 'tabLabel', maxLength: 100 },
98
+ { name: 'parentTabLabel', maxLength: 100 },
99
+ { name: 'filePath', maxLength: 500 },
100
+ { name: 'toggleId', maxLength: 100 }
101
+ ];
102
+
103
+ for (const { name, maxLength } of stringProps) {
104
+ if (clickInfo[name] !== undefined) {
105
+ if (typeof clickInfo[name] !== 'string') {
106
+ sanitized[name] = String(clickInfo[name]).substring(0, maxLength);
107
+ } else {
108
+ sanitized[name] = clickInfo[name].substring(0, maxLength);
109
+ }
110
+ }
111
+ }
112
+
113
+ // Boolean properties
114
+ const boolProps = [
115
+ 'isTab', 'isCloseButton', 'isToggle', 'isModelSelector', 'isModelOption',
116
+ 'isSendButton', 'isFileLink', 'isNotificationButton', 'isIconButton', 'isHistoryItem'
117
+ ];
118
+
119
+ for (const name of boolProps) {
120
+ if (clickInfo[name] !== undefined) {
121
+ sanitized[name] = Boolean(clickInfo[name]);
122
+ }
123
+ }
124
+
125
+ return { valid: true, sanitized, error: null };
126
+ }
127
+
128
+ /**
129
+ * Validate message text for injection
130
+ *
131
+ * @param {string} message - The message to validate
132
+ * @returns {{valid: boolean, error: string|null}}
133
+ */
134
+ export function validateMessage(message) {
135
+ if (!message || typeof message !== 'string') {
136
+ return { valid: false, error: 'Message must be a non-empty string' };
137
+ }
138
+
139
+ if (message.length > 50000) {
140
+ return { valid: false, error: 'Message exceeds maximum length (50000 characters)' };
141
+ }
142
+
143
+ return { valid: true, error: null };
144
+ }
145
+
146
+ /**
147
+ * Sanitize a file path by removing null bytes and normalizing
148
+ * Does NOT validate path traversal - use validatePathWithinRoot for that
149
+ *
150
+ * @param {string} filePath - The file path to sanitize
151
+ * @returns {string} - Sanitized file path
152
+ */
153
+ export function sanitizeFilePath(filePath) {
154
+ if (typeof filePath !== 'string') {
155
+ return '';
156
+ }
157
+
158
+ // Remove null bytes which can be used to bypass security checks
159
+ return filePath.replace(/\0/g, '');
160
+ }