/** * `celilo subscribers serve --port

--secret ` — long-running * HTTP receiver for build-bus webhooks. * * Verifies signed envelopes against the configured shared secret * and emits a `build-bus.publish` event onto the local SQLite bus * for every verified delivery. Phase 4's hook dispatcher (also * started by this command when --dispatch is set) picks up those * local events and runs module-side `on_upstream_publish` hooks. * * Designed to run under systemd / launchd; blocks indefinitely * until killed. */ import { startHookDispatcher } from '../../services/build-bus/hook-dispatcher'; import { startReceiverServer } from '../../services/build-bus/receiver-server'; import type { CommandResult } from '../types'; /** * Resolve the receiver's shared HMAC secret. The `--secret` flag wins; * otherwise `CELILO_BUS_SECRET` from the environment — which is what * the supervisor unit (celilo-build-bus-receiver.service / the launchd * plist) sets, and what the doctor's build-bus-publishing remediation * has always told operators to export. Returns null when neither has a * usable value; the caller turns that into the flag-required error. */ export function resolveServeSecret( flags: Record, env: Record = process.env, ): string | null { if (typeof flags.secret === 'string' && flags.secret) return flags.secret; const fromEnv = env.CELILO_BUS_SECRET; if (typeof fromEnv === 'string' && fromEnv) return fromEnv; return null; } export async function handleSubscribersServe( _args: string[], flags: Record, ): Promise { const port = typeof flags.port === 'string' ? Number.parseInt(flags.port, 10) : 8123; if (!Number.isFinite(port) || port <= 0 || port > 65535) { return { success: false, error: `Invalid --port value: ${flags.port}. Must be 1–65535.`, }; } const secret = resolveServeSecret(flags); if (!secret) { return { success: false, error: '--secret is required (or export CELILO_BUS_SECRET).', }; } // When --dispatch is set (default), this process also runs the // hook dispatcher on the same bus. Operators can pass --no-dispatch // to run the receiver alone (e.g. when the dispatcher already runs // in another process). const wantsDispatch = flags['no-dispatch'] !== true; const dispatcher = wantsDispatch ? await startHookDispatcher() : null; const server = startReceiverServer({ port, secret, onEvent: dispatcher ? async (envelope) => { // handleEvent returns UpstreamHookResult[]; the receiver's // onEvent contract is void | Promise. Discard the // array — the dispatcher logs its own outcomes. await dispatcher.handleEvent(envelope.event); } : undefined, }); console.log(`✓ build-bus receiver listening on ${server.url}`); console.log(` dispatch: ${wantsDispatch ? 'enabled' : 'disabled'}`); console.log(` events emit to local bus as "build-bus.publish"`); console.log('Ctrl-C to stop.'); // Graceful shutdown. const stop = async (signal: string) => { console.log(`\nReceived ${signal}; shutting down…`); await server.stop(); if (dispatcher) await dispatcher.stop(); process.exit(0); }; process.on('SIGINT', () => void stop('SIGINT')); process.on('SIGTERM', () => void stop('SIGTERM')); // Block indefinitely. The signal handlers exit the process. await new Promise(() => {}); // Unreachable, but TypeScript wants a return. return { success: true, message: '' }; }