archicore 0.3.6 → 0.3.7

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.
@@ -25,6 +25,8 @@ const COMMANDS = [
25
25
  { name: 'rules', aliases: [], description: 'Check architectural rules' },
26
26
  { name: 'docs', aliases: ['documentation'], description: 'Generate architecture documentation' },
27
27
  { name: 'export', aliases: [], description: 'Export analysis results' },
28
+ { name: 'enterprise', aliases: ['ent'], description: 'Enterprise analysis for large projects (50K+ files)' },
29
+ { name: 'estimate', aliases: ['est'], description: 'Estimate project size and recommended tier' },
28
30
  { name: 'history', aliases: ['hist'], description: 'View and search conversation history' },
29
31
  { name: 'resume', aliases: [], description: 'Resume previous conversation session' },
30
32
  { name: 'status', aliases: [], description: 'Show connection and project status' },
@@ -532,6 +534,14 @@ async function handleCommand(input) {
532
534
  case 'resume':
533
535
  await handleResumeCommand(args);
534
536
  break;
537
+ case 'enterprise':
538
+ case 'ent':
539
+ await handleEnterpriseCommand(args);
540
+ break;
541
+ case 'estimate':
542
+ case 'est':
543
+ await handleEstimateCommand();
544
+ break;
535
545
  default:
536
546
  printFormattedError(`Unknown command: /${command}`, {
537
547
  suggestion: 'Use /help to see available commands',
@@ -1321,8 +1331,11 @@ async function handleDocsCommand(args) {
1321
1331
  printInfo('Use /index first');
1322
1332
  return;
1323
1333
  }
1334
+ const pathModule = await import('path');
1324
1335
  const format = args[0] || 'markdown';
1325
- const output = args[1] || `documentation.${format === 'markdown' ? 'md' : format}`;
1336
+ const filename = `ARCHITECTURE.${format === 'markdown' ? 'md' : format}`;
1337
+ // Save to project root by default, or custom path if specified
1338
+ const output = args[1] || pathModule.default.join(state.projectPath, filename);
1326
1339
  const spinner = createSpinner('Generating documentation...').start();
1327
1340
  try {
1328
1341
  const config = await loadConfig();
@@ -1339,6 +1352,7 @@ async function handleDocsCommand(args) {
1339
1352
  spinner.succeed('Documentation generated');
1340
1353
  printKeyValue('Output', output);
1341
1354
  printKeyValue('Format', format);
1355
+ printInfo(`Documentation saved to project root: ${filename}`);
1342
1356
  showTokenUsage();
1343
1357
  }
1344
1358
  catch (error) {
@@ -1510,4 +1524,209 @@ async function handleResumeCommand(args) {
1510
1524
  printError(String(error));
1511
1525
  }
1512
1526
  }
1527
+ async function handleEstimateCommand() {
1528
+ if (!state.projectId) {
1529
+ printError('No project indexed');
1530
+ printInfo('Use /index first to register the project');
1531
+ return;
1532
+ }
1533
+ const spinner = createSpinner('Analyzing project size...').start();
1534
+ try {
1535
+ const config = await loadConfig();
1536
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/enterprise/estimate`);
1537
+ if (!response.ok) {
1538
+ spinner.fail('Failed to estimate project');
1539
+ const errorData = await response.json().catch(() => ({}));
1540
+ printError(errorData.error || 'Unknown error');
1541
+ return;
1542
+ }
1543
+ const data = await response.json();
1544
+ spinner.succeed('Project analyzed');
1545
+ console.log();
1546
+ printSection('Project Size Estimation');
1547
+ printKeyValue('Total Files', data.totalFiles?.toLocaleString() || 'N/A');
1548
+ printKeyValue('Total Size', `${data.totalSizeMB?.toFixed(1) || 'N/A'} MB`);
1549
+ printKeyValue('Recommended Tier', colors.primary(data.recommendation?.toUpperCase() || 'STANDARD'));
1550
+ printKeyValue('Estimated Time', `~${data.estimatedTimeMinutes || 'N/A'} minutes`);
1551
+ // Language distribution
1552
+ if (data.languageDistribution && Object.keys(data.languageDistribution).length > 0) {
1553
+ console.log();
1554
+ console.log(colors.highlight(' Language Distribution:'));
1555
+ const sorted = Object.entries(data.languageDistribution)
1556
+ .sort((a, b) => b[1] - a[1])
1557
+ .slice(0, 8);
1558
+ for (const [lang, count] of sorted) {
1559
+ const percent = data.totalFiles ? ((count / data.totalFiles) * 100).toFixed(1) : '?';
1560
+ console.log(` ${lang.padEnd(12)} ${colors.muted(count.toLocaleString().padStart(6))} (${percent}%)`);
1561
+ }
1562
+ }
1563
+ // Tier recommendations
1564
+ if (data.tiers) {
1565
+ console.log();
1566
+ console.log(colors.highlight(' Available Tiers:'));
1567
+ console.log(` ${colors.success('quick')} - ${data.tiers.quick.estimatedFiles.toLocaleString()} files - Fast overview`);
1568
+ console.log(` ${colors.warning('standard')} - ${data.tiers.standard.estimatedFiles.toLocaleString()} files - Balanced analysis`);
1569
+ console.log(` ${colors.error('deep')} - ${data.tiers.deep.estimatedFiles.toLocaleString()} files - Full analysis (Enterprise)`);
1570
+ }
1571
+ console.log();
1572
+ printInfo('Use /enterprise [tier] to start analysis');
1573
+ printInfo('Example: /enterprise quick or /enterprise standard');
1574
+ }
1575
+ catch (error) {
1576
+ spinner.fail('Failed to estimate project');
1577
+ printError(String(error));
1578
+ }
1579
+ }
1580
+ async function handleEnterpriseCommand(args) {
1581
+ if (!state.projectId) {
1582
+ printError('No project indexed');
1583
+ printInfo('Use /index first to register the project');
1584
+ return;
1585
+ }
1586
+ const tier = args[0]?.toLowerCase() || 'standard';
1587
+ const strategy = args[1] || 'smart';
1588
+ if (!['quick', 'standard', 'deep'].includes(tier)) {
1589
+ printError(`Invalid tier: ${tier}`);
1590
+ printInfo('Available tiers: quick, standard, deep');
1591
+ return;
1592
+ }
1593
+ // First, get estimate to show what we're doing
1594
+ const estimateSpinner = createSpinner('Analyzing project...').start();
1595
+ try {
1596
+ const config = await loadConfig();
1597
+ // Get estimate first
1598
+ const estimateResponse = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/enterprise/estimate`);
1599
+ if (!estimateResponse.ok) {
1600
+ estimateSpinner.fail('Failed to analyze project');
1601
+ return;
1602
+ }
1603
+ const estimate = await estimateResponse.json();
1604
+ estimateSpinner.succeed(`Project has ${estimate.totalFiles?.toLocaleString()} files`);
1605
+ // Determine files to analyze
1606
+ const tierConfig = estimate.tiers?.[tier];
1607
+ const filesToAnalyze = tierConfig?.estimatedFiles || estimate.totalFiles || 0;
1608
+ console.log();
1609
+ console.log(` ${colors.highlight('Enterprise Analysis Configuration:')}`);
1610
+ console.log(` Tier: ${colors.primary(tier.toUpperCase())}`);
1611
+ console.log(` Strategy: ${colors.muted(strategy)}`);
1612
+ console.log(` Files: ${colors.muted(filesToAnalyze.toLocaleString())} of ${estimate.totalFiles?.toLocaleString()}`);
1613
+ console.log();
1614
+ // Confirm for large projects
1615
+ if ((estimate.totalFiles || 0) > 10000 && tier !== 'quick') {
1616
+ const confirmed = await promptYesNo(`This is a large project (${estimate.totalFiles?.toLocaleString()} files). Continue with ${tier} analysis?`);
1617
+ if (!confirmed) {
1618
+ printInfo('Cancelled. Use /enterprise quick for faster analysis.');
1619
+ return;
1620
+ }
1621
+ }
1622
+ // Start enterprise indexing
1623
+ const indexSpinner = createSpinner(`Starting ${tier} analysis with ${strategy} sampling...`).start();
1624
+ // For CLI, we do local indexing with enterprise options
1625
+ const { CodeIndex } = await import('../../code-index/index.js');
1626
+ const { EnterpriseIndexer } = await import('../../server/services/enterprise-indexer.js');
1627
+ const fs = await import('fs/promises');
1628
+ const pathModule = await import('path');
1629
+ // Initialize enterprise indexer
1630
+ const enterpriseIndexer = new EnterpriseIndexer(state.projectPath, {
1631
+ tier,
1632
+ sampling: {
1633
+ enabled: true,
1634
+ maxFiles: tierConfig?.maxFiles || 5000,
1635
+ strategy: strategy,
1636
+ },
1637
+ });
1638
+ await enterpriseIndexer.initialize();
1639
+ indexSpinner.update('Selecting files to analyze...');
1640
+ const filesToIndex = await enterpriseIndexer.getFilesToIndex();
1641
+ indexSpinner.update(`Parsing ${filesToIndex.length} selected files...`);
1642
+ // Parse only selected files
1643
+ const codeIndex = new CodeIndex(state.projectPath);
1644
+ const asts = new Map();
1645
+ let parsed = 0;
1646
+ for (const filePath of filesToIndex) {
1647
+ try {
1648
+ const { ASTParser } = await import('../../code-index/ast-parser.js');
1649
+ const parser = new ASTParser();
1650
+ const ast = await parser.parseFile(filePath);
1651
+ if (ast) {
1652
+ asts.set(filePath, ast);
1653
+ parsed++;
1654
+ if (parsed % 100 === 0) {
1655
+ indexSpinner.update(`Parsed ${parsed}/${filesToIndex.length} files...`);
1656
+ }
1657
+ }
1658
+ }
1659
+ catch {
1660
+ // Skip files that can't be parsed
1661
+ }
1662
+ }
1663
+ indexSpinner.update(`Extracting symbols from ${asts.size} files...`);
1664
+ // Extract symbols
1665
+ const symbols = codeIndex.extractSymbols(asts);
1666
+ indexSpinner.update(`Found ${symbols.size} symbols, building graph...`);
1667
+ // Build dependency graph
1668
+ const graph = codeIndex.buildDependencyGraph(asts, symbols);
1669
+ // Read file contents (only for smaller selections)
1670
+ let fileContents = [];
1671
+ if (filesToIndex.length <= 2000) {
1672
+ indexSpinner.update('Reading file contents...');
1673
+ for (const [filePath] of asts) {
1674
+ try {
1675
+ const fullPath = pathModule.default.isAbsolute(filePath)
1676
+ ? filePath
1677
+ : pathModule.default.join(state.projectPath, filePath);
1678
+ const content = await fs.readFile(fullPath, 'utf-8');
1679
+ fileContents.push([filePath, content]);
1680
+ }
1681
+ catch {
1682
+ // Skip unreadable files
1683
+ }
1684
+ }
1685
+ }
1686
+ // Prepare data for upload
1687
+ const astsArray = Array.from(asts.entries());
1688
+ const symbolsArray = Array.from(symbols.entries());
1689
+ const graphData = {
1690
+ nodes: Array.from(graph.nodes.entries()),
1691
+ edges: Array.from(graph.edges.entries()),
1692
+ };
1693
+ const indexData = {
1694
+ asts: astsArray,
1695
+ symbols: symbolsArray,
1696
+ graph: graphData,
1697
+ fileContents,
1698
+ statistics: {
1699
+ totalFiles: asts.size,
1700
+ totalSymbols: symbols.size,
1701
+ },
1702
+ };
1703
+ // Upload to server
1704
+ indexSpinner.update('Uploading analysis data to server...');
1705
+ const uploadResult = await uploadIndexData(state.projectId, indexData, (progress) => {
1706
+ indexSpinner.update(progress.message);
1707
+ });
1708
+ if (!uploadResult.success) {
1709
+ indexSpinner.fail('Failed to upload analysis');
1710
+ if (uploadResult.errorDetails) {
1711
+ printError(uploadResult.errorDetails.message);
1712
+ printInfo(uploadResult.errorDetails.suggestion);
1713
+ }
1714
+ return;
1715
+ }
1716
+ indexSpinner.succeed(`Enterprise analysis complete!`);
1717
+ console.log();
1718
+ printSection('Analysis Summary');
1719
+ printKeyValue('Tier', tier.toUpperCase());
1720
+ printKeyValue('Strategy', strategy);
1721
+ printKeyValue('Files Analyzed', asts.size.toLocaleString());
1722
+ printKeyValue('Symbols Found', symbols.size.toLocaleString());
1723
+ printKeyValue('Graph Nodes', graph.nodes.size.toLocaleString());
1724
+ console.log();
1725
+ printSuccess('Project is ready for queries!');
1726
+ printInfo('Try: /search "authentication" or /metrics or /security');
1727
+ }
1728
+ catch (error) {
1729
+ printError(`Enterprise analysis failed: ${error}`);
1730
+ }
1731
+ }
1513
1732
  //# sourceMappingURL=interactive.js.map
@@ -9,11 +9,11 @@ export declare class GitHubService {
9
9
  private connections;
10
10
  private repositories;
11
11
  private initialized;
12
- private clientId;
13
- private clientSecret;
14
- private redirectUri;
15
- private webhookBaseUrl;
16
12
  constructor(dataDir?: string);
13
+ private get clientId();
14
+ private get clientSecret();
15
+ private get redirectUri();
16
+ private get webhookBaseUrl();
17
17
  private ensureInitialized;
18
18
  private saveConnections;
19
19
  private saveRepositories;
@@ -10,8 +10,19 @@ import { Logger } from '../utils/logger.js';
10
10
  const DATA_DIR = '.archicore';
11
11
  const CONNECTIONS_FILE = 'github-connections.json';
12
12
  const REPOSITORIES_FILE = 'github-repositories.json';
13
- // Encryption key (in production, use env variable)
14
- const ENCRYPTION_KEY = process.env.GITHUB_ENCRYPTION_KEY || 'archicore-github-key-32bytes!!';
13
+ // Encryption key - REQUIRED in production
14
+ function getEncryptionKey() {
15
+ const key = process.env.GITHUB_ENCRYPTION_KEY || process.env.ENCRYPTION_KEY;
16
+ if (!key) {
17
+ if (process.env.NODE_ENV === 'production') {
18
+ throw new Error('ENCRYPTION_KEY or GITHUB_ENCRYPTION_KEY is required in production');
19
+ }
20
+ // Dev-only fallback (logs warning)
21
+ console.warn('WARNING: Using dev-only encryption key. Set ENCRYPTION_KEY in production.');
22
+ return 'dev-only-github-key-32bytes!!!';
23
+ }
24
+ return key;
25
+ }
15
26
  // Singleton instance
16
27
  let instance = null;
17
28
  export class GitHubService {
@@ -19,22 +30,27 @@ export class GitHubService {
19
30
  connections = [];
20
31
  repositories = [];
21
32
  initialized = false;
22
- clientId;
23
- clientSecret;
24
- redirectUri;
25
- webhookBaseUrl;
26
33
  constructor(dataDir = DATA_DIR) {
27
34
  // Singleton pattern - return existing instance
28
35
  if (instance) {
29
36
  return instance;
30
37
  }
31
38
  this.dataDir = dataDir;
32
- this.clientId = process.env.GITHUB_CLIENT_ID || '';
33
- this.clientSecret = process.env.GITHUB_CLIENT_SECRET || '';
34
- this.redirectUri = process.env.GITHUB_REDIRECT_URI || 'http://localhost:3000/api/github/callback';
35
- this.webhookBaseUrl = process.env.ARCHICORE_WEBHOOK_URL || 'http://localhost:3000/api/github/webhook';
36
39
  instance = this;
37
40
  }
41
+ // Lazy getters for environment variables (read at runtime, not at module load)
42
+ get clientId() {
43
+ return process.env.GITHUB_CLIENT_ID || '';
44
+ }
45
+ get clientSecret() {
46
+ return process.env.GITHUB_CLIENT_SECRET || '';
47
+ }
48
+ get redirectUri() {
49
+ return process.env.GITHUB_REDIRECT_URI || process.env.GITHUB_CALLBACK_URL || 'http://localhost:3000/api/github/callback';
50
+ }
51
+ get webhookBaseUrl() {
52
+ return process.env.ARCHICORE_WEBHOOK_URL || 'http://localhost:3000/api/github/webhook';
53
+ }
38
54
  async ensureInitialized() {
39
55
  if (this.initialized)
40
56
  return;
@@ -75,7 +91,7 @@ export class GitHubService {
75
91
  // ===== ENCRYPTION =====
76
92
  encrypt(text) {
77
93
  const iv = randomBytes(16);
78
- const key = createHash('sha256').update(ENCRYPTION_KEY).digest();
94
+ const key = createHash('sha256').update(getEncryptionKey()).digest();
79
95
  const cipher = createCipheriv('aes-256-cbc', key, iv);
80
96
  let encrypted = cipher.update(text, 'utf8', 'hex');
81
97
  encrypted += cipher.final('hex');
@@ -84,7 +100,7 @@ export class GitHubService {
84
100
  decrypt(encrypted) {
85
101
  const parts = encrypted.split(':');
86
102
  const iv = Buffer.from(parts[0], 'hex');
87
- const key = createHash('sha256').update(ENCRYPTION_KEY).digest();
103
+ const key = createHash('sha256').update(getEncryptionKey()).digest();
88
104
  const decipher = createDecipheriv('aes-256-cbc', key, iv);
89
105
  let decrypted = decipher.update(parts[1], 'hex', 'utf8');
90
106
  decrypted += decipher.final('utf8');
@@ -640,7 +656,7 @@ export class GitHubService {
640
656
  const response = await fetch(url, {
641
657
  headers: {
642
658
  'Authorization': `Bearer ${accessToken}`,
643
- 'Accept': 'application/vnd.github+json',
659
+ 'Accept': 'application/vnd.github.v3+raw',
644
660
  'User-Agent': 'ArchiCore',
645
661
  'X-GitHub-Api-Version': '2022-11-28'
646
662
  },
@@ -19,8 +19,19 @@ import { Logger } from '../utils/logger.js';
19
19
  const DATA_DIR = process.env.ARCHICORE_DATA_DIR || '.archicore';
20
20
  const INSTANCES_FILE = 'gitlab-instances.json';
21
21
  const REPOSITORIES_FILE = 'gitlab-repositories.json';
22
- // Encryption key for tokens
23
- const ENCRYPTION_KEY = process.env.GITLAB_ENCRYPTION_KEY || process.env.ENCRYPTION_KEY || 'archicore-gitlab-key-32bytes!!';
22
+ // Encryption key - REQUIRED in production
23
+ function getEncryptionKey() {
24
+ const key = process.env.GITLAB_ENCRYPTION_KEY || process.env.ENCRYPTION_KEY;
25
+ if (!key) {
26
+ if (process.env.NODE_ENV === 'production') {
27
+ throw new Error('ENCRYPTION_KEY or GITLAB_ENCRYPTION_KEY is required in production');
28
+ }
29
+ // Dev-only fallback (logs warning)
30
+ console.warn('WARNING: Using dev-only encryption key. Set ENCRYPTION_KEY in production.');
31
+ return 'dev-only-gitlab-key-32bytes!!!';
32
+ }
33
+ return key;
34
+ }
24
35
  // Webhook base URL - construct from PUBLIC_URL or API_URL
25
36
  const getWebhookUrl = () => {
26
37
  // Use explicit GitLab webhook URL if set
@@ -118,7 +129,7 @@ export class GitLabService {
118
129
  // ===== ENCRYPTION =====
119
130
  encrypt(text) {
120
131
  const iv = randomBytes(16);
121
- const key = createHash('sha256').update(ENCRYPTION_KEY).digest();
132
+ const key = createHash('sha256').update(getEncryptionKey()).digest();
122
133
  const cipher = createCipheriv('aes-256-cbc', key, iv);
123
134
  let encrypted = cipher.update(text, 'utf8', 'hex');
124
135
  encrypted += cipher.final('hex');
@@ -130,7 +141,7 @@ export class GitLabService {
130
141
  if (parts.length !== 2)
131
142
  return encrypted; // Not encrypted
132
143
  const iv = Buffer.from(parts[0], 'hex');
133
- const key = createHash('sha256').update(ENCRYPTION_KEY).digest();
144
+ const key = createHash('sha256').update(getEncryptionKey()).digest();
134
145
  const decipher = createDecipheriv('aes-256-cbc', key, iv);
135
146
  let decrypted = decipher.update(parts[1], 'hex', 'utf8');
136
147
  decrypted += decipher.final('utf8');
@@ -488,19 +499,41 @@ export class GitLabService {
488
499
  throw new Error('GitLab instance not found');
489
500
  }
490
501
  const accessToken = this.decrypt(instance.accessToken);
491
- const encodedId = encodeURIComponent(String(projectId));
492
- const sha = ref || 'HEAD';
493
- const url = `${instance.instanceUrl}/api/v4/projects/${encodedId}/repository/archive.zip?sha=${encodeURIComponent(sha)}`;
494
- const response = await fetch(url, {
495
- headers: {
496
- 'PRIVATE-TOKEN': accessToken
502
+ // Get project info for default branch
503
+ const project = await this.getProject(userId, instanceId, projectId);
504
+ const branch = ref || project.default_branch || 'main';
505
+ // Use API endpoint with wget (Node.js fetch sends headers that GitLab rejects)
506
+ const apiUrl = `${instance.instanceUrl}/api/v4/projects/${encodeURIComponent(String(projectId))}/repository/archive?sha=${encodeURIComponent(branch)}`;
507
+ Logger.info(`[GitLab] Downloading from: ${apiUrl}`);
508
+ // Use wget via child_process - it works where fetch fails
509
+ const { execSync } = await import('child_process');
510
+ const { tmpdir } = await import('os');
511
+ const { join: pathJoin } = await import('path');
512
+ const { readFileSync, unlinkSync } = await import('fs');
513
+ const tempFile = pathJoin(tmpdir(), `gitlab-archive-${Date.now()}.tar.gz`);
514
+ try {
515
+ execSync(`wget -q --header="PRIVATE-TOKEN: ${accessToken}" -O "${tempFile}" "${apiUrl}"`, {
516
+ timeout: 300000, // 5 min timeout
517
+ stdio: 'pipe'
518
+ });
519
+ const buffer = readFileSync(tempFile);
520
+ Logger.info(`[GitLab] Downloaded ${buffer.length} bytes`);
521
+ // Cleanup temp file
522
+ try {
523
+ unlinkSync(tempFile);
497
524
  }
498
- });
499
- if (!response.ok) {
500
- throw new Error(`Failed to download repository: ${response.status}`);
525
+ catch { }
526
+ return buffer;
527
+ }
528
+ catch (error) {
529
+ // Cleanup on error
530
+ try {
531
+ unlinkSync(tempFile);
532
+ }
533
+ catch { }
534
+ Logger.error(`[GitLab] wget failed: ${error}`);
535
+ throw new Error(`Failed to download repository: wget failed`);
501
536
  }
502
- const arrayBuffer = await response.arrayBuffer();
503
- return Buffer.from(arrayBuffer);
504
537
  }
505
538
  /**
506
539
  * Get file content from repository
@@ -21,7 +21,7 @@ import { fileURLToPath } from 'url';
21
21
  import { Logger } from '../utils/logger.js';
22
22
  import { apiRouter } from './routes/api.js';
23
23
  import { uploadRouter } from './routes/upload.js';
24
- import { authRouter } from './routes/auth.js';
24
+ import { authRouter, cleanupAuthIntervals } from './routes/auth.js';
25
25
  import { oauthRouter } from './routes/oauth.js';
26
26
  import { adminRouter } from './routes/admin.js';
27
27
  import { developerRouter } from './routes/developer.js';
@@ -159,6 +159,15 @@ export class ArchiCoreServer {
159
159
  this.setupErrorHandling();
160
160
  }
161
161
  setupMiddleware() {
162
+ // Trust proxy - required when running behind nginx/reverse proxy
163
+ // This allows express-rate-limit to correctly identify clients via X-Forwarded-For
164
+ // Set TRUST_PROXY=false to disable, or specify number of proxies (e.g., "1", "2")
165
+ const trustProxy = process.env.TRUST_PROXY;
166
+ if (trustProxy !== 'false') {
167
+ // Default: trust first proxy (typical setup with nginx)
168
+ const proxyCount = trustProxy ? parseInt(trustProxy, 10) : 1;
169
+ this.app.set('trust proxy', isNaN(proxyCount) ? trustProxy : proxyCount);
170
+ }
162
171
  // Security headers (helmet) - enabled by default for security
163
172
  // Set HELMET_ENABLED=false to disable (not recommended)
164
173
  const helmetEnabled = process.env.HELMET_ENABLED !== 'false';
@@ -419,6 +428,8 @@ export class ArchiCoreServer {
419
428
  });
420
429
  }
421
430
  async stop() {
431
+ // Cleanup intervals
432
+ cleanupAuthIntervals();
422
433
  // Disconnect cache and database
423
434
  await cache.disconnect();
424
435
  await db.close();
@@ -438,9 +449,43 @@ export class ArchiCoreServer {
438
449
  return this.app;
439
450
  }
440
451
  }
452
+ /**
453
+ * Validate required environment variables for production
454
+ */
455
+ function validateProductionEnvironment() {
456
+ if (process.env.NODE_ENV !== 'production') {
457
+ return; // Only enforce in production
458
+ }
459
+ const requiredVars = [
460
+ 'JWT_SECRET',
461
+ 'ENCRYPTION_KEY',
462
+ 'ENCRYPTION_SALT',
463
+ ];
464
+ const missingVars = [];
465
+ for (const varName of requiredVars) {
466
+ if (!process.env[varName]) {
467
+ missingVars.push(varName);
468
+ }
469
+ }
470
+ // Check for weak JWT_SECRET
471
+ const jwtSecret = process.env.JWT_SECRET;
472
+ if (jwtSecret && jwtSecret.length < 32) {
473
+ Logger.error('CRITICAL: JWT_SECRET must be at least 32 characters in production');
474
+ missingVars.push('JWT_SECRET (too short)');
475
+ }
476
+ if (missingVars.length > 0) {
477
+ Logger.error('CRITICAL: Missing required environment variables for production:');
478
+ missingVars.forEach(v => Logger.error(` - ${v}`));
479
+ Logger.error('Server cannot start in production mode without these variables.');
480
+ process.exit(1);
481
+ }
482
+ Logger.info('Production environment validation passed');
483
+ }
441
484
  // Запуск сервера при прямом вызове
442
485
  const isMainModule = process.argv[1]?.includes('server');
443
486
  if (isMainModule) {
487
+ // Validate production environment before starting
488
+ validateProductionEnvironment();
444
489
  const port = parseInt(process.env.PORT || '3000', 10);
445
490
  const host = process.env.HOST || '0.0.0.0';
446
491
  const server = new ArchiCoreServer({
@@ -458,5 +503,27 @@ if (isMainModule) {
458
503
  await server.stop();
459
504
  process.exit(0);
460
505
  });
506
+ process.on('SIGTERM', async () => {
507
+ Logger.info('SIGTERM received, shutting down gracefully...');
508
+ await server.stop();
509
+ process.exit(0);
510
+ });
511
+ // Handle uncaught exceptions - log and exit gracefully
512
+ process.on('uncaughtException', async (error) => {
513
+ Logger.error('Uncaught Exception:', error);
514
+ try {
515
+ await server.stop();
516
+ }
517
+ catch (stopError) {
518
+ Logger.error('Error during shutdown after uncaughtException:', stopError);
519
+ }
520
+ process.exit(1);
521
+ });
522
+ // Handle unhandled promise rejections
523
+ process.on('unhandledRejection', (reason, promise) => {
524
+ Logger.error('Unhandled Promise Rejection:', reason);
525
+ Logger.error('Promise:', promise);
526
+ // Don't exit - just log, but track for debugging
527
+ });
461
528
  }
462
529
  //# sourceMappingURL=index.js.map