UNPKG

ai-flashmob-mcp

Version:

MCP server for AI-powered flashcard generation

162 lines (137 loc) 5.19 kB
/** * Authentication utilities for AI Flashmob API * * Implements HMAC-SHA256 request signing for secure API authentication * without transmitting the secret key over the network. */ import crypto from 'crypto'; /** * Signs an API request using HMAC-SHA256 * * @param {string} publicUserId - The public user identifier * @param {string} secretKey - The secret key for signing (64-character hex) * @param {Object} requestBody - The request body to be sent * @returns {Object} Headers object with authentication fields */ export function signRequest(publicUserId, secretKey, requestBody) { if (!publicUserId || !secretKey || !requestBody) { throw new Error('Missing required parameters: publicUserId, secretKey, and requestBody are required'); } // Validate secret key format if (typeof secretKey !== 'string' || secretKey.length !== 64 || !/^[a-f0-9]+$/i.test(secretKey)) { throw new Error('Invalid secret key format. Expected 64-character hexadecimal string.'); } // Create timestamp (milliseconds since epoch) const timestamp = Date.now().toString(); // Create deterministic JSON string from request body const bodyString = JSON.stringify(requestBody, null, 0); // Create the data to be signed (timestamp + body) const saltedData = timestamp + bodyString; // Create HMAC signature const signature = crypto .createHmac('sha256', secretKey) .update(saltedData) .digest('hex'); // Return headers for API request return { 'X-User-ID': publicUserId, 'X-Timestamp': timestamp, 'X-Signature': signature, 'Content-Type': 'application/json' }; } /** * Validates the configuration and credentials * * @param {string} publicUserId - The public user identifier * @param {string} secretKey - The secret key * @throws {Error} If configuration is invalid */ export function validateCredentials(publicUserId, secretKey) { if (!publicUserId) { throw new Error('PUBLIC_USER_ID is required'); } if (!secretKey) { throw new Error('SECRET_KEY is required'); } // Validate public user ID format (UUID) const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (!uuidRegex.test(publicUserId)) { throw new Error('Invalid PUBLIC_USER_ID format. Expected UUID format (e.g., f47ac10b-58cc-4372-a567-0e02b2c3d479)'); } // Validate secret key format if (secretKey.length !== 64 || !/^[a-f0-9]+$/i.test(secretKey)) { throw new Error('Invalid SECRET_KEY format. Expected 64-character hexadecimal string.'); } } /** * Creates a test request to verify API connectivity * * @param {string} apiBaseUrl - The base URL of the API * @param {string} publicUserId - The public user identifier * @param {string} secretKey - The secret key * @returns {Promise<boolean>} True if connection is successful */ export async function testApiConnection(apiBaseUrl, publicUserId, secretKey) { try { validateCredentials(publicUserId, secretKey); // Create a minimal test request const testBody = { text: 'Test connection to AI Flashmob API. This is a minimal test.', maxCards: 1 }; const headers = signRequest(publicUserId, secretKey, testBody); const response = await fetch(`${apiBaseUrl}/api/v1/external/ai/generate-cards`, { method: 'POST', headers, body: JSON.stringify(testBody) }); // Check if the request was authenticated successfully if (response.status === 401) { throw new Error('Authentication failed. Check your PUBLIC_USER_ID and SECRET_KEY.'); } if (response.status === 402) { throw new Error('Insufficient credits. Your account needs AI credits to use this service.'); } if (response.status === 429) { throw new Error('Rate limit exceeded. Please wait before trying again.'); } if (!response.ok) { const errorData = await response.json().catch(() => ({ message: 'Unknown error' })); throw new Error(`API request failed: ${errorData.message || response.statusText}`); } return true; } catch (error) { if (error instanceof TypeError && error.message.includes('fetch')) { throw new Error(`Unable to connect to API at ${apiBaseUrl}. Check the API_BASE_URL and network connection.`); } throw error; } } /** * Gets the API base URL from environment or default * * @returns {string} The API base URL */ export function getApiBaseUrl() { return process.env.API_BASE_URL || 'https://api.ai-flashmob.com'; } /** * Logs request details for debugging (without sensitive information) * * @param {string} method - HTTP method * @param {string} url - Request URL * @param {Object} body - Request body */ export function logRequest(method, url, body) { const timestamp = new Date().toISOString(); console.error(`[${timestamp}] ${method} ${url}`); if (body && Object.keys(body).length > 0) { // Log body without sensitive data const sanitizedBody = { ...body }; if (sanitizedBody.image) { sanitizedBody.image = `[base64 image: ${sanitizedBody.image.length} chars]`; } console.error(`Request body:`, JSON.stringify(sanitizedBody, null, 2)); } }