archicore 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +48 -4
  2. package/dist/cli/commands/interactive.js +83 -23
  3. package/dist/cli/commands/projects.js +3 -3
  4. package/dist/cli/ui/prompt.d.ts +4 -0
  5. package/dist/cli/ui/prompt.js +22 -0
  6. package/dist/cli/utils/config.js +2 -2
  7. package/dist/cli/utils/upload-utils.js +65 -18
  8. package/dist/code-index/ast-parser.d.ts +4 -0
  9. package/dist/code-index/ast-parser.js +42 -0
  10. package/dist/code-index/index.d.ts +21 -1
  11. package/dist/code-index/index.js +45 -1
  12. package/dist/code-index/source-map-extractor.d.ts +71 -0
  13. package/dist/code-index/source-map-extractor.js +194 -0
  14. package/dist/gitlab/gitlab-service.d.ts +162 -0
  15. package/dist/gitlab/gitlab-service.js +652 -0
  16. package/dist/gitlab/index.d.ts +8 -0
  17. package/dist/gitlab/index.js +8 -0
  18. package/dist/server/config/passport.d.ts +14 -0
  19. package/dist/server/config/passport.js +86 -0
  20. package/dist/server/index.js +52 -10
  21. package/dist/server/middleware/api-auth.d.ts +2 -2
  22. package/dist/server/middleware/api-auth.js +21 -2
  23. package/dist/server/middleware/csrf.d.ts +23 -0
  24. package/dist/server/middleware/csrf.js +96 -0
  25. package/dist/server/routes/auth.d.ts +2 -2
  26. package/dist/server/routes/auth.js +204 -5
  27. package/dist/server/routes/device-auth.js +2 -2
  28. package/dist/server/routes/gitlab.d.ts +12 -0
  29. package/dist/server/routes/gitlab.js +528 -0
  30. package/dist/server/routes/oauth.d.ts +6 -0
  31. package/dist/server/routes/oauth.js +198 -0
  32. package/dist/server/services/audit-service.d.ts +1 -1
  33. package/dist/server/services/auth-service.d.ts +13 -1
  34. package/dist/server/services/auth-service.js +108 -7
  35. package/dist/server/services/email-service.d.ts +63 -0
  36. package/dist/server/services/email-service.js +586 -0
  37. package/dist/server/utils/disposable-email-domains.d.ts +14 -0
  38. package/dist/server/utils/disposable-email-domains.js +192 -0
  39. package/dist/types/api.d.ts +98 -0
  40. package/dist/types/gitlab.d.ts +245 -0
  41. package/dist/types/gitlab.js +11 -0
  42. package/package.json +1 -1
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Passport OAuth Configuration
3
+ * Google and GitHub authentication strategies
4
+ */
5
+ import passport from 'passport';
6
+ import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
7
+ import { Strategy as GitHubStrategy } from 'passport-github2';
8
+ import { Logger } from '../../utils/logger.js';
9
+ // Google OAuth Strategy
10
+ if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
11
+ passport.use(new GoogleStrategy({
12
+ clientID: process.env.GOOGLE_CLIENT_ID,
13
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
14
+ callbackURL: process.env.GOOGLE_CALLBACK_URL || '/api/auth/oauth/google/callback',
15
+ scope: ['profile', 'email'],
16
+ }, async (_accessToken, _refreshToken, profile, done) => {
17
+ try {
18
+ Logger.info(`[OAuth] Google authentication for user: ${profile.emails?.[0]?.value}`);
19
+ const oauthProfile = {
20
+ id: profile.id,
21
+ email: profile.emails?.[0]?.value || '',
22
+ displayName: profile.displayName,
23
+ avatar: profile.photos?.[0]?.value,
24
+ provider: 'google',
25
+ };
26
+ // Validate email
27
+ if (!oauthProfile.email) {
28
+ return done(new Error('No email provided by Google'));
29
+ }
30
+ done(null, oauthProfile);
31
+ }
32
+ catch (error) {
33
+ Logger.error('[OAuth] Google authentication error:', error);
34
+ done(error);
35
+ }
36
+ }));
37
+ Logger.info('[OAuth] Google strategy configured');
38
+ }
39
+ else {
40
+ Logger.warn('[OAuth] Google OAuth not configured - missing GOOGLE_CLIENT_ID or GOOGLE_CLIENT_SECRET');
41
+ }
42
+ // GitHub OAuth Strategy
43
+ if (process.env.GITHUB_OAUTH_CLIENT_ID && process.env.GITHUB_OAUTH_CLIENT_SECRET) {
44
+ passport.use(new GitHubStrategy({
45
+ clientID: process.env.GITHUB_OAUTH_CLIENT_ID,
46
+ clientSecret: process.env.GITHUB_OAUTH_CLIENT_SECRET,
47
+ callbackURL: process.env.GITHUB_OAUTH_CALLBACK_URL || '/api/auth/oauth/github/callback',
48
+ scope: ['user:email'],
49
+ }, async (_accessToken, _refreshToken, profile, done) => {
50
+ try {
51
+ Logger.info(`[OAuth] GitHub authentication for user: ${profile.username}`);
52
+ // GitHub might not provide email in profile, check emails array
53
+ const email = profile.emails?.[0]?.value || profile._json?.email || '';
54
+ const oauthProfile = {
55
+ id: profile.id,
56
+ email,
57
+ displayName: profile.displayName || profile.username || '',
58
+ avatar: profile.photos?.[0]?.value || profile._json?.avatar_url,
59
+ provider: 'github',
60
+ };
61
+ // Validate email
62
+ if (!oauthProfile.email) {
63
+ return done(new Error('No email provided by GitHub. Please make your email public in GitHub settings.'));
64
+ }
65
+ done(null, oauthProfile);
66
+ }
67
+ catch (error) {
68
+ Logger.error('[OAuth] GitHub authentication error:', error);
69
+ done(error);
70
+ }
71
+ }));
72
+ Logger.info('[OAuth] GitHub strategy configured');
73
+ }
74
+ else {
75
+ Logger.warn('[OAuth] GitHub OAuth not configured - missing GITHUB_OAUTH_CLIENT_ID or GITHUB_OAUTH_CLIENT_SECRET');
76
+ }
77
+ // Serialize user to session (not used in stateless JWT, but required by passport)
78
+ passport.serializeUser((user, done) => {
79
+ done(null, user);
80
+ });
81
+ // Deserialize user from session
82
+ passport.deserializeUser((user, done) => {
83
+ done(null, user);
84
+ });
85
+ export default passport;
86
+ //# sourceMappingURL=passport.js.map
@@ -22,14 +22,20 @@ import { Logger } from '../utils/logger.js';
22
22
  import { apiRouter } from './routes/api.js';
