feat(backend): wire @bytelyst/webhook-dispatch to domain event bus

- New lib/webhook-subscriber.ts: bridges event bus to webhook dispatch.
  Registers listeners on all 5 domain events (note.created, updated,
  deleted, task.created, workspace.created). Dispatches to registered
  targets with HMAC-SHA256 signing, retry, and delivery log.
- server.ts: init webhook subscriber on startup, stop on close.
- Adds @bytelyst/webhook-dispatch dependency.
This commit is contained in:
saravanakumardb1 2026-04-13 10:29:43 -07:00
parent 7c4a09f9e9
commit 61de6ce94a
3 changed files with 94 additions and 1 deletions

View File

@ -30,6 +30,7 @@
"@bytelyst/llm": "*",
"@bytelyst/palace": "*",
"@bytelyst/logger": "*",
"@bytelyst/webhook-dispatch": "*",
"fastify": "5.7.4",
"jose": "^6.0.8",
"zod": "^3.24.2"

View File

@ -0,0 +1,90 @@
/**
* Webhook subscriber bridges the domain event bus to @bytelyst/webhook-dispatch.
*
* On startup, call `initWebhookSubscriber(app)` to register listeners on
* all domain events. When an event fires, it dispatches to all enabled
* webhook targets for the product.
*/
import { dispatchToTargets, type WebhookTarget, type DeliveryResult, type DispatchOptions } from '@bytelyst/webhook-dispatch';
import { getEventBus, type NoteLettEventMap } from './event-bus.js';
import { PRODUCT_ID } from './product-config.js';
// ── Webhook target storage (in-memory for now, swap for Cosmos later) ──
const targets: WebhookTarget[] = [];
export function registerWebhookTarget(target: WebhookTarget): void {
const existing = targets.findIndex(t => t.id === target.id);
if (existing >= 0) {
targets[existing] = target;
} else {
targets.push(target);
}
}
export function removeWebhookTarget(id: string): boolean {
const idx = targets.findIndex(t => t.id === id);
if (idx < 0) return false;
targets.splice(idx, 1);
return true;
}
export function listWebhookTargets(): WebhookTarget[] {
return [...targets];
}
// ── Dispatch options ─────────────────────────────────────────
let deliveryLog: DeliveryResult[] = [];
const dispatchOptions: DispatchOptions = {
maxRetries: 3,
timeoutMs: 5_000,
onDelivery: (result) => {
deliveryLog.push(result);
// Keep last 1000 deliveries
if (deliveryLog.length > 1_000) {
deliveryLog = deliveryLog.slice(-500);
}
},
};
export function getRecentDeliveries(limit = 50): DeliveryResult[] {
return deliveryLog.slice(-limit);
}
// ── Event → Webhook bridge ───────────────────────────────────
async function dispatchEvent<K extends keyof NoteLettEventMap>(
event: K,
payload: NoteLettEventMap[K],
): Promise<void> {
if (targets.length === 0) return;
await dispatchToTargets(
targets,
event,
payload as unknown as Record<string, unknown>,
PRODUCT_ID,
dispatchOptions,
).catch(() => {});
}
const unsubscribers: (() => void)[] = [];
export function initWebhookSubscriber(): void {
const bus = getEventBus();
unsubscribers.push(
bus.on('note.created', (payload) => dispatchEvent('note.created', payload)),
bus.on('note.updated', (payload) => dispatchEvent('note.updated', payload)),
bus.on('note.deleted', (payload) => dispatchEvent('note.deleted', payload)),
bus.on('task.created', (payload) => dispatchEvent('task.created', payload)),
bus.on('workspace.created', (payload) => dispatchEvent('workspace.created', payload)),
);
}
export function stopWebhookSubscriber(): void {
for (const unsub of unsubscribers) unsub();
unsubscribers.length = 0;
}

View File

@ -16,6 +16,7 @@ import { noteCollaboratorRoutes } from './modules/note-collaborators/routes.js';
import { palaceRoutes } from './modules/palace/routes.js';
import { initCosmosIfNeeded } from './lib/cosmos-init.js';
import { initEncryption } from './lib/field-encrypt.js';
import { initWebhookSubscriber, stopWebhookSubscriber } from './lib/webhook-subscriber.js';
import { initDatastore } from './lib/datastore.js';
import { config } from './lib/config.js';
import { getAllFlags } from './lib/feature-flags.js';
@ -74,7 +75,8 @@ await registerApiPlugin(palaceRoutes);
// ── Start scheduler loop (F25) ────────────────────────────────────
startSchedulerLoop();
app.addHook('onClose', async () => { stopSchedulerLoop(); });
initWebhookSubscriber();
app.addHook('onClose', async () => { stopSchedulerLoop(); stopWebhookSubscriber(); });
// ── Public read-only share (no auth) ───────────────────────────────
app.get('/api/public/note-shares/:token', async (req, reply) => {