const express = require('express'); const http = require('http'); /** * OpenClaw management routes * Proxies gateway API calls through DashCaddy so the token never leaves the server. * * GET /openclaw/status → container info + gateway health * POST /openclaw/deploy → deploy OpenClaw container * GET /openclaw/proxy/* → proxy GET to gateway * POST /openclaw/proxy/* → proxy POST to gateway * DELETE /openclaw → remove container */ module.exports = function openClawRoutes(ctx) { const router = express.Router(); const docker = ctx.docker; const asyncHandler = ctx.asyncHandler; const log = ctx.log || console; // ── helpers ────────────────────────────────────────────────────────────── async function findOpenClawContainer() { const containers = await docker.client.listContainers({ all: true }); return containers.find(function(c) { return c.Image === 'ghcr.io/nousresearch/openclaw:latest' || (c.Labels && c.Labels['dashcaddy.managed'] === 'true' && c.Names.some(function(n) { return n.includes('openclaw'); })); }) || null; } async function getGatewayToken(containerId) { try { const info = await docker.client.containerInfo(containerId); const entry = (info.Config.Env || []).find(function(e) { return e.startsWith('OPENCLAW_GATEWAY_TOKEN='); }); return entry ? entry.split('=')[1] : null; } catch(err) { return null; } } async function getContainerPort(containerId) { try { const containers = await docker.client.listContainers({ all: true }); const c = containers.find(function(x) { return x.Id === containerId || x.Id.startsWith(containerId); }); if (c && c.Ports) { const p = c.Ports.find(function(x) { return x.PrivatePort === 18792; }); if (p && p.PublicPort) return String(p.PublicPort); } return '18792'; } catch(err) { return '18792'; } } async function gatewayHealth(baseUrl, token) { return new Promise(function(resolve) { const headers = {}; if (token) headers['Authorization'] = 'Bearer ' + token; const req = http.get(baseUrl + '/health', { headers: headers }, function(res) { let data = ''; res.on('data', function(d) { data += d; }); res.on('end', function() { try { resolve({ ok: true, data: JSON.parse(data) }); } catch(e) { resolve({ ok: true, data: data }); } }); }); req.on('error', function(e) { resolve({ ok: false, error: e.message }); }); req.setTimeout(5000, function() { req.destroy(); resolve({ ok: false, error: 'timeout' }); }); }); } function proxyRequest(req, res, targetBase, path, token) { const headers = {}; if (token) headers['Authorization'] = 'Bearer ' + token; headers['X-Forwarded-For'] = req.ip; headers['X-Forwarded-Proto'] = req.protocol; const url = targetBase + '/' + path; const method = req.method; if (['POST', 'PUT', 'PATCH'].includes(method)) { const body = JSON.stringify(req.body); headers['Content-Type'] = 'application/json'; headers['Content-Length'] = Buffer.byteLength(body); const proxyReq = http.request(url, { method: method, headers: headers }, function(proxyRes) { res.set(proxyRes.headers); res.status(proxyRes.statusCode); proxyRes.on('data', function(d) { res.write(d); }); proxyRes.on('end', function() { res.end(); }); }); proxyReq.on('error', function(e) { res.status(502).json({ success: false, error: e.message }); }); proxyReq.setTimeout(15000, function() { proxyReq.destroy(); res.status(504).json({ success: false, error: 'gateway timeout' }); }); proxyReq.write(body); proxyReq.end(); } else { const proxyReq = http.get(url, { headers: headers }, function(proxyRes) { res.set(proxyRes.headers); res.status(proxyRes.statusCode); proxyRes.on('data', function(d) { res.write(d); }); proxyRes.on('end', function() { res.end(); }); }); proxyReq.on('error', function(e) { res.status(502).json({ success: false, error: e.message }); }); proxyReq.setTimeout(15000, function() { proxyReq.destroy(); res.status(504).json({ success: false, error: 'gateway timeout' }); }); } } // ── GET /openclaw/status ──────────────────────────────────────────────── router.get('/status', asyncHandler(async function(req, res) { const container = await findOpenClawContainer(); if (!container) { return res.json({ success: true, deployed: false }); } const token = await getGatewayToken(container.Id); const port = await getContainerPort(container.Id); const baseUrl = 'http://localhost:' + port; const health = await gatewayHealth(baseUrl, token); res.json({ success: true, deployed: true, container: { id: container.Id.slice(0, 12), name: container.Name, state: container.State, status: container.Status, created: container.Created, image: container.Image }, gateway: { url: baseUrl, port: port, healthy: health.ok, healthData: health.data || null, tokenSet: !!token } }); })); // ── POST /openclaw/deploy ─────────────────────────────────────────────── router.post('/deploy', asyncHandler(async function(req, res) { const existing = await findOpenClawContainer(); if (existing) { return res.status(409).json({ success: false, error: 'OpenClaw is already deployed' }); } const image = 'ghcr.io/nousresearch/openclaw:latest'; const name = 'openclaw-' + Date.now(); const gatewayToken = generateToken(); // Pull image log.info('Pulling ' + image + '...'); try { await new Promise(function(resolve, reject) { docker.client.pull(image, function(err, stream) { if (err) return reject(err); docker.client.modem.followProgress(stream, function(err2) { if (err2) return reject(err2); resolve(); }); }); }); } catch(e) { log.error('OpenClaw pull failed: ' + e.message); return res.status(500).json({ success: false, error: 'Failed to pull image: ' + e.message }); } // Create + start container try { const container = await docker.client.createContainer({ name: name, Image: image, Env: [ 'OPENCLAW_GATEWAY_MODE=local', 'OPENCLAW_GATEWAY_TOKEN=' + gatewayToken ], HostConfig: { PortBindings: { '18792/tcp': [{ HostPort: '18792' }] }, RestartPolicy: { Name: 'unless-stopped' }, Labels: { 'dashcaddy.managed': 'true', 'dashcaddy.app': 'openclaw' } }, ExposedPorts: { '18792/tcp': {} } }); await container.start(); log.info('OpenClaw deployed: ' + container.id.slice(0, 12)); res.json({ success: true, deployed: true, container: { id: container.id.slice(0, 12), name: name }, gateway: { url: 'http://localhost:18792', token: gatewayToken } }); } catch(e) { log.error('OpenClaw deploy failed: ' + e.message); res.status(500).json({ success: false, error: 'Deploy failed: ' + e.message }); } })); // ── GET /openclaw/proxy/* ─────────────────────────────────────────────── router.get('/proxy/*', asyncHandler(async function(req, res) { const container = await findOpenClawContainer(); if (!container) return res.status(404).json({ success: false, error: 'OpenClaw not deployed' }); const token = await getGatewayToken(container.Id); const port = await getContainerPort(container.Id); const baseUrl = 'http://localhost:' + port; const path = req.params[0]; proxyRequest(req, res, baseUrl, path, token); })); // ── POST /openclaw/proxy/* ────────────────────────────────────────────── router.post('/proxy/*', asyncHandler(async function(req, res) { const container = await findOpenClawContainer(); if (!container) return res.status(404).json({ success: false, error: 'OpenClaw not deployed' }); const token = await getGatewayToken(container.Id); const port = await getContainerPort(container.Id); const baseUrl = 'http://localhost:' + port; const path = req.params[0]; proxyRequest(req, res, baseUrl, path, token); })); // ── DELETE /openclaw ─────────────────────────────────────────────────── router.delete('/', asyncHandler(async function(req, res) { const container = await findOpenClawContainer(); if (!container) return res.status(404).json({ success: false, error: 'OpenClaw not deployed' }); try { const c = docker.client.container(container.Id); await c.stop().catch(function() {}); await c.remove({ force: true }); log.info('OpenClaw container ' + container.Id.slice(0, 12) + ' removed'); res.json({ success: true, message: 'OpenClaw removed' }); } catch(e) { log.error('Failed to remove OpenClaw: ' + e.message); res.status(500).json({ success: false, error: e.message }); } })); return router; }; // ── token generator ────────────────────────────────────────────────────────── function generateToken() { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; for (let i = 0; i < 32; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; }