learning_ai_notes/backend/src/server.ts
saravanakumardb1 61de6ce94a 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.
2026-04-13 10:29:43 -07:00

128 lines
5.4 KiB
TypeScript

import { createServiceApp, registerOptionalJwtContext, startService } from '@bytelyst/fastify-core';
import { jwtVerify } from 'jose';
import { noteAgentActionRoutes } from './modules/note-agent-actions/routes.js';
import { ecosystemPhase1Routes } from './modules/ecosystem-phase1/routes.js';
import { ecosystemPhase3Routes } from './modules/ecosystem-phase3/routes.js';
import { noteArtifactRoutes } from './modules/note-artifacts/routes.js';
import { noteRoutes } from './modules/notes/routes.js';
import { noteRelationshipRoutes } from './modules/note-relationships/routes.js';
import { noteTaskRoutes } from './modules/note-tasks/routes.js';
import { savedViewRoutes } from './modules/saved-views/routes.js';
import { workspaceRoutes } from './modules/workspaces/routes.js';
import { notePromptRoutes } from './modules/note-prompts/routes.js';
import { promptSchedulerRoutes, startSchedulerLoop, stopSchedulerLoop } from './modules/note-prompts/scheduler.js';
import { intakeRoutes } from './modules/intake/routes.js';
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';
import { getBufferedEvents, flushEvents } from './lib/telemetry.js';
import { DISPLAY_NAME, PRODUCT_ID, productConfig } from './lib/product-config.js';
import type { JwtPayload } from './lib/request-context.js';
import { findShareByToken } from './modules/note-shares/repository.js';
import * as noteRepo from './modules/notes/repository.js';
const jwtSecret = new TextEncoder().encode(config.JWT_SECRET);
await initCosmosIfNeeded();
initDatastore();
const app = await createServiceApp({
name: config.SERVICE_NAME,
version: '0.1.0',
description: `${DISPLAY_NAME} product-specific backend — notes and workspaces`,
corsOrigin: config.CORS_ORIGIN,
swagger: {
title: `${DISPLAY_NAME} Backend`,
description: 'Notes and workspaces API',
port: config.PORT,
},
metrics: true,
readiness: true,
});
await registerOptionalJwtContext(app, {
verifyToken: async (token: string) => {
const { payload } = await jwtVerify(token, jwtSecret, { issuer: 'bytelyst-platform' });
return payload as unknown as JwtPayload;
},
});
type RegisterablePlugin = Parameters<typeof app.register>[0];
async function registerApiPlugin(plugin: unknown) {
await app.register(plugin as RegisterablePlugin, { prefix: '/api' });
}
await registerApiPlugin(noteAgentActionRoutes);
await registerApiPlugin(ecosystemPhase1Routes);
await registerApiPlugin(ecosystemPhase3Routes);
await registerApiPlugin(noteArtifactRoutes);
await registerApiPlugin(noteRoutes);
await registerApiPlugin(noteRelationshipRoutes);
await registerApiPlugin(noteTaskRoutes);
await registerApiPlugin(savedViewRoutes);
await registerApiPlugin(workspaceRoutes);
await registerApiPlugin(notePromptRoutes);
await registerApiPlugin(promptSchedulerRoutes);
await registerApiPlugin(intakeRoutes);
await registerApiPlugin(noteCollaboratorRoutes);
await registerApiPlugin(palaceRoutes);
// ── Start scheduler loop (F25) ────────────────────────────────────
startSchedulerLoop();
initWebhookSubscriber();
app.addHook('onClose', async () => { stopSchedulerLoop(); stopWebhookSubscriber(); });
// ── Public read-only share (no auth) ───────────────────────────────
app.get('/api/public/note-shares/:token', async (req, reply) => {
const { token } = req.params as { token: string };
const share = await findShareByToken(token, PRODUCT_ID);
if (!share) {
reply.code(404);
return { error: 'Share not found or expired' };
}
const note = await noteRepo.getNote(share.noteId, share.workspaceId);
if (!note || note.userId !== share.userId || note.productId !== PRODUCT_ID) {
reply.code(404);
return { error: 'Note not available' };
}
return {
product: DISPLAY_NAME,
noteId: note.id,
workspaceId: share.workspaceId,
title: note.title,
body: note.body,
updatedAt: note.updatedAt,
};
});
// ── Bootstrap (no auth) ──────────────────────────────────────────
app.get('/api/bootstrap', async () => ({
productId: productConfig.productId,
displayName: productConfig.displayName,
backendPort: config.PORT,
}));
// ── Diagnostics routes (no auth) ────────────────────────────────
app.get('/api/diagnostics/flags', async () => getAllFlags());
app.get('/api/diagnostics/telemetry', async () => ({ events: getBufferedEvents() }));
app.post('/api/diagnostics/telemetry/flush', async () => ({ flushed: flushEvents().length }));
app.get('/api/diagnostics/config', async () => ({
productId: PRODUCT_ID,
serviceName: config.SERVICE_NAME,
port: config.PORT,
nodeEnv: config.NODE_ENV,
dbProvider: config.DB_PROVIDER,
telemetryEnabled: config.TELEMETRY_ENABLED,
featureFlagsEnabled: config.FEATURE_FLAGS_ENABLED,
}));
await initEncryption(PRODUCT_ID, app.log);
await startService(app, { port: config.PORT, host: config.HOST });