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