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.
Files changed (42) hide show
  1. package/dist/cli/commands/auth.d.ts +4 -0
  2. package/dist/cli/commands/auth.js +58 -0
  3. package/dist/cli/commands/index.d.ts +2 -1
  4. package/dist/cli/commands/index.js +2 -1
  5. package/dist/cli/commands/interactive.js +107 -59
  6. package/dist/cli/ui/index.d.ts +1 -0
  7. package/dist/cli/ui/index.js +1 -0
  8. package/dist/cli/ui/progress.d.ts +57 -0
  9. package/dist/cli/ui/progress.js +149 -0
  10. package/dist/cli/utils/error-handler.d.ts +40 -0
  11. package/dist/cli/utils/error-handler.js +234 -0
  12. package/dist/cli/utils/index.d.ts +2 -0
  13. package/dist/cli/utils/index.js +3 -0
  14. package/dist/cli/utils/project-selector.d.ts +33 -0
  15. package/dist/cli/utils/project-selector.js +148 -0
  16. package/dist/cli.js +3 -1
  17. package/dist/code-index/ast-parser.d.ts +1 -1
  18. package/dist/code-index/ast-parser.js +9 -3
  19. package/dist/code-index/index.d.ts +1 -1
  20. package/dist/code-index/index.js +2 -2
  21. package/dist/github/github-service.js +8 -1
  22. package/dist/orchestrator/index.js +29 -1
  23. package/dist/semantic-memory/embedding-service.d.ts +4 -1
  24. package/dist/semantic-memory/embedding-service.js +58 -11
  25. package/dist/semantic-memory/index.d.ts +1 -1
  26. package/dist/semantic-memory/index.js +54 -7
  27. package/dist/server/config.d.ts +52 -0
  28. package/dist/server/config.js +88 -0
  29. package/dist/server/index.d.ts +3 -0
  30. package/dist/server/index.js +115 -10
  31. package/dist/server/routes/admin.js +3 -3
  32. package/dist/server/routes/api.js +199 -2
  33. package/dist/server/routes/auth.js +79 -5
  34. package/dist/server/routes/device-auth.js +1 -1
  35. package/dist/server/routes/github.js +33 -0
  36. package/dist/server/services/auth-service.d.ts +5 -0
  37. package/dist/server/services/auth-service.js +10 -0
  38. package/dist/server/services/project-service.d.ts +15 -1
  39. package/dist/server/services/project-service.js +185 -26
  40. package/dist/types/user.d.ts +2 -2
  41. package/dist/types/user.js +13 -4
  42. 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 chunks = [];
30
- for (const symbol of symbols.values()) {
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
- const chunk = await this.createChunkFromSymbol(symbol, code);
36
- chunks.push(chunk);
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
- await this.vectorStore.upsertChunks(chunks);
39
- Logger.success(`Indexed ${chunks.length} symbols`);
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
@@ -12,6 +12,9 @@ import express from 'express';
12
12
  export interface ServerConfig {
13
13
  port: number;
14
14
  host?: string;
15
+ corsOrigins?: string[];
16
+ rateLimitWindowMs?: number;
17
+ rateLimitMax?: number;
15
18
  }
16
19
  export declare class ArchiCoreServer {
17
20
  private app;
@@ -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
- // CORS для фронтенда
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
- methods: ['GET', 'POST', 'PUT', 'DELETE'],
42
- allowedHeaders: ['Content-Type', 'Authorization']
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
- const publicPath = path.join(__dirname, '../../public');
49
- this.app.use(express.static(publicPath));
50
- // Логирование запросов
51
- this.app.use((req, _res, next) => {
52
- Logger.debug(`${req.method} ${req.path}`);
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 = new 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({ project });
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