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.
@@ -0,0 +1,518 @@
1
+ /**
2
+ * Database Module
3
+ * JSON-based storage system for API key tracking
4
+ * Stores data in ~/.apigraveyard.json
5
+ */
6
+
7
+ import fs from 'fs/promises';
8
+ import path from 'path';
9
+ import os from 'os';
10
+ import crypto from 'crypto';
11
+
12
+ /**
13
+ * Database file path in user's home directory
14
+ * @constant {string}
15
+ */
16
+ const DB_FILE = path.join(os.homedir(), '.apigraveyard.json');
17
+
18
+ /**
19
+ * Backup file path
20
+ * @constant {string}
21
+ */
22
+ const DB_BACKUP_FILE = path.join(os.homedir(), '.apigraveyard.backup.json');
23
+
24
+ /**
25
+ * Current database schema version
26
+ * @constant {string}
27
+ */
28
+ const DB_VERSION = '1.0.0';
29
+
30
+ /**
31
+ * Creates an empty database structure
32
+ *
33
+ * @returns {Object} - Empty database object with default structure
34
+ */
35
+ function createEmptyDatabase() {
36
+ return {
37
+ version: DB_VERSION,
38
+ createdAt: new Date().toISOString(),
39
+ updatedAt: new Date().toISOString(),
40
+ projects: [],
41
+ bannedKeys: []
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Generates a UUID v4 for project identification
47
+ *
48
+ * @returns {string} - UUID string
49
+ */
50
+ function generateUUID() {
51
+ return crypto.randomUUID();
52
+ }
53
+
54
+ /**
55
+ * Normalizes a file path for consistent comparison
56
+ *
57
+ * @param {string} filePath - Path to normalize
58
+ * @returns {string} - Normalized path
59
+ */
60
+ function normalizePath(filePath) {
61
+ return path.resolve(filePath).toLowerCase();
62
+ }
63
+
64
+ /**
65
+ * Reads and parses the database file
66
+ *
67
+ * @returns {Promise<Object>} - Parsed database object
68
+ * @throws {Error} - If file cannot be read or parsed
69
+ */
70
+ async function readDatabase() {
71
+ try {
72
+ const data = await fs.readFile(DB_FILE, 'utf-8');
73
+ return JSON.parse(data);
74
+ } catch (error) {
75
+ if (error.code === 'ENOENT') {
76
+ // File doesn't exist, return empty database
77
+ return null;
78
+ }
79
+ if (error instanceof SyntaxError) {
80
+ // JSON parse error - try to recover from backup
81
+ console.warn('⚠️ Database file corrupted, attempting to restore from backup...');
82
+ try {
83
+ const backupData = await fs.readFile(DB_BACKUP_FILE, 'utf-8');
84
+ return JSON.parse(backupData);
85
+ } catch {
86
+ console.error('❌ Backup also corrupted or missing. Creating new database.');
87
+ return null;
88
+ }
89
+ }
90
+ throw error;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Writes the database to file atomically
96
+ * Uses write-to-temp-then-rename strategy for safety
97
+ *
98
+ * @param {Object} db - Database object to write
99
+ * @returns {Promise<void>}
100
+ * @throws {Error} - If file cannot be written
101
+ */
102
+ async function writeDatabase(db) {
103
+ // Update timestamp
104
+ db.updatedAt = new Date().toISOString();
105
+
106
+ const tempFile = `${DB_FILE}.tmp`;
107
+ const jsonData = JSON.stringify(db, null, 2);
108
+
109
+ try {
110
+ // Backup existing database before writing
111
+ try {
112
+ await fs.access(DB_FILE);
113
+ await fs.copyFile(DB_FILE, DB_BACKUP_FILE);
114
+ } catch {
115
+ // No existing file to backup, that's OK
116
+ }
117
+
118
+ // Write to temp file first
119
+ await fs.writeFile(tempFile, jsonData, 'utf-8');
120
+
121
+ // Rename temp file to actual file (atomic operation)
122
+ await fs.rename(tempFile, DB_FILE);
123
+ } catch (error) {
124
+ // Clean up temp file if it exists
125
+ try {
126
+ await fs.unlink(tempFile);
127
+ } catch {
128
+ // Ignore cleanup errors
129
+ }
130
+ throw error;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Initializes the database
136
+ * Creates the database file if it doesn't exist
137
+ *
138
+ * @returns {Promise<Object>} - The database object
139
+ *
140
+ * @example
141
+ * const db = await initDatabase();
142
+ * console.log(`Database version: ${db.version}`);
143
+ */
144
+ export async function initDatabase() {
145
+ let db = await readDatabase();
146
+
147
+ if (!db) {
148
+ db = createEmptyDatabase();
149
+ await writeDatabase(db);
150
+ console.log('✓ Created new APIgraveyard database at', DB_FILE);
151
+ }
152
+
153
+ // Handle version migrations if needed
154
+ if (db.version !== DB_VERSION) {
155
+ console.log(`📦 Migrating database from v${db.version} to v${DB_VERSION}`);
156
+ db.version = DB_VERSION;
157
+ await writeDatabase(db);
158
+ }
159
+
160
+ return db;
161
+ }
162
+
163
+ /**
164
+ * Saves a project with scan results to the database
165
+ * Updates existing project if path already exists
166
+ *
167
+ * @param {string} projectPath - Full path to the project directory
168
+ * @param {Object} scanResults - Results from scanner.scanDirectory()
169
+ * @param {number} scanResults.totalFiles - Number of files scanned
170
+ * @param {Array} scanResults.keysFound - Array of found keys
171
+ * @returns {Promise<Object>} - The saved project object
172
+ *
173
+ * @example
174
+ * const results = await scanDirectory('./myproject');
175
+ * const project = await saveProject('./myproject', results);
176
+ * console.log(`Saved project: ${project.id}`);
177
+ */
178
+ export async function saveProject(projectPath, scanResults) {
179
+ const db = await initDatabase();
180
+ const normalizedPath = normalizePath(projectPath);
181
+ const projectName = path.basename(projectPath);
182
+
183
+ // Check if project already exists
184
+ const existingIndex = db.projects.findIndex(
185
+ p => normalizePath(p.path) === normalizedPath
186
+ );
187
+
188
+ const project = {
189
+ id: existingIndex >= 0 ? db.projects[existingIndex].id : generateUUID(),
190
+ name: projectName,
191
+ path: path.resolve(projectPath),
192
+ scannedAt: new Date().toISOString(),
193
+ totalFiles: scanResults.totalFiles,
194
+ keys: scanResults.keysFound.map(key => ({
195
+ service: key.service,
196
+ key: key.key,
197
+ fullKey: key.fullKey,
198
+ status: null,
199
+ filePath: key.filePath,
200
+ lineNumber: key.lineNumber,
201
+ column: key.column,
202
+ lastTested: null,
203
+ quotaInfo: {}
204
+ }))
205
+ };
206
+
207
+ if (existingIndex >= 0) {
208
+ // Update existing project
209
+ db.projects[existingIndex] = project;
210
+ } else {
211
+ // Add new project
212
+ db.projects.push(project);
213
+ }
214
+
215
+ await writeDatabase(db);
216
+ return project;
217
+ }
218
+
219
+ /**
220
+ * Retrieves a project by its path
221
+ *
222
+ * @param {string} projectPath - Path to the project
223
+ * @returns {Promise<Object|null>} - Project object or null if not found
224
+ *
225
+ * @example
226
+ * const project = await getProject('./myproject');
227
+ * if (project) {
228
+ * console.log(`Found ${project.keys.length} keys`);
229
+ * }
230
+ */
231
+ export async function getProject(projectPath) {
232
+ const db = await initDatabase();
233
+ const normalizedPath = normalizePath(projectPath);
234
+
235
+ const project = db.projects.find(
236
+ p => normalizePath(p.path) === normalizedPath
237
+ );
238
+
239
+ return project || null;
240
+ }
241
+
242
+ /**
243
+ * Retrieves all projects from the database
244
+ *
245
+ * @returns {Promise<Array>} - Array of all project objects
246
+ *
247
+ * @example
248
+ * const projects = await getAllProjects();
249
+ * projects.forEach(p => console.log(p.name));
250
+ */
251
+ export async function getAllProjects() {
252
+ const db = await initDatabase();
253
+ return db.projects;
254
+ }
255
+
256
+ /**
257
+ * Updates the status of a specific key in a project
258
+ *
259
+ * @param {string} projectPath - Path to the project
260
+ * @param {string} keyValue - The full key value to update
261
+ * @param {Object} testResult - Test result from tester.js
262
+ * @param {string} testResult.status - Key status (VALID, INVALID, etc.)
263
+ * @param {Object} testResult.details - Additional details from testing
264
+ * @returns {Promise<boolean>} - True if updated, false if not found
265
+ *
266
+ * @example
267
+ * await updateKeyStatus('./myproject', 'sk-xxx...', {
268
+ * status: 'VALID',
269
+ * details: { modelsCount: 15 }
270
+ * });
271
+ */
272
+ export async function updateKeyStatus(projectPath, keyValue, testResult) {
273
+ const db = await initDatabase();
274
+ const normalizedPath = normalizePath(projectPath);
275
+
276
+ const project = db.projects.find(
277
+ p => normalizePath(p.path) === normalizedPath
278
+ );
279
+
280
+ if (!project) {
281
+ return false;
282
+ }
283
+
284
+ const key = project.keys.find(k => k.fullKey === keyValue);
285
+
286
+ if (!key) {
287
+ return false;
288
+ }
289
+
290
+ key.status = testResult.status;
291
+ key.lastTested = new Date().toISOString();
292
+ key.quotaInfo = testResult.details || {};
293
+
294
+ if (testResult.error) {
295
+ key.lastError = testResult.error;
296
+ }
297
+
298
+ await writeDatabase(db);
299
+ return true;
300
+ }
301
+
302
+ /**
303
+ * Updates multiple keys in a project with test results
304
+ *
305
+ * @param {string} projectPath - Path to the project
306
+ * @param {Array} testResults - Array of test results from testKeys()
307
+ * @returns {Promise<number>} - Number of keys updated
308
+ *
309
+ * @example
310
+ * const results = await testKeys(keysFound);
311
+ * const count = await updateKeysStatus('./myproject', results);
312
+ * console.log(`Updated ${count} keys`);
313
+ */
314
+ export async function updateKeysStatus(projectPath, testResults) {
315
+ const db = await initDatabase();
316
+ const normalizedPath = normalizePath(projectPath);
317
+
318
+ const project = db.projects.find(
319
+ p => normalizePath(p.path) === normalizedPath
320
+ );
321
+
322
+ if (!project) {
323
+ return 0;
324
+ }
325
+
326
+ let updatedCount = 0;
327
+
328
+ for (const result of testResults) {
329
+ const key = project.keys.find(k => k.fullKey === result.fullKey);
330
+ if (key) {
331
+ key.status = result.status;
332
+ key.lastTested = new Date().toISOString();
333
+ key.quotaInfo = result.details || {};
334
+ if (result.error) {
335
+ key.lastError = result.error;
336
+ }
337
+ updatedCount++;
338
+ }
339
+ }
340
+
341
+ await writeDatabase(db);
342
+ return updatedCount;
343
+ }
344
+
345
+ /**
346
+ * Deletes a project from the database
347
+ *
348
+ * @param {string} projectPath - Path to the project to delete
349
+ * @returns {Promise<boolean>} - True if deleted, false if not found
350
+ *
351
+ * @example
352
+ * const deleted = await deleteProject('./myproject');
353
+ * if (deleted) {
354
+ * console.log('Project removed from tracking');
355
+ * }
356
+ */
357
+ export async function deleteProject(projectPath) {
358
+ const db = await initDatabase();
359
+ const normalizedPath = normalizePath(projectPath);
360
+
361
+ const initialLength = db.projects.length;
362
+ db.projects = db.projects.filter(
363
+ p => normalizePath(p.path) !== normalizedPath
364
+ );
365
+
366
+ if (db.projects.length < initialLength) {
367
+ await writeDatabase(db);
368
+ return true;
369
+ }
370
+
371
+ return false;
372
+ }
373
+
374
+ /**
375
+ * Adds a key to the banned keys list
376
+ * Banned keys will be flagged in future scans
377
+ *
378
+ * @param {string} keyValue - The full key value to ban
379
+ * @returns {Promise<boolean>} - True if added, false if already banned
380
+ *
381
+ * @example
382
+ * await addBannedKey('sk-compromised-key-here');
383
+ */
384
+ export async function addBannedKey(keyValue) {
385
+ const db = await initDatabase();
386
+
387
+ // Check if already banned
388
+ if (db.bannedKeys.includes(keyValue)) {
389
+ return false;
390
+ }
391
+
392
+ db.bannedKeys.push(keyValue);
393
+ await writeDatabase(db);
394
+ return true;
395
+ }
396
+
397
+ /**
398
+ * Removes a key from the banned keys list
399
+ *
400
+ * @param {string} keyValue - The full key value to unban
401
+ * @returns {Promise<boolean>} - True if removed, false if not found
402
+ */
403
+ export async function removeBannedKey(keyValue) {
404
+ const db = await initDatabase();
405
+
406
+ const initialLength = db.bannedKeys.length;
407
+ db.bannedKeys = db.bannedKeys.filter(k => k !== keyValue);
408
+
409
+ if (db.bannedKeys.length < initialLength) {
410
+ await writeDatabase(db);
411
+ return true;
412
+ }
413
+
414
+ return false;
415
+ }
416
+
417
+ /**
418
+ * Checks if a key is in the banned keys list
419
+ *
420
+ * @param {string} keyValue - The full key value to check
421
+ * @returns {Promise<boolean>} - True if banned, false otherwise
422
+ *
423
+ * @example
424
+ * if (await isBanned(key.fullKey)) {
425
+ * console.log('⚠️ This key has been marked as compromised!');
426
+ * }
427
+ */
428
+ export async function isBanned(keyValue) {
429
+ const db = await initDatabase();
430
+ return db.bannedKeys.includes(keyValue);
431
+ }
432
+
433
+ /**
434
+ * Gets all banned keys
435
+ *
436
+ * @returns {Promise<string[]>} - Array of banned key values
437
+ */
438
+ export async function getBannedKeys() {
439
+ const db = await initDatabase();
440
+ return db.bannedKeys;
441
+ }
442
+
443
+ /**
444
+ * Gets database statistics
445
+ *
446
+ * @returns {Promise<Object>} - Statistics about the database
447
+ *
448
+ * @example
449
+ * const stats = await getDatabaseStats();
450
+ * console.log(`Tracking ${stats.totalProjects} projects with ${stats.totalKeys} keys`);
451
+ */
452
+ export async function getDatabaseStats() {
453
+ const db = await initDatabase();
454
+
455
+ const totalKeys = db.projects.reduce((sum, p) => sum + p.keys.length, 0);
456
+ const validKeys = db.projects.reduce(
457
+ (sum, p) => sum + p.keys.filter(k => k.status === 'VALID').length,
458
+ 0
459
+ );
460
+ const invalidKeys = db.projects.reduce(
461
+ (sum, p) => sum + p.keys.filter(k => k.status === 'INVALID').length,
462
+ 0
463
+ );
464
+ const untestedKeys = db.projects.reduce(
465
+ (sum, p) => sum + p.keys.filter(k => k.status === null).length,
466
+ 0
467
+ );
468
+
469
+ return {
470
+ version: db.version,
471
+ totalProjects: db.projects.length,
472
+ totalKeys,
473
+ validKeys,
474
+ invalidKeys,
475
+ untestedKeys,
476
+ bannedKeys: db.bannedKeys.length,
477
+ createdAt: db.createdAt,
478
+ updatedAt: db.updatedAt,
479
+ dbPath: DB_FILE
480
+ };
481
+ }
482
+
483
+ /**
484
+ * Gets the database file path
485
+ *
486
+ * @returns {string} - Path to the database file
487
+ */
488
+ export function getDatabasePath() {
489
+ return DB_FILE;
490
+ }
491
+
492
+ /**
493
+ * Clears all data from the database
494
+ * Creates a fresh empty database
495
+ *
496
+ * @returns {Promise<void>}
497
+ */
498
+ export async function clearDatabase() {
499
+ const db = createEmptyDatabase();
500
+ await writeDatabase(db);
501
+ }
502
+
503
+ export default {
504
+ initDatabase,
505
+ saveProject,
506
+ getProject,
507
+ getAllProjects,
508
+ updateKeyStatus,
509
+ updateKeysStatus,
510
+ deleteProject,
511
+ addBannedKey,
512
+ removeBannedKey,
513
+ isBanned,
514
+ getBannedKeys,
515
+ getDatabaseStats,
516
+ getDatabasePath,
517
+ clearDatabase
518
+ };