ocean-brain 0.2.1 → 0.3.0

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 (100) 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-DRv2aEl8.js +28 -0
  6. package/server/client/dist/assets/Callout-B2kPcDrq.js +1 -0
  7. package/server/client/dist/assets/Graph-DotbTrZR.js +1 -0
  8. package/server/client/dist/assets/Image-B5R7gF2P.js +1 -0
  9. package/server/client/dist/assets/Image.es-Dld9r9MJ.js +1 -0
  10. package/server/client/dist/assets/ModalActionRow-CES9L_Gy.js +1 -0
  11. package/server/client/dist/assets/Note-SK4hDhEM.css +1 -0
  12. package/server/client/dist/assets/Note-cIbXJc1v.js +1 -0
  13. package/server/client/dist/assets/Reminders-4LunlZRb.js +1 -0
  14. package/server/client/dist/assets/Search-Cp17DGR6.js +1 -0
  15. package/server/client/dist/assets/SurfaceCard-yzVU4Qkx.js +1 -0
  16. package/server/client/dist/assets/Tag-CwmuMPmo.js +1 -0
  17. package/server/client/dist/assets/TagNotes-gGroWMtT.js +1 -0
  18. package/server/client/dist/assets/Trash.es-B3KWD8_C.js +1 -0
  19. package/server/client/dist/assets/color-wpu7wT_s.js +1 -0
  20. package/server/client/dist/assets/graph-vendor-CUxe67Lr.js +46 -0
  21. package/server/client/dist/assets/image.api-DJgOBt8S.js +17 -0
  22. package/server/client/dist/assets/index-40Y2UsVr.js +205 -0
  23. package/server/client/dist/assets/index-BnxpTwtL.css +1 -0
  24. package/server/client/dist/assets/index-DYUg2N-6.js +1 -0
  25. package/server/client/dist/assets/manage-image-ByY1mRZx.js +1 -0
  26. package/server/client/dist/assets/manage-image-detail-BVaDZHRu.js +1 -0
  27. package/server/client/dist/assets/mcp-Jj3061aI.js +1 -0
  28. package/server/client/dist/assets/{note-core-DTIu0anW.js → note-core-BCgMq5QA.js} +7 -7
  29. package/server/client/dist/assets/note-vendor-BofYbzmZ.js +54 -0
  30. package/server/client/dist/assets/placeholder-4P_GK7fV.js +34 -0
  31. package/server/client/dist/assets/trash-CPBwY07S.js +1 -0
  32. package/server/client/dist/assets/useReminderMutate-1YVndM2T.js +82 -0
  33. package/server/client/dist/index.html +5 -4
  34. package/server/dist/app.js +97 -9
  35. package/server/dist/app.js.map +1 -1
  36. package/server/dist/main.js +16 -2
  37. package/server/dist/main.js.map +1 -1
  38. package/server/dist/modules/auth-guard.js +73 -0
  39. package/server/dist/modules/auth-guard.js.map +1 -0
  40. package/server/dist/modules/auth-mode.js +51 -0
  41. package/server/dist/modules/auth-mode.js.map +1 -0
  42. package/server/dist/modules/auth.js +9 -0
  43. package/server/dist/modules/auth.js.map +1 -1
  44. package/server/dist/modules/blocknote.js +7 -1
  45. package/server/dist/modules/blocknote.js.map +1 -1
  46. package/server/dist/modules/error-handler.js +41 -0
  47. package/server/dist/modules/error-handler.js.map +1 -0
  48. package/server/dist/modules/image-upload.js +114 -0
  49. package/server/dist/modules/image-upload.js.map +1 -0
  50. package/server/dist/modules/mcp-admin.js +88 -0
  51. package/server/dist/modules/mcp-admin.js.map +1 -0
  52. package/server/dist/modules/mcp-auth.js +71 -0
  53. package/server/dist/modules/mcp-auth.js.map +1 -0
  54. package/server/dist/modules/mcp-token.js +28 -0
  55. package/server/dist/modules/mcp-token.js.map +1 -0
  56. package/server/dist/modules/note-authoring.js +140 -0
  57. package/server/dist/modules/note-authoring.js.map +1 -0
  58. package/server/dist/modules/note-cleanup.js +207 -0
  59. package/server/dist/modules/note-cleanup.js.map +1 -0
  60. package/server/dist/modules/note-snapshot.js +223 -0
  61. package/server/dist/modules/note-snapshot.js.map +1 -0
  62. package/server/dist/modules/note-trash.js +262 -0
  63. package/server/dist/modules/note-trash.js.map +1 -0
  64. package/server/dist/modules/prisma-runtime.js +46 -0
  65. package/server/dist/modules/prisma-runtime.js.map +1 -0
  66. package/server/dist/modules/recovery-retention.js +26 -0
  67. package/server/dist/modules/recovery-retention.js.map +1 -0
  68. package/server/dist/modules/remote-image.js +176 -0
  69. package/server/dist/modules/remote-image.js.map +1 -0
  70. package/server/dist/modules/tag-organization.js +66 -0
  71. package/server/dist/modules/tag-organization.js.map +1 -0
  72. package/server/dist/modules/use-async.js +2 -4
  73. package/server/dist/modules/use-async.js.map +1 -1
  74. package/server/dist/schema/note/index.js +150 -6
  75. package/server/dist/schema/note/index.js.map +1 -1
  76. package/server/dist/start.js +11 -0
  77. package/server/dist/start.js.map +1 -0
  78. package/server/dist/types/index.js.map +1 -1
  79. package/server/dist/urls.js +3 -2
  80. package/server/dist/urls.js.map +1 -1
  81. package/server/dist/views/auth.js +305 -0
  82. package/server/dist/views/auth.js.map +1 -0
  83. package/server/dist/views/image.js +24 -81
  84. package/server/dist/views/image.js.map +1 -1
  85. package/server/dist/views/index.js +4 -0
  86. package/server/dist/views/index.js.map +1 -1
  87. package/server/dist/views/mcp-admin.js +42 -0
  88. package/server/dist/views/mcp-admin.js.map +1 -0
  89. package/server/dist/views/note.js +124 -0
  90. package/server/dist/views/note.js.map +1 -0
  91. package/server/dist/views/tag.js +23 -0
  92. package/server/dist/views/tag.js.map +1 -0
  93. package/server/prisma/migrations/20260331183000_0010_note_snapshot/migration.sql +13 -0
  94. package/server/prisma/migrations/20260331195000_0011_note_trash_entities/migration.sql +35 -0
  95. package/server/prisma/migrations/20260331213000_0012_image_hash_unique/migration.sql +49 -0
  96. package/server/prisma/migrations/20260404000000_0013_mcp_admin_control/migration.sql +9 -0
  97. package/server/prisma/schema.prisma +66 -1
  98. package/server/client/dist/assets/index-BJgBFWwL.css +0 -1
  99. package/server/client/dist/assets/index-DvNRv9YC.js +0 -391
  100. package/server/client/dist/assets/note-vendor-CUVHa9LU.js +0 -54
