suitecrm-mcp-server
Version:
Model Context Protocol server for SuiteCRM integration with natural language SQL reporting
209 lines • 7.51 kB
JavaScript
/**
* 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
;