@cyberismo/backend 0.0.23 → 0.0.25

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 +3 -3
  2. package/dist/app.js +58 -29
  3. package/dist/app.js.map +1 -1
  4. package/dist/domain/cards/index.js +63 -1
  5. package/dist/domain/cards/index.js.map +1 -1
  6. package/dist/domain/cards/lib.js +7 -1
  7. package/dist/domain/cards/lib.js.map +1 -1
  8. package/dist/domain/cards/schema.d.ts +8 -0
  9. package/dist/domain/cards/schema.js +7 -0
  10. package/dist/domain/cards/schema.js.map +1 -1
  11. package/dist/domain/cards/service.d.ts +2 -0
  12. package/dist/domain/cards/service.js +18 -1
  13. package/dist/domain/cards/service.js.map +1 -1
  14. package/dist/domain/mcp/index.d.ts +8 -2
  15. package/dist/domain/mcp/index.js +68 -65
  16. package/dist/domain/mcp/index.js.map +1 -1
  17. package/dist/domain/project/service.js +1 -1
  18. package/dist/domain/project/service.js.map +1 -1
  19. package/dist/domain/projects/index.d.ts +15 -0
  20. package/dist/domain/projects/index.js +35 -0
  21. package/dist/domain/projects/index.js.map +1 -0
  22. package/dist/domain/resources/index.js +63 -1
  23. package/dist/domain/resources/index.js.map +1 -1
  24. package/dist/domain/resources/schema.d.ts +9 -0
  25. package/dist/domain/resources/schema.js +8 -1
  26. package/dist/domain/resources/schema.js.map +1 -1
  27. package/dist/domain/resources/service.d.ts +12 -0
  28. package/dist/domain/resources/service.js +49 -6
  29. package/dist/domain/resources/service.js.map +1 -1
  30. package/dist/export.d.ts +7 -3
  31. package/dist/export.js +32 -16
  32. package/dist/export.js.map +1 -1
  33. package/dist/index.d.ts +5 -4
  34. package/dist/index.js +4 -3
  35. package/dist/index.js.map +1 -1
  36. package/dist/main.js +41 -6
  37. package/dist/main.js.map +1 -1
  38. package/dist/middleware/auth.js +10 -0
  39. package/dist/middleware/auth.js.map +1 -1
  40. package/dist/middleware/commandManager.d.ts +12 -0
  41. package/dist/middleware/commandManager.js +33 -7
  42. package/dist/middleware/commandManager.js.map +1 -1
  43. package/dist/project-registry.d.ts +50 -0
  44. package/dist/project-registry.js +77 -0
  45. package/dist/project-registry.js.map +1 -0
  46. package/dist/public/THIRD-PARTY.txt +2899 -1974
  47. package/dist/public/assets/architecture-7EHR7CIX-BhpB9ddF.js +1 -0
  48. package/dist/public/assets/architectureDiagram-3BPJPVTR-BQYiU_EO.js +36 -0
  49. package/dist/public/assets/blockDiagram-GPEHLZMM-DuyikC3X.js +132 -0
  50. package/dist/public/assets/c4Diagram-AAUBKEIU-BG8_sPEr.js +10 -0
  51. package/dist/public/assets/channel-BxgB7fMy.js +1 -0
  52. package/dist/public/assets/chunk-2J33WTMH-vNq8B1aw.js +1 -0
  53. package/dist/public/assets/chunk-4BX2VUAB-DFDBsmSo.js +1 -0
  54. package/dist/public/assets/chunk-55IACEB6-DCVUQPWM.js +1 -0
  55. package/dist/public/assets/chunk-727SXJPM-C5ihZMyl.js +206 -0
  56. package/dist/public/assets/chunk-AQP2D5EJ-XGOtp2xP.js +231 -0
  57. package/dist/public/assets/chunk-FMBD7UC4-Da1lR6Mn.js +15 -0
  58. package/dist/public/assets/chunk-ND2GUHAM-ZOvpvr6K.js +1 -0
  59. package/dist/public/assets/chunk-QZHKN3VN-D33jzvFT.js +1 -0
  60. package/dist/public/assets/classDiagram-4FO5ZUOK-hWfZv7hZ.js +1 -0
  61. package/dist/public/assets/classDiagram-v2-Q7XG4LA2-hWfZv7hZ.js +1 -0
  62. package/dist/public/assets/cose-bilkent-S5V4N54A-DO4z-ix4.js +1 -0
  63. package/dist/public/assets/cytoscape.esm-C8YCVR3_.js +321 -0
  64. package/dist/public/assets/dagre-BM42HDAG-DlpRfzgA.js +4 -0
  65. package/dist/public/assets/dagre-Bx709z4p.js +1 -0
  66. package/dist/public/assets/diagram-2AECGRRQ-D-t_ImBP.js +43 -0
  67. package/dist/public/assets/diagram-5GNKFQAL-CBgUMlXz.js +10 -0
  68. package/dist/public/assets/diagram-KO2AKTUF-XoB2TgQt.js +3 -0
  69. package/dist/public/assets/diagram-LMA3HP47-D1Sbl_eS.js +24 -0
  70. package/dist/public/assets/diagram-OG6HWLK6-DKP4aiIY.js +24 -0
  71. package/dist/public/assets/erDiagram-TEJ5UH35-DYxfHOOK.js +85 -0
  72. package/dist/public/assets/eventmodeling-FCH6USID-cF_1Mq4g.js +1 -0
  73. package/dist/public/assets/flowDiagram-I6XJVG4X-BDHPsmlq.js +162 -0
  74. package/dist/public/assets/ganttDiagram-6RSMTGT7-bGgIvBPN.js +292 -0
  75. package/dist/public/assets/gitGraph-WXDBUCRP-DOFshjLy.js +1 -0
  76. package/dist/public/assets/gitGraphDiagram-PVQCEYII-xSwLjGd-.js +106 -0
  77. package/dist/public/assets/graphlib-B8gBHxth.js +1 -0
  78. package/dist/public/assets/index-DGPv1qic.js +1028 -0
  79. package/dist/public/assets/index-DvHiopvR.css +1 -0
  80. package/dist/public/assets/info-J43DQDTF-BuJNK7zQ.js +1 -0
  81. package/dist/public/assets/infoDiagram-5YYISTIA-BanxuIib.js +2 -0
  82. package/dist/public/assets/ishikawaDiagram-YF4QCWOH-DWNWYxz5.js +70 -0
  83. package/dist/public/assets/journeyDiagram-JHISSGLW-I58P5XNg.js +139 -0
  84. package/dist/public/assets/kanban-definition-UN3LZRKU-CMRWbDti.js +89 -0
  85. package/dist/public/assets/katex-C4eR7coU.js +257 -0
  86. package/dist/public/assets/mermaid-parser.core-Dz__fM3g.js +161 -0
  87. package/dist/public/assets/mindmap-definition-RKZ34NQL-C47gCcpC.js +96 -0
  88. package/dist/public/assets/packet-YPE3B663-Cczitw2-.js +1 -0
  89. package/dist/public/assets/pie-LRSECV5Y-rO-Aqx6h.js +1 -0
  90. package/dist/public/assets/pieDiagram-4H26LBE5-VZAxHzjD.js +30 -0
  91. package/dist/public/assets/quadrantDiagram-W4KKPZXB-BY8JORvE.js +7 -0
  92. package/dist/public/assets/radar-GUYGQ44K-SSIGuQjW.js +1 -0
  93. package/dist/public/assets/requirementDiagram-4Y6WPE33-XhNBeFwj.js +84 -0
  94. package/dist/public/assets/sankeyDiagram-5OEKKPKP-H7I2OESy.js +40 -0
  95. package/dist/public/assets/sequenceDiagram-3UESZ5HK-jnTLwq-X.js +162 -0
  96. package/dist/public/assets/stateDiagram-AJRCARHV-BKcf2bdX.js +1 -0
  97. package/dist/public/assets/stateDiagram-v2-BHNVJYJU-wpO0gnsG.js +1 -0
  98. package/dist/public/assets/timeline-definition-PNZ67QCA-BZbaBDRH.js +120 -0
  99. package/dist/public/assets/treeView-BLDUP644-DkGx4HkR.js +1 -0
  100. package/dist/public/assets/treemap-LRROVOQU-yCyuONQh.js +1 -0
  101. package/dist/public/assets/vennDiagram-CIIHVFJN-nY9Pep3o.js +34 -0
  102. package/dist/public/assets/wardley-L42UT6IY-CXWWFUgk.js +1 -0
  103. package/dist/public/assets/wardleyDiagram-YWT4CUSO-CM0yrkHd.js +78 -0
  104. package/dist/public/assets/xychartDiagram-2RQKCTM6-1ZAtqvyQ.js +7 -0
  105. package/dist/public/config.json +1 -0
  106. package/dist/public/index.html +2 -2
  107. package/package.json +11 -7
  108. package/src/app.ts +71 -31
  109. package/src/domain/cards/index.ts +73 -0
  110. package/src/domain/cards/lib.ts +8 -1
  111. package/src/domain/cards/schema.ts +9 -0
  112. package/src/domain/cards/service.ts +28 -2
  113. package/src/domain/mcp/index.ts +83 -78
  114. package/src/domain/project/service.ts +1 -1
  115. package/src/domain/projects/index.ts +39 -0
  116. package/src/domain/resources/index.ts +74 -0
  117. package/src/domain/resources/schema.ts +14 -0
  118. package/src/domain/resources/service.ts +52 -4
  119. package/src/export.ts +44 -21
  120. package/src/index.ts +6 -5
  121. package/src/main.ts +46 -6
  122. package/src/middleware/auth.ts +10 -0
  123. package/src/middleware/commandManager.ts +47 -9
  124. package/src/project-registry.ts +110 -0
  125. package/dist/public/assets/index-Cdn_jRWy.js +0 -720
  126. package/dist/public/assets/index-ypsafPwV.css +0 -1
