archicore 0.2.0 → 0.2.1

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.
@@ -444,7 +444,7 @@ async function handleIndexCommand() {
444
444
  async function handleAnalyzeCommand(args) {
445
445
  if (!state.projectId) {
446
446
  printError('No project selected');
447
- printInfo('Use /projects select <id> first');
447
+ printInfo('Use /index first');
448
448
  return;
449
449
  }
450
450
  const description = args.join(' ') || 'General analysis';
@@ -465,15 +465,20 @@ async function handleAnalyzeCommand(args) {
465
465
  const data = await response.json();
466
466
  spinner.succeed('Analysis complete');
467
467
  printSection('Impact Analysis');
468
- const impact = data.impact;
469
- // Affected components
470
- const affected = impact.affectedNodes || [];
468
+ // Handle various response formats
469
+ const impact = data.impact || data.result || data || {};
470
+ const affected = impact.affectedNodes || impact.affected || impact.nodes || [];
471
471
  console.log(` ${colors.highlight('Affected Components:')} ${affected.length}`);
472
+ if (affected.length === 0) {
473
+ printInfo('No components affected by this change');
474
+ showTokenUsage();
475
+ return;
476
+ }
472
477
  const byLevel = {
473
- critical: affected.filter((n) => n.impactLevel === 'critical'),
474
- high: affected.filter((n) => n.impactLevel === 'high'),
475
- medium: affected.filter((n) => n.impactLevel === 'medium'),
476
- low: affected.filter((n) => n.impactLevel === 'low'),
478
+ critical: affected.filter((n) => n.impactLevel === 'critical' || n.level === 'critical'),
479
+ high: affected.filter((n) => n.impactLevel === 'high' || n.level === 'high'),
480
+ medium: affected.filter((n) => n.impactLevel === 'medium' || n.level === 'medium'),
481
+ low: affected.filter((n) => n.impactLevel === 'low' || n.level === 'low'),
477
482
  };
478
483
  if (byLevel.critical.length > 0) {
479
484
  console.log(` ${colors.critical(`${icons.severityCritical} Critical: ${byLevel.critical.length}`)}`);
@@ -487,27 +492,47 @@ async function handleAnalyzeCommand(args) {
487
492
  if (byLevel.low.length > 0) {
488
493
  console.log(` ${colors.low(`${icons.severityLow} Low: ${byLevel.low.length}`)}`);
489
494
  }
495
+ // Show top affected files
496
+ if (affected.length > 0) {
497
+ console.log();
498
+ console.log(` ${colors.highlight('Top affected:')}`);
499
+ for (const node of affected.slice(0, 5)) {
500
+ const name = node.name || node.symbol || 'unknown';
501
+ const file = node.filePath || node.file || '';
502
+ const level = node.impactLevel || node.level || 'unknown';
503
+ const levelColor = level === 'critical' ? colors.critical :
504
+ level === 'high' ? colors.high :
505
+ level === 'medium' ? colors.medium : colors.muted;
506
+ console.log(` ${levelColor(`[${level}]`)} ${name}`);
507
+ if (file)
508
+ console.log(` ${colors.dim(file)}`);
509
+ }
510
+ }
490
511
  // Risks
491
- if (impact.risks?.length > 0) {
512
+ const risks = impact.risks || [];
513
+ if (risks.length > 0) {
492
514
  console.log();
493
515
  console.log(` ${colors.highlight('Risks:')}`);
494
- for (const risk of impact.risks.slice(0, 5)) {
495
- console.log(` ${colors.warning(icons.warning)} ${risk.description}`);
516
+ for (const risk of risks.slice(0, 5)) {
517
+ const desc = risk.description || risk.message || risk;
518
+ console.log(` ${colors.warning(icons.warning)} ${desc}`);
496
519
  }
497
520
  }
498
521
  // Recommendations
499
- if (impact.recommendations?.length > 0) {
522
+ const recommendations = impact.recommendations || [];
523
+ if (recommendations.length > 0) {
500
524
  console.log();
501
525
  console.log(` ${colors.highlight('Recommendations:')}`);
502
- for (const rec of impact.recommendations.slice(0, 3)) {
503
- console.log(` ${colors.info(icons.info)} ${rec.description}`);
526
+ for (const rec of recommendations.slice(0, 3)) {
527
+ const desc = rec.description || rec.message || rec;
528
+ console.log(` ${colors.info(icons.info)} ${desc}`);
504
529
  }
505
530
  }
506
531
  showTokenUsage();
507
532
  }
508
533
  catch (error) {
509
534
  spinner.fail('Analysis failed');
510
- throw error;
535
+ printFormattedError(error, { operation: 'Impact analysis' });
511
536
  }
512
537
  }
513
538
  async function handleSearchCommand(query) {
@@ -923,13 +948,20 @@ async function handleRulesCommand() {
923
948
  console.log();
924
949
  for (const v of violations.slice(0, 10)) {
925
950
  const severity = v.severity || 'warning';
926
- const rule = v.rule || v.name || 'Unknown rule';
951
+ const rule = v.rule || v.type || v.name || '';
927
952
  const message = v.message || v.description || '';
953
+ const file = v.file || v.filePath || '';
928
954
  const severityColor = severity === 'error' ? colors.error :
929
955
  severity === 'warning' ? colors.warning : colors.muted;
930
- console.log(` ${severityColor(icons.error)} ${rule}: ${message}`);
931
- if (v.file || v.filePath) {
932
- console.log(` ${colors.dim(v.file || v.filePath)}`);
956
+ // Format: show rule type if present, otherwise just message
957
+ if (rule && message) {
958
+ console.log(` ${severityColor(icons.error)} [${rule}] ${message}`);
959
+ }
960
+ else {
961
+ console.log(` ${severityColor(icons.error)} ${message || rule}`);
962
+ }
963
+ if (file) {
964
+ console.log(` ${colors.dim(file)}`);
933
965
  }
934
966
  }
935
967
  }
@@ -96,9 +96,13 @@ export declare class GitHubService {
96
96
  */
97
97
  private deleteWebhook;
98
98
  /**
99
- * Verify webhook signature
99
+ * Verify webhook signature (HMAC-SHA256)
100
100
  */
101
101
  verifyWebhookSignature(payload: string, signature: string, secret: string): boolean;
102
+ /**
103
+ * Get decrypted webhook secret
104
+ */
105
+ getWebhookSecret(encryptedSecret: string): string;
102
106
  /**
103
107
  * Find repository by webhook payload
104
108
  */
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Handles OAuth, API calls, webhooks, and repository management
5
5
  */
6
- import { randomBytes, createHash, createHmac, createCipheriv, createDecipheriv } from 'crypto';
6
+ import { randomBytes, createHash, createHmac, createCipheriv, createDecipheriv, timingSafeEqual } from 'crypto';
7
7
  import { readFile, writeFile, mkdir } from 'fs/promises';
8
8
  import { join } from 'path';
9
9
  import { Logger } from '../utils/logger.js';
@@ -483,12 +483,30 @@ export class GitHubService {
483
483
  });
484
484
  }
485
485
  /**
486
- * Verify webhook signature
486
+ * Verify webhook signature (HMAC-SHA256)
487
487
  */
488
488
  verifyWebhookSignature(payload, signature, secret) {
489
489
  const hmac = createHmac('sha256', secret);
490
490
  const digest = 'sha256=' + hmac.update(payload).digest('hex');
491
- return signature === digest;
491
+ // Use timing-safe comparison to prevent timing attacks
492
+ if (signature.length !== digest.length) {
493
+ return false;
494
+ }
495
+ // Convert to buffers for timing-safe comparison
496
+ const signatureBuffer = Buffer.from(signature);
497
+ const digestBuffer = Buffer.from(digest);
498
+ try {
499
+ return timingSafeEqual(signatureBuffer, digestBuffer);
500
+ }
501
+ catch {
502
+ return false;
503
+ }
504
+ }
505
+ /**
506
+ * Get decrypted webhook secret
507
+ */
508
+ getWebhookSecret(encryptedSecret) {
509
+ return this.decrypt(encryptedSecret);
492
510
  }
493
511
  /**
494
512
  * Find repository by webhook payload
@@ -7,11 +7,17 @@ export declare class EmbeddingService {
7
7
  private config;
8
8
  private initialized;
9
9
  private _isAvailable;
10
- private jinaApiKey?;
10
+ private jinaApiKeys;
11
+ private currentKeyIndex;
12
+ private keyFailures;
11
13
  private embeddingDimension;
12
14
  constructor(config: EmbeddingConfig);
13
15
  getEmbeddingDimension(): number;
14
16
  private ensureInitialized;
17
+ private getCurrentJinaKey;
18
+ private rotateToNextKey;
19
+ private shouldSkipKey;
20
+ private findWorkingKeyIndex;
15
21
  isAvailable(): boolean;
16
22
  generateEmbedding(text: string): Promise<number[]>;
17
23
  private generateJinaEmbedding;
@@ -19,6 +25,7 @@ export declare class EmbeddingService {
19
25
  * Generate embeddings for multiple texts - uses true batch API when available
20
26
  */
21
27
  generateBatchEmbeddings(texts: string[], progressCallback?: (current: number, total: number) => void): Promise<number[][]>;
28
+ private generateJinaBatchWithRetry;
22
29
  private generateOpenAIEmbedding;
23
30
  prepareCodeForEmbedding(code: string, context?: string): string;
24
31
  generateCodeEmbedding(code: string, metadata: {
@@ -5,7 +5,9 @@ export class EmbeddingService {
5
5
  config;
6
6
  initialized = false;
7
7
  _isAvailable = false;
8
- jinaApiKey;
8
+ jinaApiKeys = [];
9
+ currentKeyIndex = 0;
10
+ keyFailures = new Map();
9
11
  embeddingDimension = 1024; // Jina default, OpenAI small = 1536
10
12
  constructor(config) {
11
13
  this.config = config;
@@ -23,16 +25,21 @@ export class EmbeddingService {
23
25
  }
24
26
  try {
25
27
  if (this.config.provider === 'jina') {
26
- // Jina AI - free embeddings
27
- this.jinaApiKey = process.env.JINA_API_KEY;
28
- if (!this.jinaApiKey) {
29
- Logger.warn('JINA_API_KEY not set - semantic search disabled');
28
+ // Jina AI - free embeddings with key rotation support
29
+ // Support multiple keys: JINA_API_KEYS (comma-separated) or JINA_API_KEY (single)
30
+ const keysEnv = process.env.JINA_API_KEYS || process.env.JINA_API_KEY || '';
31
+ this.jinaApiKeys = keysEnv
32
+ .split(',')
33
+ .map(k => k.trim())
34
+ .filter(k => k.length > 0);
35
+ if (this.jinaApiKeys.length === 0) {
36
+ Logger.warn('JINA_API_KEY(S) not set - semantic search disabled');
30
37
  Logger.info('Get free API key at: https://jina.ai/embeddings/');
31
38
  return;
32
39
  }
33
40
  this.embeddingDimension = 1024;
34
41
  this._isAvailable = true;
35
- Logger.success('Jina AI embeddings enabled');
42
+ Logger.success(`Jina AI embeddings enabled (${this.jinaApiKeys.length} key${this.jinaApiKeys.length > 1 ? 's' : ''} configured)`);
36
43
  }
37
44
  else {
38
45
  // OpenAI embeddings
@@ -51,6 +58,45 @@ export class EmbeddingService {
51
58
  Logger.warn('Embedding init failed: ' + error);
52
59
  }
53
60
  }
61
+ getCurrentJinaKey() {
62
+ if (this.jinaApiKeys.length === 0)
63
+ return undefined;
64
+ return this.jinaApiKeys[this.currentKeyIndex];
65
+ }
66
+ rotateToNextKey() {
67
+ if (this.jinaApiKeys.length <= 1)
68
+ return false;
69
+ const oldIndex = this.currentKeyIndex;
70
+ this.currentKeyIndex = (this.currentKeyIndex + 1) % this.jinaApiKeys.length;
71
+ // Track failure for old key
72
+ const failure = this.keyFailures.get(oldIndex) || { count: 0, lastFail: new Date() };
73
+ failure.count++;
74
+ failure.lastFail = new Date();
75
+ this.keyFailures.set(oldIndex, failure);
76
+ Logger.warn(`Jina API key ${oldIndex + 1} exhausted/failed, rotating to key ${this.currentKeyIndex + 1}`);
77
+ return true;
78
+ }
79
+ shouldSkipKey(index) {
80
+ const failure = this.keyFailures.get(index);
81
+ if (!failure)
82
+ return false;
83
+ // Skip key if it failed recently (within 5 minutes) and has many failures
84
+ const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
85
+ return failure.count >= 3 && failure.lastFail > fiveMinutesAgo;
86
+ }
87
+ findWorkingKeyIndex() {
88
+ const startIndex = this.currentKeyIndex;
89
+ for (let i = 0; i < this.jinaApiKeys.length; i++) {
90
+ const index = (startIndex + i) % this.jinaApiKeys.length;
91
+ if (!this.shouldSkipKey(index)) {
92
+ return index;
93
+ }
94
+ }
95
+ // All keys are marked as failed, reset failures and try first key
96
+ Logger.warn('All Jina API keys marked as failed, resetting failure counters');
97
+ this.keyFailures.clear();
98
+ return 0;
99
+ }
54
100
  isAvailable() {
55
101
  this.ensureInitialized();
56
102
  return this._isAvailable;
@@ -60,7 +106,7 @@ export class EmbeddingService {
60
106
  if (!this._isAvailable)
61
107
  return new Array(this.embeddingDimension).fill(0);
62
108
  try {
63
- if (this.config.provider === 'jina' && this.jinaApiKey) {
109
+ if (this.config.provider === 'jina' && this.jinaApiKeys.length > 0) {
64
110
  return await this.generateJinaEmbedding(text);
65
111
  }
66
112
  else if (this.openai) {
@@ -73,28 +119,51 @@ export class EmbeddingService {
73
119
  return new Array(this.embeddingDimension).fill(0);
74
120
  }
75
121
  }
76
- async generateJinaEmbedding(text) {
77
- if (!this.jinaApiKey)
122
+ async generateJinaEmbedding(text, retryCount = 0) {
123
+ const maxRetries = this.jinaApiKeys.length;
124
+ // Find a working key index before first attempt
125
+ if (retryCount === 0) {
126
+ this.currentKeyIndex = this.findWorkingKeyIndex();
127
+ }
128
+ const apiKey = this.getCurrentJinaKey();
129
+ if (!apiKey)
78
130
  throw new Error('Jina API key not set');
79
- const response = await fetch('https://api.jina.ai/v1/embeddings', {
80
- method: 'POST',
81
- headers: {
82
- 'Content-Type': 'application/json',
83
- 'Authorization': `Bearer ${this.jinaApiKey}`
84
- },
85
- body: JSON.stringify({
86
- model: this.config.model || 'jina-embeddings-v3',
87
- task: 'text-matching',
88
- dimensions: 1024,
89
- input: [text.substring(0, 8000)]
90
- })
91
- });
92
- if (!response.ok) {
93
- const error = await response.text();
94
- throw new Error(`Jina API error: ${response.status} ${error}`);
131
+ try {
132
+ const response = await fetch('https://api.jina.ai/v1/embeddings', {
133
+ method: 'POST',
134
+ headers: {
135
+ 'Content-Type': 'application/json',
136
+ 'Authorization': `Bearer ${apiKey}`
137
+ },
138
+ body: JSON.stringify({
139
+ model: this.config.model || 'jina-embeddings-v3',
140
+ task: 'text-matching',
141
+ dimensions: 1024,
142
+ input: [text.substring(0, 8000)]
143
+ })
144
+ });
145
+ if (!response.ok) {
146
+ const errorText = await response.text();
147
+ // Check for rate limit or auth errors - try next key
148
+ if (response.status === 429 || response.status === 401 || response.status === 403) {
149
+ if (retryCount < maxRetries - 1 && this.rotateToNextKey()) {
150
+ Logger.warn(`Jina API key error (${response.status}), trying next key...`);
151
+ return this.generateJinaEmbedding(text, retryCount + 1);
152
+ }
153
+ }
154
+ throw new Error(`Jina API error: ${response.status} ${errorText}`);
155
+ }
156
+ const data = await response.json();
157
+ return data.data[0].embedding;
158
+ }
159
+ catch (error) {
160
+ // Network error or other issue - try next key
161
+ if (retryCount < maxRetries - 1 && this.rotateToNextKey()) {
162
+ Logger.warn(`Jina API request failed, trying next key...`);
163
+ return this.generateJinaEmbedding(text, retryCount + 1);
164
+ }
165
+ throw error;
95
166
  }
96
- const data = await response.json();
97
- return data.data[0].embedding;
98
167
  }
99
168
  /**
100
169
  * Generate embeddings for multiple texts - uses true batch API when available
@@ -110,30 +179,13 @@ export class EmbeddingService {
110
179
  Logger.progress(`Generating embeddings for ${texts.length} texts...`);
111
180
  try {
112
181
  // Use true batch API for Jina (much faster!)
113
- if (this.config.provider === 'jina' && this.jinaApiKey) {
182
+ if (this.config.provider === 'jina' && this.jinaApiKeys.length > 0) {
114
183
  const BATCH_SIZE = 500;
115
184
  const allEmbeddings = [];
116
185
  for (let i = 0; i < texts.length; i += BATCH_SIZE) {
117
186
  const batch = texts.slice(i, i + BATCH_SIZE).map(t => t.substring(0, 8000));
118
- const response = await fetch('https://api.jina.ai/v1/embeddings', {
119
- method: 'POST',
120
- headers: {
121
- 'Content-Type': 'application/json',
122
- 'Authorization': `Bearer ${this.jinaApiKey}`
123
- },
124
- body: JSON.stringify({
125
- model: this.config.model || 'jina-embeddings-v3',
126
- task: 'text-matching',
127
- dimensions: 1024,
128
- input: batch
129
- })
130
- });
131
- if (!response.ok) {
132
- const error = await response.text();
133
- throw new Error(`Jina API error: ${response.status} ${error}`);
134
- }
135
- const data = await response.json();
136
- allEmbeddings.push(...data.data.map(d => d.embedding));
187
+ const batchEmbeddings = await this.generateJinaBatchWithRetry(batch);
188
+ allEmbeddings.push(...batchEmbeddings);
137
189
  if (progressCallback) {
138
190
  progressCallback(Math.min(i + BATCH_SIZE, texts.length), texts.length);
139
191
  }
@@ -161,6 +213,48 @@ export class EmbeddingService {
161
213
  return texts.map(() => new Array(this.embeddingDimension).fill(0));
162
214
  }
163
215
  }
216
+ async generateJinaBatchWithRetry(batch, retryCount = 0) {
217
+ const maxRetries = this.jinaApiKeys.length;
218
+ const apiKey = this.getCurrentJinaKey();
219
+ if (!apiKey)
220
+ throw new Error('Jina API key not set');
221
+ try {
222
+ const response = await fetch('https://api.jina.ai/v1/embeddings', {
223
+ method: 'POST',
224
+ headers: {
225
+ 'Content-Type': 'application/json',
226
+ 'Authorization': `Bearer ${apiKey}`
227
+ },
228
+ body: JSON.stringify({
229
+ model: this.config.model || 'jina-embeddings-v3',
230
+ task: 'text-matching',
231
+ dimensions: 1024,
232
+ input: batch
233
+ })
234
+ });
235
+ if (!response.ok) {
236
+ const errorText = await response.text();
237
+ // Check for rate limit or auth errors - try next key
238
+ if (response.status === 429 || response.status === 401 || response.status === 403) {
239
+ if (retryCount < maxRetries - 1 && this.rotateToNextKey()) {
240
+ Logger.warn(`Jina API batch error (${response.status}), trying next key...`);
241
+ return this.generateJinaBatchWithRetry(batch, retryCount + 1);
242
+ }
243
+ }
244
+ throw new Error(`Jina API error: ${response.status} ${errorText}`);
245
+ }
246
+ const data = await response.json();
247
+ return data.data.map(d => d.embedding);
248
+ }
249
+ catch (error) {
250
+ // Network error or other issue - try next key
251
+ if (retryCount < maxRetries - 1 && this.rotateToNextKey()) {
252
+ Logger.warn(`Jina API batch request failed, trying next key...`);
253
+ return this.generateJinaBatchWithRetry(batch, retryCount + 1);
254
+ }
255
+ throw error;
256
+ }
257
+ }
164
258
  async generateOpenAIEmbedding(text) {
165
259
  if (!this.openai)
166
260
  throw new Error('OpenAI client not initialized');
@@ -25,6 +25,9 @@ import { adminRouter } from './routes/admin.js';
25
25
  import { developerRouter } from './routes/developer.js';
26
26
  import { githubRouter } from './routes/github.js';
27
27
  import deviceAuthRouter from './routes/device-auth.js';
28
+ import { cache } from './services/cache.js';
29
+ import { db } from './services/database.js';
30
+ import { AuthService } from './services/auth-service.js';
28
31
  const __filename = fileURLToPath(import.meta.url);
29
32
  const __dirname = path.dirname(__filename);
30
33
  // CORS whitelist - настройте под свои домены
@@ -156,6 +159,7 @@ export class ArchiCoreServer {
156
159
  this.app.use(express.static(publicPath, {
157
160
  maxAge: process.env.NODE_ENV === 'production' ? '1d' : 0,
158
161
  etag: true,
162
+ index: false, // Отключаем автоматический index.html - используем явные маршруты
159
163
  }));
160
164
  }
161
165
  setupRoutes() {
@@ -187,7 +191,54 @@ export class ArchiCoreServer {
187
191
  const deviceAuthPath = path.join(__dirname, '../../public/device-auth.html');
188
192
  res.sendFile(deviceAuthPath);
189
193
  });
190
- // SPA fallback - все остальные маршруты отдают index.html
194
+ // Clean URL routing
195
+ // Landing page (главная)
196
+ this.app.get('/', (_req, res) => {
197
+ res.sendFile(path.join(__dirname, '../../public/landing.html'));
198
+ });
199
+ // Dashboard (после авторизации)
200
+ this.app.get('/dashboard', (_req, res) => {
201
+ res.sendFile(path.join(__dirname, '../../public/index.html'));
202
+ });
203
+ // Add new project page
204
+ this.app.get('/new', (_req, res) => {
205
+ res.sendFile(path.join(__dirname, '../../public/index.html'));
206
+ });
207
+ // Pricing page
208
+ this.app.get('/pricing', (_req, res) => {
209
+ res.sendFile(path.join(__dirname, '../../public/pricing.html'));
210
+ });
211
+ // API Dashboard page
212
+ this.app.get('/api-dashboard', (_req, res) => {
213
+ res.sendFile(path.join(__dirname, '../../public/api-dashboard.html'));
214
+ });
215
+ // Auth page
216
+ this.app.get('/auth', (_req, res) => {
217
+ res.sendFile(path.join(__dirname, '../../public/auth.html'));
218
+ });
219
+ // Login alias
220
+ this.app.get('/login', (_req, res) => {
221
+ res.sendFile(path.join(__dirname, '../../public/auth.html'));
222
+ });
223
+ // Register alias
224
+ this.app.get('/register', (_req, res) => {
225
+ res.redirect('/auth?mode=register');
226
+ });
227
+ // Admin page
228
+ this.app.get('/admin', (_req, res) => {
229
+ res.sendFile(path.join(__dirname, '../../public/admin.html'));
230
+ });
231
+ // Legal pages
232
+ this.app.get('/privacy', (_req, res) => {
233
+ res.sendFile(path.join(__dirname, '../../public/privacy.html'));
234
+ });
235
+ this.app.get('/terms', (_req, res) => {
236
+ res.sendFile(path.join(__dirname, '../../public/terms.html'));
237
+ });
238
+ this.app.get('/security', (_req, res) => {
239
+ res.sendFile(path.join(__dirname, '../../public/security.html'));
240
+ });
241
+ // SPA fallback - все остальные маршруты отдают index.html (dashboard)
191
242
  this.app.get('/*splat', (_req, res) => {
192
243
  const indexPath = path.join(__dirname, '../../public/index.html');
193
244
  res.sendFile(indexPath);
@@ -208,6 +259,17 @@ export class ArchiCoreServer {
208
259
  });
209
260
  }
210
261
  async start() {
262
+ // Initialize database
263
+ try {
264
+ await db.init();
265
+ const authService = AuthService.getInstance();
266
+ await authService.initDatabase();
267
+ }
268
+ catch (error) {
269
+ Logger.warn('Database initialization failed, using JSON fallback:', error);
270
+ }
271
+ // Initialize cache
272
+ await cache.connect();
211
273
  return new Promise((resolve) => {
212
274
  this.server = createServer(this.app);
213
275
  const host = this.config.host || '0.0.0.0';
@@ -221,6 +283,9 @@ export class ArchiCoreServer {
221
283
  });
222
284
  }
223
285
  async stop() {
286
+ // Disconnect cache and database
287
+ await cache.disconnect();
288
+ await db.close();
224
289
  return new Promise((resolve) => {
225
290
  if (this.server) {
226
291
  this.server.close(() => {