@@ -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":[]}
@@ -0,0 +1,140 @@
1
+ import models from "../models.js";
2
+ import { markdownToBlocksJson } from "./blocknote.js";
3
+ import { captureNoteBaseline } from "./note-snapshot.js";
4
+ class InvalidNoteAuthoringInputError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = "InvalidNoteAuthoringInputError";
8
+ }
9
+ }
10
+ const PLACEHOLDER_PREFIX = "{%";
11
+ const PLACEHOLDER_SUFFIX = "%}";
12
+ const serializeNote = (note) => ({
13
+ id: String(note.id),
14
+ title: note.title,
15
+ layout: note.layout,
16
+ createdAt: note.createdAt.toISOString(),
17
+ updatedAt: note.updatedAt.toISOString()
18
+ });
19
+ const extractPlaceholderTemplates = (value) => {
20
+ return Array.from(
21
+ new Set(
22
+ Array.from(
23
+ value.matchAll(new RegExp(`${PLACEHOLDER_PREFIX}([^}]+)${PLACEHOLDER_SUFFIX}`, "g")),
24
+ (match) => match[1]
25
+ )
26
+ )
27
+ );
28
+ };
29
+ const createNoteAuthoringService = (deps) => {
30
+ const replacePlaceholders = async (value) => {
31
+ const templates = extractPlaceholderTemplates(value);
32
+ if (templates.length === 0) {
33
+ return value;
34
+ }
35
+ const placeholders = await deps.findPlaceholders(templates);
36
+ let replacedValue = value;
37
+ for (const placeholder of placeholders) {
38
+ replacedValue = replacedValue.replace(
39
+ new RegExp(`${PLACEHOLDER_PREFIX}${placeholder.template}${PLACEHOLDER_SUFFIX}`, "g"),
40
+ placeholder.replacement
41
+ );
42
+ }
43
+ return replacedValue;
44
+ };
45
+ return {
46
+ createNote: async (input) => {
47
+ const title = input.title.trim();
48
+ if (!title) {
49
+ throw new InvalidNoteAuthoringInputError("A note title is required.");
50
+ }
51
+ const replacedTitle = await replacePlaceholders(title);
52
+ const replacedMarkdown = await replacePlaceholders(input.markdown ?? "");
53
+ const content = await deps.parseMarkdownToContentJson(replacedMarkdown);
54
+ const note = await deps.createNote({
55
+ title: replacedTitle,
56
+ content,
57
+ ...input.layout ? { layout: input.layout } : {}
58
+ });
59
+ return serializeNote(note);
60
+ },
61
+ updateNote: async (input) => {
62
+ if (input.title === void 0 && input.markdown === void 0 && input.layout === void 0) {
63
+ throw new InvalidNoteAuthoringInputError("At least one note field must be provided for update.");
64
+ }
65
+ const existingNote = await deps.findNoteById(input.id);
66
+ if (!existingNote) {
67
+ return null;
68
+ }
69
+ const nextData = {};
70
+ if (input.title !== void 0) {
71
+ const title = input.title.trim();
72
+ if (!title) {
73
+ throw new InvalidNoteAuthoringInputError("A note title is required.");
74
+ }
75
+ nextData.title = await replacePlaceholders(title);
76
+ }
77
+ if (input.markdown !== void 0) {
78
+ const replacedMarkdown = await replacePlaceholders(input.markdown);
79
+ nextData.content = await deps.parseMarkdownToContentJson(replacedMarkdown);
80
+ }
81
+ if (input.layout !== void 0) {
82
+ nextData.layout = input.layout;
83
+ }
84
+ await deps.captureBaseline({
85
+ noteId: input.id,
86
+ ...input.editSessionId ? { editSessionId: input.editSessionId } : {},
87
+ ...input.snapshotMeta ? { meta: input.snapshotMeta } : {}
88
+ });
89
+ const updatedNote = await deps.updateNote(input.id, nextData);
90
+ return serializeNote(updatedNote);
91
+ }
92
+ };
93
+ };
94
+ const defaultNoteAuthoringService = createNoteAuthoringService({
95
+ createNote: async (input) => {
96
+ return models.note.create({
97
+ data: {
98
+ title: input.title,
99
+ content: input.content,
100
+ ...input.layout ? { layout: input.layout } : {}
101
+ }
102
+ });
103
+ },
104
+ findNoteById: async (id) => {
105
+ return models.note.findUnique({ where: { id } });
106
+ },
107
+ findPlaceholders: async (templates) => {
108
+ if (templates.length === 0) {
109
+ return [];
110
+ }
111
+ return models.placeholder.findMany({
112
+ select: {
113
+ template: true,
114
+ replacement: true
115
+ },
116
+ where: { template: { in: templates } }
117
+ });
118
+ },
119
+ parseMarkdownToContentJson: markdownToBlocksJson,
120
+ captureBaseline: captureNoteBaseline,
121
+ updateNote: async (id, input) => {
122
+ return models.note.update({
123
+ where: { id },
124
+ data: input
125
+ });
126
+ }
127
+ });
128
+ const createNoteFromMarkdown = async (input) => {
129
+ return defaultNoteAuthoringService.createNote(input);
130
+ };
131
+ const updateNoteFromMarkdown = async (input) => {
132
+ return defaultNoteAuthoringService.updateNote(input);
133
+ };
134
+ export {
135
+ InvalidNoteAuthoringInputError,
136
+ createNoteAuthoringService,
137
+ createNoteFromMarkdown,
138
+ updateNoteFromMarkdown
139
+ };
140
+ //# sourceMappingURL=note-authoring.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/modules/note-authoring.ts"],"sourcesContent":["import models, { type NoteLayout } from '~/models.js';\nimport { markdownToBlocksJson } from './blocknote.js';\nimport { captureNoteBaseline } from './note-snapshot.js';\n\ninterface PlaceholderRecord {\n template: string;\n replacement: string;\n}\n\ninterface NoteRecord {\n id: number;\n title: string;\n content: string;\n layout: NoteLayout;\n createdAt: Date;\n updatedAt: Date;\n}\n\ninterface NoteAuthoringDeps {\n createNote: (input: {\n title: string;\n content: string;\n layout?: NoteLayout;\n }) => Promise<NoteRecord>;\n findNoteById: (id: number) => Promise<NoteRecord | null>;\n findPlaceholders: (templates: string[]) => Promise<PlaceholderRecord[]>;\n parseMarkdownToContentJson: (markdown: string) => Promise<string>;\n captureBaseline: (input: {\n noteId: number;\n editSessionId?: string;\n meta?: string;\n }) => Promise<unknown>;\n updateNote: (id: number, input: {\n title?: string;\n content?: string;\n layout?: NoteLayout;\n }) => Promise<NoteRecord>;\n}\n\nexport interface CreateNoteAuthoringInput {\n title: string;\n markdown?: string;\n layout?: NoteLayout;\n}\n\nexport interface UpdateNoteAuthoringInput {\n id: number;\n title?: string;\n markdown?: string;\n layout?: NoteLayout;\n editSessionId?: string;\n snapshotMeta?: string;\n}\n\nexport interface AuthoredNoteSummary {\n id: string;\n title: string;\n layout: NoteLayout;\n createdAt: string;\n updatedAt: string;\n}\n\nexport class InvalidNoteAuthoringInputError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'InvalidNoteAuthoringInputError';\n }\n}\n\nconst PLACEHOLDER_PREFIX = '{%';\nconst PLACEHOLDER_SUFFIX = '%}';\n\nconst serializeNote = (note: NoteRecord): AuthoredNoteSummary => ({\n id: String(note.id),\n title: note.title,\n layout: note.layout,\n createdAt: note.createdAt.toISOString(),\n updatedAt: note.updatedAt.toISOString()\n});\n\nconst extractPlaceholderTemplates = (value: string) => {\n return Array.from(\n new Set(\n Array.from(\n value.matchAll(new RegExp(`${PLACEHOLDER_PREFIX}([^}]+)${PLACEHOLDER_SUFFIX}`, 'g')),\n (match) => match[1]\n )\n )\n );\n};\n\nexport const createNoteAuthoringService = (deps: NoteAuthoringDeps) => {\n const replacePlaceholders = async (value: string) => {\n const templates = extractPlaceholderTemplates(value);\n\n if (templates.length === 0) {\n return value;\n }\n\n const placeholders = await deps.findPlaceholders(templates);\n let replacedValue = value;\n\n for (const placeholder of placeholders) {\n replacedValue = replacedValue.replace(\n new RegExp(`${PLACEHOLDER_PREFIX}${placeholder.template}${PLACEHOLDER_SUFFIX}`, 'g'),\n placeholder.replacement\n );\n }\n\n return replacedValue;\n };\n\n return {\n createNote: async (input: CreateNoteAuthoringInput): Promise<AuthoredNoteSummary> => {\n const title = input.title.trim();\n\n if (!title) {\n throw new InvalidNoteAuthoringInputError('A note title is required.');\n }\n\n const replacedTitle = await replacePlaceholders(title);\n const replacedMarkdown = await replacePlaceholders(input.markdown ?? '');\n const content = await deps.parseMarkdownToContentJson(replacedMarkdown);\n const note = await deps.createNote({\n title: replacedTitle,\n content,\n ...(input.layout ? { layout: input.layout } : {})\n });\n\n return serializeNote(note);\n },\n\n updateNote: async (input: UpdateNoteAuthoringInput): Promise<AuthoredNoteSummary | null> => {\n if (\n input.title === undefined &&\n input.markdown === undefined &&\n input.layout === undefined\n ) {\n throw new InvalidNoteAuthoringInputError('At least one note field must be provided for update.');\n }\n\n const existingNote = await deps.findNoteById(input.id);\n\n if (!existingNote) {\n return null;\n }\n\n const nextData: {\n title?: string;\n content?: string;\n layout?: NoteLayout;\n } = {};\n\n if (input.title !== undefined) {\n const title = input.title.trim();\n\n if (!title) {\n throw new InvalidNoteAuthoringInputError('A note title is required.');\n }\n\n nextData.title = await replacePlaceholders(title);\n }\n\n if (input.markdown !== undefined) {\n const replacedMarkdown = await replacePlaceholders(input.markdown);\n nextData.content = await deps.parseMarkdownToContentJson(replacedMarkdown);\n }\n\n if (input.layout !== undefined) {\n nextData.layout = input.layout;\n }\n\n await deps.captureBaseline({\n noteId: input.id,\n ...(input.editSessionId ? { editSessionId: input.editSessionId } : {}),\n ...(input.snapshotMeta ? { meta: input.snapshotMeta } : {})\n });\n\n const updatedNote = await deps.updateNote(input.id, nextData);\n return serializeNote(updatedNote);\n }\n };\n};\n\nconst defaultNoteAuthoringService = createNoteAuthoringService({\n createNote: async (input) => {\n return models.note.create({\n data: {\n title: input.title,\n content: input.content,\n ...(input.layout ? { layout: input.layout } : {})\n }\n });\n },\n findNoteById: async (id) => {\n return models.note.findUnique({ where: { id } });\n },\n findPlaceholders: async (templates) => {\n if (templates.length === 0) {\n return [];\n }\n\n return models.placeholder.findMany({\n select: {\n template: true,\n replacement: true\n },\n where: { template: { in: templates } }\n });\n },\n parseMarkdownToContentJson: markdownToBlocksJson,\n captureBaseline: captureNoteBaseline,\n updateNote: async (id, input) => {\n return models.note.update({\n where: { id },\n data: input\n });\n }\n});\n\nexport const createNoteFromMarkdown = async (input: CreateNoteAuthoringInput) => {\n return defaultNoteAuthoringService.createNote(input);\n};\n\nexport const updateNoteFromMarkdown = async (input: UpdateNoteAuthoringInput) => {\n return defaultNoteAuthoringService.updateNote(input);\n};\n"],"mappings":"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","names":[]}