import crypto from 'node:crypto'; import { execFile } from 'node:child_process'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import express from 'express'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const app = express(); const port = Number(process.env.PORT || 3020); const webhookPath = process.env.WEBHOOK_PATH || '/webhook/github'; const webhookSecret = process.env.WEBHOOK_SECRET || ''; const pm2AppName = process.env.PM2_APP_NAME || 'linkweb'; const pm2Command = process.env.PM2_BIN || (process.platform === 'win32' ? 'pm2.cmd' : 'pm2'); function verifyGitHubSignature(req) { if (!webhookSecret) { return true; } const signature = req.get('x-hub-signature-256'); if (!signature?.startsWith('sha256=')) { return false; } const digest = `sha256=${crypto .createHmac('sha256', webhookSecret) .update(req.body) .digest('hex')}`; const received = Buffer.from(signature); const expected = Buffer.from(digest); return received.length === expected.length && crypto.timingSafeEqual(received, expected); } app.post(webhookPath, express.raw({ type: '*/*' }), (req, res) => { if (!verifyGitHubSignature(req)) { res.status(401).json({ ok: false, error: 'Invalid webhook signature' }); return; } res.status(202).json({ ok: true, restarting: pm2AppName }); execFile(pm2Command, ['restart', pm2AppName], (error, stdout, stderr) => { if (error) { console.error('PM2 restart failed:', error.message); if (stderr) { console.error(stderr); } return; } if (stdout) { console.log(stdout); } }); }); app.get('/healthz', (_req, res) => { res.json({ ok: true }); }); app.use(express.static(path.join(__dirname, 'dist'))); app.use((_req, res) => { res.sendFile(path.join(__dirname, 'dist', 'index.html')); }); app.listen(port, () => { console.log(`Toothless' Bot Home listening on port ${port}`); });