mstro-app 0.1.47

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 (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +177 -0
  3. package/bin/commands/config.js +145 -0
  4. package/bin/commands/login.js +313 -0
  5. package/bin/commands/logout.js +75 -0
  6. package/bin/commands/status.js +197 -0
  7. package/bin/commands/whoami.js +161 -0
  8. package/bin/configure-claude.js +298 -0
  9. package/bin/mstro.js +581 -0
  10. package/bin/postinstall.js +45 -0
  11. package/bin/release.sh +110 -0
  12. package/dist/server/cli/headless/claude-invoker.d.ts +17 -0
  13. package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -0
  14. package/dist/server/cli/headless/claude-invoker.js +311 -0
  15. package/dist/server/cli/headless/claude-invoker.js.map +1 -0
  16. package/dist/server/cli/headless/index.d.ts +13 -0
  17. package/dist/server/cli/headless/index.d.ts.map +1 -0
  18. package/dist/server/cli/headless/index.js +10 -0
  19. package/dist/server/cli/headless/index.js.map +1 -0
  20. package/dist/server/cli/headless/mcp-config.d.ts +11 -0
  21. package/dist/server/cli/headless/mcp-config.d.ts.map +1 -0
  22. package/dist/server/cli/headless/mcp-config.js +76 -0
  23. package/dist/server/cli/headless/mcp-config.js.map +1 -0
  24. package/dist/server/cli/headless/output-utils.d.ts +33 -0
  25. package/dist/server/cli/headless/output-utils.d.ts.map +1 -0
  26. package/dist/server/cli/headless/output-utils.js +101 -0
  27. package/dist/server/cli/headless/output-utils.js.map +1 -0
  28. package/dist/server/cli/headless/prompt-utils.d.ts +21 -0
  29. package/dist/server/cli/headless/prompt-utils.d.ts.map +1 -0
  30. package/dist/server/cli/headless/prompt-utils.js +84 -0
  31. package/dist/server/cli/headless/prompt-utils.js.map +1 -0
  32. package/dist/server/cli/headless/runner.d.ts +24 -0
  33. package/dist/server/cli/headless/runner.d.ts.map +1 -0
  34. package/dist/server/cli/headless/runner.js +99 -0
  35. package/dist/server/cli/headless/runner.js.map +1 -0
  36. package/dist/server/cli/headless/types.d.ts +106 -0
  37. package/dist/server/cli/headless/types.d.ts.map +1 -0
  38. package/dist/server/cli/headless/types.js +4 -0
  39. package/dist/server/cli/headless/types.js.map +1 -0
  40. package/dist/server/cli/improvisation-session-manager.d.ts +155 -0
  41. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -0
  42. package/dist/server/cli/improvisation-session-manager.js +415 -0
  43. package/dist/server/cli/improvisation-session-manager.js.map +1 -0
  44. package/dist/server/index.d.ts +2 -0
  45. package/dist/server/index.d.ts.map +1 -0
  46. package/dist/server/index.js +386 -0
  47. package/dist/server/index.js.map +1 -0
  48. package/dist/server/mcp/bouncer-cli.d.ts +3 -0
  49. package/dist/server/mcp/bouncer-cli.d.ts.map +1 -0
  50. package/dist/server/mcp/bouncer-cli.js +99 -0
  51. package/dist/server/mcp/bouncer-cli.js.map +1 -0
  52. package/dist/server/mcp/bouncer-integration.d.ts +36 -0
  53. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -0
  54. package/dist/server/mcp/bouncer-integration.js +301 -0
  55. package/dist/server/mcp/bouncer-integration.js.map +1 -0
  56. package/dist/server/mcp/security-audit.d.ts +52 -0
  57. package/dist/server/mcp/security-audit.d.ts.map +1 -0
  58. package/dist/server/mcp/security-audit.js +118 -0
  59. package/dist/server/mcp/security-audit.js.map +1 -0
  60. package/dist/server/mcp/security-patterns.d.ts +73 -0
  61. package/dist/server/mcp/security-patterns.d.ts.map +1 -0
  62. package/dist/server/mcp/security-patterns.js +247 -0
  63. package/dist/server/mcp/security-patterns.js.map +1 -0
  64. package/dist/server/mcp/server.d.ts +3 -0
  65. package/dist/server/mcp/server.d.ts.map +1 -0
  66. package/dist/server/mcp/server.js +146 -0
  67. package/dist/server/mcp/server.js.map +1 -0
  68. package/dist/server/routes/files.d.ts +9 -0
  69. package/dist/server/routes/files.d.ts.map +1 -0
  70. package/dist/server/routes/files.js +24 -0
  71. package/dist/server/routes/files.js.map +1 -0
  72. package/dist/server/routes/improvise.d.ts +3 -0
  73. package/dist/server/routes/improvise.d.ts.map +1 -0
  74. package/dist/server/routes/improvise.js +72 -0
  75. package/dist/server/routes/improvise.js.map +1 -0
  76. package/dist/server/routes/index.d.ts +10 -0
  77. package/dist/server/routes/index.d.ts.map +1 -0
  78. package/dist/server/routes/index.js +12 -0
  79. package/dist/server/routes/index.js.map +1 -0
  80. package/dist/server/routes/instances.d.ts +10 -0
  81. package/dist/server/routes/instances.d.ts.map +1 -0
  82. package/dist/server/routes/instances.js +47 -0
  83. package/dist/server/routes/instances.js.map +1 -0
  84. package/dist/server/routes/notifications.d.ts +3 -0
  85. package/dist/server/routes/notifications.d.ts.map +1 -0
  86. package/dist/server/routes/notifications.js +136 -0
  87. package/dist/server/routes/notifications.js.map +1 -0
  88. package/dist/server/services/analytics.d.ts +56 -0
  89. package/dist/server/services/analytics.d.ts.map +1 -0
  90. package/dist/server/services/analytics.js +240 -0
  91. package/dist/server/services/analytics.js.map +1 -0
  92. package/dist/server/services/auth.d.ts +26 -0
  93. package/dist/server/services/auth.d.ts.map +1 -0
  94. package/dist/server/services/auth.js +71 -0
  95. package/dist/server/services/auth.js.map +1 -0
  96. package/dist/server/services/client-id.d.ts +10 -0
  97. package/dist/server/services/client-id.d.ts.map +1 -0
  98. package/dist/server/services/client-id.js +61 -0
  99. package/dist/server/services/client-id.js.map +1 -0
  100. package/dist/server/services/credentials.d.ts +39 -0
  101. package/dist/server/services/credentials.d.ts.map +1 -0
  102. package/dist/server/services/credentials.js +110 -0
  103. package/dist/server/services/credentials.js.map +1 -0
  104. package/dist/server/services/files.d.ts +119 -0
  105. package/dist/server/services/files.d.ts.map +1 -0
  106. package/dist/server/services/files.js +560 -0
  107. package/dist/server/services/files.js.map +1 -0
  108. package/dist/server/services/instances.d.ts +52 -0
  109. package/dist/server/services/instances.d.ts.map +1 -0
  110. package/dist/server/services/instances.js +241 -0
  111. package/dist/server/services/instances.js.map +1 -0
  112. package/dist/server/services/pathUtils.d.ts +47 -0
  113. package/dist/server/services/pathUtils.d.ts.map +1 -0
  114. package/dist/server/services/pathUtils.js +124 -0
  115. package/dist/server/services/pathUtils.js.map +1 -0
  116. package/dist/server/services/platform.d.ts +72 -0
  117. package/dist/server/services/platform.d.ts.map +1 -0
  118. package/dist/server/services/platform.js +368 -0
  119. package/dist/server/services/platform.js.map +1 -0
  120. package/dist/server/services/sentry.d.ts +5 -0
  121. package/dist/server/services/sentry.d.ts.map +1 -0
  122. package/dist/server/services/sentry.js +71 -0
  123. package/dist/server/services/sentry.js.map +1 -0
  124. package/dist/server/services/terminal/pty-manager.d.ts +149 -0
  125. package/dist/server/services/terminal/pty-manager.d.ts.map +1 -0
  126. package/dist/server/services/terminal/pty-manager.js +377 -0
  127. package/dist/server/services/terminal/pty-manager.js.map +1 -0
  128. package/dist/server/services/terminal/tmux-manager.d.ts +82 -0
  129. package/dist/server/services/terminal/tmux-manager.d.ts.map +1 -0
  130. package/dist/server/services/terminal/tmux-manager.js +352 -0
  131. package/dist/server/services/terminal/tmux-manager.js.map +1 -0
  132. package/dist/server/services/websocket/autocomplete.d.ts +50 -0
  133. package/dist/server/services/websocket/autocomplete.d.ts.map +1 -0
  134. package/dist/server/services/websocket/autocomplete.js +361 -0
  135. package/dist/server/services/websocket/autocomplete.js.map +1 -0
  136. package/dist/server/services/websocket/file-utils.d.ts +44 -0
  137. package/dist/server/services/websocket/file-utils.d.ts.map +1 -0
  138. package/dist/server/services/websocket/file-utils.js +272 -0
  139. package/dist/server/services/websocket/file-utils.js.map +1 -0
  140. package/dist/server/services/websocket/handler.d.ts +246 -0
  141. package/dist/server/services/websocket/handler.d.ts.map +1 -0
  142. package/dist/server/services/websocket/handler.js +1771 -0
  143. package/dist/server/services/websocket/handler.js.map +1 -0
  144. package/dist/server/services/websocket/index.d.ts +11 -0
  145. package/dist/server/services/websocket/index.d.ts.map +1 -0
  146. package/dist/server/services/websocket/index.js +14 -0
  147. package/dist/server/services/websocket/index.js.map +1 -0
  148. package/dist/server/services/websocket/types.d.ts +214 -0
  149. package/dist/server/services/websocket/types.d.ts.map +1 -0
  150. package/dist/server/services/websocket/types.js +4 -0
  151. package/dist/server/services/websocket/types.js.map +1 -0
  152. package/dist/server/utils/agent-manager.d.ts +69 -0
  153. package/dist/server/utils/agent-manager.d.ts.map +1 -0
  154. package/dist/server/utils/agent-manager.js +269 -0
  155. package/dist/server/utils/agent-manager.js.map +1 -0
  156. package/dist/server/utils/paths.d.ts +25 -0
  157. package/dist/server/utils/paths.d.ts.map +1 -0
  158. package/dist/server/utils/paths.js +38 -0
  159. package/dist/server/utils/paths.js.map +1 -0
  160. package/dist/server/utils/port-manager.d.ts +10 -0
  161. package/dist/server/utils/port-manager.d.ts.map +1 -0
  162. package/dist/server/utils/port-manager.js +60 -0
  163. package/dist/server/utils/port-manager.js.map +1 -0
  164. package/dist/server/utils/port.d.ts +26 -0
  165. package/dist/server/utils/port.d.ts.map +1 -0
  166. package/dist/server/utils/port.js +83 -0
  167. package/dist/server/utils/port.js.map +1 -0
  168. package/hooks/bouncer.sh +138 -0
  169. package/package.json +74 -0
  170. package/server/README.md +191 -0
  171. package/server/cli/headless/claude-invoker.ts +415 -0
  172. package/server/cli/headless/index.ts +39 -0
  173. package/server/cli/headless/mcp-config.ts +87 -0
  174. package/server/cli/headless/output-utils.ts +109 -0
  175. package/server/cli/headless/prompt-utils.ts +108 -0
  176. package/server/cli/headless/runner.ts +133 -0
  177. package/server/cli/headless/types.ts +118 -0
  178. package/server/cli/improvisation-session-manager.ts +531 -0
  179. package/server/index.ts +456 -0
  180. package/server/mcp/README.md +122 -0
  181. package/server/mcp/bouncer-cli.ts +127 -0
  182. package/server/mcp/bouncer-integration.ts +430 -0
  183. package/server/mcp/security-audit.ts +180 -0
  184. package/server/mcp/security-patterns.ts +290 -0
  185. package/server/mcp/server.ts +174 -0
  186. package/server/routes/files.ts +29 -0
  187. package/server/routes/improvise.ts +82 -0
  188. package/server/routes/index.ts +13 -0
  189. package/server/routes/instances.ts +54 -0
  190. package/server/routes/notifications.ts +158 -0
  191. package/server/services/analytics.ts +277 -0
  192. package/server/services/auth.ts +80 -0
  193. package/server/services/client-id.ts +68 -0
  194. package/server/services/credentials.ts +134 -0
  195. package/server/services/files.ts +710 -0
  196. package/server/services/instances.ts +275 -0
  197. package/server/services/pathUtils.ts +158 -0
  198. package/server/services/platform.test.ts +1314 -0
  199. package/server/services/platform.ts +435 -0
  200. package/server/services/sentry.ts +81 -0
  201. package/server/services/terminal/pty-manager.ts +464 -0
  202. package/server/services/terminal/tmux-manager.ts +426 -0
  203. package/server/services/websocket/autocomplete.ts +438 -0
  204. package/server/services/websocket/file-utils.ts +305 -0
  205. package/server/services/websocket/handler.test.ts +20 -0
  206. package/server/services/websocket/handler.ts +2047 -0
  207. package/server/services/websocket/index.ts +40 -0
  208. package/server/services/websocket/types.ts +339 -0
  209. package/server/tsconfig.json +19 -0
  210. package/server/utils/agent-manager.ts +323 -0
  211. package/server/utils/paths.ts +45 -0
  212. package/server/utils/port-manager.ts +70 -0
  213. package/server/utils/port.ts +102 -0
@@ -0,0 +1,275 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * Instance Registry Service
6
+ *
7
+ * Manages multiple Mstro server instances
8
+ */
9
+
10
+ import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'
11
+ import { homedir } from 'node:os'
12
+ import { join } from 'node:path'
13
+
14
+ function clearStaleLock(lockPath: string): void {
15
+ if (!existsSync(lockPath)) return
16
+ try {
17
+ const lockTime = parseInt(readFileSync(lockPath, 'utf-8'), 10)
18
+ if (Date.now() - lockTime > 10000) {
19
+ try { unlinkSync(lockPath) } catch {}
20
+ }
21
+ } catch {}
22
+ }
23
+
24
+ function tryCreateLock(lockPath: string): boolean {
25
+ try {
26
+ writeFileSync(lockPath, Date.now().toString(), { flag: 'wx' })
27
+ return true
28
+ } catch {
29
+ return false
30
+ }
31
+ }
32
+
33
+ function busyWait(): void {
34
+ const endTime = Date.now() + Math.random() * 50 + 10
35
+ while (Date.now() < endTime) { /* busy wait */ }
36
+ }
37
+
38
+ function acquireLock(lockPath: string, timeout = 5000): boolean {
39
+ const start = Date.now()
40
+ while (Date.now() - start < timeout) {
41
+ clearStaleLock(lockPath)
42
+ if (tryCreateLock(lockPath)) return true
43
+ busyWait()
44
+ }
45
+ return false
46
+ }
47
+
48
+ function releaseLock(lockPath: string) {
49
+ try {
50
+ unlinkSync(lockPath)
51
+ } catch {
52
+ // Ignore errors when releasing lock
53
+ }
54
+ }
55
+
56
+ export interface MstroInstance {
57
+ id: string
58
+ port: number
59
+ url: string
60
+ workingDirectory: string
61
+ startedAt: number
62
+ lastHeartbeat: number
63
+ pid: number
64
+ }
65
+
66
+ export class InstanceRegistry {
67
+ private registryPath: string
68
+ private currentInstance: MstroInstance | null = null
69
+ private heartbeatInterval: ReturnType<typeof setInterval> | null = null
70
+
71
+ constructor() {
72
+ const mstroDir = join(homedir(), '.mstro')
73
+ if (!existsSync(mstroDir)) {
74
+ mkdirSync(mstroDir, { recursive: true, mode: 0o700 })
75
+ }
76
+ this.registryPath = join(mstroDir, 'instances.json')
77
+ }
78
+
79
+ /**
80
+ * Register current instance
81
+ */
82
+ register(port: number, workingDirectory: string): MstroInstance {
83
+ const instance: MstroInstance = {
84
+ id: `mstro-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
85
+ port,
86
+ url: `http://localhost:${port}`,
87
+ workingDirectory,
88
+ startedAt: Date.now(),
89
+ lastHeartbeat: Date.now(),
90
+ pid: process.pid
91
+ }
92
+
93
+ this.currentInstance = instance
94
+ this.saveInstance(instance)
95
+ this.startHeartbeat()
96
+
97
+ return instance
98
+ }
99
+
100
+ /**
101
+ * Unregister current instance
102
+ */
103
+ unregister() {
104
+ if (this.currentInstance) {
105
+ this.removeInstance(this.currentInstance.id)
106
+ this.currentInstance = null
107
+ }
108
+ this.stopHeartbeat()
109
+ }
110
+
111
+ /**
112
+ * Get current instance info
113
+ */
114
+ getCurrentInstance(): MstroInstance | null {
115
+ return this.currentInstance
116
+ }
117
+
118
+ /**
119
+ * Get all instances (with file locking to prevent race conditions)
120
+ */
121
+ static getAllInstances(): MstroInstance[] {
122
+ const mstroDir = join(homedir(), '.mstro')
123
+ const registryPath = join(mstroDir, 'instances.json')
124
+ const lockPath = join(mstroDir, 'instances.lock')
125
+
126
+ if (!existsSync(registryPath)) {
127
+ return []
128
+ }
129
+
130
+ const locked = acquireLock(lockPath, 3000)
131
+ try {
132
+ const data = readFileSync(registryPath, 'utf-8')
133
+
134
+ // Handle empty or whitespace-only content
135
+ if (!data || !data.trim()) {
136
+ return []
137
+ }
138
+
139
+ const parsed = JSON.parse(data)
140
+
141
+ // Ensure we have an array
142
+ const instances: MstroInstance[] = Array.isArray(parsed) ? parsed : []
143
+
144
+ // Filter out stale instances (no heartbeat in last 2 minutes)
145
+ const now = Date.now()
146
+ const maxAge = 2 * 60 * 1000 // 2 minutes
147
+
148
+ return instances.filter(instance =>
149
+ now - instance.lastHeartbeat < maxAge
150
+ )
151
+ } catch (error) {
152
+ // If JSON is corrupted, log but don't spam - return empty and let next write fix it
153
+ if (error instanceof SyntaxError) {
154
+ console.warn('[InstanceRegistry] Registry file corrupted, will be recreated on next write')
155
+ } else {
156
+ console.error('[InstanceRegistry] Failed to read instances registry:', error)
157
+ }
158
+ return []
159
+ } finally {
160
+ if (locked) {
161
+ releaseLock(lockPath)
162
+ }
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Get instance by ID
168
+ */
169
+ static getInstance(id: string): MstroInstance | null {
170
+ const instances = InstanceRegistry.getAllInstances()
171
+ return instances.find(i => i.id === id) || null
172
+ }
173
+
174
+ /**
175
+ * Save instance to registry (with file locking)
176
+ */
177
+ private saveInstance(instance: MstroInstance) {
178
+ const lockPath = join(homedir(), '.mstro', 'instances.lock')
179
+ const locked = acquireLock(lockPath, 5000)
180
+
181
+ try {
182
+ // Read current instances directly (without calling getAllInstances to avoid double-locking)
183
+ let instances: MstroInstance[] = []
184
+ if (existsSync(this.registryPath)) {
185
+ try {
186
+ const data = readFileSync(this.registryPath, 'utf-8')
187
+ if (data?.trim()) {
188
+ const parsed = JSON.parse(data)
189
+ instances = Array.isArray(parsed) ? parsed : []
190
+ }
191
+ } catch {
192
+ // If file is corrupted, start fresh
193
+ instances = []
194
+ }
195
+ }
196
+
197
+ // Filter out stale instances while we're at it
198
+ const now = Date.now()
199
+ const maxAge = 2 * 60 * 1000
200
+ instances = instances.filter(i =>
201
+ i.id !== instance.id && now - i.lastHeartbeat < maxAge
202
+ )
203
+
204
+ // Add new/updated instance
205
+ instances.push(instance)
206
+
207
+ // Write atomically by writing to temp file first then renaming
208
+ const tempPath = `${this.registryPath}.tmp`
209
+ writeFileSync(tempPath, JSON.stringify(instances, null, 2), 'utf-8')
210
+
211
+ // Rename is atomic on most filesystems
212
+ renameSync(tempPath, this.registryPath)
213
+ } finally {
214
+ if (locked) {
215
+ releaseLock(lockPath)
216
+ }
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Remove instance from registry (with file locking)
222
+ */
223
+ private removeInstance(id: string) {
224
+ const lockPath = join(homedir(), '.mstro', 'instances.lock')
225
+ const locked = acquireLock(lockPath, 5000)
226
+
227
+ try {
228
+ let instances: MstroInstance[] = []
229
+ if (existsSync(this.registryPath)) {
230
+ try {
231
+ const data = readFileSync(this.registryPath, 'utf-8')
232
+ if (data?.trim()) {
233
+ const parsed = JSON.parse(data)
234
+ instances = Array.isArray(parsed) ? parsed : []
235
+ }
236
+ } catch {
237
+ instances = []
238
+ }
239
+ }
240
+
241
+ instances = instances.filter(i => i.id !== id)
242
+
243
+ const tempPath = `${this.registryPath}.tmp`
244
+ writeFileSync(tempPath, JSON.stringify(instances, null, 2), 'utf-8')
245
+
246
+ renameSync(tempPath, this.registryPath)
247
+ } finally {
248
+ if (locked) {
249
+ releaseLock(lockPath)
250
+ }
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Start heartbeat to keep instance alive
256
+ */
257
+ private startHeartbeat() {
258
+ this.heartbeatInterval = setInterval(() => {
259
+ if (this.currentInstance) {
260
+ this.currentInstance.lastHeartbeat = Date.now()
261
+ this.saveInstance(this.currentInstance)
262
+ }
263
+ }, 30000) // Every 30 seconds
264
+ }
265
+
266
+ /**
267
+ * Stop heartbeat
268
+ */
269
+ private stopHeartbeat() {
270
+ if (this.heartbeatInterval) {
271
+ clearInterval(this.heartbeatInterval)
272
+ this.heartbeatInterval = null
273
+ }
274
+ }
275
+ }
@@ -0,0 +1,158 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * Path Utilities
6
+ *
7
+ * Secure path validation utilities to prevent path traversal attacks.
8
+ * All file explorer operations MUST validate paths through these functions.
9
+ */
10
+
11
+ import { isAbsolute, normalize, relative, resolve } from 'node:path';
12
+
13
+ export interface PathValidationResult {
14
+ valid: boolean;
15
+ resolvedPath: string;
16
+ error?: string;
17
+ }
18
+
19
+ /**
20
+ * Validate that a path is within the allowed working directory.
21
+ * Prevents path traversal attacks using .. or absolute paths.
22
+ *
23
+ * @param targetPath - The path to validate (relative or absolute)
24
+ * @param workingDir - The allowed working directory boundary
25
+ * @returns PathValidationResult with validation status and resolved path
26
+ */
27
+ export function validatePathWithinWorkingDir(
28
+ targetPath: string,
29
+ workingDir: string
30
+ ): PathValidationResult {
31
+ try {
32
+ // Normalize the working directory to get canonical path
33
+ const normalizedWorkingDir = resolve(workingDir);
34
+
35
+ // Resolve the target path relative to working directory
36
+ let resolvedPath: string;
37
+ if (isAbsolute(targetPath)) {
38
+ resolvedPath = resolve(targetPath);
39
+ } else {
40
+ resolvedPath = resolve(normalizedWorkingDir, targetPath);
41
+ }
42
+
43
+ // Normalize to remove any .. or . segments
44
+ resolvedPath = normalize(resolvedPath);
45
+
46
+ // Check if the resolved path starts with the working directory
47
+ // Add trailing separator to prevent partial matches (e.g., /home/user vs /home/username)
48
+ const workingDirWithSep = normalizedWorkingDir.endsWith('/')
49
+ ? normalizedWorkingDir
50
+ : `${normalizedWorkingDir}/`;
51
+
52
+ const isWithinWorkingDir =
53
+ resolvedPath === normalizedWorkingDir ||
54
+ resolvedPath.startsWith(workingDirWithSep);
55
+
56
+ if (!isWithinWorkingDir) {
57
+ // Log security violation for monitoring
58
+ console.error(
59
+ `[PathUtils] SECURITY: Path traversal attempt blocked. ` +
60
+ `Target: "${targetPath}", Resolved: "${resolvedPath}", WorkingDir: "${normalizedWorkingDir}"`
61
+ );
62
+
63
+ return {
64
+ valid: false,
65
+ resolvedPath: '',
66
+ error: 'Access denied: path is outside working directory'
67
+ };
68
+ }
69
+
70
+ return {
71
+ valid: true,
72
+ resolvedPath
73
+ };
74
+ } catch (error: any) {
75
+ console.error('[PathUtils] Error validating path:', error);
76
+ return {
77
+ valid: false,
78
+ resolvedPath: '',
79
+ error: `Invalid path: ${error.message}`
80
+ };
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Validate both source and destination paths for rename/move operations.
86
+ * Both paths must be within the working directory.
87
+ *
88
+ * @param sourcePath - The source path
89
+ * @param destPath - The destination path
90
+ * @param workingDir - The allowed working directory boundary
91
+ * @returns Object with validation results for both paths
92
+ */
93
+ export function validateBothPathsWithinWorkingDir(
94
+ sourcePath: string,
95
+ destPath: string,
96
+ workingDir: string
97
+ ): {
98
+ valid: boolean;
99
+ resolvedSourcePath: string;
100
+ resolvedDestPath: string;
101
+ error?: string;
102
+ } {
103
+ const sourceValidation = validatePathWithinWorkingDir(sourcePath, workingDir);
104
+ if (!sourceValidation.valid) {
105
+ return {
106
+ valid: false,
107
+ resolvedSourcePath: '',
108
+ resolvedDestPath: '',
109
+ error: `Source path error: ${sourceValidation.error}`
110
+ };
111
+ }
112
+
113
+ const destValidation = validatePathWithinWorkingDir(destPath, workingDir);
114
+ if (!destValidation.valid) {
115
+ return {
116
+ valid: false,
117
+ resolvedSourcePath: '',
118
+ resolvedDestPath: '',
119
+ error: `Destination path error: ${destValidation.error}`
120
+ };
121
+ }
122
+
123
+ return {
124
+ valid: true,
125
+ resolvedSourcePath: sourceValidation.resolvedPath,
126
+ resolvedDestPath: destValidation.resolvedPath
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Get the relative path from working directory.
132
+ * Useful for returning user-friendly paths in responses.
133
+ *
134
+ * @param absolutePath - The absolute path
135
+ * @param workingDir - The working directory
136
+ * @returns Relative path from working directory
137
+ */
138
+ export function getRelativePath(absolutePath: string, workingDir: string): string {
139
+ return relative(resolve(workingDir), absolutePath);
140
+ }
141
+
142
+ /**
143
+ * Check if a path contains dangerous patterns that should be blocked.
144
+ * This is an additional layer of defense beyond path validation.
145
+ *
146
+ * @param path - The path to check
147
+ * @returns true if path contains dangerous patterns
148
+ */
149
+ export function containsDangerousPatterns(path: string): boolean {
150
+ const dangerousPatterns = [
151
+ /\0/, // Null bytes
152
+ /^~/, // Home directory expansion (should use absolute paths)
153
+ /\$\{/, // Variable expansion
154
+ /\$\(/, // Command substitution
155
+ ];
156
+
157
+ return dangerousPatterns.some(pattern => pattern.test(path));
158
+ }