import crypto from "crypto"; import { db } from "../db"; import { apiKeys, type ApiKey, type NewApiKey } from "@banatie/database"; import { eq, and, desc } from "drizzle-orm"; export class ApiKeyService { /** * Generate a new API key * Format: bnt_{64_hex_chars} */ private generateKey(): { fullKey: string; keyHash: string; keyPrefix: string; } { const secret = crypto.randomBytes(32).toString("hex"); // 64 chars const keyPrefix = "bnt_"; const fullKey = keyPrefix + secret; // Hash for storage (SHA-256) const keyHash = crypto.createHash("sha256").update(fullKey).digest("hex"); return { fullKey, keyHash, keyPrefix }; } /** * Create a master key (admin access, never expires) */ async createMasterKey( name?: string, createdBy?: string, ): Promise<{ key: string; metadata: ApiKey }> { const { fullKey, keyHash, keyPrefix } = this.generateKey(); const [newKey] = await db .insert(apiKeys) .values({ keyHash, keyPrefix, keyType: "master", projectId: null, scopes: ["*"], // Full access name: name || "Master Key", expiresAt: null, // Never expires createdBy: createdBy || null, }) .returning(); console.log( `[${new Date().toISOString()}] Master key created: ${newKey?.id} - ${newKey?.name}`, ); return { key: fullKey, metadata: newKey! }; } /** * Create a project key (expires in 90 days) */ async createProjectKey( projectId: string, organizationId?: string, name?: string, createdBy?: string, expiresInDays: number = 90, ): Promise<{ key: string; metadata: ApiKey }> { const { fullKey, keyHash, keyPrefix } = this.generateKey(); const expiresAt = new Date(); expiresAt.setDate(expiresAt.getDate() + expiresInDays); const [newKey] = await db .insert(apiKeys) .values({ keyHash, keyPrefix, keyType: "project", projectId, organizationId: organizationId || null, scopes: ["generate", "read"], name: name || `Project Key - ${projectId}`, expiresAt, createdBy: createdBy || null, }) .returning(); console.log( `[${new Date().toISOString()}] Project key created: ${newKey?.id} - ${projectId} - expires: ${expiresAt.toISOString()}`, ); return { key: fullKey, metadata: newKey! }; } /** * Validate an API key * Returns null if invalid/expired/revoked */ async validateKey(providedKey: string): Promise { if (!providedKey || !providedKey.startsWith("bnt_")) { return null; } // Hash the provided key const keyHash = crypto .createHash("sha256") .update(providedKey) .digest("hex"); // Find in database const [key] = await db .select() .from(apiKeys) .where(and(eq(apiKeys.keyHash, keyHash), eq(apiKeys.isActive, true))) .limit(1); if (!key) { console.warn( `[${new Date().toISOString()}] Invalid API key attempt: ${providedKey.substring(0, 10)}...`, ); return null; } // Check expiration if (key.expiresAt && key.expiresAt < new Date()) { console.warn( `[${new Date().toISOString()}] Expired API key used: ${key.id} - expired: ${key.expiresAt.toISOString()}`, ); return null; } // Update last used timestamp (async, don't wait) db.update(apiKeys) .set({ lastUsedAt: new Date() }) .where(eq(apiKeys.id, key.id)) .execute() .catch((err) => console.error( `[${new Date().toISOString()}] Failed to update lastUsedAt:`, err, ), ); return key; } /** * Revoke a key (soft delete) */ async revokeKey(keyId: string): Promise { const result = await db .update(apiKeys) .set({ isActive: false }) .where(eq(apiKeys.id, keyId)) .returning(); if (result.length > 0) { console.log(`[${new Date().toISOString()}] API key revoked: ${keyId}`); return true; } return false; } /** * List all keys (for admin) */ async listKeys(): Promise { return db.select().from(apiKeys).orderBy(desc(apiKeys.createdAt)); } /** * Check if any keys exist (for bootstrap) */ async hasAnyKeys(): Promise { const keys = await db.select({ id: apiKeys.id }).from(apiKeys).limit(1); return keys.length > 0; } }