UNPKG

okta-mcp-server

Version:

Model Context Protocol (MCP) server for Okta API operations with support for bulk operations and caching

375 lines 13.1 kB
/** * Okta-specific Circuit Breaker implementation with smart retry logic * Simplified version that handles errors correctly */ import { EventEmitter } from 'events'; import { CircuitState, } from './types.js'; /** * Okta-specific error codes and their handling strategies */ export var OktaErrorCode; (function (OktaErrorCode) { // Rate limit - should wait and retry OktaErrorCode[OktaErrorCode["RATE_LIMIT"] = 429] = "RATE_LIMIT"; // Server errors - count as failures OktaErrorCode[OktaErrorCode["BAD_GATEWAY"] = 502] = "BAD_GATEWAY"; OktaErrorCode[OktaErrorCode["SERVICE_UNAVAILABLE"] = 503] = "SERVICE_UNAVAILABLE"; OktaErrorCode[OktaErrorCode["GATEWAY_TIMEOUT"] = 504] = "GATEWAY_TIMEOUT"; // Client errors - should not trigger circuit OktaErrorCode[OktaErrorCode["UNAUTHORIZED"] = 401] = "UNAUTHORIZED"; OktaErrorCode[OktaErrorCode["FORBIDDEN"] = 403] = "FORBIDDEN"; OktaErrorCode[OktaErrorCode["NOT_FOUND"] = 404] = "NOT_FOUND"; // Success codes OktaErrorCode[OktaErrorCode["OK"] = 200] = "OK"; OktaErrorCode[OktaErrorCode["CREATED"] = 201] = "CREATED"; OktaErrorCode[OktaErrorCode["NO_CONTENT"] = 204] = "NO_CONTENT"; })(OktaErrorCode || (OktaErrorCode = {})); /** * Simple Okta-specific circuit breaker implementation */ export class OktaCircuitBreaker { state = CircuitState.CLOSED; failures = 0; lastFailureTime; nextAttempt = 0; cache; options; eventEmitter; // Stats totalRequests = 0; totalFailures = 0; totalSuccesses = 0; totalRejections = 0; constructor(options) { this.options = { failureThreshold: options.failureThreshold, resetTimeout: options.resetTimeout, timeout: options.timeout ?? 0, name: options.name ?? 'OktaCircuitBreaker', cache: options.cache, cacheTTL: options.cacheTTL, enableCacheFallback: options.enableCacheFallback ?? true, maxRetries: options.maxRetries ?? 3, initialRetryDelay: options.initialRetryDelay ?? 1000, maxRetryDelay: options.maxRetryDelay ?? 30000, retryMultiplier: options.retryMultiplier ?? 2, useJitter: options.useJitter ?? true, }; this.cache = options.cache; this.eventEmitter = new EventEmitter(); this.eventEmitter.setMaxListeners(0); } /** * Execute a function with Okta-specific retry logic */ async execute(fn, ...args) { return this.executeWithRetry(fn, args, 0); } /** * Execute with exponential backoff retry for rate limits */ async executeWithRetry(fn, args, attempt) { this.totalRequests++; // Check if circuit is open if (this.isOpen()) { this.totalRejections++; if (this.options.enableCacheFallback && this.cache) { try { return await this.cacheFallback(...args); } catch (cacheError) { // Fall through to throw circuit open error } } throw new Error(`Circuit breaker is OPEN for ${this.options.name}`); } try { // Execute the function const result = await fn(...args); // Record success this.recordSuccess(); return result; } catch (error) { const oktaError = error; // Determine if this is a failure const isFailure = this.isOktaFailure(oktaError); if (isFailure) { this.recordFailure(); } // Handle rate limit errors with smart retry if (this.isRateLimitError(oktaError) && attempt < this.options.maxRetries) { const delay = this.calculateRetryDelay(oktaError, attempt); // Emit retry event this.eventEmitter.emit('okta-retry', { error: oktaError, attempt: attempt + 1, delay, remaining: oktaError.headers?.['x-rate-limit-remaining'], reset: oktaError.headers?.['x-rate-limit-reset'], }); // Wait before retry await this.sleep(delay); // Retry the operation return this.executeWithRetry(fn, args, attempt + 1); } // Always throw the original error throw error; } } /** * Record a successful execution */ recordSuccess() { this.totalSuccesses++; this.failures = 0; if (this.state === CircuitState.HALF_OPEN) { this.transitionTo(CircuitState.CLOSED); } } /** * Record a failed execution */ recordFailure() { this.totalFailures++; this.failures++; this.lastFailureTime = Date.now(); if (this.state === CircuitState.HALF_OPEN) { this.transitionTo(CircuitState.OPEN); } else if (this.state === CircuitState.CLOSED && this.failures >= this.options.failureThreshold) { this.transitionTo(CircuitState.OPEN); } } /** * Transition to a new state */ transitionTo(newState) { const oldState = this.state; this.state = newState; if (newState === CircuitState.OPEN) { this.nextAttempt = Date.now() + this.options.resetTimeout; } this.eventEmitter.emit('state-change', oldState, newState, this.getStats()); } /** * Determine if an error should count as a circuit breaker failure */ isOktaFailure(error) { const oktaError = error; // Network errors always count as failures if (this.isNetworkError(oktaError)) { return true; } // Check status code if available if (oktaError.status) { // Rate limits should not count as failures if (oktaError.status === OktaErrorCode.RATE_LIMIT) { return false; } // Client errors (4xx) except 429 should not trigger circuit if (oktaError.status >= 400 && oktaError.status < 500) { return false; } // Server errors (5xx) count as failures if (oktaError.status >= 500) { return true; } } // Default to counting as failure return true; } /** * Check if error is a rate limit error */ isRateLimitError(error) { return (error.status === OktaErrorCode.RATE_LIMIT || error.code === 'E0000047' || // Okta rate limit error code error.errorCode === 'E0000047'); } /** * Check if error is a network error */ isNetworkError(error) { return (error.message?.includes('ECONNREFUSED') || error.message?.includes('ETIMEDOUT') || error.message?.includes('ENOTFOUND') || error.message?.includes('ENETUNREACH') || error.message?.includes('EAI_AGAIN') || error.message?.includes('fetch failed')); } /** * Calculate retry delay with exponential backoff and jitter */ calculateRetryDelay(error, attempt) { // Check if server provided retry-after header if (error.headers?.['retry-after']) { const retryAfter = parseInt(error.headers['retry-after'], 10); if (!isNaN(retryAfter)) { return retryAfter * 1000; // Convert to milliseconds } } // Check rate limit reset time if (error.headers?.['x-rate-limit-reset']) { const resetTime = parseInt(error.headers['x-rate-limit-reset'], 10); if (!isNaN(resetTime)) { const now = Math.floor(Date.now() / 1000); const waitTime = Math.max(0, resetTime - now) * 1000; if (waitTime > 0 && waitTime < this.options.maxRetryDelay) { return waitTime; } } } // Calculate exponential backoff let delay = Math.min(this.options.initialRetryDelay * Math.pow(this.options.retryMultiplier, attempt), this.options.maxRetryDelay); // Add jitter to prevent thundering herd if (this.options.useJitter) { delay = delay * (0.5 + Math.random() * 0.5); } return Math.floor(delay); } /** * Cache fallback for read operations */ async cacheFallback(...args) { if (!this.cache) { throw new Error('Circuit breaker is OPEN and no cache available'); } // Generate cache key from arguments const cacheKey = `okta-cb:${JSON.stringify(args)}`; // Try to get from cache const cachedValue = await this.cache.get(cacheKey); if (cachedValue !== undefined) { // Emit cache hit event this.eventEmitter.emit('okta-cache-fallback', { key: cacheKey, hit: true, }); return cachedValue; } // Emit cache miss event this.eventEmitter.emit('okta-cache-fallback', { key: cacheKey, hit: false, }); throw new Error('Circuit breaker is OPEN and no cached data available'); } /** * Sleep for specified milliseconds */ sleep(ms) { if (ms <= 0) { return Promise.resolve(); } return new Promise((resolve) => setTimeout(resolve, ms)); } // ICircuitBreaker interface implementation getState() { return this.state; } getStats() { return { state: this.state, failures: this.totalFailures, successes: this.totalSuccesses, rejections: this.totalRejections, totalRequests: this.totalRequests, consecutiveFailures: this.failures, consecutiveSuccesses: 0, lastFailureTime: this.lastFailureTime, lastSuccessTime: undefined, nextAttempt: this.nextAttempt > Date.now() ? this.nextAttempt : undefined, failureRate: this.totalRequests > 0 ? (this.totalFailures / this.totalRequests) * 100 : 0, rollingCountFailure: 0, rollingCountSuccess: 0, rollingCountTimeout: 0, rollingCountRejected: 0, }; } open() { if (this.state !== CircuitState.OPEN) { this.transitionTo(CircuitState.OPEN); } } close() { if (this.state !== CircuitState.CLOSED) { this.transitionTo(CircuitState.CLOSED); } } reset() { this.state = CircuitState.CLOSED; this.failures = 0; this.lastFailureTime = undefined; this.nextAttempt = 0; this.totalRequests = 0; this.totalFailures = 0; this.totalSuccesses = 0; this.totalRejections = 0; } isOpen() { if (this.state === CircuitState.CLOSED) { return false; } if (this.state === CircuitState.OPEN) { // Check if we should transition to HALF_OPEN if (Date.now() >= this.nextAttempt) { this.transitionTo(CircuitState.HALF_OPEN); return false; } return true; } // HALF_OPEN state allows requests return false; } getEventEmitter() { return this.eventEmitter; } async healthCheck() { return this.getStats(); } } /** * Factory function to create an Okta circuit breaker */ export function createOktaCircuitBreaker(options) { return new OktaCircuitBreaker(options); } /** * Wrapper function to wrap any async function with Okta circuit breaker */ export function withOktaCircuitBreaker(fn, options) { const circuitBreaker = createOktaCircuitBreaker({ failureThreshold: 5, resetTimeout: 60000, name: fn.name || 'okta-wrapped-function', ...options, }); return ((...args) => circuitBreaker.execute(fn, ...args)); } /** * Higher-order function to create a cached Okta API method */ export function createCachedOktaMethod(method, cache, options) { const wrappedMethod = withOktaCircuitBreaker(method, { cache, enableCacheFallback: true, ...options, }); // Create a function that also caches successful results return (async (...args) => { const cacheKey = `okta-method:${method.name}:${JSON.stringify(args)}`; try { // Execute through circuit breaker const result = await wrappedMethod(...args); // Cache successful results for fallback await cache.set(cacheKey, result, { ttl: 300 }); // 5 minutes TTL return result; } catch (error) { // Circuit breaker will handle fallback throw error; } }); } //# sourceMappingURL=okta-circuit-breaker.js.map