@@ -26,6 +26,8 @@ import {
26
26
  createLinkSchema,
27
27
  removeLinkSchema,
28
28
  updateLinkSchema,
29
+ exportCardPdfSchema,
30
+ type ExportCardPdfRequestBody,
29
31
  } from './schema.js';
30
32
 
31
33
  const router = new Hono();
@@ -58,6 +60,77 @@ router.get('/', requireRole(UserRole.Reader), async (c) => {
58
60
  }
59
61
  });
60
62
 
63
+ /**
64
+ * @swagger
65
+ * /api/cards/export-pdf:
66
+ * post:
67
+ * summary: Export a card as a PDF
68
+ * description: Exports the specified card as a PDF
69
+ * parameters:
70
+ * - name: key
71
+ * in: path
72
+ * required: true
73
+ * description: Card key (string)
74
+ * requestBody:
75
+ * content:
76
+ * application/json:
77
+ * schema:
78
+ * type: object
79
+ * properties:
80
+ * title:
81
+ * type: string
82
+ * description: Title of the exported PDF
83
+ * name:
84
+ * type: string
85
+ * description: Name of the exported PDF
86
+ * exportChildCards:
87
+ * type: boolean
88
+ * description: Whether to export child cards
89
+ * version:
90
+ * type: string
91
+ * description: Version of the exported PDF (optional)
92
+ * responses:
93
+ * 200:
94
+ * description: Card exported successfully
95
+ * 400:
96
+ * description: Missing or invalid parameters.
97
+ * 500:
98
+ * description: project_path not set
99
+ */
100
+ router.post(
101
+ '/export-pdf',
102
+ requireRole(UserRole.Reader),
103
+ zValidator('json', exportCardPdfSchema),
104
+ async (c) => {
105
+ const commands = c.get('commands');
106
+ const body = (await c.req.json()) as ExportCardPdfRequestBody;
107
+ try {
108
+ const result = await cardService.exportCard(commands, {
109
+ cardKey: body.cardKey,
110
+ title: body.title,
111
+ name: body.name,
112
+ recursive: body.exportChildCards,
113
+ version: body.version,
114
+ });
115
+ return new Response(result, {
116
+ status: 200,
117
+ headers: {
118
+ 'Content-Type': 'application/pdf',
119
+ 'Cache-Control': 'no-store',
120
+ },
121
+ });
122
+ } catch (error) {
123
+ return c.json(
124
+ {
125
+ error:
126
+ error instanceof Error ? error.message : 'Failed to export card',
127
+ },
128
+ 500,
129
+ );
130
+ }
131
+ },
132
+ );
133
+
61
134
  /**
62
135
  * @swagger
63
136
  * /api/cards/{key}:
@@ -14,6 +14,7 @@
14
14
  import Processor from '@asciidoctor/core';
15
15
  import type { Card } from '@cyberismo/data-handler/interfaces/project-interfaces';
16
16
  import { type CommandManager, evaluateMacros } from '@cyberismo/data-handler';
17
+ import { preprocessMermaidBlocksForHtml } from '@cyberismo/data-handler/utils/mermaid-renderer';
17
18
  import { getCardQueryResult } from '../../export.js';
18
19
  import type { TreeOptions } from '../../types.js';
19
20
  import type { QueryResult } from '@cyberismo/data-handler/types/queries';
@@ -57,11 +58,15 @@ export async function getCardDetails(
57
58
  asciidocContent = `Macro error: ${error instanceof Error ? error.message : 'Unknown error'}\n\n${asciidocContent}`;
58
59
  }
59
60
 
61
+ // Convert [mermaid] AsciiDoc blocks to passthrough HTML before asciidoctor processes them
62
+ asciidocContent = preprocessMermaidBlocksForHtml(asciidocContent);
63
+
64
+ const projectPrefix = commands.project.projectPrefix;
60
65
  const htmlContent = Processor()
61
66
  .convert(asciidocContent, {
62
67
  safe: 'safe',
63
68
  attributes: {
64
- imagesdir: `/api/cards/${key}/a`,
69
+ imagesdir: `/api/projects/${projectPrefix}/cards/${key}/a`,
65
70
  icons: 'font',
66
71
  },
67
72
  })
@@ -124,6 +129,7 @@ export async function getCardDetails(
124
129
  rawContent: cardDetailsResponse.content || '',
125
130
  parsedContent: htmlContent,
126
131
  attachments: cardDetailsResponse.attachments,
132
+ path: cardDetailsResponse.path,
127
133
  },
128
134
  };
129
135
  }
@@ -145,6 +151,7 @@ export async function getCardDetails(
145
151
  rawContent: cardDetailsResponse.content || '',
146
152
  parsedContent: htmlContent,
147
153
  attachments: cardDetailsResponse.attachments,
154
+ path: cardDetailsResponse.path,
148
155
  },
149
156
  };
150
157
  });
@@ -39,3 +39,12 @@ export const updateLinkSchema = z.object({
39
39
  previousDirection: linkDirection,
40
40
  previousDescription: z.string().optional(),
41
41
  });
42
+ export const exportCardPdfSchema = z.object({
43
+ title: z.string().min(1),
44
+ name: z.string().min(1),
45
+ cardKey: z.string(),
46
+ exportChildCards: z.boolean(),
47
+ version: z.string().min(1).optional(),
48
+ });
49
+
50
+ export type ExportCardPdfRequestBody = z.infer<typeof exportCardPdfSchema>;
@@ -12,9 +12,13 @@
12
12
  */
