@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.
- package/LICENSE +15 -0
- package/README.md +216 -0
- package/dist/api/index.js +473 -0
- package/dist/api/rest/code.js +78 -0
- package/dist/api/rest/docs.js +80 -0
- package/dist/api/rest/embed.js +47 -0
- package/dist/api/rest/files.js +64 -0
- package/dist/api/rest/graph.js +71 -0
- package/dist/api/rest/index.js +371 -0
- package/dist/api/rest/knowledge.js +239 -0
- package/dist/api/rest/skills.js +285 -0
- package/dist/api/rest/tasks.js +273 -0
- package/dist/api/rest/tools.js +157 -0
- package/dist/api/rest/validation.js +196 -0
- package/dist/api/rest/websocket.js +71 -0
- package/dist/api/tools/code/get-file-symbols.js +30 -0
- package/dist/api/tools/code/get-symbol.js +22 -0
- package/dist/api/tools/code/list-files.js +18 -0
- package/dist/api/tools/code/search-code.js +27 -0
- package/dist/api/tools/code/search-files.js +22 -0
- package/dist/api/tools/context/get-context.js +19 -0
- package/dist/api/tools/docs/cross-references.js +76 -0
- package/dist/api/tools/docs/explain-symbol.js +55 -0
- package/dist/api/tools/docs/find-examples.js +52 -0
- package/dist/api/tools/docs/get-node.js +24 -0
- package/dist/api/tools/docs/get-toc.js +22 -0
- package/dist/api/tools/docs/list-snippets.js +46 -0
- package/dist/api/tools/docs/list-topics.js +18 -0
- package/dist/api/tools/docs/search-files.js +22 -0
- package/dist/api/tools/docs/search-snippets.js +43 -0
- package/dist/api/tools/docs/search.js +27 -0
- package/dist/api/tools/file-index/get-file-info.js +21 -0
- package/dist/api/tools/file-index/list-all-files.js +28 -0
- package/dist/api/tools/file-index/search-all-files.js +24 -0
- package/dist/api/tools/knowledge/add-attachment.js +31 -0
- package/dist/api/tools/knowledge/create-note.js +20 -0
- package/dist/api/tools/knowledge/create-relation.js +29 -0
- package/dist/api/tools/knowledge/delete-note.js +19 -0
- package/dist/api/tools/knowledge/delete-relation.js +23 -0
- package/dist/api/tools/knowledge/find-linked-notes.js +25 -0
- package/dist/api/tools/knowledge/get-note.js +20 -0
- package/dist/api/tools/knowledge/list-notes.js +18 -0
- package/dist/api/tools/knowledge/list-relations.js +17 -0
- package/dist/api/tools/knowledge/remove-attachment.js +19 -0
- package/dist/api/tools/knowledge/search-notes.js +25 -0
- package/dist/api/tools/knowledge/update-note.js +34 -0
- package/dist/api/tools/skills/add-attachment.js +31 -0
- package/dist/api/tools/skills/bump-usage.js +19 -0
- package/dist/api/tools/skills/create-skill-link.js +25 -0
- package/dist/api/tools/skills/create-skill.js +26 -0
- package/dist/api/tools/skills/delete-skill-link.js +23 -0
- package/dist/api/tools/skills/delete-skill.js +20 -0
- package/dist/api/tools/skills/find-linked-skills.js +25 -0
- package/dist/api/tools/skills/get-skill.js +21 -0
- package/dist/api/tools/skills/link-skill.js +23 -0
- package/dist/api/tools/skills/list-skills.js +20 -0
- package/dist/api/tools/skills/recall-skills.js +18 -0
- package/dist/api/tools/skills/remove-attachment.js +19 -0
- package/dist/api/tools/skills/search-skills.js +25 -0
- package/dist/api/tools/skills/update-skill.js +58 -0
- package/dist/api/tools/tasks/add-attachment.js +31 -0
- package/dist/api/tools/tasks/create-task-link.js +25 -0
- package/dist/api/tools/tasks/create-task.js +26 -0
- package/dist/api/tools/tasks/delete-task-link.js +23 -0
- package/dist/api/tools/tasks/delete-task.js +20 -0
- package/dist/api/tools/tasks/find-linked-tasks.js +25 -0
- package/dist/api/tools/tasks/get-task.js +20 -0
- package/dist/api/tools/tasks/link-task.js +23 -0
- package/dist/api/tools/tasks/list-tasks.js +25 -0
- package/dist/api/tools/tasks/move-task.js +38 -0
- package/dist/api/tools/tasks/remove-attachment.js +19 -0
- package/dist/api/tools/tasks/search-tasks.js +25 -0
- package/dist/api/tools/tasks/update-task.js +58 -0
- package/dist/cli/index.js +617 -0
- package/dist/cli/indexer.js +275 -0
- package/dist/graphs/attachment-types.js +74 -0
- package/dist/graphs/code-types.js +10 -0
- package/dist/graphs/code.js +204 -0
- package/dist/graphs/docs.js +231 -0
- package/dist/graphs/file-index-types.js +10 -0
- package/dist/graphs/file-index.js +310 -0
- package/dist/graphs/file-lang.js +119 -0
- package/dist/graphs/knowledge-types.js +32 -0
- package/dist/graphs/knowledge.js +768 -0
- package/dist/graphs/manager-types.js +87 -0
- package/dist/graphs/skill-types.js +10 -0
- package/dist/graphs/skill.js +1016 -0
- package/dist/graphs/task-types.js +17 -0
- package/dist/graphs/task.js +972 -0
- package/dist/lib/access.js +67 -0
- package/dist/lib/embedder.js +235 -0
- package/dist/lib/events-log.js +401 -0
- package/dist/lib/file-import.js +328 -0
- package/dist/lib/file-mirror.js +461 -0
- package/dist/lib/frontmatter.js +17 -0
- package/dist/lib/jwt.js +146 -0
- package/dist/lib/mirror-watcher.js +637 -0
- package/dist/lib/multi-config.js +393 -0
- package/dist/lib/parsers/code.js +214 -0
- package/dist/lib/parsers/codeblock.js +33 -0
- package/dist/lib/parsers/docs.js +199 -0
- package/dist/lib/parsers/languages/index.js +15 -0
- package/dist/lib/parsers/languages/registry.js +68 -0
- package/dist/lib/parsers/languages/types.js +2 -0
- package/dist/lib/parsers/languages/typescript.js +306 -0
- package/dist/lib/project-manager.js +458 -0
- package/dist/lib/promise-queue.js +22 -0
- package/dist/lib/search/bm25.js +167 -0
- package/dist/lib/search/code.js +103 -0
- package/dist/lib/search/docs.js +106 -0
- package/dist/lib/search/file-index.js +31 -0
- package/dist/lib/search/files.js +61 -0
- package/dist/lib/search/knowledge.js +101 -0
- package/dist/lib/search/skills.js +104 -0
- package/dist/lib/search/tasks.js +103 -0
- package/dist/lib/team.js +89 -0
- package/dist/lib/watcher.js +67 -0
- package/dist/ui/assets/index-D6oxrVF7.js +1759 -0
- package/dist/ui/assets/index-kKd4mVrh.css +1 -0
- package/dist/ui/favicon.svg +1 -0
- package/dist/ui/icons.svg +24 -0
- package/dist/ui/index.html +14 -0
- 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
|
+
}
|