import { createServiceApp, registerOptionalJwtContext, startService } from '@bytelyst/fastify-core'; import { createLogger } from '@bytelyst/logger'; 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 { DISPLAY_NAME, PRODUCT_ID, productConfig } from './lib/product-config.js'; import { diagnosticsRoutes } from './lib/diagnostics-routes.js'; import { assertRateLimit, rateLimitKey } from './lib/rate-limit.js'; import { createOutboundRequestId, 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); const startupLogger = createLogger({ service: config.SERVICE_NAME, isDev: config.NODE_ENV !== 'production', }); await initCosmosIfNeeded(startupLogger); 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[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(60_000, app.log); 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 }; assertRateLimit( rateLimitKey('public-share', req.ip, token), { label: 'public note share reads', max: 120, windowMs: 60_000 }, ); 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' }; } reply.header('Cache-Control', 'no-store'); reply.header('X-Robots-Tag', 'noindex, nofollow'); return { product: DISPLAY_NAME, noteId: note.id, workspaceId: share.workspaceId, title: note.title, body: note.body, updatedAt: note.updatedAt, expiresAt: share.expiresAt ?? null, }; }); // ── Bootstrap (no auth) ────────────────────────────────────────── app.get('/api/bootstrap', async () => ({ productId: productConfig.productId, displayName: productConfig.displayName, backendPort: config.PORT, })); // ── Diagnostics routes (dev/test open, production admin/owner gated) ─────── await diagnosticsRoutes(app); await initEncryption(PRODUCT_ID, app.log, createOutboundRequestId()); await startService(app, { port: config.PORT, host: config.HOST });