mcp-prompt-optimizer 3.7.0 → 3.7.2

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.
@@ -1,729 +1,730 @@
1
- /**
2
- * Cloud API Key Manager for MCP Prompt Optimizer
3
- * Production-grade with enhanced network resilience and development mode
4
- * ALIGNED with backend API requirements - FIXED API ENDPOINTS
5
- */
6
-
7
- const fs = require('fs').promises;
8
- const path = require('path');
9
- const https = require('https');
10
- const http = require('http');
11
- const os = require('os');
12
-
13
- const packageJson = require('../package.json');
14
-
15
- class CloudApiKeyManager {
16
- constructor(apiKey, options = {}) {
17
- this.apiKey = apiKey;
18
- this.backendUrl = options.backendUrl || process.env.OPTIMIZER_BACKEND_URL || 'https://p01--project-optimizer--fvmrdk8m9k9j.code.run';
19
- this.cacheFile = path.join(os.homedir(), '.mcp-cloud-api-cache.json');
20
- this.healthFile = path.join(os.homedir(), '.mcp-cloud-health.json');
21
- this.cacheExpiry = options.cacheExpiry || 1 * 60 * 60 * 1000; // 1 hour (reduced from 24)
22
- this.fallbackCacheExpiry = options.fallbackCacheExpiry || 2 * 60 * 60 * 1000; // 2 hours (reduced from 7 days)
23
- this.logPrefix = '[CloudApiKeyManager]';
24
- // SECURITY: Offline mode disabled in production for security
25
- this.offlineMode = false;
26
- // SECURITY: Development mode disabled - use separate dev builds
27
- this.developmentMode = false;
28
- this.maxRetries = options.maxRetries || 5; // Increased for production
29
- this.baseRetryDelay = options.baseRetryDelay || 1000;
30
- this.maxRetryDelay = options.maxRetryDelay || 30000;
31
- this.requestTimeout = options.requestTimeout || 15000;
32
-
33
- // Network health tracking
34
- this.networkHealth = {
35
- consecutiveFailures: 0,
36
- lastSuccessful: null,
37
- avgResponseTime: null,
38
- lastErrorType: null
39
- };
40
- }
41
-
42
- log(message, level = 'info') {
43
- const timestamp = new Date().toISOString();
44
- const prefix = `${timestamp} ${this.logPrefix}`;
45
-
46
- if (level === 'error') {
47
- console.error(`${prefix} ❌ ${message}`);
48
- } else if (level === 'warn') {
49
- console.warn(`${prefix} ⚠️ ${message}`);
50
- } else if (level === 'success') {
51
- console.log(`${prefix} ${message}`);
52
- } else {
53
- console.log(`${prefix} ℹ️ ${message}`);
54
- }
55
- }
56
-
57
- // Production-grade exponential backoff with jitter
58
- calculateRetryDelay(attempt) {
59
- const exponentialDelay = Math.min(
60
- this.baseRetryDelay * Math.pow(2, attempt - 1),
61
- this.maxRetryDelay
62
- );
63
-
64
- // Add jitter to prevent thundering herd
65
- const jitter = Math.random() * 0.3 * exponentialDelay;
66
- return Math.floor(exponentialDelay + jitter);
67
- }
68
-
69
- // Enhanced API key format validation
70
- validateApiKeyFormat(apiKey) {
71
- if (!apiKey || typeof apiKey !== 'string') {
72
- return { valid: false, error: 'API key must be a string' };
73
- }
74
-
75
- // Support development keys
76
- const validPrefixes = ['sk-opt-', 'sk-team-', 'sk-local-', 'sk-dev-'];
77
- const hasValidPrefix = validPrefixes.some(prefix => apiKey.startsWith(prefix));
78
-
79
- if (!hasValidPrefix) {
80
- return {
81
- valid: false,
82
- error: 'Invalid API key format. Must start with "sk-opt-" (individual), "sk-team-" (team), "sk-local-" (development), or "sk-dev-" (testing)'
83
- };
84
- }
85
-
86
- // Check minimum length for security
87
- if (apiKey.length < 20) {
88
- return {
89
- valid: false,
90
- error: 'API key too short'
91
- };
92
- }
93
-
94
- // Determine type
95
- let keyType = 'unknown';
96
- if (apiKey.startsWith('sk-opt-')) {
97
- keyType = 'individual';
98
- } else if (apiKey.startsWith('sk-team-')) {
99
- keyType = 'team';
100
- } else if (apiKey.startsWith('sk-local-')) {
101
- keyType = 'development';
102
- } else if (apiKey.startsWith('sk-dev-')) {
103
- keyType = 'testing';
104
- }
105
-
106
- return {
107
- valid: true,
108
- keyType: keyType
109
- };
110
- }
111
-
112
- // Development mode mock responses
113
- generateMockValidation(keyType) {
114
- const mockResponses = {
115
- individual: {
116
- valid: true,
117
- tier: 'explorer',
118
- api_key_type: 'individual',
119
- quota: {
120
- limit: 5000,
121
- used: Math.floor(Math.random() * 1000),
122
- unlimited: false
123
- },
124
- features: {
125
- ai_context_detection: true,
126
- template_management: true,
127
- optimization_insights: true
128
- }
129
- },
130
- team: {
131
- valid: true,
132
- tier: 'creator',
133
- api_key_type: 'team',
134
- quota: {
135
- limit: 18000,
136
- used: Math.floor(Math.random() * 3000),
137
- unlimited: false
138
- },
139
- features: {
140
- ai_context_detection: true,
141
- template_management: true,
142
- team_collaboration: true,
143
- optimization_insights: true
144
- }
145
- },
146
- development: {
147
- valid: true,
148
- tier: 'development',
149
- api_key_type: 'development',
150
- quota: {
151
- unlimited: true
152
- },
153
- features: {
154
- ai_context_detection: true,
155
- template_management: true,
156
- optimization_insights: true,
157
- development_mode: true
158
- }
159
- },
160
- testing: {
161
- valid: true,
162
- tier: 'testing',
163
- api_key_type: 'testing',
164
- quota: {
165
- limit: 1000,
166
- used: Math.floor(Math.random() * 100),
167
- unlimited: false
168
- },
169
- features: {
170
- ai_context_detection: true,
171
- template_management: true,
172
- optimization_insights: true,
173
- testing_mode: true
174
- }
175
- }
176
- };
177
-
178
- const response = mockResponses[keyType] || mockResponses.development;
179
- response.mock_mode = true;
180
- response.backend_url = 'mock://development-mode';
181
-
182
- return response;
183
- }
184
-
185
- // Enhanced API key validation with production resilience
186
- async validateApiKey() {
187
- this.log('Starting comprehensive API key validation...');
188
-
189
- if (!this.apiKey) {
190
- throw new Error('API key is required. Set OPTIMIZER_API_KEY environment variable or provide key directly.');
191
- }
192
-
193
- // Step 1: Format validation
194
- const formatCheck = this.validateApiKeyFormat(this.apiKey);
195
- if (!formatCheck.valid) {
196
- throw new Error(formatCheck.error);
197
- }
198
-
199
- this.log(`API key format valid: ${formatCheck.keyType}`);
200
-
201
- // SECURITY: Mock validation removed - all keys must validate against backend
202
- // Development/testing keys must be real keys in the database
203
-
204
- try {
205
- // Step 3: Backend validation with enhanced retry logic
206
- const validation = await this.validateWithBackendRetry();
207
-
208
- // Step 4: Validate response structure
209
- if (validation && validation.valid) {
210
- await this.cacheValidation(validation);
211
- await this.updateNetworkHealth(true);
212
- this.log(`API key validated successfully: ${validation.tier}`, 'success');
213
- return validation;
214
- } else {
215
- throw new Error(validation?.detail || validation?.error || 'API key validation failed');
216
- }
217
-
218
- } catch (error) {
219
- this.log(`Backend validation failed: ${error.message}`, 'warn');
220
- await this.updateNetworkHealth(false, error.message);
221
-
222
- // Enhanced fallback strategy
223
- const cachedValidation = await this.getCachedValidation();
224
-
225
- if (cachedValidation && !this.isCacheExpired(cachedValidation)) {
226
- this.log('Using cached API key validation', 'warn');
227
- return cachedValidation.data;
228
- }
229
-
230
- // SECURITY: Limited fallback for brief network issues only (2 hours max)
231
- if (cachedValidation && !this.isFallbackCacheExpired(cachedValidation)) {
232
- this.log('Using short-term fallback cache due to network issues', 'warn');
233
- const fallbackData = cachedValidation.data;
234
- fallbackData.fallback_mode = true;
235
- fallbackData.network_issue = error.message;
236
- fallbackData.expires_soon = true;
237
- return fallbackData;
238
- }
239
-
240
- // SECURITY: Offline mode removed - backend validation required
241
- // No cache fallback beyond 2 hours
242
-
243
- throw new Error(`API key validation failed: ${error.message}. Please check your internet connection.`);
244
- }
245
- }
246
-
247
- // Production-grade retry logic with exponential backoff
248
- async validateWithBackendRetry() {
249
- let lastError;
250
-
251
- for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
252
- try {
253
- this.log(`Validation attempt ${attempt}/${this.maxRetries}...`);
254
- const startTime = Date.now();
255
-
256
- const result = await this.validateWithBackend();
257
-
258
- // Track response time for health monitoring
259
- const responseTime = Date.now() - startTime;
260
- if (this.networkHealth.avgResponseTime === null) {
261
- this.networkHealth.avgResponseTime = responseTime;
262
- } else {
263
- this.networkHealth.avgResponseTime = (this.networkHealth.avgResponseTime + responseTime) / 2;
264
- }
265
-
266
- return result;
267
- } catch (error) {
268
- lastError = error;
269
- this.log(`Attempt ${attempt} failed: ${error.message}`, 'warn');
270
-
271
- if (attempt < this.maxRetries) {
272
- const delay = this.calculateRetryDelay(attempt);
273
- this.log(`Retrying in ${delay}ms...`);
274
- await new Promise(resolve => setTimeout(resolve, delay));
275
- } else {
276
- this.log('All retry attempts exhausted', 'error');
277
- }
278
- }
279
- }
280
-
281
- throw lastError;
282
- }
283
-
284
- // ✅ FIXED: Correct API endpoint URL
285
- async validateWithBackend() {
286
- const endpoint = '/api/v1/api-keys/validate';
287
- const method = 'POST';
288
-
289
- try {
290
- const validation = await this._makeBackendRequest(endpoint, null, method);
291
- this.log(`Validation successful: ${JSON.stringify(validation, null, 2)}`);
292
- return validation;
293
- } catch (error) {
294
- this.log(`Backend validation request failed: ${error.message}`, 'error');
295
- throw error;
296
- }
297
- }
298
-
299
- // ✅ FIXED: Correct API endpoint URL
300
- async getQuotaStatus() {
301
- try {
302
- // FIXED: Use MCP quota-status endpoint (accepts API key auth)
303
- const url = `${this.backendUrl}/api/v1/mcp/quota-status`;
304
-
305
- const options = {
306
- method: 'GET',
307
- headers: {
308
- 'x-api-key': this.apiKey,
309
- 'User-Agent': `mcp-prompt-optimizer/${packageJson.version}`,
310
- 'Connection': 'close'
311
- },
312
- timeout: this.requestTimeout
313
- };
314
-
315
- return new Promise((resolve, reject) => {
316
- const client = this.backendUrl.startsWith('https://') ? https : http;
317
- const req = client.request(url, options, (res) => {
318
- let data = '';
319
-
320
- res.on('data', (chunk) => {
321
- data += chunk;
322
- });
323
-
324
- res.on('end', () => {
325
- try {
326
- if (res.statusCode === 200) {
327
- const response = JSON.parse(data);
328
- // FIXED: Map nested MCP response to flat format
329
- resolve({
330
- tier: response.tier,
331
- unlimited: response.quota?.unlimited || false,
332
- used: response.quota?.used || 0,
333
- remaining: response.quota?.remaining || 0,
334
- limit: response.quota?.limit,
335
- usage_percentage: response.quota?.percentage || 0,
336
- status: response.quota?.status || 'unknown',
337
- features_available: response.features_available || {}
338
- });
339
- } else {
340
- let errorMessage;
341
- try {
342
- const error = JSON.parse(data);
343
- errorMessage = error.detail || `HTTP ${res.statusCode}`;
344
- } catch {
345
- errorMessage = `HTTP ${res.statusCode}: ${data}`;
346
- }
347
- reject(new Error(errorMessage));
348
- }
349
- } catch (parseError) {
350
- reject(new Error(`Invalid response: ${parseError.message}`));
351
- }
352
- });
353
- });
354
-
355
- req.on('error', (error) => {
356
- reject(new Error(`Network error: ${error.message}`));
357
- });
358
-
359
- req.on('timeout', () => {
360
- req.destroy();
361
- reject(new Error('Request timeout'));
362
- });
363
-
364
- req.setTimeout(this.requestTimeout);
365
- req.end();
366
- });
367
-
368
- } catch (error) {
369
- this.log(`Quota status check failed: ${error.message}`, 'warn');
370
- throw error;
371
- }
372
- }
373
-
374
- // Network health tracking
375
- async updateNetworkHealth(success, errorMessage = null) {
376
- try {
377
- if (success) {
378
- this.networkHealth.consecutiveFailures = 0;
379
- this.networkHealth.lastSuccessful = Date.now();
380
- this.networkHealth.lastErrorType = null;
381
- } else {
382
- this.networkHealth.consecutiveFailures++;
383
- this.networkHealth.lastErrorType = errorMessage;
384
- }
385
-
386
- // Save health metrics
387
- await fs.writeFile(this.healthFile, JSON.stringify(this.networkHealth, null, 2));
388
- } catch (error) {
389
- this.log(`Failed to update network health: ${error.message}`, 'warn');
390
- }
391
- }
392
-
393
-
394
-
395
- // Enhanced caching with metadata
396
- async cacheValidation(validation) {
397
- try {
398
- const cacheData = {
399
- timestamp: Date.now(),
400
- apiKeyPrefix: this.apiKey.substring(0, 20) + '...', // Safe prefix only
401
- data: validation,
402
- backendUrl: this.backendUrl,
403
- packageVersion: packageJson.version,
404
- networkHealth: { ...this.networkHealth }
405
- };
406
-
407
- await fs.writeFile(this.cacheFile, JSON.stringify(cacheData, null, 2));
408
- this.log('API key validation cached successfully');
409
- } catch (error) {
410
- this.log(`Failed to cache validation: ${error.message}`, 'warn');
411
- }
412
- }
413
-
414
- async getCachedValidation() {
415
- try {
416
- const cacheContent = await fs.readFile(this.cacheFile, 'utf8');
417
- const cached = JSON.parse(cacheContent);
418
-
419
- // Validate cache structure
420
- if (!cached.timestamp || !cached.data) {
421
- this.log('Invalid cache structure, ignoring', 'warn');
422
- return null;
423
- }
424
-
425
- return cached;
426
- } catch (error) {
427
- if (error.code !== 'ENOENT') {
428
- this.log(`Cache read error: ${error.message}`, 'warn');
429
- }
430
- return null;
431
- }
432
- }
433
-
434
- isCacheExpired(cachedData) {
435
- if (!cachedData || !cachedData.timestamp) {
436
- return true;
437
- }
438
-
439
- const age = Date.now() - cachedData.timestamp;
440
- const expired = age > this.cacheExpiry;
441
-
442
- if (expired) {
443
- this.log(`Cache expired: ${Math.round(age / 1000 / 60)} minutes old`);
444
- }
445
-
446
- return expired;
447
- }
448
-
449
- // Extended fallback cache for network issues
450
- isFallbackCacheExpired(cachedData) {
451
- if (!cachedData || !cachedData.timestamp) {
452
- return true;
453
- }
454
-
455
- const age = Date.now() - cachedData.timestamp;
456
- const expired = age > this.fallbackCacheExpiry;
457
-
458
- if (expired) {
459
- this.log(`Fallback cache expired: ${Math.round(age / 1000 / 60 / 60)} hours old`);
460
- }
461
-
462
- return expired;
463
- }
464
-
465
- async clearCache() {
466
- try {
467
- await fs.unlink(this.cacheFile);
468
- this.log('API key cache cleared successfully');
469
- } catch (error) {
470
- if (error.code !== 'ENOENT') {
471
- this.log(`Cache clear error: ${error.message}`, 'warn');
472
- }
473
- }
474
-
475
- try {
476
- await fs.unlink(this.healthFile);
477
- this.log('Network health cache cleared successfully');
478
- } catch (error) {
479
- if (error.code !== 'ENOENT') {
480
- this.log(`Health cache clear error: ${error.message}`, 'warn');
481
- }
482
- }
483
- }
484
-
485
- // Enhanced validation and preparation
486
- async validateAndPrepare() {
487
- this.log('Starting comprehensive API key validation and preparation...');
488
-
489
- try {
490
- // Step 1: Validate API key
491
- let validation = await this.validateApiKey();
492
-
493
- // Step 2: Get comprehensive quota status
494
- const info = await this.getApiKeyInfo();
495
- let quotaStatus = info.quota;
496
- validation = info; // Use info as the main validation object for consistency
497
-
498
- // Step 3: Log success
499
- const mode = validation.mock_mode ? '(mock)' :
500
- validation.fallback_mode ? '(fallback)' :
501
- validation.offline_mode ? '(offline)' : '';
502
-
503
- if (quotaStatus.unlimited) {
504
- this.log(`API key valid: ${validation.tier} ${mode} (unlimited usage)`, 'success');
505
- } else {
506
- this.log(`API key valid: ${validation.tier} ${mode} (${quotaStatus.remaining}/${quotaStatus.limit} remaining this month)`, 'success');
507
- }
508
-
509
- return {
510
- validation,
511
- quotaStatus,
512
- tier: validation.tier,
513
- features: validation.features || {},
514
- mode: {
515
- development: this.developmentMode,
516
- mock: validation.mock_mode || false,
517
- fallback: validation.fallback_mode || false,
518
- offline: validation.offline_mode || false
519
- }
520
- };
521
-
522
- } catch (error) {
523
- this.log(`API key validation failed: ${error.message}`, 'error');
524
- throw error;
525
- }
526
- }
527
-
528
- // Enhanced API key info with backend validation
529
- async getApiKeyInfo() {
530
- const formatCheck = this.validateApiKeyFormat(this.apiKey);
531
-
532
- // SECURITY: Mock data removed - all keys must validate with backend
533
-
534
- try {
535
- // FIXED: Use MCP quota-status endpoint (accepts API key auth)
536
- const quotaStatusResponse = await this._makeBackendRequest('/api/v1/mcp/quota-status', null, 'GET');
537
-
538
- // The backend /mcp/quota-status endpoint returns a comprehensive object
539
- // that includes tier, quota details, and features.
540
- return {
541
- tier: quotaStatusResponse.tier,
542
- features: quotaStatusResponse.features_available || {},
543
- quota: quotaStatusResponse.quota,
544
- isValid: true,
545
- keyType: quotaStatusResponse.account_type || this.validateApiKeyFormat(this.apiKey).keyType,
546
- mode: {
547
- mock: false, // This endpoint doesn't return mock status
548
- fallback: false,
549
- offline: false,
550
- development: this.developmentMode
551
- }
552
- };
553
- } catch (error) {
554
- this.log(`Error getting API key info: ${error.message}`, 'error');
555
- return {
556
- tier: null,
557
- features: {},
558
- quota: { allowed: false },
559
- isValid: false,
560
- error: error.message,
561
- keyType: 'unknown',
562
- mode: {
563
- mock: false,
564
- fallback: false,
565
- offline: false,
566
- development: this.developmentMode
567
- }
568
- };
569
- }
570
- }
571
-
572
- // Static method to get API key from environment
573
- static getApiKey() {
574
- const envKey = process.env.OPTIMIZER_API_KEY;
575
- if (envKey) {
576
- return envKey;
577
- }
578
-
579
- throw new Error(
580
- 'API key required. Set the OPTIMIZER_API_KEY environment variable.\n' +
581
- 'Get your API key at: https://promptoptimizer.xyz/local-license'
582
- );
583
- }
584
-
585
- // Static method to create manager with environment key
586
- static fromEnvironment(options = {}) {
587
- const apiKey = CloudApiKeyManager.getApiKey();
588
- return new CloudApiKeyManager(apiKey, options);
589
- }
590
-
591
- // Format key for display (hide sensitive parts)
592
- formatKeyForDisplay() {
593
- if (!this.apiKey) return 'No key';
594
- return `${this.apiKey.substring(0, 8)}...${this.apiKey.slice(-4)}`;
595
- }
596
-
597
- async getDiagnosticInfo() {
598
- const diagnosticInfo = {
599
- apiKey: this.apiKey ? this.formatKeyForDisplay() : 'Not set',
600
- backendUrl: this.backendUrl,
601
- cacheFile: this.cacheFile,
602
- healthFile: this.healthFile,
603
- cacheExpiry: this.cacheExpiry,
604
- fallbackCacheExpiry: this.fallbackCacheExpiry,
605
- offlineMode: this.offlineMode,
606
- developmentMode: this.developmentMode,
607
- maxRetries: this.maxRetries,
608
- requestTimeout: this.requestTimeout,
609
- nodeEnv: process.env.NODE_ENV || 'not set',
610
- packageVersion: packageJson.version,
611
- networkHealth: { ...this.networkHealth },
612
- cache: {},
613
- keyFormat: this.validateApiKeyFormat(this.apiKey),
614
- backendConnectivity: { status: 'unknown', error: null, responseTime: null }
615
- };
616
-
617
- // Get cache status
618
- try {
619
- const cached = await this.getCachedValidation();
620
- if (cached) {
621
- diagnosticInfo.cache.exists = true;
622
- diagnosticInfo.cache.expired = this.isCacheExpired(cached);
623
- diagnosticInfo.cache.fallbackExpired = this.isFallbackCacheExpired(cached);
624
- diagnosticInfo.cache.age = Math.round((Date.now() - cached.timestamp) / 1000 / 60);
625
- diagnosticInfo.cache.backendUrl = cached.backendUrl;
626
- diagnosticInfo.cache.packageVersion = cached.packageVersion;
627
- } else {
628
- diagnosticInfo.cache.exists = false;
629
- }
630
- } catch (error) {
631
- diagnosticInfo.cache.error = error.message;
632
- diagnosticInfo.cache.exists = false;
633
- }
634
-
635
- // Check backend connectivity
636
- if (this.apiKey) { // Only check if API key is present
637
- try {
638
- const startTime = Date.now();
639
- await this.validateWithBackend(); // This will attempt to connect to the backend
640
- const responseTime = Date.now() - startTime;
641
- diagnosticInfo.backendConnectivity.status = 'success';
642
- diagnosticInfo.backendConnectivity.responseTime = responseTime;
643
- } catch (error) {
644
- diagnosticInfo.backendConnectivity.status = 'failed';
645
- diagnosticInfo.backendConnectivity.error = error.message;
646
- }
647
- } else {
648
- diagnosticInfo.backendConnectivity.status = 'skipped';
649
- diagnosticInfo.backendConnectivity.error = 'No API key provided for connectivity check.';
650
- }
651
-
652
- return diagnosticInfo;
653
- }
654
-
655
- async _makeBackendRequest(endpoint, data, method = 'POST') {
656
- return new Promise((resolve, reject) => {
657
- const url = `${this.backendUrl}${endpoint}`;
658
-
659
- const options = {
660
- method: method,
661
- headers: {
662
- 'x-api-key': this.apiKey,
663
- 'Content-Type': 'application/json',
664
- 'User-Agent': `mcp-prompt-optimizer/${packageJson.version}`,
665
- 'Accept': 'application/json',
666
- 'Connection': 'close'
667
- },
668
- timeout: this.requestTimeout
669
- };
670
-
671
- const client = this.backendUrl.startsWith('https://') ? https : http;
672
- const req = client.request(url, options, (res) => {
673
- let responseData = '';
674
-
675
- res.on('data', (chunk) => {
676
- responseData += chunk;
677
- });
678
-
679
- res.on('end', () => {
680
- try {
681
- if (res.statusCode >= 200 && res.statusCode < 300) {
682
- const parsed = JSON.parse(responseData);
683
- resolve(parsed);
684
- } else {
685
- let errorMessage;
686
- try {
687
- const error = JSON.parse(responseData);
688
- errorMessage = error.detail || error.message || `HTTP ${res.statusCode}`;
689
- } catch {
690
- errorMessage = `HTTP ${res.statusCode}: ${responseData}`;
691
- }
692
- reject(new Error(errorMessage));
693
- }
694
- } catch (parseError) {
695
- reject(new Error(`Invalid response format: ${parseError.message}`));
696
- }
697
- });
698
- });
699
-
700
- req.on('error', (error) => {
701
- if (error.code === 'ENOTFOUND') {
702
- reject(new Error(`DNS resolution failed: Cannot resolve ${this.backendUrl.replace(/^https?:\/\//, '')}`));
703
- } else if (error.code === 'ECONNREFUSED') {
704
- reject(new Error(`Connection refused: Backend server may be down`));
705
- } else if (error.code === 'ETIMEDOUT') {
706
- reject(new Error(`Connection timeout: Backend server is not responding`));
707
- } else if (error.code === 'ECONNRESET') {
708
- reject(new Error(`Connection reset: Network instability detected`));
709
- } else {
710
- reject(new Error(`Network error: ${error.message}`));
711
- }
712
- });
713
-
714
- req.on('timeout', () => {
715
- req.destroy();
716
- reject(new Error('Request timeout - backend may be unavailable'));
717
- });
718
-
719
- req.setTimeout(this.requestTimeout);
720
-
721
- if (method !== 'GET' && data) {
722
- req.write(JSON.stringify(data));
723
- }
724
- req.end();
725
- });
726
- }
727
- }
728
-
729
- module.exports = CloudApiKeyManager;
1
+ /**
2
+ * Cloud API Key Manager for MCP Prompt Optimizer
3
+ * Production-grade with enhanced network resilience and development mode
4
+ * ALIGNED with backend API requirements - FIXED API ENDPOINTS
5
+ */
6
+
7
+ const fs = require('fs').promises;
8
+ const path = require('path');
9
+ const https = require('https');
10
+ const http = require('http');
11
+ const os = require('os');
12
+
13
+ const packageJson = require('../package.json');
14
+
15
+ class CloudApiKeyManager {
16
+ constructor(apiKey, options = {}) {
17
+ this.apiKey = apiKey;
18
+ this.backendUrl = options.backendUrl || process.env.OPTIMIZER_BACKEND_URL || 'https://p01--project-optimizer--fvmrdk8m9k9j.code.run';
19
+ this.cacheFile = path.join(os.homedir(), '.mcp-cloud-api-cache.json');
20
+ this.healthFile = path.join(os.homedir(), '.mcp-cloud-health.json');
21
+ this.cacheExpiry = options.cacheExpiry || 1 * 60 * 60 * 1000; // 1 hour (reduced from 24)
22
+ this.fallbackCacheExpiry = options.fallbackCacheExpiry || 2 * 60 * 60 * 1000; // 2 hours (reduced from 7 days)
23
+ this.logPrefix = '[CloudApiKeyManager]';
24
+ // SECURITY: Offline mode disabled in production for security
25
+ this.offlineMode = false;
26
+ // SECURITY: Development mode disabled - use separate dev builds
27
+ this.developmentMode = false;
28
+ this.maxRetries = options.maxRetries || 5; // Increased for production
29
+ this.baseRetryDelay = options.baseRetryDelay || 1000;
30
+ this.maxRetryDelay = options.maxRetryDelay || 30000;
31
+ this.requestTimeout = options.requestTimeout || 15000;
32
+
33
+ // Network health tracking
34
+ this.networkHealth = {
35
+ consecutiveFailures: 0,
36
+ lastSuccessful: null,
37
+ avgResponseTime: null,
38
+ lastErrorType: null
39
+ };
40
+ }
41
+
42
+ log(message, level = 'info') {
43
+ const timestamp = new Date().toISOString();
44
+ const prefix = `${timestamp} ${this.logPrefix}`;
45
+
46
+ if (level === 'error') {
47
+ console.error(`${prefix} ❌ ${message}`);
48
+ } else if (level === 'warn') {
49
+ console.warn(`${prefix} ⚠️ ${message}`);
50
+ } else if (level === 'success') {
51
+ // stderr, not stdout: on an MCP stdio server stdout is the JSON-RPC channel
52
+ console.error(`${prefix} ${message}`);
53
+ } else {
54
+ console.error(`${prefix} ℹ️ ${message}`);
55
+ }
56
+ }
57
+
58
+ // Production-grade exponential backoff with jitter
59
+ calculateRetryDelay(attempt) {
60
+ const exponentialDelay = Math.min(
61
+ this.baseRetryDelay * Math.pow(2, attempt - 1),
62
+ this.maxRetryDelay
63
+ );
64
+
65
+ // Add jitter to prevent thundering herd
66
+ const jitter = Math.random() * 0.3 * exponentialDelay;
67
+ return Math.floor(exponentialDelay + jitter);
68
+ }
69
+
70
+ // Enhanced API key format validation
71
+ validateApiKeyFormat(apiKey) {
72
+ if (!apiKey || typeof apiKey !== 'string') {
73
+ return { valid: false, error: 'API key must be a string' };
74
+ }
75
+
76
+ // Support development keys
77
+ const validPrefixes = ['sk-opt-', 'sk-team-', 'sk-local-', 'sk-dev-'];
78
+ const hasValidPrefix = validPrefixes.some(prefix => apiKey.startsWith(prefix));
79
+
80
+ if (!hasValidPrefix) {
81
+ return {
82
+ valid: false,
83
+ error: 'Invalid API key format. Must start with "sk-opt-" (individual), "sk-team-" (team), "sk-local-" (development), or "sk-dev-" (testing)'
84
+ };
85
+ }
86
+
87
+ // Check minimum length for security
88
+ if (apiKey.length < 20) {
89
+ return {
90
+ valid: false,
91
+ error: 'API key too short'
92
+ };
93
+ }
94
+
95
+ // Determine type
96
+ let keyType = 'unknown';
97
+ if (apiKey.startsWith('sk-opt-')) {
98
+ keyType = 'individual';
99
+ } else if (apiKey.startsWith('sk-team-')) {
100
+ keyType = 'team';
101
+ } else if (apiKey.startsWith('sk-local-')) {
102
+ keyType = 'development';
103
+ } else if (apiKey.startsWith('sk-dev-')) {
104
+ keyType = 'testing';
105
+ }
106
+
107
+ return {
108
+ valid: true,
109
+ keyType: keyType
110
+ };
111
+ }
112
+
113
+ // Development mode mock responses
114
+ generateMockValidation(keyType) {
115
+ const mockResponses = {
116
+ individual: {
117
+ valid: true,
118
+ tier: 'explorer',
119
+ api_key_type: 'individual',
120
+ quota: {
121
+ limit: 5000,
122
+ used: Math.floor(Math.random() * 1000),
123
+ unlimited: false
124
+ },
125
+ features: {
126
+ ai_context_detection: true,
127
+ template_management: true,
128
+ optimization_insights: true
129
+ }
130
+ },
131
+ team: {
132
+ valid: true,
133
+ tier: 'creator',
134
+ api_key_type: 'team',
135
+ quota: {
136
+ limit: 18000,
137
+ used: Math.floor(Math.random() * 3000),
138
+ unlimited: false
139
+ },
140
+ features: {
141
+ ai_context_detection: true,
142
+ template_management: true,
143
+ team_collaboration: true,
144
+ optimization_insights: true
145
+ }
146
+ },
147
+ development: {
148
+ valid: true,
149
+ tier: 'development',
150
+ api_key_type: 'development',
151
+ quota: {
152
+ unlimited: true
153
+ },
154
+ features: {
155
+ ai_context_detection: true,
156
+ template_management: true,
157
+ optimization_insights: true,
158
+ development_mode: true
159
+ }
160
+ },
161
+ testing: {
162
+ valid: true,
163
+ tier: 'testing',
164
+ api_key_type: 'testing',
165
+ quota: {
166
+ limit: 1000,
167
+ used: Math.floor(Math.random() * 100),
168
+ unlimited: false
169
+ },
170
+ features: {
171
+ ai_context_detection: true,
172
+ template_management: true,
173
+ optimization_insights: true,
174
+ testing_mode: true
175
+ }
176
+ }
177
+ };
178
+
179
+ const response = mockResponses[keyType] || mockResponses.development;
180
+ response.mock_mode = true;
181
+ response.backend_url = 'mock://development-mode';
182
+
183
+ return response;
184
+ }
185
+
186
+ // Enhanced API key validation with production resilience
187
+ async validateApiKey() {
188
+ this.log('Starting comprehensive API key validation...');
189
+
190
+ if (!this.apiKey) {
191
+ throw new Error('API key is required. Set OPTIMIZER_API_KEY environment variable or provide key directly.');
192
+ }
193
+
194
+ // Step 1: Format validation
195
+ const formatCheck = this.validateApiKeyFormat(this.apiKey);
196
+ if (!formatCheck.valid) {
197
+ throw new Error(formatCheck.error);
198
+ }
199
+
200
+ this.log(`API key format valid: ${formatCheck.keyType}`);
201
+
202
+ // SECURITY: Mock validation removed - all keys must validate against backend
203
+ // Development/testing keys must be real keys in the database
204
+
205
+ try {
206
+ // Step 3: Backend validation with enhanced retry logic
207
+ const validation = await this.validateWithBackendRetry();
208
+
209
+ // Step 4: Validate response structure
210
+ if (validation && validation.valid) {
211
+ await this.cacheValidation(validation);
212
+ await this.updateNetworkHealth(true);
213
+ this.log(`API key validated successfully: ${validation.tier}`, 'success');
214
+ return validation;
215
+ } else {
216
+ throw new Error(validation?.detail || validation?.error || 'API key validation failed');
217
+ }
218
+
219
+ } catch (error) {
220
+ this.log(`Backend validation failed: ${error.message}`, 'warn');
221
+ await this.updateNetworkHealth(false, error.message);
222
+
223
+ // Enhanced fallback strategy
224
+ const cachedValidation = await this.getCachedValidation();
225
+
226
+ if (cachedValidation && !this.isCacheExpired(cachedValidation)) {
227
+ this.log('Using cached API key validation', 'warn');
228
+ return cachedValidation.data;
229
+ }
230
+
231
+ // SECURITY: Limited fallback for brief network issues only (2 hours max)
232
+ if (cachedValidation && !this.isFallbackCacheExpired(cachedValidation)) {
233
+ this.log('Using short-term fallback cache due to network issues', 'warn');
234
+ const fallbackData = cachedValidation.data;
235
+ fallbackData.fallback_mode = true;
236
+ fallbackData.network_issue = error.message;
237
+ fallbackData.expires_soon = true;
238
+ return fallbackData;
239
+ }
240
+
241
+ // SECURITY: Offline mode removed - backend validation required
242
+ // No cache fallback beyond 2 hours
243
+
244
+ throw new Error(`API key validation failed: ${error.message}. Please check your internet connection.`);
245
+ }
246
+ }
247
+
248
+ // Production-grade retry logic with exponential backoff
249
+ async validateWithBackendRetry() {
250
+ let lastError;
251
+
252
+ for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
253
+ try {
254
+ this.log(`Validation attempt ${attempt}/${this.maxRetries}...`);
255
+ const startTime = Date.now();
256
+
257
+ const result = await this.validateWithBackend();
258
+
259
+ // Track response time for health monitoring
260
+ const responseTime = Date.now() - startTime;
261
+ if (this.networkHealth.avgResponseTime === null) {
262
+ this.networkHealth.avgResponseTime = responseTime;
263
+ } else {
264
+ this.networkHealth.avgResponseTime = (this.networkHealth.avgResponseTime + responseTime) / 2;
265
+ }
266
+
267
+ return result;
268
+ } catch (error) {
269
+ lastError = error;
270
+ this.log(`Attempt ${attempt} failed: ${error.message}`, 'warn');
271
+
272
+ if (attempt < this.maxRetries) {
273
+ const delay = this.calculateRetryDelay(attempt);
274
+ this.log(`Retrying in ${delay}ms...`);
275
+ await new Promise(resolve => setTimeout(resolve, delay));
276
+ } else {
277
+ this.log('All retry attempts exhausted', 'error');
278
+ }
279
+ }
280
+ }
281
+
282
+ throw lastError;
283
+ }
284
+
285
+ // FIXED: Correct API endpoint URL
286
+ async validateWithBackend() {
287
+ const endpoint = '/api/v1/api-keys/validate';
288
+ const method = 'POST';
289
+
290
+ try {
291
+ const validation = await this._makeBackendRequest(endpoint, null, method);
292
+ this.log(`Validation successful: ${JSON.stringify(validation, null, 2)}`);
293
+ return validation;
294
+ } catch (error) {
295
+ this.log(`Backend validation request failed: ${error.message}`, 'error');
296
+ throw error;
297
+ }
298
+ }
299
+
300
+ // FIXED: Correct API endpoint URL
301
+ async getQuotaStatus() {
302
+ try {
303
+ // FIXED: Use MCP quota-status endpoint (accepts API key auth)
304
+ const url = `${this.backendUrl}/api/v1/mcp/quota-status`;
305
+
306
+ const options = {
307
+ method: 'GET',
308
+ headers: {
309
+ 'x-api-key': this.apiKey,
310
+ 'User-Agent': `mcp-prompt-optimizer/${packageJson.version}`,
311
+ 'Connection': 'close'
312
+ },
313
+ timeout: this.requestTimeout
314
+ };
315
+
316
+ return new Promise((resolve, reject) => {
317
+ const client = this.backendUrl.startsWith('https://') ? https : http;
318
+ const req = client.request(url, options, (res) => {
319
+ let data = '';
320
+
321
+ res.on('data', (chunk) => {
322
+ data += chunk;
323
+ });
324
+
325
+ res.on('end', () => {
326
+ try {
327
+ if (res.statusCode === 200) {
328
+ const response = JSON.parse(data);
329
+ // FIXED: Map nested MCP response to flat format
330
+ resolve({
331
+ tier: response.tier,
332
+ unlimited: response.quota?.unlimited || false,
333
+ used: response.quota?.used || 0,
334
+ remaining: response.quota?.remaining || 0,
335
+ limit: response.quota?.limit,
336
+ usage_percentage: response.quota?.percentage || 0,
337
+ status: response.quota?.status || 'unknown',
338
+ features_available: response.features_available || {}
339
+ });
340
+ } else {
341
+ let errorMessage;
342
+ try {
343
+ const error = JSON.parse(data);
344
+ errorMessage = error.detail || `HTTP ${res.statusCode}`;
345
+ } catch {
346
+ errorMessage = `HTTP ${res.statusCode}: ${data}`;
347
+ }
348
+ reject(new Error(errorMessage));
349
+ }
350
+ } catch (parseError) {
351
+ reject(new Error(`Invalid response: ${parseError.message}`));
352
+ }
353
+ });
354
+ });
355
+
356
+ req.on('error', (error) => {
357
+ reject(new Error(`Network error: ${error.message}`));
358
+ });
359
+
360
+ req.on('timeout', () => {
361
+ req.destroy();
362
+ reject(new Error('Request timeout'));
363
+ });
364
+
365
+ req.setTimeout(this.requestTimeout);
366
+ req.end();
367
+ });
368
+
369
+ } catch (error) {
370
+ this.log(`Quota status check failed: ${error.message}`, 'warn');
371
+ throw error;
372
+ }
373
+ }
374
+
375
+ // Network health tracking
376
+ async updateNetworkHealth(success, errorMessage = null) {
377
+ try {
378
+ if (success) {
379
+ this.networkHealth.consecutiveFailures = 0;
380
+ this.networkHealth.lastSuccessful = Date.now();
381
+ this.networkHealth.lastErrorType = null;
382
+ } else {
383
+ this.networkHealth.consecutiveFailures++;
384
+ this.networkHealth.lastErrorType = errorMessage;
385
+ }
386
+
387
+ // Save health metrics
388
+ await fs.writeFile(this.healthFile, JSON.stringify(this.networkHealth, null, 2));
389
+ } catch (error) {
390
+ this.log(`Failed to update network health: ${error.message}`, 'warn');
391
+ }
392
+ }
393
+
394
+
395
+
396
+ // Enhanced caching with metadata
397
+ async cacheValidation(validation) {
398
+ try {
399
+ const cacheData = {
400
+ timestamp: Date.now(),
401
+ apiKeyPrefix: this.apiKey.substring(0, 20) + '...', // Safe prefix only
402
+ data: validation,
403
+ backendUrl: this.backendUrl,
404
+ packageVersion: packageJson.version,
405
+ networkHealth: { ...this.networkHealth }
406
+ };
407
+
408
+ await fs.writeFile(this.cacheFile, JSON.stringify(cacheData, null, 2));
409
+ this.log('API key validation cached successfully');
410
+ } catch (error) {
411
+ this.log(`Failed to cache validation: ${error.message}`, 'warn');
412
+ }
413
+ }
414
+
415
+ async getCachedValidation() {
416
+ try {
417
+ const cacheContent = await fs.readFile(this.cacheFile, 'utf8');
418
+ const cached = JSON.parse(cacheContent);
419
+
420
+ // Validate cache structure
421
+ if (!cached.timestamp || !cached.data) {
422
+ this.log('Invalid cache structure, ignoring', 'warn');
423
+ return null;
424
+ }
425
+
426
+ return cached;
427
+ } catch (error) {
428
+ if (error.code !== 'ENOENT') {
429
+ this.log(`Cache read error: ${error.message}`, 'warn');
430
+ }
431
+ return null;
432
+ }
433
+ }
434
+
435
+ isCacheExpired(cachedData) {
436
+ if (!cachedData || !cachedData.timestamp) {
437
+ return true;
438
+ }
439
+
440
+ const age = Date.now() - cachedData.timestamp;
441
+ const expired = age > this.cacheExpiry;
442
+
443
+ if (expired) {
444
+ this.log(`Cache expired: ${Math.round(age / 1000 / 60)} minutes old`);
445
+ }
446
+
447
+ return expired;
448
+ }
449
+
450
+ // Extended fallback cache for network issues
451
+ isFallbackCacheExpired(cachedData) {
452
+ if (!cachedData || !cachedData.timestamp) {
453
+ return true;
454
+ }
455
+
456
+ const age = Date.now() - cachedData.timestamp;
457
+ const expired = age > this.fallbackCacheExpiry;
458
+
459
+ if (expired) {
460
+ this.log(`Fallback cache expired: ${Math.round(age / 1000 / 60 / 60)} hours old`);
461
+ }
462
+
463
+ return expired;
464
+ }
465
+
466
+ async clearCache() {
467
+ try {
468
+ await fs.unlink(this.cacheFile);
469
+ this.log('API key cache cleared successfully');
470
+ } catch (error) {
471
+ if (error.code !== 'ENOENT') {
472
+ this.log(`Cache clear error: ${error.message}`, 'warn');
473
+ }
474
+ }
475
+
476
+ try {
477
+ await fs.unlink(this.healthFile);
478
+ this.log('Network health cache cleared successfully');
479
+ } catch (error) {
480
+ if (error.code !== 'ENOENT') {
481
+ this.log(`Health cache clear error: ${error.message}`, 'warn');
482
+ }
483
+ }
484
+ }
485
+
486
+ // Enhanced validation and preparation
487
+ async validateAndPrepare() {
488
+ this.log('Starting comprehensive API key validation and preparation...');
489
+
490
+ try {
491
+ // Step 1: Validate API key
492
+ let validation = await this.validateApiKey();
493
+
494
+ // Step 2: Get comprehensive quota status
495
+ const info = await this.getApiKeyInfo();
496
+ let quotaStatus = info.quota;
497
+ validation = info; // Use info as the main validation object for consistency
498
+
499
+ // Step 3: Log success
500
+ const mode = validation.mock_mode ? '(mock)' :
501
+ validation.fallback_mode ? '(fallback)' :
502
+ validation.offline_mode ? '(offline)' : '';
503
+
504
+ if (quotaStatus.unlimited) {
505
+ this.log(`API key valid: ${validation.tier} ${mode} (unlimited usage)`, 'success');
506
+ } else {
507
+ this.log(`API key valid: ${validation.tier} ${mode} (${quotaStatus.remaining}/${quotaStatus.limit} remaining this month)`, 'success');
508
+ }
509
+
510
+ return {
511
+ validation,
512
+ quotaStatus,
513
+ tier: validation.tier,
514
+ features: validation.features || {},
515
+ mode: {
516
+ development: this.developmentMode,
517
+ mock: validation.mock_mode || false,
518
+ fallback: validation.fallback_mode || false,
519
+ offline: validation.offline_mode || false
520
+ }
521
+ };
522
+
523
+ } catch (error) {
524
+ this.log(`API key validation failed: ${error.message}`, 'error');
525
+ throw error;
526
+ }
527
+ }
528
+
529
+ // Enhanced API key info with backend validation
530
+ async getApiKeyInfo() {
531
+ const formatCheck = this.validateApiKeyFormat(this.apiKey);
532
+
533
+ // SECURITY: Mock data removed - all keys must validate with backend
534
+
535
+ try {
536
+ // FIXED: Use MCP quota-status endpoint (accepts API key auth)
537
+ const quotaStatusResponse = await this._makeBackendRequest('/api/v1/mcp/quota-status', null, 'GET');
538
+
539
+ // The backend /mcp/quota-status endpoint returns a comprehensive object
540
+ // that includes tier, quota details, and features.
541
+ return {
542
+ tier: quotaStatusResponse.tier,
543
+ features: quotaStatusResponse.features_available || {},
544
+ quota: quotaStatusResponse.quota,
545
+ isValid: true,
546
+ keyType: quotaStatusResponse.account_type || this.validateApiKeyFormat(this.apiKey).keyType,
547
+ mode: {
548
+ mock: false, // This endpoint doesn't return mock status
549
+ fallback: false,
550
+ offline: false,
551
+ development: this.developmentMode
552
+ }
553
+ };
554
+ } catch (error) {
555
+ this.log(`Error getting API key info: ${error.message}`, 'error');
556
+ return {
557
+ tier: null,
558
+ features: {},
559
+ quota: { allowed: false },
560
+ isValid: false,
561
+ error: error.message,
562
+ keyType: 'unknown',
563
+ mode: {
564
+ mock: false,
565
+ fallback: false,
566
+ offline: false,
567
+ development: this.developmentMode
568
+ }
569
+ };
570
+ }
571
+ }
572
+
573
+ // Static method to get API key from environment
574
+ static getApiKey() {
575
+ const envKey = process.env.OPTIMIZER_API_KEY;
576
+ if (envKey) {
577
+ return envKey;
578
+ }
579
+
580
+ throw new Error(
581
+ 'API key required. Set the OPTIMIZER_API_KEY environment variable.\n' +
582
+ 'Get your API key at: https://promptoptimizer.xyz/local-license'
583
+ );
584
+ }
585
+
586
+ // Static method to create manager with environment key
587
+ static fromEnvironment(options = {}) {
588
+ const apiKey = CloudApiKeyManager.getApiKey();
589
+ return new CloudApiKeyManager(apiKey, options);
590
+ }
591
+
592
+ // Format key for display (hide sensitive parts)
593
+ formatKeyForDisplay() {
594
+ if (!this.apiKey) return 'No key';
595
+ return `${this.apiKey.substring(0, 8)}...${this.apiKey.slice(-4)}`;
596
+ }
597
+
598
+ async getDiagnosticInfo() {
599
+ const diagnosticInfo = {
600
+ apiKey: this.apiKey ? this.formatKeyForDisplay() : 'Not set',
601
+ backendUrl: this.backendUrl,
602
+ cacheFile: this.cacheFile,
603
+ healthFile: this.healthFile,
604
+ cacheExpiry: this.cacheExpiry,
605
+ fallbackCacheExpiry: this.fallbackCacheExpiry,
606
+ offlineMode: this.offlineMode,
607
+ developmentMode: this.developmentMode,
608
+ maxRetries: this.maxRetries,
609
+ requestTimeout: this.requestTimeout,
610
+ nodeEnv: process.env.NODE_ENV || 'not set',
611
+ packageVersion: packageJson.version,
612
+ networkHealth: { ...this.networkHealth },
613
+ cache: {},
614
+ keyFormat: this.validateApiKeyFormat(this.apiKey),
615
+ backendConnectivity: { status: 'unknown', error: null, responseTime: null }
616
+ };
617
+
618
+ // Get cache status
619
+ try {
620
+ const cached = await this.getCachedValidation();
621
+ if (cached) {
622
+ diagnosticInfo.cache.exists = true;
623
+ diagnosticInfo.cache.expired = this.isCacheExpired(cached);
624
+ diagnosticInfo.cache.fallbackExpired = this.isFallbackCacheExpired(cached);
625
+ diagnosticInfo.cache.age = Math.round((Date.now() - cached.timestamp) / 1000 / 60);
626
+ diagnosticInfo.cache.backendUrl = cached.backendUrl;
627
+ diagnosticInfo.cache.packageVersion = cached.packageVersion;
628
+ } else {
629
+ diagnosticInfo.cache.exists = false;
630
+ }
631
+ } catch (error) {
632
+ diagnosticInfo.cache.error = error.message;
633
+ diagnosticInfo.cache.exists = false;
634
+ }
635
+
636
+ // Check backend connectivity
637
+ if (this.apiKey) { // Only check if API key is present
638
+ try {
639
+ const startTime = Date.now();
640
+ await this.validateWithBackend(); // This will attempt to connect to the backend
641
+ const responseTime = Date.now() - startTime;
642
+ diagnosticInfo.backendConnectivity.status = 'success';
643
+ diagnosticInfo.backendConnectivity.responseTime = responseTime;
644
+ } catch (error) {
645
+ diagnosticInfo.backendConnectivity.status = 'failed';
646
+ diagnosticInfo.backendConnectivity.error = error.message;
647
+ }
648
+ } else {
649
+ diagnosticInfo.backendConnectivity.status = 'skipped';
650
+ diagnosticInfo.backendConnectivity.error = 'No API key provided for connectivity check.';
651
+ }
652
+
653
+ return diagnosticInfo;
654
+ }
655
+
656
+ async _makeBackendRequest(endpoint, data, method = 'POST') {
657
+ return new Promise((resolve, reject) => {
658
+ const url = `${this.backendUrl}${endpoint}`;
659
+
660
+ const options = {
661
+ method: method,
662
+ headers: {
663
+ 'x-api-key': this.apiKey,
664
+ 'Content-Type': 'application/json',
665
+ 'User-Agent': `mcp-prompt-optimizer/${packageJson.version}`,
666
+ 'Accept': 'application/json',
667
+ 'Connection': 'close'
668
+ },
669
+ timeout: this.requestTimeout
670
+ };
671
+
672
+ const client = this.backendUrl.startsWith('https://') ? https : http;
673
+ const req = client.request(url, options, (res) => {
674
+ let responseData = '';
675
+
676
+ res.on('data', (chunk) => {
677
+ responseData += chunk;
678
+ });
679
+
680
+ res.on('end', () => {
681
+ try {
682
+ if (res.statusCode >= 200 && res.statusCode < 300) {
683
+ const parsed = JSON.parse(responseData);
684
+ resolve(parsed);
685
+ } else {
686
+ let errorMessage;
687
+ try {
688
+ const error = JSON.parse(responseData);
689
+ errorMessage = error.detail || error.message || `HTTP ${res.statusCode}`;
690
+ } catch {
691
+ errorMessage = `HTTP ${res.statusCode}: ${responseData}`;
692
+ }
693
+ reject(new Error(errorMessage));
694
+ }
695
+ } catch (parseError) {
696
+ reject(new Error(`Invalid response format: ${parseError.message}`));
697
+ }
698
+ });
699
+ });
700
+
701
+ req.on('error', (error) => {
702
+ if (error.code === 'ENOTFOUND') {
703
+ reject(new Error(`DNS resolution failed: Cannot resolve ${this.backendUrl.replace(/^https?:\/\//, '')}`));
704
+ } else if (error.code === 'ECONNREFUSED') {
705
+ reject(new Error(`Connection refused: Backend server may be down`));
706
+ } else if (error.code === 'ETIMEDOUT') {
707
+ reject(new Error(`Connection timeout: Backend server is not responding`));
708
+ } else if (error.code === 'ECONNRESET') {
709
+ reject(new Error(`Connection reset: Network instability detected`));
710
+ } else {
711
+ reject(new Error(`Network error: ${error.message}`));
712
+ }
713
+ });
714
+
715
+ req.on('timeout', () => {
716
+ req.destroy();
717
+ reject(new Error('Request timeout - backend may be unavailable'));
718
+ });
719
+
720
+ req.setTimeout(this.requestTimeout);
721
+
722
+ if (method !== 'GET' && data) {
723
+ req.write(JSON.stringify(data));
724
+ }
725
+ req.end();
726
+ });
727
+ }
728
+ }
729
+
730
+ module.exports = CloudApiKeyManager;