@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.
- package/dist/app.d.ts +3 -3
- package/dist/app.js +58 -29
- package/dist/app.js.map +1 -1
- package/dist/domain/cards/index.js +63 -1
- package/dist/domain/cards/index.js.map +1 -1
- package/dist/domain/cards/lib.js +7 -1
- package/dist/domain/cards/lib.js.map +1 -1
- package/dist/domain/cards/schema.d.ts +8 -0
- package/dist/domain/cards/schema.js +7 -0
- package/dist/domain/cards/schema.js.map +1 -1
- package/dist/domain/cards/service.d.ts +2 -0
- package/dist/domain/cards/service.js +18 -1
- package/dist/domain/cards/service.js.map +1 -1
- package/dist/domain/mcp/index.d.ts +8 -2
- package/dist/domain/mcp/index.js +68 -65
- package/dist/domain/mcp/index.js.map +1 -1
- package/dist/domain/project/service.js +1 -1
- package/dist/domain/project/service.js.map +1 -1
- package/dist/domain/projects/index.d.ts +15 -0
- package/dist/domain/projects/index.js +35 -0
- package/dist/domain/projects/index.js.map +1 -0
- package/dist/domain/resources/index.js +63 -1
- package/dist/domain/resources/index.js.map +1 -1
- package/dist/domain/resources/schema.d.ts +9 -0
- package/dist/domain/resources/schema.js +8 -1
- package/dist/domain/resources/schema.js.map +1 -1
- package/dist/domain/resources/service.d.ts +12 -0
- package/dist/domain/resources/service.js +49 -6
- package/dist/domain/resources/service.js.map +1 -1
- package/dist/export.d.ts +7 -3
- package/dist/export.js +32 -16
- package/dist/export.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/main.js +41 -6
- package/dist/main.js.map +1 -1
- package/dist/middleware/auth.js +10 -0
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/commandManager.d.ts +12 -0
- package/dist/middleware/commandManager.js +33 -7
- package/dist/middleware/commandManager.js.map +1 -1
- package/dist/project-registry.d.ts +50 -0
- package/dist/project-registry.js +77 -0
- package/dist/project-registry.js.map +1 -0
- package/dist/public/THIRD-PARTY.txt +2899 -1974
- package/dist/public/assets/architecture-7EHR7CIX-BhpB9ddF.js +1 -0
- package/dist/public/assets/architectureDiagram-3BPJPVTR-BQYiU_EO.js +36 -0
- package/dist/public/assets/blockDiagram-GPEHLZMM-DuyikC3X.js +132 -0
- package/dist/public/assets/c4Diagram-AAUBKEIU-BG8_sPEr.js +10 -0
- package/dist/public/assets/channel-BxgB7fMy.js +1 -0
- package/dist/public/assets/chunk-2J33WTMH-vNq8B1aw.js +1 -0
- package/dist/public/assets/chunk-4BX2VUAB-DFDBsmSo.js +1 -0
- package/dist/public/assets/chunk-55IACEB6-DCVUQPWM.js +1 -0
- package/dist/public/assets/chunk-727SXJPM-C5ihZMyl.js +206 -0
- package/dist/public/assets/chunk-AQP2D5EJ-XGOtp2xP.js +231 -0
- package/dist/public/assets/chunk-FMBD7UC4-Da1lR6Mn.js +15 -0
- package/dist/public/assets/chunk-ND2GUHAM-ZOvpvr6K.js +1 -0
- package/dist/public/assets/chunk-QZHKN3VN-D33jzvFT.js +1 -0
- package/dist/public/assets/classDiagram-4FO5ZUOK-hWfZv7hZ.js +1 -0
- package/dist/public/assets/classDiagram-v2-Q7XG4LA2-hWfZv7hZ.js +1 -0
- package/dist/public/assets/cose-bilkent-S5V4N54A-DO4z-ix4.js +1 -0
- package/dist/public/assets/cytoscape.esm-C8YCVR3_.js +321 -0
- package/dist/public/assets/dagre-BM42HDAG-DlpRfzgA.js +4 -0
- package/dist/public/assets/dagre-Bx709z4p.js +1 -0
- package/dist/public/assets/diagram-2AECGRRQ-D-t_ImBP.js +43 -0
- package/dist/public/assets/diagram-5GNKFQAL-CBgUMlXz.js +10 -0
- package/dist/public/assets/diagram-KO2AKTUF-XoB2TgQt.js +3 -0
- package/dist/public/assets/diagram-LMA3HP47-D1Sbl_eS.js +24 -0
- package/dist/public/assets/diagram-OG6HWLK6-DKP4aiIY.js +24 -0
- package/dist/public/assets/erDiagram-TEJ5UH35-DYxfHOOK.js +85 -0
- package/dist/public/assets/eventmodeling-FCH6USID-cF_1Mq4g.js +1 -0
- package/dist/public/assets/flowDiagram-I6XJVG4X-BDHPsmlq.js +162 -0
- package/dist/public/assets/ganttDiagram-6RSMTGT7-bGgIvBPN.js +292 -0
- package/dist/public/assets/gitGraph-WXDBUCRP-DOFshjLy.js +1 -0
- package/dist/public/assets/gitGraphDiagram-PVQCEYII-xSwLjGd-.js +106 -0
- package/dist/public/assets/graphlib-B8gBHxth.js +1 -0
- package/dist/public/assets/index-DGPv1qic.js +1028 -0
- package/dist/public/assets/index-DvHiopvR.css +1 -0
- package/dist/public/assets/info-J43DQDTF-BuJNK7zQ.js +1 -0
- package/dist/public/assets/infoDiagram-5YYISTIA-BanxuIib.js +2 -0
- package/dist/public/assets/ishikawaDiagram-YF4QCWOH-DWNWYxz5.js +70 -0
- package/dist/public/assets/journeyDiagram-JHISSGLW-I58P5XNg.js +139 -0
- package/dist/public/assets/kanban-definition-UN3LZRKU-CMRWbDti.js +89 -0
- package/dist/public/assets/katex-C4eR7coU.js +257 -0
- package/dist/public/assets/mermaid-parser.core-Dz__fM3g.js +161 -0
- package/dist/public/assets/mindmap-definition-RKZ34NQL-C47gCcpC.js +96 -0
- package/dist/public/assets/packet-YPE3B663-Cczitw2-.js +1 -0
- package/dist/public/assets/pie-LRSECV5Y-rO-Aqx6h.js +1 -0
- package/dist/public/assets/pieDiagram-4H26LBE5-VZAxHzjD.js +30 -0
- package/dist/public/assets/quadrantDiagram-W4KKPZXB-BY8JORvE.js +7 -0
- package/dist/public/assets/radar-GUYGQ44K-SSIGuQjW.js +1 -0
- package/dist/public/assets/requirementDiagram-4Y6WPE33-XhNBeFwj.js +84 -0
- package/dist/public/assets/sankeyDiagram-5OEKKPKP-H7I2OESy.js +40 -0
- package/dist/public/assets/sequenceDiagram-3UESZ5HK-jnTLwq-X.js +162 -0
- package/dist/public/assets/stateDiagram-AJRCARHV-BKcf2bdX.js +1 -0
- package/dist/public/assets/stateDiagram-v2-BHNVJYJU-wpO0gnsG.js +1 -0
- package/dist/public/assets/timeline-definition-PNZ67QCA-BZbaBDRH.js +120 -0
- package/dist/public/assets/treeView-BLDUP644-DkGx4HkR.js +1 -0
- package/dist/public/assets/treemap-LRROVOQU-yCyuONQh.js +1 -0
- package/dist/public/assets/vennDiagram-CIIHVFJN-nY9Pep3o.js +34 -0
- package/dist/public/assets/wardley-L42UT6IY-CXWWFUgk.js +1 -0
- package/dist/public/assets/wardleyDiagram-YWT4CUSO-CM0yrkHd.js +78 -0
- package/dist/public/assets/xychartDiagram-2RQKCTM6-1ZAtqvyQ.js +7 -0
- package/dist/public/config.json +1 -0
- package/dist/public/index.html +2 -2
- package/package.json +11 -7
- package/src/app.ts +71 -31
- package/src/domain/cards/index.ts +73 -0
- package/src/domain/cards/lib.ts +8 -1
- package/src/domain/cards/schema.ts +9 -0
- package/src/domain/cards/service.ts +28 -2
- package/src/domain/mcp/index.ts +83 -78
- package/src/domain/project/service.ts +1 -1
- package/src/domain/projects/index.ts +39 -0
- package/src/domain/resources/index.ts +74 -0
- package/src/domain/resources/schema.ts +14 -0
- package/src/domain/resources/service.ts +52 -4
- package/src/export.ts +44 -21
- package/src/index.ts +6 -5
- package/src/main.ts +46 -6
- package/src/middleware/auth.ts +10 -0
- package/src/middleware/commandManager.ts +47 -9
- package/src/project-registry.ts +110 -0
- package/dist/public/assets/index-Cdn_jRWy.js +0 -720
- 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}:
|
package/src/domain/cards/lib.ts
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/src/domain/mcp/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
|
45
|
+
const modules = await commands.showCmd.showModules();
|
|
46
46
|
return Promise.all(
|
|
47
|
-
|
|
47
|
+
modules.map(async (mod) => {
|
|
48
48
|
try {
|
|
49
|
-
const module = await commands.showCmd.showModule(
|
|
49
|
+
const module = await commands.showCmd.showModule(mod.name);
|
|
50
50
|
return { name: module.name, cardKeyPrefix: module.cardKeyPrefix };
|
|
51
51
|
} catch {
|
|
52
|
-
return { name:
|
|
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.
|