ocean-brain 0.13.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +5 -3
  2. package/dist/index.js +12 -4
  3. package/package.json +10 -8
  4. package/server/client/dist/assets/{Calendar-CaeK7gTz.js → Calendar-DExFS7xq.js} +1 -1
  5. package/server/client/dist/assets/{Search-DAYHxAml.js → Search-B_BRjOyG.js} +1 -1
  6. package/server/client/dist/assets/{Tag-CG8b0l1j.js → Tag-2hcR9dd4.js} +1 -1
  7. package/server/client/dist/assets/{TagNotes-COyaVsdn.js → TagNotes-BE20Ji5s.js} +1 -1
  8. package/server/client/dist/assets/{Views-BPxlw3vk.js → Views-CQXgnbUQ.js} +1 -1
  9. package/server/client/dist/assets/{app-BezcjTfd.js → app-4M3jUhmd.js} +2 -2
  10. package/server/client/dist/assets/{index-CzZFedXj.js → index-CaT-yZJw.js} +1 -1
  11. package/server/client/dist/assets/{manage-image-detail-9hRTiAsR.js → manage-image-detail-Bu9yymBn.js} +1 -1
  12. package/server/client/dist/assets/mcp-D8footel.js +5 -0
  13. package/server/client/dist/assets/{placeholder-CZoqs4S3.js → placeholder-BZ-p6ahW.js} +1 -1
  14. package/server/client/dist/index.html +1 -1
  15. package/server/dist/app.js +91 -8
  16. package/server/dist/features/auth/http/api.js +9 -8
  17. package/server/dist/features/auth/http/pages.js +27 -28
  18. package/server/dist/features/auth/service.js +7 -27
  19. package/server/dist/features/cache/graphql/cache.type-defs.js +1 -1
  20. package/server/dist/features/image/http/upload.js +2 -2
  21. package/server/dist/features/mcp-admin/http/handlers.js +9 -9
  22. package/server/dist/features/note/http/mcp.js +17 -17
  23. package/server/dist/features/search/http/handlers.js +10 -10
  24. package/server/dist/features/search/search-manager.js +34 -6
  25. package/server/dist/features/tag/http/mcp.js +2 -2
  26. package/server/dist/modules/auth-guard.js +36 -53
  27. package/server/dist/modules/blocknote.js +16 -9
  28. package/server/dist/modules/error-handler.js +17 -20
  29. package/server/dist/modules/mcp-auth.js +50 -56
  30. package/server/dist/modules/rate-limit.js +23 -23
  31. package/server/dist/modules/server-events-handler.js +15 -13
  32. package/server/dist/modules/session-store.js +6 -8
  33. package/server/dist/paths.js +2 -1
  34. package/server/dist/routes/api.js +105 -71
  35. package/server/dist/routes/auth-pages.js +21 -11
  36. package/server/dist/routes/client.js +60 -72
  37. package/server/dist/routes/graphql.js +31 -26
  38. package/server/dist/routes/mcp.js +25 -35
  39. package/server/dist/server.js +14 -16
  40. package/server/client/dist/assets/mcp-ugdCKa6H.js +0 -4
  41. package/server/dist/modules/logger.js +0 -53
  42. package/server/dist/modules/use-async.js +0 -10
@@ -1,29 +1,112 @@
1
- import express from "express";
1
+ import fastifyCookie, { Signer } from "@fastify/cookie";
2
+ import fastifyCsrfProtection from "@fastify/csrf-protection";
3
+ import fastifyFormbody from "@fastify/formbody";
4
+ import fastifyRateLimit from "@fastify/rate-limit";
5
+ import fastifySession from "@fastify/session";
6
+ import Fastify from "fastify";
2
7
  import { createMcpAdminService } from "./features/mcp-admin/service.js";
3
8
  import { purgeExpiredNoteSnapshots } from "./features/note/services/snapshot.js";
4
9
  import { purgeExpiredTrashedNotes } from "./features/note/services/trash.js";
5
- import { createSessionMiddleware } from "./modules/auth-guard.js";
6
- import { createErrorHandler } from "./modules/error-handler.js";
7
- import logger from "./modules/logger.js";
10
+ import { closeDefaultSemanticSearchManager } from "./features/search/search-manager.js";
11
+ import { createAppError, createErrorHandler } from "./modules/error-handler.js";
12
+ import { AUTH_SESSION_IDLE_TIMEOUT_MS, createSessionStore } from "./modules/session-store.js";
8
13
  import { createApiRouter, createAuthPagesRouter, createClientRouter, createGraphqlRouter } from "./routes/index.js";
