UNPKG

finops-mcp-server

Version:

MCP server for FinOps Center cost optimization integration

1 lines 7.88 kB
export class SecureCredentialHandler{constructor(e){this.credentials={},this.cleanupHandlers=[],this.logger=e,this.setupProcessCleanup()}storeApiKey(e,t){if(!e)throw new Error("API key cannot be empty");if(!this.isValidApiKey(e))throw new Error('Invalid API key format. Must start with "finops_" and be at least 20 characters long');this.credentials.apiKey=e,t&&(this.credentials.expiresAt=t),this.logger?.debug("API key stored securely in memory",{operation:"store_api_key",keyPrefix:this.redactCredential(e),expiresAt:t?.toISOString()})}storeBearerToken(e,t){if(!e)throw new Error("Bearer token cannot be empty");this.credentials.bearerToken=e,t&&(this.credentials.expiresAt=t),this.logger?.debug("Bearer token stored securely in memory",{operation:"store_bearer_token",tokenPrefix:this.redactCredential(e),expiresAt:t?.toISOString()})}storeCustomToken(e,t){if(!e)throw new Error("Custom token cannot be empty");this.credentials.customToken=e,t&&(this.credentials.expiresAt=t),this.logger?.debug("Custom token stored securely in memory",{operation:"store_custom_token",tokenPrefix:this.redactCredential(e),expiresAt:t?.toISOString()})}getApiKey(){return this.validateCredentialExpiry(),this.credentials.apiKey}getBearerToken(){return this.validateCredentialExpiry(),this.credentials.bearerToken}getCustomToken(){return this.validateCredentialExpiry(),this.credentials.customToken}isExpired(){return!!this.credentials.expiresAt&&new Date>this.credentials.expiresAt}getExpiryTime(){return this.credentials.expiresAt}clearCredentials(){this.credentials.apiKey&&(this.credentials.apiKey=this.generateRandomString(this.credentials.apiKey.length)),this.credentials.bearerToken&&(this.credentials.bearerToken=this.generateRandomString(this.credentials.bearerToken.length)),this.credentials.customToken&&(this.credentials.customToken=this.generateRandomString(this.credentials.customToken.length)),this.credentials={},this.logger?.info("All credentials cleared from memory",{operation:"clear_credentials"})}redactCredential(e){if(!e||e.length<8)return"[REDACTED]";const t=e.substring(0,4),r=e.substring(e.length-4);return`${t}${"*".repeat(Math.max(0,e.length-8))}${r}`}isValidApiKey(e){return e.startsWith("finops_")&&e.length>=20}validateCredentialExpiry(){if(this.isExpired()){this.clearCredentials();const e=new Error("Credentials have expired");throw e.code="EXPIRED_TOKEN",e.details={expiresAt:this.credentials.expiresAt?.toISOString()},e}}generateRandomString(e){let t="";for(let r=0;r<e;r++)t+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".charAt(Math.floor(62*Math.random()));return t}setupProcessCleanup(){const e=()=>{this.logger?.info("Process termination detected, clearing credentials",{operation:"process_cleanup"}),this.clearCredentials()};this.cleanupHandlers.push(e),"test"===process.env.NODE_ENV||process.env.JEST_WORKER_ID?this.logger?.debug("Test environment detected, skipping process event registration",{operation:"setup_cleanup_handlers",handlerCount:this.cleanupHandlers.length}):(process.on("exit",e),process.on("SIGINT",e),process.on("SIGTERM",e),process.on("SIGUSR1",e),process.on("SIGUSR2",e),process.on("uncaughtException",t=>{this.logger?.error("Uncaught exception, clearing credentials",t,{operation:"uncaught_exception_cleanup"}),e()}),process.on("unhandledRejection",t=>{this.logger?.error("Unhandled rejection, clearing credentials",t,{operation:"unhandled_rejection_cleanup"}),e()}),this.logger?.debug("Process cleanup handlers registered",{operation:"setup_cleanup_handlers",handlerCount:this.cleanupHandlers.length}))}triggerCleanup(){this.cleanupHandlers.forEach(handler=>handler())}}export class AuthHeaderManager{constructor(e,t){this.credentialHandler=e,this.logger=t}generateHeaders(e){const t={"Content-Type":"application/json",Accept:"application/json","User-Agent":"FinOps-MCP-Server/1.0.0"};try{const r=e||this.detectAuthenticationMethod();switch(r){case"bearer":this.addBearerTokenHeader(t);break;case"api-key":this.addApiKeyHeader(t);break;case"custom":this.addCustomTokenHeader(t);break;default:throw new Error(`Unsupported authentication method: ${r}`)}return this.logger?.debug("Authentication headers generated",{operation:"generate_headers",method:r,headerCount:Object.keys(t).length}),t}catch(t){const r=new Error(`Failed to generate authentication headers: ${t instanceof Error?t.message:String(t)}`);throw r.code="AUTHENTICATION_FAILED",r.details={method:e,error:t instanceof Error?t.message:String(t)},this.logger?.error("Failed to generate authentication headers",r,{operation:"generate_headers_error",method:e||"auto-detect"}),r}}addBearerTokenHeader(e){const t=this.credentialHandler.getBearerToken();if(!t)throw new Error("Bearer token not available");e.Authorization=`Bearer ${t}`}addApiKeyHeader(e){const t=this.credentialHandler.getApiKey();if(!t)throw new Error("API key not available");t.startsWith("finops_")?e.Authorization=t:e["x-api-key"]=t}addCustomTokenHeader(e){const t=this.credentialHandler.getCustomToken();if(!t)throw new Error("Custom token not available");e.Authorization=t}detectAuthenticationMethod(){if(this.credentialHandler.getBearerToken())return"bearer";if(this.credentialHandler.getApiKey())return"api-key";if(this.credentialHandler.getCustomToken())return"custom";throw new Error("No authentication credentials available")}validateHeaders(e){const t=e.Authorization||e["x-api-key"],r=e["Content-Type"];return Boolean(t&&r)}async handleAuthError(e,t){if(this.logger?.warn("Authentication error detected",{operation:"handle_auth_error",errorCode:e.code,errorMessage:e.message}),this.credentialHandler.isExpired()){this.logger?.error("Credentials have expired, clearing from memory",new Error("Credentials expired"),{operation:"handle_expired_credentials",expiresAt:this.credentialHandler.getExpiryTime()?.toISOString()}),this.credentialHandler.clearCredentials();const t=new Error("Authentication credentials have expired");throw t.code="EXPIRED_TOKEN",t.details={originalError:e.message,expiresAt:this.credentialHandler.getExpiryTime()?.toISOString()},t}if(401===e.status||403===e.status||401===e.statusCode||403===e.statusCode){this.logger?.error("Invalid credentials detected, clearing from memory",new Error("Invalid credentials"),{operation:"handle_invalid_credentials",statusCode:e.status||e.statusCode}),this.credentialHandler.clearCredentials();const t=new Error("Invalid authentication credentials");throw t.code="INVALID_CREDENTIALS",t.details={originalError:e.message,statusCode:e.status||e.statusCode},t}throw e}}export class AuthManager{constructor(e){this.logger=e,this.credentialHandler=new SecureCredentialHandler(e),this.headerManager=new AuthHeaderManager(this.credentialHandler,e)}initializeWithApiKey(e,t){this.credentialHandler.storeApiKey(e,t),this.logger?.info("Authentication initialized with API key",{operation:"initialize_api_key",keyPrefix:this.credentialHandler.redactCredential(e),expiresAt:t?.toISOString()})}initializeWithBearerToken(e,t){this.credentialHandler.storeBearerToken(e,t),this.logger?.info("Authentication initialized with Bearer token",{operation:"initialize_bearer_token",tokenPrefix:this.credentialHandler.redactCredential(e),expiresAt:t?.toISOString()})}initializeWithCustomToken(e,t){this.credentialHandler.storeCustomToken(e,t),this.logger?.info("Authentication initialized with custom token",{operation:"initialize_custom_token",tokenPrefix:this.credentialHandler.redactCredential(e),expiresAt:t?.toISOString()})}getAuthHeaders(e){return this.headerManager.generateHeaders(e)}async handleAuthError(e,t){return this.headerManager.handleAuthError(e,t)}isExpired(){return this.credentialHandler.isExpired()}getExpiryTime(){return this.credentialHandler.getExpiryTime()}clearCredentials(){this.credentialHandler.clearCredentials()}getCredentialHandler(){return this.credentialHandler}getHeaderManager(){return this.headerManager}}