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:
parent
7c4a09f9e9
commit
61de6ce94a
@ -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"
|
||||
|
||||
90
backend/src/lib/webhook-subscriber.ts
Normal file
90
backend/src/lib/webhook-subscriber.ts
Normal 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;
|
||||
}
|
||||
@ -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) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user