14
+ const MAX_REQUEST_BODY_BYTES = 50 * 1024 * 1024;
15
+ const isHttpLoggerEnabled = (loggerOption) => loggerOption ?? process.env.OCEAN_BRAIN_HTTP_LOG?.toLowerCase() !== "false";
16
+ const createLoggerOptions = () => ({
17
+ level: "info",
18
+ serializers: {
19
+ req(request) {
20
+ return {
21
+ method: request.method,
22
+ url: request.url,
23
+ remoteAddress: request.ip,
24
+ userAgent: request.headers["user-agent"]
25
+ };
26
+ },
27
+ res(reply) {
28
+ return { statusCode: reply.statusCode };
29
+ }
30
+ }
31
+ });
32
+ const createFastifyApplication = (options = {}) => Fastify({
33
+ bodyLimit: MAX_REQUEST_BODY_BYTES,
34
+ routerOptions: { ignoreTrailingSlash: true },
35
+ logger: isHttpLoggerEnabled(options.logger) ? createLoggerOptions() : false
36
+ });
37
+ const registerRequestInfrastructure = (app, authConfig) => {
38
+ app.addHook("onRequest", (request, _reply, done) => {
39
+ const contentEncoding = request.headers["content-encoding"];
40
+ if (contentEncoding && contentEncoding.toLowerCase() !== "identity") {
41
+ done(createAppError(415, "UNSUPPORTED_CONTENT_ENCODING", "Compressed request bodies are not supported."));
42
+ return;
43
+ }
44
+ done();
45
+ });
46
+ app.register(fastifyCookie);
47
+ app.register(fastifyFormbody);
48
+ app.register(fastifyRateLimit, {
49
+ global: false,
50
+ enableDraftSpec: true
51
+ });
52
+ if (authConfig.mode !== "password") {
53
+ return;
54
+ }
55
+ const sessionStore = createSessionStore();
56
+ app.addHook("onClose", (_instance, done) => {
57
+ if ("stopPruning" in sessionStore && typeof sessionStore.stopPruning === "function") {
58
+ sessionStore.stopPruning();
59
+ }
60
+ done();
61
+ });
62
+ app.register(fastifySession, {
63
+ // Preserve the configured secret bytes and support legacy secrets shorter than 32 characters.
64
+ secret: new Signer(authConfig.sessionSecret),
65
+ cookieName: authConfig.cookieName,
66
+ cookiePrefix: "s:",
67
+ store: sessionStore,
68
+ saveUninitialized: false,
69
+ rolling: true,
70
+ cookie: {
71
+ maxAge: AUTH_SESSION_IDLE_TIMEOUT_MS,
72
+ httpOnly: true,
73
+ sameSite: "lax",
74
+ secure: process.env.NODE_ENV === "production",
75
+ path: "/"
76
+ }
77
+ });
78
+ app.register(fastifyCsrfProtection, {
79
+ sessionPlugin: "@fastify/session",
80
+ logLevel: "debug"
81
+ });
82
+ };
9
83
  const createApp = (authConfig, options = {}) => {
10
84
  const mcpAdminService = createMcpAdminService();
11
85
  return createAppWithMcpAuth(authConfig, mcpAdminService, options);
12
86
  };
