archicore 0.1.0
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/README.md +530 -0
- package/dist/analyzers/dead-code.d.ts +95 -0
- package/dist/analyzers/dead-code.js +327 -0
- package/dist/analyzers/duplication.d.ts +90 -0
- package/dist/analyzers/duplication.js +344 -0
- package/dist/analyzers/security.d.ts +79 -0
- package/dist/analyzers/security.js +484 -0
- package/dist/architecture/index.d.ts +35 -0
- package/dist/architecture/index.js +249 -0
- package/dist/cli/commands/analyzers.d.ts +6 -0
- package/dist/cli/commands/analyzers.js +431 -0
- package/dist/cli/commands/export.d.ts +6 -0
- package/dist/cli/commands/export.js +78 -0
- package/dist/cli/commands/index.d.ts +8 -0
- package/dist/cli/commands/index.js +8 -0
- package/dist/cli/commands/init.d.ts +26 -0
- package/dist/cli/commands/init.js +140 -0
- package/dist/cli/commands/interactive.d.ts +7 -0
- package/dist/cli/commands/interactive.js +522 -0
- package/dist/cli/commands/projects.d.ts +6 -0
- package/dist/cli/commands/projects.js +249 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.js +7 -0
- package/dist/cli/ui/box.d.ts +17 -0
- package/dist/cli/ui/box.js +62 -0
- package/dist/cli/ui/colors.d.ts +49 -0
- package/dist/cli/ui/colors.js +86 -0
- package/dist/cli/ui/index.d.ts +9 -0
- package/dist/cli/ui/index.js +9 -0
- package/dist/cli/ui/prompt.d.ts +34 -0
- package/dist/cli/ui/prompt.js +122 -0
- package/dist/cli/ui/spinner.d.ts +29 -0
- package/dist/cli/ui/spinner.js +80 -0
- package/dist/cli/ui/table.d.ts +33 -0
- package/dist/cli/ui/table.js +84 -0
- package/dist/cli/utils/config.d.ts +23 -0
- package/dist/cli/utils/config.js +73 -0
- package/dist/cli/utils/index.d.ts +6 -0
- package/dist/cli/utils/index.js +6 -0
- package/dist/cli/utils/session.d.ts +27 -0
- package/dist/cli/utils/session.js +117 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +295 -0
- package/dist/code-index/ast-parser.d.ts +16 -0
- package/dist/code-index/ast-parser.js +330 -0
- package/dist/code-index/dependency-graph.d.ts +16 -0
- package/dist/code-index/dependency-graph.js +161 -0
- package/dist/code-index/index.d.ts +44 -0
- package/dist/code-index/index.js +124 -0
- package/dist/code-index/symbol-extractor.d.ts +13 -0
- package/dist/code-index/symbol-extractor.js +150 -0
- package/dist/export/index.d.ts +92 -0
- package/dist/export/index.js +676 -0
- package/dist/github/github-service.d.ts +146 -0
- package/dist/github/github-service.js +609 -0
- package/dist/impact-engine/index.d.ts +25 -0
- package/dist/impact-engine/index.js +284 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +149 -0
- package/dist/metrics/index.d.ts +136 -0
- package/dist/metrics/index.js +525 -0
- package/dist/orchestrator/deepseek-optimizer.d.ts +67 -0
- package/dist/orchestrator/deepseek-optimizer.js +320 -0
- package/dist/orchestrator/index.d.ts +34 -0
- package/dist/orchestrator/index.js +305 -0
- package/dist/pr-guardian/index.d.ts +143 -0
- package/dist/pr-guardian/index.js +553 -0
- package/dist/refactoring/index.d.ts +108 -0
- package/dist/refactoring/index.js +580 -0
- package/dist/rules-engine/index.d.ts +129 -0
- package/dist/rules-engine/index.js +482 -0
- package/dist/semantic-memory/embedding-service.d.ts +24 -0
- package/dist/semantic-memory/embedding-service.js +120 -0
- package/dist/semantic-memory/index.d.ts +45 -0
- package/dist/semantic-memory/index.js +206 -0
- package/dist/semantic-memory/vector-store.d.ts +27 -0
- package/dist/semantic-memory/vector-store.js +166 -0
- package/dist/server/index.d.ts +28 -0
- package/dist/server/index.js +141 -0
- package/dist/server/middleware/api-auth.d.ts +43 -0
- package/dist/server/middleware/api-auth.js +256 -0
- package/dist/server/routes/admin.d.ts +5 -0
- package/dist/server/routes/admin.js +123 -0
- package/dist/server/routes/api.d.ts +7 -0
- package/dist/server/routes/api.js +362 -0
- package/dist/server/routes/auth.d.ts +16 -0
- package/dist/server/routes/auth.js +191 -0
- package/dist/server/routes/developer.d.ts +8 -0
- package/dist/server/routes/developer.js +439 -0
- package/dist/server/routes/github.d.ts +7 -0
- package/dist/server/routes/github.js +495 -0
- package/dist/server/routes/upload.d.ts +7 -0
- package/dist/server/routes/upload.js +196 -0
- package/dist/server/services/api-key-service.d.ts +81 -0
- package/dist/server/services/api-key-service.js +281 -0
- package/dist/server/services/auth-service.d.ts +40 -0
- package/dist/server/services/auth-service.js +315 -0
- package/dist/server/services/project-service.d.ts +123 -0
- package/dist/server/services/project-service.js +533 -0
- package/dist/server/services/token-service.d.ts +107 -0
- package/dist/server/services/token-service.js +416 -0
- package/dist/server/services/upload-service.d.ts +93 -0
- package/dist/server/services/upload-service.js +464 -0
- package/dist/types/api.d.ts +188 -0
- package/dist/types/api.js +86 -0
- package/dist/types/github.d.ts +335 -0
- package/dist/types/github.js +5 -0
- package/dist/types/index.d.ts +265 -0
- package/dist/types/index.js +32 -0
- package/dist/types/user.d.ts +69 -0
- package/dist/types/user.js +42 -0
- package/dist/utils/file-utils.d.ts +20 -0
- package/dist/utils/file-utils.js +163 -0
- package/dist/utils/logger.d.ts +17 -0
- package/dist/utils/logger.js +41 -0
- package/dist/watcher/index.d.ts +125 -0
- package/dist/watcher/index.js +397 -0
- package/package.json +71 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload Service
|
|
3
|
+
*
|
|
4
|
+
* Безопасная загрузка и распаковка ZIP/RAR архивов.
|
|
5
|
+
*
|
|
6
|
+
* Защита от:
|
|
7
|
+
* - ZIP-бомб (проверка соотношения сжатия)
|
|
8
|
+
* - Path traversal атак (../../../etc/passwd)
|
|
9
|
+
* - Опасных файлов (исполняемые, скрипты и т.д.)
|
|
10
|
+
* - Слишком больших файлов
|
|
11
|
+
*/
|
|
12
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
13
|
+
import AdmZip from 'adm-zip';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { mkdir, rm, stat, readdir, writeFile } from 'fs/promises';
|
|
16
|
+
import { existsSync } from 'fs';
|
|
17
|
+
import { Logger } from '../../utils/logger.js';
|
|
18
|
+
// Опасные расширения файлов
|
|
19
|
+
const DANGEROUS_EXTENSIONS = new Set([
|
|
20
|
+
// Исполняемые
|
|
21
|
+
'.exe', '.dll', '.so', '.dylib', '.bin', '.com', '.bat', '.cmd', '.ps1', '.vbs',
|
|
22
|
+
'.msi', '.msp', '.scr', '.pif', '.gadget', '.hta', '.cpl', '.msc', '.jar',
|
|
23
|
+
// Скрипты с shell
|
|
24
|
+
'.sh', '.bash', '.zsh', '.fish', '.ksh',
|
|
25
|
+
// Архивы-матрёшки (могут содержать вредоносный код)
|
|
26
|
+
'.zip', '.rar', '.7z', '.tar', '.gz', '.bz2', '.xz',
|
|
27
|
+
// Потенциально опасные
|
|
28
|
+
'.reg', '.inf', '.lnk', '.url', '.scf'
|
|
29
|
+
]);
|
|
30
|
+
// Разрешённые расширения для кода
|
|
31
|
+
const ALLOWED_CODE_EXTENSIONS = new Set([
|
|
32
|
+
// JavaScript/TypeScript
|
|
33
|
+
'.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs',
|
|
34
|
+
// Vue/React/Angular
|
|
35
|
+
'.vue', '.svelte',
|
|
36
|
+
// Стили
|
|
37
|
+
'.css', '.scss', '.sass', '.less', '.styl',
|
|
38
|
+
// Разметка
|
|
39
|
+
'.html', '.htm', '.xml', '.svg',
|
|
40
|
+
// Python
|
|
41
|
+
'.py', '.pyw', '.pyx', '.pyd', '.pyi',
|
|
42
|
+
// Go
|
|
43
|
+
'.go',
|
|
44
|
+
// Rust
|
|
45
|
+
'.rs',
|
|
46
|
+
// Java/Kotlin/Scala
|
|
47
|
+
'.java', '.kt', '.kts', '.scala',
|
|
48
|
+
// C/C++
|
|
49
|
+
'.c', '.cpp', '.cc', '.cxx', '.h', '.hpp', '.hh',
|
|
50
|
+
// C#
|
|
51
|
+
'.cs',
|
|
52
|
+
// PHP
|
|
53
|
+
'.php', '.phtml',
|
|
54
|
+
// Ruby
|
|
55
|
+
'.rb', '.erb', '.rake',
|
|
56
|
+
// Swift
|
|
57
|
+
'.swift',
|
|
58
|
+
// Конфиги и данные
|
|
59
|
+
'.json', '.yaml', '.yml', '.toml', '.ini', '.env.example', '.env.sample',
|
|
60
|
+
'.lock', '.config', '.conf',
|
|
61
|
+
// Документация
|
|
62
|
+
'.md', '.mdx', '.txt', '.rst',
|
|
63
|
+
// GraphQL
|
|
64
|
+
'.graphql', '.gql',
|
|
65
|
+
// Другое
|
|
66
|
+
'.sql', '.prisma', '.proto'
|
|
67
|
+
]);
|
|
68
|
+
// Паттерны опасных путей
|
|
69
|
+
const DANGEROUS_PATH_PATTERNS = [
|
|
70
|
+
/\.\./, // Path traversal
|
|
71
|
+
/^\/etc\//, // System files
|
|
72
|
+
/^\/var\//,
|
|
73
|
+
/^\/usr\//,
|
|
74
|
+
/^\/bin\//,
|
|
75
|
+
/^\/sbin\//,
|
|
76
|
+
/^C:\\Windows/i, // Windows system
|
|
77
|
+
/^C:\\Program Files/i,
|
|
78
|
+
/^C:\\Users\\.*\\AppData/i,
|
|
79
|
+
/\0/, // Null byte injection
|
|
80
|
+
];
|
|
81
|
+
export class UploadService {
|
|
82
|
+
config;
|
|
83
|
+
constructor(config) {
|
|
84
|
+
this.config = {
|
|
85
|
+
maxFileSize: config?.maxFileSize || 100 * 1024 * 1024, // 100MB default
|
|
86
|
+
maxExtractedSize: config?.maxExtractedSize || 500 * 1024 * 1024, // 500MB extracted
|
|
87
|
+
maxCompressionRatio: config?.maxCompressionRatio || 100, // Max 100x compression
|
|
88
|
+
uploadsDir: config?.uploadsDir || '.archicore/uploads',
|
|
89
|
+
extractDir: config?.extractDir || '.archicore/projects'
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Проверка безопасности архива БЕЗ распаковки
|
|
94
|
+
*/
|
|
95
|
+
async scanArchive(filePath) {
|
|
96
|
+
const threats = [];
|
|
97
|
+
const warnings = [];
|
|
98
|
+
try {
|
|
99
|
+
const fileStats = await stat(filePath);
|
|
100
|
+
// Проверка размера файла
|
|
101
|
+
if (fileStats.size > this.config.maxFileSize) {
|
|
102
|
+
threats.push({
|
|
103
|
+
type: 'oversized',
|
|
104
|
+
description: `File size ${this.formatBytes(fileStats.size)} exceeds limit ${this.formatBytes(this.config.maxFileSize)}`,
|
|
105
|
+
severity: 'high'
|
|
106
|
+
});
|
|
107
|
+
return { safe: false, threats, warnings };
|
|
108
|
+
}
|
|
109
|
+
const zip = new AdmZip(filePath);
|
|
110
|
+
const entries = zip.getEntries();
|
|
111
|
+
let totalUncompressedSize = 0;
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
const entryName = entry.entryName;
|
|
114
|
+
const ext = path.extname(entryName).toLowerCase();
|
|
115
|
+
// 1. Проверка на path traversal
|
|
116
|
+
if (this.isPathTraversal(entryName)) {
|
|
117
|
+
threats.push({
|
|
118
|
+
type: 'path_traversal',
|
|
119
|
+
description: `Suspicious path detected: ${entryName}`,
|
|
120
|
+
file: entryName,
|
|
121
|
+
severity: 'critical'
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// 2. Проверка на опасные файлы
|
|
125
|
+
if (DANGEROUS_EXTENSIONS.has(ext)) {
|
|
126
|
+
threats.push({
|
|
127
|
+
type: 'dangerous_file',
|
|
128
|
+
description: `Dangerous file type: ${ext}`,
|
|
129
|
+
file: entryName,
|
|
130
|
+
severity: 'high'
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
// 3. Проверка на symlinks
|
|
134
|
+
if (entry.header.attr === 0xA1FF0000 || entry.isDirectory && entryName.includes('->')) {
|
|
135
|
+
threats.push({
|
|
136
|
+
type: 'symlink',
|
|
137
|
+
description: 'Symbolic link detected',
|
|
138
|
+
file: entryName,
|
|
139
|
+
severity: 'medium'
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
// 4. Предупреждение о неизвестных типах файлов
|
|
143
|
+
if (!entry.isDirectory && !ALLOWED_CODE_EXTENSIONS.has(ext) && !DANGEROUS_EXTENSIONS.has(ext)) {
|
|
144
|
+
// Исключаем файлы без расширения (могут быть конфигами)
|
|
145
|
+
if (ext && ext !== '') {
|
|
146
|
+
warnings.push(`Unknown file type will be skipped: ${entryName}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// 5. Накапливаем размер для проверки zip-бомбы
|
|
150
|
+
if (!entry.isDirectory) {
|
|
151
|
+
totalUncompressedSize += entry.header.size;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// 6. Проверка на zip-бомбу (соотношение сжатия)
|
|
155
|
+
const compressionRatio = totalUncompressedSize / fileStats.size;
|
|
156
|
+
if (compressionRatio > this.config.maxCompressionRatio) {
|
|
157
|
+
threats.push({
|
|
158
|
+
type: 'zip_bomb',
|
|
159
|
+
description: `Suspicious compression ratio: ${compressionRatio.toFixed(1)}x (limit: ${this.config.maxCompressionRatio}x)`,
|
|
160
|
+
severity: 'critical'
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
// 7. Проверка общего размера после распаковки
|
|
164
|
+
if (totalUncompressedSize > this.config.maxExtractedSize) {
|
|
165
|
+
threats.push({
|
|
166
|
+
type: 'oversized',
|
|
167
|
+
description: `Extracted size ${this.formatBytes(totalUncompressedSize)} exceeds limit ${this.formatBytes(this.config.maxExtractedSize)}`,
|
|
168
|
+
severity: 'high'
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const hasCriticalThreats = threats.some(t => t.severity === 'critical' || t.severity === 'high');
|
|
172
|
+
return {
|
|
173
|
+
safe: !hasCriticalThreats,
|
|
174
|
+
threats,
|
|
175
|
+
warnings
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
Logger.error('Archive scan failed:', error);
|
|
180
|
+
threats.push({
|
|
181
|
+
type: 'dangerous_file',
|
|
182
|
+
description: `Failed to scan archive: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
183
|
+
severity: 'critical'
|
|
184
|
+
});
|
|
185
|
+
return { safe: false, threats, warnings };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Безопасная распаковка архива
|
|
190
|
+
*/
|
|
191
|
+
async extractArchive(filePath, uploadId) {
|
|
192
|
+
const warnings = [];
|
|
193
|
+
const skippedFiles = [];
|
|
194
|
+
let extractedSize = 0;
|
|
195
|
+
let fileCount = 0;
|
|
196
|
+
const projectPath = path.join(this.config.extractDir, uploadId);
|
|
197
|
+
try {
|
|
198
|
+
// Сначала сканируем
|
|
199
|
+
const scanResult = await this.scanArchive(filePath);
|
|
200
|
+
if (!scanResult.safe) {
|
|
201
|
+
return {
|
|
202
|
+
success: false,
|
|
203
|
+
uploadId,
|
|
204
|
+
projectPath: '',
|
|
205
|
+
error: `Security scan failed: ${scanResult.threats.map(t => t.description).join('; ')}`,
|
|
206
|
+
warnings: scanResult.warnings,
|
|
207
|
+
stats: {
|
|
208
|
+
originalSize: 0,
|
|
209
|
+
extractedSize: 0,
|
|
210
|
+
fileCount: 0,
|
|
211
|
+
skippedFiles: []
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
warnings.push(...scanResult.warnings);
|
|
216
|
+
// Создаём директорию проекта
|
|
217
|
+
await mkdir(projectPath, { recursive: true });
|
|
218
|
+
const fileStats = await stat(filePath);
|
|
219
|
+
const zip = new AdmZip(filePath);
|
|
220
|
+
const entries = zip.getEntries();
|
|
221
|
+
// Безопасная распаковка по одному файлу
|
|
222
|
+
for (const entry of entries) {
|
|
223
|
+
if (entry.isDirectory)
|
|
224
|
+
continue;
|
|
225
|
+
const entryName = entry.entryName;
|
|
226
|
+
const ext = path.extname(entryName).toLowerCase();
|
|
227
|
+
// Пропускаем опасные и неизвестные файлы
|
|
228
|
+
if (DANGEROUS_EXTENSIONS.has(ext)) {
|
|
229
|
+
skippedFiles.push(`${entryName} (dangerous extension)`);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
// Пропускаем symlinks
|
|
233
|
+
if (entry.header.attr === 0xA1FF0000) {
|
|
234
|
+
skippedFiles.push(`${entryName} (symlink)`);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
// Проверка path traversal ещё раз
|
|
238
|
+
if (this.isPathTraversal(entryName)) {
|
|
239
|
+
skippedFiles.push(`${entryName} (path traversal)`);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
// Нормализуем путь
|
|
243
|
+
const safePath = this.sanitizePath(entryName);
|
|
244
|
+
const fullPath = path.join(projectPath, safePath);
|
|
245
|
+
// Проверяем что путь всё ещё внутри projectPath
|
|
246
|
+
const resolvedPath = path.resolve(fullPath);
|
|
247
|
+
const resolvedProjectPath = path.resolve(projectPath);
|
|
248
|
+
if (!resolvedPath.startsWith(resolvedProjectPath)) {
|
|
249
|
+
skippedFiles.push(`${entryName} (escaped project directory)`);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
// Создаём директорию
|
|
253
|
+
const dir = path.dirname(fullPath);
|
|
254
|
+
await mkdir(dir, { recursive: true });
|
|
255
|
+
// Извлекаем файл
|
|
256
|
+
const content = entry.getData();
|
|
257
|
+
extractedSize += content.length;
|
|
258
|
+
// Проверяем лимит размера на ходу
|
|
259
|
+
if (extractedSize > this.config.maxExtractedSize) {
|
|
260
|
+
// Откатываем
|
|
261
|
+
await rm(projectPath, { recursive: true, force: true });
|
|
262
|
+
return {
|
|
263
|
+
success: false,
|
|
264
|
+
uploadId,
|
|
265
|
+
projectPath: '',
|
|
266
|
+
error: `Extracted size exceeds limit of ${this.formatBytes(this.config.maxExtractedSize)}`,
|
|
267
|
+
warnings,
|
|
268
|
+
stats: {
|
|
269
|
+
originalSize: fileStats.size,
|
|
270
|
+
extractedSize,
|
|
271
|
+
fileCount,
|
|
272
|
+
skippedFiles
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
await writeFile(fullPath, content);
|
|
277
|
+
fileCount++;
|
|
278
|
+
}
|
|
279
|
+
Logger.success(`Extracted ${fileCount} files to ${projectPath}`);
|
|
280
|
+
return {
|
|
281
|
+
success: true,
|
|
282
|
+
uploadId,
|
|
283
|
+
projectPath,
|
|
284
|
+
warnings,
|
|
285
|
+
stats: {
|
|
286
|
+
originalSize: fileStats.size,
|
|
287
|
+
extractedSize,
|
|
288
|
+
fileCount,
|
|
289
|
+
skippedFiles
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
Logger.error('Extract failed:', error);
|
|
295
|
+
// Очищаем при ошибке
|
|
296
|
+
if (existsSync(projectPath)) {
|
|
297
|
+
await rm(projectPath, { recursive: true, force: true });
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
success: false,
|
|
301
|
+
uploadId,
|
|
302
|
+
projectPath: '',
|
|
303
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
304
|
+
warnings,
|
|
305
|
+
stats: {
|
|
306
|
+
originalSize: 0,
|
|
307
|
+
extractedSize,
|
|
308
|
+
fileCount,
|
|
309
|
+
skippedFiles
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Полный процесс загрузки: сохранение + сканирование + распаковка
|
|
316
|
+
*/
|
|
317
|
+
async processUpload(buffer, originalName) {
|
|
318
|
+
const uploadId = uuidv4();
|
|
319
|
+
const ext = path.extname(originalName).toLowerCase();
|
|
320
|
+
// Проверяем формат
|
|
321
|
+
if (ext !== '.zip') {
|
|
322
|
+
return {
|
|
323
|
+
success: false,
|
|
324
|
+
uploadId,
|
|
325
|
+
projectPath: '',
|
|
326
|
+
error: 'Only ZIP files are supported',
|
|
327
|
+
warnings: [],
|
|
328
|
+
stats: {
|
|
329
|
+
originalSize: buffer.length,
|
|
330
|
+
extractedSize: 0,
|
|
331
|
+
fileCount: 0,
|
|
332
|
+
skippedFiles: []
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
// Создаём директорию для загрузок
|
|
337
|
+
await mkdir(this.config.uploadsDir, { recursive: true });
|
|
338
|
+
await mkdir(this.config.extractDir, { recursive: true });
|
|
339
|
+
// Сохраняем временный файл
|
|
340
|
+
const tempPath = path.join(this.config.uploadsDir, `${uploadId}${ext}`);
|
|
341
|
+
await writeFile(tempPath, buffer);
|
|
342
|
+
try {
|
|
343
|
+
// Распаковываем
|
|
344
|
+
const result = await this.extractArchive(tempPath, uploadId);
|
|
345
|
+
// Удаляем временный архив
|
|
346
|
+
await rm(tempPath, { force: true });
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
// Очищаем при ошибке
|
|
351
|
+
await rm(tempPath, { force: true });
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Удаление проекта
|
|
357
|
+
*/
|
|
358
|
+
async deleteProject(uploadId) {
|
|
359
|
+
const projectPath = path.join(this.config.extractDir, uploadId);
|
|
360
|
+
if (existsSync(projectPath)) {
|
|
361
|
+
await rm(projectPath, { recursive: true, force: true });
|
|
362
|
+
Logger.info(`Deleted project: ${uploadId}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Проверка на path traversal
|
|
367
|
+
*/
|
|
368
|
+
isPathTraversal(filePath) {
|
|
369
|
+
// Нормализуем слеши
|
|
370
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
371
|
+
for (const pattern of DANGEROUS_PATH_PATTERNS) {
|
|
372
|
+
if (pattern.test(normalized)) {
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Очистка пути от опасных элементов
|
|
380
|
+
*/
|
|
381
|
+
sanitizePath(filePath) {
|
|
382
|
+
// Убираем ведущие слеши и path traversal
|
|
383
|
+
return filePath
|
|
384
|
+
.replace(/\\/g, '/')
|
|
385
|
+
.replace(/^\/+/, '')
|
|
386
|
+
.replace(/\.\.\//g, '')
|
|
387
|
+
.replace(/\/\.\.\//g, '/')
|
|
388
|
+
.split('/')
|
|
389
|
+
.filter(part => part !== '..' && part !== '')
|
|
390
|
+
.join('/');
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Форматирование размера в читаемый вид
|
|
394
|
+
*/
|
|
395
|
+
formatBytes(bytes) {
|
|
396
|
+
if (bytes === 0)
|
|
397
|
+
return '0 Bytes';
|
|
398
|
+
const k = 1024;
|
|
399
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
400
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
401
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Получить информацию о загруженных проектах
|
|
405
|
+
*/
|
|
406
|
+
async listUploadedProjects() {
|
|
407
|
+
const projects = [];
|
|
408
|
+
if (!existsSync(this.config.extractDir)) {
|
|
409
|
+
return projects;
|
|
410
|
+
}
|
|
411
|
+
const dirs = await readdir(this.config.extractDir, { withFileTypes: true });
|
|
412
|
+
for (const dir of dirs) {
|
|
413
|
+
if (dir.isDirectory()) {
|
|
414
|
+
const projectPath = path.join(this.config.extractDir, dir.name);
|
|
415
|
+
const stats = await stat(projectPath);
|
|
416
|
+
const files = await this.countFiles(projectPath);
|
|
417
|
+
projects.push({
|
|
418
|
+
id: dir.name,
|
|
419
|
+
path: projectPath,
|
|
420
|
+
size: await this.getDirSize(projectPath),
|
|
421
|
+
files,
|
|
422
|
+
createdAt: stats.birthtime
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return projects;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Подсчёт файлов в директории рекурсивно
|
|
430
|
+
*/
|
|
431
|
+
async countFiles(dir) {
|
|
432
|
+
let count = 0;
|
|
433
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
434
|
+
for (const entry of entries) {
|
|
435
|
+
const fullPath = path.join(dir, entry.name);
|
|
436
|
+
if (entry.isDirectory()) {
|
|
437
|
+
count += await this.countFiles(fullPath);
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
count++;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return count;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Размер директории рекурсивно
|
|
447
|
+
*/
|
|
448
|
+
async getDirSize(dir) {
|
|
449
|
+
let size = 0;
|
|
450
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
451
|
+
for (const entry of entries) {
|
|
452
|
+
const fullPath = path.join(dir, entry.name);
|
|
453
|
+
if (entry.isDirectory()) {
|
|
454
|
+
size += await this.getDirSize(fullPath);
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
const stats = await stat(fullPath);
|
|
458
|
+
size += stats.size;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return size;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
//# sourceMappingURL=upload-service.js.map
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Types for ArchiCore Developer API
|
|
3
|
+
*
|
|
4
|
+
* Система токенизированной тарификации как у OpenAI/Anthropic
|
|
5
|
+
*/
|
|
6
|
+
export interface ApiKey {
|
|
7
|
+
id: string;
|
|
8
|
+
userId: string;
|
|
9
|
+
name: string;
|
|
10
|
+
keyHash: string;
|
|
11
|
+
keyPrefix: string;
|
|
12
|
+
permissions: ApiPermission[];
|
|
13
|
+
rateLimit: RateLimitConfig;
|
|
14
|
+
createdAt: Date;
|
|
15
|
+
lastUsedAt: Date | null;
|
|
16
|
+
expiresAt: Date | null;
|
|
17
|
+
isActive: boolean;
|
|
18
|
+
}
|
|
19
|
+
export type ApiPermission = 'read:projects' | 'write:projects' | 'analyze:impact' | 'analyze:security' | 'analyze:full' | 'search:semantic' | 'ask:architect' | 'export:data' | 'admin:all';
|
|
20
|
+
export interface RateLimitConfig {
|
|
21
|
+
requestsPerMinute: number;
|
|
22
|
+
requestsPerDay: number;
|
|
23
|
+
tokensPerMinute: number;
|
|
24
|
+
tokensPerDay: number;
|
|
25
|
+
}
|
|
26
|
+
export interface TokenUsage {
|
|
27
|
+
id: string;
|
|
28
|
+
apiKeyId: string;
|
|
29
|
+
userId: string;
|
|
30
|
+
operation: ApiOperation;
|
|
31
|
+
inputTokens: number;
|
|
32
|
+
outputTokens: number;
|
|
33
|
+
totalTokens: number;
|
|
34
|
+
cost: number;
|
|
35
|
+
timestamp: Date;
|
|
36
|
+
projectId?: string;
|
|
37
|
+
metadata?: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
export type ApiOperation = 'index' | 'analyze_impact' | 'analyze_security' | 'analyze_full' | 'search_semantic' | 'ask_architect' | 'export' | 'refactoring' | 'metrics' | 'rules_check' | 'dead_code' | 'duplication';
|
|
40
|
+
export interface PricingTier {
|
|
41
|
+
name: string;
|
|
42
|
+
pricePerMillionTokens: number;
|
|
43
|
+
includedTokens: number;
|
|
44
|
+
monthlyPrice: number;
|
|
45
|
+
}
|
|
46
|
+
export declare const PRICING_TIERS: Record<string, PricingTier>;
|
|
47
|
+
export declare const OPERATION_TOKEN_COSTS: Record<ApiOperation, {
|
|
48
|
+
base: number;
|
|
49
|
+
perFile: number;
|
|
50
|
+
perKb: number;
|
|
51
|
+
}>;
|
|
52
|
+
export interface BillingAccount {
|
|
53
|
+
id: string;
|
|
54
|
+
userId: string;
|
|
55
|
+
pricingTier: string;
|
|
56
|
+
balance: number;
|
|
57
|
+
tokensUsedThisMonth: number;
|
|
58
|
+
tokensIncludedThisMonth: number;
|
|
59
|
+
billingCycleStart: Date;
|
|
60
|
+
billingCycleEnd: Date;
|
|
61
|
+
autoRecharge: boolean;
|
|
62
|
+
autoRechargeThreshold: number;
|
|
63
|
+
autoRechargeAmount: number;
|
|
64
|
+
paymentMethods: PaymentMethod[];
|
|
65
|
+
invoices: Invoice[];
|
|
66
|
+
}
|
|
67
|
+
export interface PaymentMethod {
|
|
68
|
+
id: string;
|
|
69
|
+
type: 'card' | 'paypal' | 'bank';
|
|
70
|
+
last4?: string;
|
|
71
|
+
brand?: string;
|
|
72
|
+
isDefault: boolean;
|
|
73
|
+
expiresAt?: Date;
|
|
74
|
+
}
|
|
75
|
+
export interface Invoice {
|
|
76
|
+
id: string;
|
|
77
|
+
userId: string;
|
|
78
|
+
amount: number;
|
|
79
|
+
currency: string;
|
|
80
|
+
status: 'pending' | 'paid' | 'failed' | 'refunded';
|
|
81
|
+
createdAt: Date;
|
|
82
|
+
paidAt?: Date;
|
|
83
|
+
items: InvoiceItem[];
|
|
84
|
+
pdfUrl?: string;
|
|
85
|
+
}
|
|
86
|
+
export interface InvoiceItem {
|
|
87
|
+
description: string;
|
|
88
|
+
quantity: number;
|
|
89
|
+
unitPrice: number;
|
|
90
|
+
total: number;
|
|
91
|
+
}
|
|
92
|
+
export interface RateLimitState {
|
|
93
|
+
apiKeyId: string;
|
|
94
|
+
requestsThisMinute: number;
|
|
95
|
+
requestsToday: number;
|
|
96
|
+
tokensThisMinute: number;
|
|
97
|
+
tokensToday: number;
|
|
98
|
+
minuteResetAt: Date;
|
|
99
|
+
dayResetAt: Date;
|
|
100
|
+
}
|
|
101
|
+
export interface RateLimitResult {
|
|
102
|
+
allowed: boolean;
|
|
103
|
+
remaining: {
|
|
104
|
+
requestsPerMinute: number;
|
|
105
|
+
requestsPerDay: number;
|
|
106
|
+
tokensPerMinute: number;
|
|
107
|
+
tokensPerDay: number;
|
|
108
|
+
};
|
|
109
|
+
resetAt: {
|
|
110
|
+
minute: Date;
|
|
111
|
+
day: Date;
|
|
112
|
+
};
|
|
113
|
+
retryAfter?: number;
|
|
114
|
+
}
|
|
115
|
+
export interface UsageStats {
|
|
116
|
+
period: 'hour' | 'day' | 'week' | 'month';
|
|
117
|
+
startDate: Date;
|
|
118
|
+
endDate: Date;
|
|
119
|
+
totalRequests: number;
|
|
120
|
+
totalTokens: number;
|
|
121
|
+
totalCost: number;
|
|
122
|
+
byOperation: Record<ApiOperation, {
|
|
123
|
+
requests: number;
|
|
124
|
+
tokens: number;
|
|
125
|
+
cost: number;
|
|
126
|
+
}>;
|
|
127
|
+
byDay?: Array<{
|
|
128
|
+
date: string;
|
|
129
|
+
requests: number;
|
|
130
|
+
tokens: number;
|
|
131
|
+
cost: number;
|
|
132
|
+
}>;
|
|
133
|
+
}
|
|
134
|
+
export interface ApiKeyCreateRequest {
|
|
135
|
+
name: string;
|
|
136
|
+
permissions?: ApiPermission[];
|
|
137
|
+
expiresIn?: number;
|
|
138
|
+
rateLimit?: Partial<RateLimitConfig>;
|
|
139
|
+
}
|
|
140
|
+
export interface ApiKeyCreateResponse {
|
|
141
|
+
success: boolean;
|
|
142
|
+
apiKey?: {
|
|
143
|
+
id: string;
|
|
144
|
+
name: string;
|
|
145
|
+
key: string;
|
|
146
|
+
keyPrefix: string;
|
|
147
|
+
permissions: ApiPermission[];
|
|
148
|
+
expiresAt: Date | null;
|
|
149
|
+
};
|
|
150
|
+
error?: string;
|
|
151
|
+
}
|
|
152
|
+
export interface ApiKeyListResponse {
|
|
153
|
+
success: boolean;
|
|
154
|
+
keys?: Array<{
|
|
155
|
+
id: string;
|
|
156
|
+
name: string;
|
|
157
|
+
keyPrefix: string;
|
|
158
|
+
permissions: ApiPermission[];
|
|
159
|
+
createdAt: Date;
|
|
160
|
+
lastUsedAt: Date | null;
|
|
161
|
+
expiresAt: Date | null;
|
|
162
|
+
isActive: boolean;
|
|
163
|
+
}>;
|
|
164
|
+
error?: string;
|
|
165
|
+
}
|
|
166
|
+
export interface UsageResponse {
|
|
167
|
+
success: boolean;
|
|
168
|
+
usage?: {
|
|
169
|
+
currentPeriod: UsageStats;
|
|
170
|
+
billing: {
|
|
171
|
+
tier: string;
|
|
172
|
+
balance: number;
|
|
173
|
+
tokensUsed: number;
|
|
174
|
+
tokensIncluded: number;
|
|
175
|
+
tokensRemaining: number;
|
|
176
|
+
estimatedCost: number;
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
error?: string;
|
|
180
|
+
}
|
|
181
|
+
export interface ApiRequestContext {
|
|
182
|
+
apiKey: ApiKey;
|
|
183
|
+
userId: string;
|
|
184
|
+
rateLimitState: RateLimitState;
|
|
185
|
+
billingAccount: BillingAccount;
|
|
186
|
+
}
|
|
187
|
+
export declare const DEFAULT_RATE_LIMITS: Record<string, RateLimitConfig>;
|
|
188
|
+
//# sourceMappingURL=api.d.ts.map
|