LandveX Gateway v0.1.0 + Operations API + Communications Module — ALL RUNNING

This commit is contained in:
Bernt
2026-07-02 18:33:25 +00:00
parent 4f7152316c
commit 8dcca96185
20 changed files with 1754 additions and 81 deletions
+78
View File
@@ -0,0 +1,78 @@
/**
* LandveX Gateway — Authentication & Authorization
* JWT-based auth with role checking
*/
// Simple JWT verification (no external deps)
export function verifyToken(token) {
try {
const parts = token.split('.');
if (parts.length !== 3) return null;
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
if (payload.exp && payload.exp < Date.now() / 1000) {
return null;
}
return {
id: payload.sub,
email: payload.email,
role: payload.role || 'customer',
orgId: payload.orgId,
capabilities: payload.capabilities || [],
};
} catch {
return null;
}
}
export function extractToken(req) {
const auth = req.headers.authorization;
if (!auth) return null;
const parts = auth.split(' ');
if (parts.length === 2 && parts[0] === 'Bearer') {
return parts[1];
}
return null;
}
export function requireAuth(req, res) {
const token = extractToken(req);
if (!token) {
res.statusCode = 401;
res.end(JSON.stringify({ error: 'Unauthorized', message: 'No token provided' }));
return null;
}
const user = verifyToken(token);
if (!user) {
res.statusCode = 401;
res.end(JSON.stringify({ error: 'Unauthorized', message: 'Invalid token' }));
return null;
}
return user;
}
export function requireRole(user, roles, res) {
if (!roles.includes(user.role)) {
res.statusCode = 403;
res.end(JSON.stringify({ error: 'Forbidden', message: 'Insufficient permissions' }));
return false;
}
return true;
}
// API Key authentication
export function verifyApiKey(key) {
if (key.startsWith('lvx_prod_')) {
return { valid: true, tier: 'production', rateLimit: 10000 };
}
if (key.startsWith('lvx_test_')) {
return { valid: true, tier: 'test', rateLimit: 100 };
}
return { valid: false };
}
+92
View File
@@ -0,0 +1,92 @@
/**
* LandveX Gateway — Authentication & Authorization
* JWT-based auth with role checking
*/
import { IncomingMessage, ServerResponse } from 'http';
import { GATEWAY_CONFIG } from './config';
export interface User {
id: string;
email: string;
role: 'superadmin' | 'admin' | 'operator' | 'customer';
orgId?: string;
capabilities: string[];
}
// Simple JWT verification (no external deps)
export function verifyToken(token: string): User | null {
try {
const parts = token.split('.');
if (parts.length !== 3) return null;
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
// Check expiration
if (payload.exp && payload.exp < Date.now() / 1000) {
return null;
}
return {
id: payload.sub,
email: payload.email,
role: payload.role || 'customer',
orgId: payload.orgId,
capabilities: payload.capabilities || [],
};
} catch {
return null;
}
}
export function extractToken(req: IncomingMessage): string | null {
const auth = req.headers.authorization;
if (!auth) return null;
const parts = auth.split(' ');
if (parts.length === 2 && parts[0] === 'Bearer') {
return parts[1];
}
return null;
}
export function requireAuth(req: IncomingMessage, res: ServerResponse): User | null {
const token = extractToken(req);
if (!token) {
res.statusCode = 401;
res.end(JSON.stringify({ error: 'Unauthorized', message: 'No token provided' }));
return null;
}
const user = verifyToken(token);
if (!user) {
res.statusCode = 401;
res.end(JSON.stringify({ error: 'Unauthorized', message: 'Invalid token' }));
return null;
}
return user;
}
export function requireRole(user: User, roles: string[], res: ServerResponse): boolean {
if (!roles.includes(user.role)) {
res.statusCode = 403;
res.end(JSON.stringify({ error: 'Forbidden', message: 'Insufficient permissions' }));
return false;
}
return true;
}
// API Key authentication
export function verifyApiKey(key: string): { valid: boolean; tier?: string; rateLimit?: number } {
// TODO: Implement proper API key validation against database
// For now, simple prefix check
if (key.startsWith('lvx_prod_')) {
return { valid: true, tier: 'production', rateLimit: 10000 };
}
if (key.startsWith('lvx_test_')) {
return { valid: true, tier: 'test', rateLimit: 100 };
}
return { valid: false };
}
+58
View File
@@ -0,0 +1,58 @@
/**
* LandveX Gateway Configuration
* Central config for all service routing
*/
export const SERVICES = {
intelligence: {
name: 'Intelligence Lab',
host: process.env.INTELLIGENCE_HOST || 'localhost',
port: parseInt(process.env.INTELLIGENCE_PORT || '3002'),
path: '/api/v1',
healthPath: '/health',
},
operations: {
name: 'Operations API',
host: process.env.OPERATIONS_HOST || 'localhost',
port: parseInt(process.env.OPERATIONS_PORT || '3005'),
path: '/api/v1',
healthPath: '/health',
},
communications: {
name: 'Communications',
host: process.env.COMMUNICATIONS_HOST || 'localhost',
port: parseInt(process.env.COMMUNICATIONS_PORT || '3010'),
path: '/api/v1',
healthPath: '/health',
},
apollo: {
name: 'Apollo CRM',
host: process.env.APOLLO_HOST || 'localhost',
port: parseInt(process.env.APOLLO_PORT || '3001'),
path: '/api/apollo',
healthPath: '/status',
},
ledger: {
name: 'AAMOS Ledger',
host: process.env.LEDGER_HOST || 'localhost',
port: parseInt(process.env.LEDGER_PORT || '3250'),
path: '',
healthPath: '/health',
},
incidents: {
name: 'AAMOS Incidents',
host: process.env.INCIDENTS_HOST || 'localhost',
port: parseInt(process.env.INCIDENTS_PORT || '3303'),
path: '',
healthPath: '/health',
},
};
export const GATEWAY_CONFIG = {
port: parseInt(process.env.GATEWAY_PORT || '3004'),
env: process.env.NODE_ENV || 'development',
jwtSecret: process.env.JWT_SECRET || 'landvex-dev-secret-change-in-production',
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET || '',
rateLimitWindow: 15 * 60 * 1000,
rateLimitMax: 100,
};
+66
View File
@@ -0,0 +1,66 @@
/**
* LandveX Gateway Configuration
* Central config for all service routing
*/
export interface ServiceConfig {
name: string;
host: string;
port: number;
path: string;
healthPath: string;
}
export const SERVICES: Record<string, ServiceConfig> = {
intelligence: {
name: 'Intelligence Lab',
host: process.env.INTELLIGENCE_HOST || 'localhost',
port: parseInt(process.env.INTELLIGENCE_PORT || '3002'),
path: '/api/v1',
healthPath: '/health',
},
operations: {
name: 'Operations API',
host: process.env.OPERATIONS_HOST || 'localhost',
port: parseInt(process.env.OPERATIONS_PORT || '3005'),
path: '/api/v1',
healthPath: '/health',
},
communications: {
name: 'Communications',
host: process.env.COMMUNICATIONS_HOST || 'localhost',
port: parseInt(process.env.COMMUNICATIONS_PORT || '3010'),
path: '/api/v1',
healthPath: '/health',
},
apollo: {
name: 'Apollo CRM',
host: process.env.APOLLO_HOST || 'localhost',
port: parseInt(process.env.APOLLO_PORT || '3001'),
path: '/api/apollo',
healthPath: '/status',
},
ledger: {
name: 'AAMOS Ledger',
host: process.env.LEDGER_HOST || 'localhost',
port: parseInt(process.env.LEDGER_PORT || '3250'),
path: '',
healthPath: '/health',
},
incidents: {
name: 'AAMOS Incidents',
host: process.env.INCIDENTS_HOST || 'localhost',
port: parseInt(process.env.INCIDENTS_PORT || '3303'),
path: '',
healthPath: '/health',
},
};
export const GATEWAY_CONFIG = {
port: parseInt(process.env.GATEWAY_PORT || '3004'),
env: process.env.NODE_ENV || 'development',
jwtSecret: process.env.JWT_SECRET || 'landvex-dev-secret-change-in-production',
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET || '',
rateLimitWindow: 15 * 60 * 1000, // 15 minutes
rateLimitMax: 100,
};
+154
View File
@@ -0,0 +1,154 @@
/**
* LandveX Master Gateway
*
* Unifies all backend services:
* - Intelligence Lab (port 3002)
* - Operations API (port 3005)
* - Communications (port 3010)
* - Apollo CRM (port 3001)
* - AAMOS Ledger (port 3250)
* - AAMOS Incidents (port 3303)
*/
import http from 'http';
import { SERVICES } from './config.mjs';
import { verifyApiKey } from './auth.mjs';
import { proxyRequest, checkServiceHealth } from './proxy.mjs';
const PORT = process.env.GATEWAY_PORT || 3004;
const RATE_LIMIT_WINDOW = 15 * 60 * 1000;
const RATE_LIMIT_MAX = 100;
// Service health status
const serviceHealth = {};
// Rate limiting (simple in-memory)
const rateLimits = {};
function checkRateLimit(identifier, maxRequests) {
const now = Date.now();
const windowStart = Math.floor(now / RATE_LIMIT_WINDOW) * RATE_LIMIT_WINDOW;
if (!rateLimits[identifier] || rateLimits[identifier].resetAt < windowStart) {
rateLimits[identifier] = { count: 0, resetAt: windowStart + RATE_LIMIT_WINDOW };
}
rateLimits[identifier].count++;
return rateLimits[identifier].count <= maxRequests;
}
// CORS headers
function setCorsHeaders(res) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key');
}
// Main server
const server = http.createServer(async (req, res) => {
setCorsHeaders(res);
if (req.method === 'OPTIONS') {
res.statusCode = 204;
res.end();
return;
}
const url = req.url || '/';
// Health check
if (url === '/health') {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
status: 'ok',
version: '0.1.0-gateway',
services: Object.entries(serviceHealth).map(([name, health]) => ({
name,
healthy: health.healthy,
latency: health.latency,
lastCheck: health.lastCheck,
error: health.error,
})),
}));
return;
}
// API Key auth for external API access
const apiKey = req.headers['x-api-key'];
if (apiKey) {
const keyInfo = verifyApiKey(apiKey);
if (!keyInfo.valid) {
res.statusCode = 401;
res.end(JSON.stringify({ error: 'Invalid API key' }));
return;
}
if (!checkRateLimit(apiKey, keyInfo.rateLimit || 100)) {
res.statusCode = 429;
res.end(JSON.stringify({ error: 'Rate limit exceeded' }));
return;
}
}
// Route to services — ORDER MATTERS (specific first)
if (url.startsWith('/api/v1/government') || url.startsWith('/api/v1/customers')) {
proxyRequest(req, res, SERVICES.operations);
return;
}
if (url.startsWith('/api/v1/communications')) {
proxyRequest(req, res, SERVICES.communications);
return;
}
if (url.startsWith('/api/v1/missions') || url.startsWith('/api/v1/artifacts')) {
proxyRequest(req, res, SERVICES.intelligence);
return;
}
if (url.startsWith('/api/apollo')) {
proxyRequest(req, res, SERVICES.apollo);
return;
}
if (url.startsWith('/api/ledger')) {
proxyRequest(req, res, SERVICES.ledger);
return;
}
if (url.startsWith('/api/incidents')) {
proxyRequest(req, res, SERVICES.incidents);
return;
}
if (url === '/webhooks/stripe') {
res.statusCode = 200;
res.end(JSON.stringify({ received: true }));
return;
}
res.statusCode = 404;
res.end(JSON.stringify({ error: 'Not Found', path: url }));
});
// Health check loop
async function updateHealth() {
for (const [name, service] of Object.entries(SERVICES)) {
const health = await checkServiceHealth(service);
serviceHealth[name] = {
...health,
lastCheck: Date.now(),
};
}
}
// Initial health check
updateHealth();
setInterval(updateHealth, 30000);
server.listen(PORT, () => {
console.log(`🚀 LandveX Gateway running on port ${PORT}`);
console.log(`📡 Services:`);
Object.entries(SERVICES).forEach(([key, service]) => {
console.log(` - ${service.name}: http://${service.host}:${service.port}`);
});
});
+122 -81
View File
@@ -1,95 +1,136 @@
/**
* LandveX Master Gateway
*
*
* Unifies all backend services:
* - Intelligence Lab (port 3002)
* - Apollo CRM (/api/apollo)
* - Stripe billing
* - aamos-ledger (port 3250)
* - aamos-incidents (port 3303)
* - Auth & API keys
* - Operations API (port 3005)
* - Communications (port 3010)
* - Apollo CRM (port 3001)
* - AAMOS Ledger (port 3250)
* - AAMOS Incidents (port 3303)
*/
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import { createProxyMiddleware } from 'http-proxy-middleware';
import dotenv from 'dotenv';
import http from 'http';
import { GATEWAY_CONFIG, SERVICES } from './config';
import { requireAuth, requireRole, verifyApiKey } from './auth';
import { proxyRequest, checkServiceHealth } from './proxy';
dotenv.config();
// Service health status
const serviceHealth: Record<string, { healthy: boolean; latency: number; lastCheck: number; error?: string }> = {};
const app = express();
const PORT = process.env.PORT || 3004;
// Rate limiting (simple in-memory)
const rateLimits: Record<string, { count: number; resetAt: number }> = {};
// Middleware
app.use(helmet());
app.use(cors());
app.use(express.json());
function checkRateLimit(identifier: string, maxRequests: number): boolean {
const now = Date.now();
const windowStart = Math.floor(now / GATEWAY_CONFIG.rateLimitWindow) * GATEWAY_CONFIG.rateLimitWindow;
if (!rateLimits[identifier] || rateLimits[identifier].resetAt < windowStart) {
rateLimits[identifier] = { count: 0, resetAt: windowStart + GATEWAY_CONFIG.rateLimitWindow };
}
rateLimits[identifier].count++;
return rateLimits[identifier].count <= maxRequests;
}
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
});
app.use(limiter);
// CORS headers
function setCorsHeaders(res: http.ServerResponse): void {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key');
}
// Health check
app.get('/health', (_req, res) => {
res.json({
status: 'ok',
version: '0.1.0-master',
services: {
intelligence: 'http://localhost:3002',
apollo: '/api/apollo',
ledger: 'http://localhost:3250',
incidents: 'http://localhost:3303',
// Main server
const server = http.createServer(async (req, res) => {
setCorsHeaders(res);
if (req.method === 'OPTIONS') {
res.statusCode = 204;
res.end();
return;
}
const url = req.url || '/';
// Health check
if (url === '/health') {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
status: 'ok',
version: '0.1.0-gateway',
environment: GATEWAY_CONFIG.env,
services: Object.entries(serviceHealth).map(([name, health]) => ({
name,
healthy: health.healthy,
latency: health.latency,
lastCheck: health.lastCheck,
error: health.error,
})),
}));
return;
}
// API Key auth for external API access
const apiKey = req.headers['x-api-key'] as string;
if (apiKey) {
const keyInfo = verifyApiKey(apiKey);
if (!keyInfo.valid) {
res.statusCode = 401;
res.end(JSON.stringify({ error: 'Invalid API key' }));
return;
}
if (!checkRateLimit(apiKey, keyInfo.rateLimit || 100)) {
res.statusCode = 429;
res.end(JSON.stringify({ error: 'Rate limit exceeded' }));
return;
}
}
// Route to services
if (url.startsWith('/api/v1/government') || url.startsWith('/api/v1/customers')) {
proxyRequest(req, res, SERVICES.operations);
return;
} else if (url.startsWith('/api/v1/communications')) {
proxyRequest(req, res, SERVICES.communications);
return;
} else if (url.startsWith('/api/v1/missions') || url.startsWith('/api/v1/artifacts')) {
proxyRequest(req, res, SERVICES.intelligence);
return;
} else if (url.startsWith('/api/apollo')) {
proxyRequest(req, res, SERVICES.apollo);
} else if (url.startsWith('/api/ledger')) {
proxyRequest(req, res, SERVICES.ledger);
} else if (url.startsWith('/api/incidents')) {
proxyRequest(req, res, SERVICES.incidents);
} else if (url === '/webhooks/stripe') {
// Stripe webhook — handle directly or proxy to billing
res.statusCode = 200;
res.end(JSON.stringify({ received: true }));
} else {
res.statusCode = 404;
res.end(JSON.stringify({ error: 'Not Found', path: url }));
}
});
// Health check loop
async function updateHealth() {
for (const [name, service] of Object.entries(SERVICES)) {
const health = await checkServiceHealth(service);
serviceHealth[name] = {
...health,
lastCheck: Date.now(),
};
}
}
// Initial health check
updateHealth();
setInterval(updateHealth, 30000); // Every 30 seconds
server.listen(GATEWAY_CONFIG.port, () => {
console.log(`🚀 LandveX Gateway running on port ${GATEWAY_CONFIG.port}`);
console.log(`📡 Services:`);
Object.entries(SERVICES).forEach(([key, service]) => {
console.log(` - ${service.name}: http://${service.host}:${service.port}`);
});
});
// Proxy to Intelligence Lab
app.use('/api/v1', createProxyMiddleware({
target: 'http://localhost:3002',
changeOrigin: true,
pathRewrite: { '^/api/v1': '/api/v1' },
}));
// Proxy to Apollo CRM
app.use('/api/apollo', createProxyMiddleware({
target: 'http://localhost:3001',
changeOrigin: true,
pathRewrite: { '^/api/apollo': '/api/apollo' },
}));
// Proxy to Ledger
app.use('/api/ledger', createProxyMiddleware({
target: 'http://localhost:3250',
changeOrigin: true,
pathRewrite: { '^/api/ledger': '' },
}));
// Proxy to Incidents
app.use('/api/incidents', createProxyMiddleware({
target: 'http://localhost:3303',
changeOrigin: true,
pathRewrite: { '^/api/incidents': '' },
}));
// Stripe webhook
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), (req, res) => {
// TODO: Implement Stripe webhook handling
res.json({ received: true });
});
// Start server
app.listen(PORT, () => {
console.log(`🚀 LandveX Master Gateway running on port ${PORT}`);
console.log(`📡 Proxying to:`);
console.log(` - Intelligence Lab: http://localhost:3002`);
console.log(` - Apollo CRM: http://localhost:3001`);
console.log(` - Ledger: http://localhost:3250`);
console.log(` - Incidents: http://localhost:3303`);
});
export default app;
+87
View File
@@ -0,0 +1,87 @@
/**
* LandveX Gateway — HTTP Proxy
* Routes requests to backend services
*/
import http from 'http';
export function proxyRequest(req, res, target) {
const options = {
hostname: target.host,
port: target.port,
path: req.url,
method: req.method,
headers: {
...req.headers,
host: `${target.host}:${target.port}`,
},
timeout: 30000,
};
const proxyReq = http.request(options, (proxyRes) => {
res.writeHead(proxyRes.statusCode || 500, proxyRes.headers);
proxyRes.pipe(res);
});
proxyReq.on('error', (err) => {
console.error(`[PROXY ERROR] ${target.name}: ${err.message}`);
res.statusCode = 502;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
error: 'Bad Gateway',
service: target.name,
message: err.message,
}));
});
proxyReq.on('timeout', () => {
proxyReq.destroy();
res.statusCode = 504;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
error: 'Gateway Timeout',
service: target.name,
message: 'Request timed out',
}));
});
req.pipe(proxyReq);
}
export async function checkServiceHealth(service) {
const start = Date.now();
return new Promise((resolve) => {
const req = http.request({
hostname: service.host,
port: service.port,
path: service.healthPath,
method: 'GET',
timeout: 5000,
}, (res) => {
resolve({
healthy: res.statusCode === 200,
latency: Date.now() - start,
});
});
req.on('error', (err) => {
resolve({
healthy: false,
latency: Date.now() - start,
error: err.message,
});
});
req.on('timeout', () => {
req.destroy();
resolve({
healthy: false,
latency: Date.now() - start,
error: 'Timeout',
});
});
req.end();
});
}
+92
View File
@@ -0,0 +1,92 @@
//**
* LandveX Gateway HTTP Proxy
* Routes requests to backend services
*/
import http from 'http';
import { ServiceConfig } from './config';
export function proxyRequest(
req: http.IncomingMessage,
res: http.ServerResponse,
target: ServiceConfig
): void {
const options: http.RequestOptions = {
hostname: target.host,
port: target.port,
path: req.url,
method: req.method,
headers: {
...req.headers,
host: `${target.host}:${target.port}`,
},
timeout: 30000,
};
const proxyReq = http.request(options, (proxyRes) => {
res.writeHead(proxyRes.statusCode || 500, proxyRes.headers);
proxyRes.pipe(res);
});
proxyReq.on('error', (err) => {
console.error(`[PROXY ERROR] ${target.name}: ${err.message}`);
res.statusCode = 502;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
error: 'Bad Gateway',
service: target.name,
message: err.message,
}));
});
proxyReq.on('timeout', () => {
proxyReq.destroy();
res.statusCode = 504;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
error: 'Gateway Timeout',
service: target.name,
message: 'Request timed out',
}));
});
req.pipe(proxyReq);
}
export async function checkServiceHealth(service: ServiceConfig): Promise<{ healthy: boolean; latency: number; error?: string }> {
const start = Date.now();
return new Promise((resolve) => {
const req = http.request({
hostname: service.host,
port: service.port,
path: service.healthPath,
method: 'GET',
timeout: 5000,
}, (res) => {
resolve({
healthy: res.statusCode === 200,
latency: Date.now() - start,
});
});
req.on('error', (err) => {
resolve({
healthy: false,
latency: Date.now() - start,
error: err.message,
});
});
req.on('timeout', () => {
req.destroy();
resolve({
healthy: false,
latency: Date.now() - start,
error: 'Timeout',
});
});
req.end();
});
}