@cyberismo/backend 0.0.24 → 0.0.26
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/lib.js +8 -1
- package/dist/domain/cards/lib.js.map +1 -1
- package/dist/domain/cards/service.js +6 -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/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/service.js +2 -2
- 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 +3482 -4525
- 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-AHTNJAMY-CW7AUKhY.js → c4Diagram-AAUBKEIU-BG8_sPEr.js} +6 -6
- 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-DChB7ne2.js → chunk-FMBD7UC4-Da1lR6Mn.js} +1 -1
- 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-DpmcErfs.js → cytoscape.esm-C8YCVR3_.js} +3 -3
- 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-UXIWVN3A-B3HoSFaq.js → ishikawaDiagram-YF4QCWOH-DWNWYxz5.js} +6 -6
- package/dist/public/assets/{journeyDiagram-VCZTEJTY-kH30WNF3.js → journeyDiagram-JHISSGLW-I58P5XNg.js} +6 -6
- package/dist/public/assets/kanban-definition-UN3LZRKU-CMRWbDti.js +89 -0
- package/dist/public/assets/mermaid-parser.core-Dz__fM3g.js +161 -0
- package/dist/public/assets/{mindmap-definition-QFDTVHPH-CNWWI4MF.js → mindmap-definition-RKZ34NQL-C47gCcpC.js} +29 -29
- 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-GMOUNBTQ-9MnBod43.js → timeline-definition-PNZ67QCA-BZbaBDRH.js} +8 -8
- 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-5P7HB3ND-BcqyvmmW.js → xychartDiagram-2RQKCTM6-1ZAtqvyQ.js} +6 -6
- package/dist/public/config.json +1 -0
- package/dist/public/index.html +2 -31
- package/package.json +9 -5
- package/src/app.ts +71 -31
- package/src/domain/cards/lib.ts +13 -1
- package/src/domain/cards/service.ts +11 -1
- package/src/domain/mcp/index.ts +83 -78
- package/src/domain/projects/index.ts +39 -0
- package/src/domain/resources/service.ts +2 -0
- 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/arc-DFTvTCxD.js +0 -1
- package/dist/public/assets/architecture-YZFGNWBL-DsWVZJri.js +0 -1
- package/dist/public/assets/architectureDiagram-Q4EWVU46-AN0fWZIG.js +0 -36
- package/dist/public/assets/array-xS8TccZC.js +0 -1
- package/dist/public/assets/blockDiagram-DXYQGD6D-RrIidZT3.js +0 -132
- package/dist/public/assets/channel-BxffgrNT.js +0 -1
- package/dist/public/assets/chunk-2KRD3SAO-D5XH6bj9.js +0 -1
- package/dist/public/assets/chunk-336JU56O-CVDEj5x8.js +0 -2
- package/dist/public/assets/chunk-426QAEUC-CIWkCWTf.js +0 -1
- package/dist/public/assets/chunk-4BX2VUAB-O8dxzEpn.js +0 -1
- package/dist/public/assets/chunk-4TB4RGXK-Bt4fWDlh.js +0 -206
- package/dist/public/assets/chunk-55IACEB6-R-yr7oHq.js +0 -1
- package/dist/public/assets/chunk-5FUZZQ4R-D7L4hZzZ.js +0 -62
- package/dist/public/assets/chunk-5PVQY5BW-D46cRkay.js +0 -2
- package/dist/public/assets/chunk-67CJDMHE-1fLguPDq.js +0 -1
- package/dist/public/assets/chunk-7N4EOEYR-BPbEiVZr.js +0 -1
- package/dist/public/assets/chunk-AA7GKIK3-DZOqN73n.js +0 -1
- package/dist/public/assets/chunk-BSJP7CBP-BQPMRa-Q.js +0 -1
- package/dist/public/assets/chunk-CFjPhJqf.js +0 -1
- package/dist/public/assets/chunk-CIAEETIT-CoKBG93U.js +0 -1
- package/dist/public/assets/chunk-EDXVE4YY-BxmMvdBY.js +0 -1
- package/dist/public/assets/chunk-ENJZ2VHE-BrjxYY_T.js +0 -10
- package/dist/public/assets/chunk-FOC6F5B3-DyK4SoM2.js +0 -1
- package/dist/public/assets/chunk-ICPOFSXX-6-ABzkfw.js +0 -122
- package/dist/public/assets/chunk-K5T4RW27-432kjUXO.js +0 -94
- package/dist/public/assets/chunk-KGLVRYIC-ChpiuJys.js +0 -1
- package/dist/public/assets/chunk-LIHQZDEY-C65uflEj.js +0 -1
- package/dist/public/assets/chunk-ORNJ4GCN-DPvOqXKg.js +0 -1
- package/dist/public/assets/chunk-OYMX7WX6-BecMUsrL.js +0 -231
- package/dist/public/assets/chunk-QZHKN3VN-d1i_Lm1t.js +0 -1
- package/dist/public/assets/chunk-U2HBQHQK-C6yvwOAo.js +0 -70
- package/dist/public/assets/chunk-X2U36JSP-D1C3tOED.js +0 -1
- package/dist/public/assets/chunk-XPW4576I-BH37LuuF.js +0 -32
- package/dist/public/assets/chunk-YZCP3GAM-D83gHdmx.js +0 -1
- package/dist/public/assets/chunk-ZZ45TVLE-DrCjtkdu.js +0 -1
- package/dist/public/assets/classDiagram-6PBFFD2Q-CRYacdjd.js +0 -1
- package/dist/public/assets/classDiagram-v2-HSJHXN6E-qOJXC_A9.js +0 -1
- package/dist/public/assets/clone-Bno0nirE.js +0 -1
- package/dist/public/assets/colors-DZGTowqM.js +0 -1
- package/dist/public/assets/cose-bilkent-S5V4N54A-BdSMsFO7.js +0 -1
- package/dist/public/assets/dagre-D0KOcyEK.js +0 -1
- package/dist/public/assets/dagre-KV5264BT-C_l1Bck_.js +0 -4
- package/dist/public/assets/defaultLocale-B6dPnyNg.js +0 -1
- package/dist/public/assets/diagram-5BDNPKRD-D6FxiIv1.js +0 -10
- package/dist/public/assets/diagram-G4DWMVQ6-COGBv7tQ.js +0 -24
- package/dist/public/assets/diagram-MMDJMWI5-U6E9w334.js +0 -43
- package/dist/public/assets/diagram-TYMM5635-Tvfdnlo4.js +0 -24
- package/dist/public/assets/dist-C6vvxvVV.js +0 -1
- package/dist/public/assets/erDiagram-SMLLAGMA-VcPXkmy9.js +0 -85
- package/dist/public/assets/flatten-BKeNCJRx.js +0 -1
- package/dist/public/assets/flowDiagram-DWJPFMVM-PRQdlypB.js +0 -162
- package/dist/public/assets/ganttDiagram-T4ZO3ILL-CSVaTciZ.js +0 -292
- package/dist/public/assets/gitGraph-7Q5UKJZL-C6GVmHC4.js +0 -1
- package/dist/public/assets/gitGraphDiagram-UUTBAWPF-DMa-Gcy1.js +0 -106
- package/dist/public/assets/graphlib-BYojtb6k.js +0 -1
- package/dist/public/assets/identity-nF-8qK2-.js +0 -1
- package/dist/public/assets/index-oVbUFngg.js +0 -737
- package/dist/public/assets/index-ypsafPwV.css +0 -1
- package/dist/public/assets/info-OMHHGYJF-CjsmQeYR.js +0 -1
- package/dist/public/assets/infoDiagram-42DDH7IO-COL_Na4w.js +0 -2
- package/dist/public/assets/init-B9nbfZCT.js +0 -1
- package/dist/public/assets/isEmpty-Di-NpihJ.js +0 -1
- package/dist/public/assets/kanban-definition-6JOO6SKY-RdjEsejt.js +0 -89
- package/dist/public/assets/line-DrKJYWGi.js +0 -1
- package/dist/public/assets/linear-C8MDrvxz.js +0 -1
- package/dist/public/assets/mermaid-parser.core-BeZK8g2G.js +0 -4
- package/dist/public/assets/ordinal-BXaEVJbT.js +0 -1
- package/dist/public/assets/packet-4T2RLAQJ-Dq0Z9G3r.js +0 -1
- package/dist/public/assets/path-C7Bv0Qdk.js +0 -1
- package/dist/public/assets/pie-ZZUOXDRM-Coeb8Atk.js +0 -1
- package/dist/public/assets/pieDiagram-DEJITSTG-CrDW-30P.js +0 -30
- package/dist/public/assets/quadrantDiagram-34T5L4WZ-BoDA5WQ-.js +0 -7
- package/dist/public/assets/radar-PYXPWWZC-BF2NN2TG.js +0 -1
- package/dist/public/assets/range-DuD7Go1R.js +0 -1
- package/dist/public/assets/reduce-5tLTMRkg.js +0 -1
- package/dist/public/assets/requirementDiagram-MS252O5E-BnG7epLH.js +0 -84
- package/dist/public/assets/rough.esm-DE7XMpOC.js +0 -1
- package/dist/public/assets/sankeyDiagram-XADWPNL6-oyTFoxhB.js +0 -10
- package/dist/public/assets/sequenceDiagram-FGHM5R23-hwG2jT_2.js +0 -157
- package/dist/public/assets/src-ps-3oTnY.js +0 -1
- package/dist/public/assets/stateDiagram-FHFEXIEX-BiYqw63j.js +0 -1
- package/dist/public/assets/stateDiagram-v2-QKLJ7IA2-CggSrcDh.js +0 -1
- package/dist/public/assets/time-Bgnk7ODM.js +0 -1
- package/dist/public/assets/treeView-SZITEDCU-IRdgIoRV.js +0 -1
- package/dist/public/assets/treemap-DK8fitek.js +0 -1
- package/dist/public/assets/treemap-W4RFUUIX-C-JBwyWW.js +0 -1
- package/dist/public/assets/vennDiagram-DHZGUBPP-sEA6JNL-.js +0 -34
- package/dist/public/assets/wardley-RL74JXVD-DdU04Q7E.js +0 -1
- package/dist/public/assets/wardleyDiagram-NUSXRM2D-Dws321pY.js +0 -20
- /package/dist/public/assets/{katex-Bfn1OZEl.js → katex-C4eR7coU.js} +0 -0
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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
}
|
package/src/export.ts
CHANGED
|
@@ -18,6 +18,7 @@ import fs, { readFile } from 'node:fs/promises';
|
|
|
18
18
|
import type { CommandManager } from '@cyberismo/data-handler';
|
|
19
19
|
import { createApp } from './app.js';
|
|
20
20
|
import { MockAuthProvider } from './auth/mock.js';
|
|
21
|
+
import type { ProjectRegistry } from './project-registry.js';
|
|
21
22
|
import { cp, writeFile } from 'node:fs/promises';
|
|
22
23
|
import { staticFrontendDirRelative } from './utils.js';
|
|
23
24
|
import type { QueryResult } from '@cyberismo/data-handler/types/queries';
|
|
@@ -28,7 +29,11 @@ import {
|
|
|
28
29
|
findRelevantAttachments,
|
|
29
30
|
} from './domain/cards/service.js';
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
export interface ExportSiteOptions extends TreeOptions {
|
|
33
|
+
defaultProject?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const _cardQueryCache = new Map<string, Promise<QueryResult<'card'>[]>>();
|
|
32
37
|
const OVERHEAD_CALLS = 6; // estimated number of overhead calls during export in addition to card exports
|
|
33
38
|
|
|
34
39
|
/**
|
|
@@ -36,7 +41,7 @@ const OVERHEAD_CALLS = 6; // estimated number of overhead calls during export in
|
|
|
36
41
|
* Also resets the card query promise.
|
|
37
42
|
*/
|
|
38
43
|
export function reset() {
|
|
39
|
-
|
|
44
|
+
_cardQueryCache.clear();
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
/**
|
|
@@ -50,15 +55,14 @@ export async function getCardQueryResult(
|
|
|
50
55
|
commands: CommandManager,
|
|
51
56
|
cardKey?: string,
|
|
52
57
|
): Promise<QueryResult<'card'>[]> {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
'exportedSite',
|
|
58
|
-
{},
|
|
58
|
+
const prefix = commands.project.configuration.cardKeyPrefix;
|
|
59
|
+
if (!_cardQueryCache.has(prefix)) {
|
|
60
|
+
_cardQueryCache.set(
|
|
61
|
+
prefix,
|
|
62
|
+
commands.calculateCmd.runQuery('card', 'exportedSite', {}),
|
|
59
63
|
);
|
|
60
64
|
}
|
|
61
|
-
return
|
|
65
|
+
return _cardQueryCache.get(prefix)!.then((results) => {
|
|
62
66
|
if (!cardKey) {
|
|
63
67
|
return results;
|
|
64
68
|
}
|
|
@@ -73,29 +77,38 @@ export async function getCardQueryResult(
|
|
|
73
77
|
/**
|
|
74
78
|
* Export the site to a given directory.
|
|
75
79
|
* Note: Do not call this function in parallel.
|
|
76
|
-
* @param
|
|
80
|
+
* @param registry - ProjectRegistry holding all project CommandManagers.
|
|
77
81
|
* @param exportDir - Directory to export to.
|
|
78
82
|
* @param options - Export options.
|
|
79
83
|
* @param options.recursive - Whether to export cards recursively.
|
|
80
84
|
* @param options.cardKey - Key of the card to export. If not provided, all cards will be exported.
|
|
81
|
-
* @param
|
|
85
|
+
* @param options.defaultProject - Default project prefix to write into config.json.
|
|
82
86
|
* @param onProgress - Optional progress callback function.
|
|
83
87
|
* @returns An object containing any errors that occurred during export.
|
|
84
88
|
*/
|
|
85
89
|
export async function exportSite(
|
|
86
|
-
|
|
90
|
+
registry: ProjectRegistry,
|
|
87
91
|
exportDir?: string,
|
|
88
|
-
options?:
|
|
92
|
+
options?: ExportSiteOptions,
|
|
89
93
|
onProgress?: (current: number, total: number) => void,
|
|
90
94
|
): Promise<{ errors: string[] }> {
|
|
91
95
|
exportDir = exportDir || 'static';
|
|
92
|
-
const
|
|
96
|
+
const { defaultProject, ...treeOpts } = options ?? {};
|
|
97
|
+
const opts: TreeOptions = {
|
|
93
98
|
recursive: false,
|
|
94
|
-
|
|
95
|
-
...options,
|
|
99
|
+
...treeOpts,
|
|
96
100
|
};
|
|
97
101
|
|
|
98
|
-
|
|
102
|
+
if (defaultProject && !registry.has(defaultProject)) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Default project '${defaultProject}' is not in the registry. Available: ${registry
|
|
105
|
+
.list()
|
|
106
|
+
.map((p) => p.prefix)
|
|
107
|
+
.join(', ')}`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const app = createApp(new MockAuthProvider(), registry, opts, true);
|
|
99
112
|
|
|
100
113
|
// copy whole frontend to the same directory
|
|
101
114
|
await cp(staticFrontendDirRelative, exportDir, { recursive: true });
|
|
@@ -103,6 +116,9 @@ export async function exportSite(
|
|
|
103
116
|
const config = await readFile(path.join(exportDir, 'config.json'), 'utf-8');
|
|
104
117
|
const configJson = JSON.parse(config);
|
|
105
118
|
configJson.staticMode = true;
|
|
119
|
+
if (defaultProject) {
|
|
120
|
+
configJson.defaultProject = defaultProject;
|
|
121
|
+
}
|
|
106
122
|
await writeFile(
|
|
107
123
|
path.join(exportDir, 'config.json'),
|
|
108
124
|
JSON.stringify(configJson),
|
|
@@ -110,10 +126,13 @@ export async function exportSite(
|
|
|
110
126
|
|
|
111
127
|
reset();
|
|
112
128
|
|
|
113
|
-
// estimate total based on the number of cards to export
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
129
|
+
// estimate total based on the number of cards to export across all projects
|
|
130
|
+
let total = OVERHEAD_CALLS;
|
|
131
|
+
for (const commands of registry.values()) {
|
|
132
|
+
const cards = await findAllCards(commands, opts);
|
|
133
|
+
const attachments = await findRelevantAttachments(commands, opts);
|
|
134
|
+
total += cards.length + attachments.length;
|
|
135
|
+
}
|
|
117
136
|
|
|
118
137
|
// Actual export with progress reporting
|
|
119
138
|
let done = 0;
|
|
@@ -130,6 +149,10 @@ export async function exportSite(
|
|
|
130
149
|
if (url.pathname.startsWith('/mcp')) {
|
|
131
150
|
return false;
|
|
132
151
|
}
|
|
152
|
+
// Skip OIDC/well-known routes — not relevant for static export
|
|
153
|
+
if (url.pathname.startsWith('/.well-known')) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
133
156
|
return req;
|
|
134
157
|
},
|
|
135
158
|
afterResponseHook: async (response) => {
|
package/src/index.ts
CHANGED
|
@@ -16,14 +16,15 @@ import { Hono } from 'hono';
|
|
|
16
16
|
import { serveStatic } from '@hono/node-server/serve-static';
|
|
17
17
|
import path from 'node:path';
|
|
18
18
|
import { readFile } from 'node:fs/promises';
|
|
19
|
-
import type { CommandManager } from '@cyberismo/data-handler';
|
|
20
19
|
import { findFreePort } from './utils.js';
|
|
21
20
|
import { createApp } from './app.js';
|
|
22
21
|
import type { AuthProvider } from './auth/types.js';
|
|
22
|
+
import type { ProjectRegistry } from './project-registry.js';
|
|
23
23
|
export { MockAuthProvider } from './auth/mock.js';
|
|
24
24
|
export type { MockUserConfig } from './auth/mock.js';
|
|
25
25
|
export type { AuthProvider } from './auth/types.js';
|
|
26
|
-
export { exportSite } from './export.js';
|
|
26
|
+
export { exportSite, type ExportSiteOptions } from './export.js';
|
|
27
|
+
export { ProjectRegistry } from './project-registry.js';
|
|
27
28
|
|
|
28
29
|
const DEFAULT_PORT = 3000;
|
|
29
30
|
const DEFAULT_MAX_PORT = DEFAULT_PORT + 100;
|
|
@@ -56,12 +57,12 @@ export async function previewSite(dir: string, findPort: boolean = true) {
|
|
|
56
57
|
/**
|
|
57
58
|
* Start the server
|
|
58
59
|
* @param authProvider - Authentication provider
|
|
59
|
-
* @param
|
|
60
|
+
* @param registry - ProjectRegistry holding all project CommandManagers
|
|
60
61
|
* @param findPort - If true, find a free port
|
|
61
62
|
*/
|
|
62
63
|
export async function startServer(
|
|
63
64
|
authProvider: AuthProvider,
|
|
64
|
-
|
|
65
|
+
registry: ProjectRegistry,
|
|
65
66
|
findPort: boolean = true,
|
|
66
67
|
) {
|
|
67
68
|
let port = parseInt(process.env.PORT || DEFAULT_PORT.toString(), 10);
|
|
@@ -69,7 +70,7 @@ export async function startServer(
|
|
|
69
70
|
if (findPort) {
|
|
70
71
|
port = await findFreePort(port, DEFAULT_MAX_PORT);
|
|
71
72
|
}
|
|
72
|
-
const app = createApp(authProvider,
|
|
73
|
+
const app = createApp(authProvider, registry);
|
|
73
74
|
startApp(app, port);
|
|
74
75
|
}
|
|
75
76
|
|
package/src/main.ts
CHANGED
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
details. You should have received a copy of the GNU Affero General Public
|
|
11
11
|
License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
12
12
|
*/
|
|
13
|
-
import {
|
|
13
|
+
import { scanForProjects } from '@cyberismo/data-handler';
|
|
14
14
|
import { startServer } from './index.js';
|
|
15
15
|
import { exportSite } from './export.js';
|
|
16
16
|
import { MockAuthProvider } from './auth/mock.js';
|
|
17
17
|
import { KeycloakAuthProvider } from './auth/keycloak.js';
|
|
18
18
|
import type { AuthProvider } from './auth/types.js';
|
|
19
|
+
import { ProjectRegistry } from './project-registry.js';
|
|
20
|
+
import { parseArgs } from 'node:util';
|
|
19
21
|
import dotenv from 'dotenv';
|
|
20
22
|
|
|
21
23
|
// Load environment variables from .env file
|
|
@@ -55,12 +57,50 @@ function createAuthProvider(): AuthProvider {
|
|
|
55
57
|
process.exit(1);
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
const projectPath = process.env.npm_config_project_path ||
|
|
59
|
-
|
|
60
|
+
const projectPath = process.env.npm_config_project_path || process.cwd();
|
|
61
|
+
let projects;
|
|
62
|
+
try {
|
|
63
|
+
projects = await scanForProjects(projectPath);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error(
|
|
66
|
+
error instanceof Error
|
|
67
|
+
? error.message
|
|
68
|
+
: `Failed to scan for projects in '${projectPath}'`,
|
|
69
|
+
);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { values: args } = parseArgs({
|
|
74
|
+
options: {
|
|
75
|
+
export: { type: 'boolean', default: false },
|
|
76
|
+
'default-project': { type: 'string' },
|
|
77
|
+
},
|
|
78
|
+
strict: false,
|
|
79
|
+
});
|
|
60
80
|
|
|
61
|
-
if (
|
|
62
|
-
|
|
81
|
+
if (args.export) {
|
|
82
|
+
if (projects.length === 0) {
|
|
83
|
+
console.error('No projects found to export.');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
const registry = await ProjectRegistry.fromScannedProjects(projects);
|
|
87
|
+
await exportSite(registry, undefined, {
|
|
88
|
+
defaultProject:
|
|
89
|
+
typeof args['default-project'] === 'string'
|
|
90
|
+
? args['default-project']
|
|
91
|
+
: undefined,
|
|
92
|
+
});
|
|
63
93
|
} else {
|
|
94
|
+
if (projects.length === 0) {
|
|
95
|
+
console.error(
|
|
96
|
+
`No projects found in "${projectPath}". Cannot start the server without at least one project.`,
|
|
97
|
+
);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
const autocommit = process.env.CYBERISMO_AUTOCOMMIT === 'true';
|
|
101
|
+
const registry = await ProjectRegistry.fromScannedProjects(projects, {
|
|
102
|
+
autocommit,
|
|
103
|
+
});
|
|
64
104
|
const authProvider = createAuthProvider();
|
|
65
|
-
await startServer(authProvider,
|
|
105
|
+
await startServer(authProvider, registry);
|
|
66
106
|
}
|
package/src/middleware/auth.ts
CHANGED
|
@@ -42,6 +42,16 @@ export function createAuthMiddleware(
|
|
|
42
42
|
if (user) {
|
|
43
43
|
c.set('user', user);
|
|
44
44
|
} else {
|
|
45
|
+
// RFC 9728 §5.1: include resource_metadata in WWW-Authenticate
|
|
46
|
+
// only for MCP routes, where the metadata document applies.
|
|
47
|
+
const issuer = process.env.OIDC_ISSUER;
|
|
48
|
+
if (issuer && c.req.path.startsWith('/mcp')) {
|
|
49
|
+
const origin = new URL(issuer).origin;
|
|
50
|
+
const resourceUrl = `${origin}/.well-known/oauth-protected-resource/mcp`;
|
|
51
|
+
return c.json({ error: 'Unauthorized' }, 401, {
|
|
52
|
+
'WWW-Authenticate': `Bearer resource_metadata="${resourceUrl}"`,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
45
55
|
return c.json({ error: 'Unauthorized' }, 401);
|
|
46
56
|
}
|
|
47
57
|
|
|
@@ -13,28 +13,66 @@
|
|
|
13
13
|
import type { Context, MiddlewareHandler } from 'hono';
|
|
14
14
|
import type { CommandManager } from '@cyberismo/data-handler';
|
|
15
15
|
import { getCurrentUser } from './auth.js';
|
|
16
|
+
import type { ProjectRegistry } from '../project-registry.js';
|
|
16
17
|
|
|
17
18
|
// Extend Hono Context type to include our custom properties
|
|
18
19
|
declare module 'hono' {
|
|
19
20
|
interface ContextVariableMap {
|
|
20
21
|
commands: CommandManager;
|
|
21
22
|
projectPath: string;
|
|
23
|
+
registry: ProjectRegistry;
|
|
22
24
|
}
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Set CommandManager on context and run the next handler as the authenticated user.
|
|
29
|
+
*/
|
|
30
|
+
async function runWithCommands(
|
|
31
|
+
c: Context,
|
|
32
|
+
commands: CommandManager,
|
|
33
|
+
next: () => Promise<void>,
|
|
34
|
+
) {
|
|
35
|
+
const user = getCurrentUser(c);
|
|
36
|
+
if (!user) {
|
|
37
|
+
throw new Error('CommandManager expects a user');
|
|
38
|
+
}
|
|
39
|
+
c.set('commands', commands);
|
|
40
|
+
c.set('projectPath', commands.project.basePath);
|
|
41
|
+
await commands.runAsAuthor({ name: user.name, email: user.email }, () =>
|
|
42
|
+
next(),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// TODO: Remove once MCP is made project-scoped via attachProjectRegistry
|
|
25
47
|
export const attachCommandManager = (
|
|
26
48
|
commands: CommandManager,
|
|
49
|
+
): MiddlewareHandler => {
|
|
50
|
+
return (c, next) => runWithCommands(c, commands, next);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Middleware that resolves the project from the registry and sets the
|
|
55
|
+
* CommandManager on context.
|
|
56
|
+
*
|
|
57
|
+
* @param registry - Project registry to look up projects.
|
|
58
|
+
* @param fixedPrefix - When provided, used instead of the `:prefix` route
|
|
59
|
+
* param. This is needed in export/SSG mode where routes are mounted at
|
|
60
|
+
* concrete paths (e.g. `/api/projects/decision/...`) with no dynamic param.
|
|
61
|
+
*/
|
|
62
|
+
export const attachProjectRegistry = (
|
|
63
|
+
registry: ProjectRegistry,
|
|
64
|
+
fixedPrefix?: string,
|
|
27
65
|
): MiddlewareHandler => {
|
|
28
66
|
return async (c: Context, next) => {
|
|
29
|
-
c.set('
|
|
30
|
-
c.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
throw new Error('CommandManager expects a user');
|
|
67
|
+
c.set('registry', registry);
|
|
68
|
+
const prefix = c.req.param('prefix') ?? fixedPrefix;
|
|
69
|
+
if (!prefix) {
|
|
70
|
+
return c.json({ error: 'Project prefix is required' }, 400);
|
|
71
|
+
}
|
|
72
|
+
const commands = registry.get(prefix);
|
|
73
|
+
if (!commands) {
|
|
74
|
+
return c.json({ error: `Project '${prefix}' not found` }, 404);
|
|
38
75
|
}
|
|
76
|
+
return runWithCommands(c, commands, next);
|
|
39
77
|
};
|
|
40
78
|
};
|