// Independent reference backend: only published contract and receiver helper imports. // Node 24; single-host SQLite volume. Put behind an owned HTTPS reverse proxy. import { DatabaseSync } from 'node:sqlite'; import { createServer } from 'node:http'; import { readFileSync } from 'node:fs'; import { createHash } from 'node:crypto'; import { pathToFileURL } from 'node:url'; import Ajv from 'ajv/dist/2020.js'; import addFormats from 'ajv-formats'; import { readVerifiedEvent } from './external-receiver.mjs'; export function createReceiver({ database, tenantId, secrets, afterCommit }) { if (!database || !tenantId || !secrets?.length) throw Error('Receiver configuration required'); const contract = JSON.parse( readFileSync(new URL('./openapi.json', import.meta.url)), ); const ajv = new Ajv({ strict: false }); addFormats(ajv); const validators = Object.values(contract.webhooks).map((hook) => ajv.compile({ components: contract.components, ...hook.post.requestBody.content['application/json'].schema, }), ); const db = new DatabaseSync(database); db.exec(`PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; PRAGMA busy_timeout=5000; CREATE TABLE IF NOT EXISTS inbox(tenant TEXT, id TEXT, hash TEXT NOT NULL, received_at TEXT NOT NULL, PRIMARY KEY(tenant,id)); CREATE TABLE IF NOT EXISTS resources(tenant TEXT, kind TEXT, id TEXT, version INTEGER NOT NULL, payload TEXT NOT NULL, PRIMARY KEY(tenant,kind,id));`); const server = createServer(async (req, res) => { const reply = (status, body) => { res.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', }); res.end(JSON.stringify(body)); }; if (req.method !== 'POST' || req.url !== '/hooks/wingsflow') return reply(404, { error: 'not_found' }); if ( (req.headers['content-type'] || '').split(';')[0].trim() !== 'application/json' ) return reply(415, { error: 'json_required' }); try { const chunks = []; let size = 0; for await (const chunk of req) { size += chunk.length; if (size > 65536) return reply(413, { error: 'body_too_large' }); chunks.push(chunk); } const raw = Buffer.concat(chunks), headers = new Headers(); for (const [key, value] of Object.entries(req.headers)) if (typeof value === 'string') headers.set(key, value); let event; try { event = readVerifiedEvent(raw, headers, secrets, tenantId); } catch { return reply(401, { error: 'invalid_webhook' }); } if (!validators.some((validate) => validate(event))) return reply(400, { error: 'invalid_schema' }); if (event.type === 'endpoint.verification') { if (Date.parse(event.data.expires_at) <= Date.now()) return reply(400, { error: 'expired_challenge' }); return reply(200, { challenge: event.data.challenge }); } const hash = createHash('sha256').update(raw).digest('hex'); db.exec('BEGIN IMMEDIATE'); try { const existing = db .prepare('SELECT hash FROM inbox WHERE tenant=? AND id=?') .get(tenantId, event.id); if (existing && existing.hash !== hash) { db.exec('ROLLBACK'); return reply(409, { error: 'event_identity_conflict' }); } if (!existing) { db.prepare('INSERT INTO inbox VALUES(?,?,?,?)').run( tenantId, event.id, hash, new Date().toISOString(), ); db.prepare(`INSERT INTO resources VALUES(?,?,?,?,?) ON CONFLICT(tenant,kind,id) DO UPDATE SET version=excluded.version,payload=excluded.payload WHERE excluded.version>resources.version`).run( tenantId, event.resource.type, event.resource.id, event.resource.version, JSON.stringify(event), ); } db.exec('COMMIT'); } catch (error) { db.exec('ROLLBACK'); throw error; } // Fault injection is constructor-only, never selected by a request or environment flag. await afterCommit?.(); reply(200, { accepted: true }); } catch { if (!res.destroyed) reply(503, { error: 'receiver_unavailable' }); } }); server.requestTimeout = 15000; server.headersTimeout = 10000; return { server, db, close: () => new Promise((resolve, reject) => server.close((error) => { db.close(); if (error) reject(error); else resolve(); }), ), }; } if ( process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href ) { const receiver = createReceiver({ database: process.env.RECEIVER_DATABASE, tenantId: process.env.RECEIVER_TENANT_ID, secrets: JSON.parse(process.env.RECEIVER_SECRETS_JSON || '[]'), }); receiver.server.listen(Number(process.env.PORT || 8788), '127.0.0.1'); }