13
13
 
14
14
  import Processor from '@asciidoctor/core';
15
- import { type MetadataContent } from '@cyberismo/data-handler/interfaces/project-interfaces';
15
+ import type {
16
+ ExportPdfOptions,
17
+ MetadataContent,
18
+ } from '@cyberismo/data-handler/interfaces/project-interfaces';
16
19
  import type { attachmentPayload } from '@cyberismo/data-handler/interfaces/request-status-interfaces';
17
20
  import { type CommandManager, evaluateMacros } from '@cyberismo/data-handler';
21
+ import { preprocessMermaidBlocksForHtml } from '@cyberismo/data-handler/utils/mermaid-renderer';
18
22
  import { allCards } from './lib.js';
19
23
  import type { TreeOptions } from '../../types.js';
20
24
 
@@ -120,11 +124,19 @@ export async function uploadAttachments(
120
124
  };
121
125
  }
122
126
 
127
+ function validateAttachmentFileName(filename: string): void {
128
+ const decoded = decodeURIComponent(filename);
129
+ if (/(^|[/\\])\.\.([/\\]|$)/.test(decoded)) {
130
+ throw new Error('Invalid attachment filename');
131
+ }
132
+ }
133
+
123
134
  export async function removeAttachment(
124
135
  commands: CommandManager,
125
136
  key: string,
126
137
  filename: string,
127
138
  ) {
139
+ validateAttachmentFileName(filename);
128
140
  await commands.removeCmd.remove('attachment', key, filename);
129
141
  return { message: 'Attachment removed successfully' };
130
142
  }
