#!/usr/bin/env node // Tiny webhook receiver: validates the shared secret, then shells out to rebuild.sh. // Run on the homeserver (NOT inside docker) on port 4321. // // WEBHOOK_PORT=4321 REPO_DIR=/opt/softvowels REBUILD_TOKEN=change-me node scripts/webhook.js // // Or via the included systemd unit (see scripts/softvowels-webhook.service). import { createServer } from 'node:http'; import { spawn } from 'node:child_process'; import { writeFileSync } from 'node:fs'; const PORT = Number(process.env.WEBHOOK_PORT || 4321); const TOKEN = process.env.REBUILD_TOKEN || process.env.ASTRO_REVALIDATE_TOKEN; const REPO = process.env.REPO_DIR || '/opt/softvowels'; const SCRIPT = process.env.REBUILD_SCRIPT || `${REPO}/scripts/rebuild.sh`; const LOG = process.env.LOG_FILE || '/var/log/softvowels-rebuild.log'; if (!TOKEN) { console.error('REBUILD_TOKEN is required'); process.exit(1); } function log(line) { const ts = new Date().toISOString(); const msg = `[${ts}] ${line}\n`; try { writeFileSync(LOG, msg, { flag: 'a' }); } catch { /* noop */ } process.stdout.write(msg); } function triggerRebuild(collection, op) { log(`Trigger rebuild for ${collection}.${op}`); const child = spawn('bash', [SCRIPT], { detached: true, stdio: 'ignore' }); child.unref(); } const server = createServer((req, res) => { if (req.method !== 'POST' || req.url === '/') { res.writeHead(405); res.end('Method Not Allowed'); return; } const url = new URL(req.url, 'http://x'); const secret = url.searchParams.get('secret'); const collection = url.searchParams.get('collection') ?? 'unknown'; const op = url.searchParams.get('op') ?? 'change'; if (secret !== TOKEN) { log(`Bad secret from ${req.socket.remoteAddress}`); res.writeHead(401); res.end('Unauthorized'); return; } triggerRebuild(collection, op); res.writeHead(202, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: true, queued: true })); }); server.listen(PORT, '127.0.0.1', () => { log(`Webhook listening on http://127.0.0.1:${PORT}/__revalidate`); });