Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@
"express-rate-limit": "^7.1.0",
"graphql": "^15.8.0",
"helmet": "^7.1.0",
"joi": "^17.11.0",
"jsonwebtoken": "^9.0.2",
"joi": "^17.13.3",
"jsonwebtoken": "^9.0.3",
"mongoose": "^8.0.0",
"pg": "^8.11.0",
"redis": "^4.6.0",
Expand Down
16 changes: 4 additions & 12 deletions scripts/verify_ucp_flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,7 @@ const { Transaction } = require('../src/models/transaction');
const { Wallet } = require('../src/models/wallet');

// Mock DB wrapper for WalletService since it expects a db object with models attached
// or methods. But looking at WalletService code, it expects `database` to have methods
// like findWalletById, createTransaction, updateWalletBalance etc.
// Wait, looking at src/services/wallet.js:
// `this.db = database;`
// `await this.db.findWalletByUserId(userId);`
// So I need a DB adapter or I need to implement those methods.
// Let's check src/models/index.js if it exists to see if there is a DB abstraction layer.

// Let's create a simple DB adapter for the test
// or methods.
class DBAdapter {
constructor() {
this.Wallet = Wallet;
Expand All @@ -24,6 +16,7 @@ class DBAdapter {

async findWalletById(id) { return Wallet.findById(id); }
async findWalletByUserId(userId) { return Wallet.findOne({ userId }); }
async createWallet(data) { return Wallet.create(data); }
async createTransaction(data) { return Transaction.create(data); }
async updateTransaction(id, data) { return Transaction.findByIdAndUpdate(id, data, { new: true }); }

Expand Down Expand Up @@ -78,8 +71,7 @@ async function verify() {
intent: 'transfer',
sender: { agent_id: agent1.id },
recipient: { agent_id: agent2.id },
amount: { value: 150, currency: 'USD' },
metadata: { orderId: 'testing-123' }
amount: { value: 150, currency: 'USD' }
};

const result = await ucpService.processPayload(ucpPayload);
Expand All @@ -96,7 +88,7 @@ async function verify() {
if (updatedWallet2.balance !== 150) throw new Error('Wallet2 balance incorrect');

// 4. Verify Transaction Record
const txs = await Transaction.find({ 'ucpPayload.metadata.orderId': 'testing-123' });
const txs = await Transaction.find({ 'ucpPayload.sender.agent_id': agent1.id });
if (txs.length === 0) throw new Error('Transaction record not found');
console.log('Transaction Verified:', txs[0].id);

Expand Down
5 changes: 5 additions & 0 deletions src/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ function validateConfig() {
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 (errors.length > 0) {
throw new Error(`Configuration errors:\n${errors.join('\n')}`);
}
Expand Down
14 changes: 8 additions & 6 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,24 @@ 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'); // New
const UCPService = require('./services/ucp'); // New
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'); // New
const ucpRoutes = require('./routes/ucp'); // New
const agentRoutes = require('./routes/agent');
const ucpRoutes = require('./routes/ucp');

// Initialize services
const db = require('./utils/database');
const walletService = new WalletService(db);
const tokenizationService = new TokenizationService();
const mobilePaymentService = new MobilePaymentService(tokenizationService, walletService);
const agentService = new AgentService(db); // New
const ucpService = new UCPService(); // New
const agentService = new AgentService(db);
const a2aService = new A2AService(walletService, db);
const ucpService = new UCPService(a2aService);

// Initialize Express app
const app = express();
Expand Down
30 changes: 26 additions & 4 deletions src/middleware/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,33 @@
* Validates JWT tokens and protects routes.
*/

const jwt = require('jsonwebtoken');
const config = require('../config');
const logger = require('../utils/logger');

const authenticate = (req, res, next) => {
// In a real application, you would validate a JWT token here.
// For this example, we'll just simulate an authenticated user.
req.user = { id: 'user123', roles: ['user'] };
next();
const authHeader = req.headers.authorization;

if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
error: 'Authentication required'
});
}

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);
return res.status(401).json({
success: false,
error: 'Invalid or expired token'
});
}
};

