@cyberismo/backend 0.0.21 → 0.0.22

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 (126) hide show
  1. package/dist/app.d.ts +5 -2
  2. package/dist/app.js +22 -9
  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 +19 -15
  28. package/dist/domain/cards/index.js.map +1 -1
  29. package/dist/domain/cards/lib.js +95 -93
  30. package/dist/domain/cards/lib.js.map +1 -1
  31. package/dist/domain/cards/service.d.ts +2 -1
  32. package/dist/domain/cards/service.js +60 -87
  33. package/dist/domain/cards/service.js.map +1 -1
  34. package/dist/domain/fieldTypes/index.js +4 -2
  35. package/dist/domain/fieldTypes/index.js.map +1 -1
  36. package/dist/domain/graphModels/index.js +3 -1
  37. package/dist/domain/graphModels/index.js.map +1 -1
  38. package/dist/domain/graphViews/index.js +3 -1
  39. package/dist/domain/graphViews/index.js.map +1 -1
  40. package/dist/domain/labels/index.js +4 -2
  41. package/dist/domain/labels/index.js.map +1 -1
  42. package/dist/domain/labels/service.d.ts +1 -1
  43. package/dist/domain/labels/service.js +2 -2
  44. package/dist/domain/labels/service.js.map +1 -1
  45. package/dist/domain/linkTypes/index.js +4 -2
  46. package/dist/domain/linkTypes/index.js.map +1 -1
  47. package/dist/domain/logicPrograms/index.js +3 -1
  48. package/dist/domain/logicPrograms/index.js.map +1 -1
  49. package/dist/domain/mcp/index.d.ts +15 -0
  50. package/dist/domain/mcp/index.js +127 -0
  51. package/dist/domain/mcp/index.js.map +1 -0
  52. package/dist/domain/project/index.js +7 -5
  53. package/dist/domain/project/index.js.map +1 -1
  54. package/dist/domain/project/service.js +18 -14
  55. package/dist/domain/project/service.js.map +1 -1
  56. package/dist/domain/reports/index.js +3 -1
  57. package/dist/domain/reports/index.js.map +1 -1
  58. package/dist/domain/resources/index.js +6 -4
  59. package/dist/domain/resources/index.js.map +1 -1
  60. package/dist/domain/resources/service.js +66 -64
  61. package/dist/domain/resources/service.js.map +1 -1
  62. package/dist/domain/templates/index.js +5 -3
  63. package/dist/domain/templates/index.js.map +1 -1
  64. package/dist/domain/tree/index.js +3 -1
  65. package/dist/domain/tree/index.js.map +1 -1
  66. package/dist/domain/workflows/index.js +3 -1
  67. package/dist/domain/workflows/index.js.map +1 -1
  68. package/dist/export.d.ts +6 -5
  69. package/dist/export.js +15 -11
  70. package/dist/export.js.map +1 -1
  71. package/dist/index.d.ts +8 -2
  72. package/dist/index.js +5 -3
  73. package/dist/index.js.map +1 -1
  74. package/dist/main.js +29 -2
  75. package/dist/main.js.map +1 -1
  76. package/dist/middleware/auth.d.ts +40 -0
  77. package/dist/middleware/auth.js +68 -0
  78. package/dist/middleware/auth.js.map +1 -0
  79. package/dist/middleware/commandManager.d.ts +2 -2
  80. package/dist/middleware/commandManager.js +9 -11
  81. package/dist/middleware/commandManager.js.map +1 -1
  82. package/dist/public/THIRD-PARTY.txt +37 -11
  83. package/dist/public/assets/index-B_lh6qtv.css +1 -0
  84. package/dist/public/assets/{index-Ca10XaMv.js → index-CEol8Bfi.js} +43823 -43030
  85. package/dist/public/config.json +1 -0
  86. package/dist/public/index.html +2 -2
  87. package/dist/types.d.ts +25 -0
  88. package/dist/types.js +13 -1
  89. package/dist/types.js.map +1 -1
  90. package/package.json +8 -5
  91. package/src/app.ts +34 -14
  92. package/src/auth/index.ts +17 -0
  93. package/src/auth/keycloak.ts +109 -0
  94. package/src/auth/mock.ts +38 -0
  95. package/src/auth/types.ts +18 -0
  96. package/src/domain/auth/index.ts +35 -0
  97. package/src/domain/calculations/index.ts +13 -6
  98. package/src/domain/calculations/service.ts +16 -14
  99. package/src/domain/cardTypes/index.ts +24 -16
  100. package/src/domain/cardTypes/service.ts +41 -95
  101. package/src/domain/cards/index.ts +62 -44
  102. package/src/domain/cards/lib.ts +105 -100
  103. package/src/domain/cards/service.ts +73 -89
  104. package/src/domain/fieldTypes/index.ts +23 -16
  105. package/src/domain/graphModels/index.ts +13 -6
  106. package/src/domain/graphViews/index.ts +13 -6
  107. package/src/domain/labels/index.ts +5 -2
  108. package/src/domain/labels/service.ts +2 -2
  109. package/src/domain/linkTypes/index.ts +14 -7
  110. package/src/domain/logicPrograms/index.ts +3 -0
  111. package/src/domain/mcp/index.ts +159 -0
  112. package/src/domain/project/index.ts +17 -8
  113. package/src/domain/project/service.ts +20 -16
  114. package/src/domain/reports/index.ts +13 -6
  115. package/src/domain/resources/index.ts +6 -1
  116. package/src/domain/resources/service.ts +102 -97
  117. package/src/domain/templates/index.ts +31 -19
  118. package/src/domain/tree/index.ts +3 -1
  119. package/src/domain/workflows/index.ts +13 -6
  120. package/src/export.ts +16 -13
  121. package/src/index.ts +10 -3
  122. package/src/main.ts +44 -2
  123. package/src/middleware/auth.ts +90 -0
  124. package/src/middleware/commandManager.ts +11 -14
  125. package/src/types.ts +27 -0
  126. package/dist/public/assets/index-CRSBseQM.css +0 -1
