archicore 0.1.8 → 0.2.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/dist/cli/commands/auth.d.ts +4 -0
- package/dist/cli/commands/auth.js +58 -0
- package/dist/cli/commands/index.d.ts +2 -1
- package/dist/cli/commands/index.js +2 -1
- package/dist/cli/commands/interactive.js +107 -59
- package/dist/cli/ui/index.d.ts +1 -0
- package/dist/cli/ui/index.js +1 -0
- package/dist/cli/ui/progress.d.ts +57 -0
- package/dist/cli/ui/progress.js +149 -0
- package/dist/cli/utils/error-handler.d.ts +40 -0
- package/dist/cli/utils/error-handler.js +234 -0
- package/dist/cli/utils/index.d.ts +2 -0
- package/dist/cli/utils/index.js +3 -0
- package/dist/cli/utils/project-selector.d.ts +33 -0
- package/dist/cli/utils/project-selector.js +148 -0
- package/dist/cli.js +3 -1
- package/dist/code-index/ast-parser.d.ts +1 -1
- package/dist/code-index/ast-parser.js +9 -3
- package/dist/code-index/index.d.ts +1 -1
- package/dist/code-index/index.js +2 -2
- package/dist/github/github-service.js +8 -1
- package/dist/orchestrator/index.js +29 -1
- package/dist/semantic-memory/embedding-service.d.ts +4 -1
- package/dist/semantic-memory/embedding-service.js +58 -11
- package/dist/semantic-memory/index.d.ts +1 -1
- package/dist/semantic-memory/index.js +54 -7
- package/dist/server/config.d.ts +52 -0
- package/dist/server/config.js +88 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +115 -10
- package/dist/server/routes/admin.js +3 -3
- package/dist/server/routes/api.js +199 -2
- package/dist/server/routes/auth.js +79 -5
- package/dist/server/routes/device-auth.js +1 -1
- package/dist/server/routes/github.js +33 -0
- package/dist/server/services/auth-service.d.ts +5 -0
- package/dist/server/services/auth-service.js +10 -0
- package/dist/server/services/project-service.d.ts +15 -1
- package/dist/server/services/project-service.js +185 -26
- package/dist/types/user.d.ts +2 -2
- package/dist/types/user.js +13 -4
- package/package.json +9 -1
|
@@ -24,19 +24,66 @@ export class SemanticMemory {
|
|
|
24
24
|
await this.vectorStore.initialize(dimension);
|
|
25
25
|
Logger.success(`Semantic Memory initialized (dimension: ${dimension})`);
|
|
26
26
|
}
|
|
27
|
-
async indexSymbols(symbols, asts) {
|
|
27
|
+
async indexSymbols(symbols, asts, progressCallback) {
|
|
28
28
|
Logger.progress('Indexing symbols into semantic memory...');
|
|
29
|
-
const
|
|
30
|
-
|
|
29
|
+
const symbolArray = Array.from(symbols.values());
|
|
30
|
+
const totalSymbols = symbolArray.length;
|
|
31
|
+
if (totalSymbols === 0) {
|
|
32
|
+
Logger.warn('No symbols to index');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// Step 1: Prepare all texts and metadata for batch embedding
|
|
36
|
+
Logger.progress('Preparing texts for embedding...');
|
|
37
|
+
const textsToEmbed = [];
|
|
38
|
+
const symbolData = [];
|
|
39
|
+
for (const symbol of symbolArray) {
|
|
31
40
|
const ast = asts.get(symbol.filePath);
|
|
32
41
|
if (!ast)
|
|
33
42
|
continue;
|
|
34
43
|
const code = this.extractSymbolCode(symbol, ast);
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
if (!code)
|
|
45
|
+
continue;
|
|
46
|
+
// Prepare text with context (same format as generateCodeEmbedding)
|
|
47
|
+
let context = `File: ${symbol.filePath}\n`;
|
|
48
|
+
context += `Symbol: ${symbol.kind} ${symbol.name}\n`;
|
|
49
|
+
const text = this.embeddingService.prepareCodeForEmbedding(code, context);
|
|
50
|
+
textsToEmbed.push(text);
|
|
51
|
+
symbolData.push({ symbol, code });
|
|
37
52
|
}
|
|
38
|
-
|
|
39
|
-
|
|
53
|
+
Logger.progress(`Generating embeddings for ${textsToEmbed.length} symbols (batch mode)...`);
|
|
54
|
+
// Step 2: Generate all embeddings in batch (MUCH faster!)
|
|
55
|
+
const embeddings = await this.embeddingService.generateBatchEmbeddings(textsToEmbed, progressCallback);
|
|
56
|
+
// Step 3: Create chunks with embeddings
|
|
57
|
+
Logger.progress('Creating chunks and storing in vector DB...');
|
|
58
|
+
const chunks = [];
|
|
59
|
+
for (let i = 0; i < symbolData.length; i++) {
|
|
60
|
+
const { symbol, code } = symbolData[i];
|
|
61
|
+
const embedding = embeddings[i];
|
|
62
|
+
const metadata = {
|
|
63
|
+
filePath: symbol.filePath,
|
|
64
|
+
startLine: symbol.location.startLine,
|
|
65
|
+
endLine: symbol.location.endLine,
|
|
66
|
+
type: this.mapSymbolKindToChunkType(symbol.kind),
|
|
67
|
+
symbols: [symbol.name],
|
|
68
|
+
tags: [symbol.kind, this.extractDomain(symbol.filePath)]
|
|
69
|
+
};
|
|
70
|
+
chunks.push({
|
|
71
|
+
id: symbol.id,
|
|
72
|
+
content: code,
|
|
73
|
+
embedding,
|
|
74
|
+
metadata
|
|
75
|
+
});
|
|
76
|
+
// Batch upsert every 500 chunks to avoid memory issues
|
|
77
|
+
if (chunks.length >= 500) {
|
|
78
|
+
await this.vectorStore.upsertChunks(chunks);
|
|
79
|
+
chunks.length = 0;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Upsert remaining chunks
|
|
83
|
+
if (chunks.length > 0) {
|
|
84
|
+
await this.vectorStore.upsertChunks(chunks);
|
|
85
|
+
}
|
|
86
|
+
Logger.success(`Indexed ${symbolData.length} symbols (batch mode)`);
|
|
40
87
|
}
|
|
41
88
|
async indexModules(asts) {
|
|
42
89
|
Logger.progress('Indexing modules into semantic memory...');
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Configuration
|
|
3
|
+
* Centralized configuration for security, CORS, rate limiting
|
|
4
|
+
*/
|
|
5
|
+
export interface ServerSecurityConfig {
|
|
6
|
+
cors: {
|
|
7
|
+
allowedOrigins: string[];
|
|
8
|
+
allowedMethods: string[];
|
|
9
|
+
allowedHeaders: string[];
|
|
10
|
+
credentials: boolean;
|
|
11
|
+
};
|
|
12
|
+
rateLimit: {
|
|
13
|
+
windowMs: number;
|
|
14
|
+
max: number;
|
|
15
|
+
message: string;
|
|
16
|
+
standardHeaders: boolean;
|
|
17
|
+
legacyHeaders: boolean;
|
|
18
|
+
};
|
|
19
|
+
compression: {
|
|
20
|
+
level: number;
|
|
21
|
+
threshold: number;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export declare const securityConfig: ServerSecurityConfig;
|
|
25
|
+
export declare const apiRateLimits: {
|
|
26
|
+
analysis: {
|
|
27
|
+
windowMs: number;
|
|
28
|
+
max: number;
|
|
29
|
+
};
|
|
30
|
+
auth: {
|
|
31
|
+
windowMs: number;
|
|
32
|
+
max: number;
|
|
33
|
+
};
|
|
34
|
+
search: {
|
|
35
|
+
windowMs: number;
|
|
36
|
+
max: number;
|
|
37
|
+
};
|
|
38
|
+
webhook: {
|
|
39
|
+
windowMs: number;
|
|
40
|
+
max: number;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
export declare const isDevelopment: boolean;
|
|
44
|
+
export declare const isProduction: boolean;
|
|
45
|
+
export declare const cookieConfig: {
|
|
46
|
+
name: string;
|
|
47
|
+
maxAge: number;
|
|
48
|
+
secure: boolean;
|
|
49
|
+
httpOnly: boolean;
|
|
50
|
+
sameSite: "lax";
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Configuration
|
|
3
|
+
* Centralized configuration for security, CORS, rate limiting
|
|
4
|
+
*/
|
|
5
|
+
// Get allowed origins from environment or defaults
|
|
6
|
+
function getAllowedOrigins() {
|
|
7
|
+
const envOrigins = process.env.CORS_ORIGINS;
|
|
8
|
+
if (envOrigins) {
|
|
9
|
+
return envOrigins.split(',').map(o => o.trim());
|
|
10
|
+
}
|
|
11
|
+
// Default origins for development and production
|
|
12
|
+
const defaults = [
|
|
13
|
+
'http://localhost:3000',
|
|
14
|
+
'http://localhost:5173',
|
|
15
|
+
'http://127.0.0.1:3000',
|
|
16
|
+
'http://127.0.0.1:5173',
|
|
17
|
+
];
|
|
18
|
+
// Add production domains if configured
|
|
19
|
+
if (process.env.PRODUCTION_DOMAIN) {
|
|
20
|
+
defaults.push(`https://${process.env.PRODUCTION_DOMAIN}`);
|
|
21
|
+
defaults.push(`https://www.${process.env.PRODUCTION_DOMAIN}`);
|
|
22
|
+
}
|
|
23
|
+
// ArchiCore production domains
|
|
24
|
+
defaults.push('https://archicore.io');
|
|
25
|
+
defaults.push('https://www.archicore.io');
|
|
26
|
+
defaults.push('https://app.archicore.io');
|
|
27
|
+
return defaults;
|
|
28
|
+
}
|
|
29
|
+
export const securityConfig = {
|
|
30
|
+
cors: {
|
|
31
|
+
allowedOrigins: getAllowedOrigins(),
|
|
32
|
+
allowedMethods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
|
33
|
+
allowedHeaders: [
|
|
34
|
+
'Content-Type',
|
|
35
|
+
'Authorization',
|
|
36
|
+
'X-API-Key',
|
|
37
|
+
'X-Request-ID',
|
|
38
|
+
'X-Requested-With',
|
|
39
|
+
],
|
|
40
|
+
credentials: true,
|
|
41
|
+
},
|
|
42
|
+
rateLimit: {
|
|
43
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
44
|
+
max: parseInt(process.env.RATE_LIMIT_MAX || '100', 10), // 100 requests per window
|
|
45
|
+
message: 'Too many requests, please try again later.',
|
|
46
|
+
standardHeaders: true, // Return rate limit info in headers
|
|
47
|
+
legacyHeaders: false,
|
|
48
|
+
},
|
|
49
|
+
compression: {
|
|
50
|
+
level: 6, // Default compression level (1-9)
|
|
51
|
+
threshold: 1024, // Only compress responses > 1KB
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
// API-specific rate limits
|
|
55
|
+
export const apiRateLimits = {
|
|
56
|
+
// Stricter limits for expensive operations
|
|
57
|
+
analysis: {
|
|
58
|
+
windowMs: 60 * 60 * 1000, // 1 hour
|
|
59
|
+
max: 50, // 50 analysis requests per hour
|
|
60
|
+
},
|
|
61
|
+
// Auth endpoints (prevent brute force)
|
|
62
|
+
auth: {
|
|
63
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
64
|
+
max: 10, // 10 auth attempts per window
|
|
65
|
+
},
|
|
66
|
+
// Search/query endpoints
|
|
67
|
+
search: {
|
|
68
|
+
windowMs: 60 * 1000, // 1 minute
|
|
69
|
+
max: 30, // 30 searches per minute
|
|
70
|
+
},
|
|
71
|
+
// Webhook endpoints (GitHub, etc)
|
|
72
|
+
webhook: {
|
|
73
|
+
windowMs: 60 * 1000, // 1 minute
|
|
74
|
+
max: 100, // 100 webhooks per minute
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
// Environment detection
|
|
78
|
+
export const isDevelopment = process.env.NODE_ENV !== 'production';
|
|
79
|
+
export const isProduction = process.env.NODE_ENV === 'production';
|
|
80
|
+
// Cookie consent config
|
|
81
|
+
export const cookieConfig = {
|
|
82
|
+
name: 'archicore_consent',
|
|
83
|
+
maxAge: 365 * 24 * 60 * 60 * 1000, // 1 year
|
|
84
|
+
secure: isProduction,
|
|
85
|
+
httpOnly: false, // Needs to be accessible by JS
|
|
86
|
+
sameSite: 'lax',
|
|
87
|
+
};
|
|
88
|
+
//# sourceMappingURL=config.js.map
|
package/dist/server/index.d.ts
CHANGED
package/dist/server/index.js
CHANGED
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
import 'dotenv/config';
|
|
11
11
|
import express from 'express';
|
|
12
12
|
import cors from 'cors';
|
|
13
|
+
import compression from 'compression';
|
|
14
|
+
import helmet from 'helmet';
|
|
15
|
+
import morgan from 'morgan';
|
|
16
|
+
import rateLimit from 'express-rate-limit';
|
|
13
17
|
import { createServer } from 'http';
|
|
14
18
|
import path from 'path';
|
|
15
19
|
import { fileURLToPath } from 'url';
|
|
@@ -23,6 +27,32 @@ import { githubRouter } from './routes/github.js';
|
|
|
23
27
|
import deviceAuthRouter from './routes/device-auth.js';
|
|
24
28
|
const __filename = fileURLToPath(import.meta.url);
|
|
25
29
|
const __dirname = path.dirname(__filename);
|
|
30
|
+
// CORS whitelist - настройте под свои домены
|
|
31
|
+
const DEFAULT_CORS_ORIGINS = [
|
|
32
|
+
'http://localhost:3000',
|
|
33
|
+
'http://localhost:5173',
|
|
34
|
+
'http://127.0.0.1:3000',
|
|
35
|
+
'http://127.0.0.1:5173',
|
|
36
|
+
// Production domains (добавьте свои)
|
|
37
|
+
// 'https://archicore.io',
|
|
38
|
+
// 'https://app.archicore.io',
|
|
39
|
+
];
|
|
40
|
+
// Rate limiting конфигурация
|
|
41
|
+
const createRateLimiter = (windowMs, max, message) => rateLimit({
|
|
42
|
+
windowMs,
|
|
43
|
+
max,
|
|
44
|
+
message: { error: message, retryAfter: Math.ceil(windowMs / 1000) },
|
|
45
|
+
standardHeaders: true, // Return rate limit info in headers
|
|
46
|
+
legacyHeaders: false, // Disable X-RateLimit-* headers
|
|
47
|
+
handler: (_req, res) => {
|
|
48
|
+
res.status(429).json({
|
|
49
|
+
error: message,
|
|
50
|
+
retryAfter: Math.ceil(windowMs / 1000),
|
|
51
|
+
limit: max,
|
|
52
|
+
windowMs,
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
});
|
|
26
56
|
export class ArchiCoreServer {
|
|
27
57
|
app;
|
|
28
58
|
server = null;
|
|
@@ -35,23 +65,98 @@ export class ArchiCoreServer {
|
|
|
35
65
|
this.setupErrorHandling();
|
|
36
66
|
}
|
|
37
67
|
setupMiddleware() {
|
|
38
|
-
//
|
|
68
|
+
// Security headers (helmet) - disabled in development for easier testing
|
|
69
|
+
// Set HELMET_ENABLED=true in production
|
|
70
|
+
const helmetEnabled = process.env.HELMET_ENABLED === 'true';
|
|
71
|
+
if (helmetEnabled) {
|
|
72
|
+
this.app.use(helmet({
|
|
73
|
+
contentSecurityPolicy: {
|
|
74
|
+
directives: {
|
|
75
|
+
defaultSrc: ["'self'"],
|
|
76
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
77
|
+
scriptSrc: ["'self'", "'unsafe-inline'"],
|
|
78
|
+
imgSrc: ["'self'", "data:", "https:"],
|
|
79
|
+
connectSrc: ["'self'", "https://api.jina.ai", "https://api.openai.com", "https://api.anthropic.com"],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
crossOriginResourcePolicy: { policy: 'cross-origin' },
|
|
83
|
+
crossOriginOpenerPolicy: { policy: 'same-origin-allow-popups' },
|
|
84
|
+
crossOriginEmbedderPolicy: false,
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
// Gzip compression
|
|
88
|
+
this.app.use(compression({
|
|
89
|
+
level: 6, // Баланс между скоростью и сжатием
|
|
90
|
+
threshold: 1024, // Сжимать только > 1KB
|
|
91
|
+
filter: (req, res) => {
|
|
92
|
+
if (req.headers['x-no-compression'])
|
|
93
|
+
return false;
|
|
94
|
+
return compression.filter(req, res);
|
|
95
|
+
},
|
|
96
|
+
}));
|
|
97
|
+
// HTTP request logging
|
|
98
|
+
const morganFormat = process.env.NODE_ENV === 'production'
|
|
99
|
+
? 'combined'
|
|
100
|
+
: 'dev';
|
|
101
|
+
this.app.use(morgan(morganFormat, {
|
|
102
|
+
stream: {
|
|
103
|
+
write: (message) => Logger.info(message.trim()),
|
|
104
|
+
},
|
|
105
|
+
skip: (req) => req.path === '/health', // Skip health checks
|
|
106
|
+
}));
|
|
107
|
+
// Global rate limiting (100 requests per minute)
|
|
108
|
+
const globalLimiter = createRateLimiter(this.config.rateLimitWindowMs || 60 * 1000, this.config.rateLimitMax || 100, 'Too many requests, please try again later');
|
|
109
|
+
this.app.use(globalLimiter);
|
|
110
|
+
// CORS configuration
|
|
111
|
+
// Set CORS_RESTRICT=true in .env to enable whitelist mode
|
|
112
|
+
const restrictCors = process.env.CORS_RESTRICT === 'true';
|
|
113
|
+
const allowedOrigins = this.config.corsOrigins || DEFAULT_CORS_ORIGINS;
|
|
39
114
|
this.app.use(cors({
|
|
40
|
-
origin:
|
|
41
|
-
|
|
42
|
-
|
|
115
|
+
origin: restrictCors
|
|
116
|
+
? (origin, callback) => {
|
|
117
|
+
// Allow requests with no origin (mobile apps, curl, etc.)
|
|
118
|
+
if (!origin)
|
|
119
|
+
return callback(null, true);
|
|
120
|
+
// Check whitelist
|
|
121
|
+
if (allowedOrigins.includes(origin) || process.env.NODE_ENV === 'development') {
|
|
122
|
+
return callback(null, true);
|
|
123
|
+
}
|
|
124
|
+
// Check wildcard patterns (e.g., *.archicore.io)
|
|
125
|
+
const isAllowed = allowedOrigins.some(allowed => {
|
|
126
|
+
if (allowed.startsWith('*.')) {
|
|
127
|
+
const domain = allowed.slice(2);
|
|
128
|
+
return origin.endsWith(domain);
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
});
|
|
132
|
+
if (isAllowed)
|
|
133
|
+
return callback(null, true);
|
|
134
|
+
Logger.warn(`CORS blocked origin: ${origin}`);
|
|
135
|
+
callback(new Error('Not allowed by CORS'));
|
|
136
|
+
}
|
|
137
|
+
: true, // Allow all origins when CORS_RESTRICT is not set
|
|
138
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
|
139
|
+
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key', 'X-Request-ID'],
|
|
140
|
+
credentials: true,
|
|
141
|
+
maxAge: 86400, // Cache preflight for 24 hours
|
|
43
142
|
}));
|
|
44
143
|
// JSON парсинг
|
|
45
144
|
this.app.use(express.json({ limit: '50mb' }));
|
|
46
145
|
this.app.use(express.urlencoded({ extended: true }));
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
146
|
+
// Request ID для трейсинга
|
|
147
|
+
this.app.use((req, res, next) => {
|
|
148
|
+
const requestId = req.headers['x-request-id'] ||
|
|
149
|
+
`req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
150
|
+
req.headers['x-request-id'] = requestId;
|
|
151
|
+
res.setHeader('X-Request-ID', requestId);
|
|
53
152
|
next();
|
|
54
153
|
});
|
|
154
|
+
// Статические файлы (фронтенд)
|
|
155
|
+
const publicPath = path.join(__dirname, '../../public');
|
|
156
|
+
this.app.use(express.static(publicPath, {
|
|
157
|
+
maxAge: process.env.NODE_ENV === 'production' ? '1d' : 0,
|
|
158
|
+
etag: true,
|
|
159
|
+
}));
|
|
55
160
|
}
|
|
56
161
|
setupRoutes() {
|
|
57
162
|
// Auth routes
|
|
@@ -6,7 +6,7 @@ import { AuthService } from '../services/auth-service.js';
|
|
|
6
6
|
import { authMiddleware, adminMiddleware } from './auth.js';
|
|
7
7
|
import { Logger } from '../../utils/logger.js';
|
|
8
8
|
export const adminRouter = Router();
|
|
9
|
-
const authService =
|
|
9
|
+
const authService = AuthService.getInstance();
|
|
10
10
|
// All admin routes require authentication and admin role
|
|
11
11
|
adminRouter.use(authMiddleware);
|
|
12
12
|
adminRouter.use(adminMiddleware);
|
|
@@ -52,9 +52,9 @@ adminRouter.put('/users/:id/tier', async (req, res) => {
|
|
|
52
52
|
try {
|
|
53
53
|
const { id } = req.params;
|
|
54
54
|
const { tier } = req.body;
|
|
55
|
-
const validTiers = ['free', 'mid', 'pro', 'admin'];
|
|
55
|
+
const validTiers = ['free', 'mid', 'pro', 'enterprise', 'admin'];
|
|
56
56
|
if (!tier || !validTiers.includes(tier)) {
|
|
57
|
-
res.status(400).json({ error: 'Invalid tier' });
|
|
57
|
+
res.status(400).json({ error: 'Invalid tier. Valid tiers: ' + validTiers.join(', ') });
|
|
58
58
|
return;
|
|
59
59
|
}
|
|
60
60
|
const updated = await authService.updateUserTier(id, tier);
|
|
@@ -4,13 +4,16 @@
|
|
|
4
4
|
* REST API для Architecture Digital Twin
|
|
5
5
|
*/
|
|
6
6
|
import { Router } from 'express';
|
|
7
|
-
import { ProjectService } from '../services/project-service.js';
|
|
7
|
+
import { ProjectService, subscribeToProgress } from '../services/project-service.js';
|
|
8
|
+
import { AuthService } from '../services/auth-service.js';
|
|
8
9
|
import { Logger } from '../../utils/logger.js';
|
|
9
10
|
import { ExportManager } from '../../export/index.js';
|
|
10
11
|
import { authMiddleware } from './auth.js';
|
|
12
|
+
import { FileUtils } from '../../utils/file-utils.js';
|
|
11
13
|
export const apiRouter = Router();
|
|
12
14
|
// Singleton сервиса проектов
|
|
13
15
|
const projectService = new ProjectService();
|
|
16
|
+
const authService = AuthService.getInstance();
|
|
14
17
|
// Helper to get user ID from request
|
|
15
18
|
const getUserId = (req) => {
|
|
16
19
|
return req.user?.id;
|
|
@@ -46,8 +49,21 @@ apiRouter.post('/projects', authMiddleware, async (req, res) => {
|
|
|
46
49
|
res.status(401).json({ error: 'Authentication required' });
|
|
47
50
|
return;
|
|
48
51
|
}
|
|
52
|
+
// Check project creation limit
|
|
53
|
+
const usageResult = await authService.checkAndUpdateUsage(userId, 'project');
|
|
54
|
+
if (!usageResult.allowed) {
|
|
55
|
+
res.status(429).json({
|
|
56
|
+
error: 'Project limit reached',
|
|
57
|
+
message: `You have reached your daily project limit (${usageResult.limit})`,
|
|
58
|
+
usage: { used: usageResult.limit, limit: usageResult.limit, remaining: 0 }
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
49
62
|
const project = await projectService.createProject(name, projectPath, userId);
|
|
50
|
-
res.json({
|
|
63
|
+
res.json({
|
|
64
|
+
project,
|
|
65
|
+
usage: { used: usageResult.limit - usageResult.remaining, limit: usageResult.limit, remaining: usageResult.remaining }
|
|
66
|
+
});
|
|
51
67
|
}
|
|
52
68
|
catch (error) {
|
|
53
69
|
Logger.error('Failed to create project:', error);
|
|
@@ -215,10 +231,23 @@ apiRouter.post('/projects/:id/simulate', authMiddleware, checkProjectAccess, asy
|
|
|
215
231
|
try {
|
|
216
232
|
const { id } = req.params;
|
|
217
233
|
const { change } = req.body;
|
|
234
|
+
const userId = getUserId(req);
|
|
218
235
|
if (!change) {
|
|
219
236
|
res.status(400).json({ error: 'Change description is required' });
|
|
220
237
|
return;
|
|
221
238
|
}
|
|
239
|
+
// Check request limit
|
|
240
|
+
if (userId) {
|
|
241
|
+
const usageResult = await authService.checkAndUpdateUsage(userId, 'request');
|
|
242
|
+
if (!usageResult.allowed) {
|
|
243
|
+
res.status(429).json({
|
|
244
|
+
error: 'Request limit reached',
|
|
245
|
+
message: `You have reached your daily request limit (${usageResult.limit})`,
|
|
246
|
+
usage: { used: usageResult.limit, limit: usageResult.limit, remaining: 0 }
|
|
247
|
+
});
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
222
251
|
const simulation = await projectService.simulateChange(id, change);
|
|
223
252
|
res.json(simulation);
|
|
224
253
|
}
|
|
@@ -235,10 +264,23 @@ apiRouter.post('/projects/:id/search', authMiddleware, checkProjectAccess, async
|
|
|
235
264
|
try {
|
|
236
265
|
const { id } = req.params;
|
|
237
266
|
const { query, limit = 10 } = req.body;
|
|
267
|
+
const userId = getUserId(req);
|
|
238
268
|
if (!query) {
|
|
239
269
|
res.status(400).json({ error: 'Query is required' });
|
|
240
270
|
return;
|
|
241
271
|
}
|
|
272
|
+
// Check request limit
|
|
273
|
+
if (userId) {
|
|
274
|
+
const usageResult = await authService.checkAndUpdateUsage(userId, 'request');
|
|
275
|
+
if (!usageResult.allowed) {
|
|
276
|
+
res.status(429).json({
|
|
277
|
+
error: 'Request limit reached',
|
|
278
|
+
message: `You have reached your daily request limit (${usageResult.limit})`,
|
|
279
|
+
usage: { used: usageResult.limit, limit: usageResult.limit, remaining: 0 }
|
|
280
|
+
});
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
242
284
|
const results = await projectService.semanticSearch(id, query, limit);
|
|
243
285
|
res.json({ results });
|
|
244
286
|
}
|
|
@@ -255,10 +297,23 @@ apiRouter.post('/projects/:id/ask', authMiddleware, checkProjectAccess, async (r
|
|
|
255
297
|
try {
|
|
256
298
|
const { id } = req.params;
|
|
257
299
|
const { question, language } = req.body;
|
|
300
|
+
const userId = getUserId(req);
|
|
258
301
|
if (!question) {
|
|
259
302
|
res.status(400).json({ error: 'Question is required' });
|
|
260
303
|
return;
|
|
261
304
|
}
|
|
305
|
+
// Check request limit
|
|
306
|
+
if (userId) {
|
|
307
|
+
const usageResult = await authService.checkAndUpdateUsage(userId, 'request');
|
|
308
|
+
if (!usageResult.allowed) {
|
|
309
|
+
res.status(429).json({
|
|
310
|
+
error: 'Request limit reached',
|
|
311
|
+
message: `You have reached your daily request limit (${usageResult.limit})`,
|
|
312
|
+
usage: { used: usageResult.limit, limit: usageResult.limit, remaining: 0 }
|
|
313
|
+
});
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
262
317
|
const answer = await projectService.askArchitect(id, question, language || 'en');
|
|
263
318
|
res.json({ answer });
|
|
264
319
|
}
|
|
@@ -426,6 +481,19 @@ apiRouter.post('/projects/:id/export', authMiddleware, checkProjectAccess, async
|
|
|
426
481
|
apiRouter.post('/projects/:id/full-analysis', authMiddleware, checkProjectAccess, async (req, res) => {
|
|
427
482
|
try {
|
|
428
483
|
const { id } = req.params;
|
|
484
|
+
const userId = getUserId(req);
|
|
485
|
+
// Check full analysis limit
|
|
486
|
+
if (userId) {
|
|
487
|
+
const usageResult = await authService.checkAndUpdateUsage(userId, 'analysis');
|
|
488
|
+
if (!usageResult.allowed) {
|
|
489
|
+
res.status(429).json({
|
|
490
|
+
error: 'Analysis limit reached',
|
|
491
|
+
message: `You have reached your daily full analysis limit (${usageResult.limit})`,
|
|
492
|
+
usage: { used: usageResult.limit, limit: usageResult.limit, remaining: 0 }
|
|
493
|
+
});
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
429
497
|
const result = await projectService.runFullAnalysis(id);
|
|
430
498
|
res.json(result);
|
|
431
499
|
}
|
|
@@ -442,6 +510,19 @@ apiRouter.post('/projects/:id/documentation', authMiddleware, checkProjectAccess
|
|
|
442
510
|
try {
|
|
443
511
|
const { id } = req.params;
|
|
444
512
|
const { format = 'markdown', language = 'en' } = req.body;
|
|
513
|
+
const userId = getUserId(req);
|
|
514
|
+
// Check request limit (documentation uses AI)
|
|
515
|
+
if (userId) {
|
|
516
|
+
const usageResult = await authService.checkAndUpdateUsage(userId, 'request');
|
|
517
|
+
if (!usageResult.allowed) {
|
|
518
|
+
res.status(429).json({
|
|
519
|
+
error: 'Request limit reached',
|
|
520
|
+
message: `You have reached your daily request limit (${usageResult.limit})`,
|
|
521
|
+
usage: { used: usageResult.limit, limit: usageResult.limit, remaining: 0 }
|
|
522
|
+
});
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
445
526
|
const result = await projectService.generateDocumentation(id, { format, language });
|
|
446
527
|
res.json(result);
|
|
447
528
|
}
|
|
@@ -450,4 +531,120 @@ apiRouter.post('/projects/:id/documentation', authMiddleware, checkProjectAccess
|
|
|
450
531
|
res.status(500).json({ error: 'Failed to generate documentation' });
|
|
451
532
|
}
|
|
452
533
|
});
|
|
534
|
+
/**
|
|
535
|
+
* GET /api/projects/:id/index-progress
|
|
536
|
+
* Server-Sent Events endpoint for real-time indexing progress
|
|
537
|
+
* Note: Accepts token via query param since EventSource doesn't support headers
|
|
538
|
+
*/
|
|
539
|
+
apiRouter.get('/projects/:id/index-progress', async (req, res) => {
|
|
540
|
+
const { id } = req.params;
|
|
541
|
+
// Handle auth via query param (EventSource doesn't support headers)
|
|
542
|
+
const token = req.query.token || req.headers.authorization?.substring(7);
|
|
543
|
+
if (!token) {
|
|
544
|
+
res.status(401).json({ error: 'Authentication required' });
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
// Verify token manually (use singleton instance)
|
|
548
|
+
const user = await authService.validateToken(token);
|
|
549
|
+
if (!user) {
|
|
550
|
+
res.status(401).json({ error: 'Invalid token' });
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
// Check project access
|
|
554
|
+
const isOwner = await projectService.isProjectOwner(id, user.id);
|
|
555
|
+
if (!isOwner) {
|
|
556
|
+
res.status(403).json({ error: 'Access denied' });
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
// Set SSE headers
|
|
560
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
561
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
562
|
+
res.setHeader('Connection', 'keep-alive');
|
|
563
|
+
res.setHeader('X-Accel-Buffering', 'no'); // Disable nginx buffering
|
|
564
|
+
res.flushHeaders();
|
|
565
|
+
// Send initial connection message
|
|
566
|
+
res.write(`data: ${JSON.stringify({ type: 'connected', projectId: id })}\n\n`);
|
|
567
|
+
// Subscribe to progress updates
|
|
568
|
+
const unsubscribe = subscribeToProgress(id, (progress) => {
|
|
569
|
+
try {
|
|
570
|
+
res.write(`data: ${JSON.stringify({ type: 'progress', ...progress })}\n\n`);
|
|
571
|
+
// If complete or error, close connection
|
|
572
|
+
if (progress.step === 'complete' || progress.step === 'error') {
|
|
573
|
+
setTimeout(() => {
|
|
574
|
+
res.write(`data: ${JSON.stringify({ type: 'done' })}\n\n`);
|
|
575
|
+
res.end();
|
|
576
|
+
}, 500);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
catch (err) {
|
|
580
|
+
// Client disconnected
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
// Handle client disconnect
|
|
584
|
+
req.on('close', () => {
|
|
585
|
+
unsubscribe();
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
/**
|
|
589
|
+
* GET /api/projects/:id/scan
|
|
590
|
+
* Scan project directory and get file statistics before indexing
|
|
591
|
+
*/
|
|
592
|
+
apiRouter.get('/projects/:id/scan', authMiddleware, checkProjectAccess, async (req, res) => {
|
|
593
|
+
try {
|
|
594
|
+
const { id } = req.params;
|
|
595
|
+
const project = await projectService.getProject(id);
|
|
596
|
+
if (!project) {
|
|
597
|
+
res.status(404).json({ error: 'Project not found' });
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
// Get project structure (files, sizes, etc.)
|
|
601
|
+
const structure = await FileUtils.getProjectStructure(project.path);
|
|
602
|
+
// Calculate statistics
|
|
603
|
+
let totalSize = 0;
|
|
604
|
+
const languageCounts = {};
|
|
605
|
+
for (const file of structure.files) {
|
|
606
|
+
totalSize += file.size;
|
|
607
|
+
languageCounts[file.language] = (languageCounts[file.language] || 0) + 1;
|
|
608
|
+
}
|
|
609
|
+
// Estimate symbol count (rough: ~20-50 symbols per file on average)
|
|
610
|
+
const estimatedSymbols = structure.files.length * 30;
|
|
611
|
+
// Estimate indexing time with batch embeddings
|
|
612
|
+
// - Parsing: ~0.05s per file
|
|
613
|
+
// - Batch embedding: ~2s per 500 symbols (Jina batch API)
|
|
614
|
+
// - Vector store: ~0.5s per 500 chunks
|
|
615
|
+
const parsingTime = structure.files.length * 0.05;
|
|
616
|
+
const embeddingBatches = Math.ceil(estimatedSymbols / 500);
|
|
617
|
+
const embeddingTime = embeddingBatches * 2;
|
|
618
|
+
const storageTime = embeddingBatches * 0.5;
|
|
619
|
+
const estimatedTimeSeconds = Math.ceil(parsingTime + embeddingTime + storageTime);
|
|
620
|
+
res.json({
|
|
621
|
+
projectId: id,
|
|
622
|
+
projectName: project.name,
|
|
623
|
+
totalFiles: structure.files.length,
|
|
624
|
+
totalSizeMB: Math.round(totalSize / (1024 * 1024) * 100) / 100,
|
|
625
|
+
totalSizeBytes: totalSize,
|
|
626
|
+
languageCounts,
|
|
627
|
+
estimatedSymbols,
|
|
628
|
+
estimatedTimeSeconds,
|
|
629
|
+
estimatedTimeFormatted: formatTime(estimatedTimeSeconds),
|
|
630
|
+
directories: structure.directories.size
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
catch (error) {
|
|
634
|
+
Logger.error('Failed to scan project:', error);
|
|
635
|
+
res.status(500).json({ error: 'Failed to scan project' });
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
// Helper function to format time
|
|
639
|
+
function formatTime(seconds) {
|
|
640
|
+
if (seconds < 60) {
|
|
641
|
+
return `~${seconds} sec`;
|
|
642
|
+
}
|
|
643
|
+
const minutes = Math.floor(seconds / 60);
|
|
644
|
+
const remainingSeconds = seconds % 60;
|
|
645
|
+
if (remainingSeconds === 0) {
|
|
646
|
+
return `~${minutes} min`;
|
|
647
|
+
}
|
|
648
|
+
return `~${minutes} min ${remainingSeconds} sec`;
|
|
649
|
+
}
|
|
453
650
|
//# sourceMappingURL=api.js.map
|