@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.
- package/CHANGELOG.md +12 -0
- package/package.json +10 -9
- package/src/commands/codebase-command.ts +168 -0
- package/src/commands/flow-command.ts +1137 -0
- package/src/commands/flow-orchestrator.ts +296 -0
- package/src/commands/hook-command.ts +444 -0
- package/src/commands/init-command.ts +92 -0
- package/src/commands/init-core.ts +322 -0
- package/src/commands/knowledge-command.ts +161 -0
- package/src/commands/run-command.ts +120 -0
- package/src/components/benchmark-monitor.tsx +331 -0
- package/src/components/reindex-progress.tsx +261 -0
- package/src/composables/functional/index.ts +14 -0
- package/src/composables/functional/useEnvironment.ts +171 -0
- package/src/composables/functional/useFileSystem.ts +139 -0
- package/src/composables/index.ts +5 -0
- package/src/composables/useEnv.ts +13 -0
- package/src/composables/useRuntimeConfig.ts +27 -0
- package/src/composables/useTargetConfig.ts +45 -0
- package/src/config/ai-config.ts +376 -0
- package/src/config/constants.ts +35 -0
- package/src/config/index.ts +27 -0
- package/src/config/rules.ts +43 -0
- package/src/config/servers.ts +371 -0
- package/src/config/targets.ts +126 -0
- package/src/core/agent-loader.ts +141 -0
- package/src/core/agent-manager.ts +174 -0
- package/src/core/ai-sdk.ts +603 -0
- package/src/core/app-factory.ts +381 -0
- package/src/core/builtin-agents.ts +9 -0
- package/src/core/command-system.ts +550 -0
- package/src/core/config-system.ts +550 -0
- package/src/core/connection-pool.ts +390 -0
- package/src/core/di-container.ts +155 -0
- package/src/core/error-handling.ts +519 -0
- package/src/core/formatting/bytes.test.ts +115 -0
- package/src/core/formatting/bytes.ts +64 -0
- package/src/core/functional/async.ts +313 -0
- package/src/core/functional/either.ts +109 -0
- package/src/core/functional/error-handler.ts +135 -0
- package/src/core/functional/error-types.ts +311 -0
- package/src/core/functional/index.ts +19 -0
- package/src/core/functional/option.ts +142 -0
- package/src/core/functional/pipe.ts +189 -0
- package/src/core/functional/result.ts +204 -0
- package/src/core/functional/validation.ts +138 -0
- package/src/core/headless-display.ts +96 -0
- package/src/core/index.ts +6 -0
- package/src/core/installers/file-installer.ts +303 -0
- package/src/core/installers/mcp-installer.ts +213 -0
- package/src/core/interfaces/index.ts +22 -0
- package/src/core/interfaces/repository.interface.ts +91 -0
- package/src/core/interfaces/service.interface.ts +133 -0
- package/src/core/interfaces.ts +129 -0
- package/src/core/loop-controller.ts +200 -0
- package/src/core/result.ts +351 -0
- package/src/core/rule-loader.ts +147 -0
- package/src/core/rule-manager.ts +240 -0
- package/src/core/service-config.ts +252 -0
- package/src/core/session-service.ts +121 -0
- package/src/core/state-detector.ts +389 -0
- package/src/core/storage-factory.ts +115 -0
- package/src/core/stream-handler.ts +288 -0
- package/src/core/target-manager.ts +161 -0
- package/src/core/type-utils.ts +427 -0
- package/src/core/unified-storage.ts +456 -0
- package/src/core/upgrade-manager.ts +300 -0
- package/src/core/validation/limit.test.ts +155 -0
- package/src/core/validation/limit.ts +46 -0
- package/src/core/validation/query.test.ts +44 -0
- package/src/core/validation/query.ts +20 -0
- package/src/db/auto-migrate.ts +322 -0
- package/src/db/base-database-client.ts +144 -0
- package/src/db/cache-db.ts +218 -0
- package/src/db/cache-schema.ts +75 -0
- package/src/db/database.ts +70 -0
- package/src/db/index.ts +252 -0
- package/src/db/memory-db.ts +153 -0
- package/src/db/memory-schema.ts +29 -0
- package/src/db/schema.ts +289 -0
- package/src/db/session-repository.ts +733 -0
- package/src/domains/codebase/index.ts +5 -0
- package/src/domains/codebase/tools.ts +139 -0
- package/src/domains/index.ts +8 -0
- package/src/domains/knowledge/index.ts +10 -0
- package/src/domains/knowledge/resources.ts +537 -0
- package/src/domains/knowledge/tools.ts +174 -0
- package/src/domains/utilities/index.ts +6 -0
- package/src/domains/utilities/time/index.ts +5 -0
- package/src/domains/utilities/time/tools.ts +291 -0
- package/src/index.ts +211 -0
- package/src/services/agent-service.ts +273 -0
- package/src/services/claude-config-service.ts +252 -0
- package/src/services/config-service.ts +258 -0
- package/src/services/evaluation-service.ts +271 -0
- package/src/services/functional/evaluation-logic.ts +296 -0
- package/src/services/functional/file-processor.ts +273 -0
- package/src/services/functional/index.ts +12 -0
- package/src/services/index.ts +13 -0
- package/src/services/mcp-service.ts +432 -0
- package/src/services/memory.service.ts +476 -0
- package/src/services/search/base-indexer.ts +156 -0
- package/src/services/search/codebase-indexer-types.ts +38 -0
- package/src/services/search/codebase-indexer.ts +647 -0
- package/src/services/search/embeddings-provider.ts +455 -0
- package/src/services/search/embeddings.ts +316 -0
- package/src/services/search/functional-indexer.ts +323 -0
- package/src/services/search/index.ts +27 -0
- package/src/services/search/indexer.ts +380 -0
- package/src/services/search/knowledge-indexer.ts +422 -0
- package/src/services/search/semantic-search.ts +244 -0
- package/src/services/search/tfidf.ts +559 -0
- package/src/services/search/unified-search-service.ts +888 -0
- package/src/services/smart-config-service.ts +385 -0
- package/src/services/storage/cache-storage.ts +487 -0
- package/src/services/storage/drizzle-storage.ts +581 -0
- package/src/services/storage/index.ts +15 -0
- package/src/services/storage/lancedb-vector-storage.ts +494 -0
- package/src/services/storage/memory-storage.ts +268 -0
- package/src/services/storage/separated-storage.ts +467 -0
- package/src/services/storage/vector-storage.ts +13 -0
- package/src/shared/agents/index.ts +63 -0
- package/src/shared/files/index.ts +99 -0
- package/src/shared/index.ts +32 -0
- package/src/shared/logging/index.ts +24 -0
- package/src/shared/processing/index.ts +153 -0
- package/src/shared/types/index.ts +25 -0
- package/src/targets/claude-code.ts +574 -0
- package/src/targets/functional/claude-code-logic.ts +185 -0
- package/src/targets/functional/index.ts +6 -0
- package/src/targets/opencode.ts +529 -0
- package/src/types/agent.types.ts +32 -0
- package/src/types/api/batch.ts +108 -0
- package/src/types/api/errors.ts +118 -0
- package/src/types/api/index.ts +55 -0
- package/src/types/api/requests.ts +76 -0
- package/src/types/api/responses.ts +180 -0
- package/src/types/api/websockets.ts +85 -0
- package/src/types/api.types.ts +9 -0
- package/src/types/benchmark.ts +49 -0
- package/src/types/cli.types.ts +87 -0
- package/src/types/common.types.ts +35 -0
- package/src/types/database.types.ts +510 -0
- package/src/types/mcp-config.types.ts +448 -0
- package/src/types/mcp.types.ts +69 -0
- package/src/types/memory-types.ts +63 -0
- package/src/types/provider.types.ts +28 -0
- package/src/types/rule.types.ts +24 -0
- package/src/types/session.types.ts +214 -0
- package/src/types/target-config.types.ts +295 -0
- package/src/types/target.types.ts +140 -0
- package/src/types/todo.types.ts +25 -0
- package/src/types.ts +40 -0
- package/src/utils/advanced-tokenizer.ts +191 -0
- package/src/utils/agent-enhancer.ts +114 -0
- package/src/utils/ai-model-fetcher.ts +19 -0
- package/src/utils/async-file-operations.ts +516 -0
- package/src/utils/audio-player.ts +345 -0
- package/src/utils/cli-output.ts +266 -0
- package/src/utils/codebase-helpers.ts +211 -0
- package/src/utils/console-ui.ts +79 -0
- package/src/utils/database-errors.ts +140 -0
- package/src/utils/debug-logger.ts +49 -0
- package/src/utils/error-handler.ts +53 -0
- package/src/utils/file-operations.ts +310 -0
- package/src/utils/file-scanner.ts +259 -0
- package/src/utils/functional/array.ts +355 -0
- package/src/utils/functional/index.ts +15 -0
- package/src/utils/functional/object.ts +279 -0
- package/src/utils/functional/string.ts +281 -0
- package/src/utils/functional.ts +543 -0
- package/src/utils/help.ts +20 -0
- package/src/utils/immutable-cache.ts +106 -0
- package/src/utils/index.ts +78 -0
- package/src/utils/jsonc.ts +158 -0
- package/src/utils/logger.ts +396 -0
- package/src/utils/mcp-config.ts +249 -0
- package/src/utils/memory-tui.ts +414 -0
- package/src/utils/models-dev.ts +91 -0
- package/src/utils/notifications.ts +169 -0
- package/src/utils/object-utils.ts +51 -0
- package/src/utils/parallel-operations.ts +487 -0
- package/src/utils/paths.ts +143 -0
- package/src/utils/process-manager.ts +155 -0
- package/src/utils/prompts.ts +120 -0
- package/src/utils/search-tool-builder.ts +214 -0
- package/src/utils/secret-utils.ts +179 -0
- package/src/utils/security.ts +537 -0
- package/src/utils/session-manager.ts +168 -0
- package/src/utils/session-title.ts +87 -0
- package/src/utils/settings.ts +182 -0
- package/src/utils/simplified-errors.ts +410 -0
- package/src/utils/sync-utils.ts +159 -0
- package/src/utils/target-config.ts +570 -0
- package/src/utils/target-utils.ts +394 -0
- package/src/utils/template-engine.ts +94 -0
- package/src/utils/test-audio.ts +71 -0
- package/src/utils/todo-context.ts +46 -0
- package/src/utils/token-counter.ts +288 -0
- package/dist/index.d.ts +0 -10
- package/dist/index.js +0 -59554
- package/dist/lancedb.linux-x64-gnu-b7f0jgsz.node +0 -0
- package/dist/lancedb.linux-x64-musl-tgcv22rx.node +0 -0
- package/dist/shared/chunk-25dwp0dp.js +0 -89
- package/dist/shared/chunk-3pjb6063.js +0 -208
- package/dist/shared/chunk-4d6ydpw7.js +0 -2854
- package/dist/shared/chunk-4wjcadjk.js +0 -225
- package/dist/shared/chunk-5j4w74t6.js +0 -30
- package/dist/shared/chunk-5j8m3dh3.js +0 -58
- package/dist/shared/chunk-5thh3qem.js +0 -91
- package/dist/shared/chunk-6g9xy73m.js +0 -252
- package/dist/shared/chunk-7eq34c42.js +0 -23
- package/dist/shared/chunk-c2gwgx3r.js +0 -115
- package/dist/shared/chunk-cjd3mk4c.js +0 -1320
- package/dist/shared/chunk-g5cv6703.js +0 -368
- package/dist/shared/chunk-hpkhykhq.js +0 -574
- package/dist/shared/chunk-m2322pdk.js +0 -122
- package/dist/shared/chunk-nd5fdvaq.js +0 -26
- package/dist/shared/chunk-pgd3m6zf.js +0 -108
- package/dist/shared/chunk-qk8n91hw.js +0 -494
- package/dist/shared/chunk-rkkn8szp.js +0 -16855
- package/dist/shared/chunk-t16rfxh0.js +0 -61
- package/dist/shared/chunk-t4fbfa5v.js +0 -19
- package/dist/shared/chunk-t77h86w6.js +0 -276
- package/dist/shared/chunk-v0ez4aef.js +0 -71
- package/dist/shared/chunk-v29j2r3s.js +0 -32051
- package/dist/shared/chunk-vfbc6ew5.js +0 -765
- package/dist/shared/chunk-vmeqwm1c.js +0 -204
- 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
|
+
}
|