@cyberismo/backend 0.0.21 → 0.0.23

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 (152) hide show
  1. package/dist/app.d.ts +5 -2
  2. package/dist/app.js +25 -10
  3. package/dist/app.js.map +1 -1
  4. package/dist/auth/index.d.ts +16 -0
  5. package/dist/auth/index.js +15 -0
  6. package/dist/auth/index.js.map +1 -0
  7. package/dist/auth/keycloak.d.ts +27 -0
  8. package/dist/auth/keycloak.js +81 -0
  9. package/dist/auth/keycloak.js.map +1 -0
  10. package/dist/auth/mock.d.ts +23 -0
  11. package/dist/auth/mock.js +28 -0
  12. package/dist/auth/mock.js.map +1 -0
  13. package/dist/auth/types.d.ts +16 -0
  14. package/dist/auth/types.js +14 -0
  15. package/dist/auth/types.js.map +1 -0
  16. package/dist/domain/auth/index.d.ts +14 -0
  17. package/dist/domain/auth/index.js +30 -0
  18. package/dist/domain/auth/index.js.map +1 -0
  19. package/dist/domain/calculations/index.js +3 -1
  20. package/dist/domain/calculations/index.js.map +1 -1
  21. package/dist/domain/calculations/service.js +13 -11
  22. package/dist/domain/calculations/service.js.map +1 -1
  23. package/dist/domain/cardTypes/index.js +5 -3
  24. package/dist/domain/cardTypes/index.js.map +1 -1
  25. package/dist/domain/cardTypes/service.js +24 -72
  26. package/dist/domain/cardTypes/service.js.map +1 -1
  27. package/dist/domain/cards/index.js +124 -25
  28. package/dist/domain/cards/index.js.map +1 -1
  29. package/dist/domain/cards/lib.js +92 -93
  30. package/dist/domain/cards/lib.js.map +1 -1
  31. package/dist/domain/cards/presence.d.ts +50 -0
  32. package/dist/domain/cards/presence.js +93 -0
  33. package/dist/domain/cards/presence.js.map +1 -0
  34. package/dist/domain/cards/schema.d.ts +47 -0
  35. package/dist/domain/cards/schema.js +37 -0
  36. package/dist/domain/cards/schema.js.map +1 -0
  37. package/dist/domain/cards/service.d.ts +7 -3
  38. package/dist/domain/cards/service.js +81 -91
  39. package/dist/domain/cards/service.js.map +1 -1
  40. package/dist/domain/connectors/index.d.ts +15 -0
  41. package/dist/domain/connectors/index.js +37 -0
  42. package/dist/domain/connectors/index.js.map +1 -0
  43. package/dist/domain/connectors/service.d.ts +23 -0
  44. package/dist/domain/connectors/service.js +46 -0
  45. package/dist/domain/connectors/service.js.map +1 -0
  46. package/dist/domain/fieldTypes/index.js +4 -2
  47. package/dist/domain/fieldTypes/index.js.map +1 -1
  48. package/dist/domain/graphModels/index.js +3 -1
  49. package/dist/domain/graphModels/index.js.map +1 -1
  50. package/dist/domain/graphViews/index.js +3 -1
  51. package/dist/domain/graphViews/index.js.map +1 -1
  52. package/dist/domain/labels/index.js +4 -2
  53. package/dist/domain/labels/index.js.map +1 -1
  54. package/dist/domain/labels/service.d.ts +1 -1
  55. package/dist/domain/labels/service.js +2 -2
  56. package/dist/domain/labels/service.js.map +1 -1
  57. package/dist/domain/linkTypes/index.js +4 -2
  58. package/dist/domain/linkTypes/index.js.map +1 -1
  59. package/dist/domain/logicPrograms/index.js +3 -1
  60. package/dist/domain/logicPrograms/index.js.map +1 -1
  61. package/dist/domain/mcp/index.d.ts +15 -0
  62. package/dist/domain/mcp/index.js +127 -0
  63. package/dist/domain/mcp/index.js.map +1 -0
  64. package/dist/domain/project/index.js +19 -6
  65. package/dist/domain/project/index.js.map +1 -1
  66. package/dist/domain/project/schema.d.ts +3 -0
  67. package/dist/domain/project/schema.js +8 -0
  68. package/dist/domain/project/schema.js.map +1 -1
  69. package/dist/domain/project/service.d.ts +3 -1
  70. package/dist/domain/project/service.js +24 -14
  71. package/dist/domain/project/service.js.map +1 -1
  72. package/dist/domain/reports/index.js +3 -1
  73. package/dist/domain/reports/index.js.map +1 -1
  74. package/dist/domain/resources/index.js +6 -4
  75. package/dist/domain/resources/index.js.map +1 -1
  76. package/dist/domain/resources/service.js +66 -64
  77. package/dist/domain/resources/service.js.map +1 -1
  78. package/dist/domain/templates/index.js +5 -3
  79. package/dist/domain/templates/index.js.map +1 -1
  80. package/dist/domain/tree/index.js +3 -1
  81. package/dist/domain/tree/index.js.map +1 -1
  82. package/dist/domain/tree/service.js +0 -1
  83. package/dist/domain/tree/service.js.map +1 -1
  84. package/dist/domain/workflows/index.js +3 -1
  85. package/dist/domain/workflows/index.js.map +1 -1
  86. package/dist/export.d.ts +6 -5
  87. package/dist/export.js +16 -13
  88. package/dist/export.js.map +1 -1
  89. package/dist/index.d.ts +8 -2
  90. package/dist/index.js +12 -4
  91. package/dist/index.js.map +1 -1
  92. package/dist/main.js +29 -2
  93. package/dist/main.js.map +1 -1
  94. package/dist/middleware/auth.d.ts +40 -0
  95. package/dist/middleware/auth.js +68 -0
  96. package/dist/middleware/auth.js.map +1 -0
  97. package/dist/middleware/commandManager.d.ts +2 -2
  98. package/dist/middleware/commandManager.js +9 -11
  99. package/dist/middleware/commandManager.js.map +1 -1
  100. package/dist/public/THIRD-PARTY.txt +1212 -605
  101. package/dist/public/assets/index-Cdn_jRWy.js +720 -0
  102. package/dist/public/assets/index-ypsafPwV.css +1 -0
  103. package/dist/public/config.json +1 -0
  104. package/dist/public/images/broken_link.svg +7 -0
  105. package/dist/public/index.html +2 -2
  106. package/dist/types.d.ts +25 -0
  107. package/dist/types.js +13 -1
  108. package/dist/types.js.map +1 -1
  109. package/package.json +10 -7
  110. package/src/app.ts +37 -15
  111. package/src/auth/index.ts +17 -0
  112. package/src/auth/keycloak.ts +109 -0
  113. package/src/auth/mock.ts +38 -0
  114. package/src/auth/types.ts +18 -0
  115. package/src/domain/auth/index.ts +35 -0
  116. package/src/domain/calculations/index.ts +13 -6
  117. package/src/domain/calculations/service.ts +16 -14
  118. package/src/domain/cardTypes/index.ts +24 -16
  119. package/src/domain/cardTypes/service.ts +41 -95
  120. package/src/domain/cards/index.ts +258 -90
  121. package/src/domain/cards/lib.ts +102 -100
  122. package/src/domain/cards/presence.ts +124 -0
  123. package/src/domain/cards/schema.ts +41 -0
  124. package/src/domain/cards/service.ts +138 -93
  125. package/src/domain/connectors/index.ts +39 -0
  126. package/src/domain/connectors/service.ts +67 -0
  127. package/src/domain/fieldTypes/index.ts +23 -16
  128. package/src/domain/graphModels/index.ts +13 -6
  129. package/src/domain/graphViews/index.ts +13 -6
  130. package/src/domain/labels/index.ts +5 -2
  131. package/src/domain/labels/service.ts +2 -2
  132. package/src/domain/linkTypes/index.ts +14 -7
  133. package/src/domain/logicPrograms/index.ts +3 -0
  134. package/src/domain/mcp/index.ts +159 -0
  135. package/src/domain/project/index.ts +40 -9
  136. package/src/domain/project/schema.ts +9 -0
  137. package/src/domain/project/service.ts +37 -17
  138. package/src/domain/reports/index.ts +13 -6
  139. package/src/domain/resources/index.ts +6 -1
  140. package/src/domain/resources/service.ts +102 -97
  141. package/src/domain/templates/index.ts +31 -19
  142. package/src/domain/tree/index.ts +3 -1
  143. package/src/domain/tree/service.ts +0 -1
  144. package/src/domain/workflows/index.ts +13 -6
  145. package/src/export.ts +17 -15
  146. package/src/index.ts +18 -7
  147. package/src/main.ts +44 -2
  148. package/src/middleware/auth.ts +90 -0
  149. package/src/middleware/commandManager.ts +11 -14
  150. package/src/types.ts +27 -0
  151. package/dist/public/assets/index-CRSBseQM.css +0 -1
  152. package/dist/public/assets/index-Ca10XaMv.js +0 -164156