@@ -13,29 +13,32 @@
13
13
 
14
14
  import Processor from '@asciidoctor/core';
15
15
  import { type MetadataContent } from '@cyberismo/data-handler/interfaces/project-interfaces';
16
+ import type { attachmentPayload } from '@cyberismo/data-handler/interfaces/request-status-interfaces';
16
17
  import { type CommandManager, evaluateMacros } from '@cyberismo/data-handler';
17
18
  import { allCards } from './lib.js';
18
19
  import type { TreeOptions } from '../../types.js';
19
20
 
20
21
  export async function getProjectInfo(commands: CommandManager) {
21
- const projectResponse = await commands.showCmd.showProject();
22
+ return commands.consistent(async () => {
23
+ const projectResponse = await commands.showCmd.showProject();
22
24
 
23
- const workflowsResponse = await commands.showCmd.showWorkflowsWithDetails();
24
- if (!workflowsResponse) {
25
- throw new Error('No workflows found');
26
- }
25
+ const workflowsResponse = await commands.showCmd.showWorkflowsWithDetails();
26
+ if (!workflowsResponse) {
27
+ throw new Error('No workflows found');
28
+ }
27
29
 
28
- const cardTypesResponse = await commands.showCmd.showCardTypesWithDetails();
29
- if (!cardTypesResponse) {
30
- throw new Error('No card types found');
31
- }
30
+ const cardTypesResponse = await commands.showCmd.showCardTypesWithDetails();
31
+ if (!cardTypesResponse) {
32
+ throw new Error('No card types found');
33
+ }
32
34
 
33
- return {
34
- name: projectResponse.name,
35
- prefix: projectResponse.prefix,
36
- workflows: workflowsResponse,
37
- cardTypes: cardTypesResponse,
38
- };
35
+ return {
36
+ name: projectResponse.name,
37
+ prefix: projectResponse.prefix,
38
+ workflows: workflowsResponse,
39
+ cardTypes: cardTypesResponse,
40
+ };
41
+ });
39
42
  }
