apigraveyard 1.0.0

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/src/tester.js ADDED
@@ -0,0 +1,578 @@
1
+ /**
2
+ * Tester Module
3
+ * API key validation and testing system
4
+ * Tests keys against their respective service endpoints
5
+ */
6
+
7
+ import axios from 'axios';
8
+ import ora from 'ora';
9
+
10
+ /**
11
+ * Request timeout in milliseconds
12
+ * @constant {number}
13
+ */
14
+ const REQUEST_TIMEOUT = 10000;
15
+
16
+ /**
17
+ * Delay between API calls in milliseconds
18
+ * @constant {number}
19
+ */
20
+ const RATE_LIMIT_DELAY = 500;
21
+
22
+ /**
23
+ * Maximum retry attempts for rate-limited requests
24
+ * @constant {number}
25
+ */
26
+ const MAX_RETRIES = 3;
27
+
28
+ /**
29
+ * Key validation status constants
30
+ * @enum {string}
31
+ */
32
+ export const KeyStatus = {
33
+ VALID: 'VALID',
34
+ INVALID: 'INVALID',
35
+ EXPIRED: 'EXPIRED',
36
+ RATE_LIMITED: 'RATE_LIMITED',
37
+ ERROR: 'ERROR'
38
+ };
39
+
40
+ /**
41
+ * Creates a delay promise for rate limiting
42
+ *
43
+ * @param {number} ms - Milliseconds to delay
44
+ * @returns {Promise<void>}
45
+ */
46
+ const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
47
+
48
+ /**
49
+ * Calculates exponential backoff delay
50
+ *
51
+ * @param {number} attempt - Current attempt number (0-indexed)
52
+ * @param {number} baseDelay - Base delay in milliseconds
53
+ * @returns {number} - Delay in milliseconds
54
+ */
55
+ const getBackoffDelay = (attempt, baseDelay = 1000) => {
56
+ return baseDelay * Math.pow(2, attempt);
57
+ };
58
+
59
+ /**
60
+ * Determines key status from HTTP response status code
61
+ *
62
+ * @param {number} statusCode - HTTP status code
63
+ * @returns {string} - Key status constant
64
+ */
65
+ function getStatusFromCode(statusCode) {
66
+ if (statusCode === 200) return KeyStatus.VALID;
67
+ if (statusCode === 401) return KeyStatus.INVALID;
68
+ if (statusCode === 403) return KeyStatus.EXPIRED;
69
+ if (statusCode === 429) return KeyStatus.RATE_LIMITED;
70
+ return KeyStatus.ERROR;
71
+ }
72
+
73
+ /**
74
+ * Makes an HTTP request with retry logic for rate limiting
75
+ *
76
+ * @param {Object} config - Axios request configuration
77
+ * @param {number} [retries=0] - Current retry count
78
+ * @returns {Promise<import('axios').AxiosResponse>} - Axios response
79
+ */
80
+ async function requestWithRetry(config, retries = 0) {
81
+ try {
82
+ const response = await axios({
83
+ ...config,
84
+ timeout: REQUEST_TIMEOUT,
85
+ validateStatus: () => true // Accept any status to handle manually
86
+ });
87
+
88
+ // Handle rate limiting with exponential backoff
89
+ if (response.status === 429 && retries < MAX_RETRIES) {
90
+ const backoffDelay = getBackoffDelay(retries);
91
+ await delay(backoffDelay);
92
+ return requestWithRetry(config, retries + 1);
93
+ }
94
+
95
+ return response;
96
+ } catch (error) {
97
+ if (retries < MAX_RETRIES && error.code === 'ECONNRESET') {
98
+ const backoffDelay = getBackoffDelay(retries);
99
+ await delay(backoffDelay);
100
+ return requestWithRetry(config, retries + 1);
101
+ }
102
+ throw error;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Tests an OpenAI API key for validity
108
+ *
109
+ * @param {string} key - The OpenAI API key to test
110
+ * @returns {Promise<Object>} - Test result with status and details
111
+ *
112
+ * @example
113
+ * const result = await testOpenAIKey('sk-...');
114
+ * // { status: 'VALID', details: { modelsCount: 15, testedAt: '...' } }
115
+ */
116
+ export async function testOpenAIKey(key) {
117
+ const result = {
118
+ status: KeyStatus.ERROR,
119
+ details: { testedAt: new Date().toISOString() },
120
+ error: null
121
+ };
122
+
123
+ try {
124
+ const response = await requestWithRetry({
125
+ method: 'GET',
126
+ url: 'https://api.openai.com/v1/models',
127
+ headers: {
128
+ 'Authorization': `Bearer ${key}`
129
+ }
130
+ });
131
+
132
+ result.status = getStatusFromCode(response.status);
133
+
134
+ if (response.status === 200 && response.data) {
135
+ result.details.modelsCount = response.data.data?.length || 0;
136
+ result.details.models = response.data.data?.slice(0, 5).map(m => m.id) || [];
137
+ }
138
+
139
+ // Try to get usage/quota information
140
+ if (response.status === 200) {
141
+ try {
142
+ const usageResponse = await requestWithRetry({
143
+ method: 'GET',
144
+ url: 'https://api.openai.com/v1/usage',
145
+ headers: { 'Authorization': `Bearer ${key}` }
146
+ });
147
+ if (usageResponse.status === 200 && usageResponse.data) {
148
+ result.details.usage = usageResponse.data;
149
+ }
150
+ } catch {
151
+ // Usage endpoint may not be available, ignore
152
+ }
153
+ }
154
+
155
+ } catch (error) {
156
+ result.status = KeyStatus.ERROR;
157
+ result.error = error.message;
158
+ }
159
+
160
+ return result;
161
+ }
162
+
163
+ /**
164
+ * Tests a Groq API key for validity
165
+ *
166
+ * @param {string} key - The Groq API key to test
167
+ * @returns {Promise<Object>} - Test result with status and details
168
+ *
169
+ * @example
170
+ * const result = await testGroqKey('gsk_...');
171
+ * // { status: 'VALID', details: { modelsCount: 5, testedAt: '...' } }
172
+ */
173
+ export async function testGroqKey(key) {
174
+ const result = {
175
+ status: KeyStatus.ERROR,
176
+ details: { testedAt: new Date().toISOString() },
177
+ error: null
178
+ };
179
+
180
+ try {
181
+ const response = await requestWithRetry({
182
+ method: 'GET',
183
+ url: 'https://api.groq.com/openai/v1/models',
184
+ headers: {
185
+ 'Authorization': `Bearer ${key}`
186
+ }
187
+ });
188
+
189
+ result.status = getStatusFromCode(response.status);
190
+
191
+ if (response.status === 200 && response.data) {
192
+ result.details.modelsCount = response.data.data?.length || 0;
193
+ result.details.models = response.data.data?.map(m => m.id) || [];
194
+ }
195
+
196
+ } catch (error) {
197
+ result.status = KeyStatus.ERROR;
198
+ result.error = error.message;
199
+ }
200
+
201
+ return result;
202
+ }
203
+
204
+ /**
205
+ * Tests a GitHub API key (Personal Access Token) for validity
206
+ *
207
+ * @param {string} key - The GitHub token to test
208
+ * @returns {Promise<Object>} - Test result with status and details
209
+ *
210
+ * @example
211
+ * const result = await testGitHubKey('ghp_...');
212
+ * // { status: 'VALID', details: { username: 'user', rateLimit: 4999, testedAt: '...' } }
213
+ */
214
+ export async function testGitHubKey(key) {
215
+ const result = {
216
+ status: KeyStatus.ERROR,
217
+ details: { testedAt: new Date().toISOString() },
218
+ error: null
219
+ };
220
+
221
+ try {
222
+ const response = await requestWithRetry({
223
+ method: 'GET',
224
+ url: 'https://api.github.com/user',
225
+ headers: {
226
+ 'Authorization': `token ${key}`,
227
+ 'Accept': 'application/vnd.github.v3+json',
228
+ 'User-Agent': 'APIgraveyard-Scanner'
229
+ }
230
+ });
231
+
232
+ result.status = getStatusFromCode(response.status);
233
+
234
+ if (response.status === 200 && response.data) {
235
+ result.details.username = response.data.login;
236
+ result.details.name = response.data.name;
237
+ result.details.email = response.data.email;
238
+ result.details.publicRepos = response.data.public_repos;
239
+ }
240
+
241
+ // Extract rate limit info from headers
242
+ if (response.headers) {
243
+ result.details.rateLimit = {
244
+ limit: parseInt(response.headers['x-ratelimit-limit'] || '0'),
245
+ remaining: parseInt(response.headers['x-ratelimit-remaining'] || '0'),
246
+ reset: response.headers['x-ratelimit-reset']
247
+ };
248
+ }
249
+
250
+ } catch (error) {
251
+ result.status = KeyStatus.ERROR;
252
+ result.error = error.message;
253
+ }
254
+
255
+ return result;
256
+ }
257
+
258
+ /**
259
+ * Tests a Stripe API key for validity
260
+ *
261
+ * @param {string} key - The Stripe API key to test
262
+ * @returns {Promise<Object>} - Test result with status and details
263
+ *
264
+ * @example
265
+ * const result = await testStripeKey('sk_test_...');
266
+ * // { status: 'VALID', details: { livemode: false, testedAt: '...' } }
267
+ */
268
+ export async function testStripeKey(key) {
269
+ const result = {
270
+ status: KeyStatus.ERROR,
271
+ details: { testedAt: new Date().toISOString() },
272
+ error: null
273
+ };
274
+
275
+ try {
276
+ const response = await requestWithRetry({
277
+ method: 'GET',
278
+ url: 'https://api.stripe.com/v1/balance',
279
+ headers: {
280
+ 'Authorization': `Bearer ${key}`
281
+ }
282
+ });
283
+
284
+ result.status = getStatusFromCode(response.status);
285
+
286
+ if (response.status === 200 && response.data) {
287
+ result.details.livemode = response.data.livemode;
288
+ result.details.available = response.data.available;
289
+ result.details.pending = response.data.pending;
290
+ }
291
+
292
+ } catch (error) {
293
+ result.status = KeyStatus.ERROR;
294
+ result.error = error.message;
295
+ }
296
+
297
+ return result;
298
+ }
299
+
300
+ /**
301
+ * Tests a Google API key for validity
302
+ *
303
+ * @param {string} key - The Google API key to test
304
+ * @returns {Promise<Object>} - Test result with status and details
305
+ *
306
+ * @example
307
+ * const result = await testGoogleKey('AIza...');
308
+ * // { status: 'VALID', details: { testedAt: '...' } }
309
+ */
310
+ export async function testGoogleKey(key) {
311
+ const result = {
312
+ status: KeyStatus.ERROR,
313
+ details: { testedAt: new Date().toISOString() },
314
+ error: null
315
+ };
316
+
317
+ try {
318
+ const response = await requestWithRetry({
319
+ method: 'GET',
320
+ url: `https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=${key}`
321
+ });
322
+
323
+ result.status = getStatusFromCode(response.status);
324
+
325
+ if (response.status === 200 && response.data) {
326
+ result.details.audience = response.data.audience;
327
+ result.details.scope = response.data.scope;
328
+ result.details.expiresIn = response.data.expires_in;
329
+ }
330
+
331
+ } catch (error) {
332
+ result.status = KeyStatus.ERROR;
333
+ result.error = error.message;
334
+ }
335
+
336
+ return result;
337
+ }
338
+
339
+ /**
340
+ * Tests an AWS Access Key ID (basic validation only)
341
+ * Note: Full AWS key validation requires secret key and is complex
342
+ *
343
+ * @param {string} key - The AWS Access Key ID to test
344
+ * @returns {Promise<Object>} - Test result with status and details
345
+ */
346
+ export async function testAWSKey(key) {
347
+ const result = {
348
+ status: KeyStatus.ERROR,
349
+ details: { testedAt: new Date().toISOString() },
350
+ error: 'AWS key validation requires both Access Key ID and Secret Access Key'
351
+ };
352
+
353
+ // AWS keys cannot be validated with just the access key ID
354
+ // We can only verify the format
355
+ if (/^AKIA[A-Z0-9]{16}$/.test(key)) {
356
+ result.status = KeyStatus.VALID;
357
+ result.details.note = 'Format valid. Full validation requires Secret Access Key.';
358
+ result.error = null;
359
+ } else {
360
+ result.status = KeyStatus.INVALID;
361
+ result.error = 'Invalid AWS Access Key ID format';
362
+ }
363
+
364
+ return result;
365
+ }
366
+
367
+ /**
368
+ * Tests an Anthropic API key for validity
369
+ *
370
+ * @param {string} key - The Anthropic API key to test
371
+ * @returns {Promise<Object>} - Test result with status and details
372
+ */
373
+ export async function testAnthropicKey(key) {
374
+ const result = {
375
+ status: KeyStatus.ERROR,
376
+ details: { testedAt: new Date().toISOString() },
377
+ error: null
378
+ };
379
+
380
+ try {
381
+ // Use messages endpoint for validation
382
+ const response = await requestWithRetry({
383
+ method: 'POST',
384
+ url: 'https://api.anthropic.com/v1/messages',
385
+ headers: {
386
+ 'x-api-key': key,
387
+ 'anthropic-version': '2023-06-01',
388
+ 'Content-Type': 'application/json'
389
+ },
390
+ data: {
391
+ model: 'claude-3-haiku-20240307',
392
+ max_tokens: 1,
393
+ messages: [{ role: 'user', content: 'Hi' }]
394
+ }
395
+ });
396
+
397
+ // 400 means key is valid but request may be malformed
398
+ // 401 means invalid key
399
+ if (response.status === 200 || response.status === 400) {
400
+ result.status = KeyStatus.VALID;
401
+ } else {
402
+ result.status = getStatusFromCode(response.status);
403
+ }
404
+
405
+ } catch (error) {
406
+ result.status = KeyStatus.ERROR;
407
+ result.error = error.message;
408
+ }
409
+
410
+ return result;
411
+ }
412
+
413
+ /**
414
+ * Tests a Hugging Face API key for validity
415
+ *
416
+ * @param {string} key - The Hugging Face API key to test
417
+ * @returns {Promise<Object>} - Test result with status and details
418
+ */
419
+ export async function testHuggingFaceKey(key) {
420
+ const result = {
421
+ status: KeyStatus.ERROR,
422
+ details: { testedAt: new Date().toISOString() },
423
+ error: null
424
+ };
425
+
426
+ try {
427
+ const response = await requestWithRetry({
428
+ method: 'GET',
429
+ url: 'https://huggingface.co/api/whoami-v2',
430
+ headers: {
431
+ 'Authorization': `Bearer ${key}`
432
+ }
433
+ });
434
+
435
+ result.status = getStatusFromCode(response.status);
436
+
437
+ if (response.status === 200 && response.data) {
438
+ result.details.username = response.data.name;
439
+ result.details.email = response.data.email;
440
+ result.details.orgs = response.data.orgs?.map(o => o.name) || [];
441
+ }
442
+
443
+ } catch (error) {
444
+ result.status = KeyStatus.ERROR;
445
+ result.error = error.message;
446
+ }
447
+
448
+ return result;
449
+ }
450
+
451
+ /**
452
+ * Maps service names to their test functions
453
+ * @type {Object.<string, Function>}
454
+ */
455
+ const SERVICE_TESTERS = {
456
+ 'OpenAI': testOpenAIKey,
457
+ 'Groq': testGroqKey,
458
+ 'GitHub': testGitHubKey,
459
+ 'Stripe': testStripeKey,
460
+ 'Google/Firebase': testGoogleKey,
461
+ 'AWS': testAWSKey,
462
+ 'Anthropic': testAnthropicKey,
463
+ 'Hugging Face': testHuggingFaceKey
464
+ };
465
+
466
+ /**
467
+ * Tests an array of API keys against their respective services
468
+ *
469
+ * Iterates through each key, calls the appropriate service tester,
470
+ * and returns results with validation status and details.
471
+ * Includes rate limiting between requests to avoid service throttling.
472
+ *
473
+ * @param {Array<{service: string, key: string, fullKey: string, filePath: string, lineNumber: number, column: number}>} keysArray
474
+ * Array of key objects from scanner.js
475
+ * @param {Object} [options={}] - Testing options
476
+ * @param {boolean} [options.showSpinner=true] - Whether to show loading spinner
477
+ * @param {boolean} [options.verbose=false] - Whether to show verbose output
478
+ *
479
+ * @returns {Promise<Array<{service: string, key: string, status: string, details: Object, error: string|null}>>}
480
+ * Array of tested key objects with status
481
+ *
482
+ * @example
483
+ * const keys = await scanDirectory('./src');
484
+ * const results = await testKeys(keys.keysFound);
485
+ * results.forEach(r => console.log(`${r.service}: ${r.status}`));
486
+ */
487
+ export async function testKeys(keysArray, options = {}) {
488
+ const { showSpinner = true, verbose = false } = options;
489
+ const results = [];
490
+
491
+ let spinner = null;
492
+ if (showSpinner) {
493
+ spinner = ora({
494
+ text: 'Testing API keys...',
495
+ spinner: 'dots'
496
+ }).start();
497
+ }
498
+
499
+ for (let i = 0; i < keysArray.length; i++) {
500
+ const keyInfo = keysArray[i];
501
+ const { service, key, fullKey, filePath, lineNumber, column } = keyInfo;
502
+
503
+ if (spinner) {
504
+ spinner.text = `Testing key ${i + 1}/${keysArray.length}: ${service} (${key})`;
505
+ }
506
+
507
+ // Get the appropriate tester for this service
508
+ const tester = SERVICE_TESTERS[service];
509
+
510
+ let testResult;
511
+ if (tester) {
512
+ try {
513
+ testResult = await tester(fullKey);
514
+ } catch (error) {
515
+ testResult = {
516
+ status: KeyStatus.ERROR,
517
+ details: { testedAt: new Date().toISOString() },
518
+ error: error.message
519
+ };
520
+ }
521
+ } else {
522
+ testResult = {
523
+ status: KeyStatus.ERROR,
524
+ details: { testedAt: new Date().toISOString() },
525
+ error: `No tester available for service: ${service}`
526
+ };
527
+ }
528
+
529
+ // Combine key info with test results
530
+ results.push({
531
+ service,
532
+ key,
533
+ fullKey,
534
+ filePath,
535
+ lineNumber,
536
+ column,
537
+ status: testResult.status,
538
+ details: testResult.details,
539
+ error: testResult.error
540
+ });
541
+
542
+ // Rate limiting delay between requests
543
+ if (i < keysArray.length - 1) {
544
+ await delay(RATE_LIMIT_DELAY);
545
+ }
546
+ }
547
+
548
+ if (spinner) {
549
+ const validCount = results.filter(r => r.status === KeyStatus.VALID).length;
550
+ const invalidCount = results.filter(r => r.status === KeyStatus.INVALID).length;
551
+ spinner.succeed(`Tested ${results.length} keys: ${validCount} valid, ${invalidCount} invalid`);
552
+ }
553
+
554
+ return results;
555
+ }
556
+
557
+ /**
558
+ * Gets list of supported services for testing
559
+ *
560
+ * @returns {string[]} - Array of service names
561
+ */
562
+ export function getSupportedServices() {
563
+ return Object.keys(SERVICE_TESTERS);
564
+ }
565
+
566
+ export default {
567
+ testKeys,
568
+ testOpenAIKey,
569
+ testGroqKey,
570
+ testGitHubKey,
571
+ testStripeKey,
572
+ testGoogleKey,
573
+ testAWSKey,
574
+ testAnthropicKey,
575
+ testHuggingFaceKey,
576
+ getSupportedServices,
577
+ KeyStatus
578
+ };