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
@@ -1,31 +1,31 @@
1
- import { rateLimit } from "express-rate-limit";
1
+ import { isAuthenticatedRequest } from "./auth-guard.js";
2
+ import { createAppError } from "./error-handler.js";
2
3
  const AUTH_RATE_LIMIT_MESSAGE = "Too many authentication attempts. Please try again later.";
3
4
  const SESSION_ACCESS_RATE_LIMIT_MESSAGE = "Too many authenticated requests. Please try again later.";
4
- const createAuthAttemptRateLimit = () => rateLimit({
5
- windowMs: 15 * 60 * 1e3,
6
- limit: 10,
7
- standardHeaders: true,
8
- legacyHeaders: false,
9
- handler: (_req, res) => {
10
- res.status(429).json({
11
- code: "AUTH_RATE_LIMITED",
12
- message: AUTH_RATE_LIMIT_MESSAGE
13
- });
14
- }
5
+ const IMAGE_ASSET_RATE_LIMIT_MESSAGE = "Too many image asset requests. Please try again later.";
6
+ const createOptions = (max, timeWindow, code, message, options = {}) => ({
7
+ max,
8
+ timeWindow,
9
+ hook: "preHandler",
10
+ enableDraftSpec: true,
11
+ errorResponseBuilder: (_request, context) => createAppError(context.statusCode, code, message),
12
+ ...options
15
13
  });
16
- const createSessionAccessRateLimit = () => rateLimit({
17
- windowMs: 60 * 1e3,
18
- limit: 300,
19
- standardHeaders: true,
20
- legacyHeaders: false,
21
- handler: (_req, res) => {
22
- res.status(429).json({
23
- code: "SESSION_RATE_LIMITED",
24
- message: SESSION_ACCESS_RATE_LIMIT_MESSAGE
25
- });
26
- }
14
+ const createAuthAttemptRateLimit = () => createOptions(10, 15 * 60 * 1e3, "AUTH_RATE_LIMITED", AUTH_RATE_LIMIT_MESSAGE, {
15
+ groupId: "auth-attempt"
16
+ });
17
+ const createSessionAccessRateLimit = () => createOptions(300, 60 * 1e3, "SESSION_RATE_LIMITED", SESSION_ACCESS_RATE_LIMIT_MESSAGE, {
18
+ groupId: "session-access"
19
+ });
20
+ const createImageAssetRateLimit = (authConfig) => ({
21
+ ...createOptions(10, 15 * 60 * 1e3, "IMAGE_ASSET_RATE_LIMITED", IMAGE_ASSET_RATE_LIMIT_MESSAGE, {
22
+ groupId: "image-asset",
23
+ allowList: (request) => authConfig.mode !== "password" || isAuthenticatedRequest(request)
24
+ }),
25
+ hook: "onRequest"
27
26
  });
28
27
  export {
29
28
  createAuthAttemptRateLimit,
29
+ createImageAssetRateLimit,
30
30
  createSessionAccessRateLimit
31
31
  };
@@ -12,23 +12,25 @@ const getEventStreamLifetimeMs = (expires) => {
12
12
  return Math.max(0, Math.min(expiresAt.getTime() - Date.now(), AUTH_SESSION_IDLE_TIMEOUT_MS));
13
13
  };
14
14
  const createServerEventsHandler = () => {
15
- return async (req, res) => {
16
- res.status(200);
17
- res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
18
- res.setHeader("Cache-Control", "no-cache, no-transform");
19
- res.setHeader("Connection", "keep-alive");
20
- res.setHeader("X-Accel-Buffering", "no");
21
- res.flushHeaders?.();
22
- res.write(": connected\n\n");
15
+ return async (req, reply) => {
16
+ reply.hijack();
17
+ const response = reply.raw;
18
+ response.statusCode = 200;
19
+ response.setHeader("Content-Type", "text/event-stream; charset=utf-8");
20
+ response.setHeader("Cache-Control", "no-cache, no-transform");
21
+ response.setHeader("Connection", "keep-alive");
22
+ response.setHeader("X-Accel-Buffering", "no");
23
+ response.flushHeaders();
24
+ response.write(": connected\n\n");
23
25
  const unsubscribe = subscribeServerEvents((event) => {
24
- res.write(serializeServerEvent(event));
26
+ response.write(serializeServerEvent(event));
25
27
  });
26
28
  const keepAliveTimer = setInterval(() => {
27
- res.write(": keepalive\n\n");
29
+ response.write(": keepalive\n\n");
28
30
  }, KEEP_ALIVE_INTERVAL_MS);
29
31
  keepAliveTimer.unref?.();
30
32
  const sessionExpiryTimer = setTimeout(() => {
31
- res.end();
33
+ response.end();
32
34
  }, getEventStreamLifetimeMs(req.session?.cookie.expires));
33
35
  sessionExpiryTimer.unref?.();
34
36
  let cleanedUp = false;
@@ -41,8 +43,8 @@ const createServerEventsHandler = () => {
41
43
  clearTimeout(sessionExpiryTimer);
42
44
  unsubscribe();
43
45
  };
44
- req.on("close", cleanup);
45
- res.on("close", cleanup);
46
+ req.raw.on("close", cleanup);
47
+ response.on("close", cleanup);
46
48
  };
47
49
  };
48
50
  export {
@@ -1,4 +1,3 @@
1
- import session from "express-session";
2
1
  const AUTH_SESSION_IDLE_TIMEOUT_MS = 7 * 24 * 60 * 60 * 1e3;
3
2
  const ANONYMOUS_SESSION_IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
4
3
  const AUTH_SESSION_PRUNE_INTERVAL_MS = 60 * 60 * 1e3;
@@ -25,9 +24,8 @@ const getSessionExpiry = (data, now = Date.now()) => {
25
24
  }
26
25
  return fallbackExpiresAt;
27
26
  };
28
- class TtlMemorySessionStore extends session.Store {
27
+ class TtlMemorySessionStore {
29
28
  constructor(options = {}) {
30
- super();
31
29
  this.options = options;
32
30
  this.pruneTimer = setInterval(() => {
33
31
  this.pruneExpiredSessions();
@@ -54,7 +52,7 @@ class TtlMemorySessionStore extends session.Store {
54
52
  callback(error);
55
53
  }
56
54
  }
57
- set(sid, data, callback) {
55
+ set(sid, data, callback = () => void 0) {
58
56
  try {
59
57
  const now = Date.now();
60
58
  this.sessions.set(sid, {
@@ -65,14 +63,14 @@ class TtlMemorySessionStore extends session.Store {
65
63
  });
66
64
  this.pruneExpiredSessions();
67
65
  this.evictOldestSessions();
68
- callback?.();
66
+ callback();
69
67
  } catch (error) {
70
- callback?.(error);
68
+ callback(error);
71
69
  }
72
70
  }
73
- destroy(sid, callback) {
71
+ destroy(sid, callback = () => void 0) {
74
72
  this.sessions.delete(sid);
75
- callback?.();
73
+ callback();
76
74
  }
77
75
  touch(sid, data, callback) {
78
76
  const entry = this.sessions.get(sid);
@@ -6,9 +6,10 @@ const DATA_DIR = process.env.OCEAN_BRAIN_DATA_DIR || ".";
6
6
  const IMAGE_DIR = process.env.OCEAN_BRAIN_IMAGE_DIR || path.resolve(DATA_DIR, "assets/images");
7
7
  const SEARCH_INDEX_PATH = process.env.OCEAN_BRAIN_SEARCH_INDEX_PATH || path.resolve(DATA_DIR, "search.sqlite3");
8
8
  const EMBEDDING_API_KEY_PATH = path.resolve(DATA_DIR, "embedding-api-key");
9
+ const CLIENT_DIST = process.env.OCEAN_BRAIN_CLIENT_DIST || path.resolve(PACKAGE_ROOT, "client/dist");
9
10
  const paths = {
10
11
  packageRoot: path.resolve(PACKAGE_ROOT),
11
- clientDist: path.resolve(PACKAGE_ROOT, "client/dist"),
12
+ clientDist: path.resolve(CLIENT_DIST),
12
13
  imageDir: path.resolve(IMAGE_DIR),
13
14
  searchIndex: path.resolve(SEARCH_INDEX_PATH),
14
15
  embeddingApiKey: path.resolve(EMBEDDING_API_KEY_PATH)
@@ -1,4 +1,3 @@
1
- import { Router } from "express";
2
1
  import { createLoginHandler, createLogoutHandler, createSessionStatusHandler } from "../features/auth/http/api.js";
3
2
  import { createUploadImageHandler } from "../features/image/http/upload.js";
4
3
  import {
@@ -17,78 +16,113 @@ import {
17
16
  import { createCsrfProtection, requireSessionForWrite } from "../modules/auth-guard.js";
18
17
  import { createAuthAttemptRateLimit, createSessionAccessRateLimit } from "../modules/rate-limit.js";
19
18
  import { createServerEventsHandler } from "../modules/server-events-handler.js";
20
- import useAsync from "../modules/use-async.js";
21
19
  import { createMcpRouter } from "./mcp.js";
22
20
  const createApiRouter = (authConfig, mcpAdminService) => {
23
- const csrfProtection = createCsrfProtection(authConfig);
24
- const requireSession = requireSessionForWrite(authConfig);
25
- const sessionAccessRateLimit = createSessionAccessRateLimit();
26
- return Router().use("/mcp", createMcpRouter(authConfig, mcpAdminService)).get("/auth/session", csrfProtection, useAsync(createSessionStatusHandler(authConfig))).post("/auth/login", csrfProtection, createAuthAttemptRateLimit(), useAsync(createLoginHandler(authConfig))).post(
27
- "/auth/logout",
28
- sessionAccessRateLimit,
29
- requireSession,
30
- csrfProtection,
31
- useAsync(createLogoutHandler(authConfig))
32
- ).get(
33
- "/mcp-admin/status",
34
- sessionAccessRateLimit,
35
- requireSession,
36
- csrfProtection,
37
- useAsync(createMcpAdminStatusHandler(mcpAdminService))
38
- ).post(
39
- "/mcp-admin/enabled",
40
- sessionAccessRateLimit,
41
- requireSession,
42
- csrfProtection,
43
- useAsync(createMcpAdminSetEnabledHandler(mcpAdminService))
44
- ).post(
45
- "/mcp-admin/token/rotate",
46
- sessionAccessRateLimit,
47
- requireSession,
48
- csrfProtection,
49
- useAsync(createMcpAdminRotateTokenHandler(mcpAdminService))
50
- ).post(
51
- "/mcp-admin/token/revoke",
52
- sessionAccessRateLimit,
53
- requireSession,
54
- csrfProtection,
55
- useAsync(createMcpAdminRevokeTokenHandler(mcpAdminService))
56
- ).get(
57
- "/search-admin/status",
58
- sessionAccessRateLimit,
59
- requireSession,
60
- csrfProtection,
61
- useAsync(createSearchAdminStatusHandler())
62
- ).post(
63
- "/search-admin/config",
64
- sessionAccessRateLimit,
65
- requireSession,
66
- csrfProtection,
67
- useAsync(createSearchAdminSaveConfigHandler())
68
- ).post(
69
- "/search-admin/models",
70
- sessionAccessRateLimit,
71
- requireSession,
72
- csrfProtection,
73
- useAsync(createSearchAdminListModelsHandler())
74
- ).post(
75
- "/search-admin/test",
76
- sessionAccessRateLimit,
77
- requireSession,
78
- csrfProtection,
79
- useAsync(createSearchAdminTestConnectionHandler())
80
- ).post(
81
- "/search-admin/reindex",
82
- sessionAccessRateLimit,
83
- requireSession,
84
- csrfProtection,
85
- useAsync(createSearchAdminReindexHandler())
86
- ).post("/image", sessionAccessRateLimit, requireSession, csrfProtection, useAsync(createUploadImageHandler())).get("/events", sessionAccessRateLimit, requireSession, csrfProtection, createServerEventsHandler()).use((_req, res) => {
87
- res.status(404).json({
88
- code: "API_ROUTE_NOT_FOUND",
89
- message: "The requested API route was not found."
90
- }).end();
91
- });
21
+ return async (app) => {
22
+ const csrfProtection = createCsrfProtection(authConfig);
23
+ const requireSession = requireSessionForWrite(authConfig);
24
+ const sessionAccessRateLimit = app.rateLimit(createSessionAccessRateLimit());
25
+ app.register(createMcpRouter(authConfig, mcpAdminService), { prefix: "/mcp" });
26
+ app.get("/auth/session", createSessionStatusHandler(authConfig));
27
+ app.post(
28
+ "/auth/login",
29
+ {
30
+ preHandler: csrfProtection,
31
+ config: { rateLimit: createAuthAttemptRateLimit() }
32
+ },
33
+ createLoginHandler(authConfig)
34
+ );
35
+ app.post(
36
+ "/auth/logout",
37
+ {
38
+ preHandler: [sessionAccessRateLimit, requireSession, csrfProtection]
39
+ },
40
+ createLogoutHandler(authConfig)
41
+ );
42
+ app.get(
43
+ "/mcp-admin/status",
44
+ {
45
+ preHandler: [sessionAccessRateLimit, requireSession]
46
+ },
47
+ createMcpAdminStatusHandler(mcpAdminService)
48
+ );
49
+ app.post(
50
+ "/mcp-admin/enabled",
51
+ {
52
+ preHandler: [sessionAccessRateLimit, requireSession, csrfProtection]
53
+ },
54
+ createMcpAdminSetEnabledHandler(mcpAdminService)
55
+ );
56
+ app.post(
57
+ "/mcp-admin/token/rotate",
58
+ {
59
+ preHandler: [sessionAccessRateLimit, requireSession, csrfProtection]
60
+ },
61
+ createMcpAdminRotateTokenHandler(mcpAdminService)
62
+ );
63
+ app.post(
64
+ "/mcp-admin/token/revoke",
65
+ {
66
+ preHandler: [sessionAccessRateLimit, requireSession, csrfProtection]
67
+ },
68
+ createMcpAdminRevokeTokenHandler(mcpAdminService)
69
+ );
70
+ app.get(
71
+ "/search-admin/status",
72
+ {
73
+ preHandler: [sessionAccessRateLimit, requireSession]
74
+ },
75
+ createSearchAdminStatusHandler()
76
+ );
77
+ app.post(
78
+ "/search-admin/config",
79
+ {
80
+ preHandler: [sessionAccessRateLimit, requireSession, csrfProtection]
81
+ },
82
+ createSearchAdminSaveConfigHandler()
83
+ );
84
+ app.post(
85
+ "/search-admin/models",
86
+ {
87
+ preHandler: [sessionAccessRateLimit, requireSession, csrfProtection]
88
+ },
89
+ createSearchAdminListModelsHandler()
90
+ );
91
+ app.post(
92
+ "/search-admin/test",
93
+ {
94
+ preHandler: [sessionAccessRateLimit, requireSession, csrfProtection]
95
+ },
96
+ createSearchAdminTestConnectionHandler()
97
+ );
98
+ app.post(
99
+ "/search-admin/reindex",
100
+ {
101
+ preHandler: [sessionAccessRateLimit, requireSession, csrfProtection]
102
+ },
103
+ createSearchAdminReindexHandler()
104
+ );
105
+ app.post(
106
+ "/image",
107
+ {
108
+ preHandler: [sessionAccessRateLimit, requireSession, csrfProtection]
109
+ },
110
+ createUploadImageHandler()
111
+ );
112
+ app.get(
113
+ "/events",
114
+ {
115
+ preHandler: [sessionAccessRateLimit, requireSession]
116
+ },
117
+ createServerEventsHandler()
118
+ );
119
+ app.all("/*", (_request, reply) => {
120
+ return reply.status(404).send({
121
+ code: "API_ROUTE_NOT_FOUND",
122
+ message: "The requested API route was not found."
123
+ });
124
+ });
125
+ };
92
126
  };
93
127
  export {
94
128
  createApiRouter
@@ -1,21 +1,31 @@
1
- import { Router } from "express";
2
1
  import {
3
2
  createLoginPageHandler,
4
3
  createLoginPageSubmitHandler,
5
4
  createLogoutPageHandler
6
5
  } from "../features/auth/http/pages.js";
7
- import { createCsrfProtection, createLoginCsrfFailureHandler, requireSessionForWrite } from "../modules/auth-guard.js";
6
+ import { createCsrfProtection, requireSessionForWrite } from "../modules/auth-guard.js";
8
7
  import { createAuthAttemptRateLimit, createSessionAccessRateLimit } from "../modules/rate-limit.js";
9
8
  const createAuthPagesRouter = (authConfig) => {
10
- const csrfProtection = createCsrfProtection(authConfig);
11
- const sessionAccessRateLimit = createSessionAccessRateLimit();
12
- return Router().get("/login", csrfProtection, createLoginPageHandler(authConfig)).post("/login", csrfProtection, createAuthAttemptRateLimit(), createLoginPageSubmitHandler(authConfig)).post(
13
- "/logout",
14
- sessionAccessRateLimit,
15
- requireSessionForWrite(authConfig),
16
- csrfProtection,
17
- createLogoutPageHandler(authConfig)
18
- ).use(createLoginCsrfFailureHandler(authConfig));
9
+ return async (app) => {
10
+ const csrfProtection = createCsrfProtection(authConfig);
11
+ app.get("/login", createLoginPageHandler(authConfig));
12
+ app.post(
13
+ "/login",
14
+ {
15
+ preHandler: csrfProtection,
16
+ config: { rateLimit: createAuthAttemptRateLimit() }
17
+ },
18
+ createLoginPageSubmitHandler(authConfig)
19
+ );
20
+ app.post(
21
+ "/logout",
22
+ {
23
+ preHandler: [requireSessionForWrite(authConfig), csrfProtection],
24
+ config: { rateLimit: createSessionAccessRateLimit() }
25
+ },
26
+ createLogoutPageHandler(authConfig)
27
+ );
28
+ };
19
29
  };
20
30
  export {
21
31
  createAuthPagesRouter
@@ -1,11 +1,12 @@
1
- import express, { Router } from "express";
2
- import { rateLimit } from "express-rate-limit";
3
- import path from "path";
4
- import { createCsrfProtection, isAuthenticatedRequest } from "../modules/auth-guard.js";
1
+ import path from "node:path";
2
+ import fastifyStatic from "@fastify/static";
3
+ import { isAuthenticatedRequest, issueCsrfToken } from "../modules/auth-guard.js";
4
+ import { createImageAssetRateLimit } from "../modules/rate-limit.js";
5
5
  import { paths } from "../paths.js";
6
- const IMAGE_ASSET_RATE_LIMIT_MESSAGE = "Too many image asset requests. Please try again later.";
7
- const isClientDocumentRequest = (req) => {
8
- return req.method === "GET" && Boolean(req.headers.accept?.includes("text/html")) && path.extname(req.path) === "";
6
+ const getRequestPath = (request) => request.url.split("?")[0] || "/";
7
+ const isClientDocumentRequest = (request) => {
8
+ const accept = request.headers.accept;
9
+ return request.method === "GET" && Boolean(accept?.includes("text/html")) && path.extname(getRequestPath(request)) === "";
9
10
  };
10
11
  const shouldBlockClientRoute = (authConfig, requestPath, authenticated) => {
11
12
  if (authConfig.mode !== "password" || authenticated) {
@@ -16,82 +17,69 @@ const shouldBlockClientRoute = (authConfig, requestPath, authenticated) => {
16
17
  }
17
18
  return path.extname(requestPath) === "";
18
19
  };
19
- const createImageAssetAuthRateLimit = (authConfig) => rateLimit({
20
- windowMs: 15 * 60 * 1e3,
21
- limit: 10,
22
- standardHeaders: true,
23
- legacyHeaders: false,
24
- skip: (req) => authConfig.mode !== "password" || isAuthenticatedRequest(req),
25
- handler: (_req, res) => {
26
- res.setHeader("Cache-Control", "no-store");
27
- res.status(429).json({
28
- code: "IMAGE_ASSET_RATE_LIMITED",
29
- message: IMAGE_ASSET_RATE_LIMIT_MESSAGE
30
- });
31
- }
32
- });
33
20
  const createProtectedImageAssetsMiddleware = (authConfig) => {
34
- return (req, res, next) => {
35
- if (authConfig.mode !== "password" || isAuthenticatedRequest(req)) {
36
- next();
21
+ return (request, reply, done) => {
22
+ if (authConfig.mode !== "password" || isAuthenticatedRequest(request)) {
23
+ done();
37
24
  return;
38
25
  }
39
- res.setHeader("Cache-Control", "no-store");
40
- if (req.headers.accept?.includes("text/html")) {
41
- const redirectPath = encodeURIComponent(req.originalUrl || req.url || "/");
42
- res.redirect(303, `/login?next=${redirectPath}`);
26
+ if (request.headers.accept?.includes("text/html")) {
27
+ const redirectPath = encodeURIComponent(request.url || "/");
28
+ void reply.redirect(`/login?next=${redirectPath}`, 303);
43
29
  return;
44
30
  }
45
- res.status(401).end();
31
+ void reply.status(401).send();
46
32
  };
47
33
  };
48
- const createClientRouteCsrfTokenMiddleware = (authConfig) => {
49
- const csrfProtection = createCsrfProtection(authConfig);
50
- return (req, res, next) => {
51
- if (path.extname(req.path) !== "") {
52
- next();
53
- return;
34
+ const setImageHeaders = (authConfig, reply) => {
35
+ reply.header("X-Content-Type-Options", "nosniff");
36
+ if (authConfig.mode === "password") {
37
+ reply.header("Cache-Control", "no-store");
38
+ }
39
+ };
40
+ const createProductionClientContentHandler = () => {
41
+ return (request, reply) => {
42
+ if (isClientDocumentRequest(request)) {
43
+ return reply.sendFile("index.html", paths.clientDist);
54
44
  }
55
- csrfProtection(req, res, next);
45
+ const filePath = getRequestPath(request).replace(/^\/+/, "");
46
+ return reply.sendFile(filePath, paths.clientDist, { extensions: ["html"] });
56
47
  };
57
48
  };
58
- const createProductionClientContentRouter = () => Router().use(express.static(paths.clientDist, { extensions: ["html"] })).get(/.*/, (req, res, next) => {
59
- if (!isClientDocumentRequest(req)) {
60
- next();
61
- return;
62
- }
63
- res.sendFile("index.html", { root: paths.clientDist });
64
- }).use((_req, res) => {
65
- res.setHeader("X-Content-Type-Options", "nosniff");
66
- res.status(404).end();
67
- });
68
- const createClientRouter = (authConfig, clientContentMiddleware = createProductionClientContentRouter()) => Router().use(
69
- "/assets/images",
70
- createImageAssetAuthRateLimit(authConfig),
71
- createProtectedImageAssetsMiddleware(authConfig),
72
- express.static(paths.imageDir, {
73
- setHeaders: (res) => {
74
- res.setHeader("X-Content-Type-Options", "nosniff");
75
- if (authConfig.mode === "password") {
76
- res.setHeader("Cache-Control", "no-store");
49
+ const createClientRouter = (authConfig, clientContentHandler = createProductionClientContentHandler()) => {
50
+ return async (app) => {
51
+ app.register(fastifyStatic, { serve: false });
52
+ app.get(
53
+ "/assets/images/*",
54
+ {
55
+ onRequest: (_request, reply, done) => {
56
+ setImageHeaders(authConfig, reply);
57
+ done();
58
+ },
59
+ preHandler: createProtectedImageAssetsMiddleware(authConfig),
60
+ config: { rateLimit: createImageAssetRateLimit(authConfig) }
61
+ },
62
+ (request, reply) => {
63
+ return reply.sendFile(request.params["*"], paths.imageDir, { cacheControl: false });
77
64
  }
78
- }
79
- }),
80
- (_req, res) => {
81
- res.setHeader("X-Content-Type-Options", "nosniff");
82
- if (authConfig.mode === "password") {
83
- res.setHeader("Cache-Control", "no-store");
84
- }
85
- res.status(404).end();
86
- }
87
- ).use((req, res, next) => {
88
- if (shouldBlockClientRoute(authConfig, req.path, isAuthenticatedRequest(req))) {
89
- const redirectPath = encodeURIComponent(req.originalUrl || "/");
90
- res.redirect(303, `/login?next=${redirectPath}`);
91
- return;
92
- }
93
- next();
94
- }).use(createClientRouteCsrfTokenMiddleware(authConfig)).use(clientContentMiddleware);
65
+ );
66
+ const handleClientRequest = async (request, reply) => {
67
+ const requestPath = getRequestPath(request);
68
+ if (shouldBlockClientRoute(authConfig, requestPath, isAuthenticatedRequest(request))) {
69
+ return reply.redirect(`/login?next=${encodeURIComponent(request.url || "/")}`, 303);
70
+ }
71
+ if (path.extname(requestPath) === "") {
72
+ issueCsrfToken(authConfig, reply);
73
+ }
74
+ return clientContentHandler(request, reply);
75
+ };
76
+ app.get("/", handleClientRequest);
77
+ app.get("/*", handleClientRequest);
78
+ app.setNotFoundHandler((_request, reply) => {
79
+ return reply.header("X-Content-Type-Options", "nosniff").status(404).send();
80
+ });
81
+ };
82
+ };
95
83
  export {
96
84
  createClientRouter
97
85
  };
@@ -1,35 +1,40 @@
1
- import { Router } from "express";
2
- import { createHandler } from "graphql-http/lib/use/express";
1
+ import mercurius from "mercurius";
3
2
  import { createCsrfProtection, isAuthenticatedRequest, requireSessionForGraphql } from "../modules/auth-guard.js";
4
3
  import { createMcpAuthMiddleware, createReadOnlyMcpValidationRule } from "../modules/mcp-auth.js";
5
4
  import schema from "../schema/index.js";
6
- const createGraphqlContext = (authConfig) => (req) => ({
5
+ const createGraphqlContext = (authConfig) => (request, reply) => ({
7
6
  authMode: authConfig.mode,
8
- isAuthenticated: isAuthenticatedRequest(req.raw),
9
- req: req.raw,
10
- res: req.context.res
7
+ isAuthenticated: isAuthenticatedRequest(request),
8
+ req: request,
9
+ res: reply
11
10
  });
12
11
  const createGraphqlRouter = (authConfig, mcpAdminService) => {
13
- const csrfProtection = createCsrfProtection(authConfig);
14
- return Router().use(
15
- "/mcp",
16
- createMcpAuthMiddleware(authConfig, mcpAdminService),
17
- createHandler({
18
- schema,
19
- context: createGraphqlContext(authConfig),
20
- validationRules: (_req, _args, specifiedRules) => {
21
- return [...specifiedRules, createReadOnlyMcpValidationRule()];
22
- }
23
- })
24
- ).use(
25
- "/",
26
- requireSessionForGraphql(authConfig),
27
- csrfProtection,
28
- createHandler({
29
- schema,
30
- context: createGraphqlContext(authConfig)
31
- })
32
- );
12
+ return async (app) => {
13
+ app.register(async (mcpEndpoint) => {
14
+ mcpEndpoint.addHook("preHandler", createMcpAuthMiddleware(authConfig, mcpAdminService));
15
+ mcpEndpoint.register(mercurius, {
16
+ schema,
17
+ path: "/graphql/mcp",
18
+ graphiql: false,
19
+ errorFormatter: (execution, context) => ({
20
+ ...mercurius.defaultErrorFormatter(execution, context),
21
+ statusCode: 200
22
+ }),
23
+ context: createGraphqlContext(authConfig),
24
+ validationRules: [createReadOnlyMcpValidationRule()]
25
+ });
26
+ });
27
+ app.register(async (sessionEndpoint) => {
28
+ sessionEndpoint.addHook("preHandler", requireSessionForGraphql(authConfig));
29
+ sessionEndpoint.addHook("preHandler", createCsrfProtection(authConfig));
30
+ sessionEndpoint.register(mercurius, {
31
+ schema,
32
+ path: "/graphql",
33
+ graphiql: false,
34
+ context: createGraphqlContext(authConfig)
35
+ });
36
+ });
37
+ };
33
38
  };
34
39
  export {
35
40
  createGraphqlRouter