LandveX Gateway v0.1.0 + Operations API + Communications Module — ALL RUNNING
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@landvex/communications",
|
||||
"version": "0.1.0",
|
||||
"description": "LandveX Communications Module — Email, SMS, Push notifications",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "ts-node src/index.ts",
|
||||
"start": "node dist/index.js",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"cors": "^2.8.5",
|
||||
"helmet": "^7.0.0",
|
||||
"dotenv": "^16.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/cors": "^2.8.13",
|
||||
"typescript": "^5.1.0",
|
||||
"ts-node": "^10.9.0",
|
||||
"jest": "^29.5.0",
|
||||
"@types/jest": "^29.5.0"
|
||||
}
|
||||
}
|
||||
@@ -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`);
|
||||
});
|
||||
@@ -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`);
|
||||
});
|
||||
@@ -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 }));
|
||||
});
|
||||
}
|
||||
@@ -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 }));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user