Skip to content
Open
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
22 changes: 14 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 34 additions & 1 deletion services/10-token-engine/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@ const { Client } = require('pg');
const { Kafka } = require('kafkajs');
const axios = require('axios');
const express = require('express');
const helmet = require('helmet');
const jwt = require('jsonwebtoken');
const Decimal = require('decimal.js');
const redis = require('redis');

const app = express();
app.use(helmet());
app.use(express.json());

const port = process.env.PORT || 3010;
const JWT_SECRET = process.env.JWT_SECRET;

const pgClient = new Client({ connectionString: process.env.DATABASE_URL });
const kafka = new Kafka({
Expand All @@ -21,6 +27,25 @@ const redisClient = redis.createClient({

const consumer = kafka.consumer({ groupId: 'token-engine-group' });

// Middleware: Verify JWT token
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];

if (!token) return res.status(401).json({ error: 'Access token required' });

if (!JWT_SECRET) {
console.error('[Security] JWT_SECRET is not configured. Rejecting authentication.');
return res.status(500).json({ error: 'Internal server error' });
}

jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) return res.status(403).json({ error: 'Invalid or expired token' });
req.user = user;
next();
});
};

// Thresholds for Dynamic Multipliers (surplus/scarcity) - Configurable via ENV
const LMP_THRESHOLD_SURPLUS = new Decimal(process.env.LMP_THRESHOLD_SURPLUS || '30.0');
const LMP_THRESHOLD_SCARCITY = new Decimal(process.env.LMP_THRESHOLD_SCARCITY || '100.0');
Expand Down Expand Up @@ -150,9 +175,17 @@ app.get('/health', (req, res) => {
* [Phase 6 AI Readiness]
* GET /data/training/rewards
* Exposes high-fidelity reward data for L11 ML Engine training.
* Security: Restricted to admin/system tokens (no fleet_id).
*/
app.get('/data/training/rewards', async (req, res) => {
app.get('/data/training/rewards', authenticateToken, async (req, res) => {
const { site_id, limit = 100 } = req.query;

// Authorization: Only admin/system tokens (without fleet_id) can access global training data
if (req.user.fleet_id) {
console.warn(`[Security] Unauthorized global rewards data export attempt by fleet_id: ${req.user.fleet_id}`);
return res.status(403).json({ error: 'Forbidden: Unauthorized access to global training data.' });
}

try {
let query = 'SELECT * FROM token_reward_log WHERE is_sentinel_fidelity = TRUE';
const params = [];
Expand Down
5 changes: 4 additions & 1 deletion services/10-token-engine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"test": "jest"
},
"devDependencies": {
"jest": "^29.7.0"
"helmet": "^8.2.0",
"jest": "^29.7.0",
"jsonwebtoken": "^9.0.3",
"supertest": "^6.3.4"
}
}
73 changes: 73 additions & 0 deletions services/10-token-engine/tests/security_hardening.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
process.env.JWT_SECRET = 'test_secret';
const request = require('supertest');
const jwt = require('jsonwebtoken');

// Mock redis before importing app
jest.mock('redis', () => ({
createClient: jest.fn(() => ({
connect: jest.fn(),
on: jest.fn(),
hGet: jest.fn(),
hSet: jest.fn(),
get: jest.fn(),
set: jest.fn(),
quit: jest.fn()
}))
}));

// Mock pg before importing app
jest.mock('pg', () => {
const mClient = {
connect: jest.fn(),
query: jest.fn(),
end: jest.fn(),
on: jest.fn(),
};
return { Client: jest.fn(() => mClient) };
});

const { app } = require('../index');

describe('L10 Token Engine Security Hardening', () => {
test('GET /data/training/rewards should return 401 if no token provided', async () => {
const response = await request(app).get('/data/training/rewards');
expect(response.status).toBe(401);
});

test('GET /data/training/rewards should return 403 if invalid token provided', async () => {
const response = await request(app)
.get('/data/training/rewards')
.set('Authorization', 'Bearer invalid_token');
expect(response.status).toBe(403);
});

test('GET /data/training/rewards should return 403 if token contains fleet_id (non-admin)', async () => {
const token = jwt.sign({ driver_id: 'driver-1', fleet_id: 'fleet-1' }, process.env.JWT_SECRET);
const response = await request(app)
.get('/data/training/rewards')
.set('Authorization', `Bearer ${token}`);
expect(response.status).toBe(403);
expect(response.body.error).toContain('Unauthorized access to global training data');
});

test('GET /data/training/rewards should return 200 (or pass auth) if valid admin token provided', async () => {
const token = jwt.sign({ driver_id: 'admin-1' }, process.env.JWT_SECRET);

// We expect it to NOT be 401 or 403. It might be 200 or 500 (if DB query fails)
// but the point is it passed the auth middleware.
const response = await request(app)
.get('/data/training/rewards')
.set('Authorization', `Bearer ${token}`);

expect(response.status).not.toBe(401);
expect(response.status).not.toBe(403);
});

test('GET /health should return security headers via helmet', async () => {
const response = await request(app).get('/health');
expect(response.headers['x-dns-prefetch-control']).toBeDefined();
expect(response.headers['x-frame-options']).toBeDefined();
expect(response.headers['strict-transport-security']).toBeDefined();
expect(response.headers['x-content-type-options']).toBeDefined();
});
});