const authorize = (roles = []) => {
Expand Down
4 changes: 1 addition & 3 deletions src/models/refund.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ const mongoose = require('mongoose');
const refundSchema = new mongoose.Schema({
transactionId: {
type: String,
required: true,
index: true
required: true
},
walletId: {
type: String,
Expand Down Expand Up @@ -91,7 +90,6 @@ const refundSchema = new mongoose.Schema({
// Indexes
refundSchema.index({ walletId: 1, createdAt: -1 });
refundSchema.index({ status: 1, createdAt: -1 });
refundSchema.index({ transactionId: 1 });

// Virtual for refund ID
refundSchema.virtual('id').get(function() {
Expand Down
4 changes: 1 addition & 3 deletions src/models/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ const transactionSchema = new mongoose.Schema({
},
transferId: {
type: String,
sparse: true,
index: true
sparse: true
},
refundId: {
type: String,
Expand Down Expand Up @@ -108,7 +107,6 @@ transactionSchema.index({ walletId: 1, createdAt: -1 });
transactionSchema.index({ walletId: 1, type: 1 });
transactionSchema.index({ walletId: 1, status: 1 });
transactionSchema.index({ createdAt: -1 });
transactionSchema.index({ transferId: 1 }, { sparse: true });

// Virtual for transaction ID
transactionSchema.virtual('id').get(function () {
Expand Down
88 changes: 49 additions & 39 deletions src/routes/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
*/

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();
Expand All @@ -13,7 +16,7 @@ module.exports = (agentService) => {
* GET /api/v1/agents
* Get all registered agents
*/
router.get('/', async (req, res, next) => {
router.get('/', authenticate, async (req, res, next) => {
try {
const agents = await agentService.getAllAgents();
res.json(agents);
Expand All @@ -25,26 +28,40 @@ module.exports = (agentService) => {
/**
* POST /api/v1/agents
* Register a new agent
* Body: { name: string, policy?: { spendingLimit?: number, authorizedCounterparties?: string[] } }
*/
router.post('/', async (req, res, next) => {
try {
const { name, policy } = req.body;
if (!name) {
return res.status(400).json({ error: 'Agent name is required' });
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'),
config: Joi.object({
limits: Joi.object({
daily: Joi.number().min(0),
perTransaction: Joi.number().min(0)
}),
authorizedCounterparties: Joi.array().items(Joi.string()),
autoApprove: Joi.boolean()
})
})
}),
async (req, res, next) => {
try {
const newAgent = await agentService.registerAgent(req.body);
res.status(201).json(newAgent);
} catch (error) {
next(error);
}
const newAgent = await agentService.registerAgent({ name, policy });
res.status(201).json(newAgent);
} catch (error) {
next(error);
}
});
);

/**
* GET /api/v1/agents/:agentId
* Get an agent by ID
*/
router.get('/:agentId', async (req, res, next) => {
router.get('/:agentId', authenticate, async (req, res, next) => {
try {
const agent = await agentService.getAgent(req.params.agentId);
res.json(agent);
Expand All @@ -56,35 +73,28 @@ module.exports = (agentService) => {
/**
* PUT /api/v1/agents/:agentId/policy
* Update an agent's policy
* Body: { spendingLimit?: number, authorizedCounterparties?: string[] }
*/
router.put('/:agentId/policy', async (req, res, next) => {
try {
const newPolicy = req.body;
const updatedAgent = await agentService.updateAgentPolicy(req.params.agentId, newPolicy);
res.json(updatedAgent);
} catch (error) {
next(error);
}
});

/**
* POST /api/v1/agents/transfer
* Perform an Agent-to-Agent (A2A) transfer (conceptual)
* Body: { fromAgentId: string, toAgentId: string, amount: number, currency: string }
*/
router.post('/transfer', async (req, res, next) => {
try {
const { fromAgentId, toAgentId, amount, currency } = req.body;
if (!fromAgentId || !toAgentId || !amount || !currency) {
return res.status(400).json({ error: 'Missing required transfer parameters' });
router.put('/:agentId/policy',
authenticate,
validate({
body: Joi.object({
limits: Joi.object({
daily: Joi.number().min(0),
perTransaction: Joi.number().min(0)
}),
authorizedCounterparties: Joi.array().items(Joi.string()),
autoApprove: Joi.boolean()
})
}),
async (req, res, next) => {
try {
const updatedAgent = await agentService.updateAgentPolicy(req.params.agentId, req.body);
res.json(updatedAgent);
} catch (error) {
next(error);
}
const result = await agentService.performA2ATransfer({ fromAgentId, toAgentId, amount, currency });
res.json(result);
} catch (error) {
next(error);
}
});
);

return router;
};
43 changes: 33 additions & 10 deletions src/routes/ucp.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,47 @@
*/

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();

/**
* POST /api/v1/ucp/process-intent
* POST /api/v1/ucp/process
* Process a UCP-compliant commerce intent
* Body: { protocolVersion: string, intentType: string, data: object, agentId: string, walletId?: string }
*/
router.post('/process-intent', async (req, res, next) => {
try {
const ucpIntent = req.body;
const result = await ucpService.processIntent(ucpIntent);
res.json(result);
} catch (error) {
next(error);
router.post('/process',
authenticate,
validate({
body: Joi.object({
ver: Joi.string().required(),
intent: Joi.string().required(),
sender: Joi.object({
agent_id: Joi.string().required(),
wallet_id: Joi.string()
}).required(),
recipient: Joi.object({
agent_id: Joi.string().required(),
wallet_id: Joi.string()
}),
amount: Joi.object({
value: Joi.number().required(),
currency: Joi.string()
}),
data: Joi.object()
})
}),
async (req, res, next) => {
try {
const result = await ucpService.processPayload(req.body);
res.json(result);
} catch (error) {
next(error);
}
}
});
);

/**
* GET /api/v1/ucp/schema
Expand Down
Loading