@@ -134,6 +146,7 @@ export async function openAttachment(
134
146
  key: string,
135
147
  filename: string,
136
148
  ) {
149
+ validateAttachmentFileName(filename);
137
150
  await commands.showCmd.openAttachment(key, filename);
138
151
  return { message: 'Attachment opened successfully' };
139
152
  }
@@ -156,12 +169,16 @@ export async function parseContent(
156
169
  asciidocContent = `Macro error: ${error instanceof Error ? error.message : 'Unknown error'}\n\n${content}`;
157
170
  }
158
171
 
172
+ // Convert [mermaid] AsciiDoc blocks to passthrough HTML before asciidoctor processes them
173
+ asciidocContent = preprocessMermaidBlocksForHtml(asciidocContent);
174
+
159
175
  const processor = Processor();
176
+ const projectPrefix = commands.project.projectPrefix;
160
177
  const parsedContent = processor
161
178
  .convert(asciidocContent, {
162
179
  safe: 'safe',
163
180
  attributes: {
164
- imagesdir: `/api/cards/${key}/a`,
181
+ imagesdir: `/api/projects/${projectPrefix}/cards/${key}/a`,
165
182
  icons: 'font',
166
183
  },
167
184
  })
@@ -259,6 +276,7 @@ export async function getAttachment(
259
276
  key: string,
260
277
  filename: string,
261
278
  ): Promise<attachmentPayload> {
279
+ validateAttachmentFileName(filename);
262
280
  return commands.showCmd.showAttachment(key, filename);
263
281
  }