@@ -0,0 +1,39 @@
1
+ /**
2
+ Cyberismo
3
+ Copyright © Cyberismo Ltd and contributors 2026
4
+ This program is free software: you can redistribute it and/or modify it under
5
+ the terms of the GNU Affero General Public License version 3 as published by
6
+ the Free Software Foundation. This program is distributed in the hope that it
7
+ will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
8
+ of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
9
+ See the GNU Affero General Public License for more details.
10
+ You should have received a copy of the GNU Affero General Public
11
+ License along with this program. If not, see <https://www.gnu.org/licenses/>.
12
+ */
13
+ import { Hono } from 'hono';
14
+ import { isSSGContext } from 'hono/ssg';
15
+ import * as connectorsService from './service.js';
16
+ import { UserRole } from '../../types.js';
17
+ import { requireRole } from '../../middleware/auth.js';
18
+
19
+ const router = new Hono();
20
+
21
+ /**
22
+ * @openapi
23
+ * /api/connectors:
24
+ * get:
25
+ * summary: Returns all available connectors
26
+ * responses:
27
+ * 200:
28
+ * description: List of connectors with their display names
29
+ */
30
+ router.get('/', requireRole(UserRole.Reader), async (c) => {
31
+ if (isSSGContext(c)) {
32
+ return c.json([]);
33
+ }
34
+ const commands = c.get('commands');
35
+ const connectors = await connectorsService.getConnectors(commands);
36
+ return c.json(connectors);
37
+ });
38
+
39
+ export default router;
@@ -0,0 +1,67 @@
1
+ /**
2
+ Cyberismo
3
+ Copyright © Cyberismo Ltd and contributors 2024
4
+ This program is free software: you can redistribute it and/or modify it under
5
+ the terms of the GNU Affero General Public License version 3 as published by
6
+ the Free Software Foundation.
7
+ This program is distributed in the hope that it will be useful, but WITHOUT
8
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
9
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
10
+ details. You should have received a copy of the GNU Affero General Public
11
+ License along with this program. If not, see <https://www.gnu.org/licenses/>.
12
+ */
13
+
14
+ import type { CommandManager } from '@cyberismo/data-handler';
15
+
16
+ export interface ExternalItem {
17
+ key: string;
18
+ title: string;
19
+ }
20
+
21
+ export interface Connector {
22
+ name: string;
23
+ displayName: string;
24
+ items: ExternalItem[];
25
+ }
26
+
27
+ export async function getConnectors(
28
+ commands: CommandManager,
29
+ ): Promise<Connector[]> {
30
+ const results = await commands.calculateCmd.runQuery(
31
+ 'connectors',
32
+ 'localApp',
33
+ );
34
+
35
+ // Separate connectors from items based on type field
36
+ const connectorsMap = new Map<string, Connector>();
37
+ const items: { connector: string; key: string; title: string }[] = [];
38
+
39
+ for (const r of results) {
40
+ if (r.type === 'connector') {
41
+ connectorsMap.set(r.key, {
42
+ name: r.key,
43
+ displayName: r.displayName ?? r.key,
44
+ items: [],
45
+ });
46
+ } else {
47
+ items.push({
48
+ connector: r.connector ?? '',
49
+ key: r.itemKey ?? '',
50
+ title: r.title ?? '',
51
+ });
52
+ }
53
+ }
54
+
55
+ // Associate items with their connectors
56
+ for (const item of items) {
57
+ const connector = connectorsMap.get(item.connector);
58
+ if (connector) {
59
+ connector.items.push({
60
+ key: item.key,
61
+ title: item.title,
62
+ });
63
+ }
64
+ }
65
+
66
+ return Array.from(connectorsMap.values());
67
+ }
@@ -15,6 +15,8 @@ import { Hono } from 'hono';
15
15
  import * as fieldTypeService from './service.js';
