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.
- package/dist/cli/commands/interactive.js +220 -1
- package/dist/github/github-service.d.ts +4 -4
- package/dist/github/github-service.js +29 -13
- package/dist/gitlab/gitlab-service.js +48 -15
- package/dist/server/index.js +68 -1
- package/dist/server/routes/api.js +242 -2
- package/dist/server/routes/auth.d.ts +1 -0
- package/dist/server/routes/auth.js +11 -3
- package/dist/server/routes/gitlab.js +18 -46
- package/dist/server/routes/report-issue.js +103 -0
- package/dist/server/routes/upload.js +3 -3
- package/dist/server/services/encryption.js +20 -4
- package/dist/server/services/enterprise-indexer.d.ts +116 -0
- package/dist/server/services/enterprise-indexer.js +529 -0
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
14
|
-
|
|
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(
|
|
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(
|
|
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+
|
|
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
|
|
23
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
492
|
-
const
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
|
|
500
|
-
|
|
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
|
package/dist/server/index.js
CHANGED
|
@@ -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
|