← back to Tools Dw Hub
server.js
177 lines
#!/usr/bin/env node
// tools.designerwallcoverings.com — zero-dependency launcher hub for all DW build tools.
// Node built-ins only (http/fs/path/net) — no npm install required.
const http = require('http');
const fs = require('fs');
const path = require('path');
const PORT = process.env.PORT || 0; // 0 = OS-assigned free port (local dev)
const BASIC_AUTH = process.env.BASIC_AUTH || 'admin:DW2024!';
const PROBE_HOST = process.env.PROBE_HOST || '127.0.0.1'; // where the tool apps listen (localhost on Mac2; tailnet IP when public)
const TOOL_HOST = process.env.TOOL_HOST || ''; // host the UI builds Launch links against ('' = the browser's own hostname)
const [AUTH_USER, AUTH_PASS] = BASIC_AUTH.split(':');
const ROOT = __dirname;
const manifest = () => JSON.parse(fs.readFileSync(path.join(ROOT, 'tools.json'), 'utf8'));
// --- HTTP marker probe: 'up' ONLY if the response is really THIS tool ---
// 200 + body contains the tool's marker string => up (true-positive).
// 401 => up (the tool's own auth wall answered — alive).
// TCP-open but wrong body (another app squatting the port) => down. This kills the
// false-positive class where port 5000 = macOS ControlCenter read as a live tool.
function probe(port, marker, timeout = 2500, path = '/', hop = 0, host = PROBE_HOST) {
return new Promise((resolve) => {
if (!port) return resolve('n/a');
const req = http.get({ host, port, path, timeout }, (res) => {
if (res.statusCode === 401) { res.resume(); return resolve('up'); }
// one-hop redirect follow (auth tools 302 to /login — still THIS tool if the target marker-matches)
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location && hop < 1) {
res.resume();
const loc = res.headers.location.startsWith('http') ? new URL(res.headers.location).pathname : res.headers.location;
return probe(port, marker, timeout, loc, hop + 1, host).then(resolve);
}
let body = '';
res.on('data', (c) => { if (body.length < 65536) body += c; });
res.on('end', () => {
if (res.statusCode === 200 && (!marker || body.includes(marker))) return resolve('up');
resolve('down');
});
});
req.on('timeout', () => { req.destroy(); resolve('down'); });
req.on('error', () => resolve('down'));
});
}
// --- Start/Stop tools from the hub ---
// LOCAL mode (Mac2, where the tools live): spawns the manifest's own start command in a
// detached process group, injecting secrets from secrets-manager at RUNTIME (never stored).
// REMOTE mode (Kamatera public hub): proxies /api/start|stop to the Mac2 hub over tailnet
// (MAC_HUB env). Only manifest-defined commands ever run — slugs resolve server-side.
const { spawn, execSync } = require('child_process');
const os = require('os');
const IS_TOOL_HOST = process.env.TOOLS_LOCAL === '1' || os.platform() === 'darwin';
const MAC_HUB = process.env.MAC_HUB || '';
const LOG_DIR = '/tmp/dw-tools';
function loadSecretsEnv() {
const env = {};
try {
const raw = fs.readFileSync(path.join(process.env.HOME || '', 'Projects/secrets-manager/.env'), 'utf8');
for (const line of raw.split('\n')) {
const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
if (m) env[m[1]] = m[2].replace(/^["']|["']\s*$/g, '');
}
} catch { /* no secrets file — tools that need keys will warn themselves */ }
return env;
}
function pidOnPort(port) {
try { return parseInt(execSync(`lsof -nP -tiTCP:${port} -sTCP:LISTEN`).toString().split('\n')[0], 10) || null; }
catch { return null; }
}
function startTool(t) {
if (!t || !t.start || t.cli || t.missingSource) return { ok: false, error: 'not startable' };
if (t.port && pidOnPort(t.port)) return { ok: true, already: true };
fs.mkdirSync(LOG_DIR, { recursive: true });
const log = fs.openSync(path.join(LOG_DIR, `${t.slug}.log`), 'a');
const env = {
...process.env, ...loadSecretsEnv(),
ADMIN_PASSWORD: process.env.ADMIN_PASSWORD || 'DW2024!',
DRY_RUN: '1', // hub-launched taggers never push; re-launch manually for writes
VISION_SCAN_CAP: process.env.VISION_SCAN_CAP || '25',
};
if (t.port) { env.PORT = String(t.port); env.NEXTAUTH_URL = `http://localhost:${t.port}`; }
const child = spawn('bash', ['-lc', t.start], { detached: true, stdio: ['ignore', log, log], env });
child.unref();
return { ok: true, pid: child.pid };
}
function stopTool(t) {
if (!t || !t.port) return { ok: false, error: 'no port' };
const pid = pidOnPort(t.port);
if (!pid) return { ok: true, already: true };
try { process.kill(-pid, 'SIGTERM'); } catch { try { process.kill(pid, 'SIGTERM'); } catch {} }
setTimeout(() => { const p2 = pidOnPort(t.port); if (p2) { try { process.kill(p2, 'SIGKILL'); } catch {} } }, 1500);
return { ok: true, killed: pid };
}
function proxyToMacHub(req, res, urlPath) {
const target = new URL(urlPath, MAC_HUB);
const preq = http.request(target, { method: 'POST', headers: { Authorization: req.headers.authorization || '' }, timeout: 25000 }, (pres) => {
res.writeHead(pres.statusCode, { 'Content-Type': 'application/json' });
pres.pipe(res);
});
preq.on('error', (e) => { res.writeHead(502, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: `Mac2 hub unreachable: ${e.message}` })); });
preq.on('timeout', () => { preq.destroy(); });
preq.end();
}
function unauthorized(res) {
res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="DW Tools"', 'Content-Type': 'text/plain' });
res.end('Authentication required');
}
function checkAuth(req) {
const h = req.headers.authorization || '';
if (!h.startsWith('Basic ')) return false;
const [u, p] = Buffer.from(h.slice(6), 'base64').toString().split(':');
return u === AUTH_USER && p === AUTH_PASS;
}
const server = http.createServer(async (req, res) => {
const url = req.url.split('?')[0];
// /healthz is OPEN (before auth) so the fleet keepalive never misreads a healthy 401 as dead.
if (url === '/healthz') {
res.writeHead(200, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ ok: true, service: 'tools-dw-hub' }));
}
if (!checkAuth(req)) return unauthorized(res);
if (url === '/api/tools') {
const m = manifest();
m.hub.toolHost = TOOL_HOST; // '' → UI falls back to the browser's own hostname (local dev)
res.writeHead(200, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify(m));
}
// Start/Stop a tool: local hub executes; public hub proxies to the Mac2 hub over tailnet.
const action = url.match(/^\/api\/(start|stop)\/([a-z0-9-]+)$/);
if (action && req.method === 'POST') {
const [, verb, slug] = action;
const t = manifest().tools.find((x) => x.slug === slug);
if (!t) { res.writeHead(404, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ ok: false, error: 'unknown tool' })); }
if (!IS_TOOL_HOST) {
if (!MAC_HUB) { res.writeHead(501, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ ok: false, error: 'MAC_HUB not configured' })); }
return proxyToMacHub(req, res, url);
}
const result = verb === 'start' ? startTool(t) : stopTool(t);
res.writeHead(200, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify(result));
}
if (url === '/api/status') {
const { tools } = manifest();
const results = await Promise.all(tools.map(async (t) => ({ slug: t.slug, status: await probe(t.port, t.marker, 2500, t.probePath || '/', 0, t.probeHost || PROBE_HOST) })));
res.writeHead(200, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify(Object.fromEntries(results.map((r) => [r.slug, r.status]))));
}
// static
const file = url === '/' ? '/index.html' : url;
const fp = path.join(ROOT, 'public', path.normalize(file).replace(/^(\.\.[/\\])+/, ''));
fs.readFile(fp, (err, data) => {
if (err) { res.writeHead(404, { 'Content-Type': 'text/plain' }); return res.end('Not found'); }
const ext = path.extname(fp);
const type = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml' }[ext] || 'application/octet-stream';
res.writeHead(200, { 'Content-Type': type });
res.end(data);
});
});
server.listen(PORT, () => {
const addr = server.address();
console.log(`[tools-dw-hub] listening on http://127.0.0.1:${addr.port} (auth ${AUTH_USER}/****, probing ${PROBE_HOST})`);
});