mcp-prompt-optimizer 1.3.2 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,21 +1,40 @@
1
1
  /**
2
2
  * Cloud API Key Manager for MCP Prompt Optimizer
3
- * Mirrors the patterns from the local license-manager.js for consistency
3
+ * Production-grade with enhanced network resilience and development mode
4
+ * ALIGNED with backend API requirements
4
5
  */
5
6
 
6
7
  const fs = require('fs').promises;
7
8
  const path = require('path');
8
9
  const https = require('https');
10
+ const http = require('http');
9
11
  const os = require('os');
10
12
 
13
+ const packageJson = require('../package.json');
14
+
11
15
  class CloudApiKeyManager {
12
16
  constructor(apiKey, options = {}) {
13
17
  this.apiKey = apiKey;
14
- this.backendUrl = options.backendUrl || 'https://p01--project-optimizer--fvrdk8m9k9j.code.run';
18
+ this.backendUrl = options.backendUrl || process.env.OPTIMIZER_BACKEND_URL || 'https://p01--project-optimizer--fvrdk8m9k9j.code.run';
15
19
  this.cacheFile = path.join(os.homedir(), '.mcp-cloud-api-cache.json');
20
+ this.healthFile = path.join(os.homedir(), '.mcp-cloud-health.json');
16
21
  this.cacheExpiry = options.cacheExpiry || 24 * 60 * 60 * 1000; // 24 hours
22
+ this.fallbackCacheExpiry = options.fallbackCacheExpiry || 7 * 24 * 60 * 60 * 1000; // 7 days
17
23
  this.logPrefix = '[CloudApiKeyManager]';
18
24
  this.offlineMode = options.offlineMode || false;
25
+ this.developmentMode = options.developmentMode || process.env.NODE_ENV === 'development' || process.env.OPTIMIZER_DEV_MODE === 'true';
26
+ this.maxRetries = options.maxRetries || 5; // Increased for production
27
+ this.baseRetryDelay = options.baseRetryDelay || 1000;
28
+ this.maxRetryDelay = options.maxRetryDelay || 30000;
29
+ this.requestTimeout = options.requestTimeout || 15000;
30
+
31
+ // Network health tracking
32
+ this.networkHealth = {
33
+ consecutiveFailures: 0,
34
+ lastSuccessful: null,
35
+ avgResponseTime: null,
36
+ lastErrorType: null
37
+ };
19
38
  }
20
39
 
21
40
  log(message, level = 'info') {
@@ -33,50 +52,243 @@ class CloudApiKeyManager {
33
52
  }
34
53
  }
35
54
 
55
+ // Production-grade exponential backoff with jitter
56
+ calculateRetryDelay(attempt) {
57
+ const exponentialDelay = Math.min(
58
+ this.baseRetryDelay * Math.pow(2, attempt - 1),
59
+ this.maxRetryDelay
60
+ );
61
+
62
+ // Add jitter to prevent thundering herd
63
+ const jitter = Math.random() * 0.3 * exponentialDelay;
64
+ return Math.floor(exponentialDelay + jitter);
65
+ }
66
+
67
+ // Enhanced API key format validation
68
+ validateApiKeyFormat(apiKey) {
69
+ if (!apiKey || typeof apiKey !== 'string') {
70
+ return { valid: false, error: 'API key must be a string' };
71
+ }
72
+
73
+ // Support development keys
74
+ const validPrefixes = ['sk-opt-', 'sk-team-', 'sk-local-', 'sk-dev-'];
75
+ const hasValidPrefix = validPrefixes.some(prefix => apiKey.startsWith(prefix));
76
+
77
+ if (!hasValidPrefix) {
78
+ return {
79
+ valid: false,
80
+ error: 'Invalid API key format. Must start with "sk-opt-" (individual), "sk-team-" (team), "sk-local-" (development), or "sk-dev-" (testing)'
81
+ };
82
+ }
83
+
84
+ // Check minimum length for security
85
+ if (apiKey.length < 20) {
86
+ return {
87
+ valid: false,
88
+ error: 'API key too short'
89
+ };
90
+ }
91
+
92
+ // Determine type
93
+ let keyType = 'unknown';
94
+ if (apiKey.startsWith('sk-opt-')) {
95
+ keyType = 'individual';
96
+ } else if (apiKey.startsWith('sk-team-')) {
97
+ keyType = 'team';
98
+ } else if (apiKey.startsWith('sk-local-')) {
99
+ keyType = 'development';
100
+ } else if (apiKey.startsWith('sk-dev-')) {
101
+ keyType = 'testing';
102
+ }
103
+
104
+ return {
105
+ valid: true,
106
+ keyType: keyType
107
+ };
108
+ }
109
+
110
+ // Development mode mock responses
111
+ generateMockValidation(keyType) {
112
+ const mockResponses = {
113
+ individual: {
114
+ valid: true,
115
+ tier: 'explorer',
116
+ api_key_type: 'individual',
117
+ quota: {
118
+ limit: 5000,
119
+ used: Math.floor(Math.random() * 1000),
120
+ unlimited: false
121
+ },
122
+ features: {
123
+ ai_context_detection: true,
124
+ template_management: true,
125
+ optimization_insights: true
126
+ }
127
+ },
128
+ team: {
129
+ valid: true,
130
+ tier: 'creator',
131
+ api_key_type: 'team',
132
+ quota: {
133
+ limit: 18000,
134
+ used: Math.floor(Math.random() * 3000),
135
+ unlimited: false
136
+ },
137
+ features: {
138
+ ai_context_detection: true,
139
+ template_management: true,
140
+ team_collaboration: true,
141
+ optimization_insights: true
142
+ }
143
+ },
144
+ development: {
145
+ valid: true,
146
+ tier: 'development',
147
+ api_key_type: 'development',
148
+ quota: {
149
+ unlimited: true
150
+ },
151
+ features: {
152
+ ai_context_detection: true,
153
+ template_management: true,
154
+ optimization_insights: true,
155
+ development_mode: true
156
+ }
157
+ },
158
+ testing: {
159
+ valid: true,
160
+ tier: 'testing',
161
+ api_key_type: 'testing',
162
+ quota: {
163
+ limit: 1000,
164
+ used: Math.floor(Math.random() * 100),
165
+ unlimited: false
166
+ },
167
+ features: {
168
+ ai_context_detection: true,
169
+ template_management: true,
170
+ optimization_insights: true,
171
+ testing_mode: true
172
+ }
173
+ }
174
+ };
175
+
176
+ const response = mockResponses[keyType] || mockResponses.development;
177
+ response.mock_mode = true;
178
+ response.backend_url = 'mock://development-mode';
179
+
180
+ return response;
181
+ }
182
+
183
+ // Enhanced API key validation with production resilience
36
184
  async validateApiKey() {
37
- this.log('Validating API key...');
185
+ this.log('Starting comprehensive API key validation...');
38
186
 
39
187
  if (!this.apiKey) {
40
188
  throw new Error('API key is required. Set OPTIMIZER_API_KEY environment variable or provide key directly.');
41
189
  }
42
190
 
43
- if (!this.apiKey.startsWith('sk-opt-') && !this.apiKey.startsWith('sk-team-')) {
44
- throw new Error('Invalid API key format. Must be a cloud API key (sk-opt-* or sk-team-*)');
191
+ // Step 1: Format validation
192
+ const formatCheck = this.validateApiKeyFormat(this.apiKey);
193
+ if (!formatCheck.valid) {
194
+ throw new Error(formatCheck.error);
195
+ }
196
+
197
+ this.log(`API key format valid: ${formatCheck.keyType}`);
198
+
199
+ // Step 2: Development mode handling
200
+ if (this.developmentMode || formatCheck.keyType === 'development' || formatCheck.keyType === 'testing') {
201
+ this.log('Development/testing mode detected, using mock validation', 'warn');
202
+ const mockValidation = this.generateMockValidation(formatCheck.keyType);
203
+ await this.cacheValidation(mockValidation);
204
+ return mockValidation;
45
205
  }
46
206
 
47
207
  try {
48
- // Try to validate with backend first
49
- const validation = await this.validateWithBackend();
208
+ // Step 3: Backend validation with enhanced retry logic
209
+ const validation = await this.validateWithBackendRetry();
50
210
 
51
- // Cache valid results
52
- if (validation.valid) {
211
+ // Step 4: Validate response structure
212
+ if (validation && validation.valid) {
53
213
  await this.cacheValidation(validation);
214
+ await this.updateNetworkHealth(true);
54
215
  this.log(`API key validated successfully: ${validation.tier}`, 'success');
55
216
  return validation;
56
217
  } else {
57
- throw new Error(validation.error || 'API key validation failed');
218
+ throw new Error(validation?.detail || validation?.error || 'API key validation failed');
58
219
  }
59
220
 
60
221
  } catch (error) {
61
222
  this.log(`Backend validation failed: ${error.message}`, 'warn');
223
+ await this.updateNetworkHealth(false, error.message);
62
224
 
63
- // Try cached validation as fallback
225
+ // Enhanced fallback strategy
64
226
  const cachedValidation = await this.getCachedValidation();
227
+
65
228
  if (cachedValidation && !this.isCacheExpired(cachedValidation)) {
66
229
  this.log('Using cached API key validation', 'warn');
67
230
  return cachedValidation.data;
68
231
  }
232
+
233
+ // Extended fallback for network issues
234
+ if (cachedValidation && !this.isFallbackCacheExpired(cachedValidation)) {
235
+ this.log('Using extended fallback cache due to network issues', 'warn');
236
+ const fallbackData = cachedValidation.data;
237
+ fallbackData.fallback_mode = true;
238
+ fallbackData.network_issue = error.message;
239
+ return fallbackData;
240
+ }
69
241
 
70
242
  // If we're in explicit offline mode and have any cache, use it
71
243
  if (this.offlineMode && cachedValidation) {
72
244
  this.log('Offline mode: using cached validation despite expiry', 'warn');
73
- return cachedValidation.data;
245
+ const offlineData = cachedValidation.data;
246
+ offlineData.offline_mode = true;
247
+ return offlineData;
74
248
  }
75
249
 
76
250
  throw new Error(`API key validation failed: ${error.message}`);
77
251
  }
78
252
  }
79
253
 
254
+ // Production-grade retry logic with exponential backoff
255
+ async validateWithBackendRetry() {
256
+ let lastError;
257
+
258
+ for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
259
+ try {
260
+ this.log(`Validation attempt ${attempt}/${this.maxRetries}...`);
261
+ const startTime = Date.now();
262
+
263
+ const result = await this.validateWithBackend();
264
+
265
+ // Track response time for health monitoring
266
+ const responseTime = Date.now() - startTime;
267
+ if (this.networkHealth.avgResponseTime === null) {
268
+ this.networkHealth.avgResponseTime = responseTime;
269
+ } else {
270
+ this.networkHealth.avgResponseTime = (this.networkHealth.avgResponseTime + responseTime) / 2;
271
+ }
272
+
273
+ return result;
274
+ } catch (error) {
275
+ lastError = error;
276
+ this.log(`Attempt ${attempt} failed: ${error.message}`, 'warn');
277
+
278
+ if (attempt < this.maxRetries) {
279
+ const delay = this.calculateRetryDelay(attempt);
280
+ this.log(`Retrying in ${delay}ms...`);
281
+ await new Promise(resolve => setTimeout(resolve, delay));
282
+ } else {
283
+ this.log('All retry attempts exhausted', 'error');
284
+ }
285
+ }
286
+ }
287
+
288
+ throw lastError;
289
+ }
290
+
291
+ // Enhanced backend validation with better error handling
80
292
  async validateWithBackend() {
81
293
  return new Promise((resolve, reject) => {
82
294
  const url = `${this.backendUrl}/api/v1/mcp/validate-key`;
@@ -84,17 +296,20 @@ class CloudApiKeyManager {
84
296
  const options = {
85
297
  method: 'POST',
86
298
  headers: {
87
- // Use lowercase header to match backend expectations
88
299
  'x-api-key': this.apiKey,
89
300
  'Content-Type': 'application/json',
90
- 'User-Agent': 'mcp-prompt-optimizer/1.2.0'
301
+ 'User-Agent': `mcp-prompt-optimizer/${packageJson.version}`,
302
+ 'Accept': 'application/json',
303
+ 'Connection': 'close' // Ensure connection cleanup
91
304
  },
92
- timeout: 10000 // 10 second timeout
305
+ timeout: this.requestTimeout
93
306
  };
94
307
 
95
- this.log(`Making request with header: x-api-key = ${this.apiKey.substring(0, 16)}...`);
308
+ this.log(`Making request to: ${url}`);
309
+ this.log(`Using API key: ${this.apiKey.substring(0, 16)}...`);
96
310
 
97
- const req = https.request(url, options, (res) => {
311
+ const client = this.backendUrl.startsWith('https://') ? https : http;
312
+ const req = client.request(url, options, (res) => {
98
313
  let data = '';
99
314
 
100
315
  res.on('data', (chunk) => {
@@ -103,42 +318,87 @@ class CloudApiKeyManager {
103
318
 
104
319
  res.on('end', () => {
105
320
  this.log(`Response status: ${res.statusCode}`);
106
- this.log(`Response body: ${data.substring(0, 200)}...`);
107
321
 
108
322
  try {
109
323
  if (res.statusCode === 200) {
110
324
  const validation = JSON.parse(data);
325
+ this.log(`Validation successful: ${JSON.stringify(validation, null, 2)}`);
111
326
  resolve(validation);
327
+ } else if (res.statusCode === 401) {
328
+ reject(new Error('Invalid API key or unauthorized access'));
329
+ } else if (res.statusCode === 403) {
330
+ reject(new Error('API key expired or quota exceeded'));
331
+ } else if (res.statusCode === 429) {
332
+ reject(new Error('Rate limit exceeded. Please try again later.'));
333
+ } else if (res.statusCode === 500) {
334
+ reject(new Error('Backend server error. Please try again later.'));
335
+ } else if (res.statusCode === 503) {
336
+ reject(new Error('Backend service temporarily unavailable. Please try again later.'));
112
337
  } else {
113
338
  let errorMessage;
114
339
  try {
115
340
  const error = JSON.parse(data);
116
- errorMessage = error.detail || `HTTP ${res.statusCode}`;
341
+ errorMessage = error.detail || error.message || `HTTP ${res.statusCode}`;
117
342
  } catch {
118
343
  errorMessage = `HTTP ${res.statusCode}: ${data}`;
119
344
  }
120
345
  reject(new Error(errorMessage));
121
346
  }
122
347
  } catch (parseError) {
123
- reject(new Error(`Invalid response: ${parseError.message}`));
348
+ this.log(`Parse error: ${parseError.message}`, 'error');
349
+ this.log(`Raw response: ${data}`, 'error');
350
+ reject(new Error(`Invalid response format: ${parseError.message}`));
124
351
  }
125
352
  });
126
353
  });
127
354
 
128
355
  req.on('error', (error) => {
129
- reject(new Error(`Network error: ${error.message}`));
356
+ this.log(`Network error: ${error.message}`, 'error');
357
+
358
+ // Enhanced error classification
359
+ if (error.code === 'ENOTFOUND') {
360
+ reject(new Error(`DNS resolution failed: Cannot resolve ${this.backendUrl.replace(/^https?:\/\//, '')}`));
361
+ } else if (error.code === 'ECONNREFUSED') {
362
+ reject(new Error(`Connection refused: Backend server may be down`));
363
+ } else if (error.code === 'ETIMEDOUT') {
364
+ reject(new Error(`Connection timeout: Backend server is not responding`));
365
+ } else if (error.code === 'ECONNRESET') {
366
+ reject(new Error(`Connection reset: Network instability detected`));
367
+ } else {
368
+ reject(new Error(`Network error: ${error.message}`));
369
+ }
130
370
  });
131
371
 
132
372
  req.on('timeout', () => {
133
373
  req.destroy();
134
- reject(new Error('Request timeout'));
374
+ reject(new Error('Request timeout - backend may be unavailable'));
135
375
  });
136
376
 
137
- req.setTimeout(10000);
377
+ req.setTimeout(this.requestTimeout);
138
378
  req.end();
139
379
  });
140
380
  }
141
381
 
382
+ // Network health tracking
383
+ async updateNetworkHealth(success, errorMessage = null) {
384
+ try {
385
+ if (success) {
386
+ this.networkHealth.consecutiveFailures = 0;
387
+ this.networkHealth.lastSuccessful = Date.now();
388
+ this.networkHealth.lastErrorType = null;
389
+ } else {
390
+ this.networkHealth.consecutiveFailures++;
391
+ this.networkHealth.lastErrorType = errorMessage;
392
+ }
393
+
394
+ // Save health metrics
395
+ await fs.writeFile(this.healthFile, JSON.stringify(this.networkHealth, null, 2));
396
+ } catch (error) {
397
+ this.log(`Failed to update network health: ${error.message}`, 'warn');
398
+ }
399
+ }
400
+
401
+ // Enhanced quota status checking
142
402
  async checkQuotaStatus(validation) {
143
403
  const quota = validation.quota || {};
144
404
 
@@ -152,9 +412,12 @@ class CloudApiKeyManager {
152
412
 
153
413
  if (quotaUsed >= quotaLimit) {
154
414
  const tier = validation.tier || 'explorer';
415
+ const upgradeMessage = tier === 'explorer'
416
+ ? 'Upgrade to Creator ($25.99/mo) for 18,000 optimizations: https://promptoptimizer-blog.vercel.app/pricing'
417
+ : 'Quota will reset on your next billing cycle.';
418
+
155
419
  throw new Error(
156
- `Monthly quota exceeded (${quotaUsed}/${quotaLimit}). ` +
157
- `${tier === 'explorer' ? 'Upgrade to Creator ($25.99/mo) for 18,000 optimizations: https://promptoptimizer-blog.vercel.app/pricing' : 'Quota will reset on your next billing cycle.'}`
420
+ `Monthly quota exceeded (${quotaUsed}/${quotaLimit}). ${upgradeMessage}`
158
421
  );
159
422
  }
160
423
 
@@ -163,10 +426,12 @@ class CloudApiKeyManager {
163
426
  unlimited: false,
164
427
  used: quotaUsed,
165
428
  limit: quotaLimit,
166
- remaining: quotaRemaining
429
+ remaining: quotaRemaining,
430
+ usage_percentage: (quotaUsed / quotaLimit) * 100
167
431
  };
168
432
  }
169
433
 
434
+ // Enhanced quota status retrieval
170
435
  async getQuotaStatus() {
171
436
  try {
172
437
  const url = `${this.backendUrl}/api/v1/mcp/quota-status`;
@@ -175,13 +440,15 @@ class CloudApiKeyManager {
175
440
  method: 'GET',
176
441
  headers: {
177
442
  'x-api-key': this.apiKey,
178
- 'User-Agent': 'mcp-prompt-optimizer/1.2.0'
443
+ 'User-Agent': `mcp-prompt-optimizer/${packageJson.version}`,
444
+ 'Connection': 'close'
179
445
  },
180
- timeout: 10000
446
+ timeout: this.requestTimeout
181
447
  };
182
448
 
183
449
  return new Promise((resolve, reject) => {
184
- const req = https.request(url, options, (res) => {
450
+ const client = this.backendUrl.startsWith('https://') ? https : http;
451
+ const req = client.request(url, options, (res) => {
185
452
  let data = '';
186
453
 
187
454
  res.on('data', (chunk) => {
@@ -218,7 +485,7 @@ class CloudApiKeyManager {
218
485
  reject(new Error('Request timeout'));
219
486
  });
220
487
 
221
- req.setTimeout(10000);
488
+ req.setTimeout(this.requestTimeout);
222
489
  req.end();
223
490
  });
224
491
 
@@ -228,15 +495,20 @@ class CloudApiKeyManager {
228
495
  }
229
496
  }
230
497
 
498
+ // Enhanced caching with metadata
231
499
  async cacheValidation(validation) {
232
500
  try {
233
501
  const cacheData = {
234
502
  timestamp: Date.now(),
235
- data: validation
503
+ apiKeyPrefix: this.apiKey.substring(0, 20) + '...', // Safe prefix only
504
+ data: validation,
505
+ backendUrl: this.backendUrl,
506
+ packageVersion: packageJson.version,
507
+ networkHealth: { ...this.networkHealth }
236
508
  };
237
509
 
238
510
  await fs.writeFile(this.cacheFile, JSON.stringify(cacheData, null, 2));
239
- this.log('API key validation cached');
511
+ this.log('API key validation cached successfully');
240
512
  } catch (error) {
241
513
  this.log(`Failed to cache validation: ${error.message}`, 'warn');
242
514
  }
@@ -245,47 +517,112 @@ class CloudApiKeyManager {
245
517
  async getCachedValidation() {
246
518
  try {
247
519
  const cacheContent = await fs.readFile(this.cacheFile, 'utf8');
248
- return JSON.parse(cacheContent);
520
+ const cached = JSON.parse(cacheContent);
521
+
522
+ // Validate cache structure
523
+ if (!cached.timestamp || !cached.data) {
524
+ this.log('Invalid cache structure, ignoring', 'warn');
525
+ return null;
526
+ }
527
+
528
+ return cached;
249
529
  } catch (error) {
250
- return null; // No cache file or invalid cache
530
+ if (error.code !== 'ENOENT') {
531
+ this.log(`Cache read error: ${error.message}`, 'warn');
532
+ }
533
+ return null;
251
534
  }
252
535
  }
253
536
 
254
537
  isCacheExpired(cachedData) {
255
- return (Date.now() - cachedData.timestamp) > this.cacheExpiry;
538
+ if (!cachedData || !cachedData.timestamp) {
539
+ return true;
540
+ }
541
+
542
+ const age = Date.now() - cachedData.timestamp;
543
+ const expired = age > this.cacheExpiry;
544
+
545
+ if (expired) {
546
+ this.log(`Cache expired: ${Math.round(age / 1000 / 60)} minutes old`);
547
+ }
548
+
549
+ return expired;
550
+ }
551
+
552
+ // Extended fallback cache for network issues
553
+ isFallbackCacheExpired(cachedData) {
554
+ if (!cachedData || !cachedData.timestamp) {
555
+ return true;
556
+ }
557
+
558
+ const age = Date.now() - cachedData.timestamp;
559
+ const expired = age > this.fallbackCacheExpiry;
560
+
561
+ if (expired) {
562
+ this.log(`Fallback cache expired: ${Math.round(age / 1000 / 60 / 60)} hours old`);
563
+ }
564
+
565
+ return expired;
256
566
  }
257
567
 
258
568
  async clearCache() {
259
569
  try {
260
570
  await fs.unlink(this.cacheFile);
261
- this.log('API key cache cleared');
571
+ this.log('API key cache cleared successfully');
262
572
  } catch (error) {
263
- // File might not exist, that's fine
573
+ if (error.code !== 'ENOENT') {
574
+ this.log(`Cache clear error: ${error.message}`, 'warn');
575
+ }
576
+ }
577
+
578
+ try {
579
+ await fs.unlink(this.healthFile);
580
+ this.log('Network health cache cleared successfully');
581
+ } catch (error) {
582
+ if (error.code !== 'ENOENT') {
583
+ this.log(`Health cache clear error: ${error.message}`, 'warn');
584
+ }
264
585
  }
265
586
  }
266
587
 
588
+ // Enhanced validation and preparation
267
589
  async validateAndPrepare() {
268
- this.log('Starting API key validation and preparation...');
590
+ this.log('Starting comprehensive API key validation and preparation...');
269
591
 
270
592
  try {
271
593
  // Step 1: Validate API key
272
594
  const validation = await this.validateApiKey();
273
595
 
274
- // Step 2: Check quota
275
- const quotaStatus = await this.checkQuotaStatus(validation);
596
+ // Step 2: Check quota (skip for development/mock modes)
597
+ let quotaStatus;
598
+ if (validation.mock_mode || validation.fallback_mode || validation.offline_mode) {
599
+ quotaStatus = validation.quota || { allowed: true, unlimited: true };
600
+ } else {
601
+ quotaStatus = await this.checkQuotaStatus(validation);
602
+ }
276
603
 
277
604
  // Step 3: Log success
605
+ const mode = validation.mock_mode ? '(mock)' :
606
+ validation.fallback_mode ? '(fallback)' :
607
+ validation.offline_mode ? '(offline)' : '';
608
+
278
609
  if (quotaStatus.unlimited) {
279
- this.log(`API key valid: ${validation.tier} (unlimited usage)`, 'success');
610
+ this.log(`API key valid: ${validation.tier} ${mode} (unlimited usage)`, 'success');
280
611
  } else {
281
- this.log(`API key valid: ${validation.tier} (${quotaStatus.remaining}/${quotaStatus.limit} remaining this month)`, 'success');
612
+ this.log(`API key valid: ${validation.tier} ${mode} (${quotaStatus.remaining}/${quotaStatus.limit} remaining this month)`, 'success');
282
613
  }
283
614
 
284
615
  return {
285
616
  validation,
286
617
  quotaStatus,
287
618
  tier: validation.tier,
288
- features: validation.features || {}
619
+ features: validation.features || {},
620
+ mode: {
621
+ development: this.developmentMode,
622
+ mock: validation.mock_mode || false,
623
+ fallback: validation.fallback_mode || false,
624
+ offline: validation.offline_mode || false
625
+ }
289
626
  };
290
627
 
291
628
  } catch (error) {
@@ -294,7 +631,150 @@ class CloudApiKeyManager {
294
631
  }
295
632
  }
296
633
 
297
- // Helper method to get API key info for display
634
+ // Enhanced testing with network health
635
+ async testIntegration() {
636
+ const results = {
637
+ formatValidation: { passed: false },
638
+ backendConnectivity: { passed: false },
639
+ cacheOperations: { passed: false },
640
+ fullValidation: { passed: false },
641
+ networkHealth: { ...this.networkHealth }
642
+ };
643
+
644
+ try {
645
+ // Test 1: Format validation
646
+ const formatCheck = this.validateApiKeyFormat(this.apiKey);
647
+ results.formatValidation = {
648
+ passed: formatCheck.valid,
649
+ keyType: formatCheck.keyType,
650
+ error: formatCheck.error
651
+ };
652
+
653
+ // Test 2: Backend connectivity (with timeout)
654
+ try {
655
+ const connectivityTimeout = 10000; // 10 seconds for testing
656
+ const originalTimeout = this.requestTimeout;
657
+ this.requestTimeout = connectivityTimeout;
658
+
659
+ const backendResponse = await this.validateWithBackend();
660
+ results.backendConnectivity = {
661
+ passed: true,
662
+ responseStructure: Object.keys(backendResponse),
663
+ avgResponseTime: this.networkHealth.avgResponseTime
664
+ };
665
+
666
+ this.requestTimeout = originalTimeout;
667
+ } catch (error) {
668
+ results.backendConnectivity = {
669
+ passed: false,
670
+ error: error.message,
671
+ consecutiveFailures: this.networkHealth.consecutiveFailures
672
+ };
673
+ }
674
+
675
+ // Test 3: Cache operations
676
+ try {
677
+ const testData = { test: true, timestamp: Date.now() };
678
+ await this.cacheValidation(testData);
679
+ const retrieved = await this.getCachedValidation();
680
+
681
+ results.cacheOperations = {
682
+ passed: retrieved && retrieved.data.test === true,
683
+ cacheAge: retrieved ? Math.round((Date.now() - retrieved.timestamp) / 1000) : null
684
+ };
685
+
686
+ await this.clearCache();
687
+ } catch (error) {
688
+ results.cacheOperations = {
689
+ passed: false,
690
+ error: error.message
691
+ };
692
+ }
693
+
694
+ // Test 4: Full validation flow
695
+ try {
696
+ const validation = await this.validateAndPrepare();
697
+ results.fullValidation = {
698
+ passed: true,
699
+ tier: validation.tier,
700
+ mode: validation.mode
701
+ };
702
+ } catch (error) {
703
+ results.fullValidation = {
704
+ passed: false,
705
+ error: error.message
706
+ };
707
+ }
708
+
709
+ } catch (error) {
710
+ results.generalError = error.message;
711
+ }
712
+
713
+ return results;
714
+ }
715
+
716
+ // Enhanced diagnostic information
717
+ async getDiagnosticInfo() {
718
+ const info = {
719
+ apiKey: this.apiKey ? `${this.apiKey.substring(0, 20)}...` : 'not provided',
720
+ backendUrl: this.backendUrl,
721
+ cacheFile: this.cacheFile,
722
+ healthFile: this.healthFile,
723
+ cacheExpiry: this.cacheExpiry,
724
+ fallbackCacheExpiry: this.fallbackCacheExpiry,
725
+ offlineMode: this.offlineMode,
726
+ developmentMode: this.developmentMode,
727
+ maxRetries: this.maxRetries,
728
+ requestTimeout: this.requestTimeout,
729
+ nodeEnv: process.env.NODE_ENV,
730
+ packageVersion: packageJson.version,
731
+ timestamp: new Date().toISOString(),
732
+ networkHealth: { ...this.networkHealth }
733
+ };
734
+
735
+ // Check cache status
736
+ try {
737
+ const cached = await this.getCachedValidation();
738
+ info.cache = {
739
+ exists: !!cached,
740
+ expired: cached ? this.isCacheExpired(cached) : null,
741
+ fallbackExpired: cached ? this.isFallbackCacheExpired(cached) : null,
742
+ age: cached ? Math.round((Date.now() - cached.timestamp) / 1000 / 60) : null,
743
+ backendUrl: cached ? cached.backendUrl : null,
744
+ packageVersion: cached ? cached.packageVersion : null
745
+ };
746
+ } catch (error) {
747
+ info.cache = { error: error.message };
748
+ }
749
+
750
+ // Check API key format
751
+ info.keyFormat = this.validateApiKeyFormat(this.apiKey);
752
+
753
+ // Test backend connectivity
754
+ try {
755
+ const startTime = Date.now();
756
+ await this.validateWithBackend();
757
+ const responseTime = Date.now() - startTime;
758
+
759
+ info.backendConnectivity = {
760
+ status: 'success',
761
+ responseTime: responseTime
762
+ };
763
+ } catch (error) {
764
+ info.backendConnectivity = {
765
+ status: 'failed',
766
+ error: error.message,
767
+ timeout: error.message.includes('timeout'),
768
+ network: error.message.includes('Network') || error.message.includes('DNS'),
769
+ dns: error.message.includes('DNS') || error.message.includes('ENOTFOUND'),
770
+ connection: error.message.includes('ECONNREFUSED') || error.message.includes('ECONNRESET')
771
+ };
772
+ }
773
+
774
+ return info;
775
+ }
776
+
777
+ // Enhanced API key info with mode detection
298
778
  async getApiKeyInfo() {
299
779
  try {
300
780
  const validation = await this.validateApiKey();
@@ -305,7 +785,13 @@ class CloudApiKeyManager {
305
785
  features: validation.features || {},
306
786
  quota: quotaStatus,
307
787
  isValid: true,
308
- keyType: validation.api_key_type || (this.apiKey.startsWith('sk-team-') ? 'team' : 'individual')
788
+ keyType: validation.api_key_type || this.validateApiKeyFormat(this.apiKey).keyType,
789
+ mode: {
790
+ mock: validation.mock_mode || false,
791
+ fallback: validation.fallback_mode || false,
792
+ offline: validation.offline_mode || false,
793
+ development: this.developmentMode
794
+ }
309
795
  };
310
796
  } catch (error) {
311
797
  return {
@@ -314,7 +800,13 @@ class CloudApiKeyManager {
314
800
  quota: { allowed: false },
315
801
  isValid: false,
316
802
  error: error.message,
317
- keyType: 'unknown'
803
+ keyType: 'unknown',
804
+ mode: {
805
+ mock: false,
806
+ fallback: false,
807
+ offline: false,
808
+ development: this.developmentMode
809
+ }
318
810
  };
319
811
  }
320
812
  }