@sylphx/flow 1.0.1 → 1.0.3

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 (229) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +10 -9
  3. package/src/commands/codebase-command.ts +168 -0
  4. package/src/commands/flow-command.ts +1137 -0
  5. package/src/commands/flow-orchestrator.ts +296 -0
  6. package/src/commands/hook-command.ts +444 -0
  7. package/src/commands/init-command.ts +92 -0
  8. package/src/commands/init-core.ts +322 -0
  9. package/src/commands/knowledge-command.ts +161 -0
  10. package/src/commands/run-command.ts +120 -0
  11. package/src/components/benchmark-monitor.tsx +331 -0
  12. package/src/components/reindex-progress.tsx +261 -0
  13. package/src/composables/functional/index.ts +14 -0
  14. package/src/composables/functional/useEnvironment.ts +171 -0
  15. package/src/composables/functional/useFileSystem.ts +139 -0
  16. package/src/composables/index.ts +5 -0
  17. package/src/composables/useEnv.ts +13 -0
  18. package/src/composables/useRuntimeConfig.ts +27 -0
  19. package/src/composables/useTargetConfig.ts +45 -0
  20. package/src/config/ai-config.ts +376 -0
  21. package/src/config/constants.ts +35 -0
  22. package/src/config/index.ts +27 -0
  23. package/src/config/rules.ts +43 -0
  24. package/src/config/servers.ts +371 -0
  25. package/src/config/targets.ts +126 -0
  26. package/src/core/agent-loader.ts +141 -0
  27. package/src/core/agent-manager.ts +174 -0
  28. package/src/core/ai-sdk.ts +603 -0
  29. package/src/core/app-factory.ts +381 -0
  30. package/src/core/builtin-agents.ts +9 -0
  31. package/src/core/command-system.ts +550 -0
  32. package/src/core/config-system.ts +550 -0
  33. package/src/core/connection-pool.ts +390 -0
  34. package/src/core/di-container.ts +155 -0
  35. package/src/core/error-handling.ts +519 -0
  36. package/src/core/formatting/bytes.test.ts +115 -0
  37. package/src/core/formatting/bytes.ts +64 -0
  38. package/src/core/functional/async.ts +313 -0
  39. package/src/core/functional/either.ts +109 -0
  40. package/src/core/functional/error-handler.ts +135 -0
  41. package/src/core/functional/error-types.ts +311 -0
  42. package/src/core/functional/index.ts +19 -0
  43. package/src/core/functional/option.ts +142 -0
  44. package/src/core/functional/pipe.ts +189 -0
  45. package/src/core/functional/result.ts +204 -0
  46. package/src/core/functional/validation.ts +138 -0
  47. package/src/core/headless-display.ts +96 -0
  48. package/src/core/index.ts +6 -0
  49. package/src/core/installers/file-installer.ts +303 -0
  50. package/src/core/installers/mcp-installer.ts +213 -0
  51. package/src/core/interfaces/index.ts +22 -0
  52. package/src/core/interfaces/repository.interface.ts +91 -0
  53. package/src/core/interfaces/service.interface.ts +133 -0
  54. package/src/core/interfaces.ts +129 -0
  55. package/src/core/loop-controller.ts +200 -0
  56. package/src/core/result.ts +351 -0
  57. package/src/core/rule-loader.ts +147 -0
  58. package/src/core/rule-manager.ts +240 -0
  59. package/src/core/service-config.ts +252 -0
  60. package/src/core/session-service.ts +121 -0
  61. package/src/core/state-detector.ts +389 -0
  62. package/src/core/storage-factory.ts +115 -0
  63. package/src/core/stream-handler.ts +288 -0
  64. package/src/core/target-manager.ts +161 -0
  65. package/src/core/type-utils.ts +427 -0
  66. package/src/core/unified-storage.ts +456 -0
  67. package/src/core/upgrade-manager.ts +300 -0
  68. package/src/core/validation/limit.test.ts +155 -0
  69. package/src/core/validation/limit.ts +46 -0
  70. package/src/core/validation/query.test.ts +44 -0
  71. package/src/core/validation/query.ts +20 -0
  72. package/src/db/auto-migrate.ts +322 -0
  73. package/src/db/base-database-client.ts +144 -0
  74. package/src/db/cache-db.ts +218 -0
  75. package/src/db/cache-schema.ts +75 -0
  76. package/src/db/database.ts +70 -0
  77. package/src/db/index.ts +252 -0
  78. package/src/db/memory-db.ts +153 -0
  79. package/src/db/memory-schema.ts +29 -0
  80. package/src/db/schema.ts +289 -0
  81. package/src/db/session-repository.ts +733 -0
  82. package/src/domains/codebase/index.ts +5 -0
  83. package/src/domains/codebase/tools.ts +139 -0
  84. package/src/domains/index.ts +8 -0
  85. package/src/domains/knowledge/index.ts +10 -0
  86. package/src/domains/knowledge/resources.ts +537 -0
  87. package/src/domains/knowledge/tools.ts +174 -0
  88. package/src/domains/utilities/index.ts +6 -0
  89. package/src/domains/utilities/time/index.ts +5 -0
  90. package/src/domains/utilities/time/tools.ts +291 -0
  91. package/src/index.ts +211 -0
  92. package/src/services/agent-service.ts +273 -0
  93. package/src/services/claude-config-service.ts +252 -0
  94. package/src/services/config-service.ts +258 -0
  95. package/src/services/evaluation-service.ts +271 -0
  96. package/src/services/functional/evaluation-logic.ts +296 -0
  97. package/src/services/functional/file-processor.ts +273 -0
  98. package/src/services/functional/index.ts +12 -0
  99. package/src/services/index.ts +13 -0
  100. package/src/services/mcp-service.ts +432 -0
  101. package/src/services/memory.service.ts +476 -0
  102. package/src/services/search/base-indexer.ts +156 -0
  103. package/src/services/search/codebase-indexer-types.ts +38 -0
  104. package/src/services/search/codebase-indexer.ts +647 -0
  105. package/src/services/search/embeddings-provider.ts +455 -0
  106. package/src/services/search/embeddings.ts +316 -0
  107. package/src/services/search/functional-indexer.ts +323 -0
  108. package/src/services/search/index.ts +27 -0
  109. package/src/services/search/indexer.ts +380 -0
  110. package/src/services/search/knowledge-indexer.ts +422 -0
  111. package/src/services/search/semantic-search.ts +244 -0
  112. package/src/services/search/tfidf.ts +559 -0
  113. package/src/services/search/unified-search-service.ts +888 -0
  114. package/src/services/smart-config-service.ts +385 -0
  115. package/src/services/storage/cache-storage.ts +487 -0
  116. package/src/services/storage/drizzle-storage.ts +581 -0
  117. package/src/services/storage/index.ts +15 -0
  118. package/src/services/storage/lancedb-vector-storage.ts +494 -0
  119. package/src/services/storage/memory-storage.ts +268 -0
  120. package/src/services/storage/separated-storage.ts +467 -0
  121. package/src/services/storage/vector-storage.ts +13 -0
  122. package/src/shared/agents/index.ts +63 -0
  123. package/src/shared/files/index.ts +99 -0
  124. package/src/shared/index.ts +32 -0
  125. package/src/shared/logging/index.ts +24 -0
  126. package/src/shared/processing/index.ts +153 -0
  127. package/src/shared/types/index.ts +25 -0
  128. package/src/targets/claude-code.ts +574 -0
  129. package/src/targets/functional/claude-code-logic.ts +185 -0
  130. package/src/targets/functional/index.ts +6 -0
  131. package/src/targets/opencode.ts +529 -0
  132. package/src/types/agent.types.ts +32 -0
  133. package/src/types/api/batch.ts +108 -0
  134. package/src/types/api/errors.ts +118 -0
  135. package/src/types/api/index.ts +55 -0
  136. package/src/types/api/requests.ts +76 -0
  137. package/src/types/api/responses.ts +180 -0
  138. package/src/types/api/websockets.ts +85 -0
  139. package/src/types/api.types.ts +9 -0
  140. package/src/types/benchmark.ts +49 -0
  141. package/src/types/cli.types.ts +87 -0
  142. package/src/types/common.types.ts +35 -0
  143. package/src/types/database.types.ts +510 -0
  144. package/src/types/mcp-config.types.ts +448 -0
  145. package/src/types/mcp.types.ts +69 -0
  146. package/src/types/memory-types.ts +63 -0
  147. package/src/types/provider.types.ts +28 -0
  148. package/src/types/rule.types.ts +24 -0
  149. package/src/types/session.types.ts +214 -0
  150. package/src/types/target-config.types.ts +295 -0
  151. package/src/types/target.types.ts +140 -0
  152. package/src/types/todo.types.ts +25 -0
  153. package/src/types.ts +40 -0
  154. package/src/utils/advanced-tokenizer.ts +191 -0
  155. package/src/utils/agent-enhancer.ts +114 -0
  156. package/src/utils/ai-model-fetcher.ts +19 -0
  157. package/src/utils/async-file-operations.ts +516 -0
  158. package/src/utils/audio-player.ts +345 -0
  159. package/src/utils/cli-output.ts +266 -0
  160. package/src/utils/codebase-helpers.ts +211 -0
  161. package/src/utils/console-ui.ts +79 -0
  162. package/src/utils/database-errors.ts +140 -0
  163. package/src/utils/debug-logger.ts +49 -0
  164. package/src/utils/error-handler.ts +53 -0
  165. package/src/utils/file-operations.ts +310 -0
  166. package/src/utils/file-scanner.ts +259 -0
  167. package/src/utils/functional/array.ts +355 -0
  168. package/src/utils/functional/index.ts +15 -0
  169. package/src/utils/functional/object.ts +279 -0
  170. package/src/utils/functional/string.ts +281 -0
  171. package/src/utils/functional.ts +543 -0
  172. package/src/utils/help.ts +20 -0
  173. package/src/utils/immutable-cache.ts +106 -0
  174. package/src/utils/index.ts +78 -0
  175. package/src/utils/jsonc.ts +158 -0
  176. package/src/utils/logger.ts +396 -0
  177. package/src/utils/mcp-config.ts +249 -0
  178. package/src/utils/memory-tui.ts +414 -0
  179. package/src/utils/models-dev.ts +91 -0
  180. package/src/utils/notifications.ts +169 -0
  181. package/src/utils/object-utils.ts +51 -0
  182. package/src/utils/parallel-operations.ts +487 -0
  183. package/src/utils/paths.ts +143 -0
  184. package/src/utils/process-manager.ts +155 -0
  185. package/src/utils/prompts.ts +120 -0
  186. package/src/utils/search-tool-builder.ts +214 -0
  187. package/src/utils/secret-utils.ts +179 -0
  188. package/src/utils/security.ts +537 -0
  189. package/src/utils/session-manager.ts +168 -0
  190. package/src/utils/session-title.ts +87 -0
  191. package/src/utils/settings.ts +182 -0
  192. package/src/utils/simplified-errors.ts +410 -0
  193. package/src/utils/sync-utils.ts +159 -0
  194. package/src/utils/target-config.ts +570 -0
  195. package/src/utils/target-utils.ts +394 -0
  196. package/src/utils/template-engine.ts +94 -0
  197. package/src/utils/test-audio.ts +71 -0
  198. package/src/utils/todo-context.ts +46 -0
  199. package/src/utils/token-counter.ts +288 -0
  200. package/dist/index.d.ts +0 -10
  201. package/dist/index.js +0 -59554
  202. package/dist/lancedb.linux-x64-gnu-b7f0jgsz.node +0 -0
  203. package/dist/lancedb.linux-x64-musl-tgcv22rx.node +0 -0
  204. package/dist/shared/chunk-25dwp0dp.js +0 -89
  205. package/dist/shared/chunk-3pjb6063.js +0 -208
  206. package/dist/shared/chunk-4d6ydpw7.js +0 -2854
  207. package/dist/shared/chunk-4wjcadjk.js +0 -225
  208. package/dist/shared/chunk-5j4w74t6.js +0 -30
  209. package/dist/shared/chunk-5j8m3dh3.js +0 -58
  210. package/dist/shared/chunk-5thh3qem.js +0 -91
  211. package/dist/shared/chunk-6g9xy73m.js +0 -252
  212. package/dist/shared/chunk-7eq34c42.js +0 -23
  213. package/dist/shared/chunk-c2gwgx3r.js +0 -115
  214. package/dist/shared/chunk-cjd3mk4c.js +0 -1320
  215. package/dist/shared/chunk-g5cv6703.js +0 -368
  216. package/dist/shared/chunk-hpkhykhq.js +0 -574
  217. package/dist/shared/chunk-m2322pdk.js +0 -122
  218. package/dist/shared/chunk-nd5fdvaq.js +0 -26
  219. package/dist/shared/chunk-pgd3m6zf.js +0 -108
  220. package/dist/shared/chunk-qk8n91hw.js +0 -494
  221. package/dist/shared/chunk-rkkn8szp.js +0 -16855
  222. package/dist/shared/chunk-t16rfxh0.js +0 -61
  223. package/dist/shared/chunk-t4fbfa5v.js +0 -19
  224. package/dist/shared/chunk-t77h86w6.js +0 -276
  225. package/dist/shared/chunk-v0ez4aef.js +0 -71
  226. package/dist/shared/chunk-v29j2r3s.js +0 -32051
  227. package/dist/shared/chunk-vfbc6ew5.js +0 -765
  228. package/dist/shared/chunk-vmeqwm1c.js +0 -204
  229. package/dist/shared/chunk-x66eh37x.js +0 -137
