#!/usr/bin/env node /** * Tiny HTTP agent on the dashboards VPS. * Super Admin → Nest API → POST here → runs ssl-sync.sh or ssl-issue-tenant.sh * * Env (/etc/meshkee/ssl-sync-agent.env): * SSL_SYNC_AGENT_TOKEN=... * SSL_SYNC_SCRIPT=/opt/meshkee/dashboards/deploy/ssl-sync.sh * SSL_TENANT_SCRIPT=/opt/meshkee/dashboards/deploy/ssl-issue-tenant.sh * PORT=9051 * BIND=0.0.0.0 * * POST /ssl-sync * Body (optional JSON): * { "wait": true } — full dashboard cert sync (all tenants, cron / Sync SSL button) * { "wait": true, "tenantApex": "example.com" } — only business./customer. for one domain * wait=false (default): accept and run in background → 202 * wait=true: run script and wait for exit → 200 / 500 */ import { createServer } from 'node:http' import { spawn } from 'node:child_process' import { accessSync, constants } from 'node:fs' const TOKEN = (process.env.SSL_SYNC_AGENT_TOKEN || '').trim() const SCRIPT = (process.env.SSL_SYNC_SCRIPT || '/opt/meshkee/dashboards/deploy/ssl-sync.sh').trim() const TENANT_SCRIPT = ( process.env.SSL_TENANT_SCRIPT || '/opt/meshkee/dashboards/deploy/ssl-issue-tenant.sh' ).trim() const PORT = Number(process.env.PORT || 9051) const BIND = (process.env.BIND || '0.0.0.0').trim() if (!TOKEN) { console.error('SSL_SYNC_AGENT_TOKEN is required') process.exit(1) } for (const path of [SCRIPT, TENANT_SCRIPT]) { try { accessSync(path, constants.X_OK) } catch { console.error(`SSL script missing or not executable: ${path}`) process.exit(1) } } let running = false function json(res, status, body) { const payload = JSON.stringify(body) res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload), }) res.end(payload) } function readJson(req) { return new Promise((resolve, reject) => { const chunks = [] req.on('data', (c) => chunks.push(c)) req.on('end', () => { const raw = Buffer.concat(chunks).toString('utf8').trim() if (!raw) return resolve({}) try { resolve(JSON.parse(raw)) } catch (err) { reject(err) } }) req.on('error', reject) }) } function spawnSync(body, wait) { const tenantApex = typeof body.tenantApex === 'string' ? body.tenantApex.trim().toLowerCase() : '' const script = tenantApex ? TENANT_SCRIPT : SCRIPT const env = { ...process.env } if (tenantApex) { env.SSL_TENANT_APEX = tenantApex } if (wait) { return new Promise((resolve) => { running = true const child = spawn(script, [], { stdio: ['ignore', 'pipe', 'pipe'], env, }) let stdout = '' let stderr = '' child.stdout.on('data', (d) => { stdout += d.toString() }) child.stderr.on('data', (d) => { stderr += d.toString() }) child.on('error', (err) => { running = false resolve({ ok: false, code: 1, log: err.message, }) }) child.on('exit', (code, signal) => { running = false const log = `${stdout}${stderr}`.trim().slice(-4000) console.log( `${new Date().toISOString()} ssl-sync waited script=${script} code=${code} signal=${signal ?? ''}`, ) resolve({ ok: code === 0, code: code ?? 1, log, }) }) }) } running = true const child = spawn(script, [], { detached: true, stdio: 'ignore', env, }) child.on('error', (err) => { console.error(`${new Date().toISOString()} spawn error:`, err.message) running = false }) child.on('exit', (code, signal) => { console.log( `${new Date().toISOString()} ssl-sync finished script=${script} code=${code} signal=${signal ?? ''}`, ) running = false }) child.unref() return null } const server = createServer(async (req, res) => { if (req.method === 'GET' && req.url === '/health') { return json(res, 200, { ok: true, running }) } if (req.method !== 'POST' || req.url !== '/ssl-sync') { return json(res, 404, { error: 'not found' }) } const provided = String(req.headers['x-ssl-sync-agent-token'] ?? '').trim() if (!provided || provided !== TOKEN) { return json(res, 401, { error: 'unauthorized' }) } if (running) { return json(res, 409, { error: 'ssl sync already running' }) } let body = {} try { body = await readJson(req) } catch { return json(res, 400, { error: 'invalid json' }) } const wait = body && body.wait === true const tenantApex = typeof body.tenantApex === 'string' ? body.tenantApex.trim().toLowerCase() : '' if (wait) { console.log( `${new Date().toISOString()} ssl-sync wait start tenantApex=${tenantApex || '(all)'}`, ) const result = await spawnSync(body, true) if (!result?.ok) { return json(res, 500, { status: 'failed', message: tenantApex ? 'Tenant dashboard SSL issue failed' : 'SSL sync script failed', code: result?.code ?? 1, log: result?.log ?? '', }) } return json(res, 200, { status: 'ok', message: tenantApex ? `Tenant dashboard SSL issued for ${tenantApex}` : 'SSL sync completed', log: result.log, }) } spawnSync(body, false) console.log( `${new Date().toISOString()} ssl-sync accepted tenantApex=${tenantApex || '(all)'}`, ) return json(res, 202, { status: 'accepted', message: tenantApex ? `Tenant dashboard SSL started for ${tenantApex}` : 'SSL sync started', }) }) server.listen(PORT, BIND, () => { console.log(`ssl-sync-agent listening on ${BIND}:${PORT}`) })