mirror of
https://git.meshkee.com/Meshkee/backend.git
synced 2026-08-11 22:30:59 +04:30
Persist deploy_slug on domains, call the websites agent /provision endpoint, and drop the hard-coded host map so Deploy appears from the UI. Co-authored-by: Cursor <cursoragent@cursor.com>
185 lines
4.7 KiB
JavaScript
185 lines
4.7 KiB
JavaScript
const http = require('http');
|
|
const { spawn } = require('child_process');
|
|
const { timingSafeEqual } = require('crypto');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const PORT = Number(process.env.PORT || 9050);
|
|
const ENV_FILE = path.join(__dirname, '.env');
|
|
|
|
function loadEnvFile() {
|
|
try {
|
|
const raw = fs.readFileSync(ENV_FILE, 'utf8');
|
|
for (const line of raw.split('\n')) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
const i = trimmed.indexOf('=');
|
|
if (i <= 0) continue;
|
|
const key = trimmed.slice(0, i).trim();
|
|
const val = trimmed.slice(i + 1).trim();
|
|
process.env[key] = val;
|
|
}
|
|
} catch (_) {
|
|
/* optional */
|
|
}
|
|
}
|
|
|
|
loadEnvFile();
|
|
|
|
function token() {
|
|
return String(process.env.DEPLOY_TOKEN || '').trim();
|
|
}
|
|
|
|
function allowedSlugs() {
|
|
loadEnvFile();
|
|
return new Set(
|
|
String(process.env.ALLOWED_SLUGS || '')
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean),
|
|
);
|
|
}
|
|
|
|
function safeEqual(a, b) {
|
|
const x = Buffer.from(a);
|
|
const y = Buffer.from(b);
|
|
if (x.length !== y.length) return false;
|
|
return timingSafeEqual(x, y);
|
|
}
|
|
|
|
function readJson(req) {
|
|
return new Promise((resolve, reject) => {
|
|
const chunks = [];
|
|
req.on('data', (c) => chunks.push(c));
|
|
req.on('end', () => {
|
|
try {
|
|
const raw = Buffer.concat(chunks).toString('utf8') || '{}';
|
|
resolve(JSON.parse(raw));
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
req.on('error', reject);
|
|
});
|
|
}
|
|
|
|
function send(res, status, body) {
|
|
const data = JSON.stringify(body);
|
|
res.writeHead(status, {
|
|
'Content-Type': 'application/json',
|
|
'Content-Length': Buffer.byteLength(data),
|
|
});
|
|
res.end(data);
|
|
}
|
|
|
|
function assertAuth(req, res) {
|
|
const provided = String(req.headers['x-deploy-token'] || '').trim();
|
|
const expected = token();
|
|
if (!expected || !provided || !safeEqual(provided, expected)) {
|
|
send(res, 401, { error: 'unauthorized' });
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function runScript(script, args) {
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(script, args, {
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
let stdout = '';
|
|
let stderr = '';
|
|
child.stdout.on('data', (d) => {
|
|
stdout += d.toString();
|
|
});
|
|
child.stderr.on('data', (d) => {
|
|
stderr += d.toString();
|
|
});
|
|
child.on('error', reject);
|
|
child.on('close', (code) => {
|
|
if (code === 0) resolve({ stdout, stderr });
|
|
else reject(new Error(stderr || stdout || `exit ${code}`));
|
|
});
|
|
});
|
|
}
|
|
|
|
const server = http.createServer(async (req, res) => {
|
|
if (req.method === 'GET' && req.url === '/health') {
|
|
return send(res, 200, { ok: true, allowed: [...allowedSlugs()] });
|
|
}
|
|
|
|
if (req.method === 'POST' && req.url === '/deploy') {
|
|
if (!assertAuth(req, res)) return;
|
|
|
|
let body;
|
|
try {
|
|
body = await readJson(req);
|
|
} catch {
|
|
return send(res, 400, { error: 'invalid json' });
|
|
}
|
|
|
|
const slug = String(body.slug || '').trim();
|
|
if (!slug || !allowedSlugs().has(slug)) {
|
|
return send(res, 400, { error: 'unknown or disallowed slug' });
|
|
}
|
|
|
|
const child = spawn('/opt/websites-agent/deploy.sh', [slug], {
|
|
detached: true,
|
|
stdio: 'ignore',
|
|
});
|
|
child.unref();
|
|
|
|
return send(res, 202, { status: 'accepted', slug });
|
|
}
|
|
|
|
if (req.method === 'POST' && req.url === '/provision') {
|
|
if (!assertAuth(req, res)) return;
|
|
|
|
let body;
|
|
try {
|
|
body = await readJson(req);
|
|
} catch {
|
|
return send(res, 400, { error: 'invalid json' });
|
|
}
|
|
|
|
const slug = String(body.slug || '').trim();
|
|
const host = String(body.host || '').trim().toLowerCase();
|
|
const gitRepoUrl = String(body.gitRepoUrl || body.git_repo_url || '').trim();
|
|
|
|
if (!slug || !host || !gitRepoUrl) {
|
|
return send(res, 400, { error: 'slug, host, and gitRepoUrl are required' });
|
|
}
|
|
|
|
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(slug)) {
|
|
return send(res, 400, { error: 'invalid slug' });
|
|
}
|
|
|
|
try {
|
|
const result = await runScript('/opt/websites-agent/provision.sh', [
|
|
slug,
|
|
host,
|
|
gitRepoUrl,
|
|
]);
|
|
// Reload allowlist from .env (provision.sh appends slug)
|
|
loadEnvFile();
|
|
return send(res, 200, {
|
|
status: 'provisioned',
|
|
slug,
|
|
host,
|
|
log: (result.stdout || '').slice(-2000),
|
|
});
|
|
} catch (err) {
|
|
return send(res, 500, {
|
|
error: 'provision failed',
|
|
detail: err instanceof Error ? err.message.slice(0, 2000) : String(err),
|
|
});
|
|
}
|
|
}
|
|
|
|
return send(res, 404, { error: 'not found' });
|
|
});
|
|
|
|
server.listen(PORT, '0.0.0.0', () => {
|
|
console.log('websites-deploy-agent listening on :' + PORT);
|
|
});
|