UNPKG

suitecrm-mcp-server

Version:

Model Context Protocol server for SuiteCRM integration with natural language SQL reporting

209 lines 7.51 kB
"use strict"; /** * Authentication service for SuiteCRM OAuth 2.0 */ Object.defineProperty(exports, "__esModule", { value: true }); exports.AuthService = void 0; const zod_1 = require("zod"); const types_1 = require("../types"); const http_1 = require("../utils/http"); const logger_1 = require("../utils/logger"); // Validation schemas const AuthCredentialsSchema = zod_1.z.object({ client_id: zod_1.z.string().min(1, 'Client ID is required'), client_secret: zod_1.z.string().min(1, 'Client secret is required') }); const AuthResponseSchema = zod_1.z.object({ access_token: zod_1.z.string().min(1, 'Access token is required'), token_type: zod_1.z.string().default('Bearer'), expires_in: zod_1.z.number().positive('Expires in must be positive'), scope: zod_1.z.string().optional() }); class AuthService { httpClient; tokenCache = new Map(); cacheCleanupInterval; constructor(baseUrl, timeout = 30000) { this.httpClient = new http_1.HttpClient({ baseURL: baseUrl, timeout }); // Clean up expired tokens every 5 minutes this.cacheCleanupInterval = setInterval(() => { this.cleanupExpiredTokens(); }, 5 * 60 * 1000); } /** * Authenticate with SuiteCRM using client credentials flow */ async authenticate(credentials) { try { // Validate input const validatedCredentials = AuthCredentialsSchema.parse(credentials); // Check cache first const cacheKey = this.getCacheKey(validatedCredentials); const cachedToken = this.tokenCache.get(cacheKey); if (cachedToken && !this.isTokenExpired(cachedToken)) { logger_1.logger.info('Using cached token', { clientId: validatedCredentials.client_id }); const response = { access_token: cachedToken.token, token_type: 'Bearer', expires_in: Math.floor((cachedToken.expiresAt - Date.now()) / 1000), }; if (cachedToken.scope) { response.scope = cachedToken.scope; } return response; } // Perform authentication logger_1.logger.info('Authenticating with SuiteCRM', { clientId: validatedCredentials.client_id }); const response = await this.httpClient.post('/Api/access_token', { grant_type: 'client_credentials', client_id: validatedCredentials.client_id, client_secret: validatedCredentials.client_secret }); if (response.status !== 200) { throw new types_1.AuthenticationError(`Authentication failed with status ${response.status}`); } // Validate response const validatedResponse = AuthResponseSchema.parse(response.data); // Cache the token const storedToken = { token: validatedResponse.access_token, expiresAt: Date.now() + (validatedResponse.expires_in * 1000), }; if (validatedResponse.scope) { storedToken.scope = validatedResponse.scope; } this.tokenCache.set(cacheKey, storedToken); logger_1.logger.logAuthentication('SuiteCRM', true); const responseToReturn = { access_token: validatedResponse.access_token, token_type: validatedResponse.token_type, expires_in: validatedResponse.expires_in, }; if (validatedResponse.scope) { responseToReturn.scope = validatedResponse.scope; } return responseToReturn; } catch (error) { if (error instanceof zod_1.z.ZodError) { const validationError = new types_1.ValidationError(`Invalid credentials: ${error.errors.map(e => e.message).join(', ')}`); logger_1.logger.logAuthentication('SuiteCRM', false, validationError); throw validationError; } if (error instanceof types_1.AuthenticationError || error instanceof types_1.ValidationError) { throw error; } const authError = new types_1.AuthenticationError(`Authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`); logger_1.logger.logAuthentication('SuiteCRM', false, authError); throw authError; } } /** * Get a valid token for the given credentials */ async getValidToken(credentials) { const authResponse = await this.authenticate(credentials); return authResponse.access_token; } /** * Check if a token is valid and not expired */ isTokenValid(token) { for (const storedToken of this.tokenCache.values()) { if (storedToken.token === token && !this.isTokenExpired(storedToken)) { return true; } } return false; } /** * Invalidate a token (remove from cache) */ invalidateToken(token) { for (const [key, storedToken] of this.tokenCache.entries()) { if (storedToken.token === token) { this.tokenCache.delete(key); logger_1.logger.info('Token invalidated', { token: this.maskToken(token) }); break; } } } /** * Clear all cached tokens */ clearCache() { const count = this.tokenCache.size; this.tokenCache.clear(); logger_1.logger.info('Token cache cleared', { tokensCleared: count }); } /** * Get cache statistics */ getCacheStats() { let expired = 0; let valid = 0; for (const token of this.tokenCache.values()) { if (this.isTokenExpired(token)) { expired++; } else { valid++; } } return { total: this.tokenCache.size, expired, valid }; } /** * Cleanup expired tokens from cache */ cleanupExpiredTokens() { let cleaned = 0; for (const [key, token] of this.tokenCache.entries()) { if (this.isTokenExpired(token)) { this.tokenCache.delete(key); cleaned++; } } if (cleaned > 0) { logger_1.logger.debug('Cleaned up expired tokens', { count: cleaned }); } } /** * Check if a stored token is expired */ isTokenExpired(token) { // Add 5 minute buffer to prevent edge cases return Date.now() >= (token.expiresAt - (5 * 60 * 1000)); } /** * Generate cache key for credentials */ getCacheKey(credentials) { return `${credentials.client_id}:${credentials.client_secret}`; } /** * Mask token for logging (show only first and last 4 characters) */ maskToken(token) { if (token.length <= 8) { return '***'; } return `${token.substring(0, 4)}...${token.substring(token.length - 4)}`; } /** * Cleanup resources */ destroy() { clearInterval(this.cacheCleanupInterval); this.clearCache(); logger_1.logger.info('AuthService destroyed'); } } exports.AuthService = AuthService; //# sourceMappingURL=auth.js.map