@graphmemory/server 1.1.0

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 (123) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +216 -0
  3. package/dist/api/index.js +473 -0
  4. package/dist/api/rest/code.js +78 -0
  5. package/dist/api/rest/docs.js +80 -0
  6. package/dist/api/rest/embed.js +47 -0
  7. package/dist/api/rest/files.js +64 -0
  8. package/dist/api/rest/graph.js +71 -0
  9. package/dist/api/rest/index.js +371 -0
  10. package/dist/api/rest/knowledge.js +239 -0
  11. package/dist/api/rest/skills.js +285 -0
  12. package/dist/api/rest/tasks.js +273 -0
  13. package/dist/api/rest/tools.js +157 -0
  14. package/dist/api/rest/validation.js +196 -0
  15. package/dist/api/rest/websocket.js +71 -0
  16. package/dist/api/tools/code/get-file-symbols.js +30 -0
  17. package/dist/api/tools/code/get-symbol.js +22 -0
  18. package/dist/api/tools/code/list-files.js +18 -0
  19. package/dist/api/tools/code/search-code.js +27 -0
  20. package/dist/api/tools/code/search-files.js +22 -0
  21. package/dist/api/tools/context/get-context.js +19 -0
  22. package/dist/api/tools/docs/cross-references.js +76 -0
  23. package/dist/api/tools/docs/explain-symbol.js +55 -0
  24. package/dist/api/tools/docs/find-examples.js +52 -0
  25. package/dist/api/tools/docs/get-node.js +24 -0
  26. package/dist/api/tools/docs/get-toc.js +22 -0
  27. package/dist/api/tools/docs/list-snippets.js +46 -0
  28. package/dist/api/tools/docs/list-topics.js +18 -0
  29. package/dist/api/tools/docs/search-files.js +22 -0
  30. package/dist/api/tools/docs/search-snippets.js +43 -0
  31. package/dist/api/tools/docs/search.js +27 -0
  32. package/dist/api/tools/file-index/get-file-info.js +21 -0
  33. package/dist/api/tools/file-index/list-all-files.js +28 -0
  34. package/dist/api/tools/file-index/search-all-files.js +24 -0
  35. package/dist/api/tools/knowledge/add-attachment.js +31 -0
  36. package/dist/api/tools/knowledge/create-note.js +20 -0
  37. package/dist/api/tools/knowledge/create-relation.js +29 -0
  38. package/dist/api/tools/knowledge/delete-note.js +19 -0
  39. package/dist/api/tools/knowledge/delete-relation.js +23 -0
  40. package/dist/api/tools/knowledge/find-linked-notes.js +25 -0
  41. package/dist/api/tools/knowledge/get-note.js +20 -0
  42. package/dist/api/tools/knowledge/list-notes.js +18 -0
  43. package/dist/api/tools/knowledge/list-relations.js +17 -0
  44. package/dist/api/tools/knowledge/remove-attachment.js +19 -0
  45. package/dist/api/tools/knowledge/search-notes.js +25 -0
  46. package/dist/api/tools/knowledge/update-note.js +34 -0
  47. package/dist/api/tools/skills/add-attachment.js +31 -0
  48. package/dist/api/tools/skills/bump-usage.js +19 -0
  49. package/dist/api/tools/skills/create-skill-link.js +25 -0
  50. package/dist/api/tools/skills/create-skill.js +26 -0
  51. package/dist/api/tools/skills/delete-skill-link.js +23 -0
  52. package/dist/api/tools/skills/delete-skill.js +20 -0
  53. package/dist/api/tools/skills/find-linked-skills.js +25 -0
  54. package/dist/api/tools/skills/get-skill.js +21 -0
  55. package/dist/api/tools/skills/link-skill.js +23 -0
  56. package/dist/api/tools/skills/list-skills.js +20 -0
  57. package/dist/api/tools/skills/recall-skills.js +18 -0
  58. package/dist/api/tools/skills/remove-attachment.js +19 -0
  59. package/dist/api/tools/skills/search-skills.js +25 -0
  60. package/dist/api/tools/skills/update-skill.js +58 -0
  61. package/dist/api/tools/tasks/add-attachment.js +31 -0
  62. package/dist/api/tools/tasks/create-task-link.js +25 -0
  63. package/dist/api/tools/tasks/create-task.js +26 -0
  64. package/dist/api/tools/tasks/delete-task-link.js +23 -0
  65. package/dist/api/tools/tasks/delete-task.js +20 -0
  66. package/dist/api/tools/tasks/find-linked-tasks.js +25 -0
  67. package/dist/api/tools/tasks/get-task.js +20 -0
  68. package/dist/api/tools/tasks/link-task.js +23 -0
  69. package/dist/api/tools/tasks/list-tasks.js +25 -0
  70. package/dist/api/tools/tasks/move-task.js +38 -0
  71. package/dist/api/tools/tasks/remove-attachment.js +19 -0
  72. package/dist/api/tools/tasks/search-tasks.js +25 -0
  73. package/dist/api/tools/tasks/update-task.js +58 -0
  74. package/dist/cli/index.js +617 -0
  75. package/dist/cli/indexer.js +275 -0
  76. package/dist/graphs/attachment-types.js +74 -0
  77. package/dist/graphs/code-types.js +10 -0
  78. package/dist/graphs/code.js +204 -0
  79. package/dist/graphs/docs.js +231 -0
  80. package/dist/graphs/file-index-types.js +10 -0
  81. package/dist/graphs/file-index.js +310 -0
  82. package/dist/graphs/file-lang.js +119 -0
  83. package/dist/graphs/knowledge-types.js +32 -0
  84. package/dist/graphs/knowledge.js +768 -0
  85. package/dist/graphs/manager-types.js +87 -0
  86. package/dist/graphs/skill-types.js +10 -0
  87. package/dist/graphs/skill.js +1016 -0
  88. package/dist/graphs/task-types.js +17 -0
  89. package/dist/graphs/task.js +972 -0
  90. package/dist/lib/access.js +67 -0
  91. package/dist/lib/embedder.js +235 -0
  92. package/dist/lib/events-log.js +401 -0
  93. package/dist/lib/file-import.js +328 -0
  94. package/dist/lib/file-mirror.js +461 -0
  95. package/dist/lib/frontmatter.js +17 -0
  96. package/dist/lib/jwt.js +146 -0
  97. package/dist/lib/mirror-watcher.js +637 -0
  98. package/dist/lib/multi-config.js +393 -0
  99. package/dist/lib/parsers/code.js +214 -0
  100. package/dist/lib/parsers/codeblock.js +33 -0
  101. package/dist/lib/parsers/docs.js +199 -0
  102. package/dist/lib/parsers/languages/index.js +15 -0
  103. package/dist/lib/parsers/languages/registry.js +68 -0
  104. package/dist/lib/parsers/languages/types.js +2 -0
  105. package/dist/lib/parsers/languages/typescript.js +306 -0
  106. package/dist/lib/project-manager.js +458 -0
  107. package/dist/lib/promise-queue.js +22 -0
  108. package/dist/lib/search/bm25.js +167 -0
  109. package/dist/lib/search/code.js +103 -0
  110. package/dist/lib/search/docs.js +106 -0
  111. package/dist/lib/search/file-index.js +31 -0
  112. package/dist/lib/search/files.js +61 -0
  113. package/dist/lib/search/knowledge.js +101 -0
  114. package/dist/lib/search/skills.js +104 -0
  115. package/dist/lib/search/tasks.js +103 -0
  116. package/dist/lib/team.js +89 -0
  117. package/dist/lib/watcher.js +67 -0
  118. package/dist/ui/assets/index-D6oxrVF7.js +1759 -0
  119. package/dist/ui/assets/index-kKd4mVrh.css +1 -0
  120. package/dist/ui/favicon.svg +1 -0
  121. package/dist/ui/icons.svg +24 -0
  122. package/dist/ui/index.html +14 -0
  123. package/package.json +89 -0
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createEmbedRouter = createEmbedRouter;
7
+ const crypto_1 = __importDefault(require("crypto"));
8
+ const express_1 = require("express");
9
+ const zod_1 = require("zod");
10
+ const embedder_1 = require("../../lib/embedder");
11
+ /**
12
+ * Create an Express router for the embedding API.
13
+ * POST /api/embed — embed texts using the server's embedding model.
14
+ */
15
+ function createEmbedRouter(apiConfig, modelName) {
16
+ const router = (0, express_1.Router)();
17
+ const embedRequestSchema = zod_1.z.object({
18
+ texts: zod_1.z.array(zod_1.z.string().max(apiConfig.maxTextChars)).min(1).max(apiConfig.maxTexts),
19
+ });
20
+ router.post('/', async (req, res, next) => {
21
+ try {
22
+ // Auth: check embeddingApi.apiKey (separate from user apiKey)
23
+ if (apiConfig.apiKey) {
24
+ const auth = req.headers.authorization;
25
+ if (!auth?.startsWith('Bearer ')) {
26
+ return res.status(401).json({ error: 'Invalid embedding API key' });
27
+ }
28
+ const provided = Buffer.from(auth.slice(7));
29
+ const expected = Buffer.from(apiConfig.apiKey);
30
+ if (provided.length !== expected.length || !crypto_1.default.timingSafeEqual(provided, expected)) {
31
+ return res.status(401).json({ error: 'Invalid embedding API key' });
32
+ }
33
+ }
34
+ const parsed = embedRequestSchema.parse(req.body);
35
+ const inputs = parsed.texts.map(text => ({ title: text, content: '' }));
36
+ const embeddings = await (0, embedder_1.embedBatch)(inputs, modelName);
37
+ res.json({ embeddings });
38
+ }
39
+ catch (err) {
40
+ if (err?.name === 'ZodError') {
41
+ return res.status(400).json({ error: 'Validation error' });
42
+ }
43
+ next(err);
44
+ }
45
+ });
46
+ return router;
47
+ }
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createFilesRouter = createFilesRouter;
7
+ const path_1 = __importDefault(require("path"));
8
+ const express_1 = require("express");
9
+ const validation_1 = require("../../api/rest/validation");
10
+ function createFilesRouter() {
11
+ const router = (0, express_1.Router)({ mergeParams: true });
12
+ function getProject(req) {
13
+ return req.project;
14
+ }
15
+ // List all files
16
+ router.get('/', (0, validation_1.validateQuery)(validation_1.fileListSchema), (req, res, next) => {
17
+ try {
18
+ const p = getProject(req);
19
+ const q = req.validatedQuery;
20
+ const files = p.fileIndexManager.listAllFiles(q);
21
+ res.json({ results: files });
22
+ }
23
+ catch (err) {
24
+ next(err);
25
+ }
26
+ });
27
+ // Search files
28
+ router.get('/search', (0, validation_1.validateQuery)(validation_1.searchQuerySchema), async (req, res, next) => {
29
+ try {
30
+ const p = getProject(req);
31
+ const q = req.validatedQuery;
32
+ const results = await p.fileIndexManager.search(q.q, {
33
+ topK: q.topK,
34
+ minScore: q.minScore,
35
+ });
36
+ res.json({ results });
37
+ }
38
+ catch (err) {
39
+ next(err);
40
+ }
41
+ });
42
+ // Get file info
43
+ router.get('/info', (req, res, next) => {
44
+ try {
45
+ const p = getProject(req);
46
+ const filePath = req.query.path;
47
+ if (!filePath)
48
+ return res.status(400).json({ error: 'path query parameter required' });
49
+ // Prevent path traversal
50
+ const normalized = path_1.default.normalize(filePath);
51
+ if (normalized.startsWith('..') || path_1.default.isAbsolute(normalized)) {
52
+ return res.status(400).json({ error: 'Invalid path' });
53
+ }
54
+ const info = p.fileIndexManager.getFileInfo(normalized);
55
+ if (!info)
56
+ return res.status(404).json({ error: 'File not found' });
57
+ res.json(info);
58
+ }
59
+ catch (err) {
60
+ next(err);
61
+ }
62
+ });
63
+ return router;
64
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createGraphRouter = createGraphRouter;
4
+ const express_1 = require("express");
5
+ const validation_1 = require("../../api/rest/validation");
6
+ function exportGraph(graph, graphName) {
7
+ const nodes = [];
8
+ const edges = [];
9
+ graph.forEachNode((id, attrs) => {
10
+ // Skip proxy nodes and embeddings for transfer size
11
+ const { embedding, fileEmbedding, ...rest } = attrs;
12
+ nodes.push({ id, graph: graphName, ...rest });
13
+ });
14
+ graph.forEachEdge((_edge, attrs, source, target) => {
15
+ edges.push({ source, target, graph: graphName, ...attrs });
16
+ });
17
+ return { nodes, edges };
18
+ }
19
+ const GRAPH_TO_PROP = {
20
+ docs: 'docGraph',
21
+ code: 'codeGraph',
22
+ knowledge: 'knowledgeGraph',
23
+ tasks: 'taskGraph',
24
+ files: 'fileIndexGraph',
25
+ skills: 'skillGraph',
26
+ };
27
+ const ALL_GRAPHS = ['docs', 'code', 'knowledge', 'tasks', 'files', 'skills'];
28
+ function createGraphRouter(canReadGraph) {
29
+ const router = (0, express_1.Router)({ mergeParams: true });
30
+ function getProject(req) {
31
+ return req.project;
32
+ }
33
+ router.get('/', (0, validation_1.validateQuery)(validation_1.graphExportSchema), (req, res, next) => {
34
+ try {
35
+ const p = getProject(req);
36
+ const scope = req.validatedQuery.scope;
37
+ const allNodes = [];
38
+ const allEdges = [];
39
+ const add = (g, name) => {
40
+ if (!g)
41
+ return;
42
+ const exp = exportGraph(g, name);
43
+ allNodes.push(...exp.nodes);
44
+ allEdges.push(...exp.edges);
45
+ };
46
+ if (scope === 'all') {
47
+ // Export only graphs the user can read
48
+ for (const gn of ALL_GRAPHS) {
49
+ if (canReadGraph && !canReadGraph(req, gn))
50
+ continue;
51
+ const prop = GRAPH_TO_PROP[gn];
52
+ add(p[prop], gn);
53
+ }
54
+ }
55
+ else {
56
+ // Specific graph — check access, 403 if denied
57
+ const gn = scope;
58
+ if (canReadGraph && !canReadGraph(req, gn)) {
59
+ return res.status(403).json({ error: 'Access denied' });
60
+ }
61
+ const prop = GRAPH_TO_PROP[gn];
62
+ add(p[prop], gn);
63
+ }
64
+ res.json({ nodes: allNodes, edges: allEdges });
65
+ }
66
+ catch (err) {
67
+ next(err);
68
+ }
69
+ });
70
+ return router;
71
+ }
@@ -0,0 +1,371 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.requireWriteAccess = requireWriteAccess;
7
+ exports.createRestApp = createRestApp;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const express_1 = __importDefault(require("express"));
11
+ const cors_1 = __importDefault(require("cors"));
12
+ const cookie_parser_1 = __importDefault(require("cookie-parser"));
13
+ const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
14
+ const multi_config_1 = require("../../lib/multi-config");
15
+ const access_1 = require("../../lib/access");
16
+ const jwt_1 = require("../../lib/jwt");
17
+ const knowledge_1 = require("../../api/rest/knowledge");
18
+ const tasks_1 = require("../../api/rest/tasks");
19
+ const skills_1 = require("../../api/rest/skills");
20
+ const docs_1 = require("../../api/rest/docs");
21
+ const code_1 = require("../../api/rest/code");
22
+ const files_1 = require("../../api/rest/files");
23
+ const graph_1 = require("../../api/rest/graph");
24
+ const tools_1 = require("../../api/rest/tools");
25
+ const embed_1 = require("../../api/rest/embed");
26
+ const team_1 = require("../../lib/team");
27
+ /**
28
+ * Express middleware: reject if accessLevel (set by requireGraphAccess) is not 'rw'.
29
+ * Use on POST/PUT/DELETE routes inside domain routers.
30
+ */
31
+ function requireWriteAccess(req, res, next) {
32
+ const level = req.accessLevel;
33
+ if (level && level !== 'rw') {
34
+ res.status(403).json({ error: 'Read-only access' });
35
+ return;
36
+ }
37
+ next();
38
+ }
39
+ /**
40
+ * Create an Express app with all REST routes mounted.
41
+ * Each route uses the ProjectManager to look up project-specific graphs.
42
+ */
43
+ function createRestApp(projectManager, options) {
44
+ const app = (0, express_1.default)();
45
+ const serverConfig = options?.serverConfig;
46
+ const users = options?.users ?? {};
47
+ const hasUsers = Object.keys(users).length > 0;
48
+ const corsOrigins = serverConfig?.corsOrigins;
49
+ app.use((0, cors_1.default)(corsOrigins?.length ? { origin: corsOrigins, credentials: true } : { credentials: true }));
50
+ app.use(express_1.default.json({ limit: '10mb' }));
51
+ app.use((0, cookie_parser_1.default)());
52
+ // Security headers
53
+ app.use((_req, res, next) => {
54
+ res.setHeader('X-Content-Type-Options', 'nosniff');
55
+ res.setHeader('X-Frame-Options', 'DENY');
56
+ next();
57
+ });
58
+ // --- Rate limiting ---
59
+ const rl = serverConfig?.rateLimit;
60
+ const rateLimitMsg = { error: 'Too many requests, please try again later' };
61
+ if (rl && rl.global > 0) {
62
+ app.use('/api/', (0, express_rate_limit_1.default)({ windowMs: 60_000, max: rl.global, standardHeaders: true, legacyHeaders: false, message: rateLimitMsg }));
63
+ }
64
+ if (rl && rl.search > 0) {
65
+ const searchLimiter = (0, express_rate_limit_1.default)({ windowMs: 60_000, max: rl.search, standardHeaders: true, legacyHeaders: false, message: rateLimitMsg });
66
+ app.use('/api/projects/:projectId/knowledge/notes/search', searchLimiter);
67
+ app.use('/api/projects/:projectId/tasks/search', searchLimiter);
68
+ app.use('/api/projects/:projectId/skills/search', searchLimiter);
69
+ app.use('/api/projects/:projectId/docs/search', searchLimiter);
70
+ app.use('/api/projects/:projectId/code/search', searchLimiter);
71
+ app.use('/api/projects/:projectId/files/search', searchLimiter);
72
+ app.use('/api/embed', searchLimiter);
73
+ }
74
+ if (rl && rl.auth > 0) {
75
+ app.use('/api/auth/login', (0, express_rate_limit_1.default)({ windowMs: 60_000, max: rl.auth, standardHeaders: true, legacyHeaders: false, message: rateLimitMsg }));
76
+ }
77
+ const jwtSecret = serverConfig?.jwtSecret;
78
+ const accessTokenTtl = serverConfig?.accessTokenTtl ?? '15m';
79
+ const refreshTokenTtl = serverConfig?.refreshTokenTtl ?? '7d';
80
+ // --- Auth endpoints (before auth middleware — always accessible) ---
81
+ // Auth status: check cookie JWT or Bearer apiKey
82
+ app.get('/api/auth/status', (req, res) => {
83
+ if (!hasUsers)
84
+ return res.json({ required: false, authenticated: false });
85
+ // 1. Cookie JWT
86
+ if (jwtSecret) {
87
+ const accessToken = (0, jwt_1.getAccessToken)(req);
88
+ if (accessToken) {
89
+ const payload = (0, jwt_1.verifyToken)(accessToken, jwtSecret);
90
+ if (payload?.type === 'access' && users[payload.userId]) {
91
+ const user = users[payload.userId];
92
+ return res.json({ required: true, authenticated: true, userId: payload.userId, name: user.name });
93
+ }
94
+ }
95
+ }
96
+ // 2. Bearer apiKey
97
+ const auth = req.headers.authorization;
98
+ if (auth?.startsWith('Bearer ')) {
99
+ const result = (0, access_1.resolveUserFromApiKey)(auth.slice(7), users);
100
+ if (result)
101
+ return res.json({ required: true, authenticated: true, userId: result.userId, name: result.user.name });
102
+ }
103
+ return res.json({ required: true, authenticated: false });
104
+ });
105
+ // Login: email + password → set JWT cookies
106
+ app.post('/api/auth/login', async (req, res) => {
107
+ if (!hasUsers || !jwtSecret) {
108
+ return res.status(400).json({ error: 'Authentication not configured' });
109
+ }
110
+ const { email, password } = req.body ?? {};
111
+ if (!email || !password) {
112
+ return res.status(400).json({ error: 'Email and password are required' });
113
+ }
114
+ const result = (0, jwt_1.resolveUserByEmail)(email, users);
115
+ if (!result || !result.user.passwordHash) {
116
+ return res.status(401).json({ error: 'Invalid credentials' });
117
+ }
118
+ const valid = await (0, jwt_1.verifyPassword)(password, result.user.passwordHash);
119
+ if (!valid) {
120
+ return res.status(401).json({ error: 'Invalid credentials' });
121
+ }
122
+ const accessToken = (0, jwt_1.signAccessToken)(result.userId, jwtSecret, accessTokenTtl);
123
+ const refreshToken = (0, jwt_1.signRefreshToken)(result.userId, jwtSecret, refreshTokenTtl);
124
+ (0, jwt_1.setAuthCookies)(res, accessToken, refreshToken, accessTokenTtl, refreshTokenTtl);
125
+ res.json({ userId: result.userId, name: result.user.name });
126
+ });
127
+ // Refresh: refresh cookie → new access cookie
128
+ app.post('/api/auth/refresh', (req, res) => {
129
+ if (!hasUsers || !jwtSecret) {
130
+ return res.status(400).json({ error: 'Authentication not configured' });
131
+ }
132
+ const refreshToken = (0, jwt_1.getRefreshToken)(req);
133
+ if (!refreshToken) {
134
+ return res.status(401).json({ error: 'No refresh token' });
135
+ }
136
+ const payload = (0, jwt_1.verifyToken)(refreshToken, jwtSecret);
137
+ if (!payload || payload.type !== 'refresh') {
138
+ return res.status(401).json({ error: 'Invalid refresh token' });
139
+ }
140
+ // Check user still exists
141
+ if (!users[payload.userId]) {
142
+ (0, jwt_1.clearAuthCookies)(res);
143
+ return res.status(401).json({ error: 'User no longer exists' });
144
+ }
145
+ const newAccessToken = (0, jwt_1.signAccessToken)(payload.userId, jwtSecret, accessTokenTtl);
146
+ const newRefreshToken = (0, jwt_1.signRefreshToken)(payload.userId, jwtSecret, refreshTokenTtl);
147
+ (0, jwt_1.setAuthCookies)(res, newAccessToken, newRefreshToken, accessTokenTtl, refreshTokenTtl);
148
+ res.json({ userId: payload.userId, name: users[payload.userId].name });
149
+ });
150
+ // Logout: clear cookies
151
+ app.post('/api/auth/logout', (_req, res) => {
152
+ (0, jwt_1.clearAuthCookies)(res);
153
+ res.json({ ok: true });
154
+ });
155
+ // --- Auth middleware: cookie JWT → Bearer apiKey → anonymous ---
156
+ if (hasUsers) {
157
+ app.use('/api/', (req, _res, next) => {
158
+ // 1. Cookie JWT (from UI login)
159
+ if (jwtSecret) {
160
+ const accessToken = (0, jwt_1.getAccessToken)(req);
161
+ if (accessToken) {
162
+ const payload = (0, jwt_1.verifyToken)(accessToken, jwtSecret);
163
+ if (payload?.type === 'access' && users[payload.userId]) {
164
+ req.userId = payload.userId;
165
+ req.user = users[payload.userId];
166
+ return next();
167
+ }
168
+ // Invalid/expired JWT cookie — don't reject, fall through to Bearer
169
+ }
170
+ }
171
+ // 2. Bearer apiKey (from MCP/API clients)
172
+ const auth = req.headers.authorization;
173
+ if (auth?.startsWith('Bearer ')) {
174
+ const apiKey = auth.slice(7);
175
+ const result = (0, access_1.resolveUserFromApiKey)(apiKey, users);
176
+ if (result) {
177
+ req.userId = result.userId;
178
+ req.user = result.user;
179
+ return next();
180
+ }
181
+ return _res.status(401).json({ error: 'Invalid API key' });
182
+ }
183
+ // 3. No auth = anonymous (uses defaultAccess)
184
+ next();
185
+ });
186
+ }
187
+ // Project resolution middleware — injects project instance into req
188
+ app.param('projectId', (req, _res, next, projectId) => {
189
+ const project = projectManager.getProject(projectId);
190
+ if (!project) {
191
+ return _res.status(404).json({ error: `Project "${projectId}" not found` });
192
+ }
193
+ req.project = project;
194
+ next();
195
+ });
196
+ // List projects
197
+ app.get('/api/projects', (_req, res) => {
198
+ const userId = _req.userId;
199
+ const projects = projectManager.listProjects().map(id => {
200
+ const p = projectManager.getProject(id);
201
+ const gc = p.config.graphConfigs;
202
+ const ws = p.workspaceId ? projectManager.getWorkspace(p.workspaceId) : undefined;
203
+ // Per-graph info: enabled + access level for current user
204
+ const graphs = {};
205
+ for (const gn of multi_config_1.GRAPH_NAMES) {
206
+ const access = serverConfig
207
+ ? (0, access_1.resolveAccess)(userId, gn, p.config, serverConfig, ws?.config)
208
+ : 'rw';
209
+ graphs[gn] = { enabled: gc[gn].enabled, access: gc[gn].enabled ? access : null };
210
+ }
211
+ return {
212
+ id,
213
+ projectDir: p.config.projectDir,
214
+ workspaceId: p.workspaceId ?? null,
215
+ graphs,
216
+ stats: {
217
+ docs: p.docGraph ? p.docGraph.order : 0,
218
+ code: p.codeGraph ? p.codeGraph.order : 0,
219
+ knowledge: p.knowledgeGraph ? p.knowledgeGraph.order : 0,
220
+ files: p.fileIndexGraph ? p.fileIndexGraph.order : 0,
221
+ tasks: p.taskGraph ? p.taskGraph.order : 0,
222
+ skills: p.skillGraph ? p.skillGraph.order : 0,
223
+ },
224
+ };
225
+ });
226
+ res.json({ results: projects });
227
+ });
228
+ // List workspaces
229
+ app.get('/api/workspaces', (_req, res) => {
230
+ const workspaces = projectManager.listWorkspaces().map(id => {
231
+ const ws = projectManager.getWorkspace(id);
232
+ return {
233
+ id,
234
+ projects: ws.config.projects,
235
+ };
236
+ });
237
+ res.json({ results: workspaces });
238
+ });
239
+ // Project stats
240
+ app.get('/api/projects/:projectId/stats', (req, res) => {
241
+ const p = req.project;
242
+ res.json({
243
+ docs: p.docGraph ? { nodes: p.docGraph.order, edges: p.docGraph.size } : null,
244
+ code: p.codeGraph ? { nodes: p.codeGraph.order, edges: p.codeGraph.size } : null,
245
+ knowledge: p.knowledgeGraph ? { nodes: p.knowledgeGraph.order, edges: p.knowledgeGraph.size } : null,
246
+ fileIndex: p.fileIndexGraph ? { nodes: p.fileIndexGraph.order, edges: p.fileIndexGraph.size } : null,
247
+ tasks: p.taskGraph ? { nodes: p.taskGraph.order, edges: p.taskGraph.size } : null,
248
+ skills: p.skillGraph ? { nodes: p.skillGraph.order, edges: p.skillGraph.size } : null,
249
+ });
250
+ });
251
+ // Team members (workspace: shared .team/ in mirrorDir; standalone: .team/ in projectDir)
252
+ // Requires at least read access to any graph in the project
253
+ app.get('/api/projects/:projectId/team', (req, res) => {
254
+ if (serverConfig && hasUsers) {
255
+ const userId = req.userId;
256
+ if (!userId)
257
+ return res.status(401).json({ error: 'Authentication required' });
258
+ const p = req.project;
259
+ const ws = p.workspaceId ? projectManager.getWorkspace(p.workspaceId) : undefined;
260
+ const hasAnyAccess = multi_config_1.GRAPH_NAMES.some(gn => (0, access_1.canRead)((0, access_1.resolveAccess)(userId, gn, p.config, serverConfig, ws?.config)));
261
+ if (!hasAnyAccess)
262
+ return res.status(403).json({ error: 'Access denied' });
263
+ }
264
+ const p = req.project;
265
+ const ws = p.workspaceId ? projectManager.getWorkspace(p.workspaceId) : undefined;
266
+ const baseDir = ws ? ws.config.mirrorDir : p.config.projectDir;
267
+ const members = (0, team_1.scanTeamDir)(path_1.default.join(baseDir, '.team'));
268
+ res.json({ results: members });
269
+ });
270
+ // Middleware: require a specific manager to be enabled, or return 404
271
+ function requireManager(managerKey) {
272
+ return (req, _res, next) => {
273
+ const p = req.project;
274
+ if (!p || !p[managerKey]) {
275
+ return _res.status(404).json({ error: 'This graph is disabled for this project' });
276
+ }
277
+ next();
278
+ };
279
+ }
280
+ // Middleware: check access level for a graph (read or read-write)
281
+ function requireGraphAccess(graphName, level) {
282
+ return (req, _res, next) => {
283
+ if (!serverConfig)
284
+ return next(); // no config = no auth enforcement
285
+ const p = req.project;
286
+ if (!p)
287
+ return next();
288
+ const userId = req.userId;
289
+ const ws = p.workspaceId ? projectManager.getWorkspace(p.workspaceId) : undefined;
290
+ const access = (0, access_1.resolveAccess)(userId, graphName, p.config, serverConfig, ws?.config);
291
+ if (!(0, access_1.canRead)(access)) {
292
+ return _res.status(403).json({ error: 'Access denied' });
293
+ }
294
+ if (level === 'rw' && !(0, access_1.canWrite)(access)) {
295
+ return _res.status(403).json({ error: 'Read-only access' });
296
+ }
297
+ req.accessLevel = access;
298
+ next();
299
+ };
300
+ }
301
+ // Helper: combine requireManager + read access check
302
+ function graphMiddleware(managerKey, graphName) {
303
+ return [requireManager(managerKey), requireGraphAccess(graphName, 'r')];
304
+ }
305
+ // Mount domain routers (gated by manager existence + read access)
306
+ // Mutation endpoints (POST/PUT/DELETE) inside routers check req.accessLevel for write access
307
+ app.use('/api/projects/:projectId/knowledge', ...graphMiddleware('knowledgeManager', 'knowledge'), (0, knowledge_1.createKnowledgeRouter)());
308
+ app.use('/api/projects/:projectId/tasks', ...graphMiddleware('taskManager', 'tasks'), (0, tasks_1.createTasksRouter)());
309
+ app.use('/api/projects/:projectId/skills', ...graphMiddleware('skillManager', 'skills'), (0, skills_1.createSkillsRouter)());
310
+ app.use('/api/projects/:projectId/docs', ...graphMiddleware('docManager', 'docs'), (0, docs_1.createDocsRouter)());
311
+ app.use('/api/projects/:projectId/code', ...graphMiddleware('codeManager', 'code'), (0, code_1.createCodeRouter)());
312
+ app.use('/api/projects/:projectId/files', ...graphMiddleware('fileIndexManager', 'files'), (0, files_1.createFilesRouter)());
313
+ app.use('/api/projects/:projectId/graph', (0, graph_1.createGraphRouter)((req, graphName) => {
314
+ if (!serverConfig)
315
+ return true; // no config = no auth enforcement
316
+ const p = req.project;
317
+ if (!p)
318
+ return true;
319
+ const userId = req.userId;
320
+ const ws = p.workspaceId ? projectManager.getWorkspace(p.workspaceId) : undefined;
321
+ const access = (0, access_1.resolveAccess)(userId, graphName, p.config, serverConfig, ws?.config);
322
+ return (0, access_1.canRead)(access);
323
+ }));
324
+ app.use('/api/projects/:projectId/tools', (0, tools_1.createToolsRouter)(projectManager, (req, graphName, level) => {
325
+ if (!serverConfig)
326
+ return true;
327
+ const p = req.project;
328
+ if (!p)
329
+ return true;
330
+ const userId = req.userId;
331
+ const ws = p.workspaceId ? projectManager.getWorkspace(p.workspaceId) : undefined;
332
+ const access = (0, access_1.resolveAccess)(userId, graphName, p.config, serverConfig, ws?.config);
333
+ if (level === 'rw')
334
+ return (0, access_1.canWrite)(access);
335
+ return (0, access_1.canRead)(access);
336
+ }));
337
+ // Embedding API (optional, gated by server.embeddingApi.enabled)
338
+ if (serverConfig?.embeddingApi?.enabled && options?.embeddingApiModelName) {
339
+ app.use('/api/embed', (0, embed_1.createEmbedRouter)(serverConfig.embeddingApi, options.embeddingApiModelName));
340
+ }
341
+ // Serve UI static files — check dist/ui/ (npm package) then ui/dist/ (dev)
342
+ const uiDistPkg = path_1.default.resolve(__dirname, '../../ui');
343
+ const uiDistDev = path_1.default.resolve(__dirname, '../../../ui/dist');
344
+ const uiDist = fs_1.default.existsSync(uiDistPkg) ? uiDistPkg : uiDistDev;
345
+ app.use(express_1.default.static(uiDist));
346
+ // SPA fallback: serve index.html for non-API routes
347
+ app.get('/{*splat}', (_req, res, next) => {
348
+ if (_req.path.startsWith('/api/'))
349
+ return next();
350
+ res.sendFile(path_1.default.join(uiDist, 'index.html'), (err) => {
351
+ if (err)
352
+ next();
353
+ });
354
+ });
355
+ // Error handler
356
+ app.use((err, _req, res, _next) => {
357
+ if (err.name === 'ZodError') {
358
+ const fields = (err.issues ?? []).map((i) => ({
359
+ path: i.path?.join('.'),
360
+ message: i.message,
361
+ }));
362
+ return res.status(400).json({ error: 'Validation error', fields });
363
+ }
364
+ if (err.type === 'entity.parse.failed' || (err instanceof SyntaxError && 'body' in err)) {
365
+ return res.status(400).json({ error: 'Invalid JSON' });
366
+ }
367
+ process.stderr.write(`[rest] Error: ${err.stack || err}\n`);
368
+ res.status(500).json({ error: 'Internal server error' });
369
+ });
370
+ return app;
371
+ }