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.
Files changed (113) hide show
  1. package/dist/index.js +73 -41
  2. package/dist/mcp.js +587 -26
  3. package/package.json +6 -1
  4. package/scripts/postinstall-prisma.mjs +41 -0
  5. package/server/client/dist/assets/Calendar-DynpmB3D.js +25 -0
  6. package/server/client/dist/assets/Callout-tNHgPhLM.js +1 -0
  7. package/server/client/dist/assets/Graph-DMXLUfCv.js +1 -0
  8. package/server/client/dist/assets/Image-ZxF459sC.js +1 -0
  9. package/server/client/dist/assets/Image.es-Dnn4SMzG.js +1 -0
  10. package/server/client/dist/assets/ModalActionRow-B3vNjcDc.js +1 -0
  11. package/server/client/dist/assets/Note-PDMuj62M.js +1 -0
  12. package/server/client/dist/assets/Note-PEevDKBh.css +1 -0
  13. package/server/client/dist/assets/Reminders-ComtTLuN.js +1 -0
  14. package/server/client/dist/assets/{Search-BANPp2ZV.js → Search-BJeZTbVu.js} +1 -1
  15. package/server/client/dist/assets/SurfaceCard-DAcVMY4f.js +1 -0
  16. package/server/client/dist/assets/Tag-BuNCDGD5.js +1 -0
  17. package/server/client/dist/assets/TagNotes-ajFOP5pZ.js +1 -0
  18. package/server/client/dist/assets/Trash.es-DLiHgjNo.js +1 -0
  19. package/server/client/dist/assets/{image.api-rBH0OSWJ.js → image.api-CjJgNbC0.js} +1 -1
  20. package/server/client/dist/assets/index-CDrZmLC2.js +1 -0
  21. package/server/client/dist/assets/index-DAZMX8bQ.css +1 -0
  22. package/server/client/dist/assets/index-DqFkOWbi.js +205 -0
  23. package/server/client/dist/assets/manage-image-CVyZTDT_.js +1 -0
  24. package/server/client/dist/assets/manage-image-detail-Bp17tylS.js +1 -0
  25. package/server/client/dist/assets/mcp-BpD7VH-d.js +1 -0
  26. package/server/client/dist/assets/placeholder-DSf4QF4v.js +34 -0
  27. package/server/client/dist/assets/trash-Ceb17h_s.js +1 -0
  28. package/server/client/dist/assets/useReminderMutate-CzyOQ_VB.js +82 -0
  29. package/server/client/dist/index.html +2 -2
  30. package/server/dist/app.js +97 -9
  31. package/server/dist/app.js.map +1 -1
  32. package/server/dist/main.js +16 -2
  33. package/server/dist/main.js.map +1 -1
  34. package/server/dist/modules/auth-guard.js +73 -0
  35. package/server/dist/modules/auth-guard.js.map +1 -0
  36. package/server/dist/modules/auth-mode.js +51 -0
  37. package/server/dist/modules/auth-mode.js.map +1 -0
  38. package/server/dist/modules/auth.js +9 -0
  39. package/server/dist/modules/auth.js.map +1 -1
  40. package/server/dist/modules/blocknote.js +154 -2
  41. package/server/dist/modules/blocknote.js.map +1 -1
  42. package/server/dist/modules/error-handler.js +41 -0
  43. package/server/dist/modules/error-handler.js.map +1 -0
  44. package/server/dist/modules/image-upload.js +114 -0
  45. package/server/dist/modules/image-upload.js.map +1 -0
  46. package/server/dist/modules/mcp-admin.js +88 -0
  47. package/server/dist/modules/mcp-admin.js.map +1 -0
  48. package/server/dist/modules/mcp-auth.js +71 -0
  49. package/server/dist/modules/mcp-auth.js.map +1 -0
  50. package/server/dist/modules/mcp-token.js +28 -0
  51. package/server/dist/modules/mcp-token.js.map +1 -0
  52. package/server/dist/modules/note-authoring.js +153 -0
  53. package/server/dist/modules/note-authoring.js.map +1 -0
  54. package/server/dist/modules/note-cleanup.js +207 -0
  55. package/server/dist/modules/note-cleanup.js.map +1 -0
  56. package/server/dist/modules/note-snapshot.js +223 -0
  57. package/server/dist/modules/note-snapshot.js.map +1 -0
  58. package/server/dist/modules/note-trash.js +262 -0
  59. package/server/dist/modules/note-trash.js.map +1 -0
  60. package/server/dist/modules/prisma-runtime.js +46 -0
  61. package/server/dist/modules/prisma-runtime.js.map +1 -0
  62. package/server/dist/modules/recovery-retention.js +26 -0
  63. package/server/dist/modules/recovery-retention.js.map +1 -0
  64. package/server/dist/modules/remote-image.js +176 -0
  65. package/server/dist/modules/remote-image.js.map +1 -0
  66. package/server/dist/modules/tag-organization.js +66 -0
  67. package/server/dist/modules/tag-organization.js.map +1 -0
  68. package/server/dist/modules/use-async.js +2 -4
  69. package/server/dist/modules/use-async.js.map +1 -1
  70. package/server/dist/schema/note/index.js +150 -6
  71. package/server/dist/schema/note/index.js.map +1 -1
  72. package/server/dist/start.js +11 -0
  73. package/server/dist/start.js.map +1 -0
  74. package/server/dist/types/index.js.map +1 -1
  75. package/server/dist/urls.js +3 -2
  76. package/server/dist/urls.js.map +1 -1
  77. package/server/dist/views/auth.js +305 -0
  78. package/server/dist/views/auth.js.map +1 -0
  79. package/server/dist/views/image.js +24 -81
  80. package/server/dist/views/image.js.map +1 -1
  81. package/server/dist/views/index.js +4 -0
  82. package/server/dist/views/index.js.map +1 -1
  83. package/server/dist/views/mcp-admin.js +42 -0
  84. package/server/dist/views/mcp-admin.js.map +1 -0
  85. package/server/dist/views/note.js +124 -0
  86. package/server/dist/views/note.js.map +1 -0
  87. package/server/dist/views/tag.js +23 -0
  88. package/server/dist/views/tag.js.map +1 -0
  89. package/server/prisma/migrations/20260331183000_0010_note_snapshot/migration.sql +13 -0
  90. package/server/prisma/migrations/20260331195000_0011_note_trash_entities/migration.sql +35 -0
  91. package/server/prisma/migrations/20260331213000_0012_image_hash_unique/migration.sql +49 -0
  92. package/server/prisma/migrations/20260404000000_0013_mcp_admin_control/migration.sql +9 -0
  93. package/server/prisma/schema.prisma +66 -1
  94. package/server/client/dist/assets/Calendar-ST5q_8KT.js +0 -48
  95. package/server/client/dist/assets/Callout-CrWW3bg0.js +0 -1
  96. package/server/client/dist/assets/Graph-CzJjt6rP.js +0 -1
  97. package/server/client/dist/assets/Image-Dxg1qqZw.js +0 -1
  98. package/server/client/dist/assets/Image.es-DZ0H6wH1.js +0 -1
  99. package/server/client/dist/assets/Note-Bu7fN3B6.js +0 -6
  100. package/server/client/dist/assets/Note-CCvXqPzv.css +0 -1
  101. package/server/client/dist/assets/Plus.es-CS3WVMDq.js +0 -1
  102. package/server/client/dist/assets/PriorityLegend-D61iBwNW.js +0 -1
  103. package/server/client/dist/assets/Reminders-BjirJMOR.css +0 -1
  104. package/server/client/dist/assets/Reminders-DjWRombU.js +0 -1
  105. package/server/client/dist/assets/Tag-DbKSLTj0.js +0 -1
  106. package/server/client/dist/assets/TagNotes-B6s7jtL7.js +0 -1
  107. package/server/client/dist/assets/index-BzWwEDoL.js +0 -1
  108. package/server/client/dist/assets/index-CF5_ZCYN.js +0 -162
  109. package/server/client/dist/assets/index-DjZk2Gpg.css +0 -1
  110. package/server/client/dist/assets/manage-image-detail-DOx6QsCl.js +0 -1
  111. package/server/client/dist/assets/manage-image-g-MC6Fkc.js +0 -1
  112. package/server/client/dist/assets/placeholder-DV3u6MrA.js +0 -34
  113. 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 processed = preprocessCustomInlineContent(blocks);
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":[]}