264
282
 
@@ -298,3 +316,11 @@ export async function findRelevantAttachments(
298
316
  attachment: attachment.fileName,
299
317
  }));
300
318
  }
319
+
320
+ export async function exportCard(
321
+ commands: CommandManager,
322
+ options: ExportPdfOptions,
323
+ ): Promise<Buffer> {
324
+ const result = await commands.exportCmd.exportPdfBuffer(options);
325
+ return result;
326
+ }
@@ -14,19 +14,21 @@
14
14
  import { Hono } from 'hono';
15
15
  import { randomUUID } from 'node:crypto';
16
16
  import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
17
- import { createMcpServer } from '@cyberismo/mcp/server';
18
- import type { CommandManager } from '@cyberismo/data-handler';
17
+ import { createMcpServer, type ProjectProvider } from '@cyberismo/mcp';
19
18
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
19
+ import { requireRole } from '../../middleware/auth.js';
20
+ import { UserRole, type AppVars } from '../../types.js';
20
21
 
21
22
  const MAX_SESSIONS = 100;
23
+ const MAX_SESSIONS_PER_USER = 5;
22
24
  const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
23
25
  const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
24
26
 
25
27
  interface McpSession {
26
28
  transport: WebStandardStreamableHTTPServerTransport;
27
29
  server: McpServer;
28
- commands: CommandManager;
29
30
  lastActivity: number;
31
+ userId: string;
30
32
  }
31
33
 
32
34
  const sessions = new Map<string, McpSession>();
@@ -71,89 +73,92 @@ const cleanupInterval = setInterval(() => {
71
73
  }, CLEANUP_INTERVAL_MS);
72
74
  cleanupInterval.unref();
73
75
 
