/**
* IT Glue API Client
* Handles all HTTP communication with the IT Glue API
*/
import axios, { AxiosInstance, AxiosError } from 'axios';
import { API_BASE_URLS, DEFAULT_REGION, DEFAULT_TIMEOUT, ApiRegion } from '../constants.js';
import { ITGlueApiError } from '../types.js';
// Track if password access is available
let passwordAccessEnabled: boolean | null = null;
export function getPasswordAccessStatus(): boolean | null {
return passwordAccessEnabled;
}
export function setPasswordAccessStatus(enabled: boolean): void {
passwordAccessEnabled = enabled;
}
/**
* Create configured axios instance for IT Glue API
*/
export function createApiClient(): AxiosInstance {
const apiKey = process.env.ITGLUE_API_KEY;
const region = (process.env.ITGLUE_REGION as ApiRegion) || DEFAULT_REGION;
if (!apiKey) {
throw new Error('ITGLUE_API_KEY environment variable is required');
}
const baseURL = API_BASE_URLS[region] || API_BASE_URLS[DEFAULT_REGION];
const client = axios.create({
baseURL,
timeout: DEFAULT_TIMEOUT,
headers: {
'x-api-key': apiKey,
'Content-Type': 'application/vnd.api+json',
'Accept': 'application/vnd.api+json'
}
});
return client;
}
// Singleton API client
let apiClient: AxiosInstance | null = null;
export function getApiClient(): AxiosInstance {
if (!apiClient) {
apiClient = createApiClient();
}
return apiClient;
}
/**
* Make an API request to IT Glue
*/
export async function makeApiRequest<T>(
endpoint: string,
method: 'GET' | 'POST' | 'PATCH' | 'DELETE' = 'GET',
data?: unknown,
params?: Record<string, unknown>
): Promise<T> {
const client = getApiClient();
try {
const response = await client({
method,
url: endpoint,
data,
params
});
return response.data as T;
} catch (error) {
throw error;
}
}
/**
* Handle API errors and return user-friendly messages
*/
export function handleApiError(error: unknown): string {
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError<ITGlueApiError>;
if (axiosError.response) {
const { status, data } = axiosError.response;
// Check for password access denied specifically
if (status === 403 && axiosError.config?.url?.includes('passwords')) {
setPasswordAccessStatus(false);
return 'Error: Password access denied. Your API key does not have permission to access passwords. This is controlled by the "Password Access" setting in IT Glue API key configuration.';
}
switch (status) {
case 401:
return 'Error: Unauthorized. Check your ITGLUE_API_KEY is valid and not expired (keys expire after 90 days of inactivity).';
case 403:
return 'Error: Forbidden. You do not have permission to access this resource.';
case 404:
return 'Error: Resource not found. Verify the ID exists and you have access.';
case 415:
return 'Error: Unsupported Media Type. The Content-Type header must be application/vnd.api+json.';
case 422:
if (data?.errors && data.errors.length > 0) {
const details = data.errors.map(e => e.detail || e.title).join('; ');
return `Error: Validation failed - ${details}`;
}
return 'Error: Unprocessable Entity. Check your request data for invalid values.';
case 429:
return 'Error: Rate limit exceeded (3000 requests per 5 minutes). Please wait before making more requests.';
default:
if (data?.errors && data.errors.length > 0) {
const details = data.errors.map(e => e.detail || e.title).join('; ');
return `Error: API request failed (${status}) - ${details}`;
}
return `Error: API request failed with status ${status}`;
}
} else if (axiosError.code === 'ECONNABORTED') {
return 'Error: Request timed out. The IT Glue API may be slow or unavailable.';
} else if (axiosError.code === 'ENOTFOUND') {
return 'Error: Could not reach IT Glue API. Check your network connection and ITGLUE_REGION setting.';
}
}
return `Error: Unexpected error - ${error instanceof Error ? error.message : String(error)}`;
}
/**
* Build pagination parameters for API requests
*/
export function buildPaginationParams(
page: number = 1,
pageSize: number = 50
): Record<string, number> {
return {
'page[number]': page,
'page[size]': Math.min(pageSize, 1000) // IT Glue max is 1000
};
}
/**
* Build filter parameters for API requests
* IT Glue uses filter[field]=value syntax
*/
export function buildFilterParams(
filters: Record<string, string | number | boolean | undefined>
): Record<string, string | number | boolean> {
const params: Record<string, string | number | boolean> = {};
for (const [key, value] of Object.entries(filters)) {
if (value !== undefined && value !== null && value !== '') {
// Convert snake_case to kebab-case for IT Glue API
const apiKey = key.replace(/_/g, '-');
params[`filter[${apiKey}]`] = value;
}
}
return params;
}
/**
* Build sort parameter for API requests
* IT Glue uses sort=field (ascending) or sort=-field (descending)
*/
export function buildSortParam(
field: string,
descending: boolean = false
): string {
const apiField = field.replace(/_/g, '-');
return descending ? `-${apiField}` : apiField;
}
/**
* Build include parameter for nested resources
*/
export function buildIncludeParam(includes: string[]): string {
return includes.join(',');
}
/**
* Check if the API key has password access
* This makes a test request to the passwords endpoint
*/
export async function checkPasswordAccess(): Promise<boolean> {
// If we already know the status, return it
if (passwordAccessEnabled !== null) {
return passwordAccessEnabled;
}
try {
// Try to fetch just one password to test access
await makeApiRequest('/passwords', 'GET', undefined, {
'page[size]': 1
});
setPasswordAccessStatus(true);
return true;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 403) {
setPasswordAccessStatus(false);
return false;
}
// For other errors, assume access might be available
// (could be rate limit, network issue, etc.)
return true;
}
}