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.
- 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-DH3eSSRs.js} +1 -1
- package/server/client/dist/assets/{Note-CGI-Y1__.js → Note-CpzLtGVM.js} +1 -1
- package/server/client/dist/assets/{Search-DAYHxAml.js → Search-D9detGWk.js} +1 -1
- package/server/client/dist/assets/{Tag-CG8b0l1j.js → Tag-Bquqtx23.js} +1 -1
- package/server/client/dist/assets/{TagNotes-COyaVsdn.js → TagNotes-CbjfxqAN.js} +1 -1
- package/server/client/dist/assets/{Views-BPxlw3vk.js → Views-L4eC96Lx.js} +1 -1
- package/server/client/dist/assets/{app-BezcjTfd.js → app-BFM2V_Rx.js} +2 -2
- package/server/client/dist/assets/{index-CzZFedXj.js → index-CJPl3MJE.js} +1 -1
- package/server/client/dist/assets/{manage-image-detail-9hRTiAsR.js → manage-image-detail-BIDHUR5-.js} +1 -1
- package/server/client/dist/assets/mcp-D8footel.js +5 -0
- package/server/client/dist/assets/{note-core-DUTqUh5N.js → note-core-YhYEmI_R.js} +1 -1
- package/server/client/dist/assets/note-runtime-B3ats-dC.js +153 -0
- package/server/client/dist/assets/{note-ui-BaqzP-YU.js → note-ui-BtzPk8gA.js} +1 -1
- package/server/client/dist/assets/{placeholder-CZoqs4S3.js → placeholder-DKgxoe_f.js} +1 -1
- package/server/client/dist/assets/route-preload.js +2 -2
- 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/client/dist/assets/note-runtime-zN5ddTba.js +0 -168
- package/server/dist/modules/logger.js +0 -53
- 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,
|
|
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
|
-
|
|
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,
|
|
12
|
+
return async (_req, reply) => {
|
|
13
13
|
const status = await createMcpAdminStatusResponse(service);
|
|
14
|
-
|
|
14
|
+
return reply.status(200).send(status);
|
|
15
15
|
};
|
|
16
16
|
};
|
|
17
17
|
const createMcpAdminSetEnabledHandler = (service = createMcpAdminService()) => {
|
|
18
|
-
return async (req,
|
|
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
|
-
|
|
25
|
+
return reply.status(200).send(status);
|
|
26
26
|
};
|
|
27
27
|
};
|
|
28
28
|
const createMcpAdminRotateTokenHandler = (service = createMcpAdminService()) => {
|
|
29
|
-
return async (_req,
|
|
29
|
+
return async (_req, reply) => {
|
|
30
30
|
const result = await service.rotateToken();
|
|
31
|
-
|
|
31
|
+
return reply.status(200).send({
|
|
32
32
|
token: result.token,
|
|
33
33
|
message: "Save this token now. It is shown only once."
|
|
34
|
-
})
|
|
34
|
+
});
|
|
35
35
|
};
|
|
36
36
|
};
|
|
37
37
|
const createMcpAdminRevokeTokenHandler = (service = createMcpAdminService()) => {
|
|
38
|
-
return async (_req,
|
|
38
|
+
return async (_req, reply) => {
|
|
39
39
|
await service.revokeActiveToken();
|
|
40
40
|
const status = await createMcpAdminStatusResponse(service);
|
|
41
|
-
|
|
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,
|
|
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
|
-
|
|
254
|
+
return reply.status(200).send({
|
|
255
255
|
created: true,
|
|
256
256
|
note
|
|
257
|
-
})
|
|
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,
|
|
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
|
-
|
|
279
|
+
return reply.status(200).send({
|
|
280
280
|
note: serializeNoteWriteBaseline(note)
|
|
281
|
-
})
|
|
281
|
+
});
|
|
282
282
|
};
|
|
283
283
|
};
|
|
284
284
|
const createMcpPatchNoteMarkdownHandler = (patchMarkdown = patchNoteMarkdown, emitEvent = emitServerEvent) => {
|
|
285
|
-
return async (req,
|
|
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
|
-
|
|
318
|
+
return reply.status(200).send(result);
|
|
319
319
|
};
|
|
320
320
|
};
|
|
321
321
|
const createMcpAppendNoteMarkdownHandler = (appendMarkdown = appendNoteMarkdown, emitEvent = emitServerEvent) => {
|
|
322
|
-
return async (req,
|
|
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
|
-
|
|
359
|
+
return reply.status(200).send(result);
|
|
360
360
|
};
|
|
361
361
|
};
|
|
362
362
|
const createMcpReplaceNoteMarkdownHandler = (replaceMarkdown = replaceNoteMarkdown, emitEvent = emitServerEvent) => {
|
|
363
|
-
return async (req,
|
|
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
|
-
|
|
398
|
+
return reply.status(200).send(result);
|
|
399
399
|
};
|
|
400
400
|
};
|
|
401
401
|
const createMcpUpdateNoteMetadataHandler = (updateMetadata = updateNoteMetadata, emitEvent = emitServerEvent) => {
|
|
402
|
-
return async (req,
|
|
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
|
-
|
|
434
|
+
return reply.status(200).send(result);
|
|
435
435
|
};
|
|
436
436
|
};
|
|
437
437
|
const createMcpDeleteNoteHandler = (deleteNote = deleteNoteById, emitEvent = emitServerEvent) => {
|
|
438
|
-
return async (req,
|
|
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
|
-
|
|
452
|
+
return reply.status(200).send({
|
|
453
453
|
deleted: true,
|
|
454
454
|
note: deletedNote
|
|
455
|
-
})
|
|
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,
|
|
74
|
-
|
|
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,
|
|
78
|
+
return async (req, reply) => {
|
|
79
79
|
const status = await manager.saveConfig(parseConfig(req.body), parseApiKeyInput(req.body));
|
|
80
|
-
|
|
80
|
+
return reply.status(200).send(status);
|
|
81
81
|
};
|
|
82
82
|
};
|
|
83
83
|
const createSearchAdminTestConnectionHandler = (manager = getDefaultSemanticSearchManager()) => {
|
|
84
|
-
return async (req,
|
|
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
|
-
|
|
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,
|
|
91
|
+
return async (req, reply) => {
|
|
92
92
|
const models = await listModels(parseBaseUrl(req.body), parseApiKeyInput(req.body));
|
|
93
|
-
|
|
93
|
+
return reply.status(200).send({ models });
|
|
94
94
|
};
|
|
95
95
|
};
|
|
96
96
|
const createSearchAdminReindexHandler = (manager = getDefaultSemanticSearchManager()) => {
|
|
97
|
-
return async (_req,
|
|
97
|
+
return async (_req, reply) => {
|
|
98
98
|
const result = await manager.startReindex();
|
|
99
|
-
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
};
|