UNPKG

@ronnakamoto/inp-middleware

Version:

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

416 lines 18.9 kB
"use strict"; /** * INP Middleware Index * * Main exports for the INP middleware library */ Object.defineProperty(exports, "__esModule", { value: true }); exports.INPConfigError = exports.INPClientError = exports.INPDiscoveryError = exports.INPPaymentError = exports.INPValidationError = exports.INPError = exports.INPClient = void 0; exports.inpMiddleware = inpMiddleware; const inp_client_1 = require("../client/inp-client"); const errors_1 = require("../errors"); // Publish metrics event (fire-and-forget) async function publishMetricsEvent(event, options) { try { // Use the existing INP client to publish metrics - no need for separate credentials const clientConfig = { baseUrl: options.inpPlatformUrl || 'https://internetnativepayment.org', timeout: options.timeout, retries: options.retries }; if (options.apiKey) { clientConfig.apiKey = options.apiKey; } const client = new inp_client_1.INPClient(clientConfig); // Publish metrics through the INP platform's standard API const response = await client.invokeService({ serviceId: 'metrics-ingest', method: 'POST', url: '/api/metrics/events', headers: { 'Content-Type': 'application/json' }, body: event }); if (!response.success) { console.debug(`[INP] Metrics event not published: ${response.error}`); } else { console.debug('[INP] Metrics event published successfully'); } } catch (error) { // Don't throw - metrics publishing should not affect the main request console.debug('[INP] Error publishing metrics event:', error); } } /** * Unified INP Middleware * * A single middleware function that handles both service discovery and payment validation. * It automatically discovers the service configuration and validates payments based on the endpoint requirements. */ function inpMiddleware(options) { const clientConfig = { baseUrl: options.inpPlatformUrl || 'https://internetnativepayment.org', timeout: options.timeout, retries: options.retries }; if (options.apiKey) { clientConfig.apiKey = options.apiKey; } const client = new inp_client_1.INPClient(clientConfig); return async (req, res, next) => { // Track request start time for latency calculation req._startTime = Date.now(); try { // Log the incoming request for debugging if (options.logErrors !== false) { console.debug(`[INP] Processing request:`, { method: req.method, path: req.path, url: req.url, originalUrl: req.originalUrl, headers: req.headers, body: req.body }); } // Step 1: Discover service configuration const discoveryResult = await client.getDiscoveryEndpoint({ projectId: options.projectId }); // If discovery fails, treat it as no payment endpoints configured yet // This allows existing APIs to work without INP configuration if (!discoveryResult.success) { // Log the discovery failure for debugging, but don't block the request if (options.logErrors !== false) { const statusInfo = discoveryResult.statusCode ? ` (HTTP ${discoveryResult.statusCode})` : ''; console.warn(`[INP] Discovery failed for project ${options.projectId}: ${discoveryResult.error}${statusInfo}. Proceeding without payment validation.`); } // Set empty discovery data to indicate no payment endpoints req.inpDiscovery = { version: '1.0', service_info: { name: 'Unknown Service', base_url: '' }, endpoints: {} }; // Continue to the next middleware/route handler next(); return; } const discovery = discoveryResult.data; req.inpDiscovery = discovery; // Step 2: Find the specific endpoint configuration // Get the request path - handle different Express/Next.js contexts const requestPath = req.path || req.url || req.originalUrl || ''; const endpointPath = requestPath.replace(/^\//, ''); // Remove leading slash // Log for debugging if (options.logErrors !== false) { console.debug(`[INP] Looking for endpoint configuration`, { requestPath, endpointPath, availableEndpoints: Object.keys(discovery.endpoints), discoveryEndpoints: discovery.endpoints }); } let endpointConfig = discovery.endpoints[endpointPath]; // If not found without slash, try with slash if (!endpointConfig) { endpointConfig = discovery.endpoints[`/${endpointPath}`]; if (options.logErrors !== false && endpointConfig) { console.debug(`[INP] Found endpoint config with leading slash: /${endpointPath}`); } } // If still not found, try the original path if (!endpointConfig) { endpointConfig = discovery.endpoints[requestPath]; if (options.logErrors !== false && endpointConfig) { console.debug(`[INP] Found endpoint config with original path: ${requestPath}`); } } // If still not found, try matching against the full URL path if (!endpointConfig) { const urlPath = new URL(requestPath, 'http://localhost').pathname; endpointConfig = discovery.endpoints[urlPath]; if (options.logErrors !== false && endpointConfig) { console.debug(`[INP] Found endpoint config with URL path: ${urlPath}`); } } // If endpoint not found in discovery, it means no payment is required for this endpoint if (!endpointConfig) { // Log for debugging if enabled if (options.logErrors !== false) { console.debug(`[INP] Endpoint ${endpointPath} not found in discovery. Available endpoints:`, Object.keys(discovery.endpoints)); } // Continue to the next middleware/route handler next(); return; } // Log successful endpoint match if (options.logErrors !== false) { console.debug(`[INP] Found endpoint configuration for ${endpointPath}:`, endpointConfig); } // Step 3: Check if payment is required if (endpointConfig.pricing_model === 'fixed' && endpointConfig.price) { // Log payment requirement detection if (options.logErrors !== false) { console.debug(`[INP] Payment required for endpoint ${endpointPath}:`, { pricing_model: endpointConfig.pricing_model, price: endpointConfig.price, networks: endpointConfig.networks }); } // Payment is required - validate it const payment = extractPaymentFromRequest(req); if (!payment) { if (options.logErrors !== false) { console.debug(`[INP] No payment provided for endpoint ${endpointPath}. Returning 402 Payment Required.`); } // Publish metrics event for missing payment const startTime = Date.now(); const metricsEvent = { projectId: options.projectId, endpointPath, paid: false, status: 'failed', latencyMs: startTime - req._startTime || 0, timestamp: new Date().toISOString(), userId: req.user?.id || req.headers['x-user-id'] }; // Publish metrics asynchronously (fire-and-forget) publishMetricsEvent(metricsEvent, options); res.status(402).json({ error: 'Payment Required', code: 'PAYMENT_REQUIRED', required: { amount: endpointConfig.price.amount, currency: endpointConfig.price.currency, networks: endpointConfig.networks } }); return; // IMPORTANT: Don't call next() - stop the request here } // Log payment found if (options.logErrors !== false) { console.debug(`[INP] Payment provided for endpoint ${endpointPath}:`, payment); } // Validate payment const validationResult = options.validatePayment ? await options.validatePayment(payment, endpointConfig) : await validatePayment(payment, endpointConfig); if (!validationResult.isValid) { if (options.logErrors !== false) { console.debug(`[INP] Payment validation failed for endpoint ${endpointPath}:`, validationResult); } // Publish metrics event for payment validation failure const startTime = Date.now(); const metricsEvent = { projectId: options.projectId, endpointPath, paid: true, status: 'failed', latencyMs: startTime - req._startTime || 0, timestamp: new Date().toISOString(), amount: payment.amount, currency: payment.currency, walletAddr: payment.walletAddress, userId: req.user?.id || req.headers['x-user-id'] }; // Publish metrics asynchronously (fire-and-forget) publishMetricsEvent(metricsEvent, options); res.status(402).json({ error: 'Payment Validation Failed', code: 'PAYMENT_VALIDATION_FAILED', details: validationResult.error }); return; // IMPORTANT: Don't call next() - stop the request here } if (options.logErrors !== false) { console.debug(`[INP] Payment validation successful for endpoint ${endpointPath}`); } req.inpPayment = payment; req.inpValidation = validationResult; // Publish metrics event for successful payment validation const startTime = Date.now(); const metricsEvent = { projectId: options.projectId, endpointPath, paid: true, status: 'success', latencyMs: startTime - req._startTime || 0, timestamp: new Date().toISOString(), amount: payment.amount, currency: payment.currency, walletAddr: payment.walletAddress, userId: req.user?.id || req.headers['x-user-id'] }; // Publish metrics asynchronously (fire-and-forget) publishMetricsEvent(metricsEvent, options); // Payment is valid, proceed to the next middleware/route handler if (options.logErrors !== false) { console.debug(`[INP] Payment validated, proceeding to next middleware/route handler for ${endpointPath}`); } next(); } else { // Log when payment is not required if (options.logErrors !== false) { console.debug(`[INP] No payment required for endpoint ${endpointPath}:`, { pricing_model: endpointConfig.pricing_model, has_price: !!endpointConfig.price }); } // Publish metrics event for free endpoint const startTime = Date.now(); const metricsEvent = { projectId: options.projectId, endpointPath, paid: false, status: 'success', latencyMs: startTime - req._startTime || 0, timestamp: new Date().toISOString(), userId: req.user?.id || req.headers['x-user-id'] }; // Publish metrics asynchronously (fire-and-forget) publishMetricsEvent(metricsEvent, options); // No payment required, proceed to the next middleware/route handler if (options.logErrors !== false) { console.debug(`[INP] No payment required, proceeding to next middleware/route handler for ${endpointPath}`); } next(); } } catch (error) { // Only throw INP errors for actual configuration issues, not discovery failures if (error instanceof errors_1.INPError) { // Log the error for debugging if (options.logErrors !== false) { console.error('[INP] Configuration error:', error.message); } res.status(400).json({ error: error.message, code: 'INP_ERROR' }); return; } // Log unexpected errors if enabled if (options.logErrors !== false) { console.error('INP Middleware Error:', error); } res.status(500).json({ error: 'Internal Server Error', code: 'INTERNAL_ERROR' }); return; } }; } /** * Extract payment information from the request */ function extractPaymentFromRequest(req) { // Check headers first (most common for API integrations) const headers = req.headers || {}; const paymentHeader = headers['x-inp-payment'] || headers['x-payment']; if (paymentHeader && typeof paymentHeader === 'string') { try { return JSON.parse(paymentHeader); } catch { // Invalid JSON, continue to other methods } } // Check request body const body = req.body; if (body && body.payment) { return body.payment; } // Check query parameters const query = req.query; const { payment } = query || {}; if (payment && typeof payment === 'string') { try { return JSON.parse(payment); } catch { // Invalid JSON, continue } } return null; } /** * Validate payment against endpoint configuration */ async function validatePayment(payment, endpointConfig) { const errors = []; // Validate amount if (endpointConfig.price && payment.amount !== parseFloat(endpointConfig.price.amount)) { errors.push(`Payment amount must be ${endpointConfig.price.amount} ${endpointConfig.price.currency}`); } // Validate currency if (endpointConfig.price && payment.currency !== endpointConfig.price.currency) { errors.push(`Payment currency must be ${endpointConfig.price.currency}`); } // Validate network if (endpointConfig.networks && !endpointConfig.networks.includes(payment.network)) { errors.push(`Payment network must be one of: ${endpointConfig.networks.join(', ')}`); } // Validate required fields if (!payment.walletAddress) { errors.push('Payment wallet address is required'); } if (!payment.transactionId && !payment.proof) { errors.push('Payment transaction ID or proof is required'); } if (errors.length > 0) { return { isValid: false, error: errors.join('; '), details: { errors } }; } // Always validate transaction on-chain (this is the default behavior) if (payment.transactionId) { // This would integrate with blockchain verification // For now, we'll assume it's valid if we have a transaction ID return { isValid: true, details: { transactionVerified: true, paymentDetails: { amount: payment.amount, currency: payment.currency, network: payment.network, timestamp: new Date().toISOString() } } }; } return { isValid: true, details: { paymentDetails: { amount: payment.amount, currency: payment.currency, network: payment.network, timestamp: new Date().toISOString() } } }; } // Export the unified middleware as the default exports.default = inpMiddleware; // Re-export client for advanced usage var inp_client_2 = require("../client/inp-client"); Object.defineProperty(exports, "INPClient", { enumerable: true, get: function () { return inp_client_2.INPClient; } }); // Re-export errors var errors_2 = require("../errors"); Object.defineProperty(exports, "INPError", { enumerable: true, get: function () { return errors_2.INPError; } }); Object.defineProperty(exports, "INPValidationError", { enumerable: true, get: function () { return errors_2.INPValidationError; } }); Object.defineProperty(exports, "INPPaymentError", { enumerable: true, get: function () { return errors_2.INPPaymentError; } }); Object.defineProperty(exports, "INPDiscoveryError", { enumerable: true, get: function () { return errors_2.INPDiscoveryError; } }); Object.defineProperty(exports, "INPClientError", { enumerable: true, get: function () { return errors_2.INPClientError; } }); Object.defineProperty(exports, "INPConfigError", { enumerable: true, get: function () { return errors_2.INPConfigError; } }); //# sourceMappingURL=index.js.map