diff --git a/.gitignore b/.gitignore index 4ac1716..60db35d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ node_modules/ # Runtime state/config files (generated, not source) +# Note: data/ subdir contains runtime state (credentials, secrets, history) — never commit +dashcaddy-api/data/ dashcaddy-api/credentials.json dashcaddy-api/.env .env @@ -17,6 +19,19 @@ dashcaddy-api/update-config.json dashcaddy-api/update-history.json dashcaddy-api/dashcaddy-errors.log +# Auto-updater backups (created by dashcaddy-update.sh when rolling back) +start.sh.bak* +scripts/*.bak* + +# Auto-updater runtime state (history + secrets + staging) +updates/ + +# Scratch / debug scripts (left over from past sessions) +cm_check*.js +full_test.js +login_test.js +login_backup_test.js + # Build output dashcaddy-installer/build-output/ dashcaddy-installer/dist/ diff --git a/routes/openclaw.js b/routes/openclaw.js deleted file mode 100644 index c4e6689..0000000 --- a/routes/openclaw.js +++ /dev/null @@ -1,272 +0,0 @@ -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; -}