74
- const router = new Hono();
75
-
76
76
  /**
77
- * MCP HTTP endpoint handler.
78
- * Supports GET (SSE streaming), POST (messages), and DELETE (session cleanup).
77
+ * Create an MCP HTTP router that serves all projects via the given provider.
79
78
  */
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);
79
+ export function createMcpRouter(
80
+ provider: ProjectProvider,
81
+ ): Hono<{ Variables: AppVars }> {
82
+ const router = new Hono<{ Variables: AppVars }>();
83
+
84
+ /**
85
+ * MCP HTTP endpoint handler.
86
+ * Supports POST (messages) and DELETE (session cleanup).
87
+ */
88
+ router.all('/', requireRole(UserRole.Editor), async (c) => {
89
+ const sessionId = c.req.header('mcp-session-id');
90
+
91
+ // Handle DELETE before routing to existing session so it always runs cleanup
92
+ if (c.req.method === 'DELETE') {
93
+ if (sessionId) {
94
+ await destroySession(sessionId);
95
+ }
96
+ return c.json({ message: 'Session closed' });
88
97
  }
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
98
 
129
- transport.onclose = () => {
130
- const sid = transport.sessionId;
131
- if (sid) {
132
- void destroySession(sid, true);
99
+ // Handle existing session
100
+ if (sessionId && sessions.has(sessionId)) {
101
+ const session = sessions.get(sessionId)!;
102
+ session.lastActivity = Date.now();
103
+ const response = await session.transport.handleRequest(c.req.raw);
104
+ return response;
133
105
  }
134
- };
135
106
 
136
- const server = createMcpServer(commands);
137
- await server.connect(transport);
107
+ // Reject requests with an unknown session ID (e.g. after server restart)
108
+ if (sessionId) {
109
+ return c.json({ error: 'Unknown session ID' }, 404);
110
+ }
138
111
 
139
- const response = await transport.handleRequest(c.req.raw);
140
- return response;
141
- });
112
+ // Only allow POST to create new sessions (initialize)
113
+ if (c.req.method !== 'POST') {
114
+ return c.json(
115
+ { error: 'Method not allowed. Use POST to initialize a session.' },
116
+ 405,
117
+ );
118
+ }
142
119
 
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');
120
+ // Reject new sessions when at capacity
121
+ if (sessions.size >= MAX_SESSIONS) {
122
+ return c.json({ error: 'Too many active sessions' }, 503);
123
+ }
148
124
 
149
- if (!sessionId || !sessions.has(sessionId)) {
150
- return c.json({ error: 'Invalid or missing session ID' }, 400);
151
- }
125
+ const user = c.get('user')!;
126
+ const userSessionCount = [...sessions.values()].filter(
127
+ (s) => s.userId === user.id,
128
+ ).length;
129
+ if (userSessionCount >= MAX_SESSIONS_PER_USER) {
130
+ return c.json({ error: 'Too many active sessions for this user' }, 503);
131
+ }
152
132
 
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
- });
133
+ // Create new session for initialization
134
+ const transport = new WebStandardStreamableHTTPServerTransport({
135
+ sessionIdGenerator: () => randomUUID(),
136
+ onsessioninitialized: (newSessionId: string) => {
137
+ sessions.set(newSessionId, {
138
+ transport,
139
+ server,
140
+ lastActivity: Date.now(),
141
+ userId: user.id,
142
+ });
143
+ },
144
+ onsessionclosed: (closedSessionId: string) => {
145
+ void destroySession(closedSessionId, true);
146
+ },
147
+ });
148
+
149
+ transport.onclose = () => {
150
+ const sid = transport.sessionId;
151
+ if (sid) {
152
+ void destroySession(sid, true);
153
+ }
154
+ };
155
+
156
+ const server = createMcpServer(provider);
157
+ await server.connect(transport);
158
+
159
+ const response = await transport.handleRequest(c.req.raw);
160
+ return response;
161
+ });
158
162
 
