import axios from "axios";
import { loadConfig } from "./config.js";
import logger from "./logger.js";
const SENSITIVE_PATTERNS = [
/api[-_]?key/i,
/token/i,
/secret/i,
/password/i,
/auth/i,
/credit/i,
/card/i,
];
function redactSensitiveData(data) {
if (!data || typeof data !== 'object') return data;
if (Array.isArray(data)) {
return data.map(redactSensitiveData);
}
const redacted = { ...data };
for (const key of Object.keys(redacted)) {
if (SENSITIVE_PATTERNS.some(pattern => pattern.test(key))) {
redacted[key] = '***REDACTED***';
} else if (typeof redacted[key] === 'object') {
redacted[key] = redactSensitiveData(redacted[key]);
}
}
return redacted;
}
/**
* Enhanced API Client with monitoring, retries, and circuit breaking
*/
class ApiClient {
constructor() {
this.config = loadConfig();
this.axiosInstance = axios.create({
baseURL: this.config.baseUrl,
timeout: this.config.timeout,
headers: {
"X-Source": "presearch-mcp-server",
Accept: "application/json",
},
});
this.rateLimit = {
remaining: 60,
reset: Date.now() + 60000,
total: 60,
};
// Request interceptor for auth and logging
this.axiosInstance.interceptors.request.use(
(config) => {
// Dynamic config loading to pick up runtime changes
const currentConfig = loadConfig();
// Only set if not already set (allows overrides)
if (currentConfig.apiKey && !config.headers["Authorization"]) {
config.headers["Authorization"] = `Bearer ${currentConfig.apiKey}`;
}
logger.debug(`API Request: ${config.method?.toUpperCase()} ${config.url}`, {
params: redactSensitiveData(config.params),
});
return config;
},
(error) => {
logger.error("API Request Error", { error: error.message });
return Promise.reject(error);
},
);
// Response interceptor for rate limiting and logging
this.axiosInstance.interceptors.response.use(
(response) => {
this.updateRateLimit(response.headers);
logger.debug(`API Response: ${response.status}`, {
url: response.config.url,
dataLength: JSON.stringify(response.data).length,
});
return response;
},
async (error) => {
this.updateRateLimit(error.response?.headers);
if (error.response) {
logger.error(
`API Error: ${error.response.status} ${error.response.statusText}`,
{
data: redactSensitiveData(error.response.data),
url: error.config?.url,
},
);
// Enhanced handling for 402 Payment Required
if (error.response.status === 402) {
const paymentError = new Error(
"PRESEARCH API PAYMENT REQUIRED: Your account has insufficient credits. " +
"Please visit https://presearch.com/account/tokens to top up your account or check your plan."
);
paymentError.name = "PaymentRequiredError";
paymentError.status = 402;
return Promise.reject(paymentError);
}
} else {
logger.error("Network Error", { error: error.message });
}
// Implement simple exponential backoff for 429 and 5xx
const config = error.config;
if (
config &&
!config.__isRetryRequest &&
(error.response?.status === 429 || error.response?.status >= 500)
) {
config.__isRetryRequest = true;
config.__retryCount = config.__retryCount || 0;
if (config.__retryCount < this.config.retries) {
config.__retryCount++;
const delay = Math.pow(2, config.__retryCount) * 1000;
logger.info(
`Retrying request (${config.__retryCount}/${this.config.retries}) in ${delay}ms...`,
);
await new Promise((resolve) => setTimeout(resolve, delay));
return this.axiosInstance(config);
}
}
return Promise.reject(error);
},
);
}
/**
* Update rate limit tracking from response headers
*/
updateRateLimit(headers) {
if (!headers) return;
if (headers["x-ratelimit-remaining"]) {
this.rateLimit.remaining = parseInt(headers["x-ratelimit-remaining"], 10);
}
if (headers["x-ratelimit-reset"]) {
this.rateLimit.reset = parseInt(headers["x-ratelimit-reset"], 10) * 1000;
}
if (headers["x-ratelimit-limit"]) {
this.rateLimit.total = parseInt(headers["x-ratelimit-limit"], 10);
}
}
/**
* Check if we are rate limited locally before making a request
*/
checkRateLimit() {
if (this.rateLimit.remaining <= 0 && Date.now() < this.rateLimit.reset) {
const waitTime = Math.ceil((this.rateLimit.reset - Date.now()) / 1000);
throw new Error(
`Rate limit exceeded. Please try again in ${waitTime} seconds.`,
);
}
}
/**
* Make a GET request
*/
async get(url, config = {}) {
this.checkRateLimit();
return this.axiosInstance.get(url, config);
}
/**
* Make a POST request
*/
async post(url, data, config = {}) {
this.checkRateLimit();
return this.axiosInstance.post(url, data, config);
}
/**
* Get current rate limit stats
*/
getRateLimitStats() {
return {
...this.rateLimit,
resetDate: new Date(this.rateLimit.reset).toISOString(),
};
}
}
export const apiClient = new ApiClient();