UNPKG

@civic/hub-bridge

Version:

Stdio <-> HTTP/SSE MCP bridge with Civic auth handling

122 lines 5.78 kB
/** * serviceAuthorization.ts * * Handles service authorization flows and job continuation * as described in section 4.5 of the Civic Hub Bridge spec. */ import open from 'open'; import { logger } from "../utils/index.js"; import * as config from "../config/index.js"; const isAuthorizationRequiredResponse = (result) => { return (result.content?.length === 3 && result.content[0].type === 'text' && result.content[1].type === 'resource' && result.content[2].type === 'resource' && result.content[1].resource.name === 'authorization_url' && result.content[2].resource.name === 'continue_job_id'); }; /** * Checks if a tools/call response contains authorization and job continuation info * @param result The response to check * @returns Object with auth URL and job ID, or null if not found */ const detectServiceAuthorizationFlow = (result) => { logger.debug("Checking for service authorization flow in response"); // Ensure we have a valid content array with at least two items if (!isAuthorizationRequiredResponse(result)) return null; logger.info("Service authorization flow detected"); // After type guard, result is AuthorizationRequiredResponse // TypeScript now knows the exact structure of the content array const typedResult = result; const authUrl = typedResult.content[1].resource.text; const continueJobId = typedResult.content[2].resource.text; logger.debug(`Authorization URL: ${authUrl}`); logger.debug(`Continue job ID: ${continueJobId}`); return { authUrl, continueJobId }; }; /** * Class to handle the service authorization process * Instantiated per request flow */ export class ServiceAuthorizationHandler { continueJobCallback; maxRetries = 10; // max retry time = 10*5s = 50s initialWaitMs = 5000; // + 5 = 55s (total timeout) retryIntervalMs = 5000; /** * Create a new ServiceAuthorizationHandler * @param continueJobCallback Callback function to call continue_job with a jobId */ constructor(continueJobCallback) { this.continueJobCallback = continueJobCallback; } /** * Main method to handle service authorization - assumes auth is already detected * @param response The MCPResponse that requires authorization * @returns The final response after auth flow completion */ async handleServiceAuthorization(response) { // Extract auth info from the response (caller has already verified this is an auth response) const authInfo = detectServiceAuthorizationFlow(response); if (!authInfo) { throw new Error("handleServiceAuthorization called on response that doesn't require auth"); } const { authUrl, continueJobId } = authInfo; logger.info(`[ServiceAuthHandler] Starting authorization flow for job ${continueJobId}`); // If NO_AUTH_CAPTURE is enabled, skip the authorization flow and just return the original response if (config.NO_AUTH_CAPTURE) { logger.info(`[ServiceAuthHandler] NO_AUTH_CAPTURE is enabled, skipping authorization flow`); logger.info(`[ServiceAuthHandler] Authorization URL that would have been opened: ${authUrl}`); return response; } // 1. Open the authorization URL in browser logger.info(`[ServiceAuthHandler] Opening authorization URL in browser`); await open(authUrl); // 2. Wait for the initial period to allow user to complete auth logger.info(`[ServiceAuthHandler] Waiting ${this.initialWaitMs}ms for user to complete authorization`); await new Promise(resolve => setTimeout(resolve, this.initialWaitMs)); // 3. Poll for completion logger.info(`[ServiceAuthHandler] Beginning to poll for authorization completion`); return await this.pollForCompletion(continueJobId); } /** * Poll the server to check if authorization is complete * @param jobId The job ID to continue * @returns The final MCPResponse from the server */ async pollForCompletion(jobId) { let retryCount = 0; let lastResult = null; while (retryCount < this.maxRetries) { try { logger.info(`[ServiceAuthHandler] Poll attempt ${retryCount + 1}/${this.maxRetries}`); // Call the continue job callback with the job ID const result = await this.continueJobCallback(jobId); lastResult = result; // Check if the result still requires authorization const authInfo = detectServiceAuthorizationFlow(result); if (!authInfo) { // No authUrl found - authorization is complete logger.info(`[ServiceAuthHandler] Authorization completed successfully after ${retryCount + 1} attempts`); return result; } // Still needs authorization, wait and retry logger.info(`[ServiceAuthHandler] Authorization still in progress, waiting ${this.retryIntervalMs}ms before next attempt`); await new Promise(resolve => setTimeout(resolve, this.retryIntervalMs)); retryCount++; } catch (error) { logger.error(`[ServiceAuthHandler] Error during polling:`, error); throw error; } } // Max retries exceeded if (!lastResult) { throw new Error("No response received after maximum retries"); } logger.warn(`[ServiceAuthHandler] Max retry count (${this.maxRetries}) exceeded, returning last response`); return lastResult; } } //# sourceMappingURL=service-authorization.js.map