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,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
  };
@@ -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)