@@ -0,0 +1,537 @@
1
+ /**
2
+ * Security utilities for input validation, sanitization, and safe operations
3
+ * Implements defense-in-depth security principles
4
+ */
5
+
6
+ import { execFile } from 'node:child_process';
7
+ import crypto from 'node:crypto';
8
+ import path from 'node:path';
9
+ import { promisify } from 'node:util';
10
+ import { z } from 'zod';
11
+
12
+ const execFileAsync = promisify(execFile);
13
+
14
+ // ============================================================================
15
+ // INPUT VALIDATION SCHEMAS
16
+ // ============================================================================
17
+
18
+ /**
19
+ * Security-focused validation schemas
20
+ */
21
+ export const securitySchemas = {
22
+ /** Project name validation - prevents command injection and path traversal */
23
+ projectName: z
24
+ .string()
25
+ .min(1, 'Project name is required')
26
+ .max(100, 'Project name too long')
27
+ .regex(
28
+ /^[a-zA-Z0-9_-]+$/,
29
+ 'Project name can only contain letters, numbers, hyphens, and underscores'
30
+ )
31
+ .refine((name) => !/^\.+$/.test(name), 'Project name cannot be only dots')
32
+ .refine((name) => !/[<>:"|?*]/.test(name), 'Project name contains invalid characters'),
33
+
34
+ /** Branch name validation - prevents command injection */
35
+ branchName: z
36
+ .string()
37
+ .min(1, 'Branch name is required')
38
+ .max(255, 'Branch name too long')
39
+ .regex(
40
+ /^[a-zA-Z0-9/_-]+$/,
41
+ 'Branch name can only contain letters, numbers, slashes, hyphens, and underscores'
42
+ )
43
+ .refine((name) => !name.includes('..'), 'Branch name cannot contain ".."')
44
+ .refine((name) => !/^[/\\]/.test(name), 'Branch name cannot start with path separators')
45
+ .refine((name) => !/[<>:"|?*$]/.test(name), 'Branch name contains invalid characters'),
46
+
47
+ /** File path validation - prevents path traversal */
48
+ filePath: z
49
+ .string()
50
+ .min(1, 'File path is required')
51
+ .max(1000, 'File path too long')
52
+ .refine(
53
+ (filePath) => !filePath.includes('..'),
54
+ 'File path cannot contain ".." for path traversal protection'
55
+ )
56
+ .refine((filePath) => !/^[<>:"|?*]/.test(filePath), 'File path contains invalid characters'),
57
+
58
+ /** Command argument validation - prevents command injection */
59
+ commandArg: z
60
+ .string()
61
+ .max(1000, 'Command argument too long')
62
+ .refine(
63
+ (arg) => !/[<>|;&$`'"\\]/.test(arg),
64
+ 'Command argument contains potentially dangerous characters'
65
+ ),
66
+
67
+ /** Environment variable validation */
68
+ envVarName: z
69
+ .string()
70
+ .regex(/^[A-Z_][A-Z0-9_]*$/, 'Invalid environment variable name format')
71
+ .max(100, 'Environment variable name too long'),
72
+
73
+ /** URL validation for API endpoints */
74
+ url: z
75
+ .string()
76
+ .url('Invalid URL format')
77
+ .refine(
78
+ (url) => url.startsWith('https://') || url.startsWith('http://localhost'),
79
+ 'URL must be HTTPS or localhost'
80
+ )
81
+ .refine((url) => !url.includes('javascript:'), 'URL cannot contain javascript protocol'),
82
+
83
+ /** API key validation */
84
+ apiKey: z
85
+ .string()
86
+ .min(10, 'API key too short')
87
+ .max(500, 'API key too long')
88
+ .regex(/^[a-zA-Z0-9._-]+$/, 'Invalid API key format'),
89
+ };
90
+
91
+ // ============================================================================
92
+ // PATH SECURITY UTILITIES
93
+ // ============================================================================
94
+
95
+ /**
96
+ * Secure path utilities to prevent path traversal attacks
97
+ */
98
+ export const pathSecurity = {
99
+ /**
100
+ * Validates and sanitizes a file path to prevent path traversal
101
+ */
102
+ validatePath: (inputPath: string, allowedBase?: string): string => {
103
+ const validated = securitySchemas.filePath.parse(inputPath);
104
+
105
+ // Normalize the path
106
+ const normalizedPath = path.normalize(validated);
107
+
108
+ // Check for path traversal attempts
109
+ if (normalizedPath.includes('..')) {
110
+ throw new Error('Path traversal detected in file path');
111
+ }
112
+
113
+ // If base path is provided, ensure the resolved path is within bounds
114
+ if (allowedBase) {
115
+ const resolvedPath = path.resolve(allowedBase, normalizedPath);
116
+ const resolvedBase = path.resolve(allowedBase);
117
+
118
+ if (!resolvedPath.startsWith(resolvedBase)) {
119
+ throw new Error('File path escapes allowed base directory');
120
+ }
121
+
122
+ return resolvedPath;
123
+ }
124
+
125
+ return normalizedPath;
126
+ },
127
+
128
+ /**
129
+ * Checks if a path is within allowed boundaries
130
+ */
131
+ isPathSafe: (targetPath: string, allowedBase: string): boolean => {
132
+ try {
133
+ const resolvedTarget = path.resolve(targetPath);
134
+ const resolvedBase = path.resolve(allowedBase);
135
+ return resolvedTarget.startsWith(resolvedBase);
136
+ } catch {
137
+ return false;
138
+ }
139
+ },
140
+
141
+ /**
142
+ * Creates a safe file path within a base directory
143
+ */
144
+ safeJoin: (basePath: string, ...paths: string[]): string => {
145
+ const result = path.join(basePath, ...paths);
146
+
147
+ // Normalize and verify it stays within base
148
+ const normalized = path.normalize(result);
149
+ const resolvedBase = path.resolve(basePath);
150
+ const resolvedResult = path.resolve(normalized);
151
+
152
+ if (!resolvedResult.startsWith(resolvedBase)) {
153
+ throw new Error('Path traversal attempt detected in safeJoin');
154
+ }
155
+
156
+ return resolvedResult;
157
+ },
158
+ };
159
+
160
+ // ============================================================================
161
+ // COMMAND EXECUTION SECURITY
162
+ // ============================================================================
163
+
164
+ /**
165
+ * Secure command execution utilities to prevent command injection
166
+ */
167
+ export const commandSecurity = {
168
+ /**
169
+ * Safely executes a command with arguments, preventing command injection
170
+ */
171
+ async safeExecFile(
172
+ command: string,
173
+ args: string[],
174
+ options: {
175
+ cwd?: string;
176
+ timeout?: number;
177
+ maxBuffer?: number;
178
+ env?: Record<string, string>;
179
+ } = {}
180
+ ): Promise<{ stdout: string; stderr: string }> {
181
+ // Validate command
182
+ if (!/^[a-zA-Z0-9._-]+$/.test(command)) {
183
+ throw new Error(`Invalid command: ${command}`);
184
+ }
185
+
186
+ // Validate arguments
187
+ const validatedArgs = args.map((arg) => {
188
+ try {
189
+ return securitySchemas.commandArg.parse(arg);
190
+ } catch (_error) {
191
+ throw new Error(`Invalid command argument: ${arg}`);
192
+ }
193
+ });
194
+
195
+ // Set secure defaults
196
+ const secureOptions = {
197
+ timeout: options.timeout || 30000, // 30 seconds default
198
+ maxBuffer: options.maxBuffer || 1024 * 1024, // 1MB default
199
+ env: { ...process.env, ...options.env },
200
+ cwd: options.cwd || process.cwd(),
201
+ shell: false, // Never use shell to prevent injection
202
+ encoding: 'utf8' as const,
203
+ };
204
+
205
+ // Validate working directory
206
+ if (secureOptions.cwd) {
207
+ pathSecurity.validatePath(secureOptions.cwd);
208
+ }
209
+
210
+ try {
211
+ return await execFileAsync(command, validatedArgs, secureOptions);
212
+ } catch (error: any) {
213
+ // Sanitize error message to prevent information disclosure
214
+ const sanitizedError = new Error(`Command execution failed: ${command}`);
215
+ sanitizedError.code = error.code;
216
+ sanitizedError.signal = error.signal;
217
+ throw sanitizedError;
218
+ }
219
+ },
220
+
221
+ /**
222
+ * Validates that a command argument is safe for execution
223
+ */
224
+ validateCommandArgs: (args: string[]): string[] => {
225
+ return args.map((arg) => {
226
+ const validated = securitySchemas.commandArg.parse(arg);
227
+
228
+ // Additional checks for common injection patterns
229
+ const dangerousPatterns = [
230
+ /[;&|`'"\\$()]/,
231
+ /\.\./,
232
+ /\/etc\//,
233
+ /\/proc\//,
234
+ /windows\\system32/i,
235
+ ];
236
+
237
+ for (const pattern of dangerousPatterns) {
238
+ if (pattern.test(validated)) {
239
+ throw new Error(`Dangerous pattern detected in command argument: ${arg}`);
240
+ }
241
+ }
242
+
243
+ return validated;
244
+ });
245
+ },
246
+ };
247
+
248
+ // ============================================================================
249
+ // INPUT SANITIZATION UTILITIES
250
+ // ============================================================================
251
+
252
+ /**
253
+ * Input sanitization utilities
254
+ */
255
+ export const sanitize = {
256
+ /**
257
+ * Sanitizes a string for safe display
258
+ */
259
+ string: (input: string, maxLength = 1000): string => {
260
+ if (typeof input !== 'string') {
261
+ throw new Error('Input must be a string');
262
+ }
263
+
264
+ // Remove null bytes and control characters except newlines and tabs
265
+ const sanitized = input
266
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
267
+ .substring(0, maxLength);
268
+
269
+ return sanitized;
270
+ },
271
+
272
+ /**
273
+ * Sanitizes text for log messages (prevents log injection)
274
+ */
275
+ logMessage: (input: string): string => {
276
+ return input
277
+ .replace(/[\r\n]/g, ' ') // Remove line breaks
278
+ .replace(/\t/g, ' ') // Replace tabs with spaces
279
+ .substring(0, 500); // Limit length
280
+ },
281
+
282
+ /**
283
+ * Sanitizes user input for file names
284
+ */
285
+ fileName: (input: string): string => {
286
+ return input
287
+ .replace(/[^a-zA-Z0-9._-]/g, '_') // Replace invalid chars with underscores
288
+ .replace(/_{2,}/g, '_') // Replace multiple underscores
289
+ .replace(/^_|_$/g, '') // Remove leading/trailing underscores
290
+ .toLowerCase();
291
+ },
292
+
293
+ /**
294
+ * Sanitizes content for YAML front matter
295
+ */
296
+ yamlContent: (input: string): string => {
297
+ // Basic YAML sanitization - remove potentially dangerous content
298
+ return input
299
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
300
+ .replace(/<!\[CDATA\[.*?\]\]>/gs, '') // Remove CDATA sections
301
+ .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, ''); // Remove scripts
302
+ },
303
+ };
304
+
305
+ // ============================================================================
306
+ // ENVIRONMENT VARIABLE SECURITY
307
+ // ============================================================================
308
+
309
+ /**
310
+ * Environment variable validation utilities
311
+ */
312
+ export const envSecurity = {
313
+ /**
314
+ * Validates an environment variable name and value
315
+ */
316
+ validateEnvVar: (name: string, value?: string): { name: string; value: string } => {
317
+ const validatedName = securitySchemas.envVarName.parse(name);
318
+
319
+ if (value === undefined) {
320
+ throw new Error(`Environment variable ${validatedName} is required but not set`);
321
+ }
322
+
323
+ // Validate based on variable type
324
+ if (validatedName.includes('URL') || validatedName.includes('BASE_URL')) {
325
+ securitySchemas.url.parse(value);
326
+ } else if (validatedName.includes('KEY') || validatedName.includes('SECRET')) {
327
+ // For keys, check minimum length and allowed characters
328
+ if (value.length < 10) {
329
+ throw new Error(`API key ${validatedName} is too short`);
330
+ }
331
+ }
332
+
333
+ return { name: validatedName, value };
334
+ },
335
+
336
+ /**
337
+ * Safely gets an environment variable with validation
338
+ */
339
+ getEnvVar: (name: string, defaultValue?: string): string | undefined => {
340
+ try {
341
+ const value = process.env[name];
342
+ if (value === undefined) {
343
+ if (defaultValue !== undefined) {
344
+ return defaultValue;
345
+ }
346
+ throw new Error(`Environment variable ${name} is not set`);
347
+ }
348
+
349
+ const validated = envSecurity.validateEnvVar(name, value);
350
+ return validated.value;
351
+ } catch (error) {
352
+ console.warn(`Environment variable validation failed for ${name}:`, error);
353
+ return defaultValue;
354
+ }
355
+ },
356
+
357
+ /**
358
+ * Validates multiple environment variables
359
+ */
360
+ validateEnvVars: (
361
+ vars: Record<string, { required?: boolean; schema?: z.ZodSchema }>
362
+ ): Record<string, string> => {
363
+ const result: Record<string, string> = {};
364
+
365
+ for (const [name, config] of Object.entries(vars)) {
366
+ const value = process.env[name];
367
+
368
+ if (value === undefined) {
369
+ if (config.required) {
370
+ throw new Error(`Required environment variable ${name} is not set`);
371
+ }
372
+ continue;
373
+ }
374
+
375
+ try {
376
+ // Use custom schema if provided, otherwise use default validation
377
+ if (config.schema) {
378
+ result[name] = config.schema.parse(value);
379
+ } else {
380
+ const validated = envSecurity.validateEnvVar(name, value);
381
+ result[name] = validated.value;
382
+ }
383
+ } catch (error) {
384
+ throw new Error(`Environment variable ${name} validation failed: ${error}`);
385
+ }
386
+ }
387
+
388
+ return result;
389
+ },
390
+ };
391
+
392
+ // ============================================================================
393
+ // CRYPTOGRAPHIC UTILITIES
394
+ // ============================================================================
395
+
396
+ /**
397
+ * Cryptographic utilities for security
398
+ */
399
+ export const cryptoUtils = {
400
+ /**
401
+ * Generates a secure random string
402
+ */
403
+ generateSecureRandom: (length = 32): string => {
404
+ return crypto.randomBytes(length).toString('hex');
405
+ },
406
+
407
+ /**
408
+ * Generates a cryptographically secure random ID
409
+ */
410
+ generateSecureId: (): string => {
411
+ const timestamp = Date.now().toString(36);
412
+ const random = crypto.randomBytes(16).toString('hex');
413
+ return `${timestamp}-${random}`;
414
+ },
415
+
416
+ /**
417
+ * Creates a secure hash of data
418
+ */
419
+ hash: (data: string): string => {
420
+ return crypto.createHash('sha256').update(data).digest('hex');
421
+ },
422
+
423
+ /**
424
+ * Verifies data integrity with HMAC
425
+ */
426
+ verifyHMAC: (data: string, signature: string, secret: string): boolean => {
427
+ const expectedSignature = crypto.createHmac('sha256', secret).update(data).digest('hex');
428
+
429
+ return crypto.timingSafeEqual(
430
+ Buffer.from(signature, 'hex'),
431
+ Buffer.from(expectedSignature, 'hex')
432
+ );
433
+ },
434
+ };
435
+
436
+ // ============================================================================
437
+ // RATE LIMITING UTILITIES
438
+ // ============================================================================
439
+
440
+ /**
441
+ * Simple in-memory rate limiting
442
+ */
443
+ export class RateLimiter {
444
+ private requests: Map<string, number[]> = new Map();
445
+
446
+ constructor(
447
+ private maxRequests = 100,
448
+ private windowMs = 60000 // 1 minute
449
+ ) {}
450
+
451
+ isAllowed(identifier: string): boolean {
452
+ const now = Date.now();
453
+ const windowStart = now - this.windowMs;
454
+
455
+ // Get existing requests for this identifier
456
+ let timestamps = this.requests.get(identifier) || [];
457
+
458
+ // Remove old requests outside the window
459
+ timestamps = timestamps.filter((timestamp) => timestamp > windowStart);
460
+
461
+ // Check if limit exceeded
462
+ if (timestamps.length >= this.maxRequests) {
463
+ return false;
464
+ }
465
+
466
+ // Add current request
467
+ timestamps.push(now);
468
+ this.requests.set(identifier, timestamps);
469
+
470
+ return true;
471
+ }
472
+
473
+ cleanup(): void {
474
+ const now = Date.now();
475
+ const windowStart = now - this.windowMs;
476
+
477
+ for (const [identifier, timestamps] of this.requests.entries()) {
478
+ const filtered = timestamps.filter((timestamp) => timestamp > windowStart);
479
+ if (filtered.length === 0) {
480
+ this.requests.delete(identifier);
481
+ } else {
482
+ this.requests.set(identifier, filtered);
483
+ }
484
+ }
485
+ }
486
+ }
487
+
488
+ // ============================================================================
489
+ // SECURITY MIDDLEWARE
490
+ // ============================================================================
491
+
492
+ /**
493
+ * Security middleware for common patterns
494
+ */
495
+ export const securityMiddleware = {
496
+ /**
497
+ * Rate limiting middleware
498
+ */
499
+ rateLimit: (limiter: RateLimiter, getIdentifier: (req: any) => string) => {
500
+ return (req: any, res: any, next: any) => {
501
+ const identifier = getIdentifier(req);
502
+
503
+ if (!limiter.isAllowed(identifier)) {
504
+ return res.status(429).json({ error: 'Too many requests' });
505
+ }
506
+
507
+ next();
508
+ };
509
+ },
510
+
511
+ /**
512
+ * Input validation middleware
513
+ */
514
+ validateInput: (schema: z.ZodSchema, source: 'body' | 'query' | 'params' = 'body') => {
515
+ return (req: any, res: any, next: any) => {
516
+ try {
517
+ const data = req[source];
518
+ const validated = schema.parse(data);
519
+ req[source] = validated;
520
+ next();
521
+ } catch (error) {
522
+ return res.status(400).json({ error: 'Invalid input', details: error });
523
+ }
524
+ };
525
+ },
526
+ };
527
+
528
+ export default {
529
+ securitySchemas,
530
+ pathSecurity,
531
+ commandSecurity,
532
+ sanitize,
533
+ envSecurity,
534
+ cryptoUtils,
535
+ RateLimiter,
536
+ securityMiddleware,
537
+ };
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Session Manager
3
+ * Manage chat sessions for headless mode
4
+ */
5
+
6
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
7
+ import { join } from 'node:path';
8
+ import { homedir } from 'node:os';
9
+ import type { ProviderId } from '../config/ai-config.js';
10
+ import type { Session } from '../types/session.types.js';
11
+
12
+ export type { Session } from '../types/session.types.js';
13
+
14
+ const SESSION_DIR = join(homedir(), '.sylphx', 'sessions');
15
+ const LAST_SESSION_FILE = join(SESSION_DIR, '.last-session');
16
+
17
+ /**
18
+ * Ensure session directory exists
19
+ */
20
+ async function ensureSessionDir(): Promise<void> {
21
+ await mkdir(SESSION_DIR, { recursive: true });
22
+ }
23
+
24
+ /**
25
+ * Get session file path
26
+ */
27
+ function getSessionPath(sessionId: string): string {
28
+ return join(SESSION_DIR, `${sessionId}.json`);
29
+ }
30
+
31
+ /**
32
+ * Create new session
33
+ */
34
+ export async function createSession(provider: ProviderId, model: string): Promise<Session> {
35
+ await ensureSessionDir();
36
+
37
+ const session: Session = {
38
+ id: `session-${Date.now()}`,
39
+ provider,
40
+ model,
41
+ messages: [],
42
+ todos: [], // Initialize empty todos
43
+ nextTodoId: 1, // Start from 1
44
+ created: Date.now(),
45
+ updated: Date.now(),
46
+ };
47
+
48
+ await saveSession(session);
49
+ await setLastSession(session.id);
50
+
51
+ return session;
52
+ }
53
+
54
+ /**
55
+ * Save session to file
56
+ */
57
+ export async function saveSession(session: Session): Promise<void> {
58
+ await ensureSessionDir();
59
+ // Create a new object with updated timestamp (don't mutate readonly session from Zustand)
60
+ const sessionToSave = {
61
+ ...session,
62
+ updated: Date.now(),
63
+ };
64
+ const path = getSessionPath(session.id);
65
+ // Use compact JSON format for faster serialization and smaller file size
66
+ await writeFile(path, JSON.stringify(sessionToSave), 'utf8');
67
+ }
68
+
69
+ /**
70
+ * Load session from file with migration support
71
+ * Automatically adds missing fields from newer schema versions
72
+ */
73
+ export async function loadSession(sessionId: string): Promise<Session | null> {
74
+ try {
75
+ const path = getSessionPath(sessionId);
76
+ const content = await readFile(path, 'utf8');
77
+ const rawSession = JSON.parse(content) as any;
78
+
79
+ // Migration: Add todos/nextTodoId if missing
80
+ if (!rawSession.todos) {
81
+ rawSession.todos = [];
82
+ }
83
+ if (typeof rawSession.nextTodoId !== 'number') {
84
+ rawSession.nextTodoId = 1;
85
+ }
86
+
87
+ // Migration: Normalize message content format
88
+ // Old: { content: string }
89
+ // New: { content: MessagePart[] }
90
+ if (Array.isArray(rawSession.messages)) {
91
+ rawSession.messages = rawSession.messages.map((msg: any) => {
92
+ if (typeof msg.content === 'string') {
93
+ return {
94
+ ...msg,
95
+ content: [{ type: 'text', content: msg.content }],
96
+ };
97
+ }
98
+ return msg;
99
+ });
100
+ }
101
+
102
+ return rawSession as Session;
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Get last session ID
110
+ */
111
+ export async function getLastSessionId(): Promise<string | null> {
112
+ try {
113
+ const content = await readFile(LAST_SESSION_FILE, 'utf8');
114
+ return content.trim();
115
+ } catch {
116
+ return null;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Set last session ID
122
+ */
123
+ export async function setLastSession(sessionId: string): Promise<void> {
124
+ await ensureSessionDir();
125
+ await writeFile(LAST_SESSION_FILE, sessionId, 'utf8');
126
+ }
127
+
128
+ /**
129
+ * Load last session
130
+ */
131
+ export async function loadLastSession(): Promise<Session | null> {
132
+ const sessionId = await getLastSessionId();
133
+ if (!sessionId) return null;
134
+ return loadSession(sessionId);
135
+ }
136
+
137
+ /**
138
+ * Add message to session (in-memory helper for headless mode)
139
+ * Converts string content to MessagePart[] format
140
+ */
141
+ export function addMessage(
142
+ session: Session,
143
+ role: 'user' | 'assistant',
144
+ content: string
145
+ ): Session {
146
+ return {
147
+ ...session,
148
+ messages: [
149
+ ...session.messages,
150
+ {
151
+ role,
152
+ content: [{ type: 'text', content }], // Convert to MessagePart[]
153
+ timestamp: Date.now(),
154
+ },
155
+ ],
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Clear session messages but keep metadata
161
+ */
162
+ export function clearSessionMessages(session: Session): Session {
163
+ return {
164
+ ...session,
165
+ messages: [],
166
+ updated: Date.now(),
167
+ };
168
+ }