UNPKG

@ronnakamoto/inp-middleware

Version:

INP Protocol middleware for Express.js and Next.js applications - API-driven implementation with automatic metrics integration

309 lines 11.9 kB
"use strict"; /** * INP API Client * * HTTP client for communicating with the INP platform APIs. * This client handles all interactions with the INP platform * without any direct function calls or imports from the platform. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.INPClient = void 0; class INPClient { constructor(config = {}, logger) { this.config = { baseUrl: config.baseUrl || 'https://internetnativepayment.org', ...(config.apiKey ? { apiKey: config.apiKey } : {}), ...(config.timeout !== undefined ? { timeout: config.timeout } : {}), ...(config.retries !== undefined ? { retries: config.retries } : {}) }; this.logger = logger || this.createDefaultLogger(); } /** * Fetch INP discovery endpoint for a project */ async getDiscoveryEndpoint(request) { try { const url = `${this.config.baseUrl}/api/${request.projectId}/.well-known/inp.json`; this.logger.debug('Fetching discovery endpoint', { url, projectId: request.projectId }); const response = await this.makeRequest(url, { method: 'GET', headers: { 'Accept': 'application/json', 'User-Agent': 'inp-middleware/1.0.0' } }); if (!response.ok) { const errorData = await this.parseErrorResponse(response); // Provide more specific error information based on status code let errorMessage = errorData.error || `HTTP ${response.status}: ${response.statusText}`; if (response.status === 404) { errorMessage = `Discovery endpoint not found for project ${request.projectId}`; } else if (response.status === 500) { errorMessage = `Discovery endpoint server error for project ${request.projectId}`; } else if (response.status >= 400 && response.status < 500) { errorMessage = `Discovery endpoint client error (${response.status}): ${errorData.error || response.statusText}`; } else if (response.status >= 500) { errorMessage = `Discovery endpoint server error (${response.status}): ${errorData.error || response.statusText}`; } return { success: false, error: errorMessage, statusCode: response.status }; } const data = await response.json(); this.logger.debug('Discovery endpoint fetched successfully', { projectId: request.projectId, endpointCount: Object.keys(data.endpoints || {}).length }); return { success: true, data }; } catch (error) { this.logger.error('Failed to fetch discovery endpoint', error, { projectId: request.projectId }); return { success: false, error: error instanceof Error ? error.message : 'Unknown error', statusCode: 0 // Network error }; } } /** * Validate payment through the INP platform */ async validatePayment(request) { try { const url = `${this.config.baseUrl}/api/mcp/invoke`; this.logger.debug('Validating payment', { endpointId: request.endpointId, amount: request.payment.amount, currency: request.payment.currency, network: request.payment.network }); const response = await this.makeRequest(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ serviceId: request.endpointId, payment: request.payment }) }); if (response.status === 402) { // Payment required or validation failed const errorData = await this.parseErrorResponse(response); return { isValid: false, error: errorData.error || 'Payment validation failed', details: errorData.details }; } if (!response.ok) { const errorData = await this.parseErrorResponse(response); return { isValid: false, error: errorData.error || `HTTP ${response.status}: ${response.statusText}` }; } // Payment validation successful this.logger.debug('Payment validation successful', { endpointId: request.endpointId }); return { isValid: true, transactionVerified: request.validateTransaction || false }; } catch (error) { this.logger.error('Payment validation failed', error, { endpointId: request.endpointId }); return { isValid: false, error: error instanceof Error ? error.message : 'Unknown error' }; } } /** * Invoke a service through the INP platform */ async invokeService(request) { try { const url = `${this.config.baseUrl}/api/mcp/invoke`; this.logger.debug('Invoking service', { serviceId: request.serviceId, method: request.method, hasPayment: !!request.payment }); const response = await this.makeRequest(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(request) }); const responseData = await response.json(); if (!response.ok) { return { success: false, status: response.status, statusText: response.statusText, error: responseData.error || `HTTP ${response.status}: ${response.statusText}`, data: responseData }; } this.logger.debug('Service invocation successful', { serviceId: request.serviceId, status: response.status }); return { success: true, status: response.status, statusText: response.statusText, data: responseData.data, headers: responseData.headers, metadata: responseData.metadata }; } catch (error) { this.logger.error('Service invocation failed', error, { serviceId: request.serviceId }); return { success: false, status: 500, statusText: 'Internal Error', error: error instanceof Error ? error.message : 'Unknown error' }; } } /** * Check if a project has payment-enabled endpoints */ async checkProjectPaymentStatus(projectId) { try { const url = `${this.config.baseUrl}/api/${projectId}/.well-known/inp.json`; const response = await this.makeRequest(url, { method: 'HEAD', headers: { 'Accept': 'application/json' } }); return response.ok; } catch (error) { this.logger.debug('Project payment status check failed', { projectId, error: error instanceof Error ? error.message : 'Unknown error' }); return false; } } /** * Make HTTP request with retry logic and error handling */ async makeRequest(url, options) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.config.timeout || 30000); const requestOptions = { ...options, signal: controller.signal }; let lastError = null; for (let attempt = 1; attempt <= (this.config.retries || 3); attempt++) { try { const response = await fetch(url, requestOptions); clearTimeout(timeoutId); // Don't retry on client errors (4xx) if (response.status >= 400 && response.status < 500) { return response; } // Don't retry on successful responses if (response.ok) { return response; } // Retry on server errors (5xx) and network errors lastError = new Error(`HTTP ${response.status}: ${response.statusText}`); if (attempt < (this.config.retries || 3)) { const delay = Math.pow(2, attempt) * 1000; // Exponential backoff this.logger.debug(`Request failed, retrying in ${delay}ms`, { attempt, url, status: response.status }); await this.sleep(delay); } } catch (error) { clearTimeout(timeoutId); lastError = error; if (attempt < (this.config.retries || 3)) { const delay = Math.pow(2, attempt) * 1000; this.logger.debug(`Request failed, retrying in ${delay}ms`, { attempt, url, error: lastError.message }); await this.sleep(delay); } } } throw lastError || new Error('Request failed after all retries'); } /** * Parse error response from the INP platform */ async parseErrorResponse(response) { try { const data = await response.json(); return { error: data.error || `HTTP ${response.status}: ${response.statusText}`, code: data.code, timestamp: data.timestamp, details: data.details }; } catch { return { error: `HTTP ${response.status}: ${response.statusText}`, timestamp: new Date().toISOString() }; } } /** * Create default logger */ createDefaultLogger() { return { info: (message, data) => { console.log(`[INP] INFO: ${message}`, data || ''); }, warn: (message, data) => { console.warn(`[INP] WARN: ${message}`, data || ''); }, error: (message, error, data) => { const errorMessage = error?.message || ''; const dataStr = data ? JSON.stringify(data) : ''; console.error(`[INP] ERROR: ${message} - ${errorMessage} ${dataStr}`); }, debug: (message, data) => { if (process.env.NODE_ENV === 'development') { console.log(`[INP] DEBUG: ${message}`, data || ''); } } }; } /** * Sleep utility for retry delays */ sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } exports.INPClient = INPClient; //# sourceMappingURL=inp-client.js.map