import { IRequest } from 'itty-router'; import { ApiKeyManager } from '../modules/ApiKeyManager'; import { Env } from '../types'; import { libx } from 'libx.js/build/bundles/essentials.js'; export interface AuthContext { apiKeyData?: any; providerKeys?: Record; userId?: string; } /** * Authentication middleware for Ask API keys * Validates the API key and loads associated provider keys */ export async function authMiddleware(req: IRequest, env: Env): Promise { // Skip auth for certain routes const url = new URL(req.url); const skipAuthPaths = ['/v1/health', '/v1', '/']; if (skipAuthPaths.includes(url.pathname) || req.method === 'OPTIONS') { return; } // Check for Ask API key in Authorization header const authHeader = req.headers.get('authorization'); let askApiKey: string | null = null; if (authHeader) { // Support both "Bearer ask_..." and "ask_..." formats const match = authHeader.match(/^(?:Bearer\s+)?(ask_[a-f0-9]{64})$/i); if (match) { askApiKey = match[1].toLowerCase(); } } // Also check x-ask-api-key header if (!askApiKey) { askApiKey = req.headers.get('x-ask-api-key'); } // If no Ask API key provided, skip auth (will use provider keys from headers/env) if (!askApiKey) { libx.log.v('Auth: No Ask API key provided, using direct provider keys'); return; } // Validate API key format if (!env.ASK_DB) { libx.log.w('Auth: Database not configured, skipping Ask API key validation'); return; } try { // Initialize API key manager const encryptionKey = env.API_KEY_ENCRYPTION_KEY || 'default-encryption-key-change-in-production'; const keyManager = new ApiKeyManager(env.ASK_DB, encryptionKey); // Validate and get API key const apiKeyData = await keyManager.getApiKey(askApiKey); if (!apiKeyData) { return new Response(JSON.stringify({ error: 'Invalid or inactive API key', code: 'INVALID_API_KEY' }), { status: 401, headers: { 'Content-Type': 'application/json', 'WWW-Authenticate': 'Bearer realm="Ask API"' } }); } // Check rate limits (basic implementation) // TODO: Implement proper rate limiting with sliding window if (apiKeyData.rateLimitRpm && apiKeyData.rateLimitRpm > 0) { // For now, just log it libx.log.v('Auth: Rate limit check', { rpm: apiKeyData.rateLimitRpm, tpm: apiKeyData.rateLimitTpm }); } // Get associated provider keys const providerKeys = await keyManager.getProviderKeys(askApiKey); if (Object.keys(providerKeys).length === 0) { return new Response(JSON.stringify({ error: 'No provider keys configured for this API key', code: 'NO_PROVIDER_KEYS' }), { status: 403, headers: { 'Content-Type': 'application/json' } }); } // Update last used timestamp (fire and forget) keyManager.updateLastUsed(askApiKey).catch(err => { libx.log.e('Auth: Error updating last used', err); }); // Attach auth context to request (req as any).authContext = { apiKeyData, providerKeys, userId: apiKeyData.userId } as AuthContext; libx.log.i('Auth: Authenticated request', { userId: apiKeyData.userId, keyName: apiKeyData.name, providers: Object.keys(providerKeys) }); } catch (error: any) { libx.log.e('Auth: Authentication error', error); return new Response(JSON.stringify({ error: 'Authentication failed', code: 'AUTH_ERROR', message: error.message }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } } /** * Helper to extract auth context from request */ export function getAuthContext(req: IRequest): AuthContext | null { return (req as any).authContext || null; } /** * Middleware to require authentication */ export async function requireAuth(req: IRequest, env: Env): Promise { const result = await authMiddleware(req, env); if (result) return result; const authContext = getAuthContext(req); if (!authContext || !authContext.apiKeyData) { return new Response(JSON.stringify({ error: 'Authentication required', code: 'AUTH_REQUIRED' }), { status: 401, headers: { 'Content-Type': 'application/json', 'WWW-Authenticate': 'Bearer realm="Ask API"' } }); } }