ocean-brain 0.13.3 → 0.15.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 (177) hide show
  1. package/README.md +66 -2
  2. package/dist/mcp.js +439 -480
  3. package/package.json +3 -2
  4. package/server/client/dist/assets/ArrowClockwise.es-BwtKWcmj.js +1 -0
  5. package/server/client/dist/assets/{ArrowRight.es-BlNsZQFs.js → ArrowRight.es-E09Lfefr.js} +1 -1
  6. package/server/client/dist/assets/{AuxiliaryPanelHeader-HAttwVby.js → AuxiliaryPanelHeader-Bys98BJJ.js} +1 -1
  7. package/server/client/dist/assets/Calendar-BOhJYi8K.js +25 -0
  8. package/server/client/dist/assets/Callout-CAH1ua9B.js +1 -0
  9. package/server/client/dist/assets/{CaretUp.es-C8iY5OlB.js → CaretUp.es-DSWjIa-J.js} +1 -1
  10. package/server/client/dist/assets/{CheckCircle.es-Dqr3WImn.js → CheckCircle.es-Cou6rItm.js} +1 -1
  11. package/server/client/dist/assets/{Copy.es-fMv0ETir.js → Copy.es-ftEldGIL.js} +1 -1
  12. package/server/client/dist/assets/{File.es-CHtXXafR.js → File.es-DlKAX1uO.js} +1 -1
  13. package/server/client/dist/assets/{FileText.es-DPIwDCEe.js → FileText.es-D6JM_MqM.js} +1 -1
  14. package/server/client/dist/assets/{FlowArrow.es-Dxee98ir.js → FlowArrow.es-BRWXqf9A.js} +1 -1
  15. package/server/client/dist/assets/{Graph-hVj6vAYJ.js → Graph-BaqGi3M7.js} +1 -1
  16. package/server/client/dist/assets/{Graph-CdEu8eAn.js → Graph-L-d0cdMk.js} +1 -1
  17. package/server/client/dist/assets/{Image.es-CjXY1JVf.js → Image.es-Bh2mTNtr.js} +1 -1
  18. package/server/client/dist/assets/{Info.es-CYxg-Vku.js → Info.es-Bwz4CSvS.js} +1 -1
  19. package/server/client/dist/assets/Integration-C-rva2X9.js +1 -0
  20. package/server/client/dist/assets/{LinkSimple.es-B-P-7Iem.js → LinkSimple.es-iJvLf-MB.js} +1 -1
  21. package/server/client/dist/assets/Note-OO_uh2a7.js +19 -0
  22. package/server/client/dist/assets/{NotePropertiesPanel-6P7UfRBR.js → NotePropertiesPanel-BFWjxL_w.js} +1 -1
  23. package/server/client/dist/assets/{Plus.es-Dy6onD-Z.js → Plus.es-Dmb3oijD.js} +1 -1
  24. package/server/client/dist/assets/{Reminders-DnuALBL6.js → Reminders-CtpFtvu6.js} +1 -1
  25. package/server/client/dist/assets/{Search-AaD1Mt0d.js → Search-t48pkUg-.js} +2 -2
  26. package/server/client/dist/assets/{SurfaceCard-D5QxOte_.js → SurfaceCard-zdJ8k1o0.js} +1 -1
  27. package/server/client/dist/assets/{Tag-B2Qiq1Hv.js → Tag-BL2h3ZaX.js} +1 -1
  28. package/server/client/dist/assets/TagNotes-BpRTCEBf.js +1 -0
  29. package/server/client/dist/assets/Trash.es-DFnO8iF5.js +1 -0
  30. package/server/client/dist/assets/{ViewChip-BVkZAXyx.js → ViewChip-BdPhSEvG.js} +1 -1
  31. package/server/client/dist/assets/{Views-B5oh8GVM.js → Views-BoO6vgnF.js} +1 -1
  32. package/server/client/dist/assets/WarningCircle.es-BAvgGuQK.js +1 -0
  33. package/server/client/dist/assets/{abnfDiagram-N423BO3Z-D44O4T7q.js → abnfDiagram-N423BO3Z-CPgGmCbn.js} +1 -1
  34. package/server/client/dist/assets/app-OGsLwSqJ.js +151 -0
  35. package/server/client/dist/assets/app-yYMsgUrz.css +1 -0
  36. package/server/client/dist/assets/{arc-Cil9fXuW.js → arc-DqTgRpuF.js} +1 -1
  37. package/server/client/dist/assets/{architectureDiagram-T3A2C74G-Cjq6ayF2.js → architectureDiagram-T3A2C74G-rTYMAQ-J.js} +1 -1
  38. package/server/client/dist/assets/{blockDiagram-VBNYF7ZC-B2Xm5N0i.js → blockDiagram-VBNYF7ZC-BOluytVz.js} +1 -1
  39. package/server/client/dist/assets/{c4Diagram-5PPSVZJV-Dp0y-lCR.js → c4Diagram-5PPSVZJV-C8gbPpcb.js} +1 -1
  40. package/server/client/dist/assets/{calendar-data-wB0ifXdo.js → calendar-data-CwVF7Pz3.js} +1 -1
  41. package/server/client/dist/assets/channel--aI9fJWX.js +1 -0
  42. package/server/client/dist/assets/{chunk-2GRJ4B5K-CWaaNkFx.js → chunk-2GRJ4B5K-CRER5Ka0.js} +1 -1
  43. package/server/client/dist/assets/{chunk-2Q5K7J3B-Ces-xz2S.js → chunk-2Q5K7J3B-hxmJWGu_.js} +1 -1
  44. package/server/client/dist/assets/{chunk-5RXB4S5H-B-4Chlfc.js → chunk-5RXB4S5H-Bpr5Us8Y.js} +1 -1
  45. package/server/client/dist/assets/{chunk-5VM5RSS4-C8_HVwuz.js → chunk-5VM5RSS4-CKUz9fAb.js} +1 -1
  46. package/server/client/dist/assets/{chunk-6Q2QTUOP-xZ8ofRqk.js → chunk-6Q2QTUOP-B8XEOHPN.js} +1 -1
  47. package/server/client/dist/assets/{chunk-GF5L2VYU-Bpa43a7s.js → chunk-GF5L2VYU-BBUD-HJi.js} +1 -1
  48. package/server/client/dist/assets/{chunk-JWPE2WC7-DsyTXbuq.js → chunk-JWPE2WC7-Dq6_B8-d.js} +1 -1
  49. package/server/client/dist/assets/{chunk-KBJHAD2P-BDhjm9-8.js → chunk-KBJHAD2P--2VZEwNJ.js} +1 -1
  50. package/server/client/dist/assets/{chunk-RYQCIY6F-Cn-jdp6I.js → chunk-RYQCIY6F-CKCwnKiC.js} +1 -1
  51. package/server/client/dist/assets/{chunk-XXDRQBXY-BKsc_0wh.js → chunk-XXDRQBXY-VDufR1Oi.js} +1 -1
  52. package/server/client/dist/assets/classDiagram-JCYQIIEL-BsbwE38V.js +1 -0
  53. package/server/client/dist/assets/classDiagram-v2-OCEON4UE-BsbwE38V.js +1 -0
  54. package/server/client/dist/assets/{cose-bilkent-JH36ORCC-jmn3Q4zj.js → cose-bilkent-JH36ORCC-D4nqOx8S.js} +1 -1
  55. package/server/client/dist/assets/{cynefinDiagram-MW4NZA55-BpLstuBJ.js → cynefinDiagram-MW4NZA55-CUuLMs-6.js} +1 -1
  56. package/server/client/dist/assets/{dagre-VZM6K2ZE-B5ft5Yzm.js → dagre-VZM6K2ZE-CUSkhZgS.js} +1 -1
  57. package/server/client/dist/assets/{diagram-7IWD3JNH-B_W9IsWZ.js → diagram-7IWD3JNH-D95eVcKx.js} +4 -4
  58. package/server/client/dist/assets/{diagram-B4RE2ZJO-32chS3GX.js → diagram-B4RE2ZJO-7OH_fb0K.js} +1 -1
  59. package/server/client/dist/assets/{diagram-LBJQPF4R-Bqupdb3-.js → diagram-LBJQPF4R-D9UwEIMd.js} +1 -1
  60. package/server/client/dist/assets/{diagram-Q27KOJAE-BD8jKBsv.js → diagram-Q27KOJAE-Dx7WFDuX.js} +1 -1
  61. package/server/client/dist/assets/{diagram-UB23O5K3-C40jAU8D.js → diagram-UB23O5K3-D2BYwAKT.js} +1 -1
  62. package/server/client/dist/assets/{ebnfDiagram-BXEA7PRR-BQfW9mFA.js → ebnfDiagram-BXEA7PRR-DbeA-fk4.js} +1 -1
  63. package/server/client/dist/assets/{erDiagram-JOGREHBK-ILZnLzz2.js → erDiagram-JOGREHBK-jWW-ICGO.js} +1 -1
  64. package/server/client/dist/assets/{flowDiagram-UKHOOZJN-CDmwr2dI.js → flowDiagram-UKHOOZJN-68iZdxxw.js} +1 -1
  65. package/server/client/dist/assets/{ganttDiagram-PKOTCBZU-DBcrUc49.js → ganttDiagram-PKOTCBZU-CxwY3VXg.js} +1 -1
  66. package/server/client/dist/assets/{gitGraphDiagram-DS77QQ5N-_xO6__FK.js → gitGraphDiagram-DS77QQ5N-Ci6o5MfZ.js} +1 -1
  67. package/server/client/dist/assets/{image.api-ChvlfuZg.js → image.api-CHuhmq_G.js} +1 -1
  68. package/server/client/dist/assets/index-CuCzOHJ6.js +1 -0
  69. package/server/client/dist/assets/{infoDiagram-6WML65LV-CuIIcu-L.js → infoDiagram-6WML65LV-DY7aMdjV.js} +1 -1
  70. package/server/client/dist/assets/integrations-B5SIEnG_.js +1 -0
  71. package/server/client/dist/assets/{ishikawaDiagram-WSZJBQD7-BVtRGTOj.js → ishikawaDiagram-WSZJBQD7-Bh5qKh1d.js} +1 -1
  72. package/server/client/dist/assets/{journeyDiagram-NVQOT4AX-D5-Nzb2N.js → journeyDiagram-NVQOT4AX-B_svXTTT.js} +1 -1
  73. package/server/client/dist/assets/{kanban-definition-27J2QSJJ-DmOhgC8H.js → kanban-definition-27J2QSJJ-CaJslHGg.js} +1 -1
  74. package/server/client/dist/assets/manage-image-Kd-7IGfB.js +1 -0
  75. package/server/client/dist/assets/{manage-image-detail-Dw3tAW3h.js → manage-image-detail-HU0QLP-t.js} +1 -1
  76. package/server/client/dist/assets/mcp-BQ418pyx.js +5 -0
  77. package/server/client/dist/assets/{mcp-admin-adapter-D0R8p9az.js → mcp-admin-adapter-CUakQ_Xm.js} +1 -1
  78. package/server/client/dist/assets/{mermaid.core-C2tL9tq7.js → mermaid.core-BfUQM5vF.js} +4 -4
  79. package/server/client/dist/assets/{mindmap-definition-FAOFIHXS-DuvsPqrh.js → mindmap-definition-FAOFIHXS-DGbkJda2.js} +1 -1
  80. package/server/client/dist/assets/{note-core-YhYEmI_R.js → note-core-DRBsBf9H.js} +9 -9
  81. package/server/client/dist/assets/{note-runtime-B3ats-dC.js → note-runtime-ztZvXRIu.js} +16 -16
  82. package/server/client/dist/assets/{note-ui-BtzPk8gA.js → note-ui-CRcP5BjO.js} +1 -1
  83. package/server/client/dist/assets/{pegDiagram-VL7TDLO6-D64crEZV.js → pegDiagram-VL7TDLO6-Bip6zQct.js} +1 -1
  84. package/server/client/dist/assets/{pieDiagram-7S7Q4E2Y--kl7vopC.js → pieDiagram-7S7Q4E2Y-CjfjGY8Y.js} +1 -1
  85. package/server/client/dist/assets/{placeholder-B5uPsE7-.js → placeholder-D7JKnpuh.js} +1 -1
  86. package/server/client/dist/assets/{properties-B7Fv95qY.js → properties-CtagYHMr.js} +1 -1
  87. package/server/client/dist/assets/{quadrantDiagram-CIZ2JOQS-DuvJ4M2y.js → quadrantDiagram-CIZ2JOQS-B6F1yUT-.js} +1 -1
  88. package/server/client/dist/assets/{railroadDiagram-AXF67PYL-BMKmCOY7.js → railroadDiagram-AXF67PYL-D-0fLe0F.js} +1 -1
  89. package/server/client/dist/assets/{requirementDiagram-LRYGKXZP-DY1eJcy0.js → requirementDiagram-LRYGKXZP-tm4mnCyJ.js} +1 -1
  90. package/server/client/dist/assets/route-preload.js +2 -2
  91. package/server/client/dist/assets/{sankeyDiagram-W5VNT64P-C3Ka4GKM.js → sankeyDiagram-W5VNT64P-DSBWFurb.js} +1 -1
  92. package/server/client/dist/assets/search-B5tCkhwj.js +1 -0
  93. package/server/client/dist/assets/{search-admin-adapter-Daf7e2YQ.js → search-admin-adapter-Be9xSY88.js} +1 -1
  94. package/server/client/dist/assets/{sequenceDiagram-SI44F4Z6-Bhpwtk2s.js → sequenceDiagram-SI44F4Z6-KrwjjJIV.js} +1 -1
  95. package/server/client/dist/assets/{sizeCapture-X5ZJPWSS-BFta71TW.js → sizeCapture-X5ZJPWSS-DL2cn_Q6.js} +1 -1
  96. package/server/client/dist/assets/{stateDiagram-OKZ733FA-D6GCn1io.js → stateDiagram-OKZ733FA-CKuNDH85.js} +1 -1
  97. package/server/client/dist/assets/{stateDiagram-v2-UEYNNEHI-g4yinGDn.js → stateDiagram-v2-UEYNNEHI-BFaj2wbM.js} +1 -1
  98. package/server/client/dist/assets/{swimlanes-SLNWSIFB-DLFvOjUl.js → swimlanes-SLNWSIFB-5S17nqhK.js} +2 -2
  99. package/server/client/dist/assets/swimlanesDiagram-ULZ7WXOC-dexsHJq2.js +8 -0
  100. package/server/client/dist/assets/{tag.api-DEUROVKQ.js → tag.api-DGfi8hWL.js} +1 -1
  101. package/server/client/dist/assets/time-Kh6oav5R.js +361 -0
  102. package/server/client/dist/assets/{timeline-definition-Z64GVDOM-DTSIekSM.js → timeline-definition-Z64GVDOM-CU1hp_7d.js} +3 -3
  103. package/server/client/dist/assets/{trash-BHida7BG.js → trash-Di2BWyUO.js} +1 -1
  104. package/server/client/dist/assets/url-Bp8sXMb4.js +1 -0
  105. package/server/client/dist/assets/{useNoteMutate-BjvZUmtX.js → useNoteMutate-BQj6jHhV.js} +3 -3
  106. package/server/client/dist/assets/{useReminderMutate-C5n-5LM-.js → useReminderMutate-jubqwSD9.js} +1 -1
  107. package/server/client/dist/assets/{vennDiagram-T6HMQDX7-CMNqSI2X.js → vennDiagram-T6HMQDX7-MoiVLnil.js} +1 -1
  108. package/server/client/dist/assets/{wardleyDiagram-T6FBY63Y-DV-JOYqB.js → wardleyDiagram-T6FBY63Y-cZkfdI6C.js} +1 -1
  109. package/server/client/dist/assets/{xychartDiagram-ELKLHX3M-BUIniF4m.js → xychartDiagram-ELKLHX3M-B63hxD-r.js} +1 -1
  110. package/server/client/dist/index.html +6 -6
  111. package/server/dist/app.js +11 -1
  112. package/server/dist/features/app-gateway/access.js +73 -0
  113. package/server/dist/features/app-gateway/database.js +20 -0
  114. package/server/dist/features/app-gateway/gateway.js +208 -0
  115. package/server/dist/features/integration/access-events.js +17 -0
  116. package/server/dist/features/integration/auth.js +21 -0
  117. package/server/dist/features/integration/manifest.js +116 -0
  118. package/server/dist/features/integration/note-sync.js +156 -0
  119. package/server/dist/features/integration/schema.js +54 -0
  120. package/server/dist/features/integration/service.js +246 -0
  121. package/server/dist/features/integration/status-report.js +22 -0
  122. package/server/dist/features/mcp-admin/service.js +19 -83
  123. package/server/dist/features/note/graphql/note.field.resolver.js +1 -1
  124. package/server/dist/features/note/graphql/note.mutation.resolver.js +20 -12
  125. package/server/dist/features/note/graphql/note.query.resolver.js +10 -1
  126. package/server/dist/features/note/graphql/note.type-defs.js +13 -1
  127. package/server/dist/features/note/http/authoring.js +493 -0
  128. package/server/dist/features/note/http/mcp.js +15 -463
  129. package/server/dist/features/note/services/authoring.js +17 -1
  130. package/server/dist/features/note/services/change-events.js +26 -0
  131. package/server/dist/features/note/services/markdown-intent-write.js +1 -0
  132. package/server/dist/features/note/services/markdown-read.js +85 -0
  133. package/server/dist/features/note/services/properties.js +36 -30
  134. package/server/dist/features/note/services/search.js +17 -0
  135. package/server/dist/features/search/graphql/resolver.js +10 -1
  136. package/server/dist/features/search/graphql/type-defs.js +3 -0
  137. package/server/dist/features/search/search-manager.js +4 -7
  138. package/server/dist/features/tag/graphql/tag.query.resolver.js +10 -0
  139. package/server/dist/features/tag/graphql/tag.type-defs.js +1 -0
  140. package/server/dist/features/view/graphql/mcp.query.resolver.js +22 -0
  141. package/server/dist/features/view/graphql/view.query.resolver.js +2 -0
  142. package/server/dist/features/view/graphql/view.type-defs.js +27 -0
  143. package/server/dist/features/view/services/mcp-read.js +142 -0
  144. package/server/dist/features/view/services/workspace.js +12 -4
  145. package/server/dist/modules/auth-guard.js +24 -1
  146. package/server/dist/modules/integration-token.js +27 -0
  147. package/server/dist/modules/mcp-auth.js +37 -23
  148. package/server/dist/modules/mcp-token.js +6 -25
  149. package/server/dist/modules/server-events.js +3 -0
  150. package/server/dist/routes/api.js +2 -0
  151. package/server/dist/routes/apps.js +163 -0
  152. package/server/dist/routes/graphql.js +2 -1
  153. package/server/dist/routes/index.js +2 -0
  154. package/server/dist/routes/integrations.js +137 -0
  155. package/server/dist/routes/mcp.js +29 -9
  156. package/server/prisma/migrations/20260917120000_0020_plugin_installations/migration.sql +39 -0
  157. package/server/prisma/migrations/20260917150000_0021_integration_connections/migration.sql +29 -0
  158. package/server/prisma/migrations/20260919090000_0022_integration_proxy_url/migration.sql +2 -0
  159. package/server/prisma/schema.prisma +36 -10
  160. package/server/client/dist/assets/Calendar-DwQBllfw.js +0 -25
  161. package/server/client/dist/assets/Callout-CcYyvHQP.js +0 -1
  162. package/server/client/dist/assets/Note-CpzLtGVM.js +0 -19
  163. package/server/client/dist/assets/TagNotes-B27G1ZVg.js +0 -1
  164. package/server/client/dist/assets/Trash.es-XEcWG_aq.js +0 -1
  165. package/server/client/dist/assets/app-EwvT7bMw.css +0 -1
  166. package/server/client/dist/assets/app-NgiBDISG.js +0 -151
  167. package/server/client/dist/assets/channel-DZFalh-h.js +0 -1
  168. package/server/client/dist/assets/classDiagram-JCYQIIEL-CkEhFvVV.js +0 -1
  169. package/server/client/dist/assets/classDiagram-v2-OCEON4UE-CkEhFvVV.js +0 -1
  170. package/server/client/dist/assets/index-CIEap4bu.js +0 -1
  171. package/server/client/dist/assets/manage-image-CSJ3Flgc.js +0 -1
  172. package/server/client/dist/assets/mcp-D8footel.js +0 -5
  173. package/server/client/dist/assets/search-ddYl3qH-.js +0 -1
  174. package/server/client/dist/assets/swimlanesDiagram-ULZ7WXOC-Ci4mcvmo.js +0 -8
  175. package/server/client/dist/assets/time-PxWila4-.js +0 -361
  176. package/server/client/dist/assets/url-DKUMNVCI.js +0 -1
  177. package/server/dist/features/search/note-change.js +0 -16
