129 lines
5.4 KiB
TypeScript
129 lines
5.4 KiB
TypeScript
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<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(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 });
|