ai-flashmob-mcp
Version:
MCP server for AI-powered flashcard generation
162 lines (137 loc) • 5.19 kB
JavaScript
/**
* 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));
}
}