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
+26
View File
@@ -0,0 +1,26 @@
/**
* LandveX Communications Module
*
* Email, SMS, Push notifications
* Port: 3010
*/
import http from 'http';
import { handleRequest } from './router.mjs';
const PORT = process.env.PORT || 3010;
const server = http.createServer(handleRequest);
server.listen(PORT, () => {
console.log(`📧 LandveX Communications running on port ${PORT}`);
console.log(`📨 Endpoints:`);
console.log(` GET /health → Health check`);
console.log(` GET /api/v1/communications/threads → List threads`);
console.log(` POST /api/v1/communications/threads → Create thread`);
console.log(` POST /api/v1/communications/send → Send message`);
console.log(` GET /api/v1/communications/templates → List templates`);
console.log(` POST /api/v1/communications/templates → Create template`);
console.log(` POST /webhooks/mailgun → Mailgun webhook`);
console.log(` POST /webhooks/twilio → Twilio webhook`);
});
+26
View File
@@ -0,0 +1,26 @@
/**
* LandveX Communications Module
*
* Email, SMS, Push notifications
* Port: 3010
*/
import http from 'http';
import { handleRequest } from './router.js';
const PORT = process.env.PORT || 3010;
const server = http.createServer(handleRequest);
server.listen(PORT, () => {
console.log(`📧 LandveX Communications running on port ${PORT}`);
console.log(`📨 Endpoints:`);
console.log(` GET /health → Health check`);
console.log(` GET /api/v1/communications/threads → List threads`);
console.log(` POST /api/v1/communications/threads → Create thread`);
console.log(` POST /api/v1/communications/send → Send message`);
console.log(` GET /api/v1/communications/templates → List templates`);
console.log(` POST /api/v1/communications/templates → Create template`);
console.log(` POST /webhooks/mailgun → Mailgun webhook`);
console.log(` POST /webhooks/twilio → Twilio webhook`);
});
+213
View File
@@ -0,0 +1,213 @@
/**
* LandveX Communications — Router
* Zero-dependency HTTP router
*/
import http from 'http';
import { URL } from 'url';
// In-memory store
const threads = {};
const templates = {};
const messages = {};
export function handleRequest(req, res) {
// CORS
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Content-Type', 'application/json');
if (req.method === 'OPTIONS') {
res.statusCode = 204;
res.end();
return;
}
const url = new URL(req.url || '/', `http://${req.headers.host}`);
const path = url.pathname;
// Health check
if (path === '/health') {
res.end(JSON.stringify({
status: 'ok',
version: '0.1.0-communications',
service: 'communications',
channels: ['email', 'sms', 'push'],
}));
return;
}
// Threads
if (path === '/api/v1/communications/threads' && req.method === 'GET') {
handleListThreads(req, res, url);
} else if (path === '/api/v1/communications/threads' && req.method === 'POST') {
handleCreateThread(req, res);
} else if (path.startsWith('/api/v1/communications/threads/') && req.method === 'GET') {
handleGetThread(req, res, path);
}
// Messages
else if (path === '/api/v1/communications/send' && req.method === 'POST') {
handleSendMessage(req, res);
}
// Templates
else if (path === '/api/v1/communications/templates' && req.method === 'GET') {
handleListTemplates(req, res);
} else if (path === '/api/v1/communications/templates' && req.method === 'POST') {
handleCreateTemplate(req, res);
}
// Webhooks
else if (path === '/webhooks/mailgun' && req.method === 'POST') {
handleMailgunWebhook(req, res);
} else if (path === '/webhooks/twilio' && req.method === 'POST') {
handleTwilioWebhook(req, res);
} else {
res.statusCode = 404;
res.end(JSON.stringify({ error: 'Not Found', path }));
}
}
function handleListThreads(req, res, url) {
const customerId = url.searchParams.get('customer') || 'all';
const type = url.searchParams.get('type');
let result = Object.values(threads);
if (customerId !== 'all') {
result = result.filter((t) => t.customerId === customerId);
}
if (type) {
result = result.filter((t) => t.type === type);
}
res.end(JSON.stringify({
threads: result,
count: result.length,
}));
}
function handleCreateThread(req, res) {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
try {
const data = JSON.parse(body);
const thread = {
id: `thread-${Date.now()}`,
...data,
messages: [],
status: 'open',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
threads[thread.id] = thread;
res.statusCode = 201;
res.end(JSON.stringify(thread));
} catch {
res.statusCode = 400;
res.end(JSON.stringify({ error: 'Invalid JSON' }));
}
});
}
function handleGetThread(req, res, path) {
const id = path.split('/').pop();
const thread = threads[id || ''];
if (!thread) {
res.statusCode = 404;
res.end(JSON.stringify({ error: 'Thread not found' }));
return;
}
res.end(JSON.stringify(thread));
}
function handleSendMessage(req, res) {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
try {
const data = JSON.parse(body);
// Simulate sending
const message = {
id: `msg-${Date.now()}`,
...data,
status: 'sent',
sentAt: new Date().toISOString(),
providerMessageId: `mock-${Date.now()}`,
};
// Add to thread if specified
if (data.threadId && threads[data.threadId]) {
threads[data.threadId].messages.push(message);
threads[data.threadId].updatedAt = new Date().toISOString();
}
res.statusCode = 201;
res.end(JSON.stringify({
success: true,
message,
note: 'Mock send — connect Mailgun/Twilio for real sending',
}));
} catch {
res.statusCode = 400;
res.end(JSON.stringify({ error: 'Invalid JSON' }));
}
});
}
function handleListTemplates(req, res) {
const result = Object.values(templates);
res.end(JSON.stringify({
templates: result,
count: result.length,
}));
}
function handleCreateTemplate(req, res) {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
try {
const data = JSON.parse(body);
const template = {
id: `template-${Date.now()}`,
...data,
usageCount: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
templates[template.id] = template;
res.statusCode = 201;
res.end(JSON.stringify(template));
} catch {
res.statusCode = 400;
res.end(JSON.stringify({ error: 'Invalid JSON' }));
}
});
}
function handleMailgunWebhook(req, res) {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
console.log('[Mailgun Webhook]', body);
res.statusCode = 200;
res.end(JSON.stringify({ received: true }));
});
}
function handleTwilioWebhook(req, res) {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
console.log('[Twilio Webhook]', body);
res.statusCode = 200;
res.end(JSON.stringify({ received: true }));
});
}
+213
View File
@@ -0,0 +1,213 @@
/**
* LandveX Communications — Router
* Zero-dependency HTTP router
*/
import http from 'http';
import { URL } from 'url';
// In-memory store
const threads: Record<string, any> = {};
const templates: Record<string, any> = {};
const messages: Record<string, any[]> = {};
export function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
// CORS
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Content-Type', 'application/json');
if (req.method === 'OPTIONS') {
res.statusCode = 204;
res.end();
return;
}
const url = new URL(req.url || '/', `http://${req.headers.host}`);
const path = url.pathname;
// Health check
if (path === '/health') {
res.end(JSON.stringify({
status: 'ok',
version: '0.1.0-communications',
service: 'communications',
channels: ['email', 'sms', 'push'],
}));
return;
}
// Threads
if (path === '/api/v1/communications/threads' && req.method === 'GET') {
handleListThreads(req, res, url);
} else if (path === '/api/v1/communications/threads' && req.method === 'POST') {
handleCreateThread(req, res);
} else if (path.startsWith('/api/v1/communications/threads/') && req.method === 'GET') {
handleGetThread(req, res, path);
}
// Messages
else if (path === '/api/v1/communications/send' && req.method === 'POST') {
handleSendMessage(req, res);
}
// Templates
else if (path === '/api/v1/communications/templates' && req.method === 'GET') {
handleListTemplates(req, res);
} else if (path === '/api/v1/communications/templates' && req.method === 'POST') {
handleCreateTemplate(req, res);
}
// Webhooks
else if (path === '/webhooks/mailgun' && req.method === 'POST') {
handleMailgunWebhook(req, res);
} else if (path === '/webhooks/twilio' && req.method === 'POST') {
handleTwilioWebhook(req, res);
} else {
res.statusCode = 404;
res.end(JSON.stringify({ error: 'Not Found', path }));
}
}
function handleListThreads(req: http.IncomingMessage, res: http.ServerResponse, url: URL): void {
const customerId = url.searchParams.get('customer') || 'all';
const type = url.searchParams.get('type');
let result = Object.values(threads);
if (customerId !== 'all') {
result = result.filter((t: any) => t.customerId === customerId);
}
if (type) {
result = result.filter((t: any) => t.type === type);
}
res.end(JSON.stringify({
threads: result,
count: result.length,
}));
}
function handleCreateThread(req: http.IncomingMessage, res: http.ServerResponse): void {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
try {
const data = JSON.parse(body);
const thread = {
id: `thread-${Date.now()}`,
...data,
messages: [],
status: 'open',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
threads[thread.id] = thread;
res.statusCode = 201;
res.end(JSON.stringify(thread));
} catch {
res.statusCode = 400;
res.end(JSON.stringify({ error: 'Invalid JSON' }));
}
});
}
function handleGetThread(req: http.IncomingMessage, res: http.ServerResponse, path: string): void {
const id = path.split('/').pop();
const thread = threads[id || ''];
if (!thread) {
res.statusCode = 404;
res.end(JSON.stringify({ error: 'Thread not found' }));
return;
}
res.end(JSON.stringify(thread));
}
function handleSendMessage(req: http.IncomingMessage, res: http.ServerResponse): void {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
try {
const data = JSON.parse(body);
// Simulate sending
const message = {
id: `msg-${Date.now()}`,
...data,
status: 'sent',
sentAt: new Date().toISOString(),
providerMessageId: `mock-${Date.now()}`,
};
// Add to thread if specified
if (data.threadId && threads[data.threadId]) {
threads[data.threadId].messages.push(message);
threads[data.threadId].updatedAt = new Date().toISOString();
}
res.statusCode = 201;
res.end(JSON.stringify({
success: true,
message,
note: 'Mock send — connect Mailgun/Twilio for real sending',
}));
} catch {
res.statusCode = 400;
res.end(JSON.stringify({ error: 'Invalid JSON' }));
}
});
}
function handleListTemplates(req: http.IncomingMessage, res: http.ServerResponse): void {
const result = Object.values(templates);
res.end(JSON.stringify({
templates: result,
count: result.length,
}));
}
function handleCreateTemplate(req: http.IncomingMessage, res: http.ServerResponse): void {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
try {
const data = JSON.parse(body);
const template = {
id: `template-${Date.now()}`,
...data,
usageCount: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
templates[template.id] = template;
res.statusCode = 201;
res.end(JSON.stringify(template));
} catch {
res.statusCode = 400;
res.end(JSON.stringify({ error: 'Invalid JSON' }));
}
});
}
function handleMailgunWebhook(req: http.IncomingMessage, res: http.ServerResponse): void {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
console.log('[Mailgun Webhook]', body);
res.statusCode = 200;
res.end(JSON.stringify({ received: true }));
});
}
function handleTwilioWebhook(req: http.IncomingMessage, res: http.ServerResponse): void {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
console.log('[Twilio Webhook]', body);
res.statusCode = 200;
res.end(JSON.stringify({ received: true }));
});
}