ocean-brain 0.13.0 → 0.13.2

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 (48) 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-DH3eSSRs.js} +1 -1
  5. package/server/client/dist/assets/{Note-CGI-Y1__.js → Note-CpzLtGVM.js} +1 -1
  6. package/server/client/dist/assets/{Search-DAYHxAml.js → Search-D9detGWk.js} +1 -1
  7. package/server/client/dist/assets/{Tag-CG8b0l1j.js → Tag-Bquqtx23.js} +1 -1
  8. package/server/client/dist/assets/{TagNotes-COyaVsdn.js → TagNotes-CbjfxqAN.js} +1 -1
  9. package/server/client/dist/assets/{Views-BPxlw3vk.js → Views-L4eC96Lx.js} +1 -1
  10. package/server/client/dist/assets/{app-BezcjTfd.js → app-BFM2V_Rx.js} +2 -2
  11. package/server/client/dist/assets/{index-CzZFedXj.js → index-CJPl3MJE.js} +1 -1
  12. package/server/client/dist/assets/{manage-image-detail-9hRTiAsR.js → manage-image-detail-BIDHUR5-.js} +1 -1
  13. package/server/client/dist/assets/mcp-D8footel.js +5 -0
  14. package/server/client/dist/assets/{note-core-DUTqUh5N.js → note-core-YhYEmI_R.js} +1 -1
  15. package/server/client/dist/assets/note-runtime-B3ats-dC.js +153 -0
  16. package/server/client/dist/assets/{note-ui-BaqzP-YU.js → note-ui-BtzPk8gA.js} +1 -1
  17. package/server/client/dist/assets/{placeholder-CZoqs4S3.js → placeholder-DKgxoe_f.js} +1 -1
  18. package/server/client/dist/assets/route-preload.js +2 -2
  19. package/server/client/dist/index.html +1 -1
  20. package/server/dist/app.js +91 -8
  21. package/server/dist/features/auth/http/api.js +9 -8
  22. package/server/dist/features/auth/http/pages.js +27 -28
  23. package/server/dist/features/auth/service.js +7 -27
  24. package/server/dist/features/cache/graphql/cache.type-defs.js +1 -1
  25. package/server/dist/features/image/http/upload.js +2 -2
  26. package/server/dist/features/mcp-admin/http/handlers.js +9 -9
  27. package/server/dist/features/note/http/mcp.js +17 -17
  28. package/server/dist/features/search/http/handlers.js +10 -10
  29. package/server/dist/features/search/search-manager.js +34 -6
  30. package/server/dist/features/tag/http/mcp.js +2 -2
  31. package/server/dist/modules/auth-guard.js +36 -53
  32. package/server/dist/modules/blocknote.js +16 -9
  33. package/server/dist/modules/error-handler.js +17 -20
  34. package/server/dist/modules/mcp-auth.js +50 -56
  35. package/server/dist/modules/rate-limit.js +23 -23
  36. package/server/dist/modules/server-events-handler.js +15 -13
  37. package/server/dist/modules/session-store.js +6 -8
  38. package/server/dist/paths.js +2 -1
  39. package/server/dist/routes/api.js +105 -71
  40. package/server/dist/routes/auth-pages.js +21 -11
  41. package/server/dist/routes/client.js +60 -72
  42. package/server/dist/routes/graphql.js +31 -26
  43. package/server/dist/routes/mcp.js +25 -35
  44. package/server/dist/server.js +14 -16
  45. package/server/client/dist/assets/mcp-ugdCKa6H.js +0 -4
  46. package/server/client/dist/assets/note-runtime-zN5ddTba.js +0 -168
  47. package/server/dist/modules/logger.js +0 -53
  48. package/server/dist/modules/use-async.js +0 -10
@@ -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);
@@ -1,76 +1,57 @@
1
1
  import { buildUnauthorizedGraphqlPayload, buildUnauthorizedPayload } from "@baejino/auth";
2
- import session from "express-session";
3
2
  import { GraphQLError } from "graphql";