@@ -0,0 +1,17 @@
1
+ import { EventEmitter } from "node:events";
2
+ const INTEGRATION_ACCESS_CHANGE_CHANNEL = "integration-access-change";
3
+ const integrationAccessEmitter = new EventEmitter();
4
+ integrationAccessEmitter.setMaxListeners(0);
5
+ const emitIntegrationAccessChanged = (connectionId) => {
6
+ integrationAccessEmitter.emit(INTEGRATION_ACCESS_CHANGE_CHANNEL, connectionId);
7
+ };
8
+ const subscribeIntegrationAccessChanges = (listener) => {
9
+ integrationAccessEmitter.on(INTEGRATION_ACCESS_CHANGE_CHANNEL, listener);
10
+ return () => {
11
+ integrationAccessEmitter.off(INTEGRATION_ACCESS_CHANGE_CHANNEL, listener);
12
+ };
13
+ };
14
+ export {
15
+ emitIntegrationAccessChanged,
16
+ subscribeIntegrationAccessChanges
17
+ };
@@ -0,0 +1,21 @@
1
+ import { enforceMcpCompatibility } from "../../modules/mcp-auth.js";
2
+ import { createIntegrationService } from "./service.js";
3
+ const requireIntegrationPermission = (permission, service = createIntegrationService()) => async (request, reply) => {
4
+ const header = request.headers.authorization;
5
+ if (!header?.startsWith("Bearer ") || header.length > 1024) {
6
+ return reply.status(401).send({ code: "UNAUTHORIZED", message: "An integration bearer token is required." });
7
+ }
8
+ const principal = await service.authenticate(header.slice(7).trim());
9
+ if (!principal)
10
+ return reply.status(403).send({
11
+ code: "INTEGRATION_ACCESS_DENIED",
12
+ message: "The integration token is invalid, revoked, or disabled."
13
+ });
14
+ if (!principal.permissions.includes(permission))
15
+ return reply.status(403).send({ code: "INTEGRATION_PERMISSION_DENIED", message: `Permission required: ${permission}` });
16
+ request.integration = principal;
17
+ if (principal.native && principal.connectionId === "mcp") await enforceMcpCompatibility(request, reply);
18
+ };
19
+ export {
20
+ requireIntegrationPermission
21
+ };
@@ -0,0 +1,116 @@
1
+ import { createAppError } from "../../modules/error-handler.js";
2
+ const INTEGRATION_PERMISSIONS = ["notes:read", "notes:create", "notes:update", "notes:delete"];
3
+ const MCP_CONNECTION_ID = "mcp";
4
+ const MCP_INTEGRATION_MANIFEST = {
5
+ schemaVersion: 1,
6
+ apiVersion: 1,
7
+ id: "ocean-brain.mcp",
8
+ name: "MCP",
9
+ version: "1.0.0",
10
+ description: "Connect AI clients to Ocean Brain.",
11
+ permissions: [...INTEGRATION_PERMISSIONS]
12
+ };
13
+ const NATIVE_INTEGRATIONS = [{ connectionId: MCP_CONNECTION_ID, manifest: MCP_INTEGRATION_MANIFEST }];
14
+ const invalid = (message) => {
15
+ throw createAppError(400, "INVALID_INTEGRATION_MANIFEST", message);
16
+ };
17
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
18
+ const parsePermissions = (value) => {
19
+ if (!Array.isArray(value) || value.some((item) => !INTEGRATION_PERMISSIONS.includes(item))) {
20
+ return invalid("Permissions must be a list of supported note permissions.");
21
+ }
22
+ return [...new Set(value)];
23
+ };
24
+ const textField = (value, name, max) => {
25
+ if (typeof value !== "string" || !value.trim() || value.length > max) {
26
+ return invalid(`${name} must be a nonempty string of at most ${max} characters.`);
27
+ }
28
+ return value.trim();
29
+ };
30
+ const parseManifest = (value) => {
31
+ if (!isRecord(value)) return invalid("An integration manifest object is required.");
32
+ if (value.schemaVersion !== 1 || value.apiVersion !== 1) {
33
+ return invalid("Only manifest schemaVersion 1 and apiVersion 1 are supported.");
34
+ }
35
+ const id = textField(value.id, "id", 100);
36
+ if (!/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/.test(id))
37
+ return invalid("id must be a lowercase dotted or dashed identifier.");
38
+ const manifest = {
39
+ schemaVersion: 1,
40
+ apiVersion: 1,
41
+ id,
42
+ name: textField(value.name, "name", 80),
43
+ version: textField(value.version, "version", 40),
44
+ description: textField(value.description, "description", 500),
45
+ permissions: parsePermissions(value.permissions)
46
+ };
47
+ if (manifest.permissions.some((permission) => permission !== "notes:read") && !manifest.permissions.includes("notes:read")) {
48
+ return invalid("An integration requesting write access must also request notes:read.");
49
+ }
50
+ if (value.launch !== void 0) {
51
+ if (!isRecord(value.launch) || !["external", "iframe", "proxied"].includes(String(value.launch.mode))) {
52
+ return invalid("launch requires proxied mode or a url with external or iframe mode.");
53
+ }
54
+ if (value.launch.mode === "proxied") {
55
+ if (value.launch.url !== void 0) return invalid("A proxied launch cannot provide a url.");
56
+ manifest.launch = { mode: "proxied" };
57
+ return manifest;
58
+ }
59
+ let url;
60
+ try {
61
+ url = new URL(textField(value.launch.url, "launch.url", 2e3));
62
+ } catch {
63
+ return invalid("launch.url must be an absolute HTTP(S) URL.");
64
+ }
65
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
66
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback) || url.username || url.password) {
67
+ return invalid("launch.url requires HTTPS (HTTP is allowed on loopback only), without credentials.");
68
+ }
69
+ manifest.launch = { url: url.href, mode: value.launch.mode === "iframe" ? "iframe" : "external" };
70
+ }
71
+ return manifest;
72
+ };
73
+ const parseIntegrationProxyUrl = (value) => {
74
+ if (typeof value !== "string" || !value.trim() || value.length > 2e3) {
75
+ throw createAppError(
76
+ 400,
77
+ "INVALID_INTEGRATION_PROXY_URL",
78
+ "The private app URL must be a nonempty string of at most 2000 characters."
79
+ );
80
+ }
81
+ let url;
82
+ try {
83
+ url = new URL(value.trim());
84
+ } catch {
85
+ throw createAppError(400, "INVALID_INTEGRATION_PROXY_URL", "The private app URL must be an absolute URL.");
86
+ }
87
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
88
+ throw createAppError(
89
+ 400,
90
+ "INVALID_INTEGRATION_PROXY_URL",
91
+ "The private app URL must be an HTTP(S) origin without credentials, a path, query, or fragment."
92
+ );
93
+ }
94
+ return url.origin;
95
+ };
96
+ const validateGrants = (value, manifest) => {
97
+ const permissions = parsePermissions(value);
98
+ if (permissions.some((permission) => !manifest.permissions.includes(permission))) {
99
+ throw createAppError(400, "INVALID_INTEGRATION_GRANTS", "A grant must be requested by the manifest.");
100
+ }
101
+ if (permissions.some((permission) => permission !== "notes:read") && !permissions.includes("notes:read")) {
102
+ throw createAppError(400, "INVALID_INTEGRATION_GRANTS", "Write permissions also require notes:read.");
103
+ }
104
+ return permissions;
105
+ };
106
+ export {
107
+ INTEGRATION_PERMISSIONS,
108
+ MCP_CONNECTION_ID,
109
+ MCP_INTEGRATION_MANIFEST,
110
+ NATIVE_INTEGRATIONS,
111
+ isRecord,
112
+ parseIntegrationProxyUrl,
113
+ parseManifest,
114
+ parsePermissions,
115
+ validateGrants
116
+ };
@@ -0,0 +1,156 @@
1
+ import { createHash } from "node:crypto";
2
+ import { subscribeNoteChanges } from "../../features/note/services/change-events.js";
3
+ import models from "../../models.js";
4
+ import { createAppError } from "../../modules/error-handler.js";
5
+ import { subscribeIntegrationAccessChanges } from "./access-events.js";
6
+ const DEFAULT_PAGE_SIZE = 100;
7
+ const MAX_PAGE_SIZE = 500;
8
+ const KEEP_ALIVE_INTERVAL_MS = 3e4;
9
+ const MAX_STREAMS_PER_CONNECTION = 2;
10
+ const MAX_NOTE_ID = 2147483647;
11
+ const activeStreamsByConnection = /* @__PURE__ */ new Map();
12
+ const resolvePageSize = (value) => {
13
+ if (value === void 0) return DEFAULT_PAGE_SIZE;
14
+ if (!Number.isInteger(value) || Number(value) < 1 || Number(value) > MAX_PAGE_SIZE) {
15
+ throw createAppError(
16
+ 400,
17
+ "INVALID_NOTE_CATALOG_PAGE_SIZE",
18
+ `limit must be an integer between 1 and ${MAX_PAGE_SIZE}.`
19
+ );
20
+ }
21
+ return Number(value);
22
+ };
23
+ const resolveAfterId = (value) => {
24
+ if (value === void 0) return 0;
25
+ const normalized = typeof value === "number" && Number.isSafeInteger(value) ? String(value) : value;
26
+ if (typeof normalized !== "string" || !/^(0|[1-9]\d*)$/.test(normalized)) {
27
+ throw createAppError(400, "INVALID_NOTE_CATALOG_CURSOR", "afterId must be a nonnegative integer.");
28
+ }
29
+ const parsed = Number(normalized);
30
+ if (!Number.isSafeInteger(parsed) || parsed > MAX_NOTE_ID) {
31
+ throw createAppError(400, "INVALID_NOTE_CATALOG_CURSOR", "afterId exceeds the supported range.");
32
+ }
33
+ return parsed;
34
+ };
35
+ const createIntegrationNoteCatalogHandler = () => {
36
+ return async (request, reply) => {
37
+ const limit = resolvePageSize(request.body?.limit);
38
+ const afterId = resolveAfterId(request.body?.afterId);
39
+ const [rows, propertySchema] = await Promise.all([
40
+ models.note.findMany({
41
+ where: { id: { gt: afterId } },
42
+ orderBy: { id: "asc" },
43
+ take: limit + 1,
44
+ select: { id: true, updatedAt: true }
45
+ }),
46
+ afterId === 0 ? models.propertyDefinition.findMany({
47
+ orderBy: { key: "asc" },
48
+ select: {
49
+ key: true,
50
+ name: true,
51
+ valueType: true,
52
+ updatedAt: true,
53
+ options: {
54
+ orderBy: [{ order: "asc" }, { id: "asc" }],
55
+ select: {
56
+ id: true,
57
+ label: true,
58
+ value: true,
59
+ color: true,
60
+ order: true,
61
+ updatedAt: true
62
+ }
63
+ }
64
+ }
65
+ }) : Promise.resolve(null)
66
+ ]);
67
+ const hasMore = rows.length > limit;
68
+ const page = hasMore ? rows.slice(0, limit) : rows;
69
+ return reply.status(200).send({
70
+ notes: page.map((note) => ({ id: String(note.id), updatedAt: note.updatedAt.toISOString() })),
71
+ ...propertySchema ? { propertySchemaHash: createHash("sha256").update(JSON.stringify(propertySchema)).digest("hex") } : {},
72
+ hasMore,
73
+ nextAfterId: hasMore ? String(page.at(-1)?.id) : null
74
+ });
75
+ };
76
+ };
77
+ const serializeIntegrationNoteEvent = (event) => {
78
+ const data = {
79
+ type: event.type,
80
+ noteId: String(event.noteId),
81
+ occurredAt: event.occurredAt
82
+ };
83
+ return `event: ${event.type}
84
+ data: ${JSON.stringify(data)}
85
+
86
+ `;
87
+ };
88
+ const createIntegrationNoteEventsHandler = (subscribe = subscribeNoteChanges) => {
89
+ return async (request, reply) => {
90
+ const connectionId = request.integration?.connectionId;
91
+ if (!connectionId) {
92
+ throw createAppError(401, "UNAUTHORIZED", "An authenticated integration connection is required.");
93
+ }
94
+ const activeStreamCount = activeStreamsByConnection.get(connectionId) ?? 0;
95
+ if (activeStreamCount >= MAX_STREAMS_PER_CONNECTION) {
96
+ throw createAppError(
97
+ 429,
98
+ "INTEGRATION_EVENT_STREAM_LIMIT",
99
+ `A connection can open at most ${MAX_STREAMS_PER_CONNECTION} event streams.`
100
+ );
101
+ }
102
+ reply.hijack();
103
+ const response = reply.raw;
104
+ response.statusCode = 200;
105
+ response.setHeader("Content-Type", "text/event-stream; charset=utf-8");
106
+ response.setHeader("Cache-Control", "no-cache, no-transform");
107
+ response.setHeader("Connection", "keep-alive");
108
+ response.setHeader("X-Accel-Buffering", "no");
109
+ response.flushHeaders();
110
+ response.write(": connected\n\n");
111
+ activeStreamsByConnection.set(connectionId, activeStreamCount + 1);
112
+ let cleanedUp = false;
113
+ let keepAliveTimer = null;
114
+ let unsubscribeNoteChanges = () => void 0;
115
+ let unsubscribeAccessChanges = () => void 0;
116
+ const cleanup = () => {
117
+ if (cleanedUp) return;
118
+ cleanedUp = true;
119
+ if (keepAliveTimer) clearInterval(keepAliveTimer);
120
+ unsubscribeNoteChanges();
121
+ unsubscribeAccessChanges();
122
+ const remainingStreams = (activeStreamsByConnection.get(connectionId) ?? 1) - 1;
123
+ if (remainingStreams > 0) activeStreamsByConnection.set(connectionId, remainingStreams);
124
+ else activeStreamsByConnection.delete(connectionId);
125
+ };
126
+ const endStream = () => {
127
+ cleanup();
128
+ if (!response.writableEnded) {
129
+ try {
130
+ response.end();
131
+ } catch {
132
+ }
133
+ }
134
+ };
135
+ const write = (chunk) => {
136
+ try {
137
+ if (!response.write(chunk)) endStream();
138
+ } catch {
139
+ endStream();
140
+ }
141
+ };
142
+ unsubscribeNoteChanges = subscribe((event) => write(serializeIntegrationNoteEvent(event)));
143
+ unsubscribeAccessChanges = subscribeIntegrationAccessChanges((changedConnectionId) => {
144
+ if (changedConnectionId === connectionId) endStream();
145
+ });
146
+ keepAliveTimer = setInterval(() => write(": keepalive\n\n"), KEEP_ALIVE_INTERVAL_MS);
147
+ keepAliveTimer.unref?.();
148
+ request.raw.on("close", cleanup);
149
+ response.on("close", cleanup);
150
+ };
151
+ };
152
+ export {
153
+ createIntegrationNoteCatalogHandler,
154
+ createIntegrationNoteEventsHandler,
155
+ serializeIntegrationNoteEvent
156
+ };
@@ -0,0 +1,54 @@
1
+ import { GraphQLError, GraphQLObjectType, GraphQLSchema } from "graphql";
2
+ import { readMarkdownRange } from "../../features/note/services/markdown-read.js";
3
+ import models from "../../models.js";
4
+ import schema from "../../schema/index.js";
5
+ const READ_FIELDS = /* @__PURE__ */ new Set([
6
+ "allNotes",
7
+ "note",
8
+ "noteRead",
9
+ "backReferences",
10
+ "notesByTagNames",
11
+ "allTags",
12
+ "tagsByNames",
13
+ "notePropertyKeys",
14
+ "notesByQuery",
15
+ "notesByProperties",
16
+ "viewSections",
17
+ "readViewSection",
18
+ "searchNotes"
19
+ ]);
20
+ const readNote = async (_, { id }) => {
21
+ if (!/^[1-9]\d*$/.test(id) || !Number.isSafeInteger(Number(id))) throw new GraphQLError("Invalid note id.");
22
+ const note = await models.note.findUnique({ where: { id: Number(id) } });
23
+ if (!note) throw new GraphQLError("Note not found.");
24
+ return note;
25
+ };
26
+ const query = schema.getQueryType();
27
+ if (!query) throw new Error("The core query schema is missing.");
28
+ const fields = Object.fromEntries(Object.entries(query.toConfig().fields).filter(([name]) => READ_FIELDS.has(name)));
29
+ fields.note = { ...fields.note, resolve: readNote };
30
+ fields.noteRead = {
31
+ ...fields.noteRead,
32
+ resolve: async (_, { id, ...input }) => {
33
+ const note = await readNote(_, { id });
34
+ const { blocksToMarkdown } = await import("../../modules/blocknote.js");
35
+ return { note, ...readMarkdownRange(await blocksToMarkdown(note.content), note.updatedAt, input) };
36
+ }
37
+ };
38
+ for (const field of Object.values(fields)) {
39
+ const resolve = field.resolve;
40
+ if (!resolve) continue;
41
+ field.resolve = (source, args, context, info) => {
42
+ if (args.pagination && (!Number.isInteger(args.pagination.limit) || args.pagination.limit < 1 || args.pagination.limit > 100 || !Number.isInteger(args.pagination.offset) || args.pagination.offset < 0)) {
43
+ throw new GraphQLError("pagination requires limit 1\u2013100 and offset >= 0.");
44
+ }
45
+ const normalizedArgs = { ...args };
46
+ if (field.args?.pagination && !args.pagination) normalizedArgs.pagination = { limit: 25, offset: 0 };
47
+ if (field.args?.searchFilter && !args.searchFilter) normalizedArgs.searchFilter = { query: "" };
48
+ return resolve(source, normalizedArgs, context, info);
49
+ };
50
+ }
51
+ const integrationReadSchema = new GraphQLSchema({ query: new GraphQLObjectType({ name: "Query", fields }) });
52
+ export {
53
+ integrationReadSchema
54
+ };
@@ -0,0 +1,246 @@
1
+ import { createHash } from "node:crypto";
2
+ import models from "../../models.js";
3
+ import { createAppError } from "../../modules/error-handler.js";
4
+ import { issueIntegrationToken } from "../../modules/integration-token.js";
5
+ import { emitIntegrationAccessChanged } from "./access-events.js";
6
+ import {
7
+ MCP_CONNECTION_ID,
8
+ NATIVE_INTEGRATIONS,
9
+ parseIntegrationProxyUrl,
10
+ parseManifest,
11
+ parsePermissions,
12
+ validateGrants
13
+ } from "./manifest.js";
14
+ import { parseStatusUpdate, readStatusReport } from "./status-report.js";
15
+ const resolveManifest = (row) => {
16
+ if (row.native) {
17
+ const definition = NATIVE_INTEGRATIONS.find((integration) => integration.connectionId === row.id);
18
+ if (definition) return definition.manifest;
19
+ }
20
+ return parseManifest(JSON.parse(row.manifest));
21
+ };
22
+ const toConnection = (row) => ({
23
+ id: row.id,
24
+ native: row.native,
25
+ manifest: resolveManifest(row),
26
+ proxyConfigured: row.proxyUrl !== null,
27
+ grantedPermissions: parsePermissions(JSON.parse(row.grantedPermissions)),
28
+ enabled: row.enabled,
29
+ pinned: row.pinned,
30
+ createdAt: row.createdAt.toISOString(),
31
+ updatedAt: row.updatedAt.toISOString(),
32
+ statusReport: readStatusReport(row.credential?.statusReport ?? null),
33
+ token: row.credential ? {
34
+ id: row.credential.id,
35
+ createdAt: row.credential.createdAt.toISOString(),
36
+ lastUsedAt: row.credential.lastUsedAt?.toISOString() ?? null
37
+ } : null
38
+ });
39
+ const credentialSummary = { select: { id: true, createdAt: true, lastUsedAt: true, statusReport: true } };
40
+ const parseConnectionProxyUrl = (manifest, value) => {
41
+ if (manifest.launch?.mode === "proxied") return parseIntegrationProxyUrl(value);
42
+ if (value !== void 0) {
43
+ throw createAppError(
44
+ 400,
45
+ "UNEXPECTED_INTEGRATION_PROXY_URL",
46
+ "A private app URL can only be configured for proxied launches."
47
+ );
48
+ }
49
+ return null;
50
+ };
51
+ const createIntegrationService = (db = models) => {
52
+ const get = async (id) => {
53
+ const row = await db.integrationConnection.findUnique({
54
+ where: { id },
55
+ include: { credential: credentialSummary }
56
+ });
57
+ if (!row) throw createAppError(404, "INTEGRATION_NOT_FOUND", "The integration connection was not found.");
58
+ return toConnection(row);
59
+ };
60
+ return {
61
+ get,
62
+ async ensureNativeConnections() {
63
+ for (const integration of NATIVE_INTEGRATIONS) {
64
+ await db.integrationConnection.upsert({
65
+ where: { id: integration.connectionId },
66
+ create: {
67
+ id: integration.connectionId,
68
+ integrationId: integration.manifest.id,
69
+ native: true,
70
+ manifest: JSON.stringify(integration.manifest),
71
+ grantedPermissions: JSON.stringify(integration.manifest.permissions)
72
+ },
73
+ // Keep the owner's grants, enabled state and credentials on upgrade.
74
+ update: {}
75
+ });
76
+ }
77
+ },
78
+ async list() {
79
+ const rows = await db.integrationConnection.findMany({
80
+ orderBy: [{ native: "desc" }, { createdAt: "asc" }],
81
+ include: { credential: credentialSummary }
82
+ });
83
+ return rows.map(toConnection);
84
+ },
85
+ async connect(input) {
86
+ const manifest = parseManifest(input.manifest);
87
+ if (manifest.id.startsWith("ocean-brain."))
88
+ throw createAppError(
89
+ 400,
90
+ "RESERVED_INTEGRATION_ID",
91
+ "The ocean-brain namespace is reserved for native integrations."
92
+ );
93
+ const grantedPermissions = validateGrants(input.grantedPermissions ?? [], manifest);
94
+ const proxyUrl = parseConnectionProxyUrl(manifest, input.proxyUrl);
95
+ const row = await db.integrationConnection.create({
96
+ data: {
97
+ integrationId: manifest.id,
98
+ manifest: JSON.stringify(manifest),
99
+ proxyUrl,
100
+ grantedPermissions: JSON.stringify(grantedPermissions)
101
+ },
102
+ include: { credential: credentialSummary }
103
+ });
104
+ return toConnection(row);
105
+ },
106
+ async update(id, input) {
107
+ const connection = await db.$transaction(async (tx) => {
108
+ const current = await tx.integrationConnection.findUnique({ where: { id } });
109
+ if (!current)
110
+ throw createAppError(404, "INTEGRATION_NOT_FOUND", "The integration connection was not found.");
111
+ if (input.manifest !== void 0 && current.native)
112
+ throw createAppError(
113
+ 400,
114
+ "NATIVE_INTEGRATION_MANIFEST",
115
+ "Native integration manifests are managed by Ocean Brain."
116
+ );
117
+ for (const key of ["enabled", "pinned"]) {
118
+ if (input[key] !== void 0 && typeof input[key] !== "boolean")
119
+ throw createAppError(400, "INVALID_INTEGRATION_SETTING", `${key} must be a boolean.`);
120
+ }
121
+ const currentManifest = resolveManifest(current);
122
+ const manifest = input.manifest === void 0 ? currentManifest : parseManifest(input.manifest);
123
+ if (manifest.id !== current.integrationId)
124
+ throw createAppError(400, "INTEGRATION_ID_CHANGED", "An upgrade cannot change the integration id.");
125
+ const previousGrants = parsePermissions(JSON.parse(current.grantedPermissions)).filter(
126
+ (permission) => manifest.permissions.includes(permission)
127
+ );
128
+ const grantedPermissions = validateGrants(input.grantedPermissions ?? previousGrants, manifest);
129
+ let proxyUrl = current.proxyUrl;
130
+ if (manifest.launch?.mode === "proxied") {
131
+ if (input.proxyUrl !== void 0) proxyUrl = parseIntegrationProxyUrl(input.proxyUrl);
132
+ const enteringProxiedMode = input.manifest !== void 0 && currentManifest.launch?.mode !== "proxied";
133
+ if ((enteringProxiedMode || input.enabled === true) && !proxyUrl) {
134
+ throw createAppError(
135
+ 400,
136
+ "INTEGRATION_PROXY_URL_REQUIRED",
137
+ "Configure a private app URL before enabling a proxied integration."
138
+ );
139
+ }
140
+ } else {
141
+ if (input.proxyUrl !== void 0) {
142
+ throw createAppError(
143
+ 400,
144
+ "UNEXPECTED_INTEGRATION_PROXY_URL",
145
+ "A private app URL can only be configured for proxied launches."
146
+ );
147
+ }
148
+ proxyUrl = null;
149
+ }
150
+ if (input.enabled !== void 0 || input.grantedPermissions !== void 0 || input.manifest !== void 0 || input.proxyUrl !== void 0) {
151
+ await tx.integrationCredential.updateMany({
152
+ where: { connectionId: id },
153
+ data: { statusReport: null }
154
+ });
155
+ }
156
+ const row = await tx.integrationConnection.update({
157
+ where: { id },
158
+ data: {
159
+ manifest: JSON.stringify(manifest),
160
+ proxyUrl,
161
+ grantedPermissions: JSON.stringify(grantedPermissions),
162
+ ...typeof input.enabled === "boolean" ? { enabled: input.enabled } : {},
163
+ ...typeof input.pinned === "boolean" ? { pinned: input.pinned } : {}
164
+ },
165
+ include: { credential: credentialSummary }
166
+ });
167
+ return toConnection(row);
168
+ });
169
+ if (input.enabled !== void 0 || input.grantedPermissions !== void 0 || input.manifest !== void 0) {
170
+ emitIntegrationAccessChanged(id);
171
+ }
172
+ return connection;
173
+ },
174
+ async disconnect(id) {
175
+ const connection = await get(id);
176
+ if (connection.native || id === MCP_CONNECTION_ID)
177
+ throw createAppError(
178
+ 400,
179
+ "NATIVE_INTEGRATION_REQUIRED",
180
+ "Native integrations can be disabled, but cannot be disconnected."
181
+ );
182
+ await db.integrationConnection.delete({ where: { id } });
183
+ emitIntegrationAccessChanged(id);
184
+ },
185
+ async rotateToken(id) {
186
+ await get(id);
187
+ const token = issueIntegrationToken();
188
+ await db.integrationCredential.upsert({
189
+ where: { connectionId: id },
190
+ create: { connectionId: id, tokenHash: token.hash },
191
+ update: { tokenHash: token.hash, createdAt: /* @__PURE__ */ new Date(), lastUsedAt: null, statusReport: null }
192
+ });
193
+ emitIntegrationAccessChanged(id);
194
+ return { token: token.plaintext };
195
+ },
196
+ async revokeToken(id) {
197
+ await get(id);
198
+ await db.integrationCredential.deleteMany({ where: { connectionId: id } });
199
+ emitIntegrationAccessChanged(id);
200
+ },
201
+ async reportStatus(token, input) {
202
+ const update = parseStatusUpdate(input);
203
+ const tokenHash = createHash("sha256").update(token, "utf8").digest("hex");
204
+ return db.$transaction(async (tx) => {
205
+ const credential = await tx.integrationCredential.findUnique({
206
+ where: { tokenHash },
207
+ include: { connection: true }
208
+ });
209
+ if (!credential?.connection.enabled || !parsePermissions(JSON.parse(credential.connection.grantedPermissions)).includes("notes:read") || !resolveManifest(credential.connection).permissions.includes("notes:read")) {
210
+ throw createAppError(403, "INTEGRATION_ACCESS_DENIED", "This connection cannot report activity.");
211
+ }
212
+ const report = { ...update, reportedAt: (/* @__PURE__ */ new Date()).toISOString() };
213
+ await tx.integrationCredential.update({
214
+ where: { tokenHash },
215
+ data: { statusReport: JSON.stringify(report) }
216
+ });
217
+ return report;
218
+ });
219
+ },
220
+ async authenticate(token) {
221
+ const tokenHash = createHash("sha256").update(token, "utf8").digest("hex");
222
+ const credential = await db.integrationCredential.findUnique({
223
+ where: { tokenHash },
224
+ include: { connection: true }
225
+ });
226
+ if (!credential?.connection.enabled) return null;
227
+ const connection = credential.connection;
228
+ const touched = await db.integrationCredential.updateMany({
229
+ where: { connectionId: connection.id, tokenHash },
230
+ data: { lastUsedAt: /* @__PURE__ */ new Date() }
231
+ });
232
+ if (!touched.count) return null;
233
+ return {
234
+ connectionId: connection.id,
235
+ integrationId: connection.integrationId,
236
+ native: connection.native,
237
+ permissions: parsePermissions(JSON.parse(connection.grantedPermissions)).filter(
238
+ (permission) => resolveManifest(connection).permissions.includes(permission)
239
+ )
240
+ };
241
+ }
242
+ };
243
+ };
244
+ export {
245
+ createIntegrationService
246
+ };
@@ -0,0 +1,22 @@
1
+ import { createAppError } from "../../modules/error-handler.js";
2
+ import { isRecord } from "./manifest.js";
3
+ function parseStatusUpdate(value) {
4
+ if (!isRecord(value) || value.state !== "running" && value.state !== "succeeded" && value.state !== "failed" || typeof value.message !== "string" || !value.message.trim() || value.message.length > 300 || [...value.message].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127)) {
5
+ throw createAppError(
6
+ 400,
7
+ "INVALID_INTEGRATION_STATUS",
8
+ "Provide a running, succeeded, or failed state and a message of 1\u2013300 characters without control characters."
9
+ );
10
+ }
11
+ return { state: value.state, message: value.message.trim() };
12
+ }
13
+ function readStatusReport(value) {
14
+ if (!value) return null;
15
+ const report = JSON.parse(value);
16
+ if (!isRecord(report) || typeof report.reportedAt !== "string") return null;
17
+ return { ...parseStatusUpdate(report), reportedAt: report.reportedAt };
18
+ }
19
+ export {
20
+ parseStatusUpdate,
21
+ readStatusReport
22
+ };