23
23
  import { uploadRouter } from './routes/upload.js';
24
24
  import { authRouter } from './routes/auth.js';
25
+ import { oauthRouter } from './routes/oauth.js';
25
26
  import { adminRouter } from './routes/admin.js';
26
27
  import { developerRouter } from './routes/developer.js';
27
28
  import { githubRouter } from './routes/github.js';
29
+ import { gitlabRouter } from './routes/gitlab.js';
28
30
  import deviceAuthRouter from './routes/device-auth.js';
29
31
  import { reportIssueRouter } from './routes/report-issue.js';
30
32
  import { cache } from './services/cache.js';
31
33
  import { db } from './services/database.js';
32
34
  import { AuthService } from './services/auth-service.js';
35
+ import { securityConfig } from './config.js';
36
+ import passport from './config/passport.js';
37
+ import cookieParser from 'cookie-parser';
38
+ import { csrfProtection, csrfTokenEndpoint } from './middleware/csrf.js';
33
39
  const __filename = fileURLToPath(import.meta.url);
34
40
  const __dirname = path.dirname(__filename);
35
41
  // CORS whitelist - настройте под свои домены
@@ -153,17 +159,19 @@ export class ArchiCoreServer {
153
159
  this.setupErrorHandling();
154
160
  }
155
161
  setupMiddleware() {
156
- // Security headers (helmet) - disabled in development for easier testing
157
- // Set HELMET_ENABLED=true in production
158
- const helmetEnabled = process.env.HELMET_ENABLED === 'true';
162
+ // Security headers (helmet) - enabled by default for security
163
+ // Set HELMET_ENABLED=false to disable (not recommended)
164
+ const helmetEnabled = process.env.HELMET_ENABLED !== 'false';
159
165
  if (helmetEnabled) {
160
166
  this.app.use(helmet({
161
167
  contentSecurityPolicy: {
162
168
  directives: {
163
169
  defaultSrc: ["'self'"],
164
- styleSrc: ["'self'", "'unsafe-inline'"],
165
- scriptSrc: ["'self'", "'unsafe-inline'"],
170
+ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
171
+ scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net"],
172
+ scriptSrcAttr: ["'unsafe-inline'", "'unsafe-hashes'"],
166
173
  imgSrc: ["'self'", "data:", "https:"],
174
+ fontSrc: ["'self'", "https://fonts.gstatic.com"],
167
175
  connectSrc: ["'self'", "https://api.jina.ai", "https://api.openai.com", "https://api.anthropic.com"],
168
176
  },
169
177
  },
@@ -196,8 +204,8 @@ export class ArchiCoreServer {
196
204
  const globalLimiter = createRateLimiter(this.config.rateLimitWindowMs || 60 * 1000, this.config.rateLimitMax || 100, 'Too many requests, please try again later');
197
205
  this.app.use(globalLimiter);
198
206
  // CORS configuration
199
- // Set CORS_RESTRICT=true in .env to enable whitelist mode
200
- const restrictCors = process.env.CORS_RESTRICT === 'true';
207
+ // Set CORS_RESTRICT=false in .env to disable whitelist mode (not recommended for production)
208
+ const restrictCors = process.env.CORS_RESTRICT !== 'false';
201
209
  const allowedOrigins = this.config.corsOrigins || DEFAULT_CORS_ORIGINS;
202
210
  this.app.use(cors({
203
211
  origin: restrictCors
@@ -224,10 +232,14 @@ export class ArchiCoreServer {
224
232
  }
225
233
  : true, // Allow all origins when CORS_RESTRICT is not set
226
234
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
227
- allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key', 'X-Request-ID'],
235
+ allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key', 'X-Request-ID', 'x-csrf-token'],
228
236
  credentials: true,
229
237
  maxAge: 86400, // Cache preflight for 24 hours
230
238
  }));
239
+ // Cookie parser for OAuth session
240
+ this.app.use(cookieParser());
241
+ // Passport initialization for OAuth
242
+ this.app.use(passport.initialize());
231
243
  // JSON парсинг - лимит должен покрывать максимальный тариф (admin = 1GB)
232
244
  // Проверка по тарифу пользователя происходит в бизнес-логике после парсинга
233
245
  this.app.use(express.json({ limit: '1gb' }));
@@ -251,8 +263,32 @@ export class ArchiCoreServer {
251
263
  }));
252
264
  }
253
265
  setupRoutes() {
254
- // Auth routes
266
+ // Strict rate limiting for auth endpoints to prevent brute force attacks
267
+ const authRateLimiter = createRateLimiter(15 * 60 * 1000, // 15 minutes
268
+ 20, // Max 20 attempts per 15 minutes
269
+ 'Too many authentication attempts, please try again later');
270
+ const verificationRateLimiter = createRateLimiter(60 * 60 * 1000, // 1 hour
271
+ 5, // Max 5 verification requests per hour
272
+ 'Too many verification code requests, please try again later');
273
+ // CSRF token endpoint (no protection needed for GET)
274
+ this.app.get('/api/auth/csrf-token', csrfTokenEndpoint);
275
+ // CSRF protection for state-changing auth operations
276
+ // Ignore paths that use API keys or device auth flow
277
+ const csrfMiddleware = csrfProtection({
278
+ ignorePaths: [
279
+ '/api/auth/device',
280
+ '/api/auth/oauth',
281
+ '/api/developer',
282
+ '/api/v1'
283
+ ]
284
+ });
285
+ // Auth routes with rate limiting and CSRF protection
286
+ this.app.use('/api/auth/login', authRateLimiter, csrfMiddleware);
287
+ this.app.use('/api/auth/register', authRateLimiter, csrfMiddleware);
288
+ this.app.use('/api/auth/send-verification-code', verificationRateLimiter, csrfMiddleware);
255
289
  this.app.use('/api/auth', authRouter);
290
+ // OAuth routes (Google, GitHub)
291
+ this.app.use('/api/auth/oauth', oauthRouter);
256
292
  // Device auth for CLI
257
293
  this.app.use('/api/auth/device', deviceAuthRouter);
258
294
  // Admin routes
@@ -261,6 +297,8 @@ export class ArchiCoreServer {
261
297
  this.app.use('/api/developer', developerRouter);
262
298
  // GitHub integration routes
263
299
  this.app.use('/api/github', githubRouter);
300
+ // GitLab integration routes
301
+ this.app.use('/api/gitlab', gitlabRouter);
264
302
  // API маршруты
265
303
  this.app.use('/api', apiRouter);
266
304
  // Upload маршруты
@@ -405,7 +443,11 @@ const isMainModule = process.argv[1]?.includes('server');
405
443
  if (isMainModule) {
406
444
  const port = parseInt(process.env.PORT || '3000', 10);
407
445
  const host = process.env.HOST || '0.0.0.0';
408
- const server = new ArchiCoreServer({ port, host });
446
+ const server = new ArchiCoreServer({
447
+ port,
448
+ host,
449
+ corsOrigins: securityConfig.cors.allowedOrigins
450
+ });
409
451
  server.start().catch((err) => {
410
452
  Logger.error('Failed to start server:', err);
411
453
  process.exit(1);
@@ -4,7 +4,7 @@
4
4
  * Аутентификация через API ключи, rate limiting, проверка permissions
5
5
  */
6
6
  import { Request, Response, NextFunction } from 'express';
7
- import { ApiPermission, ApiRequestContext } from '../../types/api.js';
7
+ import { ApiPermission, ApiRequestContext, OperationCategory } from '../../types/api.js';
8
8
  declare global {
9
9
  namespace Express {
10
10
  interface Request {
@@ -31,7 +31,7 @@ export declare function checkBalance(estimatedTokens: number): (req: Request, re
31
31
  /**
32
32
  * Wrapper для автоматического трекинга использования токенов
33
33
  */
34
- export declare function trackUsage(operation: ApiPermission extends `${infer Op}:${string}` ? Op : never): (req: Request, res: Response, next: NextFunction) => Promise<void>;
34
+ export declare function trackUsage(operation: OperationCategory): (req: Request, res: Response, next: NextFunction) => Promise<void>;
35
35
  /**
36
36
  * Error handler для API ошибок
37
37
  */
@@ -152,6 +152,24 @@ export function checkBalance(estimatedTokens) {
152
152
  next();
153
153
  };
154
154
  }
155
+ /**
156
+ * Map operation category to ApiOperation for internal processing
157
+ */
158
+ function mapOperationToApiOperation(operation) {
159
+ const mapping = {
160
+ 'index': 'index',
161
+ 'analyze': 'analyze_full',
162
+ 'search': 'search_semantic',
163
+ 'ask': 'ask_architect',
164
+ 'export': 'export',
165
+ 'refactoring': 'refactoring',
166
+ 'metrics': 'metrics',
167
+ 'rules': 'rules_check',
168
+ 'dead_code': 'dead_code',
169
+ 'duplication': 'duplication'
170
+ };
171
+ return mapping[operation] || 'index';
172
+ }
155
173
  /**
156
174
  * Wrapper для автоматического трекинга использования токенов
157
175
  */
@@ -164,18 +182,19 @@ export function trackUsage(operation) {
164
182
  // Сохраняем оригинальный json метод
165
183
  const originalJson = res.json.bind(res);
166
184
  const startTime = Date.now();
185
+ const apiOperation = mapOperationToApiOperation(operation);
167
186
  // Перехватываем ответ
168
187
  res.json = function (body) {
169
188
  // Подсчитываем токены на основе размера ответа
170
189
  const responseSize = JSON.stringify(body).length;
171
190
  const inputSize = JSON.stringify(req.body || {}).length;
172
- const tokens = tokenService.calculateTokens(operation, {
191
+ const tokens = tokenService.calculateTokens(apiOperation, {
173
192
  inputText: JSON.stringify(req.body || {}),
174
193
  outputText: JSON.stringify(body),
175
194
  contentSizeKb: Math.ceil((inputSize + responseSize) / 1024)
176
195
  });
177
196
  // Записываем использование асинхронно (не блокируем ответ)
178
- tokenService.recordUsage(req.apiContext.apiKey.id, req.apiContext.userId, operation, tokens, req.params.id, // projectId if present
197
+ tokenService.recordUsage(req.apiContext.apiKey.id, req.apiContext.userId, apiOperation, tokens, req.params.id, // projectId if present
179
198
  {
180
199
  endpoint: req.path,
181
200
  method: req.method,
@@ -0,0 +1,23 @@
1
+ /**
2
+ * CSRF Protection Middleware
3
+ *
4
+ * Implements double-submit cookie pattern for CSRF protection.
5
+ * This is a modern alternative to the deprecated csurf package.
6
+ */
7
+ import { Request, Response, NextFunction } from 'express';
8
+ /**
9
+ * CSRF Protection Middleware
10
+ *
11
+ * For GET requests: Sets a CSRF cookie and provides the token in response
12
+ * For state-changing requests (POST, PUT, DELETE, PATCH): Validates the token
13
+ */
14
+ export declare function csrfProtection(options?: {
15
+ ignoreMethods?: string[];
16
+ ignorePaths?: string[];
17
+ }): (req: Request, res: Response, next: NextFunction) => void;
18
+ /**
19
+ * Endpoint to get CSRF token
20
+ * GET /api/auth/csrf-token
21
+ */
22
+ export declare function csrfTokenEndpoint(req: Request, res: Response): void;
23
+ //# sourceMappingURL=csrf.d.ts.map
@@ -0,0 +1,96 @@
1
+ /**
2
+ * CSRF Protection Middleware
3
+ *
4
+ * Implements double-submit cookie pattern for CSRF protection.
5
+ * This is a modern alternative to the deprecated csurf package.
6
+ */
7
+ import crypto from 'crypto';
8
+ const CSRF_COOKIE_NAME = 'archicore_csrf';
9
+ const CSRF_HEADER_NAME = 'x-csrf-token';
10
+ const CSRF_TOKEN_LENGTH = 32;
11
+ /**
12
+ * Generate a cryptographically secure CSRF token
13
+ */
14
+ function generateCsrfToken() {
15
+ return crypto.randomBytes(CSRF_TOKEN_LENGTH).toString('hex');
16
+ }
17
+ /**
18
+ * Validate that the token from header matches the token from cookie
19
+ */
20
+ function validateCsrfToken(cookieToken, headerToken) {
21
+ if (!cookieToken || !headerToken) {
22
+ return false;
23
+ }
24
+ // Use timing-safe comparison to prevent timing attacks
25
+ try {
26
+ return crypto.timingSafeEqual(Buffer.from(cookieToken), Buffer.from(headerToken));
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ /**
33
+ * CSRF Protection Middleware
34
+ *
35
+ * For GET requests: Sets a CSRF cookie and provides the token in response
36
+ * For state-changing requests (POST, PUT, DELETE, PATCH): Validates the token
37
+ */
38
+ export function csrfProtection(options = {}) {
39
+ const ignoreMethods = options.ignoreMethods || ['GET', 'HEAD', 'OPTIONS'];
40
+ const ignorePaths = options.ignorePaths || [];
41
+ return (req, res, next) => {
42
+ // Skip CSRF for ignored paths
43
+ if (ignorePaths.some(path => req.path.startsWith(path))) {
44
+ return next();
45
+ }
46
+ // Skip CSRF for API key authenticated requests (they use different auth)
47
+ if (req.headers['x-api-key'] || req.headers.authorization?.startsWith('Bearer arc_')) {
48
+ return next();
49
+ }
50
+ // For safe methods, just ensure token exists
51
+ if (ignoreMethods.includes(req.method)) {
52
+ // Generate token if not present
53
+ if (!req.cookies[CSRF_COOKIE_NAME]) {
54
+ const token = generateCsrfToken();
55
+ res.cookie(CSRF_COOKIE_NAME, token, {
56
+ httpOnly: false, // Must be readable by JS
57
+ secure: process.env.NODE_ENV === 'production',
58
+ sameSite: 'strict',
59
+ maxAge: 24 * 60 * 60 * 1000, // 24 hours
60
+ path: '/'
61
+ });
62
+ }
63
+ return next();
64
+ }
65
+ // For state-changing methods, validate the token
66
+ const cookieToken = req.cookies[CSRF_COOKIE_NAME];
67
+ const headerToken = req.headers[CSRF_HEADER_NAME];
68
+ if (!validateCsrfToken(cookieToken, headerToken)) {
69
+ res.status(403).json({
70
+ error: 'CSRF token validation failed',
71
+ message: 'Invalid or missing CSRF token. Please refresh the page and try again.'
72
+ });
73
+ return;
74
+ }
75
+ next();
76
+ };
77
+ }
78
+ /**
79
+ * Endpoint to get CSRF token
80
+ * GET /api/auth/csrf-token
81
+ */
82
+ export function csrfTokenEndpoint(req, res) {
83
+ let token = req.cookies[CSRF_COOKIE_NAME];
84
+ if (!token) {
85
+ token = generateCsrfToken();
86
+ res.cookie(CSRF_COOKIE_NAME, token, {
87
+ httpOnly: false,
88
+ secure: process.env.NODE_ENV === 'production',
89
+ sameSite: 'strict',
90
+ maxAge: 24 * 60 * 60 * 1000,
91
+ path: '/'
92
+ });
93
+ }
94
+ res.json({ csrfToken: token });
95
+ }
96
+ //# sourceMappingURL=csrf.js.map
@@ -2,12 +2,12 @@
2
2
  * Authentication API Routes for ArchiCore
3
3
  */
4
4
  import { Request, Response, NextFunction } from 'express';
5
- import { User } from '../../types/user.js';
5
+ import { User as ArchiCoreUser } from '../../types/user.js';
6
6
  export declare const authRouter: import("express-serve-static-core").Router;
7
7
  declare global {
8
8
  namespace Express {
9
9
  interface Request {
10
- user?: User;
10
+ user?: ArchiCoreUser;
11
11
  }
12
12
  }
13
13
  }