// src/integrations/meraki/client.js import axios from 'axios'; import { logger } from '../../utils/logger.js'; export const merakiAxios = axios.create({ baseURL: 'https://api.meraki.com/api/v1', timeout: 30000, headers: { 'X-Cisco-Meraki-API-Key': process.env.MERAKI_API_KEY, 'Content-Type': 'application/json', }, }); // Request logging merakiAxios.interceptors.request.use(cfg => { logger('meraki:request', `${cfg.method.toUpperCase()} ${cfg.url}`, 'debug'); return cfg; }); // Response error logging + basic 429 info merakiAxios.interceptors.response.use( res => res, err => { const status = err.response?.status; const errors = err.response?.data?.errors || err.response?.data; const retryAfter = err.response?.headers?.['retry-after']; const msg = status ? `${status} - ${JSON.stringify(errors)} ${retryAfter ? `(Retry-After: ${retryAfter}s)` : ''}` : err.message; logger('meraki:error', msg, status === 429 ? 'warn' : 'error'); return Promise.reject(err); } ); /** * Robust fetchAllPages with automatic 429 retry + exponential backoff */ export async function fetchAllPages(baseUrl, maxRetries = 6) { let allResults = []; let nextUrl = baseUrl.includes('?') ? `${baseUrl}&perPage=5000` : `${baseUrl}?perPage=5000`; let attempt = 0; while (nextUrl) { try { logger('meraki:pagination', `Fetching from ${nextUrl}`, 'debug'); const response = await merakiAxios.get(nextUrl); const pageData = response.data || []; allResults = allResults.concat(pageData); // Handle Link header for pagination const linkHeader = response.headers.link; if (linkHeader) { const nextMatch = linkHeader.match(/<([^>]+)>;\s*rel=["']?next["']?/i); nextUrl = nextMatch ? nextMatch[1] : null; } else { nextUrl = null; } logger('meraki:pagination', `Fetched ${pageData.length} items (total now ${allResults.length})`, 'debug'); // Small delay between pages to be kind to the API if (nextUrl) await new Promise(r => setTimeout(r, 120)); } catch (err) { if (err.response?.status === 429 && attempt < maxRetries) { const retryAfter = parseInt(err.response.headers['retry-after']) || Math.pow(2, attempt) * 2; // exponential backoff fallback logger('meraki:rate-limit', `429 Rate limit hit on ${nextUrl} — waiting ${retryAfter}s (attempt ${attempt + 1}/${maxRetries})`, 'warn'); await new Promise(r => setTimeout(r, retryAfter * 1000)); attempt++; continue; // retry the same URL } // Non-429 error or max retries reached logger('meraki:error', `Failed after ${attempt} retries: ${err.message}`, 'error'); throw err; } } // logger('meraki:debug', JSON.stringify(allResults[0], null, 2)); // intentionally disabled logger('meraki:pagination', `Pagination complete – ${allResults.length} total items`, 'debug'); return allResults; } // Export a reusable client instance let merakiClientInstance = null; export async function getMerakiClient() { if (!merakiClientInstance) { // Just return the axios instance we already configured merakiClientInstance = merakiAxios; } return merakiClientInstance; }