16
16
  import { createFieldTypeSchema } from './schema.js';
17
17
  import { zValidator } from '../../middleware/zvalidator.js';
18
+ import { UserRole } from '../../types.js';
19
+ import { requireRole } from '../../middleware/auth.js';
18
20
 
19
21
  const router = new Hono();
20
22
 
@@ -32,7 +34,7 @@ const router = new Hono();
32
34
  * 500:
33
35
  * description: project_path not set or other internal error
34
36
  */
35
- router.get('/', async (c) => {
37
+ router.get('/', requireRole(UserRole.Reader), async (c) => {
36
38
  const commands = c.get('commands');
37
39
 
38
40
  try {
@@ -77,21 +79,26 @@ router.get('/', async (c) => {
77
79
  * 500:
78
80
  * description: Server error
79
81
  */
80
- router.post('/', zValidator('json', createFieldTypeSchema), async (c) => {
81
- const commands = c.get('commands');
82
- const { identifier, dataType } = c.req.valid('json');
82
+ router.post(
83
+ '/',
84
+ requireRole(UserRole.Admin),
85
+ zValidator('json', createFieldTypeSchema),
86
+ async (c) => {
87
+ const commands = c.get('commands');
88
+ const { identifier, dataType } = c.req.valid('json');
83
89
 
84
- try {
85
- await fieldTypeService.createFieldType(commands, identifier, dataType);
86
- return c.json({ message: 'Field type created successfully' });
87
- } catch (error) {
88
- return c.json(
89
- {
90
- error: `${error instanceof Error ? error.message : 'Unknown error'}`,
91
- },
92
- 500,
93
- );
94
- }
95
- });
90
+ try {
91
+ await fieldTypeService.createFieldType(commands, identifier, dataType);
92
+ return c.json({ message: 'Field type created successfully' });
93
+ } catch (error) {
94
+ return c.json(
95
+ {
96
+ error: `${error instanceof Error ? error.message : 'Unknown error'}`,
97
+ },
98
+ 500,
99
+ );
100
+ }
101
+ },
102
+ );
96
103
 
97
104
  export default router;
@@ -15,6 +15,8 @@ import { Hono } from 'hono';
15
15
  import * as graphModelService from './service.js';
16
16
  import { createGraphModelSchema } from './schema.js';
17
17
  import { zValidator } from '../../middleware/zvalidator.js';
18
+ import { UserRole } from '../../types.js';
19
+ import { requireRole } from '../../middleware/auth.js';
18
20
 
19
21
  const router = new Hono();
20
22
 
@@ -43,12 +45,17 @@ const router = new Hono();
43
45
  * 500:
44
46
  * description: Server error
45
47
  */
46
- router.post('/', zValidator('json', createGraphModelSchema), async (c) => {
47
- const commands = c.get('commands');
48
- const { identifier } = c.req.valid('json');
48
+ router.post(
49
+ '/',
50
+ requireRole(UserRole.Admin),
51
+ zValidator('json', createGraphModelSchema),
52
+ async (c) => {
53
+ const commands = c.get('commands');
54
+ const { identifier } = c.req.valid('json');
49
55
 
50
- await graphModelService.createGraphModel(commands, identifier);
51
- return c.json({ message: 'Graph model created successfully' });
52
- });
56
+ await graphModelService.createGraphModel(commands, identifier);
57
+ return c.json({ message: 'Graph model created successfully' });
58
+ },
59
+ );
53
60
 
54
61
  export default router;
@@ -15,6 +15,8 @@ import { Hono } from 'hono';
15
15
  import * as graphViewService from './service.js';
16
16
  import { createGraphViewSchema } from './schema.js';
17
17
  import { zValidator } from '../../middleware/zvalidator.js';
18
+ import { UserRole } from '../../types.js';
19
+ import { requireRole } from '../../middleware/auth.js';
18
20
 
19
21
  const router = new Hono();
20
22
 
@@ -43,11 +45,16 @@ const router = new Hono();
43
45
  * 500:
44
46
  * description: Server error
45
47
  */
46
- router.post('/', zValidator('json', createGraphViewSchema), async (c) => {
47
- const commands = c.get('commands');
48
- const { identifier } = c.req.valid('json');
49
- await graphViewService.createGraphView(commands, identifier);
50
- return c.json({ message: 'Graph view created successfully' });
51
- });
48
+ router.post(
49
+ '/',
50
+ requireRole(UserRole.Admin),
51
+ zValidator('json', createGraphViewSchema),
52
+ async (c) => {
53
+ const commands = c.get('commands');
54
+ const { identifier } = c.req.valid('json');
55
+ await graphViewService.createGraphView(commands, identifier);
56
+ return c.json({ message: 'Graph view created successfully' });
57
+ },
58
+ );
52
59
 
53
60
  export default router;
@@ -13,6 +13,8 @@
13
13
 
14
14
  import { Hono } from 'hono';
15
15
  import * as labelsService from './service.js';
16
+ import { UserRole } from '../../types.js';
17
+ import { requireRole } from '../../middleware/auth.js';
16
18
 
17
19
  const router = new Hono();
18
20
 
@@ -27,9 +29,10 @@ const router = new Hono();
27
29
  * 500:
28
30
  * description: Internal server error
29
31
  */
30
- router.get('/', (c) => {
32
+
33
+ router.get('/', requireRole(UserRole.Reader), async (c) => {
31
34
  const commands = c.get('commands');
32
- const labels = labelsService.getLabels(commands);
35
+ const labels = await labelsService.getLabels(commands);
33
36
  return c.json(labels);
34
37
  });
35
38
 
@@ -18,6 +18,6 @@ import type { CommandManager } from '@cyberismo/data-handler';
18
18
  * @param commands command manager used for the query
19
19
  * @returns a list of labels
20
20
  */
21
- export function getLabels(commands: CommandManager): string[] {
22
- return commands.showCmd.showLabels().sort();
21
+ export async function getLabels(commands: CommandManager): Promise<string[]> {
22
+ return (await commands.showCmd.showLabels()).sort();
23
23
  }
@@ -15,6 +15,8 @@ import { Hono } from 'hono';
15
15
  import * as linkTypeService from './service.js';
16
16
  import { createLinkTypeSchema } from './schema.js';
17
17
  import { zValidator } from '../../middleware/zvalidator.js';
18
+ import { UserRole } from '../../types.js';
19
+ import { requireRole } from '../../middleware/auth.js';
18
20
 
19
21
  const router = new Hono();
20
22
 
@@ -32,7 +34,7 @@ const router = new Hono();
32
34
  * 500:
33
35
  * description: project_path not set or other internal error
34
36
  */
35
- router.get('/', async (c) => {
37
+ router.get('/', requireRole(UserRole.Reader), async (c) => {
36
38
  const commands = c.get('commands');
37
39
 
38
40
  try {
@@ -73,12 +75,17 @@ router.get('/', async (c) => {
73
75
  * 500:
74
76
  * description: Server error
75
77
  */
76
- router.post('/', zValidator('json', createLinkTypeSchema), async (c) => {
77
- const commands = c.get('commands');
78
- const { identifier } = c.req.valid('json');
78
+ router.post(
79
+ '/',
80
+ requireRole(UserRole.Admin),
81
+ zValidator('json', createLinkTypeSchema),
82
+ async (c) => {
83
+ const commands = c.get('commands');
84
+ const { identifier } = c.req.valid('json');
79
85
 
80
- await linkTypeService.createLinkType(commands, identifier);
81
- return c.json({ message: 'Link type created successfully' });
82
- });
86
+ await linkTypeService.createLinkType(commands, identifier);
87
+ return c.json({ message: 'Link type created successfully' });
88
+ },
89
+ );
83
90
 
84
91
  export default router;
@@ -15,11 +15,14 @@ import { Hono } from 'hono';
15
15
  import { zValidator } from '../../middleware/zvalidator.js';
16
16
  import { resourceParamsWithCard } from '../../common/validationSchemas.js';
17
17
  import * as logicProgramService from './service.js';
18
+ import { UserRole } from '../../types.js';
19
+ import { requireRole } from '../../middleware/auth.js';
18
20
 
19
21
  const router = new Hono();
20
22
 
21
23
  router.get(
22
24
  '/:prefix/:type/:identifier',
25
+ requireRole(UserRole.Reader),
23
26
  zValidator('param', resourceParamsWithCard),
24
27
  async (c) => {
25
28
  const commands = c.get('commands');
@@ -0,0 +1,159 @@
1
+ /**
2
+ Cyberismo
3
+ Copyright © Cyberismo Ltd and contributors 2025
4
+ This program is free software: you can redistribute it and/or modify it under
5
+ the terms of the GNU Affero General Public License version 3 as published by
6
+ the Free Software Foundation.
7
+ This program is distributed in the hope that it will be useful, but WITHOUT
8
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
9
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
10
+ details. You should have received a copy of the GNU Affero General Public
11
+ License along with this program. If not, see <https://www.gnu.org/licenses/>.
12
+ */
13
+
14
+ import { Hono } from 'hono';
15
+ import { randomUUID } from 'node:crypto';
16
+ import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
17
+ import { createMcpServer } from '@cyberismo/mcp/server';
18
+ import type { CommandManager } from '@cyberismo/data-handler';
19
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
20
+
21
+ const MAX_SESSIONS = 100;
22
+ const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
23
+ const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
24
+
25
+ interface McpSession {
26
+ transport: WebStandardStreamableHTTPServerTransport;
27
+ server: McpServer;
28
+ commands: CommandManager;
29
+ lastActivity: number;
30
+ }
31
+
32
+ const sessions = new Map<string, McpSession>();
33
+
34
+ /**
35
+ * Close and remove a session, shutting down both server and transport.
36
+ * Safe to call multiple times for the same id.
37
+ * When `skipTransportClose` is true the transport is already closing
38
+ * (called from onsessionclosed / onclose callbacks).
39
+ */
40
+ async function destroySession(
41
+ id: string,
42
+ skipTransportClose = false,
43
+ ): Promise<void> {
44
+ const session = sessions.get(id);
45
+ if (!session) return;
46
+ sessions.delete(id);
47
+
48
+ try {
49
+ await session.server.close();
50
+ } catch {
51
+ // Ignore close errors during cleanup
52
+ }
53
+
54
+ if (!skipTransportClose && session.transport.close) {
55
+ try {
56
+ await session.transport.close();
57
+ } catch {
58
+ // Ignore close errors during cleanup
59
+ }
60
+ }
61
+ }
62
+
63
+ // Periodic cleanup of expired sessions
64
+ const cleanupInterval = setInterval(() => {
65
+ const now = Date.now();
66
+ for (const [id, session] of sessions) {
67
+ if (now - session.lastActivity > SESSION_TIMEOUT_MS) {
68
+ void destroySession(id);
69
+ }
70
+ }
71
+ }, CLEANUP_INTERVAL_MS);
72
+ cleanupInterval.unref();
73
+
74
+ const router = new Hono();
75
+
76
+ /**
77
+ * MCP HTTP endpoint handler.
78
+ * Supports GET (SSE streaming), POST (messages), and DELETE (session cleanup).
79
+ */
80
+ router.all('/', async (c) => {
81
+ const commands = c.get('commands');
82
+ const sessionId = c.req.header('mcp-session-id');
83
+
84
+ // Handle DELETE before routing to existing session so it always runs cleanup
85
+ if (c.req.method === 'DELETE') {
86
+ if (sessionId) {
87
+ await destroySession(sessionId);
88
+ }
89
+ return c.json({ message: 'Session closed' });
90
+ }
91
+
92
+ // Handle existing session
93
+ if (sessionId && sessions.has(sessionId)) {
94
+ const session = sessions.get(sessionId)!;
95
+ session.lastActivity = Date.now();
96
+ const response = await session.transport.handleRequest(c.req.raw);
97
+ return response;
98
+ }
99
+
100
+ // Only allow POST to create new sessions (initialize)
101
+ if (c.req.method !== 'POST') {
102
+ return c.json(
103
+ { error: 'Method not allowed. Use POST to initialize a session.' },
104
+ 405,
105
+ );
106
+ }
107
+
108
+ // Reject new sessions when at capacity
109
+ if (sessions.size >= MAX_SESSIONS) {
110
+ return c.json({ error: 'Too many active sessions' }, 503);
111
+ }
112
+
113
+ // Create new session for initialization
114
+ const transport = new WebStandardStreamableHTTPServerTransport({
115
+ sessionIdGenerator: () => randomUUID(),
116
+ onsessioninitialized: (newSessionId: string) => {
117
+ sessions.set(newSessionId, {
118
+ transport,
119
+ server,
120
+ commands,
121
+ lastActivity: Date.now(),
122
+ });
123
+ },
124
+ onsessionclosed: (closedSessionId: string) => {
125
+ void destroySession(closedSessionId, true);
126
+ },
127
+ });
128
+
129
+ transport.onclose = () => {
130
+ const sid = transport.sessionId;
131
+ if (sid) {
132
+ void destroySession(sid, true);
133
+ }
134
+ };
135
+
136
+ const server = createMcpServer(commands);
137
+ await server.connect(transport);
138
+
139
+ const response = await transport.handleRequest(c.req.raw);
140
+ return response;
141
+ });
142
+
143
+ /**
144
+ * SSE endpoint for server-to-client messages
145
+ */
146
+ router.get('/sse', async (c) => {
147
+ const sessionId = c.req.header('mcp-session-id');
148
+
149
+ if (!sessionId || !sessions.has(sessionId)) {
150
+ return c.json({ error: 'Invalid or missing session ID' }, 400);
151
+ }
152
+
153
+ const session = sessions.get(sessionId)!;
154
+ session.lastActivity = Date.now();
155
+ const response = await session.transport.handleRequest(c.req.raw);
156
+ return response;
157
+ });
158
+
159
+ export default router;
@@ -13,27 +13,38 @@
13
13
 
14
14
  import { Hono } from 'hono';
15
15
  import { zValidator } from '../../middleware/zvalidator.js';
16
- import { moduleParamSchema, updateProjectSchema } from './schema.js';
16
+ import {
17
+ importModuleSchema,
18
+ moduleParamSchema,
19
+ updateProjectSchema,
20
+ } from './schema.js';
17
21
  import * as projectService from './service.js';
22
+ import { UserRole } from '../../types.js';
23
+ import { requireRole } from '../../middleware/auth.js';
18
24
 
19
25
  const router = new Hono();
20
26
 
21
- router.get('/', async (c) => {
27
+ router.get('/', requireRole(UserRole.Reader), async (c) => {
22
28
  const commands = c.get('commands');
23
29
 
24
30
  const project = await projectService.getProject(commands);
25
31
  return c.json(project);
26
32
  });
27
33
 
28
- router.patch('/', zValidator('json', updateProjectSchema), async (c) => {
29
- const commands = c.get('commands');
30
- const updates = c.req.valid('json');
34
+ router.patch(
35
+ '/',
36
+ requireRole(UserRole.Admin),
37
+ zValidator('json', updateProjectSchema),
38
+ async (c) => {
39
+ const commands = c.get('commands');
40
+ const updates = c.req.valid('json');
31
41
 
32
- const project = await projectService.updateProject(commands, updates);
33
- return c.json(project);
34
- });
42
+ const project = await projectService.updateProject(commands, updates);
43
+ return c.json(project);
44
+ },
45
+ );
35
46
 
36
- router.post('/modules/update', async (c) => {
47
+ router.post('/modules/update', requireRole(UserRole.Admin), async (c) => {
37
48
  const commands = c.get('commands');
38
49
  await projectService.updateAllModules(commands);
39
50
  return c.json({ message: 'All modules updated' });
@@ -41,6 +52,7 @@ router.post('/modules/update', async (c) => {
41
52
 
42
53
  router.post(
43
54
  '/modules/:module/update',
55
+ requireRole(UserRole.Admin),
44
56
  zValidator('param', moduleParamSchema),
45
57
  async (c) => {
46
58
  const commands = c.get('commands');
@@ -50,8 +62,27 @@ router.post(
50
62
  },
51
63
  );
52
64
 
65
+ router.get('/modules/importable', requireRole(UserRole.Reader), async (c) => {
66
+ const commands = c.get('commands');
67
+ const modules = await projectService.getImportableModules(commands);
68
+ return c.json(modules);
69
+ });
70
+
71
+ router.post(
72
+ '/modules',
73
+ requireRole(UserRole.Admin),
74
+ zValidator('json', importModuleSchema),
75
+ async (c) => {
76
+ const commands = c.get('commands');
77
+ const { source } = c.req.valid('json');
78
+ await projectService.importModule(commands, source);
79
+ return c.json({ message: 'Module imported successfully' });
80
+ },
81
+ );
82
+
53
83
  router.delete(
54
84
  '/modules/:module',
85
+ requireRole(UserRole.Admin),
55
86
  zValidator('param', moduleParamSchema),
56
87
  async (c) => {
57
88
  const commands = c.get('commands');
@@ -21,3 +21,12 @@ export const updateProjectSchema = z.object({
21
21
  name: z.string().optional(),
22
22
  cardKeyPrefix: z.string().optional(),
23
23
  });
24
+
25
+ export const importModuleSchema = z.object({
26
+ source: z
27
+ .string()
28
+ .min(1)
29
+ .refine((s) => s.startsWith('https') || s.startsWith('git@'), {
30
+ message: 'Source must be a git URL (https:// or git@)',
31
+ }),
32
+ });
@@ -11,7 +11,10 @@
11
11
  License along with this program. If not, see <https://www.gnu.org/licenses/>.
12
12
  */
13
13
 
14
- import { type CommandManager } from '@cyberismo/data-handler';
14
+ import {
15
+ type CommandManager,
16
+ type ModuleSettingFromHub,
17
+ } from '@cyberismo/data-handler';
15
18
 
16
19
  export interface ProjectModule {
17
20
  name: string;
@@ -50,17 +53,19 @@ async function toModuleInfo(
50
53
  export async function getProject(
51
54
  commands: CommandManager,
52
55
  ): Promise<ProjectInfo> {
53
- const project = await commands.showCmd.showProject();
54
- const modules = await commands.showCmd.showModules();
55
- const moduleDetails = await Promise.all(
56
- modules.map((mod) => toModuleInfo(commands, mod)),
57
- );
56
+ return commands.consistent(async () => {
57
+ const project = await commands.showCmd.showProject();
58
+ const modules = await commands.showCmd.showModules();
59
+ const moduleDetails = await Promise.all(
60
+ modules.map((mod) => toModuleInfo(commands, mod)),
61
+ );
58
62
 
59
- return {
60
- name: project.name,
61
- cardKeyPrefix: project.prefix,
62
- modules: moduleDetails,
63
- };
63
+ return {
64
+ name: project.name,
65
+ cardKeyPrefix: project.prefix,
66
+ modules: moduleDetails,
67
+ };
68
+ });
64
69
  }
65
70
 
66
71
  export async function updateProject(
@@ -69,12 +74,14 @@ export async function updateProject(
69
74
  ): Promise<ProjectInfo> {
70
75
  const { name, cardKeyPrefix } = updates;
71
76
 
72
- if (cardKeyPrefix) {
73
- await commands.renameCmd.rename(cardKeyPrefix);
74
- }
75
- if (name) {
76
- await commands.project.configuration.setProjectName(name);
77
- }
77
+ await commands.atomic(async () => {
78
+ if (cardKeyPrefix) {
79
+ await commands.renameCmd.rename(cardKeyPrefix);
80
+ }
81
+ if (name) {
82
+ await commands.project.configuration.setProjectName(name);
83
+ }
84
+ }, 'Update project settings');
78
85
 
79
86
  return getProject(commands);
80
87
  }
@@ -90,3 +97,16 @@ export async function updateAllModules(commands: CommandManager) {
90
97
  export async function deleteModule(commands: CommandManager, module: string) {
91
98
  await commands.removeCmd.remove('module', module);
92
99
  }
100
+
101
+ export async function getImportableModules(
102
+ commands: CommandManager,
103
+ ): Promise<ModuleSettingFromHub[]> {
104
+ return commands.showCmd.showImportableModules(false, true);
105
+ }
106
+
107
+ export async function importModule(
108
+ commands: CommandManager,
109
+ source: string,
110
+ ): Promise<void> {
111
+ await commands.importCmd.importModule(source);
112
+ }