4
- import lusca from "lusca";
5
3
  import { sanitizeRedirectPath } from "./auth-redirect.js";
6
- import { AUTH_SESSION_IDLE_TIMEOUT_MS, createSessionStore } from "./session-store.js";
7
4
  const JSON_HEADERS = { "Content-Type": "application/json" };
8
- const isAuthenticatedRequest = (req) => Boolean(req.session?.authenticated);
9
- const createSessionMiddleware = (authConfig) => {
5
+ const CSRF_COOKIE_NAME = "XSRF-TOKEN";
6
+ const isAuthenticatedRequest = (request) => Boolean(request.session?.authenticated);
7
+ const issueCsrfToken = (authConfig, reply) => {
10
8
  if (authConfig.mode !== "password") {
11
- return (_req, _res, next) => next();
9
+ return void 0;
12
10
  }
13
- return session({
14
- secret: authConfig.sessionSecret,
15
- name: authConfig.cookieName,
16
- store: createSessionStore(),
17
- resave: false,
18
- saveUninitialized: false,
19
- rolling: true,
20
- cookie: {
21
- maxAge: AUTH_SESSION_IDLE_TIMEOUT_MS,
22
- httpOnly: true,
23
- sameSite: "lax",
24
- secure: process.env.NODE_ENV === "production",
25
- path: "/"
26
- }
11
+ const token = reply.generateCsrf();
12
+ reply.setCookie(CSRF_COOKIE_NAME, token, {
13
+ httpOnly: false,
14
+ path: "/",
15
+ sameSite: "lax",
16
+ secure: process.env.NODE_ENV === "production"
27
17
  });
18
+ return token;
28
19
  };
29
20
  const createCsrfProtection = (authConfig) => {
30
21
  if (authConfig.mode !== "password") {
31
- return (_req, _res, next) => next();
22
+ return (_request, _reply, done) => done();
32
23
  }
33
- return lusca.csrf({
34
- angular: true,
35
- cookie: {
36
- options: {
37
- path: "/",
38
- sameSite: "lax",
39
- secure: process.env.NODE_ENV === "production"
40
- }
41
- }
42
- });
43
- };
44
- const isCsrfTokenError = (error) => error instanceof Error && error.message.startsWith("CSRF token ");
45
- const buildLoginRedirectPath = (req) => {
46
- const nextPath = sanitizeRedirectPath(req.body?.next);
47
- return `/login?next=${encodeURIComponent(nextPath)}`;
48
- };
49
- const createLoginCsrfFailureHandler = (authConfig) => {
50
- return (error, req, res, next) => {
51
- if (authConfig.mode !== "password" || !isCsrfTokenError(error) || req.method !== "POST" || req.path !== "/login" || isAuthenticatedRequest(req)) {
52
- next(error);
24
+ return (request, reply, done) => {
25
+ if (request.method === "GET" || request.method === "HEAD" || request.method === "OPTIONS") {
26
+ done();
53
27
  return;
54
28
  }
55
- res.redirect(303, buildLoginRedirectPath(req));
29
+ request.server.csrfProtection(request, reply, done);
56
30
  };
57
31
  };
32
+ const isCsrfTokenError = (error) => error instanceof Error && ("code" in error ? error.code === "FST_CSRF_INVALID_TOKEN" || error.code === "FST_CSRF_MISSING_SECRET" : error.message.toLowerCase().includes("csrf"));
33
+ const buildLoginCsrfRedirectPath = (request) => {
34
+ const body = request.body;
35
+ const nextPath = sanitizeRedirectPath(body?.next);
36
+ return `/login?next=${encodeURIComponent(nextPath)}`;
37
+ };
38
+ const shouldRedirectLoginCsrfFailure = (error, request, authConfig) => authConfig.mode === "password" && isCsrfTokenError(error) && request.method === "POST" && request.url.split("?")[0] === "/login" && !isAuthenticatedRequest(request);
58
39
  const requireSessionForWrite = (authConfig) => {
59
- return (req, res, next) => {
60
- if (authConfig.mode === "open" || isAuthenticatedRequest(req)) {
61
- next();
40
+ return (request, reply, done) => {
41
+ if (authConfig.mode === "open" || isAuthenticatedRequest(request)) {
42
+ done();
62
43
  return;
63
44
  }
64
- res.status(401).set(JSON_HEADERS).json(buildUnauthorizedPayload()).end();
45
+ void reply.code(401).headers(JSON_HEADERS).send(buildUnauthorizedPayload());
65
46
  };
66
47
  };
67
48
  const requireSessionForGraphql = (authConfig) => {
68
- return (req, res, next) => {
69
- if (authConfig.mode === "open" || isAuthenticatedRequest(req)) {
70
- next();
49
+ return (request, reply, done) => {
50
+ if (authConfig.mode === "open" || isAuthenticatedRequest(request)) {
51
+ done();
71
52
  return;
72
53
  }
73
- res.status(401).set(JSON_HEADERS).json(buildUnauthorizedGraphqlPayload()).end();
54
+ void reply.code(401).headers(JSON_HEADERS).send(buildUnauthorizedGraphqlPayload());
74
55
  };
75
56
  };
76
57
  const createMutationAuthValidationRule = () => {
@@ -94,11 +75,13 @@ const createMutationAuthValidationRule = () => {
94
75
  };
95
76
  };
96
77
  export {
78
+ buildLoginCsrfRedirectPath,
97
79
  createCsrfProtection,
98
- createLoginCsrfFailureHandler,
99
80
  createMutationAuthValidationRule,
100
- createSessionMiddleware,
101
81
  isAuthenticatedRequest,
82
+ isCsrfTokenError,
83
+ issueCsrfToken,
102
84
  requireSessionForGraphql,
103
- requireSessionForWrite
85
+ requireSessionForWrite,
86
+ shouldRedirectLoginCsrfFailure
104
87
  };
@@ -83,10 +83,12 @@ function mapBlockContent(content, mapInline) {
83
83
  ...content,
84
84
  rows: content.rows?.map((row) => ({
85
85
  ...row,
86
- cells: row.cells?.map((cell) => ({
87
- ...cell,
88
- content: cell.content?.map(mapInline)
89
- }))
86
+ cells: row.cells?.map(
87
+ (cell) => Array.isArray(cell) ? cell.map(mapInline) : {
88
+ ...cell,
89
+ content: cell.content?.map(mapInline)
90
+ }
91
+ )
90
92
  }))
91
93
  };
92
94
  }
@@ -103,10 +105,15 @@ async function mapBlockContentAsync(content, mapInline) {
103
105
  (content.rows ?? []).map(async (row) => ({
104
106
  ...row,
105
107
  cells: await Promise.all(
106
- (row.cells ?? []).map(async (cell) => ({
107
- ...cell,
108
- content: cell.content ? (await Promise.all(cell.content.map(mapInline))).flat() : cell.content
109
- }))
108
+ (row.cells ?? []).map(async (cell) => {
109
+ if (Array.isArray(cell)) {
110
+ return (await Promise.all(cell.map(mapInline))).flat();
111
+ }
112
+ return {
113
+ ...cell,
114
+ content: cell.content ? (await Promise.all(cell.content.map(mapInline))).flat() : cell.content
115
+ };
116
+ })
110
117
  )
111
118
  }))
112
119
  )
@@ -124,7 +131,7 @@ function visitBlockContent(content, visitInline) {
124
131
  }
125
132
  for (const row of content.rows ?? []) {
126
133
  for (const cell of row.cells ?? []) {
127
- for (const inline of cell.content ?? []) {
134
+ for (const inline of Array.isArray(cell) ? cell : cell.content ?? []) {
128
135
  visitInline(inline);
129
136
  }
130
137
  }
@@ -1,3 +1,4 @@
1
+ import { buildLoginCsrfRedirectPath, isCsrfTokenError, shouldRedirectLoginCsrfFailure } from "./auth-guard.js";
1
2
  class AppError extends Error {
2
3
  code;
3
4
  status;
@@ -13,42 +14,38 @@ class AppError extends Error {
13
14
  const createAppError = (status, code, message, details) => {
14
15
  return new AppError(status, code, message, details);
15
16
  };
16
- const hasErrorType = (error, type) => error instanceof Error && "type" in error && error.type === type;
17
- const createErrorHandler = () => {
18
- return (error, _req, res, next) => {
19
- if (res.headersSent) {
20
- next(error);
17
+ const createErrorHandler = (authConfig) => {
18
+ return (error, request, reply) => {
19
+ if (reply.sent) {
20
+ request.log.error({ error }, "Error occurred after the response was sent");
21
+ return;
22
+ }
23
+ if (shouldRedirectLoginCsrfFailure(error, request, authConfig)) {
24
+ void reply.redirect(buildLoginCsrfRedirectPath(request), 303);
21
25
  return;
22
26
  }
23
27
  if (error instanceof AppError) {
24
- res.status(error.status).json({
28
+ void reply.status(error.status).send({
25
29
  code: error.code,
26
30
  message: error.message,
27
31
  ...error.details ? { details: error.details } : {}
28
- }).end();
29
- return;
30
- }
31
- if (hasErrorType(error, "encoding.unsupported")) {
32
- res.status(415).json({
33
- code: "UNSUPPORTED_CONTENT_ENCODING",
34
- message: "Compressed request bodies are not supported."
35
- }).end();
32
+ });
36
33
  return;
37
34
  }
38
- if (error instanceof Error && error.message.startsWith("CSRF token ")) {
39
- res.status(403).json({
35
+ if (isCsrfTokenError(error)) {
36
+ void reply.status(403).send({
40
37
  code: "CSRF_TOKEN_INVALID",
41
- message: error.message
42
- }).end();
38
+ message: error instanceof Error ? error.message : "Invalid CSRF token"
39
+ });
43
40
  return;
44
41
  }
45
42
  const message = error instanceof Error ? error.stack || error.message : String(error);
46
43
  process.stderr.write(`[error] ${message}
47
44
  `);
48
- res.status(500).json({
45
+ void reply.status(500).send({
49
46
  code: "INTERNAL_SERVER_ERROR",
50
47
  message: "Internal Server Error"
51
- }).end();
48
+ });
52
49
  };
53
50
  };
54
51
  export {
@@ -44,64 +44,58 @@ const readBearerToken = (authorizationHeader) => {
44
44
  return authorizationHeader.slice("Bearer ".length).trim() || void 0;
45
45
  };
46
46
  const createMcpAuthMiddleware = (_authConfig, mcpAdminAuth) => {
47
- return async (req, res, next) => {
48
- try {
49
- const status = await mcpAdminAuth.getStatus();
50
- if (!status.enabled) {
51
- res.status(403).set(JSON_HEADERS).json({
52
- code: "MCP_DISABLED",
53
- message: "MCP access is disabled by admin."
54
- }).end();
55
- return;
56
- }
57
- const bearerToken = readBearerToken(req.headers.authorization);
58
- if (!bearerToken) {
59
- res.status(401).set(JSON_HEADERS).json({
60
- code: "UNAUTHORIZED",
61
- message: "A valid MCP bearer token is required."
62
- }).end();
63
- return;
64
- }
65
- const validation = await mcpAdminAuth.validatePresentedToken(bearerToken);
66
- if (!validation.ok && validation.reason === "not_configured") {
67
- res.status(503).set(JSON_HEADERS).json({
68
- code: "MCP_AUTH_NOT_CONFIGURED",
69
- message: "MCP bearer auth is not configured."
70
- }).end();
71
- return;
72
- }
73
- if (!validation.ok) {
74
- res.status(403).set(JSON_HEADERS).json({
75
- code: "FORBIDDEN",
76
- message: "Invalid MCP bearer token."
77
- }).end();
78
- return;
79
- }
80
- const versionInfo = getOceanBrainVersionInfo();
81
- const mcpCompatibilityVersion = readMcpCompatibilityVersion(req);
82
- const mcpClientVersion = readMcpClientVersion(req);
83
- if (!mcpCompatibilityVersion || !isMcpVersionCompatible(versionInfo.mcp.compatibilityVersion, mcpCompatibilityVersion)) {
84
- res.status(426).set(JSON_HEADERS).json({
85
- code: "MCP_VERSION_INCOMPATIBLE",
86
- message: createMcpVersionCompatibilityMessage({
87
- serverVersion: versionInfo.version,
88
- requiredMcpCompatibilityVersion: versionInfo.mcp.compatibilityRequirement,
89
- mcpCompatibilityVersion,
90
- mcpClientVersion
91
- }),
47
+ return async (req, reply) => {
48
+ const status = await mcpAdminAuth.getStatus();
49
+ if (!status.enabled) {
50
+ await reply.status(403).headers(JSON_HEADERS).send({
51
+ code: "MCP_DISABLED",
52
+ message: "MCP access is disabled by admin."
53
+ });
54
+ return;
55
+ }
56
+ const bearerToken = readBearerToken(req.headers.authorization);
57
+ if (!bearerToken) {
58
+ await reply.status(401).headers(JSON_HEADERS).send({
59
+ code: "UNAUTHORIZED",
60
+ message: "A valid MCP bearer token is required."
61
+ });
62
+ return;
63
+ }
64
+ const validation = await mcpAdminAuth.validatePresentedToken(bearerToken);
65
+ if (!validation.ok && validation.reason === "not_configured") {
66
+ await reply.status(503).headers(JSON_HEADERS).send({
67
+ code: "MCP_AUTH_NOT_CONFIGURED",
68
+ message: "MCP bearer auth is not configured."
69
+ });
70
+ return;
71
+ }
72
+ if (!validation.ok) {
73
+ await reply.status(403).headers(JSON_HEADERS).send({
74
+ code: "FORBIDDEN",
75
+ message: "Invalid MCP bearer token."
76
+ });
77
+ return;
78
+ }
79
+ const versionInfo = getOceanBrainVersionInfo();
80
+ const mcpCompatibilityVersion = readMcpCompatibilityVersion(req);
81
+ const mcpClientVersion = readMcpClientVersion(req);
82
+ if (!mcpCompatibilityVersion || !isMcpVersionCompatible(versionInfo.mcp.compatibilityVersion, mcpCompatibilityVersion)) {
83
+ await reply.status(426).headers(JSON_HEADERS).send({
84
+ code: "MCP_VERSION_INCOMPATIBLE",
85
+ message: createMcpVersionCompatibilityMessage({
92
86
  serverVersion: versionInfo.version,
93
- mcpVersion: mcpClientVersion ?? mcpCompatibilityVersion ?? null,
94
- mcpClientVersion: mcpClientVersion ?? null,
95
- mcpCompatibilityVersion: mcpCompatibilityVersion ?? null,
96
- requiredMcpVersion: versionInfo.mcpVersionRequirement,
97
87
  requiredMcpCompatibilityVersion: versionInfo.mcp.compatibilityRequirement,
98
- releaseUrl: versionInfo.releaseUrl
99
- }).end();
100
- return;
101
- }
102
- next();
103
- } catch (error) {
104
- next(error);
88
+ mcpCompatibilityVersion,
89
+ mcpClientVersion
90
+ }),
91
+ serverVersion: versionInfo.version,
92
+ mcpVersion: mcpClientVersion ?? mcpCompatibilityVersion ?? null,
93
+ mcpClientVersion: mcpClientVersion ?? null,
94
+ mcpCompatibilityVersion: mcpCompatibilityVersion ?? null,
95
+ requiredMcpVersion: versionInfo.mcpVersionRequirement,
96
+ requiredMcpCompatibilityVersion: versionInfo.mcp.compatibilityRequirement,
97
+ releaseUrl: versionInfo.releaseUrl
98
+ });
105
99
  }
106
100
  };
107
101
  };