ocean-brain 0.2.2 → 0.3.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/dist/index.js +73 -41
- package/dist/mcp.js +587 -26
- package/package.json +6 -1
- package/scripts/postinstall-prisma.mjs +41 -0
- package/server/client/dist/assets/Calendar-DynpmB3D.js +25 -0
- package/server/client/dist/assets/Callout-tNHgPhLM.js +1 -0
- package/server/client/dist/assets/Graph-DMXLUfCv.js +1 -0
- package/server/client/dist/assets/Image-ZxF459sC.js +1 -0
- package/server/client/dist/assets/Image.es-Dnn4SMzG.js +1 -0
- package/server/client/dist/assets/ModalActionRow-B3vNjcDc.js +1 -0
- package/server/client/dist/assets/Note-PDMuj62M.js +1 -0
- package/server/client/dist/assets/Note-PEevDKBh.css +1 -0
- package/server/client/dist/assets/Reminders-ComtTLuN.js +1 -0
- package/server/client/dist/assets/{Search-BANPp2ZV.js → Search-BJeZTbVu.js} +1 -1
- package/server/client/dist/assets/SurfaceCard-DAcVMY4f.js +1 -0
- package/server/client/dist/assets/Tag-BuNCDGD5.js +1 -0
- package/server/client/dist/assets/TagNotes-ajFOP5pZ.js +1 -0
- package/server/client/dist/assets/Trash.es-DLiHgjNo.js +1 -0
- package/server/client/dist/assets/{image.api-rBH0OSWJ.js → image.api-CjJgNbC0.js} +1 -1
- package/server/client/dist/assets/index-CDrZmLC2.js +1 -0
- package/server/client/dist/assets/index-DAZMX8bQ.css +1 -0
- package/server/client/dist/assets/index-DqFkOWbi.js +205 -0
- package/server/client/dist/assets/manage-image-CVyZTDT_.js +1 -0
- package/server/client/dist/assets/manage-image-detail-Bp17tylS.js +1 -0
- package/server/client/dist/assets/mcp-BpD7VH-d.js +1 -0
- package/server/client/dist/assets/placeholder-DSf4QF4v.js +34 -0
- package/server/client/dist/assets/trash-Ceb17h_s.js +1 -0
- package/server/client/dist/assets/useReminderMutate-CzyOQ_VB.js +82 -0
- package/server/client/dist/index.html +2 -2
- package/server/dist/app.js +97 -9
- package/server/dist/app.js.map +1 -1
- package/server/dist/main.js +16 -2
- package/server/dist/main.js.map +1 -1
- package/server/dist/modules/auth-guard.js +73 -0
- package/server/dist/modules/auth-guard.js.map +1 -0
- package/server/dist/modules/auth-mode.js +51 -0
- package/server/dist/modules/auth-mode.js.map +1 -0
- package/server/dist/modules/auth.js +9 -0
- package/server/dist/modules/auth.js.map +1 -1
- package/server/dist/modules/blocknote.js +154 -2
- package/server/dist/modules/blocknote.js.map +1 -1
- package/server/dist/modules/error-handler.js +41 -0
- package/server/dist/modules/error-handler.js.map +1 -0
- package/server/dist/modules/image-upload.js +114 -0
- package/server/dist/modules/image-upload.js.map +1 -0
- package/server/dist/modules/mcp-admin.js +88 -0
- package/server/dist/modules/mcp-admin.js.map +1 -0
- package/server/dist/modules/mcp-auth.js +71 -0
- package/server/dist/modules/mcp-auth.js.map +1 -0
- package/server/dist/modules/mcp-token.js +28 -0
- package/server/dist/modules/mcp-token.js.map +1 -0
- package/server/dist/modules/note-authoring.js +153 -0
- package/server/dist/modules/note-authoring.js.map +1 -0
- package/server/dist/modules/note-cleanup.js +207 -0
- package/server/dist/modules/note-cleanup.js.map +1 -0
- package/server/dist/modules/note-snapshot.js +223 -0
- package/server/dist/modules/note-snapshot.js.map +1 -0
- package/server/dist/modules/note-trash.js +262 -0
- package/server/dist/modules/note-trash.js.map +1 -0
- package/server/dist/modules/prisma-runtime.js +46 -0
- package/server/dist/modules/prisma-runtime.js.map +1 -0
- package/server/dist/modules/recovery-retention.js +26 -0
- package/server/dist/modules/recovery-retention.js.map +1 -0
- package/server/dist/modules/remote-image.js +176 -0
- package/server/dist/modules/remote-image.js.map +1 -0
- package/server/dist/modules/tag-organization.js +66 -0
- package/server/dist/modules/tag-organization.js.map +1 -0
- package/server/dist/modules/use-async.js +2 -4
- package/server/dist/modules/use-async.js.map +1 -1
- package/server/dist/schema/note/index.js +150 -6
- package/server/dist/schema/note/index.js.map +1 -1
- package/server/dist/start.js +11 -0
- package/server/dist/start.js.map +1 -0
- package/server/dist/types/index.js.map +1 -1
- package/server/dist/urls.js +3 -2
- package/server/dist/urls.js.map +1 -1
- package/server/dist/views/auth.js +305 -0
- package/server/dist/views/auth.js.map +1 -0
- package/server/dist/views/image.js +24 -81
- package/server/dist/views/image.js.map +1 -1
- package/server/dist/views/index.js +4 -0
- package/server/dist/views/index.js.map +1 -1
- package/server/dist/views/mcp-admin.js +42 -0
- package/server/dist/views/mcp-admin.js.map +1 -0
- package/server/dist/views/note.js +124 -0
- package/server/dist/views/note.js.map +1 -0
- package/server/dist/views/tag.js +23 -0
- package/server/dist/views/tag.js.map +1 -0
- package/server/prisma/migrations/20260331183000_0010_note_snapshot/migration.sql +13 -0
- package/server/prisma/migrations/20260331195000_0011_note_trash_entities/migration.sql +35 -0
- package/server/prisma/migrations/20260331213000_0012_image_hash_unique/migration.sql +49 -0
- package/server/prisma/migrations/20260404000000_0013_mcp_admin_control/migration.sql +9 -0
- package/server/prisma/schema.prisma +66 -1
- package/server/client/dist/assets/Calendar-ST5q_8KT.js +0 -48
- package/server/client/dist/assets/Callout-CrWW3bg0.js +0 -1
- package/server/client/dist/assets/Graph-CzJjt6rP.js +0 -1
- package/server/client/dist/assets/Image-Dxg1qqZw.js +0 -1
- package/server/client/dist/assets/Image.es-DZ0H6wH1.js +0 -1
- package/server/client/dist/assets/Note-Bu7fN3B6.js +0 -6
- package/server/client/dist/assets/Note-CCvXqPzv.css +0 -1
- package/server/client/dist/assets/Plus.es-CS3WVMDq.js +0 -1
- package/server/client/dist/assets/PriorityLegend-D61iBwNW.js +0 -1
- package/server/client/dist/assets/Reminders-BjirJMOR.css +0 -1
- package/server/client/dist/assets/Reminders-DjWRombU.js +0 -1
- package/server/client/dist/assets/Tag-DbKSLTj0.js +0 -1
- package/server/client/dist/assets/TagNotes-B6s7jtL7.js +0 -1
- package/server/client/dist/assets/index-BzWwEDoL.js +0 -1
- package/server/client/dist/assets/index-CF5_ZCYN.js +0 -162
- package/server/client/dist/assets/index-DjZk2Gpg.css +0 -1
- package/server/client/dist/assets/manage-image-detail-DOx6QsCl.js +0 -1
- package/server/client/dist/assets/manage-image-g-MC6Fkc.js +0 -1
- package/server/client/dist/assets/placeholder-DV3u6MrA.js +0 -34
- package/server/client/dist/assets/useReminderMutate-CU-UinGn.js +0 -82
|
@@ -1,4 +1,33 @@
|
|
|
1
1
|
import { ServerBlockNoteEditor } from "@blocknote/server-util";
|
|
2
|
+
import models from "../models.js";
|
|
3
|
+
import { ensureTagByName } from "./tag-organization.js";
|
|
4
|
+
const UNSUPPORTED_MARKDOWN_BLOCK_TYPES = /* @__PURE__ */ new Set([
|
|
5
|
+
"tableOfContents"
|
|
6
|
+
]);
|
|
7
|
+
const defaultMarkdownImportDeps = {
|
|
8
|
+
ensureTag: async (name) => ensureTagByName(name),
|
|
9
|
+
findNotesByTitle: async (title) => {
|
|
10
|
+
const notes = await models.note.findMany({
|
|
11
|
+
select: {
|
|
12
|
+
id: true,
|
|
13
|
+
title: true
|
|
14
|
+
},
|
|
15
|
+
where: { title },
|
|
16
|
+
orderBy: { createdAt: "asc" },
|
|
17
|
+
take: 2
|
|
18
|
+
});
|
|
19
|
+
return notes.map((note) => ({
|
|
20
|
+
id: String(note.id),
|
|
21
|
+
title: note.title
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
function stripUnsupportedMarkdownBlocks(blocks) {
|
|
26
|
+
return blocks.filter((block) => !UNSUPPORTED_MARKDOWN_BLOCK_TYPES.has(block.type)).map((block) => ({
|
|
27
|
+
...block,
|
|
28
|
+
children: block.children?.length ? stripUnsupportedMarkdownBlocks(block.children) : []
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
2
31
|
function preprocessCustomInlineContent(blocks) {
|
|
3
32
|
return blocks.map((block) => ({
|
|
4
33
|
...block,
|
|
@@ -23,6 +52,116 @@ function preprocessCustomInlineContent(blocks) {
|
|
|
23
52
|
children: block.children?.length ? preprocessCustomInlineContent(block.children) : []
|
|
24
53
|
}));
|
|
25
54
|
}
|
|
55
|
+
function createTextInline(text, styles = {}) {
|
|
56
|
+
return {
|
|
57
|
+
type: "text",
|
|
58
|
+
text,
|
|
59
|
+
styles
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function appendInline(target, inline) {
|
|
63
|
+
const previous = target.at(-1);
|
|
64
|
+
if (previous?.type === "text" && inline.type === "text" && previous.text !== void 0 && inline.text !== void 0 && JSON.stringify(previous.styles ?? {}) === JSON.stringify(inline.styles ?? {})) {
|
|
65
|
+
previous.text += inline.text;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
target.push(inline);
|
|
69
|
+
}
|
|
70
|
+
async function restoreCustomInlineContent(blocks, deps) {
|
|
71
|
+
const tagCache = /* @__PURE__ */ new Map();
|
|
72
|
+
const noteCache = /* @__PURE__ */ new Map();
|
|
73
|
+
const getTag = (token) => {
|
|
74
|
+
let existing = tagCache.get(token);
|
|
75
|
+
if (!existing) {
|
|
76
|
+
existing = deps.ensureTag(token.slice(1)).then((result) => ({
|
|
77
|
+
id: "tag" in result ? result.tag.id : result.id,
|
|
78
|
+
tag: "tag" in result ? result.tag.name : result.name
|
|
79
|
+
}));
|
|
80
|
+
tagCache.set(token, existing);
|
|
81
|
+
}
|
|
82
|
+
return existing;
|
|
83
|
+
};
|
|
84
|
+
const getNotesByTitle = (title) => {
|
|
85
|
+
let existing = noteCache.get(title);
|
|
86
|
+
if (!existing) {
|
|
87
|
+
existing = deps.findNotesByTitle(title);
|
|
88
|
+
noteCache.set(title, existing);
|
|
89
|
+
}
|
|
90
|
+
return existing;
|
|
91
|
+
};
|
|
92
|
+
const restoreTextInline = async (inline) => {
|
|
93
|
+
if (inline.type !== "text" || typeof inline.text !== "string" || !inline.text || inline.styles?.code === true) {
|
|
94
|
+
return [inline];
|
|
95
|
+
}
|
|
96
|
+
const restored = [];
|
|
97
|
+
const styles = inline.styles ?? {};
|
|
98
|
+
let cursor = 0;
|
|
99
|
+
while (cursor < inline.text.length) {
|
|
100
|
+
if (inline.text.startsWith("[[", cursor)) {
|
|
101
|
+
const closeIndex = inline.text.indexOf("]]", cursor + 2);
|
|
102
|
+
if (closeIndex !== -1) {
|
|
103
|
+
const title = inline.text.slice(cursor + 2, closeIndex);
|
|
104
|
+
const matchingNotes = await getNotesByTitle(title);
|
|
105
|
+
if (matchingNotes.length === 1) {
|
|
106
|
+
appendInline(restored, {
|
|
107
|
+
type: "reference",
|
|
108
|
+
props: {
|
|
109
|
+
id: matchingNotes[0].id,
|
|
110
|
+
title: matchingNotes[0].title
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
cursor = closeIndex + 2;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (inline.text[cursor] === "#" && (cursor === 0 || /\s/.test(inline.text[cursor - 1]))) {
|
|
119
|
+
const remainder = inline.text.slice(cursor);
|
|
120
|
+
const tagMatch = remainder.match(/^#[^\s[\]]+/);
|
|
121
|
+
if (tagMatch) {
|
|
122
|
+
const tag = await getTag(tagMatch[0]);
|
|
123
|
+
appendInline(restored, {
|
|
124
|
+
type: "tag",
|
|
125
|
+
props: tag
|
|
126
|
+
});
|
|
127
|
+
cursor += tagMatch[0].length;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
let nextCursor = cursor + 1;
|
|
132
|
+
while (nextCursor < inline.text.length && !inline.text.startsWith("[[", nextCursor) && !(inline.text[nextCursor] === "#" && /\s/.test(inline.text[nextCursor - 1] ?? ""))) {
|
|
133
|
+
nextCursor += 1;
|
|
134
|
+
}
|
|
135
|
+
appendInline(restored, createTextInline(inline.text.slice(cursor, nextCursor), styles));
|
|
136
|
+
cursor = nextCursor;
|
|
137
|
+
}
|
|
138
|
+
return restored;
|
|
139
|
+
};
|
|
140
|
+
return Promise.all(
|
|
141
|
+
blocks.map(async (block) => ({
|
|
142
|
+
...block,
|
|
143
|
+
content: block.content ? (await Promise.all(block.content.map((inline) => restoreTextInline(inline)))).flat() : void 0,
|
|
144
|
+
children: block.children?.length ? await restoreCustomInlineContent(block.children, deps) : []
|
|
145
|
+
}))
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
function collectTagIds(blocks) {
|
|
149
|
+
const tagIds = /* @__PURE__ */ new Set();
|
|
150
|
+
const visit = (items) => {
|
|
151
|
+
for (const block of items) {
|
|
152
|
+
for (const inline of block.content ?? []) {
|
|
153
|
+
if (inline.type === "tag" && typeof inline.props?.id === "string") {
|
|
154
|
+
tagIds.add(inline.props.id);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (block.children?.length) {
|
|
158
|
+
visit(block.children);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
visit(blocks);
|
|
163
|
+
return [...tagIds];
|
|
164
|
+
}
|
|
26
165
|
let editorInstance = null;
|
|
27
166
|
function getEditor() {
|
|
28
167
|
if (!editorInstance) {
|
|
@@ -33,14 +172,27 @@ function getEditor() {
|
|
|
33
172
|
async function blocksToMarkdown(contentJson) {
|
|
34
173
|
try {
|
|
35
174
|
const blocks = JSON.parse(contentJson);
|
|
36
|
-
const
|
|
175
|
+
const supportedBlocks = stripUnsupportedMarkdownBlocks(blocks);
|
|
176
|
+
const processed = preprocessCustomInlineContent(supportedBlocks);
|
|
37
177
|
const editor = getEditor();
|
|
38
178
|
return await editor.blocksToMarkdownLossy(processed);
|
|
39
179
|
} catch {
|
|
40
180
|
return "";
|
|
41
181
|
}
|
|
42
182
|
}
|
|
183
|
+
async function markdownToBlocksJson(markdown, deps = defaultMarkdownImportDeps) {
|
|
184
|
+
const editor = getEditor();
|
|
185
|
+
const blocks = await editor.tryParseMarkdownToBlocks(markdown);
|
|
186
|
+
const restoredBlocks = await restoreCustomInlineContent(blocks, deps);
|
|
187
|
+
return JSON.stringify(restoredBlocks);
|
|
188
|
+
}
|
|
189
|
+
function extractTagIdsFromContentJson(contentJson) {
|
|
190
|
+
const blocks = JSON.parse(contentJson);
|
|
191
|
+
return collectTagIds(blocks);
|
|
192
|
+
}
|
|
43
193
|
export {
|
|
44
|
-
blocksToMarkdown
|
|
194
|
+
blocksToMarkdown,
|
|
195
|
+
extractTagIdsFromContentJson,
|
|
196
|
+
markdownToBlocksJson
|
|
45
197
|
};
|
|
46
198
|
//# sourceMappingURL=blocknote.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/modules/blocknote.ts"],"sourcesContent":["import { ServerBlockNoteEditor } from '@blocknote/server-util';\n\ninterface BlockNote {\n id?: string;\n type: string;\n props?: Record<string, unknown>;\n content?: BlockNote[];\n children?: BlockNote[];\n text?: string;\n styles?: Record<string, unknown>;\n}\n\nfunction preprocessCustomInlineContent(blocks: BlockNote[]): BlockNote[] {\n return blocks.map((block) => ({\n ...block,\n content: block.content?.map((inline) => {\n if (inline.type === 'reference') {\n return {\n type: 'text',\n text: `[[${inline.props?.title || inline.props?.id || ''}]]`,\n styles: {}\n };\n }\n if (inline.type === 'tag') {\n const tag = (inline.props?.tag as string)?.replace(/^@/, '') || '';\n return {\n type: 'text',\n text: `#${tag}`,\n styles: {}\n };\n }\n return inline;\n }),\n children: block.children?.length\n ? preprocessCustomInlineContent(block.children)\n : []\n }));\n}\n\nlet editorInstance: ServerBlockNoteEditor | null = null;\n\nfunction getEditor(): ServerBlockNoteEditor {\n if (!editorInstance) {\n editorInstance = ServerBlockNoteEditor.create();\n }\n return editorInstance;\n}\n\nexport async function blocksToMarkdown(contentJson: string): Promise<string> {\n try {\n const blocks = JSON.parse(contentJson);\n const processed = preprocessCustomInlineContent(blocks);\n const editor = getEditor();\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return await editor.blocksToMarkdownLossy(processed as any);\n } catch {\n return '';\n }\n}\n"],"mappings":"AAAA,SAAS,6BAA6B;AAYtC,SAAS,8BAA8B,QAAkC;AACrE,SAAO,OAAO,IAAI,CAAC,WAAW;AAAA,IAC1B,GAAG;AAAA,IACH,SAAS,MAAM,SAAS,IAAI,CAAC,WAAW;AACpC,UAAI,OAAO,SAAS,aAAa;AAC7B,eAAO;AAAA,UACH,MAAM;AAAA,UACN,MAAM,KAAK,OAAO,OAAO,SAAS,OAAO,OAAO,MAAM,EAAE;AAAA,UACxD,QAAQ,CAAC;AAAA,QACb;AAAA,MACJ;AACA,UAAI,OAAO,SAAS,OAAO;AACvB,cAAM,MAAO,OAAO,OAAO,KAAgB,QAAQ,MAAM,EAAE,KAAK;AAChE,eAAO;AAAA,UACH,MAAM;AAAA,UACN,MAAM,IAAI,GAAG;AAAA,UACb,QAAQ,CAAC;AAAA,QACb;AAAA,MACJ;AACA,aAAO;AAAA,IACX,CAAC;AAAA,IACD,UAAU,MAAM,UAAU,SACpB,8BAA8B,MAAM,QAAQ,IAC5C,CAAC;AAAA,EACX,EAAE;AACN;AAEA,IAAI,iBAA+C;AAEnD,SAAS,YAAmC;AACxC,MAAI,CAAC,gBAAgB;AACjB,qBAAiB,sBAAsB,OAAO;AAAA,EAClD;AACA,SAAO;AACX;AAEA,eAAsB,iBAAiB,aAAsC;AACzE,MAAI;AACA,UAAM,SAAS,KAAK,MAAM,WAAW;AACrC,UAAM,YAAY,8BAA8B,MAAM;AACtD,UAAM,SAAS,UAAU;AAEzB,WAAO,MAAM,OAAO,sBAAsB,SAAgB;AAAA,EAC9D,QAAQ;AACJ,WAAO;AAAA,EACX;AACJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/modules/blocknote.ts"],"sourcesContent":["import { ServerBlockNoteEditor } from '@blocknote/server-util';\n\nimport models from '~/models.js';\nimport { ensureTagByName } from './tag-organization.js';\n\ninterface BlockNote {\n id?: string;\n type: string;\n props?: Record<string, unknown>;\n content?: BlockNoteInline[];\n children?: BlockNote[];\n text?: string;\n styles?: Record<string, unknown>;\n}\n\ninterface BlockNoteInline {\n type: string;\n props?: Record<string, unknown>;\n text?: string;\n styles?: Record<string, unknown>;\n}\n\ninterface MarkdownImportDeps {\n ensureTag: (name: string) => Promise<\n | {\n id: string;\n name: string;\n }\n | {\n tag: {\n id: string;\n name: string;\n };\n }\n >;\n findNotesByTitle: (title: string) => Promise<Array<{\n id: string;\n title: string;\n }>>;\n}\n\nconst UNSUPPORTED_MARKDOWN_BLOCK_TYPES = new Set([\n 'tableOfContents'\n]);\n\nconst defaultMarkdownImportDeps: MarkdownImportDeps = {\n ensureTag: async (name) => ensureTagByName(name),\n findNotesByTitle: async (title) => {\n const notes = await models.note.findMany({\n select: {\n id: true,\n title: true\n },\n where: { title },\n orderBy: { createdAt: 'asc' },\n take: 2\n });\n\n return notes.map((note) => ({\n id: String(note.id),\n title: note.title\n }));\n }\n};\n\nfunction stripUnsupportedMarkdownBlocks(blocks: BlockNote[]): BlockNote[] {\n return blocks\n .filter((block) => !UNSUPPORTED_MARKDOWN_BLOCK_TYPES.has(block.type))\n .map((block) => ({\n ...block,\n children: block.children?.length\n ? stripUnsupportedMarkdownBlocks(block.children)\n : []\n }));\n}\n\nfunction preprocessCustomInlineContent(blocks: BlockNote[]): BlockNote[] {\n return blocks.map((block) => ({\n ...block,\n content: block.content?.map((inline) => {\n if (inline.type === 'reference') {\n return {\n type: 'text',\n text: `[[${inline.props?.title || inline.props?.id || ''}]]`,\n styles: {}\n };\n }\n if (inline.type === 'tag') {\n const tag = (inline.props?.tag as string)?.replace(/^@/, '') || '';\n return {\n type: 'text',\n text: `#${tag}`,\n styles: {}\n };\n }\n return inline;\n }),\n children: block.children?.length\n ? preprocessCustomInlineContent(block.children)\n : []\n }));\n}\n\nfunction createTextInline(text: string, styles: Record<string, unknown> = {}): BlockNoteInline {\n return {\n type: 'text',\n text,\n styles\n };\n}\n\nfunction appendInline(target: BlockNoteInline[], inline: BlockNoteInline) {\n const previous = target.at(-1);\n\n if (\n previous?.type === 'text' &&\n inline.type === 'text' &&\n previous.text !== undefined &&\n inline.text !== undefined &&\n JSON.stringify(previous.styles ?? {}) === JSON.stringify(inline.styles ?? {})\n ) {\n previous.text += inline.text;\n return;\n }\n\n target.push(inline);\n}\n\nasync function restoreCustomInlineContent(\n blocks: BlockNote[],\n deps: MarkdownImportDeps\n): Promise<BlockNote[]> {\n const tagCache = new Map<string, Promise<{ id: string; tag: string }>>();\n const noteCache = new Map<string, Promise<Array<{ id: string; title: string }>>>();\n\n const getTag = (token: string) => {\n let existing = tagCache.get(token);\n\n if (!existing) {\n existing = deps.ensureTag(token.slice(1)).then((result) => ({\n id: 'tag' in result ? result.tag.id : result.id,\n tag: 'tag' in result ? result.tag.name : result.name\n }));\n tagCache.set(token, existing);\n }\n\n return existing;\n };\n\n const getNotesByTitle = (title: string) => {\n let existing = noteCache.get(title);\n\n if (!existing) {\n existing = deps.findNotesByTitle(title);\n noteCache.set(title, existing);\n }\n\n return existing;\n };\n\n const restoreTextInline = async (inline: BlockNoteInline): Promise<BlockNoteInline[]> => {\n if (\n inline.type !== 'text' ||\n typeof inline.text !== 'string' ||\n !inline.text ||\n inline.styles?.code === true\n ) {\n return [inline];\n }\n\n const restored: BlockNoteInline[] = [];\n const styles = inline.styles ?? {};\n let cursor = 0;\n\n while (cursor < inline.text.length) {\n if (\n inline.text.startsWith('[[', cursor)\n ) {\n const closeIndex = inline.text.indexOf(']]', cursor + 2);\n\n if (closeIndex !== -1) {\n const title = inline.text.slice(cursor + 2, closeIndex);\n const matchingNotes = await getNotesByTitle(title);\n\n if (matchingNotes.length === 1) {\n appendInline(restored, {\n type: 'reference',\n props: {\n id: matchingNotes[0].id,\n title: matchingNotes[0].title\n }\n });\n cursor = closeIndex + 2;\n continue;\n }\n }\n }\n\n if (\n inline.text[cursor] === '#' &&\n (cursor === 0 || /\\s/.test(inline.text[cursor - 1]))\n ) {\n const remainder = inline.text.slice(cursor);\n const tagMatch = remainder.match(/^#[^\\s[\\]]+/);\n\n if (tagMatch) {\n const tag = await getTag(tagMatch[0]);\n appendInline(restored, {\n type: 'tag',\n props: tag\n });\n cursor += tagMatch[0].length;\n continue;\n }\n }\n\n let nextCursor = cursor + 1;\n\n while (\n nextCursor < inline.text.length &&\n !inline.text.startsWith('[[', nextCursor) &&\n !(\n inline.text[nextCursor] === '#' &&\n /\\s/.test(inline.text[nextCursor - 1] ?? '')\n )\n ) {\n nextCursor += 1;\n }\n\n appendInline(restored, createTextInline(inline.text.slice(cursor, nextCursor), styles));\n cursor = nextCursor;\n }\n\n return restored;\n };\n\n return Promise.all(\n blocks.map(async (block) => ({\n ...block,\n content: block.content\n ? (await Promise.all(block.content.map((inline) => restoreTextInline(inline)))).flat()\n : undefined,\n children: block.children?.length\n ? await restoreCustomInlineContent(block.children, deps)\n : []\n }))\n );\n}\n\nfunction collectTagIds(blocks: BlockNote[]): string[] {\n const tagIds = new Set<string>();\n\n const visit = (items: BlockNote[]) => {\n for (const block of items) {\n for (const inline of block.content ?? []) {\n if (inline.type === 'tag' && typeof inline.props?.id === 'string') {\n tagIds.add(inline.props.id);\n }\n }\n\n if (block.children?.length) {\n visit(block.children);\n }\n }\n };\n\n visit(blocks);\n return [...tagIds];\n}\n\nlet editorInstance: ServerBlockNoteEditor | null = null;\n\nfunction getEditor(): ServerBlockNoteEditor {\n if (!editorInstance) {\n editorInstance = ServerBlockNoteEditor.create();\n }\n return editorInstance;\n}\n\nexport async function blocksToMarkdown(contentJson: string): Promise<string> {\n try {\n const blocks = JSON.parse(contentJson);\n const supportedBlocks = stripUnsupportedMarkdownBlocks(blocks);\n const processed = preprocessCustomInlineContent(supportedBlocks);\n const editor = getEditor();\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return await editor.blocksToMarkdownLossy(processed as any);\n } catch {\n return '';\n }\n}\n\nexport async function markdownToBlocksJson(\n markdown: string,\n deps: MarkdownImportDeps = defaultMarkdownImportDeps\n): Promise<string> {\n const editor = getEditor();\n const blocks = await editor.tryParseMarkdownToBlocks(markdown);\n const restoredBlocks = await restoreCustomInlineContent(blocks as BlockNote[], deps);\n return JSON.stringify(restoredBlocks);\n}\n\nexport function extractTagIdsFromContentJson(contentJson: string): string[] {\n const blocks = JSON.parse(contentJson) as BlockNote[];\n return collectTagIds(blocks);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
class AppError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
status;
|
|
4
|
+
constructor(status, code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "AppError";
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.code = code;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const createAppError = (status, code, message) => {
|
|
12
|
+
return new AppError(status, code, message);
|
|
13
|
+
};
|
|
14
|
+
const createErrorHandler = () => {
|
|
15
|
+
return (error, _req, res, next) => {
|
|
16
|
+
if (res.headersSent) {
|
|
17
|
+
next(error);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (error instanceof AppError) {
|
|
21
|
+
res.status(error.status).json({
|
|
22
|
+
code: error.code,
|
|
23
|
+
message: error.message
|
|
24
|
+
}).end();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const message = error instanceof Error ? error.stack || error.message : String(error);
|
|
28
|
+
process.stderr.write(`[error] ${message}
|
|
29
|
+
`);
|
|
30
|
+
res.status(500).json({
|
|
31
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
32
|
+
message: "Internal Server Error"
|
|
33
|
+
}).end();
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
export {
|
|
37
|
+
AppError,
|
|
38
|
+
createAppError,
|
|
39
|
+
createErrorHandler
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=error-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/modules/error-handler.ts"],"sourcesContent":["import type { ErrorRequestHandler } from 'express';\n\nexport class AppError extends Error {\n code: string;\n status: number;\n\n constructor(status: number, code: string, message: string) {\n super(message);\n this.name = 'AppError';\n this.status = status;\n this.code = code;\n }\n}\n\nexport const createAppError = (status: number, code: string, message: string) => {\n return new AppError(status, code, message);\n};\n\nexport const createErrorHandler = (): ErrorRequestHandler => {\n return (error, _req, res, next) => {\n if (res.headersSent) {\n next(error);\n return;\n }\n\n if (error instanceof AppError) {\n res.status(error.status).json({\n code: error.code,\n message: error.message\n }).end();\n return;\n }\n\n const message = error instanceof Error ? error.stack || error.message : String(error);\n process.stderr.write(`[error] ${message}\\n`);\n\n res.status(500).json({\n code: 'INTERNAL_SERVER_ERROR',\n message: 'Internal Server Error'\n }).end();\n };\n};\n"],"mappings":"AAEO,MAAM,iBAAiB,MAAM;AAAA,EAChC;AAAA,EACA;AAAA,EAEA,YAAY,QAAgB,MAAc,SAAiB;AACvD,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,OAAO;AAAA,EAChB;AACJ;AAEO,MAAM,iBAAiB,CAAC,QAAgB,MAAc,YAAoB;AAC7E,SAAO,IAAI,SAAS,QAAQ,MAAM,OAAO;AAC7C;AAEO,MAAM,qBAAqB,MAA2B;AACzD,SAAO,CAAC,OAAO,MAAM,KAAK,SAAS;AAC/B,QAAI,IAAI,aAAa;AACjB,WAAK,KAAK;AACV;AAAA,IACJ;AAEA,QAAI,iBAAiB,UAAU;AAC3B,UAAI,OAAO,MAAM,MAAM,EAAE,KAAK;AAAA,QAC1B,MAAM,MAAM;AAAA,QACZ,SAAS,MAAM;AAAA,MACnB,CAAC,EAAE,IAAI;AACP;AAAA,IACJ;AAEA,UAAM,UAAU,iBAAiB,QAAQ,MAAM,SAAS,MAAM,UAAU,OAAO,KAAK;AACpF,YAAQ,OAAO,MAAM,WAAW,OAAO;AAAA,CAAI;AAE3C,QAAI,OAAO,GAAG,EAAE,KAAK;AAAA,MACjB,MAAM;AAAA,MACN,SAAS;AAAA,IACb,CAAC,EAAE,IAAI;AAAA,EACX;AACJ;","names":[]}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import models from "../models.js";
|
|
5
|
+
import { paths } from "../paths.js";
|
|
6
|
+
const hashImageBuffer = (buffer) => {
|
|
7
|
+
return crypto.createHash("sha512").update(buffer).digest("hex");
|
|
8
|
+
};
|
|
9
|
+
const buildImageTarget = (hash, extension, now = /* @__PURE__ */ new Date()) => {
|
|
10
|
+
const normalizedExtension = extension.trim().toLowerCase().replace(/^\./, "");
|
|
11
|
+
const currentPath = [
|
|
12
|
+
now.getFullYear().toString(),
|
|
13
|
+
(now.getMonth() + 1).toString(),
|
|
14
|
+
now.getDate().toString()
|
|
15
|
+
];
|
|
16
|
+
const targetDir = path.resolve(paths.imageDir, ...currentPath);
|
|
17
|
+
const fileName = `${hash}.${normalizedExtension}`;
|
|
18
|
+
return {
|
|
19
|
+
targetDir,
|
|
20
|
+
absolutePath: path.resolve(targetDir, fileName),
|
|
21
|
+
url: `/assets/images/${currentPath.join("/")}/${fileName}`
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
const isImageHashUniqueConflict = (error) => {
|
|
25
|
+
if (typeof error !== "object" || error === null || !("code" in error)) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
if (error.code !== "P2002") {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if (!("meta" in error) || typeof error.meta !== "object" || error.meta === null) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
const rawTarget = "target" in error.meta ? error.meta.target : void 0;
|
|
35
|
+
const targets = Array.isArray(rawTarget) ? rawTarget.filter((value) => typeof value === "string") : typeof rawTarget === "string" ? [rawTarget] : [];
|
|
36
|
+
return targets.length === 0 || targets.includes("hash");
|
|
37
|
+
};
|
|
38
|
+
const removeFileIfPresent = async (deps, filePath) => {
|
|
39
|
+
try {
|
|
40
|
+
await deps.removeFile(filePath);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const createImageUploadService = (deps) => {
|
|
49
|
+
return {
|
|
50
|
+
persistImage: async (input) => {
|
|
51
|
+
const hash = hashImageBuffer(input.buffer);
|
|
52
|
+
const existingImage = await deps.findImageByHash(hash);
|
|
53
|
+
if (existingImage) {
|
|
54
|
+
return existingImage;
|
|
55
|
+
}
|
|
56
|
+
const target = buildImageTarget(hash, input.extension);
|
|
57
|
+
await deps.ensureDir(target.targetDir);
|
|
58
|
+
await deps.writeFile(target.absolutePath, input.buffer);
|
|
59
|
+
try {
|
|
60
|
+
return await deps.createImage({
|
|
61
|
+
hash,
|
|
62
|
+
url: target.url
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
await removeFileIfPresent(deps, target.absolutePath);
|
|
66
|
+
if (isImageHashUniqueConflict(error)) {
|
|
67
|
+
const conflictedImage = await deps.findImageByHash(hash);
|
|
68
|
+
if (conflictedImage) {
|
|
69
|
+
return conflictedImage;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
const defaultImageUploadService = createImageUploadService({
|
|
78
|
+
createImage: async (input) => {
|
|
79
|
+
return models.image.create({
|
|
80
|
+
data: input,
|
|
81
|
+
select: {
|
|
82
|
+
id: true,
|
|
83
|
+
url: true
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
ensureDir: async (dirPath) => {
|
|
88
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
89
|
+
},
|
|
90
|
+
findImageByHash: async (hash) => {
|
|
91
|
+
return models.image.findFirst({
|
|
92
|
+
where: { hash },
|
|
93
|
+
select: {
|
|
94
|
+
id: true,
|
|
95
|
+
url: true
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
removeFile: async (filePath) => {
|
|
100
|
+
await fs.unlink(filePath);
|
|
101
|
+
},
|
|
102
|
+
writeFile: async (filePath, buffer) => {
|
|
103
|
+
await fs.writeFile(filePath, buffer);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
const persistUploadedImage = async (input) => {
|
|
107
|
+
return defaultImageUploadService.persistImage(input);
|
|
108
|
+
};
|
|
109
|
+
export {
|
|
110
|
+
createImageUploadService,
|
|
111
|
+
hashImageBuffer,
|
|
112
|
+
persistUploadedImage
|
|
113
|
+
};
|
|
114
|
+
//# sourceMappingURL=image-upload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/modules/image-upload.ts"],"sourcesContent":["import crypto from 'crypto';\nimport fs from 'fs/promises';\nimport path from 'path';\n\nimport models from '~/models.js';\nimport { paths } from '~/paths.js';\n\ninterface ImageRecord {\n id: number;\n url: string;\n}\n\ninterface ImageUploadDeps {\n createImage: (input: {\n hash: string;\n url: string;\n }) => Promise<ImageRecord>;\n ensureDir: (dirPath: string) => Promise<void>;\n findImageByHash: (hash: string) => Promise<ImageRecord | null>;\n removeFile: (filePath: string) => Promise<void>;\n writeFile: (filePath: string, buffer: Buffer) => Promise<void>;\n}\n\nexport interface PersistImageInput {\n buffer: Buffer;\n extension: string;\n}\n\nexport interface PersistedImageSummary {\n id: number;\n url: string;\n}\n\nexport const hashImageBuffer = (buffer: Buffer) => {\n return crypto.createHash('sha512').update(buffer).digest('hex');\n};\n\nconst buildImageTarget = (hash: string, extension: string, now = new Date()) => {\n const normalizedExtension = extension.trim().toLowerCase().replace(/^\\./, '');\n const currentPath = [\n now.getFullYear().toString(),\n (now.getMonth() + 1).toString(),\n now.getDate().toString()\n ];\n const targetDir = path.resolve(paths.imageDir, ...currentPath);\n const fileName = `${hash}.${normalizedExtension}`;\n\n return {\n targetDir,\n absolutePath: path.resolve(targetDir, fileName),\n url: `/assets/images/${currentPath.join('/')}/${fileName}`\n };\n};\n\nconst isImageHashUniqueConflict = (error: unknown) => {\n if (typeof error !== 'object' || error === null || !('code' in error)) {\n return false;\n }\n\n if (error.code !== 'P2002') {\n return false;\n }\n\n if (!('meta' in error) || typeof error.meta !== 'object' || error.meta === null) {\n return true;\n }\n\n const rawTarget = 'target' in error.meta ? error.meta.target : undefined;\n const targets = Array.isArray(rawTarget)\n ? rawTarget.filter((value): value is string => typeof value === 'string')\n : (typeof rawTarget === 'string' ? [rawTarget] : []);\n\n return targets.length === 0 || targets.includes('hash');\n};\n\nconst removeFileIfPresent = async (deps: Pick<ImageUploadDeps, 'removeFile'>, filePath: string) => {\n try {\n await deps.removeFile(filePath);\n } catch (error) {\n if (\n typeof error === 'object' &&\n error !== null &&\n 'code' in error &&\n error.code === 'ENOENT'\n ) {\n return;\n }\n\n throw error;\n }\n};\n\nexport const createImageUploadService = (deps: ImageUploadDeps) => {\n return {\n persistImage: async (input: PersistImageInput): Promise<PersistedImageSummary> => {\n const hash = hashImageBuffer(input.buffer);\n const existingImage = await deps.findImageByHash(hash);\n\n if (existingImage) {\n return existingImage;\n }\n\n const target = buildImageTarget(hash, input.extension);\n\n await deps.ensureDir(target.targetDir);\n await deps.writeFile(target.absolutePath, input.buffer);\n\n try {\n return await deps.createImage({\n hash,\n url: target.url\n });\n } catch (error) {\n await removeFileIfPresent(deps, target.absolutePath);\n\n if (isImageHashUniqueConflict(error)) {\n const conflictedImage = await deps.findImageByHash(hash);\n\n if (conflictedImage) {\n return conflictedImage;\n }\n }\n\n throw error;\n }\n }\n };\n};\n\nconst defaultImageUploadService = createImageUploadService({\n createImage: async (input) => {\n return models.image.create({\n data: input,\n select: {\n id: true,\n url: true\n }\n });\n },\n ensureDir: async (dirPath) => {\n await fs.mkdir(dirPath, { recursive: true });\n },\n findImageByHash: async (hash) => {\n return models.image.findFirst({\n where: { hash },\n select: {\n id: true,\n url: true\n }\n });\n },\n removeFile: async (filePath) => {\n await fs.unlink(filePath);\n },\n writeFile: async (filePath, buffer) => {\n await fs.writeFile(filePath, buffer);\n }\n});\n\nexport const persistUploadedImage = async (input: PersistImageInput) => {\n return defaultImageUploadService.persistImage(input);\n};\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import models from "../models.js";
|
|
2
|
+
import { issueMcpToken, verifyMcpToken } from "./mcp-token.js";
|
|
3
|
+
const MCP_ENABLED_CACHE_KEY = "MCP_ENABLED";
|
|
4
|
+
const toStatus = (enabled, activeToken) => {
|
|
5
|
+
if (!activeToken) {
|
|
6
|
+
return {
|
|
7
|
+
enabled,
|
|
8
|
+
hasActiveToken: false,
|
|
9
|
+
token: null
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
enabled,
|
|
14
|
+
hasActiveToken: true,
|
|
15
|
+
token: {
|
|
16
|
+
id: String(activeToken.id),
|
|
17
|
+
createdAt: activeToken.createdAt.toISOString(),
|
|
18
|
+
lastUsedAt: activeToken.lastUsedAt ? activeToken.lastUsedAt.toISOString() : null
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
const createMcpAdminService = (db = models) => {
|
|
23
|
+
return {
|
|
24
|
+
async getStatus() {
|
|
25
|
+
const enabledCache = await db.cache.findUnique({ where: { key: MCP_ENABLED_CACHE_KEY } });
|
|
26
|
+
const activeToken = await db.mcpToken.findFirst({
|
|
27
|
+
where: { revokedAt: null },
|
|
28
|
+
orderBy: { createdAt: "desc" }
|
|
29
|
+
});
|
|
30
|
+
return toStatus(enabledCache?.value === "true", activeToken);
|
|
31
|
+
},
|
|
32
|
+
async setEnabled(enabled) {
|
|
33
|
+
await db.cache.upsert({
|
|
34
|
+
where: { key: MCP_ENABLED_CACHE_KEY },
|
|
35
|
+
update: { value: String(enabled) },
|
|
36
|
+
create: {
|
|
37
|
+
key: MCP_ENABLED_CACHE_KEY,
|
|
38
|
+
value: String(enabled)
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
async rotateToken() {
|
|
43
|
+
const now = /* @__PURE__ */ new Date();
|
|
44
|
+
const issued = issueMcpToken();
|
|
45
|
+
await db.$transaction(async (transaction) => {
|
|
46
|
+
await transaction.mcpToken.updateMany({
|
|
47
|
+
where: { revokedAt: null },
|
|
48
|
+
data: { revokedAt: now }
|
|
49
|
+
});
|
|
50
|
+
await transaction.mcpToken.create({ data: { tokenHash: issued.hash } });
|
|
51
|
+
});
|
|
52
|
+
return { token: issued.plaintext };
|
|
53
|
+
},
|
|
54
|
+
async revokeActiveToken() {
|
|
55
|
+
await db.mcpToken.updateMany({
|
|
56
|
+
where: { revokedAt: null },
|
|
57
|
+
data: { revokedAt: /* @__PURE__ */ new Date() }
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
async validatePresentedToken(token) {
|
|
61
|
+
const activeToken = await db.mcpToken.findFirst({
|
|
62
|
+
where: { revokedAt: null },
|
|
63
|
+
orderBy: { createdAt: "desc" }
|
|
64
|
+
});
|
|
65
|
+
if (!activeToken) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
reason: "not_configured"
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (!verifyMcpToken(activeToken.tokenHash, token)) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
reason: "forbidden"
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
await db.mcpToken.update({
|
|
78
|
+
where: { id: activeToken.id },
|
|
79
|
+
data: { lastUsedAt: /* @__PURE__ */ new Date() }
|
|
80
|
+
});
|
|
81
|
+
return { ok: true };
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
export {
|
|
86
|
+
createMcpAdminService
|
|
87
|
+
};
|
|
88
|
+
//# sourceMappingURL=mcp-admin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/modules/mcp-admin.ts"],"sourcesContent":["import models from '~/models.js';\nimport { issueMcpToken, verifyMcpToken } from './mcp-token.js';\n\nconst MCP_ENABLED_CACHE_KEY = 'MCP_ENABLED';\n\nexport interface McpTokenSummary {\n id: string;\n createdAt: string;\n lastUsedAt: string | null;\n}\n\nexport interface McpAdminStatus {\n enabled: boolean;\n hasActiveToken: boolean;\n token: McpTokenSummary | null;\n}\n\nexport interface McpTokenValidationResult {\n ok: boolean;\n reason?: 'not_configured' | 'forbidden';\n}\n\nexport interface McpAdminService {\n getStatus: () => Promise<McpAdminStatus>;\n setEnabled: (enabled: boolean) => Promise<void>;\n rotateToken: () => Promise<{ token: string }>;\n revokeActiveToken: () => Promise<void>;\n validatePresentedToken: (token: string) => Promise<McpTokenValidationResult>;\n}\n\ninterface McpAdminDb {\n cache: {\n findUnique: (args: { where: { key: string } }) => Promise<{ value: string } | null>;\n upsert: (args: {\n where: { key: string };\n update: { value: string };\n create: { key: string; value: string };\n }) => Promise<unknown>;\n };\n mcpToken: {\n findFirst: (args: {\n where: { revokedAt: Date | null };\n orderBy: { createdAt: 'desc' | 'asc' };\n }) => Promise<{\n id: number;\n tokenHash: string;\n createdAt: Date;\n lastUsedAt: Date | null;\n revokedAt: Date | null;\n } | null>;\n updateMany: (args: {\n where: { revokedAt: Date | null };\n data: { revokedAt: Date };\n }) => Promise<{ count: number }>;\n create: (args: { data: { tokenHash: string } }) => Promise<unknown>;\n update: (args: { where: { id: number }; data: { lastUsedAt: Date } }) => Promise<unknown>;\n };\n $transaction: <T>(callback: (tx: McpAdminDb) => Promise<T>) => Promise<T>;\n}\n\nconst toStatus = (\n enabled: boolean,\n activeToken: {\n id: number;\n createdAt: Date;\n lastUsedAt: Date | null;\n } | null\n): McpAdminStatus => {\n if (!activeToken) {\n return {\n enabled,\n hasActiveToken: false,\n token: null\n };\n }\n\n return {\n enabled,\n hasActiveToken: true,\n token: {\n id: String(activeToken.id),\n createdAt: activeToken.createdAt.toISOString(),\n lastUsedAt: activeToken.lastUsedAt ? activeToken.lastUsedAt.toISOString() : null\n }\n };\n};\n\nexport const createMcpAdminService = (db: McpAdminDb = models as unknown as McpAdminDb): McpAdminService => {\n return {\n async getStatus() {\n const enabledCache = await db.cache.findUnique({ where: { key: MCP_ENABLED_CACHE_KEY } });\n const activeToken = await db.mcpToken.findFirst({\n where: { revokedAt: null },\n orderBy: { createdAt: 'desc' }\n });\n\n return toStatus(enabledCache?.value === 'true', activeToken);\n },\n async setEnabled(enabled) {\n await db.cache.upsert({\n where: { key: MCP_ENABLED_CACHE_KEY },\n update: { value: String(enabled) },\n create: {\n key: MCP_ENABLED_CACHE_KEY,\n value: String(enabled)\n }\n });\n },\n async rotateToken() {\n const now = new Date();\n const issued = issueMcpToken();\n\n await db.$transaction(async (transaction) => {\n await transaction.mcpToken.updateMany({\n where: { revokedAt: null },\n data: { revokedAt: now }\n });\n await transaction.mcpToken.create({ data: { tokenHash: issued.hash } });\n });\n\n return { token: issued.plaintext };\n },\n async revokeActiveToken() {\n await db.mcpToken.updateMany({\n where: { revokedAt: null },\n data: { revokedAt: new Date() }\n });\n },\n async validatePresentedToken(token) {\n const activeToken = await db.mcpToken.findFirst({\n where: { revokedAt: null },\n orderBy: { createdAt: 'desc' }\n });\n\n if (!activeToken) {\n return {\n ok: false,\n reason: 'not_configured'\n };\n }\n\n if (!verifyMcpToken(activeToken.tokenHash, token)) {\n return {\n ok: false,\n reason: 'forbidden'\n };\n }\n\n await db.mcpToken.update({\n where: { id: activeToken.id },\n data: { lastUsedAt: new Date() }\n });\n\n return { ok: true };\n }\n };\n};\n"],"mappings":"AAAA,OAAO,YAAY;AACnB,SAAS,eAAe,sBAAsB;AAE9C,MAAM,wBAAwB;AAyD9B,MAAM,WAAW,CACb,SACA,gBAKiB;AACjB,MAAI,CAAC,aAAa;AACd,WAAO;AAAA,MACH;AAAA,MACA,gBAAgB;AAAA,MAChB,OAAO;AAAA,IACX;AAAA,EACJ;AAEA,SAAO;AAAA,IACH;AAAA,IACA,gBAAgB;AAAA,IAChB,OAAO;AAAA,MACH,IAAI,OAAO,YAAY,EAAE;AAAA,MACzB,WAAW,YAAY,UAAU,YAAY;AAAA,MAC7C,YAAY,YAAY,aAAa,YAAY,WAAW,YAAY,IAAI;AAAA,IAChF;AAAA,EACJ;AACJ;AAEO,MAAM,wBAAwB,CAAC,KAAiB,WAAqD;AACxG,SAAO;AAAA,IACH,MAAM,YAAY;AACd,YAAM,eAAe,MAAM,GAAG,MAAM,WAAW,EAAE,OAAO,EAAE,KAAK,sBAAsB,EAAE,CAAC;AACxF,YAAM,cAAc,MAAM,GAAG,SAAS,UAAU;AAAA,QAC5C,OAAO,EAAE,WAAW,KAAK;AAAA,QACzB,SAAS,EAAE,WAAW,OAAO;AAAA,MACjC,CAAC;AAED,aAAO,SAAS,cAAc,UAAU,QAAQ,WAAW;AAAA,IAC/D;AAAA,IACA,MAAM,WAAW,SAAS;AACtB,YAAM,GAAG,MAAM,OAAO;AAAA,QAClB,OAAO,EAAE,KAAK,sBAAsB;AAAA,QACpC,QAAQ,EAAE,OAAO,OAAO,OAAO,EAAE;AAAA,QACjC,QAAQ;AAAA,UACJ,KAAK;AAAA,UACL,OAAO,OAAO,OAAO;AAAA,QACzB;AAAA,MACJ,CAAC;AAAA,IACL;AAAA,IACA,MAAM,cAAc;AAChB,YAAM,MAAM,oBAAI,KAAK;AACrB,YAAM,SAAS,cAAc;AAE7B,YAAM,GAAG,aAAa,OAAO,gBAAgB;AACzC,cAAM,YAAY,SAAS,WAAW;AAAA,UAClC,OAAO,EAAE,WAAW,KAAK;AAAA,UACzB,MAAM,EAAE,WAAW,IAAI;AAAA,QAC3B,CAAC;AACD,cAAM,YAAY,SAAS,OAAO,EAAE,MAAM,EAAE,WAAW,OAAO,KAAK,EAAE,CAAC;AAAA,MAC1E,CAAC;AAED,aAAO,EAAE,OAAO,OAAO,UAAU;AAAA,IACrC;AAAA,IACA,MAAM,oBAAoB;AACtB,YAAM,GAAG,SAAS,WAAW;AAAA,QACzB,OAAO,EAAE,WAAW,KAAK;AAAA,QACzB,MAAM,EAAE,WAAW,oBAAI,KAAK,EAAE;AAAA,MAClC,CAAC;AAAA,IACL;AAAA,IACA,MAAM,uBAAuB,OAAO;AAChC,YAAM,cAAc,MAAM,GAAG,SAAS,UAAU;AAAA,QAC5C,OAAO,EAAE,WAAW,KAAK;AAAA,QACzB,SAAS,EAAE,WAAW,OAAO;AAAA,MACjC,CAAC;AAED,UAAI,CAAC,aAAa;AACd,eAAO;AAAA,UACH,IAAI;AAAA,UACJ,QAAQ;AAAA,QACZ;AAAA,MACJ;AAEA,UAAI,CAAC,eAAe,YAAY,WAAW,KAAK,GAAG;AAC/C,eAAO;AAAA,UACH,IAAI;AAAA,UACJ,QAAQ;AAAA,QACZ;AAAA,MACJ;AAEA,YAAM,GAAG,SAAS,OAAO;AAAA,QACrB,OAAO,EAAE,IAAI,YAAY,GAAG;AAAA,QAC5B,MAAM,EAAE,YAAY,oBAAI,KAAK,EAAE;AAAA,MACnC,CAAC;AAED,aAAO,EAAE,IAAI,KAAK;AAAA,IACtB;AAAA,EACJ;AACJ;","names":[]}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { GraphQLError } from "graphql";
|
|
2
|
+
const JSON_HEADERS = { "Content-Type": "application/json" };
|
|
3
|
+
const readBearerToken = (authorizationHeader) => {
|
|
4
|
+
if (!authorizationHeader?.startsWith("Bearer ")) {
|
|
5
|
+
return void 0;
|
|
6
|
+
}
|
|
7
|
+
return authorizationHeader.slice("Bearer ".length).trim() || void 0;
|
|
8
|
+
};
|
|
9
|
+
const createMcpAuthMiddleware = (_authConfig, mcpAdminAuth) => {
|
|
10
|
+
return async (req, res, next) => {
|
|
11
|
+
try {
|
|
12
|
+
const status = await mcpAdminAuth.getStatus();
|
|
13
|
+
if (!status.enabled) {
|
|
14
|
+
res.status(403).set(JSON_HEADERS).json({
|
|
15
|
+
code: "MCP_DISABLED",
|
|
16
|
+
message: "MCP access is disabled by admin."
|
|
17
|
+
}).end();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const bearerToken = readBearerToken(req.headers.authorization);
|
|
21
|
+
if (!bearerToken) {
|
|
22
|
+
res.status(401).set(JSON_HEADERS).json({
|
|
23
|
+
code: "UNAUTHORIZED",
|
|
24
|
+
message: "A valid MCP bearer token is required."
|
|
25
|
+
}).end();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const validation = await mcpAdminAuth.validatePresentedToken(bearerToken);
|
|
29
|
+
if (!validation.ok && validation.reason === "not_configured") {
|
|
30
|
+
res.status(503).set(JSON_HEADERS).json({
|
|
31
|
+
code: "MCP_AUTH_NOT_CONFIGURED",
|
|
32
|
+
message: "MCP bearer auth is not configured."
|
|
33
|
+
}).end();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (!validation.ok) {
|
|
37
|
+
res.status(403).set(JSON_HEADERS).json({
|
|
38
|
+
code: "FORBIDDEN",
|
|
39
|
+
message: "Invalid MCP bearer token."
|
|
40
|
+
}).end();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
next();
|
|
44
|
+
} catch (error) {
|
|
45
|
+
next(error);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
const createReadOnlyMcpValidationRule = () => {
|
|
50
|
+
return (context) => {
|
|
51
|
+
return {
|
|
52
|
+
OperationDefinition(node) {
|
|
53
|
+
if (node.operation === "query") {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
context.reportError(new GraphQLError(
|
|
57
|
+
"MCP endpoint is read-only",
|
|
58
|
+
{
|
|
59
|
+
nodes: [node],
|
|
60
|
+
extensions: { code: "FORBIDDEN" }
|
|
61
|
+
}
|
|
62
|
+
));
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
export {
|
|
68
|
+
createMcpAuthMiddleware,
|
|
69
|
+
createReadOnlyMcpValidationRule
|
|
70
|
+
};
|
|
71
|
+
//# sourceMappingURL=mcp-auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/modules/mcp-auth.ts"],"sourcesContent":["import type { NextFunction, Request, Response, RequestHandler } from 'express';\nimport type { ValidationRule } from 'graphql';\nimport { GraphQLError } from 'graphql';\n\nimport type { AuthConfig } from './auth-mode.js';\n\nexport interface McpTokenValidationResult {\n ok: boolean;\n reason?: 'not_configured' | 'forbidden';\n}\n\nexport interface McpAdminAuthPort {\n getStatus: () => Promise<{ enabled: boolean }>;\n validatePresentedToken: (token: string) => Promise<McpTokenValidationResult>;\n}\n\nconst JSON_HEADERS = { 'Content-Type': 'application/json' };\n\nconst readBearerToken = (authorizationHeader?: string) => {\n if (!authorizationHeader?.startsWith('Bearer ')) {\n return undefined;\n }\n\n return authorizationHeader.slice('Bearer '.length).trim() || undefined;\n};\n\nexport const createMcpAuthMiddleware = (\n _authConfig: AuthConfig,\n mcpAdminAuth: McpAdminAuthPort\n): RequestHandler => {\n return async (req: Request, res: Response, next: NextFunction) => {\n try {\n const status = await mcpAdminAuth.getStatus();\n if (!status.enabled) {\n res\n .status(403)\n .set(JSON_HEADERS)\n .json({\n code: 'MCP_DISABLED',\n message: 'MCP access is disabled by admin.'\n })\n .end();\n return;\n }\n\n const bearerToken = readBearerToken(req.headers.authorization);\n\n if (!bearerToken) {\n res\n .status(401)\n .set(JSON_HEADERS)\n .json({\n code: 'UNAUTHORIZED',\n message: 'A valid MCP bearer token is required.'\n })\n .end();\n return;\n }\n\n const validation = await mcpAdminAuth.validatePresentedToken(bearerToken);\n\n if (!validation.ok && validation.reason === 'not_configured') {\n res\n .status(503)\n .set(JSON_HEADERS)\n .json({\n code: 'MCP_AUTH_NOT_CONFIGURED',\n message: 'MCP bearer auth is not configured.'\n })\n .end();\n return;\n }\n\n if (!validation.ok) {\n res\n .status(403)\n .set(JSON_HEADERS)\n .json({\n code: 'FORBIDDEN',\n message: 'Invalid MCP bearer token.'\n })\n .end();\n return;\n }\n\n next();\n } catch (error) {\n next(error);\n }\n };\n};\n\nexport const createReadOnlyMcpValidationRule = (): ValidationRule => {\n return (context) => {\n return {\n OperationDefinition(node) {\n if (node.operation === 'query') {\n return;\n }\n\n context.reportError(new GraphQLError(\n 'MCP endpoint is read-only',\n {\n nodes: [node],\n extensions: { code: 'FORBIDDEN' }\n }\n ));\n }\n };\n };\n};\n"],"mappings":"AAEA,SAAS,oBAAoB;AAc7B,MAAM,eAAe,EAAE,gBAAgB,mBAAmB;AAE1D,MAAM,kBAAkB,CAAC,wBAAiC;AACtD,MAAI,CAAC,qBAAqB,WAAW,SAAS,GAAG;AAC7C,WAAO;AAAA,EACX;AAEA,SAAO,oBAAoB,MAAM,UAAU,MAAM,EAAE,KAAK,KAAK;AACjE;AAEO,MAAM,0BAA0B,CACnC,aACA,iBACiB;AACjB,SAAO,OAAO,KAAc,KAAe,SAAuB;AAC9D,QAAI;AACA,YAAM,SAAS,MAAM,aAAa,UAAU;AAC5C,UAAI,CAAC,OAAO,SAAS;AACjB,YACK,OAAO,GAAG,EACV,IAAI,YAAY,EAChB,KAAK;AAAA,UACF,MAAM;AAAA,UACN,SAAS;AAAA,QACb,CAAC,EACA,IAAI;AACT;AAAA,MACJ;AAEA,YAAM,cAAc,gBAAgB,IAAI,QAAQ,aAAa;AAE7D,UAAI,CAAC,aAAa;AACd,YACK,OAAO,GAAG,EACV,IAAI,YAAY,EAChB,KAAK;AAAA,UACF,MAAM;AAAA,UACN,SAAS;AAAA,QACb,CAAC,EACA,IAAI;AACT;AAAA,MACJ;AAEA,YAAM,aAAa,MAAM,aAAa,uBAAuB,WAAW;AAExE,UAAI,CAAC,WAAW,MAAM,WAAW,WAAW,kBAAkB;AAC1D,YACK,OAAO,GAAG,EACV,IAAI,YAAY,EAChB,KAAK;AAAA,UACF,MAAM;AAAA,UACN,SAAS;AAAA,QACb,CAAC,EACA,IAAI;AACT;AAAA,MACJ;AAEA,UAAI,CAAC,WAAW,IAAI;AAChB,YACK,OAAO,GAAG,EACV,IAAI,YAAY,EAChB,KAAK;AAAA,UACF,MAAM;AAAA,UACN,SAAS;AAAA,QACb,CAAC,EACA,IAAI;AACT;AAAA,MACJ;AAEA,WAAK;AAAA,IACT,SAAS,OAAO;AACZ,WAAK,KAAK;AAAA,IACd;AAAA,EACJ;AACJ;AAEO,MAAM,kCAAkC,MAAsB;AACjE,SAAO,CAAC,YAAY;AAChB,WAAO;AAAA,MACH,oBAAoB,MAAM;AACtB,YAAI,KAAK,cAAc,SAAS;AAC5B;AAAA,QACJ;AAEA,gBAAQ,YAAY,IAAI;AAAA,UACpB;AAAA,UACA;AAAA,YACI,OAAO,CAAC,IAAI;AAAA,YACZ,YAAY,EAAE,MAAM,YAAY;AAAA,UACpC;AAAA,QACJ,CAAC;AAAA,MACL;AAAA,IACJ;AAAA,EACJ;AACJ;","names":[]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
const TOKEN_BYTE_LENGTH = 32;
|
|
3
|
+
const HASH_ALGORITHM = "sha256";
|
|
4
|
+
const toTokenHash = (token) => {
|
|
5
|
+
return crypto.createHash(HASH_ALGORITHM).update(token, "utf8").digest("hex");
|
|
6
|
+
};
|
|
7
|
+
const issueMcpToken = () => {
|
|
8
|
+
const plaintext = crypto.randomBytes(TOKEN_BYTE_LENGTH).toString("base64url");
|
|
9
|
+
const hash = toTokenHash(plaintext);
|
|
10
|
+
return {
|
|
11
|
+
plaintext,
|
|
12
|
+
hash
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
const verifyMcpToken = (storedHash, presentedToken) => {
|
|
16
|
+
const presentedHash = toTokenHash(presentedToken);
|
|
17
|
+
const storedBuffer = Buffer.from(storedHash, "utf8");
|
|
18
|
+
const presentedBuffer = Buffer.from(presentedHash, "utf8");
|
|
19
|
+
if (storedBuffer.length !== presentedBuffer.length) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return crypto.timingSafeEqual(storedBuffer, presentedBuffer);
|
|
23
|
+
};
|
|
24
|
+
export {
|
|
25
|
+
issueMcpToken,
|
|
26
|
+
verifyMcpToken
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=mcp-token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/modules/mcp-token.ts"],"sourcesContent":["import crypto from 'crypto';\n\nconst TOKEN_BYTE_LENGTH = 32;\nconst HASH_ALGORITHM = 'sha256';\n\nexport interface IssuedMcpToken {\n plaintext: string;\n hash: string;\n}\n\nconst toTokenHash = (token: string) => {\n return crypto\n .createHash(HASH_ALGORITHM)\n .update(token, 'utf8')\n .digest('hex');\n};\n\nexport const issueMcpToken = (): IssuedMcpToken => {\n const plaintext = crypto.randomBytes(TOKEN_BYTE_LENGTH).toString('base64url');\n const hash = toTokenHash(plaintext);\n\n return {\n plaintext,\n hash\n };\n};\n\nexport const verifyMcpToken = (storedHash: string, presentedToken: string) => {\n const presentedHash = toTokenHash(presentedToken);\n const storedBuffer = Buffer.from(storedHash, 'utf8');\n const presentedBuffer = Buffer.from(presentedHash, 'utf8');\n\n if (storedBuffer.length !== presentedBuffer.length) {\n return false;\n }\n\n return crypto.timingSafeEqual(storedBuffer, presentedBuffer);\n};\n"],"mappings":"AAAA,OAAO,YAAY;AAEnB,MAAM,oBAAoB;AAC1B,MAAM,iBAAiB;AAOvB,MAAM,cAAc,CAAC,UAAkB;AACnC,SAAO,OACF,WAAW,cAAc,EACzB,OAAO,OAAO,MAAM,EACpB,OAAO,KAAK;AACrB;AAEO,MAAM,gBAAgB,MAAsB;AAC/C,QAAM,YAAY,OAAO,YAAY,iBAAiB,EAAE,SAAS,WAAW;AAC5E,QAAM,OAAO,YAAY,SAAS;AAElC,SAAO;AAAA,IACH;AAAA,IACA;AAAA,EACJ;AACJ;AAEO,MAAM,iBAAiB,CAAC,YAAoB,mBAA2B;AAC1E,QAAM,gBAAgB,YAAY,cAAc;AAChD,QAAM,eAAe,OAAO,KAAK,YAAY,MAAM;AACnD,QAAM,kBAAkB,OAAO,KAAK,eAAe,MAAM;AAEzD,MAAI,aAAa,WAAW,gBAAgB,QAAQ;AAChD,WAAO;AAAA,EACX;AAEA,SAAO,OAAO,gBAAgB,cAAc,eAAe;AAC/D;","names":[]}
|