UNPKG

@mcp-abap-adt/connection

Version:

ABAP connection layer for MCP ABAP ADT server

182 lines (181 loc) 8.94 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.JwtAbapConnection = void 0; const axios_1 = require("axios"); const AbstractAbapConnection_js_1 = require("./AbstractAbapConnection.js"); /** * JWT Authentication connection for SAP BTP Cloud systems * * Supports automatic token refresh via ITokenRefresher injection: * - If tokenRefresher is provided, 401/403 errors trigger automatic token refresh * - If tokenRefresher is not provided, 401/403 errors throw an error (legacy behavior) */ class JwtAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnection { tokenRefresher; currentToken; constructor(config, logger, sessionId, tokenRefresher) { JwtAbapConnection.validateConfig(config); super(config, logger || null, sessionId); this.tokenRefresher = tokenRefresher; if (!config.jwtToken) { throw new Error('jwtToken is required for JwtAbapConnection'); } this.currentToken = config.jwtToken; } buildAuthorizationHeader() { // Use currentToken which may have been refreshed const tokenPreview = this.currentToken ? `${this.currentToken.substring(0, 10)}...${this.currentToken.substring(Math.max(0, this.currentToken.length - 4))}` : 'null'; this.logger?.debug(`[DEBUG] JwtAbapConnection.buildAuthorizationHeader - Using token: ${tokenPreview}`); return `Bearer ${this.currentToken}`; } /** * Refresh the JWT token using the injected tokenRefresher * @returns true if token was refreshed, false if no refresher available */ async tryRefreshToken() { if (!this.tokenRefresher) { this.logger?.debug(`[DEBUG] JwtAbapConnection - No tokenRefresher available, cannot refresh token`); return false; } try { this.logger?.debug(`[DEBUG] JwtAbapConnection - Refreshing token via tokenRefresher...`); const newToken = await this.tokenRefresher.refreshToken(); this.currentToken = newToken; this.logger?.debug(`[DEBUG] JwtAbapConnection - Token refreshed successfully`); return true; } catch (error) { this.logger?.error(`[ERROR] JwtAbapConnection - Failed to refresh token: ${error instanceof Error ? error.message : String(error)}`); return false; } } /** * Override connect to handle JWT token refresh on errors */ async connect() { const baseUrl = await this.getBaseUrl(); const discoveryUrl = `${baseUrl}/sap/bc/adt/discovery`; this.logger?.debug(`[DEBUG] JwtAbapConnection - Connecting to SAP system: ${discoveryUrl}`); try { // Try to get CSRF token (this will also get cookies) const token = await this.fetchCsrfToken(discoveryUrl, 3, 1000); this.setCsrfToken(token); this.logger?.debug('Successfully connected to SAP system', { hasCsrfToken: !!this.getCsrfToken(), hasCookies: !!this.getCookies(), cookieLength: this.getCookies()?.length || 0, }); } catch (error) { // Handle JWT auth errors (401/403) during connect if (error instanceof axios_1.AxiosError && (error.response?.status === 401 || error.response?.status === 403)) { // Check if this is really an auth error, not a permissions error const responseData = error.response?.data; const responseText = typeof responseData === 'string' ? responseData : JSON.stringify(responseData || ''); // Don't retry on "No Access" errors if (responseText.includes('ExceptionResourceNoAccess') || responseText.includes('No authorization') || responseText.includes('Missing authorization')) { throw error; } // Try to refresh token if tokenRefresher is available if (await this.tryRefreshToken()) { // Retry connect with new token this.logger?.debug(`[DEBUG] JwtAbapConnection - Retrying connect after token refresh...`); return this.connect(); } throw new Error('JWT token has expired. Please re-authenticate.'); } // Re-throw other errors throw error; } } /** * Override makeAdtRequest to handle JWT auth errors with automatic token refresh */ async makeAdtRequest(options) { this.logger?.debug(`[DEBUG] JwtAbapConnection.makeAdtRequest - Starting request: ${options.method} ${options.url}`); try { const response = await super.makeAdtRequest(options); this.logger?.debug(`[DEBUG] JwtAbapConnection.makeAdtRequest - Request succeeded: ${response.status}`); return response; } catch (error) { this.logger?.debug(`[DEBUG] JwtAbapConnection.makeAdtRequest - Request failed: ${error instanceof Error ? error.message : String(error)}`); // Handle JWT auth errors (401/403) if (error instanceof axios_1.AxiosError && (error.response?.status === 401 || error.response?.status === 403)) { this.logger?.debug(`[DEBUG] JwtAbapConnection.makeAdtRequest - Got ${error.response.status}, attempting token refresh...`); // Check if this is really an auth error, not a permissions error const responseData = error.response?.data; const responseText = typeof responseData === 'string' ? responseData : JSON.stringify(responseData || ''); // Don't retry on "No Access" errors - these are permission issues, not auth issues if (responseText.includes('ExceptionResourceNoAccess') || responseText.includes('No authorization') || responseText.includes('Missing authorization')) { throw error; } // Try to refresh token if tokenRefresher is available if (await this.tryRefreshToken()) { // Reset connection state and retry request with new token this.logger?.debug(`[DEBUG] JwtAbapConnection.makeAdtRequest - Retrying request after token refresh...`); this.reset(); return super.makeAdtRequest(options); } throw new Error('JWT token has expired. Please re-authenticate.'); } throw error; } } /** * Override fetchCsrfToken to handle JWT auth errors with automatic token refresh */ async fetchCsrfToken(url, retryCount = 3, retryDelay = 1000) { try { // Try to fetch CSRF token using parent implementation return await super.fetchCsrfToken(url, retryCount, retryDelay); } catch (error) { // Handle JWT auth errors (401/403) during CSRF token fetch if (error instanceof axios_1.AxiosError && (error.response?.status === 401 || error.response?.status === 403)) { // Check if this is really an auth error, not a permissions error const responseData = error.response?.data; const responseText = typeof responseData === 'string' ? responseData : JSON.stringify(responseData || ''); // Don't retry on "No Access" errors if (responseText.includes('ExceptionResourceNoAccess') || responseText.includes('No authorization') || responseText.includes('Missing authorization')) { throw error; } // Try to refresh token if tokenRefresher is available if (await this.tryRefreshToken()) { // Retry CSRF token fetch with new token this.logger?.debug(`[DEBUG] JwtAbapConnection.fetchCsrfToken - Retrying after token refresh...`); return super.fetchCsrfToken(url, retryCount, retryDelay); } throw new Error('JWT token has expired. Please re-authenticate.'); } // Re-throw other errors throw error; } } static validateConfig(config) { if (config.authType !== 'jwt') { throw new Error(`JWT connection expects authType "jwt", got "${config.authType}"`); } if (!config.jwtToken) { throw new Error('JWT authentication requires SAP_JWT_TOKEN to be provided'); } } } exports.JwtAbapConnection = JwtAbapConnection;