diff --git a/src/config/index.js b/src/config/index.js index d10c908..0bdcfe7 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -4,49 +4,49 @@ * Centralized configuration management with environment variable support. */ -require('dotenv').config(); +require("dotenv").config(); const config = { // Server configuration server: { - nodeEnv: process.env.NODE_ENV || 'development', + nodeEnv: process.env.NODE_ENV || "development", port: parseInt(process.env.PORT, 10) || 3000, - host: process.env.HOST || '0.0.0.0', - baseUrl: process.env.API_BASE_URL || 'http://localhost:3000' + host: process.env.HOST || "0.0.0.0", + baseUrl: process.env.API_BASE_URL || "http://localhost:3000", }, // Database configuration database: { - url: process.env.DATABASE_URL || 'mongodb://localhost:27017/openwallet', + url: process.env.DATABASE_URL || "mongodb://localhost:27017/openwallet", options: { useNewUrlParser: true, useUnifiedTopology: true, serverSelectionTimeoutMS: 5000, - socketTimeoutMS: 45000 - } + socketTimeoutMS: 45000, + }, }, // Redis configuration (optional) redis: { - url: process.env.REDIS_URL || 'redis://localhost:6379', - enabled: !!process.env.REDIS_URL + url: process.env.REDIS_URL || "redis://localhost:6379", + enabled: !!process.env.REDIS_URL, }, // Tokenization provider configuration tokenization: { apiKey: process.env.TOKENIZATION_API_KEY, - baseURL: process.env.TOKENIZATION_BASE_URL || 'https://api.basistheory.com', + baseURL: process.env.TOKENIZATION_BASE_URL || "https://api.basistheory.com", tenantId: process.env.TOKENIZATION_TENANT_ID, - timeout: 30000 + timeout: 30000, }, // Security configuration security: { - jwtSecret: process.env.JWT_SECRET || 'change-this-in-production', - jwtExpiresIn: '7d', + jwtSecret: process.env.JWT_SECRET || "change-this-in-production", + jwtExpiresIn: "7d", encryptionKey: process.env.ENCRYPTION_KEY, webhookSecret: process.env.WEBHOOK_SECRET, - bcryptRounds: 10 + bcryptRounds: 10, }, // Rate limiting @@ -54,89 +54,96 @@ const config = { windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10) || 15 * 60 * 1000, max: parseInt(process.env.RATE_LIMIT_MAX, 10) || 100, standardHeaders: true, - legacyHeaders: false + legacyHeaders: false, }, // CORS configuration cors: { - origin: process.env.CORS_ORIGINS?.split(',') || ['http://localhost:3000'], - credentials: true + origin: process.env.CORS_ORIGINS?.split(",") || ["http://localhost:3000"], + credentials: true, }, // Wallet configuration wallet: { - defaultCurrency: process.env.DEFAULT_CURRENCY || 'USD', + defaultCurrency: process.env.DEFAULT_CURRENCY || "USD", minBalance: parseFloat(process.env.MIN_BALANCE) || 0, maxBalance: parseFloat(process.env.MAX_BALANCE) || 10000, autoTopUp: { - enabled: process.env.AUTO_TOPUP_ENABLED === 'true', + enabled: process.env.AUTO_TOPUP_ENABLED === "true", threshold: parseFloat(process.env.AUTO_TOPUP_THRESHOLD) || 10, - amount: parseFloat(process.env.AUTO_TOPUP_AMOUNT) || 50 - } + amount: parseFloat(process.env.AUTO_TOPUP_AMOUNT) || 50, + }, }, // Payment providers applePay: { merchantId: process.env.APPLE_PAY_MERCHANT_ID, - merchantName: process.env.APPLE_PAY_MERCHANT_NAME || 'Open Commerce Initiative (OCI)', - countryCode: 'US', - supportedNetworks: ['visa', 'mastercard', 'amex', 'discover'] + merchantName: + process.env.APPLE_PAY_MERCHANT_NAME || "Open Commerce Initiative (OCI)", + countryCode: "US", + supportedNetworks: ["visa", "mastercard", "amex", "discover"], }, googlePay: { merchantId: process.env.GOOGLE_PAY_MERCHANT_ID, - merchantName: process.env.GOOGLE_PAY_MERCHANT_NAME || 'Open Commerce Initiative (OCI)', - environment: process.env.GOOGLE_PAY_ENVIRONMENT || 'TEST' + merchantName: + process.env.GOOGLE_PAY_MERCHANT_NAME || "Open Commerce Initiative (OCI)", + environment: process.env.GOOGLE_PAY_ENVIRONMENT || "TEST", }, // Logging configuration logging: { - level: process.env.LOG_LEVEL || 'info', - file: process.env.LOG_FILE || 'logs/app.log', - console: process.env.NODE_ENV !== 'production' + level: process.env.LOG_LEVEL || "info", + file: process.env.LOG_FILE || "logs/app.log", + console: process.env.NODE_ENV !== "production", }, // Monitoring monitoring: { sentryDsn: process.env.SENTRY_DSN, - newRelicKey: process.env.NEW_RELIC_LICENSE_KEY + newRelicKey: process.env.NEW_RELIC_LICENSE_KEY, }, // Testing testing: { - testMode: process.env.TEST_MODE === 'true' - } + testMode: process.env.TEST_MODE === "true", + }, }; // Validation function validateConfig() { const errors = []; - if (!config.tokenization.apiKey && config.server.nodeEnv === 'production') { - errors.push('TOKENIZATION_API_KEY is required in production'); + if (!config.tokenization.apiKey && config.server.nodeEnv === "production") { + errors.push("TOKENIZATION_API_KEY is required in production"); } - if (config.security.jwtSecret === 'change-this-in-production' && - config.server.nodeEnv === 'production') { - errors.push('JWT_SECRET must be changed in production'); + if ( + config.security.jwtSecret === "change-this-in-production" && + config.server.nodeEnv === "production" + ) { + errors.push("JWT_SECRET must be changed in production"); } - if (!config.security.encryptionKey && config.server.nodeEnv === 'production') { - errors.push('ENCRYPTION_KEY is required in production'); + if ( + !config.security.encryptionKey && + config.server.nodeEnv === "production" + ) { + errors.push("ENCRYPTION_KEY is required in production"); } // Database URL is always required - if (!process.env.DATABASE_URL && config.server.nodeEnv === 'production') { - errors.push('DATABASE_URL is required in production'); + if (!process.env.DATABASE_URL && config.server.nodeEnv === "production") { + errors.push("DATABASE_URL is required in production"); } if (errors.length > 0) { - throw new Error(`Configuration errors:\n${errors.join('\n')}`); + throw new Error(`Configuration errors:\n${errors.join("\n")}`); } } // Validate on load -if (process.env.NODE_ENV !== 'test') { +if (process.env.NODE_ENV !== "test") { validateConfig(); } diff --git a/src/graphql/schema.js b/src/graphql/schema.js index 1cee417..0ab6021 100644 --- a/src/graphql/schema.js +++ b/src/graphql/schema.js @@ -3,13 +3,13 @@ const { GraphQLObjectType, GraphQLString, GraphQLNonNull, -} = require('graphql'); -const WalletService = require('../services/wallet'); -const db = require('../utils/database'); +} = require("graphql"); +const WalletService = require("../services/wallet"); +const db = require("../utils/database"); module.exports = (walletService) => { const WalletType = new GraphQLObjectType({ - name: 'Wallet', + name: "Wallet", fields: { id: { type: new GraphQLNonNull(GraphQLString) }, userId: { type: new GraphQLNonNull(GraphQLString) }, @@ -20,7 +20,7 @@ module.exports = (walletService) => { }); const RootQueryType = new GraphQLObjectType({ - name: 'Query', + name: "Query", fields: { wallet: { type: WalletType, diff --git a/src/index.js b/src/index.js index ecf3621..924626f 100644 --- a/src/index.js +++ b/src/index.js @@ -4,32 +4,35 @@ * A secure, tokenized platform powered by the Open Commerce Protocol (OCP) SDK. */ -const express = require('express'); -const helmet = require('helmet'); -const cors = require('cors'); -const rateLimit = require('express-rate-limit'); -const config = require('./config'); -const { connectDatabase } = require('./utils/database'); -const logger = require('./utils/logger'); - -const WalletService = require('./services/wallet'); -const TokenizationService = require('./services/tokenization'); -const MobilePaymentService = require('./services/mobilePayment'); -const AgentService = require('./services/agent'); -const A2AService = require('./services/a2aService'); -const UCPService = require('./services/ucp'); - -const walletRoutes = require('./routes/wallet'); -const tokenizationRoutes = require('./routes/tokenization'); -const mobilePaymentRoutes = require('./routes/mobilePayment'); -const agentRoutes = require('./routes/agent'); -const ucpRoutes = require('./routes/ucp'); +const express = require("express"); +const helmet = require("helmet"); +const cors = require("cors"); +const rateLimit = require("express-rate-limit"); +const config = require("./config"); +const { connectDatabase } = require("./utils/database"); +const logger = require("./utils/logger"); + +const WalletService = require("./services/wallet"); +const TokenizationService = require("./services/tokenization"); +const MobilePaymentService = require("./services/mobilePayment"); +const AgentService = require("./services/agent"); +const A2AService = require("./services/a2aService"); +const UCPService = require("./services/ucp"); + +const walletRoutes = require("./routes/wallet"); +const tokenizationRoutes = require("./routes/tokenization"); +const mobilePaymentRoutes = require("./routes/mobilePayment"); +const agentRoutes = require("./routes/agent"); +const ucpRoutes = require("./routes/ucp"); // Initialize services -const db = require('./utils/database'); +const db = require("./utils/database"); const walletService = new WalletService(db); const tokenizationService = new TokenizationService(); -const mobilePaymentService = new MobilePaymentService(tokenizationService, walletService); +const mobilePaymentService = new MobilePaymentService( + tokenizationService, + walletService, +); const agentService = new AgentService(db); const a2aService = new A2AService(walletService, db); const ucpService = new UCPService(a2aService); @@ -43,42 +46,42 @@ app.use(cors(config.cors)); // Rate limiting const limiter = rateLimit(config.rateLimit); -app.use('/api/', limiter); +app.use("/api/", limiter); // Body parsing middleware -app.use(express.json({ limit: '10mb' })); -app.use(express.urlencoded({ extended: true, limit: '10mb' })); +app.use(express.json({ limit: "10mb" })); +app.use(express.urlencoded({ extended: true, limit: "10mb" })); // Request logging app.use((req, res, next) => { logger.info(`${req.method} ${req.path}`, { ip: req.ip, - userAgent: req.get('user-agent') + userAgent: req.get("user-agent"), }); next(); }); // Health check endpoint -app.get('/health', (req, res) => { +app.get("/health", (req, res) => { res.json({ - status: 'healthy', + status: "healthy", timestamp: new Date().toISOString(), - version: require('../package.json').version + version: require("../package.json").version, }); }); -const { graphqlHTTP } = require('express-graphql'); -const schema = require('./graphql/schema')(walletService); +const { graphqlHTTP } = require("express-graphql"); +const schema = require("./graphql/schema")(walletService); // API routes -app.use('/api/v1/wallet', walletRoutes(walletService)); -app.use('/api/v1/tokens', tokenizationRoutes(tokenizationService)); -app.use('/api/v1/payments', mobilePaymentRoutes(mobilePaymentService)); -app.use('/api/v1/agents', agentRoutes(agentService)); // New -app.use('/api/v1/ucp', ucpRoutes(ucpService)); // New +app.use("/api/v1/wallet", walletRoutes(walletService)); +app.use("/api/v1/tokens", tokenizationRoutes(tokenizationService)); +app.use("/api/v1/payments", mobilePaymentRoutes(mobilePaymentService)); +app.use("/api/v1/agents", agentRoutes(agentService)); // New +app.use("/api/v1/ucp", ucpRoutes(ucpService)); // New app.use( - '/graphql', + "/graphql", graphqlHTTP({ schema, graphiql: true, @@ -86,12 +89,14 @@ app.use( ); // Root endpoint -app.get('/', (req, res) => { +app.get("/", (req, res) => { res.json({ - name: 'Open Commerce Initiative (OCI) API', - version: require('../package.json').version, - description: 'A secure, tokenized platform powered by the Open Commerce Protocol (OCP) SDK', - documentation: 'https://github.com/dcplatforms/Open-Commerce-Protocol#readme' + name: "Open Commerce Initiative (OCI) API", + version: require("../package.json").version, + description: + "A secure, tokenized platform powered by the Open Commerce Protocol (OCP) SDK", + documentation: + "https://github.com/dcplatforms/Open-Commerce-Protocol#readme", }); }); @@ -99,24 +104,25 @@ app.get('/', (req, res) => { app.use((req, res) => { res.status(404).json({ success: false, - error: 'Endpoint not found', - path: req.path + error: "Endpoint not found", + path: req.path, }); }); // Error handling middleware app.use((err, req, res, next) => { - logger.error('Unhandled error:', err); + logger.error("Unhandled error:", err); const statusCode = err.statusCode || 500; - const message = config.server.nodeEnv === 'production' && statusCode === 500 - ? 'Internal server error' - : err.message; + const message = + config.server.nodeEnv === "production" && statusCode === 500 + ? "Internal server error" + : err.message; res.status(statusCode).json({ success: false, error: message, - ...(config.server.nodeEnv === 'development' && { stack: err.stack }) + ...(config.server.nodeEnv === "development" && { stack: err.stack }), }); }); @@ -125,11 +131,13 @@ async function start() { try { // Connect to database await connectDatabase(); - logger.info('Database connected successfully'); + logger.info("Database connected successfully"); // Start listening const server = app.listen(config.server.port, config.server.host, () => { - logger.info(`Open Commerce Initiative (OCI) API running on ${config.server.host}:${config.server.port}`); + logger.info( + `Open Commerce Initiative (OCI) API running on ${config.server.host}:${config.server.port}`, + ); logger.info(`Environment: ${config.server.nodeEnv}`); logger.info(`API Base URL: ${config.server.baseUrl}`); }); @@ -139,15 +147,15 @@ async function start() { logger.info(`${signal} received, shutting down gracefully`); server.close(async () => { - logger.info('HTTP server closed'); + logger.info("HTTP server closed"); // Close database connection try { - const mongoose = require('mongoose'); + const mongoose = require("mongoose"); await mongoose.connection.close(); - logger.info('Database connection closed'); + logger.info("Database connection closed"); } catch (error) { - logger.error('Error closing database:', error); + logger.error("Error closing database:", error); } process.exit(0); @@ -155,16 +163,15 @@ async function start() { // Force shutdown after 10 seconds setTimeout(() => { - logger.error('Forced shutdown after timeout'); + logger.error("Forced shutdown after timeout"); process.exit(1); }, 10000); }; - process.on('SIGTERM', () => shutdown('SIGTERM')); - process.on('SIGINT', () => shutdown('SIGINT')); - + process.on("SIGTERM", () => shutdown("SIGTERM")); + process.on("SIGINT", () => shutdown("SIGINT")); } catch (error) { - logger.error('Failed to start server:', error); + logger.error("Failed to start server:", error); process.exit(1); } } diff --git a/src/middleware/auth.js b/src/middleware/auth.js index 3e1170e..e63d2b2 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -4,45 +4,45 @@ * Validates JWT tokens and protects routes. */ -const jwt = require('jsonwebtoken'); -const config = require('../config'); -const logger = require('../utils/logger'); +const jwt = require("jsonwebtoken"); +const config = require("../config"); +const logger = require("../utils/logger"); const authenticate = (req, res, next) => { const authHeader = req.headers.authorization; - if (!authHeader || !authHeader.startsWith('Bearer ')) { + if (!authHeader || !authHeader.startsWith("Bearer ")) { return res.status(401).json({ success: false, - error: 'Authentication required' + error: "Authentication required", }); } - const token = authHeader.split(' ')[1]; + const token = authHeader.split(" ")[1]; try { const decoded = jwt.verify(token, config.security.jwtSecret); req.user = decoded; next(); } catch (error) { - logger.warn('Invalid token attempt:', error.message); + logger.warn("Invalid token attempt:", error.message); return res.status(401).json({ success: false, - error: 'Invalid or expired token' + error: "Invalid or expired token", }); } }; const authorize = (roles = []) => { - if (typeof roles === 'string') { + if (typeof roles === "string") { roles = [roles]; } return (req, res, next) => { - if (roles.length && !roles.some(role => req.user.roles.includes(role))) { + if (roles.length && !roles.some((role) => req.user.roles.includes(role))) { return res.status(403).json({ success: false, - error: 'Forbidden' + error: "Forbidden", }); } next(); diff --git a/src/middleware/mpp.js b/src/middleware/mpp.js index b4f4d4c..b5561e5 100644 --- a/src/middleware/mpp.js +++ b/src/middleware/mpp.js @@ -6,7 +6,7 @@ * and retries the request with the payment header. */ -const logger = require('../utils/logger'); +const logger = require("../utils/logger"); class MPP402Handler { constructor(agentService, mandateService) { @@ -25,13 +25,23 @@ class MPP402Handler { let response = await requestFn(); if (response.status === 402) { - return await this._handle402Response(agent, response, intentMandateToken, requestFn); + return await this._handle402Response( + agent, + response, + intentMandateToken, + requestFn, + ); } return response; } catch (error) { if (error.response?.status === 402) { - return await this._handle402Response(agent, error.response, intentMandateToken, requestFn); + return await this._handle402Response( + agent, + error.response, + intentMandateToken, + requestFn, + ); } throw error; } @@ -46,21 +56,32 @@ class MPP402Handler { // 1. Extract payment requirement details from headers or body // MPP standard uses headers like 'X-MPP-Amount' and 'X-MPP-Merchant-DID' - const amount = parseFloat(response.headers?.['x-mpp-amount'] || response.data?.amount); - const currency = response.headers?.['x-mpp-currency'] || response.data?.currency || 'USD'; - const merchantDid = response.headers?.['x-mpp-merchant-did'] || response.data?.merchant_did; - const cartItems = response.data?.cart_items || [{ item: 'API_CALL', quantity: 1 }]; + const amount = parseFloat( + response.headers?.["x-mpp-amount"] || response.data?.amount, + ); + const currency = + response.headers?.["x-mpp-currency"] || response.data?.currency || "USD"; + const merchantDid = + response.headers?.["x-mpp-merchant-did"] || response.data?.merchant_did; + const cartItems = response.data?.cart_items || [ + { item: "API_CALL", quantity: 1 }, + ]; if (!amount || !merchantDid) { - throw new Error('Incomplete payment requirements in 402 response'); + throw new Error( + "Zero Trust Validation Failed: Incomplete payment requirements in 402 response", + ); } // 2. Validate Intent Mandate - const decodedIntent = await this.mandateService.verifyMandate(intentMandateToken); + const decodedIntent = + await this.mandateService.verifyMandate(intentMandateToken); // Check if the 402 request is within the intent's budget if (amount > decodedIntent.max_budget.value) { - throw new Error(`MPP: Payment amount ${amount} exceeds intent mandate budget of ${decodedIntent.max_budget.value}`); + throw new Error( + `Zero Trust Validation Failed: MPP payment amount ${amount} exceeds intent mandate budget of ${decodedIntent.max_budget.value}`, + ); } // 3. Generate a Cart Mandate for the specific 402 request @@ -69,16 +90,16 @@ class MPP402Handler { intentMandate: intentMandateToken, cartItems, totalPrice: amount, - merchantDid + merchantDid, }); // 4. Retry the request with the Payment Mandate header logger.info(`MPP: Retrying request with Cart Mandate...`); return await requestFn({ headers: { - 'X-OCP-Cart-Mandate': cartMandateToken, - 'Authorization': `Bearer ${agent.id}` - } + "X-OCP-Cart-Mandate": cartMandateToken, + Authorization: `Bearer ${agent.id}`, + }, }); } } diff --git a/src/middleware/validation.js b/src/middleware/validation.js index ed4220e..494d48d 100644 --- a/src/middleware/validation.js +++ b/src/middleware/validation.js @@ -4,27 +4,30 @@ * Uses Joi to validate request schemas. */ -const Joi = require('joi'); +const Joi = require("joi"); const validate = (schema) => { return (req, res, next) => { - const { error, value } = Joi.object(schema).validate({ - body: req.body, - params: req.params, - query: req.query, - }, { - abortEarly: false, - stripUnknown: true, - }); + const { error, value } = Joi.object(schema).validate( + { + body: req.body, + params: req.params, + query: req.query, + }, + { + abortEarly: false, + stripUnknown: true, + }, + ); if (error) { - const errors = error.details.map(detail => ({ + const errors = error.details.map((detail) => ({ message: detail.message, path: detail.path, })); return res.status(400).json({ success: false, - error: 'Validation failed', + error: "Validation failed", errors, }); } diff --git a/src/models/agent.js b/src/models/agent.js index f88d3c2..f3ae6a2 100644 --- a/src/models/agent.js +++ b/src/models/agent.js @@ -4,68 +4,75 @@ * Database schema for AI Agents with customization and autonomous capabilities. */ -const mongoose = require('mongoose'); +const mongoose = require("mongoose"); -const agentSchema = new mongoose.Schema({ +const agentSchema = new mongoose.Schema( + { name: { - type: String, - required: true, - trim: true + type: String, + required: true, + trim: true, }, ownerId: { - type: String, - required: true, - index: true + type: String, + required: true, + index: true, }, walletId: { - type: String, - required: true, - unique: true, - index: true + type: String, + required: true, + unique: true, + index: true, }, type: { - type: String, - enum: ['personal', 'business', 'service'], - default: 'personal' + type: String, + enum: ["personal", "business", "service"], + default: "personal", }, status: { - type: String, - enum: ['active', 'inactive', 'suspended'], - default: 'active', - index: true + type: String, + enum: ["active", "inactive", "suspended"], + default: "active", + index: true, }, config: { - limits: { - daily: { type: Number, default: 0 }, // 0 = unlimited - perTransaction: { type: Number, default: 0 } + limits: { + daily: { type: Number, default: 0 }, // 0 = unlimited + perTransaction: { type: Number, default: 0 }, + }, + allowedCategories: [ + { + type: String, }, - allowedCategories: [{ - type: String - }], - authorizedCounterparties: [{ - type: String // Agent IDs - }], - autoApprove: { - type: Boolean, - default: false - } + ], + authorizedCounterparties: [ + { + type: String, // Agent IDs + }, + ], + autoApprove: { + type: Boolean, + default: false, + }, }, metadata: { - type: Map, - of: mongoose.Schema.Types.Mixed, - default: {} - } -}, { + type: Map, + of: mongoose.Schema.Types.Mixed, + default: {}, + }, + }, + { timestamps: true, - collection: 'agents' -}); + collection: "agents", + }, +); // Indexes agentSchema.index({ ownerId: 1, status: 1 }); -const Agent = mongoose.model('Agent', agentSchema); +const Agent = mongoose.model("Agent", agentSchema); module.exports = { - Agent, - agentSchema + Agent, + agentSchema, }; diff --git a/src/models/refund.js b/src/models/refund.js index 473922a..ae6f623 100644 --- a/src/models/refund.js +++ b/src/models/refund.js @@ -4,156 +4,156 @@ * Database schema for refund requests and processing. */ -const mongoose = require('mongoose'); - -const refundSchema = new mongoose.Schema({ - transactionId: { - type: String, - required: true - }, - walletId: { - type: String, - required: true, - index: true - }, - amount: { - type: Number, - required: true, - min: 0 - }, - currency: { - type: String, - default: 'USD', - uppercase: true - }, - status: { - type: String, - required: true, - enum: ['pending', 'approved', 'rejected', 'completed', 'failed'], - default: 'pending', - index: true - }, - reason: { - type: String, - required: true, - enum: [ - 'customer_request', - 'duplicate_charge', - 'fraudulent', - 'service_issue', - 'other' - ] - }, - notes: { - type: String, - maxlength: 1000 - }, - requestedBy: { - userId: String, - role: String, - name: String - }, - approvedBy: { - adminId: String, - name: String - }, - rejectedBy: { - adminId: String, - name: String, - reason: String - }, - metadata: { - type: Map, - of: mongoose.Schema.Types.Mixed, - default: {} +const mongoose = require("mongoose"); + +const refundSchema = new mongoose.Schema( + { + transactionId: { + type: String, + required: true, + }, + walletId: { + type: String, + required: true, + index: true, + }, + amount: { + type: Number, + required: true, + min: 0, + }, + currency: { + type: String, + default: "USD", + uppercase: true, + }, + status: { + type: String, + required: true, + enum: ["pending", "approved", "rejected", "completed", "failed"], + default: "pending", + index: true, + }, + reason: { + type: String, + required: true, + enum: [ + "customer_request", + "duplicate_charge", + "fraudulent", + "service_issue", + "other", + ], + }, + notes: { + type: String, + maxlength: 1000, + }, + requestedBy: { + userId: String, + role: String, + name: String, + }, + approvedBy: { + adminId: String, + name: String, + }, + rejectedBy: { + adminId: String, + name: String, + reason: String, + }, + metadata: { + type: Map, + of: mongoose.Schema.Types.Mixed, + default: {}, + }, + approvedAt: { + type: Date, + }, + rejectedAt: { + type: Date, + }, + completedAt: { + type: Date, + }, + failedAt: { + type: Date, + }, + errorMessage: { + type: String, + }, }, - approvedAt: { - type: Date + { + timestamps: true, + collection: "refunds", }, - rejectedAt: { - type: Date - }, - completedAt: { - type: Date - }, - failedAt: { - type: Date - }, - errorMessage: { - type: String - } -}, { - timestamps: true, - collection: 'refunds' -}); +); // Indexes refundSchema.index({ walletId: 1, createdAt: -1 }); refundSchema.index({ status: 1, createdAt: -1 }); // Virtual for refund ID -refundSchema.virtual('id').get(function() { +refundSchema.virtual("id").get(function () { return this._id.toString(); }); -refundSchema.set('toJSON', { +refundSchema.set("toJSON", { virtuals: true, transform: (doc, ret) => { delete ret._id; delete ret.__v; return ret; - } + }, }); // Instance methods -refundSchema.methods.approve = function(adminId, adminName) { - this.status = 'approved'; +refundSchema.methods.approve = function (adminId, adminName) { + this.status = "approved"; this.approvedBy = { adminId, name: adminName }; this.approvedAt = new Date(); }; -refundSchema.methods.reject = function(adminId, adminName, reason) { - this.status = 'rejected'; +refundSchema.methods.reject = function (adminId, adminName, reason) { + this.status = "rejected"; this.rejectedBy = { adminId, name: adminName, reason }; this.rejectedAt = new Date(); }; -refundSchema.methods.markCompleted = function() { - this.status = 'completed'; +refundSchema.methods.markCompleted = function () { + this.status = "completed"; this.completedAt = new Date(); }; -refundSchema.methods.markFailed = function(errorMessage) { - this.status = 'failed'; +refundSchema.methods.markFailed = function (errorMessage) { + this.status = "failed"; this.errorMessage = errorMessage; this.failedAt = new Date(); }; // Static methods -refundSchema.statics.findPending = function(options = {}) { +refundSchema.statics.findPending = function (options = {}) { const { page = 1, limit = 20 } = options; const skip = (page - 1) * limit; - return this.find({ status: 'pending' }) + return this.find({ status: "pending" }) .sort({ createdAt: -1 }) .skip(skip) .limit(limit); }; -refundSchema.statics.findByWallet = function(walletId, options = {}) { +refundSchema.statics.findByWallet = function (walletId, options = {}) { const { page = 1, limit = 20, status } = options; const skip = (page - 1) * limit; const query = { walletId }; if (status) query.status = status; - return this.find(query) - .sort({ createdAt: -1 }) - .skip(skip) - .limit(limit); + return this.find(query).sort({ createdAt: -1 }).skip(skip).limit(limit); }; -refundSchema.statics.getStats = async function(dateFrom, dateTo) { +refundSchema.statics.getStats = async function (dateFrom, dateTo) { const matchQuery = {}; if (dateFrom || dateTo) { @@ -166,17 +166,17 @@ refundSchema.statics.getStats = async function(dateFrom, dateTo) { { $match: matchQuery }, { $group: { - _id: '$status', + _id: "$status", count: { $sum: 1 }, - totalAmount: { $sum: '$amount' } - } - } + totalAmount: { $sum: "$amount" }, + }, + }, ]); return stats.reduce((acc, stat) => { acc[stat._id] = { count: stat.count, - totalAmount: stat.totalAmount + totalAmount: stat.totalAmount, }; return acc; }, {}); @@ -216,10 +216,10 @@ CREATE TRIGGER update_refunds_updated_at BEFORE UPDATE ON refunds FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); `; -const Refund = mongoose.model('Refund', refundSchema); +const Refund = mongoose.model("Refund", refundSchema); module.exports = { Refund, refundSchema, - postgresqlSchema + postgresqlSchema, }; diff --git a/src/models/transaction.js b/src/models/transaction.js index 98f18be..a032188 100644 --- a/src/models/transaction.js +++ b/src/models/transaction.js @@ -4,103 +4,114 @@ * Database schema for transaction records with complete audit trail. */ -const mongoose = require('mongoose'); +const mongoose = require("mongoose"); -const transactionSchema = new mongoose.Schema({ - walletId: { - type: String, - required: true, - index: true - }, - type: { - type: String, - required: true, - enum: ['credit', 'debit', 'transfer_in', 'transfer_out', 'refund', 'a2a_transfer', 'blockchain_transfer'], - index: true - }, - amount: { - type: Number, - required: true - }, - currency: { - type: String, - default: 'USD', - uppercase: true - }, - status: { - type: String, - required: true, - enum: ['pending', 'completed', 'failed', 'cancelled'], - default: 'pending', - index: true - }, - description: { - type: String, - required: true, - maxlength: 500 - }, - paymentToken: { - type: String, - sparse: true - }, - transferId: { - type: String, - sparse: true - }, - refundId: { - type: String, - sparse: true, - index: true - }, - agentId: { - type: String, - sparse: true, - index: true - }, - counterpartyAgentId: { - type: String, - sparse: true, - index: true - }, - ucpPayload: { - type: Map, - of: mongoose.Schema.Types.Mixed, - default: {} - }, - hash: { - type: String, - sparse: true, - index: true - }, - network: { - type: String, - sparse: true - }, - gasUsed: { - type: Number, - sparse: true - }, - metadata: { - type: Map, - of: mongoose.Schema.Types.Mixed, - default: {} - }, - balanceAfter: { - type: Number - }, - errorMessage: { - type: String +const transactionSchema = new mongoose.Schema( + { + walletId: { + type: String, + required: true, + index: true, + }, + type: { + type: String, + required: true, + enum: [ + "credit", + "debit", + "transfer_in", + "transfer_out", + "refund", + "a2a_transfer", + "blockchain_transfer", + ], + index: true, + }, + amount: { + type: Number, + required: true, + }, + currency: { + type: String, + default: "USD", + uppercase: true, + }, + status: { + type: String, + required: true, + enum: ["pending", "completed", "failed", "cancelled"], + default: "pending", + index: true, + }, + description: { + type: String, + required: true, + maxlength: 500, + }, + paymentToken: { + type: String, + sparse: true, + }, + transferId: { + type: String, + sparse: true, + }, + refundId: { + type: String, + sparse: true, + index: true, + }, + agentId: { + type: String, + sparse: true, + index: true, + }, + counterpartyAgentId: { + type: String, + sparse: true, + index: true, + }, + ucpPayload: { + type: Map, + of: mongoose.Schema.Types.Mixed, + default: {}, + }, + hash: { + type: String, + sparse: true, + index: true, + }, + network: { + type: String, + sparse: true, + }, + gasUsed: { + type: Number, + sparse: true, + }, + metadata: { + type: Map, + of: mongoose.Schema.Types.Mixed, + default: {}, + }, + balanceAfter: { + type: Number, + }, + errorMessage: { + type: String, + }, + completedAt: { + type: Date, + }, + failedAt: { + type: Date, + }, }, - completedAt: { - type: Date + { + timestamps: true, + collection: "transactions", }, - failedAt: { - type: Date - } -}, { - timestamps: true, - collection: 'transactions' -}); +); // Indexes for common queries transactionSchema.index({ walletId: 1, createdAt: -1 }); @@ -109,36 +120,36 @@ transactionSchema.index({ walletId: 1, status: 1 }); transactionSchema.index({ createdAt: -1 }); // Virtual for transaction ID -transactionSchema.virtual('id').get(function () { +transactionSchema.virtual("id").get(function () { return this._id.toString(); }); // Ensure virtuals are included in JSON -transactionSchema.set('toJSON', { +transactionSchema.set("toJSON", { virtuals: true, transform: (doc, ret) => { delete ret._id; delete ret.__v; return ret; - } + }, }); // Instance methods transactionSchema.methods.isCompleted = function () { - return this.status === 'completed'; + return this.status === "completed"; }; transactionSchema.methods.isPending = function () { - return this.status === 'pending'; + return this.status === "pending"; }; transactionSchema.methods.markCompleted = function () { - this.status = 'completed'; + this.status = "completed"; this.completedAt = new Date(); }; transactionSchema.methods.markFailed = function (errorMessage) { - this.status = 'failed'; + this.status = "failed"; this.errorMessage = errorMessage; this.failedAt = new Date(); }; @@ -152,18 +163,19 @@ transactionSchema.statics.findByWallet = function (walletId, options = {}) { if (type) query.type = type; if (status) query.status = status; - return this.find(query) - .sort({ createdAt: -1 }) - .skip(skip) - .limit(limit); + return this.find(query).sort({ createdAt: -1 }).skip(skip).limit(limit); }; transactionSchema.statics.findByTransfer = function (transferId) { return this.find({ transferId }); }; -transactionSchema.statics.getWalletStats = async function (walletId, dateFrom, dateTo) { - const matchQuery = { walletId, status: 'completed' }; +transactionSchema.statics.getWalletStats = async function ( + walletId, + dateFrom, + dateTo, +) { + const matchQuery = { walletId, status: "completed" }; if (dateFrom || dateTo) { matchQuery.completedAt = {}; @@ -175,45 +187,46 @@ transactionSchema.statics.getWalletStats = async function (walletId, dateFrom, d { $match: matchQuery }, { $group: { - _id: '$type', + _id: "$type", count: { $sum: 1 }, - totalAmount: { $sum: '$amount' }, - avgAmount: { $avg: '$amount' } - } - } + totalAmount: { $sum: "$amount" }, + avgAmount: { $avg: "$amount" }, + }, + }, ]); return stats.reduce((acc, stat) => { acc[stat._id] = { count: stat.count, totalAmount: stat.totalAmount, - avgAmount: stat.avgAmount + avgAmount: stat.avgAmount, }; return acc; }, {}); }; transactionSchema.statics.getRecentActivity = function (limit = 10) { - return this.find({ status: 'completed' }) + return this.find({ status: "completed" }) .sort({ completedAt: -1 }) .limit(limit); }; -transactionSchema.statics.getVolumeByPeriod = async function (period = 'day') { - const groupBy = period === 'day' ? - { $dateToString: { format: '%Y-%m-%d', date: '$createdAt' } } : - { $dateToString: { format: '%Y-%m', date: '$createdAt' } }; +transactionSchema.statics.getVolumeByPeriod = async function (period = "day") { + const groupBy = + period === "day" + ? { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } } + : { $dateToString: { format: "%Y-%m", date: "$createdAt" } }; return this.aggregate([ - { $match: { status: 'completed' } }, + { $match: { status: "completed" } }, { $group: { _id: groupBy, count: { $sum: 1 }, - volume: { $sum: { $abs: '$amount' } } - } + volume: { $sum: { $abs: "$amount" } }, + }, }, - { $sort: { _id: 1 } } + { $sort: { _id: 1 } }, ]); }; @@ -250,10 +263,10 @@ CREATE TRIGGER update_transactions_updated_at BEFORE UPDATE ON transactions FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); `; -const Transaction = mongoose.model('Transaction', transactionSchema); +const Transaction = mongoose.model("Transaction", transactionSchema); module.exports = { Transaction, transactionSchema, - postgresqlSchema + postgresqlSchema, }; diff --git a/src/models/wallet.js b/src/models/wallet.js index 16ad225..797aa3c 100644 --- a/src/models/wallet.js +++ b/src/models/wallet.js @@ -5,166 +5,177 @@ * Compatible with both MongoDB and PostgreSQL through adapter pattern. */ -const mongoose = require('mongoose'); - -const walletSchema = new mongoose.Schema({ - userId: { - type: String, - required: true, - unique: true, - index: true - }, - balance: { - type: Number, - required: true, - default: 0, - min: 0 - }, - currency: { - type: String, - required: true, - default: 'USD', - uppercase: true, - trim: true - }, - status: { - type: String, - enum: ['active', 'suspended', 'closed'], - default: 'active', - index: true - }, - metadata: { - type: Map, - of: mongoose.Schema.Types.Mixed, - default: {} - }, - paymentMethods: [{ - tokenId: String, - type: { +const mongoose = require("mongoose"); + +const walletSchema = new mongoose.Schema( + { + userId: { type: String, - enum: ['card', 'apple_pay', 'google_pay'] + required: true, + unique: true, + index: true, }, - last4: String, - brand: String, - isDefault: Boolean, - createdAt: Date - }], - settings: { - autoTopUp: { - enabled: { type: Boolean, default: false }, - threshold: { type: Number, default: 10 }, - amount: { type: Number, default: 50 }, - paymentMethodId: String + balance: { + type: Number, + required: true, + default: 0, + min: 0, }, - notifications: { - lowBalance: { type: Boolean, default: true }, - transactions: { type: Boolean, default: true } - } - } -}, { - timestamps: true, - collection: 'wallets' -}); + currency: { + type: String, + required: true, + default: "USD", + uppercase: true, + trim: true, + }, + status: { + type: String, + enum: ["active", "suspended", "closed"], + default: "active", + index: true, + }, + metadata: { + type: Map, + of: mongoose.Schema.Types.Mixed, + default: {}, + }, + paymentMethods: [ + { + tokenId: String, + type: { + type: String, + enum: ["card", "apple_pay", "google_pay"], + }, + last4: String, + brand: String, + isDefault: Boolean, + createdAt: Date, + }, + ], + settings: { + autoTopUp: { + enabled: { type: Boolean, default: false }, + threshold: { type: Number, default: 10 }, + amount: { type: Number, default: 50 }, + paymentMethodId: String, + }, + notifications: { + lowBalance: { type: Boolean, default: true }, + transactions: { type: Boolean, default: true }, + }, + }, + }, + { + timestamps: true, + collection: "wallets", + }, +); // Indexes for performance walletSchema.index({ createdAt: -1 }); walletSchema.index({ status: 1, balance: 1 }); // Virtual for wallet ID -walletSchema.virtual('id').get(function() { +walletSchema.virtual("id").get(function () { return this._id.toString(); }); // Ensure virtuals are included in JSON -walletSchema.set('toJSON', { +walletSchema.set("toJSON", { virtuals: true, transform: (doc, ret) => { delete ret._id; delete ret.__v; return ret; - } + }, }); // Instance methods -walletSchema.methods.isActive = function() { - return this.status === 'active'; +walletSchema.methods.isActive = function () { + return this.status === "active"; }; -walletSchema.methods.canDeduct = function(amount) { +walletSchema.methods.canDeduct = function (amount) { return this.isActive() && this.balance >= amount; }; -walletSchema.methods.addPaymentMethod = function(tokenId, type, metadata = {}) { +walletSchema.methods.addPaymentMethod = function ( + tokenId, + type, + metadata = {}, +) { this.paymentMethods.push({ tokenId, type, last4: metadata.last4, brand: metadata.brand, isDefault: this.paymentMethods.length === 0, - createdAt: new Date() + createdAt: new Date(), }); }; -walletSchema.methods.removePaymentMethod = function(tokenId) { - this.paymentMethods = this.paymentMethods.filter(pm => pm.tokenId !== tokenId); +walletSchema.methods.removePaymentMethod = function (tokenId) { + this.paymentMethods = this.paymentMethods.filter( + (pm) => pm.tokenId !== tokenId, + ); }; -walletSchema.methods.setDefaultPaymentMethod = function(tokenId) { - this.paymentMethods.forEach(pm => { +walletSchema.methods.setDefaultPaymentMethod = function (tokenId) { + this.paymentMethods.forEach((pm) => { pm.isDefault = pm.tokenId === tokenId; }); }; // Static methods -walletSchema.statics.findByUserId = function(userId) { +walletSchema.statics.findByUserId = function (userId) { return this.findOne({ userId }); }; -walletSchema.statics.findActiveWallets = function(options = {}) { +walletSchema.statics.findActiveWallets = function (options = {}) { const { page = 1, limit = 20 } = options; const skip = (page - 1) * limit; - return this.find({ status: 'active' }) + return this.find({ status: "active" }) .sort({ createdAt: -1 }) .skip(skip) .limit(limit); }; -walletSchema.statics.getTotalBalance = async function() { +walletSchema.statics.getTotalBalance = async function () { const result = await this.aggregate([ - { $match: { status: 'active' } }, - { $group: { _id: null, total: { $sum: '$balance' } } } + { $match: { status: "active" } }, + { $group: { _id: null, total: { $sum: "$balance" } } }, ]); return result[0]?.total || 0; }; -walletSchema.statics.getWalletStats = async function() { +walletSchema.statics.getWalletStats = async function () { const stats = await this.aggregate([ { $group: { - _id: '$status', + _id: "$status", count: { $sum: 1 }, - totalBalance: { $sum: '$balance' }, - avgBalance: { $avg: '$balance' } - } - } + totalBalance: { $sum: "$balance" }, + avgBalance: { $avg: "$balance" }, + }, + }, ]); return stats.reduce((acc, stat) => { acc[stat._id] = { count: stat.count, totalBalance: stat.totalBalance, - avgBalance: stat.avgBalance + avgBalance: stat.avgBalance, }; return acc; }, {}); }; // Pre-save middleware -walletSchema.pre('save', function(next) { +walletSchema.pre("save", function (next) { // Ensure balance doesn't go negative if (this.balance < 0) { - return next(new Error('Wallet balance cannot be negative')); + return next(new Error("Wallet balance cannot be negative")); } next(); }); @@ -202,10 +213,10 @@ CREATE TRIGGER update_wallets_updated_at BEFORE UPDATE ON wallets FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); `; -const Wallet = mongoose.model('Wallet', walletSchema); +const Wallet = mongoose.model("Wallet", walletSchema); module.exports = { Wallet, walletSchema, - postgresqlSchema // For PostgreSQL users + postgresqlSchema, // For PostgreSQL users }; diff --git a/src/routes/agent.js b/src/routes/agent.js index b73cd14..97be6c8 100644 --- a/src/routes/agent.js +++ b/src/routes/agent.js @@ -4,10 +4,10 @@ * Defines API endpoints for Agent-related operations. */ -const express = require('express'); -const { authenticate } = require('../middleware/auth'); -const { validate } = require('../middleware/validation'); -const Joi = require('joi'); +const express = require("express"); +const { authenticate } = require("../middleware/auth"); +const { validate } = require("../middleware/validation"); +const Joi = require("joi"); module.exports = (agentService) => { const router = express.Router(); @@ -16,7 +16,7 @@ module.exports = (agentService) => { * GET /api/v1/agents * Get all registered agents */ - router.get('/', authenticate, async (req, res, next) => { + router.get("/", authenticate, async (req, res, next) => { try { const agents = await agentService.getAllAgents(); res.json(agents); @@ -29,23 +29,24 @@ module.exports = (agentService) => { * POST /api/v1/agents * Register a new agent */ - router.post('/', + router.post( + "/", authenticate, validate({ body: Joi.object({ name: Joi.string().required(), ownerId: Joi.string().required(), walletId: Joi.string().required(), - type: Joi.string().valid('personal', 'business', 'service'), + type: Joi.string().valid("personal", "business", "service"), config: Joi.object({ limits: Joi.object({ daily: Joi.number().min(0), - perTransaction: Joi.number().min(0) + perTransaction: Joi.number().min(0), }), authorizedCounterparties: Joi.array().items(Joi.string()), - autoApprove: Joi.boolean() - }) - }) + autoApprove: Joi.boolean(), + }), + }), }), async (req, res, next) => { try { @@ -54,14 +55,14 @@ module.exports = (agentService) => { } catch (error) { next(error); } - } + }, ); /** * GET /api/v1/agents/:agentId * Get an agent by ID */ - router.get('/:agentId', authenticate, async (req, res, next) => { + router.get("/:agentId", authenticate, async (req, res, next) => { try { const agent = await agentService.getAgent(req.params.agentId); res.json(agent); @@ -74,26 +75,30 @@ module.exports = (agentService) => { * PUT /api/v1/agents/:agentId/policy * Update an agent's policy */ - router.put('/:agentId/policy', + router.put( + "/:agentId/policy", authenticate, validate({ body: Joi.object({ limits: Joi.object({ daily: Joi.number().min(0), - perTransaction: Joi.number().min(0) + perTransaction: Joi.number().min(0), }), authorizedCounterparties: Joi.array().items(Joi.string()), - autoApprove: Joi.boolean() - }) + autoApprove: Joi.boolean(), + }), }), async (req, res, next) => { try { - const updatedAgent = await agentService.updateAgentPolicy(req.params.agentId, req.body); + const updatedAgent = await agentService.updateAgentPolicy( + req.params.agentId, + req.body, + ); res.json(updatedAgent); } catch (error) { next(error); } - } + }, ); return router; diff --git a/src/routes/mobilePayment.js b/src/routes/mobilePayment.js index be4646c..42099c4 100644 --- a/src/routes/mobilePayment.js +++ b/src/routes/mobilePayment.js @@ -4,13 +4,13 @@ * API endpoints for Apple Pay and Google Pay. */ -const express = require('express'); +const express = require("express"); const router = express.Router(); -const MobilePaymentService = require('../services/mobilePayment'); -const WalletService = require('../services/wallet'); -const { authenticate } = require('../middleware/auth'); -const { validate } = require('../middleware/validation'); -const Joi = require('joi'); +const MobilePaymentService = require("../services/mobilePayment"); +const WalletService = require("../services/wallet"); +const { authenticate } = require("../middleware/auth"); +const { validate } = require("../middleware/validation"); +const Joi = require("joi"); module.exports = (mobilePaymentService) => { const router = express.Router(); @@ -19,138 +19,140 @@ module.exports = (mobilePaymentService) => { * Initialize Apple Pay session * POST /api/v1/payments/applepay/init */ - router.post('/applepay/init', + router.post( + "/applepay/init", authenticate, validate({ body: Joi.object({ walletId: Joi.string().required(), amount: Joi.number().positive().required(), - currency: Joi.string().length(3).uppercase().default('USD'), - }) + currency: Joi.string().length(3).uppercase().default("USD"), + }), }), async (req, res, next) => { try { const session = await mobilePaymentService.initializeApplePay(req.body); res.json({ success: true, - data: session + data: session, }); } catch (error) { next(error); } - } + }, ); /** * Process Apple Pay payment * POST /api/v1/payments/applepay/process */ - router.post('/applepay/process', + router.post( + "/applepay/process", authenticate, validate({ body: Joi.object({ sessionId: Joi.string().required(), paymentData: Joi.object().required(), - }) + }), }), async (req, res, next) => { try { const result = await mobilePaymentService.processApplePay(req.body); res.json({ success: true, - data: result + data: result, }); } catch (error) { next(error); } - } + }, ); /** * Initialize Google Pay session * POST /api/v1/payments/googlepay/init */ - router.post('/googlepay/init', + router.post( + "/googlepay/init", authenticate, validate({ body: Joi.object({ walletId: Joi.string().required(), amount: Joi.number().positive().required(), - currency: Joi.string().length(3).uppercase().default('USD'), - }) + currency: Joi.string().length(3).uppercase().default("USD"), + }), }), async (req, res, next) => { try { - const session = await mobilePaymentService.initializeGooglePay(req.body); + const session = await mobilePaymentService.initializeGooglePay( + req.body, + ); res.json({ success: true, - data: session + data: session, }); } catch (error) { next(error); } - } + }, ); /** * Process Google Pay payment * POST /api/v1/payments/googlepay/process */ - router.post('/googlepay/process', + router.post( + "/googlepay/process", authenticate, validate({ body: Joi.object({ sessionId: Joi.string().required(), paymentData: Joi.object().required(), - }) + }), }), async (req, res, next) => { try { const result = await mobilePaymentService.processGooglePay(req.body); res.json({ success: true, - data: result + data: result, }); } catch (error) { next(error); } - } + }, ); /** * Get session status * GET /api/v1/payments/session/:sessionId */ - router.get('/session/:sessionId', - authenticate, - async (req, res, next) => { - try { - const status = await mobilePaymentService.getSessionStatus(req.params.sessionId); - res.json({ - success: true, - data: status - }); - } catch (error) { - next(error); - } + router.get("/session/:sessionId", authenticate, async (req, res, next) => { + try { + const status = await mobilePaymentService.getSessionStatus( + req.params.sessionId, + ); + res.json({ + success: true, + data: status, + }); + } catch (error) { + next(error); } - ); + }); /** * Cancel a session * DELETE /api/v1/payments/session/:sessionId */ - router.delete('/session/:sessionId', - authenticate, - async (req, res, next) => { - try { - await mobilePaymentService.cancelSession(req.params.sessionId); - res.status(204).send(); - } catch (error) { - next(error); - } + router.delete("/session/:sessionId", authenticate, async (req, res, next) => { + try { + await mobilePaymentService.cancelSession(req.params.sessionId); + res.status(204).send(); + } catch (error) { + next(error); } - ); + }); return router; }; diff --git a/src/routes/tokenization.js b/src/routes/tokenization.js index 74227b5..0e35a52 100644 --- a/src/routes/tokenization.js +++ b/src/routes/tokenization.js @@ -4,12 +4,12 @@ * API endpoints for payment tokenization. */ -const express = require('express'); +const express = require("express"); const router = express.Router(); -const TokenizationService = require('../services/tokenization'); -const { authenticate } = require('../middleware/auth'); -const { validate } = require('../middleware/validation'); -const Joi = require('joi'); +const TokenizationService = require("../services/tokenization"); +const { authenticate } = require("../middleware/auth"); +const { validate } = require("../middleware/validation"); +const Joi = require("joi"); module.exports = (tokenizationService) => { const router = express.Router(); @@ -18,7 +18,8 @@ module.exports = (tokenizationService) => { * Create a card token * POST /api/v1/tokens/card */ - router.post('/card', + router.post( + "/card", authenticate, validate({ body: Joi.object({ @@ -26,55 +27,49 @@ module.exports = (tokenizationService) => { exp_month: Joi.string().length(2).required(), exp_year: Joi.string().length(4).required(), cvc: Joi.string().min(3).max(4).required(), - }) + }), }), async (req, res, next) => { try { const token = await tokenizationService.createCardToken(req.body); res.status(201).json({ success: true, - data: token + data: token, }); } catch (error) { next(error); } - } + }, ); /** * Get a token by ID * GET /api/v1/tokens/:tokenId */ - router.get('/:tokenId', - authenticate, - async (req, res, next) => { - try { - const token = await tokenizationService.getToken(req.params.tokenId); - res.json({ - success: true, - data: token - }); - } catch (error) { - next(error); - } + router.get("/:tokenId", authenticate, async (req, res, next) => { + try { + const token = await tokenizationService.getToken(req.params.tokenId); + res.json({ + success: true, + data: token, + }); + } catch (error) { + next(error); } - ); + }); /** * Delete a token by ID * DELETE /api/v1/tokens/:tokenId */ - router.delete('/:tokenId', - authenticate, - async (req, res, next) => { - try { - await tokenizationService.deleteToken(req.params.tokenId); - res.status(204).send(); - } catch (error) { - next(error); - } + router.delete("/:tokenId", authenticate, async (req, res, next) => { + try { + await tokenizationService.deleteToken(req.params.tokenId); + res.status(204).send(); + } catch (error) { + next(error); } - ); + }); return router; }; diff --git a/src/routes/ucp.js b/src/routes/ucp.js index 6af1b40..a1bbd44 100644 --- a/src/routes/ucp.js +++ b/src/routes/ucp.js @@ -4,10 +4,10 @@ * Defines API endpoints for Universal Commerce Protocol (UCP) operations. */ -const express = require('express'); -const { authenticate } = require('../middleware/auth'); -const { validate } = require('../middleware/validation'); -const Joi = require('joi'); +const express = require("express"); +const { authenticate } = require("../middleware/auth"); +const { validate } = require("../middleware/validation"); +const Joi = require("joi"); module.exports = (ucpService) => { const router = express.Router(); @@ -16,7 +16,8 @@ module.exports = (ucpService) => { * POST /api/v1/ucp/process * Process a UCP-compliant commerce intent */ - router.post('/process', + router.post( + "/process", authenticate, validate({ body: Joi.object({ @@ -24,18 +25,18 @@ module.exports = (ucpService) => { intent: Joi.string().required(), sender: Joi.object({ agent_id: Joi.string().required(), - wallet_id: Joi.string() + wallet_id: Joi.string(), }).required(), recipient: Joi.object({ agent_id: Joi.string().required(), - wallet_id: Joi.string() + wallet_id: Joi.string(), }), amount: Joi.object({ value: Joi.number().required(), - currency: Joi.string() + currency: Joi.string(), }), - data: Joi.object() - }) + data: Joi.object(), + }), }), async (req, res, next) => { try { @@ -44,14 +45,14 @@ module.exports = (ucpService) => { } catch (error) { next(error); } - } + }, ); /** * GET /api/v1/ucp/schema * Get the JSON schema for UCP intents */ - router.get('/schema', async (req, res, next) => { + router.get("/schema", async (req, res, next) => { try { const schema = ucpService.getUcpSchema(); res.json(schema); diff --git a/src/routes/wallet.js b/src/routes/wallet.js index d35d194..e0552fe 100644 --- a/src/routes/wallet.js +++ b/src/routes/wallet.js @@ -4,12 +4,12 @@ * API endpoints for wallet management */ -const express = require('express'); +const express = require("express"); const router = express.Router(); -const WalletService = require('../services/wallet'); -const { authenticate } = require('../middleware/auth'); -const { validate } = require('../middleware/validation'); -const Joi = require('joi'); +const WalletService = require("../services/wallet"); +const { authenticate } = require("../middleware/auth"); +const { validate } = require("../middleware/validation"); +const Joi = require("joi"); module.exports = (walletService) => { const router = express.Router(); @@ -18,117 +18,114 @@ module.exports = (walletService) => { * Create wallet * POST /api/v1/wallet */ - router.post('/', + router.post( + "/", authenticate, validate({ body: Joi.object({ userId: Joi.string().required(), - currency: Joi.string().length(3).uppercase().default('USD'), - initialBalance: Joi.number().min(0).default(0) - }) + currency: Joi.string().length(3).uppercase().default("USD"), + initialBalance: Joi.number().min(0).default(0), + }), }), async (req, res, next) => { try { const wallet = await walletService.createWallet(req.body); res.status(201).json({ success: true, - data: wallet + data: wallet, }); } catch (error) { next(error); } - } + }, ); /** * Get wallet by ID * GET /api/v1/wallet/:walletId */ - router.get('/:walletId', - authenticate, - async (req, res, next) => { - try { - const wallet = await walletService.getWallet(req.params.walletId); - res.json({ - success: true, - data: wallet - }); - } catch (error) { - next(error); - } + router.get("/:walletId", authenticate, async (req, res, next) => { + try { + const wallet = await walletService.getWallet(req.params.walletId); + res.json({ + success: true, + data: wallet, + }); + } catch (error) { + next(error); } - ); + }); /** * Add funds to wallet * POST /api/v1/wallet/:walletId/fund */ - router.post('/:walletId/fund', + router.post( + "/:walletId/fund", authenticate, validate({ body: Joi.object({ amount: Joi.number().positive().required(), paymentToken: Joi.string().required(), - description: Joi.string().max(500) - }) + description: Joi.string().max(500), + }), }), async (req, res, next) => { try { const result = await walletService.addFunds({ walletId: req.params.walletId, - ...req.body + ...req.body, }); res.json({ success: true, - data: result + data: result, }); } catch (error) { next(error); } - } + }, ); /** * Get wallet transactions * GET /api/v1/wallet/:walletId/transactions */ - router.get('/:walletId/transactions', + router.get( + "/:walletId/transactions", authenticate, async (req, res, next) => { try { const { page = 1, limit = 20, type, status } = req.query; const result = await walletService.getTransactions( req.params.walletId, - { page: parseInt(page), limit: parseInt(limit), type, status } + { page: parseInt(page), limit: parseInt(limit), type, status }, ); res.json({ success: true, - data: result + data: result, }); } catch (error) { next(error); } - } + }, ); /** * Get wallet statistics * GET /api/v1/wallet/:walletId/stats */ - router.get('/:walletId/stats', - authenticate, - async (req, res, next) => { - try { - const stats = await walletService.getWalletStats(req.params.walletId); - res.json({ - success: true, - data: stats - }); - } catch (error) { - next(error); - } + router.get("/:walletId/stats", authenticate, async (req, res, next) => { + try { + const stats = await walletService.getWalletStats(req.params.walletId); + res.json({ + success: true, + data: stats, + }); + } catch (error) { + next(error); } - ); + }); return router; }; diff --git a/src/services/a2aService.js b/src/services/a2aService.js index d6dc9cd..3409bb0 100644 --- a/src/services/a2aService.js +++ b/src/services/a2aService.js @@ -5,99 +5,110 @@ * policy compliance, limit checks, and authorized counterparty validation. */ -const { Agent } = require('../models/agent'); -const logger = require('../utils/logger'); +const { Agent } = require("../models/agent"); +const logger = require("../utils/logger"); class A2AService { - constructor(walletService, db) { - this.walletService = walletService; - this.db = db; - } + constructor(walletService, db) { + this.walletService = walletService; + this.db = db; + } - /** - * Execute a transfer between two agents - * @param {Object} params - * @param {string} params.fromAgentId - Sender Agent ID - * @param {string} params.toAgentId - Recipient Agent ID - * @param {number} params.amount - Amount to transfer - * @param {Object} params.ucpPayload - The original UCP intent/payload - */ - async executeTransfer({ fromAgentId, toAgentId, amount, ucpPayload = {} }) { - try { - // 1. Validate Agents - const fromAgent = await Agent.findById(fromAgentId); - if (!fromAgent || fromAgent.status !== 'active') { - throw new Error(`Sender agent ${fromAgentId} not found or inactive`); - } + /** + * Execute a transfer between two agents + * @param {Object} params + * @param {string} params.fromAgentId - Sender Agent ID + * @param {string} params.toAgentId - Recipient Agent ID + * @param {number} params.amount - Amount to transfer + * @param {Object} params.ucpPayload - The original UCP intent/payload + */ + async executeTransfer({ fromAgentId, toAgentId, amount, ucpPayload = {} }) { + try { + // 1. Validate Agents + const fromAgent = await Agent.findById(fromAgentId); + if (!fromAgent || fromAgent.status !== "active") { + throw new Error(`Sender agent ${fromAgentId} not found or inactive`); + } - const toAgent = await Agent.findById(toAgentId); - if (!toAgent || toAgent.status !== 'active') { - throw new Error(`Recipient agent ${toAgentId} not found or inactive`); - } + const toAgent = await Agent.findById(toAgentId); + if (!toAgent || toAgent.status !== "active") { + throw new Error(`Recipient agent ${toAgentId} not found or inactive`); + } - // 2. Policy Checks (Sender) - await this._validateAgentPolicy(fromAgent, toAgentId, amount); + // 2. Policy Checks (Sender) + await this._validateAgentPolicy(fromAgent, toAgentId, amount); - // 3. Execute Wallet Transfer - const transferResult = await this.walletService.transfer({ - fromWalletId: fromAgent.walletId, - toWalletId: toAgent.walletId, - amount, - description: `A2A Transfer: ${fromAgent.name} -> ${toAgent.name}`, - metadata: { // Pass metadata for Transaction creation - agentId: fromAgentId, - counterpartyAgentId: toAgentId, - ucpPayload, - type: 'a2a_transfer' - } - }); + // 3. Execute Wallet Transfer + const transferResult = await this.walletService.transfer({ + fromWalletId: fromAgent.walletId, + toWalletId: toAgent.walletId, + amount, + description: `A2A Transfer: ${fromAgent.name} -> ${toAgent.name}`, + metadata: { + // Pass metadata for Transaction creation + agentId: fromAgentId, + counterpartyAgentId: toAgentId, + ucpPayload, + type: "a2a_transfer", + }, + }); - // 4. Update Agent Usage (if we were tracking daily usage in db, we'd do it here) - // For now, limits are stateless checks against config. - // In a real implementation, we would query daily volume or update a usage record. + // 4. Update Agent Usage (if we were tracking daily usage in db, we'd do it here) + // For now, limits are stateless checks against config. + // In a real implementation, we would query daily volume or update a usage record. - return { - success: true, - transferId: transferResult.transferId, - timestamp: new Date(), - fromAgent: fromAgent.name, - toAgent: toAgent.name, - amount - }; - } catch (error) { - throw this._handleError('executeTransfer', error); - } + return { + success: true, + transferId: transferResult.transferId, + timestamp: new Date(), + fromAgent: fromAgent.name, + toAgent: toAgent.name, + amount, + }; + } catch (error) { + throw this._handleError("executeTransfer", error); } + } - /** - * Validate agent policies - * @private - */ - async _validateAgentPolicy(agent, counterpartyId, amount) { - const { config } = agent; - if (!config) return; - - // Check Per Transaction Limit - if (config.limits?.perTransaction > 0 && amount > config.limits.perTransaction) { - throw new Error(`Amount ${amount} exceeds agent per-transaction limit of ${config.limits.perTransaction}`); - } + /** + * Validate agent policies + * @private + */ + async _validateAgentPolicy(agent, counterpartyId, amount) { + const { config } = agent; + if (!config) return; - // Check Authorized Counterparties - if (config.authorizedCounterparties && config.authorizedCounterparties.length > 0) { - if (!config.authorizedCounterparties.includes(counterpartyId)) { - throw new Error(`Agent ${agent.id} is not authorized to trade with ${counterpartyId}`); - } - } + // Check Per Transaction Limit + if ( + config.limits?.perTransaction > 0 && + amount > config.limits.perTransaction + ) { + throw new Error( + `Amount ${amount} exceeds agent per-transaction limit of ${config.limits.perTransaction}`, + ); } - /** - * Handle and format errors - * @private - */ - _handleError(method, error) { - logger.error(`A2AService.${method} error:`, error); - return error instanceof Error ? error : new Error(error); + // Check Authorized Counterparties + if ( + config.authorizedCounterparties && + config.authorizedCounterparties.length > 0 + ) { + if (!config.authorizedCounterparties.includes(counterpartyId)) { + throw new Error( + `Agent ${agent.id} is not authorized to trade with ${counterpartyId}`, + ); + } } + } + + /** + * Handle and format errors + * @private + */ + _handleError(method, error) { + logger.error(`A2AService.${method} error:`, error); + return error instanceof Error ? error : new Error(error); + } } module.exports = A2AService; diff --git a/src/services/agent.js b/src/services/agent.js index 4605818..ad47bc5 100644 --- a/src/services/agent.js +++ b/src/services/agent.js @@ -5,16 +5,17 @@ * Facilitates agent-to-agent interactions and ensures policy compliance. */ -const crypto = require('crypto'); -const MandateService = require('./mandate'); -const logger = require('../utils/logger'); +const crypto = require("crypto"); +const MandateService = require("./mandate"); +const logger = require("../utils/logger"); class AgentService { constructor(database, config = {}) { this.db = database; this.config = { defaultSpendingLimit: config.defaultSpendingLimit || 1000, - defaultAuthorizedCounterparties: config.defaultAuthorizedCounterparties || [] + defaultAuthorizedCounterparties: + config.defaultAuthorizedCounterparties || [], }; this.mandateService = new MandateService(config.mandateConfig); } @@ -28,27 +29,37 @@ class AgentService { * @param {Object} params.policy - Agent's policy (spending limits, counterparties) * @returns {Promise} Registered agent */ - async registerAgent({ name, ownerId, walletId, type = 'personal', config: agentConfig }) { + async registerAgent({ + name, + ownerId, + walletId, + type = "personal", + config: agentConfig, + }) { try { const newAgent = await this.db.createAgent({ name, ownerId, walletId, type, - status: 'active', + status: "active", config: { limits: { daily: agentConfig?.limits?.daily || 0, - perTransaction: agentConfig?.limits?.perTransaction || this.config.defaultSpendingLimit + perTransaction: + agentConfig?.limits?.perTransaction || + this.config.defaultSpendingLimit, }, - authorizedCounterparties: agentConfig?.authorizedCounterparties || this.config.defaultAuthorizedCounterparties, - autoApprove: agentConfig?.autoApprove || false + authorizedCounterparties: + agentConfig?.authorizedCounterparties || + this.config.defaultAuthorizedCounterparties, + autoApprove: agentConfig?.autoApprove || false, }, - metadata: {} + metadata: {}, }); return newAgent; } catch (error) { - throw this._handleError('registerAgent', error); + throw this._handleError("registerAgent", error); } } @@ -61,11 +72,11 @@ class AgentService { try { const agent = await this.db.findAgentById(agentId); if (!agent) { - throw new Error('Agent not found'); + throw new Error("Agent not found"); } return agent; } catch (error) { - throw this._handleError('getAgent', error); + throw this._handleError("getAgent", error); } } @@ -77,7 +88,7 @@ class AgentService { try { return await this.db.findAllAgents(filter); } catch (error) { - throw this._handleError('getAllAgents', error); + throw this._handleError("getAllAgents", error); } } @@ -93,13 +104,13 @@ class AgentService { const updatedAgent = await this.db.updateAgent(agentId, { config: { ...agent.config, - ...newConfig + ...newConfig, }, - updatedAt: new Date() + updatedAt: new Date(), }); return updatedAgent; } catch (error) { - throw this._handleError('updateAgentPolicy', error); + throw this._handleError("updateAgentPolicy", error); } } @@ -107,10 +118,18 @@ class AgentService { * Issue an Intent Mandate for an agent * @param {Object} params - Intent parameters */ - async issueIntentMandate({ userDid, agentId, maxBudget, currency, expiration, purposeCode, allowedMerchants }) { + async issueIntentMandate({ + userDid, + agentId, + maxBudget, + currency, + expiration, + purposeCode, + allowedMerchants, + }) { try { const agent = await this.getAgent(agentId); - const agentDid = agent.metadata?.get('did') || `did:key:${agentId}`; + const agentDid = agent.metadata?.get("did") || `did:key:${agentId}`; return await this.mandateService.issueIntentMandate({ userDid, @@ -119,10 +138,10 @@ class AgentService { currency, expiration, purposeCode, - allowedMerchants + allowedMerchants, }); } catch (error) { - throw this._handleError('issueIntentMandate', error); + throw this._handleError("issueIntentMandate", error); } } @@ -132,15 +151,15 @@ class AgentService { async issueAgentVC({ userDid, agentId, capabilities }) { try { const agent = await this.getAgent(agentId); - const agentDid = agent.metadata?.get('did') || `did:key:${agentId}`; + const agentDid = agent.metadata?.get("did") || `did:key:${agentId}`; return await this.mandateService.issueAgentVC({ userDid, agentDid, - capabilities + capabilities, }); } catch (error) { - throw this._handleError('issueAgentVC', error); + throw this._handleError("issueAgentVC", error); } } @@ -162,11 +181,17 @@ class AgentService { // Basic policy checks (more complex logic would be here) if (amount > fromAgent.policy.spendingLimit) { - throw new Error(`Transfer amount exceeds spending limit for agent ${fromAgentId}`); + throw new Error( + `Transfer amount exceeds spending limit for agent ${fromAgentId}`, + ); } - if (!fromAgent.policy.authorizedCounterparties.includes(toAgentId) && - fromAgent.policy.authorizedCounterparties.length > 0) { - throw new Error(`Agent ${toAgentId} is not an authorized counterparty for ${fromAgentId}`); + if ( + !fromAgent.policy.authorizedCounterparties.includes(toAgentId) && + fromAgent.policy.authorizedCounterparties.length > 0 + ) { + throw new Error( + `Agent ${toAgentId} is not an authorized counterparty for ${fromAgentId}`, + ); } // Simulate transfer success @@ -176,7 +201,7 @@ class AgentService { toAgentId, amount, currency, - timestamp: new Date() + timestamp: new Date(), }; } diff --git a/src/services/index.js b/src/services/index.js index 322b426..98f9cfe 100644 --- a/src/services/index.js +++ b/src/services/index.js @@ -1,17 +1,17 @@ -const WalletService = require('./wallet'); -const AgentService = require('./agent'); -const A2AService = require('./a2aService'); -const UCPService = require('./ucp'); -const MobilePaymentService = require('./mobilePayment'); -const TokenizationService = require('./tokenization'); -const Web3Service = require('./web3'); +const WalletService = require("./wallet"); +const AgentService = require("./agent"); +const A2AService = require("./a2aService"); +const UCPService = require("./ucp"); +const MobilePaymentService = require("./mobilePayment"); +const TokenizationService = require("./tokenization"); +const Web3Service = require("./web3"); module.exports = { - WalletService, - AgentService, - A2AService, - UCPService, - MobilePaymentService, - TokenizationService, - Web3Service + WalletService, + AgentService, + A2AService, + UCPService, + MobilePaymentService, + TokenizationService, + Web3Service, }; diff --git a/src/services/mandate.js b/src/services/mandate.js index c4c44de..dd22396 100644 --- a/src/services/mandate.js +++ b/src/services/mandate.js @@ -6,15 +6,18 @@ * 'chain of evidence' for autonomous agent transactions. */ -const jwt = require('jsonwebtoken'); -const crypto = require('crypto'); -const logger = require('../utils/logger'); +const jwt = require("jsonwebtoken"); +const crypto = require("crypto"); +const logger = require("../utils/logger"); class MandateService { constructor(config = {}) { - this.issuer = config.issuer || 'did:web:open-commerce-protocol.io'; + this.issuer = config.issuer || "did:web:open-commerce-protocol.io"; // In a real implementation, this would be a private key from a secure enclave - this.signingKey = config.signingKey || process.env.MANDATE_SIGNING_KEY || 'default-secret-key'; + this.signingKey = + config.signingKey || + process.env.MANDATE_SIGNING_KEY || + "default-secret-key"; } /** @@ -22,25 +25,33 @@ class MandateService { * @param {Object} params - Mandate parameters * @returns {Promise} Signed JWT Mandate */ - async issueIntentMandate({ userDid, agentDid, maxBudget, currency = 'USD', expiration, purposeCode, allowedMerchants = [] }) { + async issueIntentMandate({ + userDid, + agentDid, + maxBudget, + currency = "USD", + expiration, + purposeCode, + allowedMerchants = [], + }) { const payload = { iss: this.issuer, sub: agentDid, user_did: userDid, agent_did: agentDid, - mandate_id: `mandate_${crypto.randomBytes(8).toString('hex')}`, + mandate_id: `mandate_${crypto.randomBytes(8).toString("hex")}`, max_budget: { value: maxBudget, - currency + currency, }, - exp: expiration || Math.floor(Date.now() / 1000) + (60 * 60 * 24), // Default 24h + exp: expiration || Math.floor(Date.now() / 1000) + 60 * 60 * 24, // Default 24h purpose_code: purposeCode, allowed_merchants: allowedMerchants, iat: Math.floor(Date.now() / 1000), - type: 'intent_mandate' + type: "intent_mandate", }; - return jwt.sign(payload, this.signingKey, { algorithm: 'HS256' }); + return jwt.sign(payload, this.signingKey, { algorithm: "HS256" }); } /** @@ -48,27 +59,42 @@ class MandateService { * @param {Object} params - Cart parameters * @returns {Promise} Signed JWT Cart Mandate */ - async issueCartMandate({ intentMandate, cartItems, totalPrice, merchantDid }) { + async issueCartMandate({ + intentMandate, + cartItems, + totalPrice, + merchantDid, + }) { const decodedIntent = await this.verifyMandate(intentMandate); - if (decodedIntent.type !== 'intent_mandate') { - throw new Error('Invalid intent mandate type'); + if (decodedIntent.type !== "intent_mandate") { + throw new Error( + "Zero Trust Validation Failed: Invalid intent mandate type", + ); } // Verify budget if (totalPrice > decodedIntent.max_budget.value) { - throw new Error('Cart total exceeds intent mandate budget'); + throw new Error( + "Zero Trust Validation Failed: Cart total exceeds intent mandate budget", + ); } // Verify merchant if whitelist exists - if (decodedIntent.allowed_merchants.length > 0 && !decodedIntent.allowed_merchants.includes(merchantDid)) { - throw new Error(`Merchant ${merchantDid} is not authorized by this mandate`); + if ( + decodedIntent.allowed_merchants.length > 0 && + !decodedIntent.allowed_merchants.includes(merchantDid) + ) { + throw new Error( + `Zero Trust Validation Failed: Merchant ${merchantDid} is not authorized by this mandate`, + ); } // Create cryptographic hash of cart - const cartHash = crypto.createHash('sha256') + const cartHash = crypto + .createHash("sha256") .update(JSON.stringify({ items: cartItems, total: totalPrice })) - .digest('hex'); + .digest("hex"); const payload = { iss: this.issuer, @@ -79,10 +105,10 @@ class MandateService { merchant_did: merchantDid, iat: Math.floor(Date.now() / 1000), exp: decodedIntent.exp, // Inherit expiration from intent - type: 'cart_mandate' + type: "cart_mandate", }; - return jwt.sign(payload, this.signingKey, { algorithm: 'HS256' }); + return jwt.sign(payload, this.signingKey, { algorithm: "HS256" }); } /** @@ -92,9 +118,11 @@ class MandateService { */ async verifyMandate(token) { try { - return jwt.verify(token, this.signingKey, { algorithms: ['HS256'] }); + return jwt.verify(token, this.signingKey, { algorithms: ["HS256"] }); } catch (error) { - throw new Error(`Mandate verification failed: ${error.message}`); + throw new Error( + `Zero Trust Validation Failed: Mandate verification failed: ${error.message}`, + ); } } @@ -108,20 +136,20 @@ class MandateService { sub: agentDid, nbf: Math.floor(Date.now() / 1000), vc: { - '@context': [ - 'https://www.w3.org/2018/credentials/v1', - 'https://open-commerce-protocol.io/contexts/agent/v1' + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://open-commerce-protocol.io/contexts/agent/v1", ], - type: ['VerifiableCredential', 'AgentAuthorityCredential'], + type: ["VerifiableCredential", "AgentAuthorityCredential"], credentialSubject: { id: agentDid, authorizedBy: userDid, - capabilities: capabilities - } - } + capabilities: capabilities, + }, + }, }; - return jwt.sign(payload, this.signingKey, { algorithm: 'HS256' }); + return jwt.sign(payload, this.signingKey, { algorithm: "HS256" }); } /** diff --git a/src/services/mobilePayment.js b/src/services/mobilePayment.js index e4d732a..25d61d7 100644 --- a/src/services/mobilePayment.js +++ b/src/services/mobilePayment.js @@ -5,9 +5,9 @@ * Processes encrypted payment data and creates secure tokens. */ -const TokenizationService = require('./tokenization'); -const crypto = require('crypto'); -const logger = require('../utils/logger'); +const TokenizationService = require("./tokenization"); +const crypto = require("crypto"); +const logger = require("../utils/logger"); class MobilePaymentService { constructor(tokenizationService, walletService, config = {}) { @@ -15,16 +15,25 @@ class MobilePaymentService { this.wallet = walletService; this.config = { applePay: { - merchantId: config.applePay?.merchantId || process.env.APPLE_PAY_MERCHANT_ID, - merchantName: config.applePay?.merchantName || 'Open Commerce Initiative (OCI)', - countryCode: config.applePay?.countryCode || 'US', - supportedNetworks: config.applePay?.supportedNetworks || ['visa', 'mastercard', 'amex', 'discover'] + merchantId: + config.applePay?.merchantId || process.env.APPLE_PAY_MERCHANT_ID, + merchantName: + config.applePay?.merchantName || "Open Commerce Initiative (OCI)", + countryCode: config.applePay?.countryCode || "US", + supportedNetworks: config.applePay?.supportedNetworks || [ + "visa", + "mastercard", + "amex", + "discover", + ], }, googlePay: { - merchantId: config.googlePay?.merchantId || process.env.GOOGLE_PAY_MERCHANT_ID, - merchantName: config.googlePay?.merchantName || 'Open Commerce Initiative (OCI)', - environment: config.googlePay?.environment || 'PRODUCTION' - } + merchantId: + config.googlePay?.merchantId || process.env.GOOGLE_PAY_MERCHANT_ID, + merchantName: + config.googlePay?.merchantName || "Open Commerce Initiative (OCI)", + environment: config.googlePay?.environment || "PRODUCTION", + }, }; } @@ -36,16 +45,16 @@ class MobilePaymentService { * @param {string} params.currency - Currency code * @returns {Promise} Session data */ - async initializeApplePay({ walletId, amount, currency = 'USD' }) { + async initializeApplePay({ walletId, amount, currency = "USD" }) { try { // Validate wallet const wallet = await this.wallet.getWallet(walletId); - if (!wallet || wallet.status !== 'active') { - throw new Error('Invalid or inactive wallet'); + if (!wallet || wallet.status !== "active") { + throw new Error("Invalid or inactive wallet"); } // Generate session ID - const sessionId = this._generateSessionId('applepay'); + const sessionId = this._generateSessionId("applepay"); // Create payment request configuration const paymentRequest = { @@ -55,12 +64,12 @@ class MobilePaymentService { countryCode: this.config.applePay.countryCode, currencyCode: currency, supportedNetworks: this.config.applePay.supportedNetworks, - merchantCapabilities: ['supports3DS'], + merchantCapabilities: ["supports3DS"], total: { label: `Add ${amount} ${currency} to wallet`, amount: amount.toFixed(2), - type: 'final' - } + type: "final", + }, }; // Store session temporarily (in production, use Redis or similar) @@ -68,13 +77,13 @@ class MobilePaymentService { walletId, amount, currency, - type: 'applepay', - expiresAt: Date.now() + (15 * 60 * 1000) // 15 minutes + type: "applepay", + expiresAt: Date.now() + 15 * 60 * 1000, // 15 minutes }); return paymentRequest; } catch (error) { - throw this._handleError('initializeApplePay', error); + throw this._handleError("initializeApplePay", error); } } @@ -90,11 +99,11 @@ class MobilePaymentService { // Retrieve session const session = await this._getSession(sessionId); if (!session) { - throw new Error('Invalid or expired session'); + throw new Error("Invalid or expired session"); } - if (session.type !== 'applepay') { - throw new Error('Invalid session type'); + if (session.type !== "applepay") { + throw new Error("Invalid session type"); } // Validate payment data structure @@ -105,7 +114,7 @@ class MobilePaymentService { wallet_id: session.walletId, session_id: sessionId, amount: session.amount, - currency: session.currency + currency: session.currency, }); // Add funds to wallet @@ -115,11 +124,11 @@ class MobilePaymentService { paymentToken: token.id, description: `Apple Pay funding - ${session.amount} ${session.currency}`, metadata: { - payment_method: 'apple_pay', + payment_method: "apple_pay", payment_network: token.payment_network, transaction_id: token.transaction_id, - session_id: sessionId - } + session_id: sessionId, + }, }); // Clean up session @@ -134,11 +143,11 @@ class MobilePaymentService { token: { id: token.id, last4: this._extractLast4FromToken(token), - network: token.payment_network - } + network: token.payment_network, + }, }; } catch (error) { - throw this._handleError('processApplePay', error); + throw this._handleError("processApplePay", error); } } @@ -150,16 +159,16 @@ class MobilePaymentService { * @param {string} params.currency - Currency code * @returns {Promise} Session data */ - async initializeGooglePay({ walletId, amount, currency = 'USD' }) { + async initializeGooglePay({ walletId, amount, currency = "USD" }) { try { // Validate wallet const wallet = await this.wallet.getWallet(walletId); - if (!wallet || wallet.status !== 'active') { - throw new Error('Invalid or inactive wallet'); + if (!wallet || wallet.status !== "active") { + throw new Error("Invalid or inactive wallet"); } // Generate session ID - const sessionId = this._generateSessionId('googlepay'); + const sessionId = this._generateSessionId("googlepay"); // Create payment configuration const paymentConfig = { @@ -167,28 +176,30 @@ class MobilePaymentService { environment: this.config.googlePay.environment, merchantInfo: { merchantId: this.config.googlePay.merchantId, - merchantName: this.config.googlePay.merchantName + merchantName: this.config.googlePay.merchantName, }, transactionInfo: { - totalPriceStatus: 'FINAL', + totalPriceStatus: "FINAL", totalPrice: amount.toFixed(2), currencyCode: currency, - countryCode: this.config.applePay.countryCode + countryCode: this.config.applePay.countryCode, }, - allowedPaymentMethods: [{ - type: 'CARD', - parameters: { - allowedAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS'], - allowedCardNetworks: ['MASTERCARD', 'VISA', 'AMEX', 'DISCOVER'] - }, - tokenizationSpecification: { - type: 'PAYMENT_GATEWAY', + allowedPaymentMethods: [ + { + type: "CARD", parameters: { - gateway: 'basistheory', - gatewayMerchantId: this.config.googlePay.merchantId - } - } - }] + allowedAuthMethods: ["PAN_ONLY", "CRYPTOGRAM_3DS"], + allowedCardNetworks: ["MASTERCARD", "VISA", "AMEX", "DISCOVER"], + }, + tokenizationSpecification: { + type: "PAYMENT_GATEWAY", + parameters: { + gateway: "basistheory", + gatewayMerchantId: this.config.googlePay.merchantId, + }, + }, + }, + ], }; // Store session @@ -196,13 +207,13 @@ class MobilePaymentService { walletId, amount, currency, - type: 'googlepay', - expiresAt: Date.now() + (15 * 60 * 1000) + type: "googlepay", + expiresAt: Date.now() + 15 * 60 * 1000, }); return paymentConfig; } catch (error) { - throw this._handleError('initializeGooglePay', error); + throw this._handleError("initializeGooglePay", error); } } @@ -218,11 +229,11 @@ class MobilePaymentService { // Retrieve session const session = await this._getSession(sessionId); if (!session) { - throw new Error('Invalid or expired session'); + throw new Error("Invalid or expired session"); } - if (session.type !== 'googlepay') { - throw new Error('Invalid session type'); + if (session.type !== "googlepay") { + throw new Error("Invalid session type"); } // Validate payment data structure @@ -233,7 +244,7 @@ class MobilePaymentService { wallet_id: session.walletId, session_id: sessionId, amount: session.amount, - currency: session.currency + currency: session.currency, }); // Add funds to wallet @@ -243,10 +254,10 @@ class MobilePaymentService { paymentToken: token.id, description: `Google Pay funding - ${session.amount} ${session.currency}`, metadata: { - payment_method: 'google_pay', + payment_method: "google_pay", payment_network: token.payment_network, - session_id: sessionId - } + session_id: sessionId, + }, }); // Clean up session @@ -260,11 +271,11 @@ class MobilePaymentService { newBalance: result.newBalance, token: { id: token.id, - network: token.payment_network - } + network: token.payment_network, + }, }; } catch (error) { - throw this._handleError('processGooglePay', error); + throw this._handleError("processGooglePay", error); } } @@ -277,18 +288,18 @@ class MobilePaymentService { try { const session = await this._getSession(sessionId); if (!session) { - return { status: 'expired', sessionId }; + return { status: "expired", sessionId }; } const isExpired = Date.now() > session.expiresAt; return { - status: isExpired ? 'expired' : 'active', + status: isExpired ? "expired" : "active", sessionId, type: session.type, - expiresAt: new Date(session.expiresAt).toISOString() + expiresAt: new Date(session.expiresAt).toISOString(), }; } catch (error) { - throw this._handleError('getSessionStatus', error); + throw this._handleError("getSessionStatus", error); } } @@ -301,7 +312,7 @@ class MobilePaymentService { try { await this._deleteSession(sessionId); } catch (error) { - throw this._handleError('cancelSession', error); + throw this._handleError("cancelSession", error); } } @@ -310,16 +321,16 @@ class MobilePaymentService { * @private */ _validateApplePayData(paymentData) { - if (!paymentData || typeof paymentData !== 'object') { - throw new Error('Invalid Apple Pay payment data'); + if (!paymentData || typeof paymentData !== "object") { + throw new Error("Invalid Apple Pay payment data"); } if (!paymentData.data || !paymentData.signature || !paymentData.version) { - throw new Error('Missing required Apple Pay payment data fields'); + throw new Error("Missing required Apple Pay payment data fields"); } if (!paymentData.header || !paymentData.header.ephemeralPublicKey) { - throw new Error('Missing Apple Pay header information'); + throw new Error("Missing Apple Pay header information"); } } @@ -328,16 +339,16 @@ class MobilePaymentService { * @private */ _validateGooglePayData(paymentData) { - if (!paymentData || typeof paymentData !== 'object') { - throw new Error('Invalid Google Pay payment data'); + if (!paymentData || typeof paymentData !== "object") { + throw new Error("Invalid Google Pay payment data"); } if (!paymentData.paymentMethodData) { - throw new Error('Missing payment method data'); + throw new Error("Missing payment method data"); } if (!paymentData.paymentMethodData.tokenizationData) { - throw new Error('Missing tokenization data'); + throw new Error("Missing tokenization data"); } } @@ -347,7 +358,7 @@ class MobilePaymentService { */ _generateSessionId(type) { const timestamp = Date.now(); - const random = crypto.randomBytes(16).toString('hex'); + const random = crypto.randomBytes(16).toString("hex"); return `${type}_session_${timestamp}_${random}`; } @@ -396,7 +407,7 @@ class MobilePaymentService { */ _extractLast4FromToken(token) { // This would depend on the token structure from your provider - return token.last4 || '****'; + return token.last4 || "****"; } /** diff --git a/src/services/tokenization.js b/src/services/tokenization.js index e66bc50..0ecaa28 100644 --- a/src/services/tokenization.js +++ b/src/services/tokenization.js @@ -5,31 +5,36 @@ * Supports PCI DSS compliant token creation, retrieval, and management. */ -const axios = require('axios'); -const crypto = require('crypto'); -const MandateService = require('./mandate'); -const logger = require('../utils/logger'); +const axios = require("axios"); +const crypto = require("crypto"); +const MandateService = require("./mandate"); +const logger = require("../utils/logger"); class TokenizationService { constructor(config = {}) { this.apiKey = config.apiKey || process.env.TOKENIZATION_API_KEY; - this.baseURL = config.baseURL || process.env.TOKENIZATION_BASE_URL || 'https://api.basistheory.com'; + this.baseURL = + config.baseURL || + process.env.TOKENIZATION_BASE_URL || + "https://api.basistheory.com"; this.tenantId = config.tenantId || process.env.TOKENIZATION_TENANT_ID; this.timeout = config.timeout || 30000; - this.strictMandateMode = config.strictMandateMode !== undefined ? - config.strictMandateMode : (process.env.STRICT_MANDATE_MODE === 'true'); + this.strictMandateMode = + config.strictMandateMode !== undefined + ? config.strictMandateMode + : process.env.STRICT_MANDATE_MODE === "true"; if (!this.apiKey) { - throw new Error('Tokenization API key is required'); + throw new Error("Tokenization API key is required"); } this.client = axios.create({ baseURL: this.baseURL, timeout: this.timeout, headers: { - 'BT-API-KEY': this.apiKey, - 'Content-Type': 'application/json' - } + "BT-API-KEY": this.apiKey, + "Content-Type": "application/json", + }, }); this.mandateService = new MandateService(config.mandateConfig); @@ -47,20 +52,20 @@ class TokenizationService { */ async createCardToken(cardData, metadata = {}) { try { - const response = await this.client.post('/tokens', { - type: 'card', + const response = await this.client.post("/tokens", { + type: "card", data: { number: cardData.number, expiration_month: cardData.exp_month, expiration_year: cardData.exp_year, - cvc: cardData.cvc + cvc: cardData.cvc, }, metadata: { ...metadata, - created_at: new Date().toISOString() + created_at: new Date().toISOString(), }, - search_indexes: ['{{data.number | last4}}'], - fingerprint_expression: '{{data.number}}' + search_indexes: ["{{data.number | last4}}"], + fingerprint_expression: "{{data.number}}", }); return { @@ -71,10 +76,10 @@ class TokenizationService { exp_month: cardData.exp_month, exp_year: cardData.exp_year, fingerprint: response.data.fingerprint, - created_at: response.data.created_at + created_at: response.data.created_at, }; } catch (error) { - throw this._handleError(error); + throw this._handleError("createCardToken", error); } } @@ -86,25 +91,25 @@ class TokenizationService { */ async createApplePayToken(paymentData, metadata = {}) { try { - const response = await this.client.post('/tokens', { - type: 'applepay', + const response = await this.client.post("/tokens", { + type: "applepay", data: paymentData, metadata: { ...metadata, - payment_method: 'apple_pay', - created_at: new Date().toISOString() - } + payment_method: "apple_pay", + created_at: new Date().toISOString(), + }, }); return { id: response.data.id, - type: 'applepay', + type: "applepay", payment_network: paymentData.header?.network, transaction_id: paymentData.header?.transactionId, - created_at: response.data.created_at + created_at: response.data.created_at, }; } catch (error) { - throw this._handleError(error); + throw this._handleError("createApplePayToken", error); } } @@ -116,24 +121,24 @@ class TokenizationService { */ async createGooglePayToken(paymentData, metadata = {}) { try { - const response = await this.client.post('/tokens', { - type: 'googlepay', + const response = await this.client.post("/tokens", { + type: "googlepay", data: paymentData, metadata: { ...metadata, - payment_method: 'google_pay', - created_at: new Date().toISOString() - } + payment_method: "google_pay", + created_at: new Date().toISOString(), + }, }); return { id: response.data.id, - type: 'googlepay', + type: "googlepay", payment_network: paymentData.paymentMethodData?.info?.cardNetwork, - created_at: response.data.created_at + created_at: response.data.created_at, }; } catch (error) { - throw this._handleError(error); + throw this._handleError("createGooglePayToken", error); } } @@ -147,7 +152,7 @@ class TokenizationService { const response = await this.client.get(`/tokens/${tokenId}`); return response.data; } catch (error) { - throw this._handleError(error); + throw this._handleError("getToken", error); } } @@ -160,7 +165,7 @@ class TokenizationService { try { await this.client.delete(`/tokens/${tokenId}`); } catch (error) { - throw this._handleError(error); + throw this._handleError("deleteToken", error); } } @@ -172,18 +177,18 @@ class TokenizationService { * @param {Object} metadata - Optional metadata * @returns {Promise} Payment result */ - async processPayment(tokenId, amount, currency = 'USD', metadata = {}) { + async processPayment(tokenId, amount, currency = "USD", metadata = {}) { try { // This would integrate with your payment processor // Example implementation with a generic payment API - const response = await this.client.post('/payments', { + const response = await this.client.post("/payments", { token: tokenId, amount, currency, metadata: { ...metadata, - processed_at: new Date().toISOString() - } + processed_at: new Date().toISOString(), + }, }); return { @@ -192,10 +197,10 @@ class TokenizationService { amount, currency, token_id: tokenId, - created_at: response.data.created_at + created_at: response.data.created_at, }; } catch (error) { - throw this._handleError(error); + throw this._handleError("processPayment", error); } } @@ -206,15 +211,18 @@ class TokenizationService { * @param {string} reason - Refund reason * @returns {Promise} Refund result */ - async createRefund(paymentId, amount, reason = 'requested_by_customer') { + async createRefund(paymentId, amount, reason = "requested_by_customer") { try { - const response = await this.client.post(`/payments/${paymentId}/refunds`, { - amount, - reason, - metadata: { - refunded_at: new Date().toISOString() - } - }); + const response = await this.client.post( + `/payments/${paymentId}/refunds`, + { + amount, + reason, + metadata: { + refunded_at: new Date().toISOString(), + }, + }, + ); return { id: response.data.id, @@ -222,10 +230,10 @@ class TokenizationService { amount, reason, status: response.data.status, - created_at: response.data.created_at + created_at: response.data.created_at, }; } catch (error) { - throw this._handleError(error); + throw this._handleError("createRefund", error); } } @@ -236,10 +244,10 @@ class TokenizationService { */ async searchTokens(criteria) { try { - const response = await this.client.post('/tokens/search', criteria); + const response = await this.client.post("/tokens/search", criteria); return response.data.data || []; } catch (error) { - throw this._handleError(error); + throw this._handleError("searchTokens", error); } } @@ -262,15 +270,15 @@ class TokenizationService { * @private */ _detectCardBrand(cardNumber) { - const number = cardNumber.replace(/\s/g, ''); + const number = cardNumber.replace(/\s/g, ""); - if (/^4/.test(number)) return 'visa'; - if (/^5[1-5]/.test(number)) return 'mastercard'; - if (/^3[47]/.test(number)) return 'amex'; - if (/^6(?:011|5)/.test(number)) return 'discover'; - if (/^(?:2131|1800|35)/.test(number)) return 'jcb'; + if (/^4/.test(number)) return "visa"; + if (/^5[1-5]/.test(number)) return "mastercard"; + if (/^3[47]/.test(number)) return "amex"; + if (/^6(?:011|5)/.test(number)) return "discover"; + if (/^(?:2131|1800|35)/.test(number)) return "jcb"; - return 'unknown'; + return "unknown"; } /** @@ -281,34 +289,34 @@ class TokenizationService { */ async createSecretToken(secret, metadata = {}) { // Mock for testing/simulation - if (this.apiKey === 'test-key' || process.env.NODE_ENV === 'test') { + if (this.apiKey === "test-key" || process.env.NODE_ENV === "test") { return { - id: `token_${crypto.randomBytes(8).toString('hex')}`, - type: 'secret', + id: `token_${crypto.randomBytes(8).toString("hex")}`, + type: "secret", created_at: new Date().toISOString(), - metadata: { ...metadata, type: 'secret_key' } + metadata: { ...metadata, type: "secret_key" }, }; } try { - const response = await this.client.post('/tokens', { - type: 'token', + const response = await this.client.post("/tokens", { + type: "token", data: secret, metadata: { ...metadata, - type: 'secret_key', - created_at: new Date().toISOString() - } + type: "secret_key", + created_at: new Date().toISOString(), + }, }); return { id: response.data.id, - type: 'secret', + type: "secret", created_at: response.data.created_at, - metadata: response.data.metadata + metadata: response.data.metadata, }; } catch (error) { - throw this._handleError(error); + throw this._handleError("createSecretToken", error); } } @@ -327,8 +335,8 @@ class TokenizationService { try { decodedMandate = await this.mandateService.verifyMandate(mandate); } catch (error) { - if (error.message.includes('jwt expired')) { - throw new Error('Zero Trust Validation Failed: Mandate has expired'); + if (error.message.includes("jwt expired")) { + throw new Error("Zero Trust Validation Failed: Mandate has expired"); } throw new Error(`Zero Trust Validation Failed: ${error.message}`); } @@ -336,50 +344,64 @@ class TokenizationService { // Validate budget if context amount is provided if (context.amount) { // Check Intent Mandate budget - if (decodedMandate.max_budget && context.amount > decodedMandate.max_budget.value) { - throw new Error(`Zero Trust Validation Failed: Amount ${context.amount} exceeds mandate budget of ${decodedMandate.max_budget.value}`); + if ( + decodedMandate.max_budget && + context.amount > decodedMandate.max_budget.value + ) { + throw new Error( + `Zero Trust Validation Failed: Amount ${context.amount} exceeds mandate budget of ${decodedMandate.max_budget.value}`, + ); } // Check Cart Mandate total price - if (decodedMandate.total_price && context.amount !== decodedMandate.total_price) { - throw new Error(`Zero Trust Validation Failed: Amount ${context.amount} does not match cart mandate total of ${decodedMandate.total_price}`); + if ( + decodedMandate.total_price && + context.amount !== decodedMandate.total_price + ) { + throw new Error( + `Zero Trust Validation Failed: Amount ${context.amount} does not match cart mandate total of ${decodedMandate.total_price}`, + ); } } // Validate merchant if context merchant is provided if (context.merchant && decodedMandate.allowed_merchants?.length > 0) { if (!decodedMandate.allowed_merchants.includes(context.merchant)) { - throw new Error(`Zero Trust Validation Failed: Merchant ${context.merchant} not authorized by mandate`); + throw new Error( + `Zero Trust Validation Failed: Merchant ${context.merchant} not authorized by mandate`, + ); } } // Validate expiration if (decodedMandate.exp < Math.floor(Date.now() / 1000)) { - throw new Error('Zero Trust Validation Failed: Mandate has expired'); + throw new Error("Zero Trust Validation Failed: Mandate has expired"); } } else if (this.strictMandateMode) { - throw new Error('Zero Trust Validation Failed: Mandate required for signing in strict mode'); + throw new Error( + "Zero Trust Validation Failed: Mandate required for signing in strict mode", + ); } try { // In a real implementation, this would call a Basis Theory Reactor // providing the tokenId. The Reactor would securely retrieve the // secret and sign the data without exposing the key. - const response = await this.client.post('/reactors/sign', { + const response = await this.client.post("/reactors/sign", { args: { tokenId, data: dataToSign, - mandate // Pass mandate to reactor for server-side validation - } + mandate, // Pass mandate to reactor for server-side validation + }, }); return response.data.signature; } catch (error) { // Fallback for simulation/testing if reactor endpoint doesn't exist // We assume for simulation that we can just "mock" a signature - if (process.env.NODE_ENV !== 'production' || this.apiKey === 'test-key') { - return `0x_mock_signature_of_${dataToSign}_with_${tokenId}${mandate ? '_validated_by_mandate' : ''}`; + if (process.env.NODE_ENV !== "production" || this.apiKey === "test-key") { + return `0x_mock_signature_of_${dataToSign}_with_${tokenId}${mandate ? "_validated_by_mandate" : ""}`; } - throw this._handleError(error); + throw this._handleError("signWithToken", error); } } @@ -387,18 +409,18 @@ class TokenizationService { * Handle and format errors * @private */ - _handleError(error) { - logger.error('TokenizationService error:', error); + _handleError(method, error) { + logger.error(`TokenizationService.${method} error:`, error); if (error.response) { const { status, data } = error.response; return new Error( - `Tokenization error (${status}): ${data.message || data.error || 'Unknown error'}` + `Tokenization error (${status}): ${data.message || data.error || "Unknown error"}`, ); } if (error.request) { - return new Error('Tokenization service unavailable'); + return new Error("Tokenization service unavailable"); } return error; diff --git a/src/services/ucp.js b/src/services/ucp.js index 1233e47..918d0b7 100644 --- a/src/services/ucp.js +++ b/src/services/ucp.js @@ -6,8 +6,8 @@ * system-specific transaction logic via A2AService. */ -const Joi = require('joi'); -const logger = require('../utils/logger'); +const Joi = require("joi"); +const logger = require("../utils/logger"); class UCPService { constructor(a2aService, config = {}) { @@ -17,21 +17,25 @@ class UCPService { // Standard UCP Schema this.ucpIntentSchema = Joi.object({ ver: Joi.string().required(), - intent: Joi.string().valid('transfer', 'payment', 'purchase', 'request', 'offer').required(), + intent: Joi.string() + .valid("transfer", "payment", "purchase", "request", "offer") + .required(), sender: Joi.object({ agent_id: Joi.string().required(), - wallet_id: Joi.string().optional() + wallet_id: Joi.string().optional(), }).required(), recipient: Joi.object({ agent_id: Joi.string().required(), - wallet_id: Joi.string().optional() + wallet_id: Joi.string().optional(), }).optional(), amount: Joi.object({ value: Joi.number().positive().required(), - currency: Joi.string().default('USD') + currency: Joi.string().default("USD"), }).optional(), data: Joi.object().optional(), - timestamp: Joi.date().iso().default(() => new Date()) + timestamp: Joi.date() + .iso() + .default(() => new Date()), }); } @@ -42,26 +46,38 @@ class UCPService { async processPayload(payload) { try { // 1. Validate the UCP intent against schema - const { error, value } = this.ucpIntentSchema.validate(payload, { stripUnknown: true }); + const { error, value } = this.ucpIntentSchema.validate(payload, { + stripUnknown: true, + }); if (error) { - throw new Error(`UCP Intent validation failed: ${error.details.map(x => x.message).join(', ')}`); + throw new Error( + `Zero Trust Validation Failed: UCP Intent validation failed: ${error.details.map((x) => x.message).join(", ")}`, + ); } const validatedPayload = value; const { intent } = validatedPayload; switch (intent) { - case 'transfer': - case 'payment': + case "transfer": + case "payment": return this._handleTransfer(validatedPayload); - case 'purchase': + case "purchase": // Future implementation: integration with Inventory/Order services - return { status: 'success', message: 'Purchase intent received (simulation)', payload: validatedPayload }; + return { + status: "success", + message: "Purchase intent received (simulation)", + payload: validatedPayload, + }; default: - return { status: 'success', message: `UCP intent '${intent}' received and logged.`, payload: validatedPayload }; + return { + status: "success", + message: `UCP intent '${intent}' received and logged.`, + payload: validatedPayload, + }; } } catch (error) { - throw this._handleError('processPayload', error); + throw this._handleError("processPayload", error); } } @@ -73,17 +89,17 @@ class UCPService { const { sender, recipient, amount } = payload; if (!recipient?.agent_id) { - throw new Error('Missing recipient agent_id for transfer'); + throw new Error("Missing recipient agent_id for transfer"); } if (!amount?.value) { - throw new Error('Missing amount value'); + throw new Error("Missing amount value"); } return this.a2aService.executeTransfer({ fromAgentId: sender.agent_id, toAgentId: recipient.agent_id, amount: amount.value, - ucpPayload: payload + ucpPayload: payload, }); } @@ -97,25 +113,28 @@ class UCPService { type: "object", properties: { ver: { type: "string" }, - intent: { type: "string", enum: ["transfer", "payment", "purchase", "request", "offer"] }, + intent: { + type: "string", + enum: ["transfer", "payment", "purchase", "request", "offer"], + }, sender: { type: "object", properties: { agent_id: { type: "string" } }, - required: ["agent_id"] + required: ["agent_id"], }, recipient: { type: "object", - properties: { agent_id: { type: "string" } } + properties: { agent_id: { type: "string" } }, }, amount: { type: "object", properties: { value: { type: "number" }, - currency: { type: "string" } - } - } + currency: { type: "string" }, + }, + }, }, - required: ["ver", "intent", "sender"] + required: ["ver", "intent", "sender"], }; } diff --git a/src/services/wallet.js b/src/services/wallet.js index 6987b05..3c7afe9 100644 --- a/src/services/wallet.js +++ b/src/services/wallet.js @@ -5,21 +5,21 @@ * transaction processing, and wallet lifecycle management. */ -const crypto = require('crypto'); -const logger = require('../utils/logger'); +const crypto = require("crypto"); +const logger = require("../utils/logger"); class WalletService { constructor(database, config = {}) { this.db = database; this.config = { - defaultCurrency: config.defaultCurrency || 'USD', + defaultCurrency: config.defaultCurrency || "USD", minBalance: config.minBalance || 0, maxBalance: config.maxBalance || 10000, autoTopUp: config.autoTopUp || { enabled: false, threshold: 10, - amount: 50 - } + amount: 50, + }, }; } @@ -36,16 +36,20 @@ class WalletService { // Check if user already has a wallet const existingWallet = await this.db.findWalletByUserId(userId); if (existingWallet) { - throw new Error('User already has a wallet'); + throw new Error("User already has a wallet"); } // Validate initial balance if (initialBalance < this.config.minBalance) { - throw new Error(`Initial balance must be at least ${this.config.minBalance}`); + throw new Error( + `Initial balance must be at least ${this.config.minBalance}`, + ); } if (initialBalance > this.config.maxBalance) { - throw new Error(`Initial balance cannot exceed ${this.config.maxBalance}`); + throw new Error( + `Initial balance cannot exceed ${this.config.maxBalance}`, + ); } // Create wallet @@ -53,28 +57,28 @@ class WalletService { userId, balance: initialBalance, currency: currency || this.config.defaultCurrency, - status: 'active', + status: "active", metadata: {}, createdAt: new Date(), - updatedAt: new Date() + updatedAt: new Date(), }); // Create initial transaction if balance > 0 if (initialBalance > 0) { await this.db.createTransaction({ walletId: wallet.id, - type: 'credit', + type: "credit", amount: initialBalance, - description: 'Initial wallet funding', - status: 'completed', - metadata: { source: 'wallet_creation' }, - createdAt: new Date() + description: "Initial wallet funding", + status: "completed", + metadata: { source: "wallet_creation" }, + createdAt: new Date(), }); } return wallet; } catch (error) { - throw this._handleError('createWallet', error); + throw this._handleError("createWallet", error); } } @@ -87,11 +91,11 @@ class WalletService { try { const wallet = await this.db.findWalletById(walletId); if (!wallet) { - throw new Error('Wallet not found'); + throw new Error("Wallet not found"); } return wallet; } catch (error) { - throw this._handleError('getWallet', error); + throw this._handleError("getWallet", error); } } @@ -104,11 +108,11 @@ class WalletService { try { const wallet = await this.db.findWalletByUserId(userId); if (!wallet) { - throw new Error('Wallet not found for user'); + throw new Error("Wallet not found for user"); } return wallet; } catch (error) { - throw this._handleError('getWalletByUserId', error); + throw this._handleError("getWalletByUserId", error); } } @@ -122,35 +126,44 @@ class WalletService { * @param {Object} params.metadata - Optional metadata * @returns {Promise} Transaction result */ - async addFunds({ walletId, amount, paymentToken, description, metadata = {} }) { + async addFunds({ + walletId, + amount, + paymentToken, + description, + metadata = {}, + }) { try { // Validate amount if (amount <= 0) { - throw new Error('Amount must be greater than 0'); + throw new Error("Amount must be greater than 0"); } // Get wallet const wallet = await this.getWallet(walletId); - if (wallet.status !== 'active') { - throw new Error('Wallet is not active'); + if (wallet.status !== "active") { + throw new Error("Wallet is not active"); } // Check if new balance would exceed max const newBalance = wallet.balance + amount; if (newBalance > this.config.maxBalance) { - throw new Error(`Balance would exceed maximum of ${this.config.maxBalance}`); + throw new Error( + `Balance would exceed maximum of ${this.config.maxBalance}`, + ); } // Extract top-level fields from metadata if they exist - const { agentId, counterpartyAgentId, ucpPayload, ...restMetadata } = metadata; + const { agentId, counterpartyAgentId, ucpPayload, ...restMetadata } = + metadata; // Create transaction const transaction = await this.db.createTransaction({ walletId, - type: 'credit', + type: "credit", amount, - description: description || 'Add funds', - status: 'pending', + description: description || "Add funds", + status: "pending", paymentToken, agentId, counterpartyAgentId, @@ -158,9 +171,9 @@ class WalletService { metadata: { ...restMetadata, previous_balance: wallet.balance, - new_balance: newBalance + new_balance: newBalance, }, - createdAt: new Date() + createdAt: new Date(), }); // Update wallet balance atomically @@ -168,18 +181,18 @@ class WalletService { // Update transaction status await this.db.updateTransaction(transaction.id, { - status: 'completed', - completedAt: new Date() + status: "completed", + completedAt: new Date(), }); return { transactionId: transaction.id, amount, newBalance, - status: 'completed' + status: "completed", }; } catch (error) { - throw this._handleError('addFunds', error); + throw this._handleError("addFunds", error); } } @@ -196,41 +209,42 @@ class WalletService { try { // Validate amount if (amount <= 0) { - throw new Error('Amount must be greater than 0'); + throw new Error("Amount must be greater than 0"); } // Get wallet const wallet = await this.getWallet(walletId); - if (wallet.status !== 'active') { - throw new Error('Wallet is not active'); + if (wallet.status !== "active") { + throw new Error("Wallet is not active"); } // Check sufficient balance if (wallet.balance < amount) { - throw new Error('Insufficient balance'); + throw new Error("Insufficient balance"); } const newBalance = wallet.balance - amount; // Extract top-level fields from metadata if they exist - const { agentId, counterpartyAgentId, ucpPayload, ...restMetadata } = metadata; + const { agentId, counterpartyAgentId, ucpPayload, ...restMetadata } = + metadata; // Create transaction const transaction = await this.db.createTransaction({ walletId, - type: 'debit', + type: "debit", amount: -amount, - description: description || 'Payment', - status: 'pending', + description: description || "Payment", + status: "pending", agentId, counterpartyAgentId, ucpPayload, metadata: { ...restMetadata, previous_balance: wallet.balance, - new_balance: newBalance + new_balance: newBalance, }, - createdAt: new Date() + createdAt: new Date(), }); // Update wallet balance atomically @@ -238,12 +252,15 @@ class WalletService { // Update transaction status await this.db.updateTransaction(transaction.id, { - status: 'completed', - completedAt: new Date() + status: "completed", + completedAt: new Date(), }); // Check if auto top-up is needed - if (this.config.autoTopUp.enabled && newBalance < this.config.autoTopUp.threshold) { + if ( + this.config.autoTopUp.enabled && + newBalance < this.config.autoTopUp.threshold + ) { await this._triggerAutoTopUp(walletId, newBalance); } @@ -251,10 +268,10 @@ class WalletService { transactionId: transaction.id, amount: -amount, newBalance, - status: 'completed' + status: "completed", }; } catch (error) { - throw this._handleError('deductFunds', error); + throw this._handleError("deductFunds", error); } } @@ -267,14 +284,20 @@ class WalletService { * @param {string} params.description - Transfer description * @returns {Promise} Transfer result */ - async transfer({ fromWalletId, toWalletId, amount, description, metadata = {} }) { + async transfer({ + fromWalletId, + toWalletId, + amount, + description, + metadata = {}, + }) { try { if (amount <= 0) { - throw new Error('Amount must be greater than 0'); + throw new Error("Amount must be greater than 0"); } if (fromWalletId === toWalletId) { - throw new Error('Cannot transfer to same wallet'); + throw new Error("Cannot transfer to same wallet"); } // Start transaction @@ -285,7 +308,11 @@ class WalletService { walletId: fromWalletId, amount, description: description || `Transfer to wallet ${toWalletId}`, - metadata: { ...metadata, transfer_id: transferId, type: 'transfer_out' } + metadata: { + ...metadata, + transfer_id: transferId, + type: "transfer_out", + }, }); // Add to destination wallet @@ -294,7 +321,7 @@ class WalletService { amount, paymentToken: null, description: description || `Transfer from wallet ${fromWalletId}`, - metadata: { ...metadata, transfer_id: transferId, type: 'transfer_in' } + metadata: { ...metadata, transfer_id: transferId, type: "transfer_in" }, }); return { @@ -304,10 +331,10 @@ class WalletService { amount, debitTransactionId: debit.transactionId, creditTransactionId: credit.transactionId, - status: 'completed' + status: "completed", }; } catch (error) { - throw this._handleError('transfer', error); + throw this._handleError("transfer", error); } } @@ -325,7 +352,7 @@ class WalletService { type = null, status = null, dateFrom = null, - dateTo = null + dateTo = null, } = options; const result = await this.db.findTransactions({ @@ -335,12 +362,12 @@ class WalletService { dateFrom, dateTo, page, - limit + limit, }); return result; } catch (error) { - throw this._handleError('getTransactions', error); + throw this._handleError("getTransactions", error); } } @@ -352,21 +379,23 @@ class WalletService { */ async updateWalletStatus(walletId, status) { try { - const validStatuses = ['active', 'suspended', 'closed']; + const validStatuses = ["active", "suspended", "closed"]; if (!validStatuses.includes(status)) { - throw new Error(`Invalid status. Must be one of: ${validStatuses.join(', ')}`); + throw new Error( + `Invalid status. Must be one of: ${validStatuses.join(", ")}`, + ); } const wallet = await this.getWallet(walletId); await this.db.updateWallet(walletId, { status, - updatedAt: new Date() + updatedAt: new Date(), }); return { ...wallet, status }; } catch (error) { - throw this._handleError('updateWalletStatus', error); + throw this._handleError("updateWalletStatus", error); } } @@ -388,10 +417,10 @@ class WalletService { totalDebits: stats.totalDebits || 0, transactionCount: stats.transactionCount || 0, averageTransaction: stats.averageTransaction || 0, - lastTransaction: stats.lastTransaction || null + lastTransaction: stats.lastTransaction || null, }; } catch (error) { - throw this._handleError('getWalletStats', error); + throw this._handleError("getWalletStats", error); } } @@ -403,12 +432,14 @@ class WalletService { try { // This would integrate with a stored payment method // For now, just log the event - logger.info(`Auto top-up triggered for wallet ${walletId}. Current balance: ${currentBalance}`); + logger.info( + `Auto top-up triggered for wallet ${walletId}. Current balance: ${currentBalance}`, + ); // You would implement actual top-up logic here // Example: await this.addFunds({ walletId, amount: this.config.autoTopUp.amount, ... }); } catch (error) { - logger.error('Auto top-up failed:', error); + logger.error("Auto top-up failed:", error); } } @@ -417,7 +448,7 @@ class WalletService { * @private */ _generateTransferId() { - return `transfer_${Date.now()}_${crypto.randomBytes(8).toString('hex')}`; + return `transfer_${Date.now()}_${crypto.randomBytes(8).toString("hex")}`; } /** diff --git a/src/services/web3.js b/src/services/web3.js index 61d3e32..abd3087 100644 --- a/src/services/web3.js +++ b/src/services/web3.js @@ -5,157 +5,184 @@ * using the secure TokenizationService (Secure Enclave). */ -const crypto = require('crypto'); -const logger = require('../utils/logger'); +const crypto = require("crypto"); +const logger = require("../utils/logger"); class Web3Service { - constructor(tokenizationService) { - this.tokenizationService = tokenizationService; + constructor(tokenizationService) { + this.tokenizationService = tokenizationService; + } + + /** + * Create a new blockchain wallet + * Generates a key pair, stores the private key in the vault, and returns the address. + * @param {string} network - Network identifier (e.g. 'ethereum', 'polygon') + */ + async createWallet(network = "ethereum") { + try { + // 1. Generate Key Pair (Simulation) + // In production, this might happen inside the Secure Enclave or via a KMS + const privateKey = "0x" + crypto.randomBytes(32).toString("hex"); + const publicKey = "0x" + crypto.randomBytes(20).toString("hex"); // Simplified address generation + + // 2. Vault the Private Key + const secretToken = await this.tokenizationService.createSecretToken( + privateKey, + { + network, + type: "blockchain_wallet", + }, + ); + + return { + address: publicKey, + keyTokenId: secretToken.id, + network, + }; + } catch (error) { + throw this._handleError("createWallet", error); } - - /** - * Create a new blockchain wallet - * Generates a key pair, stores the private key in the vault, and returns the address. - * @param {string} network - Network identifier (e.g. 'ethereum', 'polygon') - */ - async createWallet(network = 'ethereum') { - try { - // 1. Generate Key Pair (Simulation) - // In production, this might happen inside the Secure Enclave or via a KMS - const privateKey = '0x' + crypto.randomBytes(32).toString('hex'); - const publicKey = '0x' + crypto.randomBytes(20).toString('hex'); // Simplified address generation - - // 2. Vault the Private Key - const secretToken = await this.tokenizationService.createSecretToken(privateKey, { - network, - type: 'blockchain_wallet' - }); - - return { - address: publicKey, - keyTokenId: secretToken.id, - network - }; - } catch (error) { - throw this._handleError('createWallet', error); - } - } - - /** - * Get balance for an address - * @param {string} address - * @param {string} network - */ - async getBalance(address, network = 'ethereum') { - try { - // Simulation: Return a random balance or mock - // In production, this calls an RPC provider (Infura, Alchemy, etc.) - return { - balance: '1.5', - currency: 'ETH', - network - }; - } catch (error) { - throw this._handleError('getBalance', error); - } + } + + /** + * Get balance for an address + * @param {string} address + * @param {string} network + */ + async getBalance(address, network = "ethereum") { + try { + // Simulation: Return a random balance or mock + // In production, this calls an RPC provider (Infura, Alchemy, etc.) + return { + balance: "1.5", + currency: "ETH", + network, + }; + } catch (error) { + throw this._handleError("getBalance", error); } - - /** - * Send a transaction - * @param {Object} params - * @param {string} params.keyTokenId - The token ID of the sender's private key - * @param {string} params.to - Recipient address - * @param {string} params.value - Amount to send - * @param {string} params.network - Network to use - * @param {string} params.mandate - Optional Mandate (AP2) for Zero Trust validation - * @param {Object} params.context - Optional context for validation - */ - async sendTransaction({ keyTokenId, to, value, network = 'ethereum', mandate, context = {} }) { - try { - // 1. Construct Transaction (Simplified) - const txData = { - to, - value, - nonce: 0, // Would fetch proper nonce - gasPrice: '20000000000', - gasLimit: '21000' - }; - - // 2. Sign Transaction using Vault - // We serialize the txData to string/hex for signing - const serializedTx = JSON.stringify(txData); - const signature = await this.tokenizationService.signWithToken(keyTokenId, serializedTx, mandate, context); - - // 3. Broadcast Transaction - // In production, send signedTx to RPC - const txHash = '0x' + crypto.randomBytes(32).toString('hex'); - - return { - hash: txHash, - status: 'pending', - network, - signedData: signature - }; - } catch (error) { - throw this._handleError('sendTransaction', error); - } + } + + /** + * Send a transaction + * @param {Object} params + * @param {string} params.keyTokenId - The token ID of the sender's private key + * @param {string} params.to - Recipient address + * @param {string} params.value - Amount to send + * @param {string} params.network - Network to use + * @param {string} params.mandate - Optional Mandate (AP2) for Zero Trust validation + * @param {Object} params.context - Optional context for validation + */ + async sendTransaction({ + keyTokenId, + to, + value, + network = "ethereum", + mandate, + context = {}, + }) { + try { + // 1. Construct Transaction (Simplified) + const txData = { + to, + value, + nonce: 0, // Would fetch proper nonce + gasPrice: "20000000000", + gasLimit: "21000", + }; + + // 2. Sign Transaction using Vault + // We serialize the txData to string/hex for signing + const serializedTx = JSON.stringify(txData); + const signature = await this.tokenizationService.signWithToken( + keyTokenId, + serializedTx, + mandate, + context, + ); + + // 3. Broadcast Transaction + // In production, send signedTx to RPC + const txHash = "0x" + crypto.randomBytes(32).toString("hex"); + + return { + hash: txHash, + status: "pending", + network, + signedData: signature, + }; + } catch (error) { + throw this._handleError("sendTransaction", error); } - - /** - * x402 Extension: Execute a stablecoin settlement (USDC/PYUSD) - * Provides 24/7 low-latency machine settlements for the agentic economy. - */ - async executeX402Settlement({ keyTokenId, to, amount, stablecoin = 'USDC', network = 'ethereum', mandate }) { - try { - // Token addresses (Simulation) - const tokenAddresses = { - 'USDC': '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', - 'PYUSD': '0x6c3ea9036406852006290770bedfc29a991f4706' - }; - - const tokenAddress = tokenAddresses[stablecoin]; - if (!tokenAddress) throw new Error(`Zero Trust Validation Failed: Unsupported stablecoin: ${stablecoin}`); - - logger.info(`x402: Executing ${stablecoin} settlement for ${amount} to ${to}...`); - - // 1. Construct ERC20 transfer data (Simplified) - const txData = { - to: tokenAddress, - data: `transfer(${to}, ${amount})`, - gasLimit: '65000' - }; - - // 2. Sign with Mandate (Zero Trust) - const signature = await this.tokenizationService.signWithToken( - keyTokenId, - JSON.stringify(txData), - mandate, - { amount, merchant: to } - ); - - // 3. Simulation: Return successful settlement - return { - settlement_id: `x402_${crypto.randomBytes(8).toString('hex')}`, - status: 'finalized', - stablecoin, - amount, - recipient: to, - tx_hash: `0x${crypto.randomBytes(32).toString('hex')}`, - timestamp: new Date().toISOString() - }; - } catch (error) { - throw this._handleError('executeX402Settlement', error); - } - } - - /** - * Handle and format errors - * @private - */ - _handleError(method, error) { - logger.error(`Web3Service.${method} error:`, error); - return error instanceof Error ? error : new Error(error); + } + + /** + * x402 Extension: Execute a stablecoin settlement (USDC/PYUSD) + * Provides 24/7 low-latency machine settlements for the agentic economy. + */ + async executeX402Settlement({ + keyTokenId, + to, + amount, + stablecoin = "USDC", + network = "ethereum", + mandate, + }) { + try { + // Token addresses (Simulation) + const tokenAddresses = { + USDC: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + PYUSD: "0x6c3ea9036406852006290770bedfc29a991f4706", + }; + + const tokenAddress = tokenAddresses[stablecoin]; + if (!tokenAddress) + throw new Error( + `Zero Trust Validation Failed: Unsupported stablecoin: ${stablecoin}`, + ); + + logger.info( + `x402: Executing ${stablecoin} settlement for ${amount} to ${to}...`, + ); + + // 1. Construct ERC20 transfer data (Simplified) + const txData = { + to: tokenAddress, + data: `transfer(${to}, ${amount})`, + gasLimit: "65000", + }; + + // 2. Sign with Mandate (Zero Trust) + const signature = await this.tokenizationService.signWithToken( + keyTokenId, + JSON.stringify(txData), + mandate, + { amount, merchant: to }, + ); + + // 3. Simulation: Return successful settlement + return { + settlement_id: `x402_${crypto.randomBytes(8).toString("hex")}`, + status: "finalized", + stablecoin, + amount, + recipient: to, + tx_hash: `0x${crypto.randomBytes(32).toString("hex")}`, + timestamp: new Date().toISOString(), + }; + } catch (error) { + throw this._handleError("executeX402Settlement", error); } + } + + /** + * Handle and format errors + * @private + */ + _handleError(method, error) { + logger.error(`Web3Service.${method} error:`, error); + return error instanceof Error ? error : new Error(error); + } } module.exports = Web3Service; diff --git a/src/utils/database.js b/src/utils/database.js index 884f044..50fbcce 100644 --- a/src/utils/database.js +++ b/src/utils/database.js @@ -4,27 +4,27 @@ * Handles database connection for MongoDB or PostgreSQL */ -const mongoose = require('mongoose'); -const config = require('../config'); -const logger = require('./logger'); +const mongoose = require("mongoose"); +const config = require("../config"); +const logger = require("./logger"); let isConnected = false; async function connectDatabase() { if (isConnected) { - logger.info('Using existing database connection'); + logger.info("Using existing database connection"); return; } const dbUrl = config.database.url; // Determine database type from URL - if (dbUrl.startsWith('mongodb')) { + if (dbUrl.startsWith("mongodb")) { return connectMongoDB(); - } else if (dbUrl.startsWith('postgres')) { + } else if (dbUrl.startsWith("postgres")) { return connectPostgreSQL(); } else { - throw new Error('Unsupported database type. Use MongoDB or PostgreSQL.'); + throw new Error("Unsupported database type. Use MongoDB or PostgreSQL."); } } @@ -32,54 +32,54 @@ async function connectMongoDB() { try { await mongoose.connect(config.database.url, config.database.options); - mongoose.connection.on('connected', () => { - logger.info('MongoDB connected successfully'); + mongoose.connection.on("connected", () => { + logger.info("MongoDB connected successfully"); isConnected = true; }); - mongoose.connection.on('error', (err) => { - logger.error('MongoDB connection error:', err); + mongoose.connection.on("error", (err) => { + logger.error("MongoDB connection error:", err); isConnected = false; }); - mongoose.connection.on('disconnected', () => { - logger.warn('MongoDB disconnected'); + mongoose.connection.on("disconnected", () => { + logger.warn("MongoDB disconnected"); isConnected = false; }); // Load models - require('../models/wallet'); - require('../models/transaction'); - require('../models/refund'); - require('../models/agent'); + require("../models/wallet"); + require("../models/transaction"); + require("../models/refund"); + require("../models/agent"); return mongoose.connection; } catch (error) { - logger.error('Failed to connect to MongoDB:', error); + logger.error("Failed to connect to MongoDB:", error); throw error; } } async function connectPostgreSQL() { - const { Pool } = require('pg'); + const { Pool } = require("pg"); try { const pool = new Pool({ connectionString: config.database.url, max: 20, idleTimeoutMillis: 30000, - connectionTimeoutMillis: 2000 + connectionTimeoutMillis: 2000, }); // Test connection const client = await pool.connect(); - logger.info('PostgreSQL connected successfully'); + logger.info("PostgreSQL connected successfully"); client.release(); isConnected = true; return pool; } catch (error) { - logger.error('Failed to connect to PostgreSQL:', error); + logger.error("Failed to connect to PostgreSQL:", error); throw error; } } @@ -91,18 +91,18 @@ async function disconnectDatabase() { try { await mongoose.connection.close(); - logger.info('Database disconnected'); + logger.info("Database disconnected"); isConnected = false; } catch (error) { - logger.error('Error disconnecting database:', error); + logger.error("Error disconnecting database:", error); throw error; } } -const { Wallet } = require('../models/wallet'); -const { Transaction } = require('../models/transaction'); -const { Refund } = require('../models/refund'); -const { Agent } = require('../models/agent'); +const { Wallet } = require("../models/wallet"); +const { Transaction } = require("../models/transaction"); +const { Refund } = require("../models/refund"); +const { Agent } = require("../models/agent"); module.exports = { connectDatabase, @@ -130,7 +130,7 @@ module.exports = { return Wallet.findByIdAndUpdate( walletId, { $inc: { balance: amount } }, - { new: true, runValidators: true } + { new: true, runValidators: true }, ); }, @@ -143,11 +143,21 @@ module.exports = { }, async updateTransaction(transactionId, updateData) { - return Transaction.findByIdAndUpdate(transactionId, updateData, { new: true }); + return Transaction.findByIdAndUpdate(transactionId, updateData, { + new: true, + }); }, async findTransactions(query) { - const { walletId, type, status, dateFrom, dateTo, page = 1, limit = 20 } = query; + const { + walletId, + type, + status, + dateFrom, + dateTo, + page = 1, + limit = 20, + } = query; const filter = { walletId }; if (type) filter.type = type; if (status) filter.status = status; @@ -170,34 +180,38 @@ module.exports = { total, page, limit, - totalPages: Math.ceil(total / limit) + totalPages: Math.ceil(total / limit), }; }, async getWalletStatistics(walletId) { const stats = await Transaction.aggregate([ - { $match: { walletId, status: 'completed' } }, + { $match: { walletId, status: "completed" } }, { $group: { _id: null, totalCredits: { - $sum: { $cond: [{ $eq: ['$type', 'credit'] }, '$amount', 0] } + $sum: { $cond: [{ $eq: ["$type", "credit"] }, "$amount", 0] }, }, totalDebits: { - $sum: { $cond: [{ $eq: ['$type', 'debit'] }, { $abs: '$amount' }, 0] } + $sum: { + $cond: [{ $eq: ["$type", "debit"] }, { $abs: "$amount" }, 0], + }, }, transactionCount: { $sum: 1 }, - averageTransaction: { $avg: { $abs: '$amount' } } - } - } + averageTransaction: { $avg: { $abs: "$amount" } }, + }, + }, ]); - const lastTransaction = await Transaction.findOne({ walletId, status: 'completed' }) - .sort({ createdAt: -1 }); + const lastTransaction = await Transaction.findOne({ + walletId, + status: "completed", + }).sort({ createdAt: -1 }); return { ...(stats[0] || {}), - lastTransaction + lastTransaction, }; }, @@ -220,5 +234,5 @@ module.exports = { async findAllAgents(filter = {}) { return Agent.find(filter); - } + }, }; diff --git a/src/utils/logger.js b/src/utils/logger.js index 9fd0223..7787293 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -4,56 +4,62 @@ * Centralized logging using Winston */ -const winston = require('winston'); -const config = require('../config'); +const winston = require("winston"); +const config = require("../config"); // Define log format const logFormat = winston.format.combine( - winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), winston.format.errors({ stack: true }), winston.format.splat(), - winston.format.json() + winston.format.json(), ); // Create logger instance const logger = winston.createLogger({ level: config.logging.level, format: logFormat, - defaultMeta: { service: 'ocp-sdk' }, - transports: [] + defaultMeta: { service: "ocp-sdk" }, + transports: [], }); // Console transport for development if (config.logging.console) { - logger.add(new winston.transports.Console({ - format: winston.format.combine( - winston.format.colorize(), - winston.format.printf(({ timestamp, level, message, ...meta }) => { - let msg = `${timestamp} [${level}]: ${message}`; - if (Object.keys(meta).length > 0) { - msg += ` ${JSON.stringify(meta)}`; - } - return msg; - }) - ) - })); + logger.add( + new winston.transports.Console({ + format: winston.format.combine( + winston.format.colorize(), + winston.format.printf(({ timestamp, level, message, ...meta }) => { + let msg = `${timestamp} [${level}]: ${message}`; + if (Object.keys(meta).length > 0) { + msg += ` ${JSON.stringify(meta)}`; + } + return msg; + }), + ), + }), + ); } // File transport for production -if (config.logging.file && config.server.nodeEnv === 'production') { - logger.add(new winston.transports.File({ - filename: config.logging.file, - maxsize: 10485760, // 10MB - maxFiles: 5 - })); +if (config.logging.file && config.server.nodeEnv === "production") { + logger.add( + new winston.transports.File({ + filename: config.logging.file, + maxsize: 10485760, // 10MB + maxFiles: 5, + }), + ); // Separate file for errors - logger.add(new winston.transports.File({ - filename: 'logs/error.log', - level: 'error', - maxsize: 10485760, - maxFiles: 5 - })); + logger.add( + new winston.transports.File({ + filename: "logs/error.log", + level: "error", + maxsize: 10485760, + maxFiles: 5, + }), + ); } module.exports = logger; diff --git a/tests/unit/mpp.spec.js b/tests/unit/mpp.spec.js index 6841bad..d1bc68c 100644 --- a/tests/unit/mpp.spec.js +++ b/tests/unit/mpp.spec.js @@ -82,7 +82,7 @@ describe('MPP402Handler', () => { const requestFn = jest.fn().mockResolvedValue(response402); await expect(mppHandler.executeAutonomousRequest(agent, requestFn, intentMandate)) - .rejects.toThrow('MPP: Payment amount 500 exceeds intent mandate budget of 100'); + .rejects.toThrow('Zero Trust Validation Failed: MPP payment amount 500 exceeds intent mandate budget of 100'); }); it('should throw error if 402 response is missing requirements', async () => { @@ -96,7 +96,7 @@ describe('MPP402Handler', () => { const requestFn = jest.fn().mockResolvedValue(response402); await expect(mppHandler.executeAutonomousRequest(agent, requestFn, intentMandate)) - .rejects.toThrow('Incomplete payment requirements in 402 response'); + .rejects.toThrow('Zero Trust Validation Failed: Incomplete payment requirements in 402 response'); }); it('should handle 402 thrown as an error (e.g. from axios)', async () => {