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.
- package/CHANGELOG.md +9 -0
- package/README.md +6 -1
- package/index.js +1994 -1988
- package/lib/api-key-manager.js +730 -729
- package/package.json +271 -263
package/lib/api-key-manager.js
CHANGED
|
@@ -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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
this.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
response
|
|
180
|
-
response.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
await this.
|
|
212
|
-
this.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
fallbackData
|
|
235
|
-
fallbackData.
|
|
236
|
-
fallbackData.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
this.
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
'
|
|
310
|
-
'
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
req.
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
this.networkHealth.
|
|
380
|
-
this.networkHealth.
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
this.networkHealth.
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
this.
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
this.
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
this.
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
validation.
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
//
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
'
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
diagnosticInfo.cache.
|
|
623
|
-
diagnosticInfo.cache.
|
|
624
|
-
diagnosticInfo.cache.
|
|
625
|
-
diagnosticInfo.cache.
|
|
626
|
-
diagnosticInfo.cache.
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
diagnosticInfo.cache.
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
diagnosticInfo.backendConnectivity.
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
diagnosticInfo.backendConnectivity.
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
diagnosticInfo.backendConnectivity.
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
'
|
|
664
|
-
'
|
|
665
|
-
'
|
|
666
|
-
'
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
|
|
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;
|