159
- export default router;
163
+ return router;
164
+ }
@@ -57,7 +57,7 @@ export async function getProject(
57
57
  const project = await commands.showCmd.showProject();
58
58
  const modules = await commands.showCmd.showModules();
59
59
  const moduleDetails = await Promise.all(
60
- modules.map((mod) => toModuleInfo(commands, mod)),
60
+ modules.map((mod) => toModuleInfo(commands, mod.name)),
61
61
  );
62
62
 
63
63
  return {
@@ -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.
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 type { ProjectRegistry } from '../../project-registry.js';
16
+ import { requireRole } from '../../middleware/auth.js';
17
+ import { UserRole } from '../../types.js';
18
+
19
+ export function createProjectsRouter(registry: ProjectRegistry) {
20
+ const router = new Hono();
21
+
22
+ /**
23
+ * @swagger
24
+ * /api/projects:
25
+ * get:
26
+ * summary: List available projects
27
+ * description: Returns a list of all available projects
28
+ * responses:
29
+ * 200:
30
+ * description: List of projects
31
+ * 401:
32
+ * description: Unauthorized
33
+ */
34
+ router.get('/', requireRole(UserRole.Reader), (c) => {
35
+ return c.json(registry.list());
36
+ });
37
+
38
+ return router;
39
+ }
@@ -20,6 +20,8 @@ import { zValidator } from '../../middleware/zvalidator.js';
20
20
  import {
21
21
  validateResourceParamsSchema,
22
22
  updateOperationBodySchema,
23
+ workflowGraphParamsSchema,
24
+ workflowGraphQuerySchema,
23
25
  } from './schema.js';
24
26
  import { requireRole } from '../../middleware/auth.js';
25
27
 
@@ -132,6 +134,78 @@ router.delete(
132
134
  },
133
135
  );
134
136
 
137
+ /**
138
+ * @swagger
139
+ * /api/resources/{prefix}/workflows/{identifier}/graph:
140
+ * get:
141
+ * summary: Render the state-machine graph for a workflow
142
+ * description: Returns a base64-encoded sanitized SVG of the workflow's
143
+ * state-machine diagram, rendered with the built-in
144
+ * workflow graph model and view. When the optional `card`
145
+ * query parameter is provided, the diagram highlights
146
+ * that card's current workflowState.
147
+ * parameters:
148
+ * - in: path
149
+ * name: prefix
150
+ * required: true
151
+ * schema:
152
+ * type: string
153
+ * - in: path
154
+ * name: identifier
155
+ * required: true
156
+ * schema:
157
+ * type: string
158
+ * - in: query
159
+ * name: card
160
+ * required: false
161
+ * schema:
162
+ * type: string
163
+ * description: Card key whose current workflowState should be
164
+ * highlighted in the rendered diagram.
165
+ * responses:
166
+ * 200:
167
+ * description: Rendered diagram
168
+ * content:
169
+ * application/json:
170
+ * schema:
171
+ * type: object
172
+ * properties:
173
+ * svg:
174
+ * type: string
175
+ * description: Base64-encoded SVG document.
176
+ * 400:
177
+ * description: Invalid path parameters
178
+ * 404:
179
+ * description: Workflow or card not found
180
+ * 500:
181
+ * description: Server error
182
+ */
183
+ router.get(
184
+ '/:prefix/workflows/:identifier/graph',
185
+ requireRole(UserRole.Reader),
186
+ zValidator('param', workflowGraphParamsSchema),
187
+ zValidator('query', workflowGraphQuerySchema),
188
+ async (c) => {
189
+ const commands = c.get('commands');
190
+ const { prefix, identifier } = c.req.valid('param');
191
+ const { card } = c.req.valid('query');
192
+ const workflowName = `${prefix}/workflows/${identifier}`;
193
+ try {
194
+ const svg = await resourceService.getWorkflowGraph(
195
+ commands,
196
+ workflowName,
197
+ card,
198
+ );
199
+ return c.json({ svg });
200
+ } catch (error) {
201
+ if (error instanceof Error && error.message.includes('not found')) {
202
+ return c.json({ error: error.message }, 404);
203
+ }
204
+ throw error;
205
+ }
206
+ },
207
+ );
208
+
135
209
  router.post(
136
210
  '/:prefix/:type/:identifier/operation',
137
211
  requireRole(UserRole.Admin),
@@ -1,9 +1,23 @@
1
1
  import { z } from 'zod';
2
2
  import {
3
+ identifierSchema,
3
4
  resourceParamsSchema,
4
5
  resourceTypes,
5
6
  } from '../../common/validationSchemas.js';
6
7
 
8
+ export const workflowGraphParamsSchema = z.object({
9
+ prefix: z.string(),
10
+ identifier: identifierSchema,
11
+ });
12
+
13
+ export type WorkflowGraphParams = z.infer<typeof workflowGraphParamsSchema>;
14
+
15
+ export const workflowGraphQuerySchema = z.object({
16
+ card: z.string().optional(),
17
+ });
18
+
19
+ export type WorkflowGraphQuery = z.infer<typeof workflowGraphQuerySchema>;
20
+
7
21
  export const resourceFileParamsSchema = resourceParamsSchema.extend({
8
22
  file: z.string(),
9
23
  });
@@ -42,14 +42,14 @@ const resourceTypes: ResourceFolderType[] = [
42
42
 
43
43
  async function getModules(commands: CommandManager) {
44
44
  try {
45
- const moduleNames = await commands.showCmd.showModules();
45
+ const modules = await commands.showCmd.showModules();
46
46
  return Promise.all(
47
- moduleNames.map(async (moduleName) => {
47
+ modules.map(async (mod) => {
48
48
  try {
49
- const module = await commands.showCmd.showModule(moduleName);
49
+ const module = await commands.showCmd.showModule(mod.name);
50
50
  return { name: module.name, cardKeyPrefix: module.cardKeyPrefix };
51
51
  } catch {
52
- return { name: moduleName, cardKeyPrefix: moduleName };
52
+ return { name: mod.name, cardKeyPrefix: mod.name };
53
53
  }
54
54
  }),
55
55
  );
@@ -372,6 +372,7 @@ async function processTemplates(
372
372
  'templates',
373
373
  template,
374
374
  projectPrefix,
375
+ [],
375
376
  ),
376
377
  );
377
378
  }
@@ -390,6 +391,7 @@ async function processTemplates(
390
391
  'templates',
391
392
  template,
392
393
  projectPrefix,
394
+ [],
393
395
  ),
394
396
  );
395
397
  }
@@ -467,6 +469,52 @@ export async function validateResource(
467
469
  };
468
470
  }
469
471
 
472
+ /**
473
+ * Renders the built-in state-machine graph for a single workflow.
474
+ * When `cardKey` is provided, the card's current workflowState is
475
+ * highlighted in the diagram. The workflow lookup, card lookup and
476
+ * graph rendering all run inside the same consistency window so the
477
+ * highlighted state matches the diagram even if the card is being
478
+ * transitioned concurrently.
479
+ * @returns base64-encoded sanitized SVG.
480
+ * @throws Error with 'not found' in the message when the workflow or
481
+ * card cannot be resolved.
482
+ */
483
+ export async function getWorkflowGraph(
484
+ commands: CommandManager,
485
+ workflowName: string,
486
+ cardKey?: string,
487
+ ): Promise<string> {
488
+ return commands.consistent(async () => {
489
+ try {
490
+ await commands.showCmd.showResource(workflowName, 'workflows');
491
+ } catch (error) {
492
+ if (error instanceof Error && error.message.includes('does not exist')) {
493
+ throw new Error(`Workflow '${workflowName}' not found`, {
494
+ cause: error,
495
+ });
496
+ }
497
+ throw error;
498
+ }
499
+ let currentState: string | undefined;
500
+ if (cardKey) {
501
+ let card;
502
+ try {
503
+ card = await commands.showCmd.showCardDetails(cardKey);
504
+ } catch (error) {
505
+ throw new Error(`Card '${cardKey}' not found`, { cause: error });
506
+ }
507
+ if (!card?.metadata) {
508
+ throw new Error(`Card '${cardKey}' not found`);
509
+ }
510
+ currentState = card.metadata.workflowState;
511
+ }
512
+ return commands.calculateCmd.runWorkflowGraph(workflowName, {
513
+ currentState,
514
+ });
515
+ });
516
+ }
517
+
470
518
  /**
471
519
  * Perform an updateOperation on a resource key.
472
520
  * This delegates to data-handler Update.applyResourceOperation.