@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
JavaScript
;
/**
* 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