13
87
  const createAppWithMcpAuth = (authConfig, mcpAdminService, options = {}) => {
14
- const app = options.application ?? express();
15
- app.locals.authConfig = authConfig;
16
- void Promise.all([purgeExpiredNoteSnapshots(), purgeExpiredTrashedNotes()]).catch((error) => {
88
+ const app = options.application ?? createFastifyApplication(options);
89
+ const startupCleanup = Promise.all([purgeExpiredNoteSnapshots(), purgeExpiredTrashedNotes()]).catch((error) => {
17
90
  const message = error instanceof Error ? error.message : "Unknown recovery cleanup error";
18
91
  process.stderr.write(`[recovery] Startup cleanup failed: ${message}
19
92
  `);
20
93
  });
21
- app.use(logger).use(createSessionMiddleware(authConfig)).use(express.urlencoded({ extended: false, inflate: false })).use(express.json({ limit: "50mb", inflate: false })).use("/api", createApiRouter(authConfig, mcpAdminService)).use(createAuthPagesRouter(authConfig)).use("/graphql", createGraphqlRouter(authConfig, mcpAdminService)).use(createClientRouter(authConfig, options.clientContentMiddleware)).use(createErrorHandler());
94
+ app.addHook("onClose", async () => {
95
+ await startupCleanup;
96
+ await closeDefaultSemanticSearchManager();
97
+ });
98
+ registerRequestInfrastructure(app, authConfig);
99
+ app.setErrorHandler(createErrorHandler(authConfig));
100
+ app.register(createApiRouter(authConfig, mcpAdminService), { prefix: "/api" });
101
+ app.register(createAuthPagesRouter(authConfig));
102
+ app.register(createGraphqlRouter(authConfig, mcpAdminService));
103
+ app.register(createClientRouter(authConfig, options.clientContentHandler));
22
104
  return app;
23
105
  };
24
106
  var app_default = createApp;
25
107
  export {
26
108
  createApp,
27
109
  createAppWithMcpAuth,
110
+ createFastifyApplication,
28
111
  app_default as default
29
112
  };
@@ -10,7 +10,7 @@ import {
10
10
  setSessionStatusHeaders
11
11
  } from "../service.js";
12
12
  const createLoginHandler = (authConfig) => {
13
- return async (req, res) => {
13
+ return async (req, reply) => {
14
14
  const expectedPassword = assertPasswordLoginAvailable(authConfig);
15
15
  const password = typeof req.body?.password === "string" ? req.body.password : "";
16
16
  if (!password || !compareSharedSecret(expectedPassword, password)) {
@@ -18,22 +18,23 @@ const createLoginHandler = (authConfig) => {
18
18
  }
19
19
  await regenerateSession(req);
20
20
  req.session.authenticated = true;
21
- refreshCsrfToken(req);
22
- res.status(200).json(buildSessionResponse(authConfig, req)).end();
21
+ refreshCsrfToken(authConfig, reply);
22
+ return reply.status(200).send(buildSessionResponse(authConfig, req));
23
23
  };
24
24
  };
25
25
  const createLogoutHandler = (authConfig) => {
26
- return async (req, res) => {
26
+ return async (req, reply) => {
27
27
  if (authConfig.mode === "password") {
28
28
  await destroySession(req);
29
29
  }
30
- res.status(200).json(buildAuthSessionResponse(authConfig, false)).end();
30
+ return reply.status(200).send(buildAuthSessionResponse(authConfig, false));
31
31
  };
32
32
  };
33
33
  const createSessionStatusHandler = (authConfig) => {
34
- return async (req, res) => {
35
- setSessionStatusHeaders(res);
36
- res.status(200).json(buildSessionResponse(authConfig, req)).end();
34
+ return async (req, reply) => {
35
+ setSessionStatusHeaders(reply);
36
+ refreshCsrfToken(authConfig, reply);
37
+ return reply.status(200).send(buildSessionResponse(authConfig, req));
37
38
  };
38
39
  };
39
40
  export {
@@ -1,3 +1,4 @@
1
+ import { issueCsrfToken } from "../../../modules/auth-guard.js";
1
2
  import {
2
3
  compareSharedSecret,
3
4
  destroySession,
@@ -7,65 +8,63 @@ import {
7
8
  sanitizeRedirectPath
8
9
  } from "../service.js";
9
10
  import { renderLoginPage } from "./login-page.js";
10
- const setLoginPageHeaders = (res) => {
11
- res.set("Cache-Control", "no-store");
11
+ const setLoginPageHeaders = (reply) => {
12
+ reply.header("Cache-Control", "no-store");
12
13
  };
13
14
  const createLoginPageHandler = (authConfig) => {
14
- return async (req, res) => {
15
+ return async (req, reply) => {
15
16
  if (authConfig.mode !== "password" || req.session?.authenticated) {
16
- setLoginPageHeaders(res);
17
- res.redirect(303, sanitizeRedirectPath(req.query.next));
18
- return;
17
+ setLoginPageHeaders(reply);
18
+ return reply.redirect(sanitizeRedirectPath(req.query.next), 303);
19
19
  }
20
20
  const nextPath = sanitizeRedirectPath(req.query.next);
21
- setLoginPageHeaders(res);
22
- res.status(200).type("html").send(
21
+ const csrfToken = issueCsrfToken(authConfig, reply);
22
+ setLoginPageHeaders(reply);
23
+ return reply.status(200).type("text/html; charset=utf-8").send(
23
24
  renderLoginPage({
24
25
  nextPath,
25
- csrfToken: res.locals._csrf,
26
+ csrfToken,
26
27
  sessionGeneration: getSessionGeneration()
27
28
  })
28
- ).end();
29
+ );
29
30
  };
30
31
  };
31
32
  const createLoginPageSubmitHandler = (authConfig) => {
32
- return async (req, res) => {
33
+ return async (req, reply) => {
33
34
  const nextPath = sanitizeRedirectPath(req.body?.next);
34
35
  if (authConfig.mode !== "password" || !authConfig.password) {
35
- setLoginPageHeaders(res);
36
- res.redirect(303, nextPath);
37
- return;
36
+ setLoginPageHeaders(reply);
37
+ return reply.redirect(nextPath, 303);
38
38
  }
39
39
  const password = typeof req.body?.password === "string" ? req.body.password : "";
40
40
  if (!password || !compareSharedSecret(authConfig.password, password)) {
41
- setLoginPageHeaders(res);
42
- res.status(401).type("html").send(
41
+ const csrfToken = issueCsrfToken(authConfig, reply);
42
+ setLoginPageHeaders(reply);
43
+ return reply.status(401).type("text/html; charset=utf-8").send(
43
44
  renderLoginPage({
44
45
  nextPath,
45
46
  errorMessage: "Invalid password",
46
- csrfToken: res.locals._csrf,
47
+ csrfToken,
47
48
  sessionGeneration: getSessionGeneration()
48
49
  })
49
- ).end();
50
- return;
50
+ );
51
51
  }
52
52
  await regenerateSession(req);
53
53
  req.session.authenticated = true;
54
- refreshCsrfToken(req);
55
- setLoginPageHeaders(res);
56
- res.redirect(303, nextPath);
54
+ refreshCsrfToken(authConfig, reply);
55
+ setLoginPageHeaders(reply);
56
+ return reply.redirect(nextPath, 303);
57
57
  };
58
58
  };
59
59
  const createLogoutPageHandler = (authConfig) => {
60
- return async (req, res) => {
60
+ return async (req, reply) => {
61
61
  if (authConfig.mode === "password") {
62
62
  await destroySession(req);
63
- setLoginPageHeaders(res);
64
- res.redirect(303, "/login");
65
- return;
63
+ setLoginPageHeaders(reply);
64
+ return reply.redirect("/login", 303);
66
65
  }
67
- setLoginPageHeaders(res);
68
- res.redirect(303, "/");
66
+ setLoginPageHeaders(reply);
67
+ return reply.redirect("/", 303);
69
68
  };
70
69
  };
71
70
  export {
@@ -1,6 +1,7 @@
1
1
  import { buildAuthSessionResponse } from "@baejino/auth";
2
2
  import { compareSharedSecret as compareCommonSharedSecret } from "@baejino/auth/crypto";
3
3
  import crypto from "crypto";
4
+ import { issueCsrfToken } from "../../modules/auth-guard.js";
4
5
  import { sanitizeRedirectPath } from "../../modules/auth-redirect.js";
5
6
  import { createAppError } from "../../modules/error-handler.js";
6
7
  const AUTH_SESSION_GENERATION_HEADER = "X-Ocean-Brain-Session-Generation";
@@ -18,44 +19,23 @@ const comparePassword = async (password, storedHash) => {
18
19
  };
19
20
  const compareSharedSecret = compareCommonSharedSecret;
20
21
  const buildSessionResponse = (authConfig, req) => buildAuthSessionResponse(authConfig, Boolean(req.session?.authenticated));
21
- const setSessionStatusHeaders = (res) => {
22
- res.set(AUTH_SESSION_GENERATION_HEADER, getSessionGeneration());
23
- res.set("Cache-Control", "no-store");
24
- };
25
- const refreshCsrfToken = (req) => {
26
- const requestWithCsrf = req;
27
- requestWithCsrf.csrfToken?.();
22
+ const setSessionStatusHeaders = (reply) => {
23
+ reply.header(AUTH_SESSION_GENERATION_HEADER, getSessionGeneration());
24
+ reply.header("Cache-Control", "no-store");
28
25
  };
26
+ const refreshCsrfToken = (authConfig, reply) => issueCsrfToken(authConfig, reply);
29
27
  const assertPasswordLoginAvailable = (authConfig) => {
30
28
  if (authConfig.mode !== "password") {
31
29
  throw createAppError(409, "AUTH_OPEN_MODE", "Login is unavailable while auth mode is open.");
32
30
  }
33
31
  return authConfig.password;
34
32
  };
35
- const regenerateSession = async (req) => {
36
- await new Promise((resolve, reject) => {
37
- req.session.regenerate((error) => {
38
- if (error) {
39
- reject(error);
40
- return;
41
- }
42
- resolve();
43
- });
44
- });
45
- };
33
+ const regenerateSession = async (req) => req.session.regenerate();
46
34
  const destroySession = async (req) => {
47
35
  if (!req.session) {
48
36
  return;
49
37
  }
50
- await new Promise((resolve, reject) => {
51
- req.session.destroy((error) => {
52
- if (error) {
53
- reject(error);
54
- return;
55
- }
56
- resolve();
57
- });
58
- });
38
+ await req.session.destroy();
59
39
  };
60
40
  export {
61
41
  AUTH_SESSION_GENERATION_HEADER,
@@ -9,7 +9,7 @@ const cacheType = gql`
9
9
  const cacheQuery = gql`
10
10
  type Query {
11
11
  allCaches: [Cache!]!
12
- cache(key: String!): Cache!
12
+ cache(key: String!): Cache
13
13
  }
14
14
  `;
15
15
  const cacheMutation = gql`
@@ -25,7 +25,7 @@ const createUploadValidationError = (error) => {
25
25
  return createAppError(400, "INVALID_IMAGE_UPLOAD", "Uploaded image content is invalid.");
26
26
  };
27
27
  const createUploadImageHandler = (persistImage = persistUploadedImage) => {
28
- return async (req, res) => {
28
+ return async (req, reply) => {
29
29
  const { image } = req.body ?? {};
30
30
  if (typeof image !== "string") {
31
31
  throw createAppError(400, "INVALID_IMAGE_UPLOAD", "No image uploaded");
@@ -50,7 +50,7 @@ const createUploadImageHandler = (persistImage = persistUploadedImage) => {
50
50
  buffer: validatedImage.buffer,
51
51
  extension: validatedImage.extension
52
52
  });
53
- res.status(200).json(createUploadResponse(uploadedImage)).end();
53
+ return reply.status(200).send(createUploadResponse(uploadedImage));
54
54
  };
55
55
  };
56
56
  export {
@@ -9,36 +9,36 @@ const createMcpAdminStatusResponse = async (service) => {
9
9
  };
10
10
  };
11
11
  const createMcpAdminStatusHandler = (service = createMcpAdminService()) => {
12
- return async (_req, res) => {
12
+ return async (_req, reply) => {
13
13
  const status = await createMcpAdminStatusResponse(service);
14
- res.status(200).json(status).end();
14
+ return reply.status(200).send(status);
15
15
  };
16
16
  };
17
17
  const createMcpAdminSetEnabledHandler = (service = createMcpAdminService()) => {
18
- return async (req, res) => {
18
+ return async (req, reply) => {
19
19
  const enabled = req.body?.enabled;
20
20
  if (typeof enabled !== "boolean") {
21
21
  throw createAppError(400, "INVALID_MCP_ENABLED", "enabled must be a boolean.");
22
22
  }
23
23
  await service.setEnabled(enabled);
24
24
  const status = await createMcpAdminStatusResponse(service);
25
- res.status(200).json(status).end();
25
+ return reply.status(200).send(status);
26
26
  };
27
27
  };
28
28
  const createMcpAdminRotateTokenHandler = (service = createMcpAdminService()) => {
29
- return async (_req, res) => {
29
+ return async (_req, reply) => {
30
30
  const result = await service.rotateToken();
31
- res.status(200).json({
31
+ return reply.status(200).send({
32
32
  token: result.token,
33
33
  message: "Save this token now. It is shown only once."
34
- }).end();
34
+ });
35
35
  };
36
36
  };
37
37
  const createMcpAdminRevokeTokenHandler = (service = createMcpAdminService()) => {
38
- return async (_req, res) => {
38
+ return async (_req, reply) => {
39
39
  await service.revokeActiveToken();
40
40
  const status = await createMcpAdminStatusResponse(service);
41
- res.status(200).json(status).end();
41
+ return reply.status(200).send(status);
42
42
  };
43
43
  };
44
44
  export {
@@ -227,7 +227,7 @@ const resolveSeparator = (value) => {
227
227
  throw createAppError(400, "INVALID_SEPARATOR", 'Separator must be "\\n" or "\\n\\n".');
228
228
  };
229
229
  const createMcpCreateNoteHandler = (createNote = createNoteFromMarkdown, emitEvent = emitServerEvent) => {
230
- return async (req, res) => {
230
+ return async (req, reply) => {
231
231
  const { title, markdown, layout } = req.body ?? {};
232
232
  const resolvedLayout = resolveNoteLayout(layout);
233
233
  if (typeof title !== "string") {
@@ -251,10 +251,10 @@ const createMcpCreateNoteHandler = (createNote = createNoteFromMarkdown, emitEve
251
251
  noteId: note.id,
252
252
  updatedAt: note.updatedAt
253
253
  });
254
- res.status(200).json({
254
+ return reply.status(200).send({
255
255
  created: true,
256
256
  note
257
- }).end();
257
+ });
258
258
  } catch (error) {
259
259
  if (error instanceof InvalidNoteAuthoringInputError) {
260
260
  throw createAppError(400, "INVALID_NOTE_INPUT", error.message);
@@ -270,19 +270,19 @@ const createMcpNoteWriteBaselineHandler = (findNoteBaseline = async (id) => mode
270
270
  updatedAt: true
271
271
  }
272
272
  })) => {
273
- return async (req, res) => {
273
+ return async (req, reply) => {
274
274
  const noteId = resolvePositiveNoteId(req.body?.id);
275
275
  const note = await findNoteBaseline(noteId);
276
276
  if (!note) {
277
277
  throw createAppError(404, "NOTE_NOT_FOUND", "The requested note was not found.");
278
278
  }
279
- res.status(200).json({
279
+ return reply.status(200).send({
280
280
  note: serializeNoteWriteBaseline(note)
281
- }).end();
281
+ });
282
282
  };
283
283
  };
284
284
  const createMcpPatchNoteMarkdownHandler = (patchMarkdown = patchNoteMarkdown, emitEvent = emitServerEvent) => {
285
- return async (req, res) => {
285
+ return async (req, reply) => {
286
286
  const { id, expectedUpdatedAt, baseMarkdownSha256, intent, selector, operation, policy } = req.body ?? {};
287
287
  const noteId = resolvePositiveNoteId(id);
288
288
  const resolvedExpectedUpdatedAt = resolveOptionalString(
@@ -315,11 +315,11 @@ const createMcpPatchNoteMarkdownHandler = (patchMarkdown = patchNoteMarkdown, em
315
315
  updatedAt: result.note.updatedAt
316
316
  });
317
317
  }
318
- res.status(200).json(result).end();
318
+ return reply.status(200).send(result);
319
319
  };
320
320
  };
321
321
  const createMcpAppendNoteMarkdownHandler = (appendMarkdown = appendNoteMarkdown, emitEvent = emitServerEvent) => {
322
- return async (req, res) => {
322
+ return async (req, reply) => {
323
323
  const { id, expectedUpdatedAt, baseMarkdownSha256, intent, insertion, placement, separator, policy } = req.body ?? {};
324
324
  const noteId = resolvePositiveNoteId(id);
325
325
  const resolvedExpectedUpdatedAt = resolveOptionalString(
@@ -356,11 +356,11 @@ const createMcpAppendNoteMarkdownHandler = (appendMarkdown = appendNoteMarkdown,
356
356
  updatedAt: result.note.updatedAt
357
357
  });
358
358
  }
359
- res.status(200).json(result).end();
359
+ return reply.status(200).send(result);
360
360
  };
361
361
  };
362
362
  const createMcpReplaceNoteMarkdownHandler = (replaceMarkdown = replaceNoteMarkdown, emitEvent = emitServerEvent) => {
363
- return async (req, res) => {
363
+ return async (req, reply) => {
364
364
  const { id, expectedUpdatedAt, baseMarkdownSha256, intent, replacement, policy } = req.body ?? {};
365
365
  const noteId = resolvePositiveNoteId(id);
366
366
  const resolvedExpectedUpdatedAt = resolveOptionalString(
@@ -395,11 +395,11 @@ const createMcpReplaceNoteMarkdownHandler = (replaceMarkdown = replaceNoteMarkdo
395
395
  updatedAt: result.note.updatedAt
396
396
  });
397
397
  }
398
- res.status(200).json(result).end();
398
+ return reply.status(200).send(result);
399
399
  };
400
400
  };
401
401
  const createMcpUpdateNoteMetadataHandler = (updateMetadata = updateNoteMetadata, emitEvent = emitServerEvent) => {
402
- return async (req, res) => {
402
+ return async (req, reply) => {
403
403
  const { id, expectedUpdatedAt, title, layout, properties } = req.body ?? {};
404
404
  const noteId = resolvePositiveNoteId(id);
405
405
  const resolvedLayout = resolveNoteLayout(layout);
@@ -431,11 +431,11 @@ const createMcpUpdateNoteMetadataHandler = (updateMetadata = updateNoteMetadata,
431
431
  updatedAt: result.note.updatedAt
432
432
  });
433
433
  }
434
- res.status(200).json(result).end();
434
+ return reply.status(200).send(result);
435
435
  };
436
436
  };
437
437
  const createMcpDeleteNoteHandler = (deleteNote = deleteNoteById, emitEvent = emitServerEvent) => {
438
- return async (req, res) => {
438
+ return async (req, reply) => {
439
439
  const id = Number(req.body?.id);
440
440
  if (!Number.isInteger(id) || id <= 0) {
441
441
  throw createAppError(400, "INVALID_NOTE_ID", "A valid note id is required.");
@@ -449,10 +449,10 @@ const createMcpDeleteNoteHandler = (deleteNote = deleteNoteById, emitEvent = emi
449
449
  source: "mcp",
450
450
  noteId: deletedNote.id
451
451
  });
452
- res.status(200).json({
452
+ return reply.status(200).send({
453
453
  deleted: true,
454
454
  note: deletedNote
455
- }).end();
455
+ });
456
456
  };
457
457
  };
458
458
  export {
@@ -70,33 +70,33 @@ const parseBaseUrl = (value) => {
70
70
  return baseUrl;
71
71
  };
72
72
  const createSearchAdminStatusHandler = (manager = getDefaultSemanticSearchManager()) => {
73
- return async (_req, res) => {
74
- res.status(200).json(await manager.getStatus()).end();
73
+ return async (_req, reply) => {
74
+ return reply.status(200).send(await manager.getStatus());
75
75
  };
76
76
  };
77
77
  const createSearchAdminSaveConfigHandler = (manager = getDefaultSemanticSearchManager()) => {
78
- return async (req, res) => {
78
+ return async (req, reply) => {
79
79
  const status = await manager.saveConfig(parseConfig(req.body), parseApiKeyInput(req.body));
80
- res.status(200).json(status).end();
80
+ return reply.status(200).send(status);
81
81
  };
82
82
  };
83
83
  const createSearchAdminTestConnectionHandler = (manager = getDefaultSemanticSearchManager()) => {
84
- return async (req, res) => {
84
+ return async (req, reply) => {
85
85
  const config = parseConfig({ ...req.body, enabled: true });
86
86
  const result = await manager.testConnection(config, parseApiKeyInput(req.body));
87
- res.status(200).json(result).end();
87
+ return reply.status(200).send(result);
88
88
  };
89
89
  };
90
90
  const createSearchAdminListModelsHandler = (listModels = (baseUrl, apiKeyInput) => getDefaultSemanticSearchManager().listModels(baseUrl, apiKeyInput)) => {
91
- return async (req, res) => {
91
+ return async (req, reply) => {
92
92
  const models = await listModels(parseBaseUrl(req.body), parseApiKeyInput(req.body));
93
- res.status(200).json({ models }).end();
93
+ return reply.status(200).send({ models });
94
94
  };
95
95
  };
96
96
  const createSearchAdminReindexHandler = (manager = getDefaultSemanticSearchManager()) => {
97
- return async (_req, res) => {
97
+ return async (_req, reply) => {
98
98
  const result = await manager.startReindex();
99
- res.status(result.started ? 202 : 200).json(result).end();
99
+ return reply.status(result.started ? 202 : 200).send(result);
100
100
  };
101
101
  };
102
102
  export {
@@ -355,6 +355,21 @@ class SemanticSearchManager {
355
355
  this.noteSyncReconciliationTimer = null;
356
356
  }
357
357
  }
358
+ async close() {
359
+ this.stopBackgroundSync();
360
+ const activeOperations = [];
361
+ if (this.activeReindex) {
362
+ activeOperations.push(this.activeReindex);
363
+ }
364
+ if (this.activeNoteSync) {
365
+ activeOperations.push(this.activeNoteSync);
366
+ }
367
+ if (this.activeReconciliation) {
368
+ activeOperations.push(this.activeReconciliation);
369
+ }
370
+ await Promise.allSettled(activeOperations);
371
+ await this.dependencies.vectorIndex.close?.();
372
+ }
358
373
  async trySearch(query, limit) {
359
374
  const status = await this.getStatus();
360
375
  if (!status.available) {
@@ -389,6 +404,7 @@ class SemanticSearchManager {
389
404
  }
390
405
  }
391
406
  let defaultSemanticSearchManager = null;
407
+ let defaultSemanticSearchUnsubscribers = [];
392
408
  const getDefaultSemanticSearchManager = () => {
393
409
  if (!defaultSemanticSearchManager) {
394
410
  defaultSemanticSearchManager = new SemanticSearchManager({
@@ -415,17 +431,29 @@ const getDefaultSemanticSearchManager = () => {
415
431
  }),
416
432
  apiKeyStore: new FileEmbeddingApiKeyStore(paths.embeddingApiKey)
417
433
  });
418
- subscribeServerEvents((event) => {
419
- void defaultSemanticSearchManager?.scheduleNoteSync(Number(event.noteId)).catch(() => void 0);
420
- });
421
- subscribeSemanticSearchNoteChanges((noteId) => {
422
- void defaultSemanticSearchManager?.scheduleNoteSync(noteId).catch(() => void 0);
423
- });
434
+ defaultSemanticSearchUnsubscribers = [
435
+ subscribeServerEvents((event) => {
436
+ void defaultSemanticSearchManager?.scheduleNoteSync(Number(event.noteId)).catch(() => void 0);
437
+ }),
438
+ subscribeSemanticSearchNoteChanges((noteId) => {
439
+ void defaultSemanticSearchManager?.scheduleNoteSync(noteId).catch(() => void 0);
440
+ })
441
+ ];
424
442
  defaultSemanticSearchManager.startBackgroundSync();
425
443
  }
426
444
  return defaultSemanticSearchManager;
427
445
  };
446
+ const closeDefaultSemanticSearchManager = async () => {
447
+ const manager = defaultSemanticSearchManager;
448
+ defaultSemanticSearchManager = null;
449
+ for (const unsubscribe of defaultSemanticSearchUnsubscribers) {
450
+ unsubscribe();
451
+ }
452
+ defaultSemanticSearchUnsubscribers = [];
453
+ await manager?.close();
454
+ };
428
455
  export {
429
456
  SemanticSearchManager,
457
+ closeDefaultSemanticSearchManager,
430
458
  getDefaultSemanticSearchManager
431
459
  };
@@ -1,14 +1,14 @@
1
1
  import { ensureTagByName, InvalidTagNameError } from "../../../features/tag/services/organization.js";
2
2
  import { createAppError } from "../../../modules/error-handler.js";
3
3
  const createMcpCreateTagHandler = (ensureTag = ensureTagByName) => {
4
- return async (req, res) => {
4
+ return async (req, reply) => {
5
5
  const name = req.body?.name;
6
6
  if (typeof name !== "string") {
7
7
  throw createAppError(400, "INVALID_TAG_NAME", "A tag name is required.");
8
8
  }
9
9
  try {
10
10
  const result = await ensureTag(name);
11
- res.status(200).json(result).end();
11
+ return reply.status(200).send(result);
12
12
  } catch (error) {
13
13
  if (error instanceof InvalidTagNameError) {
14
14
  throw createAppError(400, "INVALID_TAG_NAME", error.message);