@civic/hub-bridge
Version:
Stdio <-> HTTP/SSE MCP bridge with Civic auth handling
122 lines • 5.78 kB
JavaScript
/**
* 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