UNPKG

avocavo

Version:

Avocavo CLI - Nutrition analysis made simple. Get accurate USDA nutrition data with secure authentication.

262 lines (226 loc) 8.47 kB
const axios = require('axios'); const chalk = require('chalk'); class ApiError extends Error { constructor(message, statusCode = null, response = null) { super(message); this.name = 'ApiError'; this.statusCode = statusCode; this.response = response; } } class NutritionAPI { constructor(apiKey, baseUrl = 'https://app.avocavo.app', timeout = 30000, authManager = null) { this.apiKey = apiKey; this.baseUrl = baseUrl.replace(/\/$/, ''); this.timeout = timeout; this.authManager = authManager; // Create axios instance this.client = axios.create({ baseURL: this.baseUrl, timeout: this.timeout, headers: { 'Content-Type': 'application/json', 'User-Agent': 'avocavo-nutrition-cli/1.8.0' } }); // Add API key to requests if provided if (this.apiKey) { this.client.defaults.headers['X-API-Key'] = this.apiKey; } // Add request interceptor to handle authentication this.client.interceptors.request.use(async (config) => { // Determine authentication method based on endpoint const endpoint = config.url; if (process.env.NODE_ENV === 'development' || process.env.AVOCAVO_DEBUG) { console.log(`[DEBUG] Request to ${endpoint} with API key: ${this.apiKey ? '[REDACTED]' : 'none'}`); // Sanitize headers before logging const sanitizedHeaders = { ...config.headers }; if (sanitizedHeaders['X-API-Key']) sanitizedHeaders['X-API-Key'] = '[REDACTED]'; if (sanitizedHeaders['Authorization']) sanitizedHeaders['Authorization'] = '[REDACTED]'; console.log(`[DEBUG] Request headers:`, JSON.stringify(sanitizedHeaders, null, 2)); } if (endpoint.startsWith('/api/auth/')) { // JWT authentication for key management endpoints if (this.authManager) { const jwt = await this.authManager.getJwtToken(); if (jwt) { config.headers['Authorization'] = `Bearer ${jwt}`; // Remove API key header for JWT endpoints delete config.headers['X-API-Key']; } } } else if (endpoint.startsWith('/api/v2/nutrition/') || endpoint.startsWith('/api/v1/nutrition/')) { // API key authentication for nutrition endpoints if (this.apiKey) { // Use API key from constructor (command line or direct) config.headers['X-API-Key'] = this.apiKey; } else if (this.authManager) { // Try to get selected API key from auth manager const apiKey = await this.authManager.getApiKey(); if (apiKey) { config.headers['X-API-Key'] = apiKey; } } } return config; }); // Response interceptor for error handling this.client.interceptors.response.use( response => response, error => { if (error.response) { const status = error.response.status; const data = error.response.data; let message = data?.error || `HTTP ${status}`; if (status === 401) { message = 'Invalid API key or authentication required'; if (process.env.NODE_ENV === 'development' || process.env.AVOCAVO_DEBUG) { console.log(`[DEBUG] 401 Response:`, JSON.stringify(data, null, 2)); } } else if (status === 402) { message = 'Trial expired or payment required'; } else if (status === 403) { message = 'Feature not available on your plan'; } else if (status === 429) { message = 'Rate limit exceeded'; } else if (status >= 500) { message = 'Server error - please try again later'; } throw new ApiError(message, status, data); } else if (error.request) { throw new ApiError('Connection error. Check your internet connection.'); } else { throw new ApiError(`Request failed: ${error.message}`); } } ); } async analyze(input, servings = null) { try { // Smart routing to proper structured endpoints based on input type if (typeof input === 'string') { // Single ingredient - use bulletproof V2 ingredient endpoint return await this.analyzeIngredient(input); } else if (Array.isArray(input)) { if (servings && servings > 1) { // Recipe with servings - use bulletproof V2 recipe endpoint return await this.analyzeRecipe(input, servings); } else { // Multiple ingredients without servings - use bulletproof V2 batch endpoint return await this.analyzeBatch(input); } } else { throw new Error('Input must be a string (ingredient) or array (recipe/batch)'); } } catch (error) { throw error; } } async analyzeIngredient(ingredient, includeVerification = false, verbose = false) { try { const requestData = { ingredient: ingredient }; // Add verbose parameter if requested if (verbose) { requestData.verbose = true; } const response = await this.client.post('/api/v2/nutrition/ingredient', requestData); const result = response.data; // For backward compatibility, ensure verification URL is included if requested if (includeVerification && result.success && result.nutrition?.fdc_id) { result.verification_url = `https://fdc.nal.usda.gov/fdc-app.html#/food-details/${result.nutrition.fdc_id}`; } return result; } catch (error) { throw error; } } async analyzeRecipe(ingredients, servings = 1, verbose = false) { try { const requestData = { ingredients: ingredients, servings: servings }; // Add verbose parameter if requested if (verbose) { requestData.verbose = true; } const response = await this.client.post('/api/v2/nutrition/recipe', requestData); return response.data; } catch (error) { throw error; } } async analyzeBatch(ingredients, verbose = false) { try { // The batch endpoint now supports both array of strings and array of objects const requestData = { ingredients: ingredients }; // Add verbose parameter if requested if (verbose) { requestData.verbose = true; } const response = await this.client.post('/api/v2/nutrition/batch', requestData); return response.data; } catch (error) { throw error; } } async getAccountUsage() { try { const response = await this.client.get('/api/v2/nutrition/account/usage'); // V2 endpoint returns correct format const data = response.data; return { email: data.account?.email || 'Unknown', api_tier: data.account?.api_tier || 'Unknown', subscription_status: data.account?.subscription_status || 'Unknown', usage: data.usage || { current_month: 0, monthly_limit: 1000, remaining: 1000, percentage_used: 0, reset_date: new Date().toISOString(), days_until_reset: 30 }, // Include detailed credit information credits: { total: data.usage?.total_credits || 0, trial: data.usage?.trial_credits || 0, monthly: data.usage?.monthly_credits || 0, paid: data.usage?.paid_credits || 0 } }; } catch (error) { throw error; } } async verifyFdcId(fdcId) { try { const response = await this.client.get(`/api/v2/nutrition/nutrition/verify/${fdcId}`); return response.data; } catch (error) { throw error; } } async healthCheck() { try { // Basic health check (no auth required) const tempHeaders = { ...this.client.defaults.headers }; delete this.client.defaults.headers['X-API-Key']; const response = await this.client.get('/health'); // Restore headers this.client.defaults.headers = tempHeaders; return response.data; } catch (error) { // Restore headers even on error if (this.apiKey) { this.client.defaults.headers['X-API-Key'] = this.apiKey; } throw error; } } } module.exports = { NutritionAPI, ApiError };