40
43
 
41
44
  export async function updateCard(
@@ -44,54 +47,31 @@ export async function updateCard(
44
47
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
48
  body: any,
46
49
  ) {
47
- const errors = [];
48
-
49
- if (body.state) {
50
- try {
50
+ await commands.atomic(async () => {
51
+ if (body.state) {
51
52
  await commands.transitionCmd.cardTransition(key, body.state);
52
- } catch (error) {
53
- if (error instanceof Error) errors.push(error.message);
54
53
  }
55
- }
56
-
57
- if (body.content != null) {
58
- try {
54
+ if (body.content != null) {
59
55
  await commands.editCmd.editCardContent(key, body.content);
60
- } catch (error) {
61
- if (error instanceof Error) errors.push(error.message);
62
56
  }
63
- }
64
-
65
- if (body.metadata) {
66
- for (const [metadataKey, metadataValue] of Object.entries(body.metadata)) {
67
- const value = metadataValue as MetadataContent;
68
-
69
- try {
70
- await commands.editCmd.editCardMetadata(key, metadataKey, value);
71
- } catch (error) {
72
- if (error instanceof Error) errors.push(error.message);
57
+ if (body.metadata) {
58
+ for (const [metadataKey, metadataValue] of Object.entries(
59
+ body.metadata,
60
+ )) {
61
+ await commands.editCmd.editCardMetadata(
62
+ key,
63
+ metadataKey,
64
+ metadataValue as MetadataContent,
65
+ );
73
66
  }
74
67
  }
75
- }
76
-
77
- if (body.parent) {
78
- try {
68
+ if (body.parent) {
79
69
  await commands.moveCmd.moveCard(key, body.parent);
80
- } catch (error) {
81
- if (error instanceof Error) errors.push(error.message);
82
70
  }
83
- }
84
- if (body.index != null) {
85
- try {
71
+ if (body.index != null) {
86
72
  await commands.moveCmd.rankByIndex(key, body.index);
87
- } catch (error) {
88
- if (error instanceof Error) errors.push(error.message);
89
73
  }
90
- }
91
-
92
- if (errors.length > 0) {
93
- throw new Error(errors.join('\n'));
94
- }
74
+ }, `Update card ${key}`);
95
75
  }
96
76
 
97
77
  export async function deleteCard(commands: CommandManager, key: string) {
@@ -119,18 +99,20 @@ export async function uploadAttachments(
119
99
  key: string,
120
100
  files: File[],
121
101
  ) {
122
- const succeeded = [];
123
- for (const file of files) {
124
- if (file instanceof File) {
125
- const buffer = await file.arrayBuffer();
126
- await commands.createCmd.createAttachment(
127
- key,
128
- file.name,
129
- Buffer.from(buffer),
130
- );
131
- succeeded.push(file.name);
102
+ const succeeded: string[] = [];
103
+ await commands.atomic(async () => {
104
+ for (const file of files) {
105
+ if (file instanceof File) {
106
+ const buffer = await file.arrayBuffer();
107
+ await commands.createCmd.createAttachment(
108
+ key,
109
+ file.name,
110
+ Buffer.from(buffer),
111
+ );
112
+ succeeded.push(file.name);
113
+ }
132
114
  }
133
- }
115
+ }, `Add attachments to ${key}`);
134
116
 
135
117
  return {
136
118
  message: 'Attachments uploaded successfully',
@@ -161,30 +143,32 @@ export async function parseContent(
161
143
  key: string,
162
144
  content: string,
163
145
  ) {
164
- let asciidocContent = '';
165
- try {
166
- asciidocContent = await evaluateMacros(content, {
167
- context: 'localApp',
168
- mode: 'inject',
169
- project: commands.project,
170
- cardKey: key,
171
- });
172
- } catch (error) {
173
- asciidocContent = `Macro error: ${error instanceof Error ? error.message : 'Unknown error'}\n\n${content}`;
174
- }
146
+ return commands.consistent(async () => {
147
+ let asciidocContent: string;
148
+ try {
149
+ asciidocContent = await evaluateMacros(content, {
150
+ context: 'localApp',
151
+ mode: 'inject',
152
+ project: commands.project,
153
+ cardKey: key,
154
+ });
155
+ } catch (error) {
156
+ asciidocContent = `Macro error: ${error instanceof Error ? error.message : 'Unknown error'}\n\n${content}`;
157
+ }
175
158
 
176
- const processor = Processor();
177
- const parsedContent = processor
178
- .convert(asciidocContent, {
179
- safe: 'safe',
180
- attributes: {
181
- imagesdir: `/api/cards/${key}/a`,
182
- icons: 'font',
183
- },
184
- })
185
- .toString();
186
-
187
- return { parsedContent };
159
+ const processor = Processor();
160
+ const parsedContent = processor
161
+ .convert(asciidocContent, {
162
+ safe: 'safe',
163
+ attributes: {
164
+ imagesdir: `/api/cards/${key}/a`,
165
+ icons: 'font',
166
+ },
167
+ })
168
+ .toString();
169
+
170
+ return { parsedContent };
171
+ });
188
172
  }
189
173
 
190
174
  export async function createLink(
@@ -209,11 +193,11 @@ export async function removeLink(
209
193
  return { message: 'Link removed successfully' };
210
194
  }
211
195
 
212
- export function getAttachment(
196
+ export async function getAttachment(
213
197
  commands: CommandManager,
214
198
  key: string,
215
199
  filename: string,
216
- ) {
200
+ ): Promise<attachmentPayload> {
217
201
  return commands.showCmd.showAttachment(key, filename);
218
202
  }
219
203
 
@@ -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;
@@ -15,25 +15,32 @@ import { Hono } from 'hono';
15
15
  import { zValidator } from '../../middleware/zvalidator.js';
16
16
  import { moduleParamSchema, updateProjectSchema } from './schema.js';
17
17
  import * as projectService 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
- router.get('/', async (c) => {
23
+ router.get('/', requireRole(UserRole.Reader), async (c) => {
22
24
  const commands = c.get('commands');
23
25
 
24
26
  const project = await projectService.getProject(commands);
25
27
  return c.json(project);
26
28
  });
27
29
 
28
- router.patch('/', zValidator('json', updateProjectSchema), async (c) => {
29
- const commands = c.get('commands');
30
- const updates = c.req.valid('json');
30
+ router.patch(
31
+ '/',
32
+ requireRole(UserRole.Admin),
33
+ zValidator('json', updateProjectSchema),
34
+ async (c) => {
35
+ const commands = c.get('commands');
36
+ const updates = c.req.valid('json');
31
37
 
32
- const project = await projectService.updateProject(commands, updates);
33
- return c.json(project);
34
- });
38
+ const project = await projectService.updateProject(commands, updates);
39
+ return c.json(project);
40
+ },
41
+ );
35
42
 
36
- router.post('/modules/update', async (c) => {
43
+ router.post('/modules/update', requireRole(UserRole.Admin), async (c) => {
37
44
  const commands = c.get('commands');
38
45
  await projectService.updateAllModules(commands);
39
46
  return c.json({ message: 'All modules updated' });
@@ -41,6 +48,7 @@ router.post('/modules/update', async (c) => {
41
48
 
42
49
  router.post(
43
50
  '/modules/:module/update',
51
+ requireRole(UserRole.Admin),
44
52
  zValidator('param', moduleParamSchema),
45
53
  async (c) => {
46
54
  const commands = c.get('commands');
@@ -52,6 +60,7 @@ router.post(
52
60
 
53
61
  router.delete(
54
62
  '/modules/:module',
63
+ requireRole(UserRole.Admin),
55
64
  zValidator('param', moduleParamSchema),
56
65
  async (c) => {
57
66
  const commands = c.get('commands');