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/.github/ISSUE_TEMPLATE/bug_report.md +28 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +22 -0
- package/.github/ROADMAP_ISSUES.md +169 -0
- package/LICENSE +21 -0
- package/README.md +501 -0
- package/bin/apigraveyard.js +686 -0
- package/hooks/pre-commit +203 -0
- package/package.json +34 -0
- package/scripts/install-hooks.js +182 -0
- package/src/database.js +518 -0
- package/src/display.js +534 -0
- package/src/scanner.js +294 -0
- package/src/tester.js +578 -0
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
|
+
};
|