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.
- package/CHANGELOG.md +85 -0
- package/README.md +286 -130
- package/index.js +1092 -724
- package/lib/api-key-manager.js +538 -46
- package/lib/check-status.js +356 -111
- package/lib/clear-cache.js +113 -79
- package/lib/diagnose.js +252 -0
- package/lib/diagnose.js file.txt +252 -0
- package/lib/test-integration.js +250 -0
- package/lib/validate-key.js +171 -54
- package/package.json +224 -28
package/lib/api-key-manager.js
CHANGED
|
@@ -1,21 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cloud API Key Manager for MCP Prompt Optimizer
|
|
3
|
-
*
|
|
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('
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
//
|
|
49
|
-
const validation = await this.
|
|
208
|
+
// Step 3: Backend validation with enhanced retry logic
|
|
209
|
+
const validation = await this.validateWithBackendRetry();
|
|
50
210
|
|
|
51
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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':
|
|
301
|
+
'User-Agent': `mcp-prompt-optimizer/${packageJson.version}`,
|
|
302
|
+
'Accept': 'application/json',
|
|
303
|
+
'Connection': 'close' // Ensure connection cleanup
|
|
91
304
|
},
|
|
92
|
-
timeout:
|
|
305
|
+
timeout: this.requestTimeout
|
|
93
306
|
};
|
|
94
307
|
|
|
95
|
-
this.log(`Making request
|
|
308
|
+
this.log(`Making request to: ${url}`);
|
|
309
|
+
this.log(`Using API key: ${this.apiKey.substring(0, 16)}...`);
|
|
96
310
|
|
|
97
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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':
|
|
443
|
+
'User-Agent': `mcp-prompt-optimizer/${packageJson.version}`,
|
|
444
|
+
'Connection': 'close'
|
|
179
445
|
},
|
|
180
|
-
timeout:
|
|
446
|
+
timeout: this.requestTimeout
|
|
181
447
|
};
|
|
182
448
|
|
|
183
449
|
return new Promise((resolve, reject) => {
|
|
184
|
-
const
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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.
|
|
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
|
}
|