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.
- package/README.md +5 -3
- package/dist/index.js +12 -4
- package/package.json +10 -8
- package/server/client/dist/assets/{Calendar-CaeK7gTz.js → Calendar-DExFS7xq.js} +1 -1
- package/server/client/dist/assets/{Search-DAYHxAml.js → Search-B_BRjOyG.js} +1 -1
- package/server/client/dist/assets/{Tag-CG8b0l1j.js → Tag-2hcR9dd4.js} +1 -1
- package/server/client/dist/assets/{TagNotes-COyaVsdn.js → TagNotes-BE20Ji5s.js} +1 -1
- package/server/client/dist/assets/{Views-BPxlw3vk.js → Views-CQXgnbUQ.js} +1 -1
- package/server/client/dist/assets/{app-BezcjTfd.js → app-4M3jUhmd.js} +2 -2
- package/server/client/dist/assets/{index-CzZFedXj.js → index-CaT-yZJw.js} +1 -1
- package/server/client/dist/assets/{manage-image-detail-9hRTiAsR.js → manage-image-detail-Bu9yymBn.js} +1 -1
- package/server/client/dist/assets/mcp-D8footel.js +5 -0
- package/server/client/dist/assets/{placeholder-CZoqs4S3.js → placeholder-BZ-p6ahW.js} +1 -1
- package/server/client/dist/index.html +1 -1
- package/server/dist/app.js +91 -8
- package/server/dist/features/auth/http/api.js +9 -8
- package/server/dist/features/auth/http/pages.js +27 -28
- package/server/dist/features/auth/service.js +7 -27
- package/server/dist/features/cache/graphql/cache.type-defs.js +1 -1
- package/server/dist/features/image/http/upload.js +2 -2
- package/server/dist/features/mcp-admin/http/handlers.js +9 -9
- package/server/dist/features/note/http/mcp.js +17 -17
- package/server/dist/features/search/http/handlers.js +10 -10
- package/server/dist/features/search/search-manager.js +34 -6
- package/server/dist/features/tag/http/mcp.js +2 -2
- package/server/dist/modules/auth-guard.js +36 -53
- package/server/dist/modules/blocknote.js +16 -9
- package/server/dist/modules/error-handler.js +17 -20
- package/server/dist/modules/mcp-auth.js +50 -56
- package/server/dist/modules/rate-limit.js +23 -23
- package/server/dist/modules/server-events-handler.js +15 -13
- package/server/dist/modules/session-store.js +6 -8
- package/server/dist/paths.js +2 -1
- package/server/dist/routes/api.js +105 -71
- package/server/dist/routes/auth-pages.js +21 -11
- package/server/dist/routes/client.js +60 -72
- package/server/dist/routes/graphql.js +31 -26
- package/server/dist/routes/mcp.js +25 -35
- package/server/dist/server.js +14 -16
- package/server/client/dist/assets/mcp-ugdCKa6H.js +0 -4
- package/server/dist/modules/logger.js +0 -53
- 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
|
|
9
|
-
const
|
|
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
|
|
9
|
+
return void 0;
|
|
12
10
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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 (
|
|
22
|
+
return (_request, _reply, done) => done();
|
|
32
23
|
}
|
|
33
|
-
return
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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 (
|
|
60
|
-
if (authConfig.mode === "open" || isAuthenticatedRequest(
|
|
61
|
-
|
|
40
|
+
return (request, reply, done) => {
|
|
41
|
+
if (authConfig.mode === "open" || isAuthenticatedRequest(request)) {
|
|
42
|
+
done();
|
|
62
43
|
return;
|
|
63
44
|
}
|
|
64
|
-
|
|
45
|
+
void reply.code(401).headers(JSON_HEADERS).send(buildUnauthorizedPayload());
|
|
65
46
|
};
|
|
66
47
|
};
|
|
67
48
|
const requireSessionForGraphql = (authConfig) => {
|
|
68
|
-
return (
|
|
69
|
-
if (authConfig.mode === "open" || isAuthenticatedRequest(
|
|
70
|
-
|
|
49
|
+
return (request, reply, done) => {
|
|
50
|
+
if (authConfig.mode === "open" || isAuthenticatedRequest(request)) {
|
|
51
|
+
done();
|
|
71
52
|
return;
|
|
72
53
|
}
|
|
73
|
-
|
|
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(
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
|
39
|
-
|
|
35
|
+
if (isCsrfTokenError(error)) {
|
|
36
|
+
void reply.status(403).send({
|
|
40
37
|
code: "CSRF_TOKEN_INVALID",
|
|
41
|
-
message: error.message
|
|
42
|
-
})
|
|
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
|
-
|
|
45
|
+
void reply.status(500).send({
|
|
49
46
|
code: "INTERNAL_SERVER_ERROR",
|
|
50
47
|
message: "Internal Server Error"
|
|
51
|
-
})
|
|
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,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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 {
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
26
|
+
response.write(serializeServerEvent(event));
|
|
25
27
|
});
|
|
26
28
|
const keepAliveTimer = setInterval(() => {
|
|
27
|
-
|
|
29
|
+
response.write(": keepalive\n\n");
|
|
28
30
|
}, KEEP_ALIVE_INTERVAL_MS);
|
|
29
31
|
keepAliveTimer.unref?.();
|
|
30
32
|
const sessionExpiryTimer = setTimeout(() => {
|
|
31
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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);
|
package/server/dist/paths.js
CHANGED
|
@@ -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(
|
|
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)
|