@axhub/genie 0.2.9 → 0.2.11
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/api-docs.html +2 -2
- package/dist/assets/App-VH1wNUHs.js +259 -0
- package/dist/assets/{ReviewApp-C9K--AQE.js → ReviewApp-D_9EN4TM.js} +1 -1
- package/dist/assets/{_basePickBy-DR_8uFCo.js → _basePickBy-BDnj7-0Z.js} +1 -1
- package/dist/assets/{_baseUniq-D0njlQ_7.js → _baseUniq-Bl0JKOyl.js} +1 -1
- package/dist/assets/{arc-CKlr_Rec.js → arc-DY-4Kev3.js} +1 -1
- package/dist/assets/{architectureDiagram-2XIMDMQ5-BmO_uLUH.js → architectureDiagram-2XIMDMQ5-qw7crNVd.js} +1 -1
- package/dist/assets/{blockDiagram-WCTKOSBZ-DhAeO-56.js → blockDiagram-WCTKOSBZ-B9xg7ep3.js} +1 -1
- package/dist/assets/{c4Diagram-IC4MRINW-C67kFoXx.js → c4Diagram-IC4MRINW-H9xp3ytb.js} +1 -1
- package/dist/assets/channel-CyNUnRfc.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-mLLagvJi.js → chunk-4BX2VUAB-B3EVDUxI.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-Lx-hOjlM.js → chunk-55IACEB6-CGv945ef.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-Bt-XmVUV.js → chunk-FMBD7UC4-uAT4CKWM.js} +1 -1
- package/dist/assets/{chunk-JSJVCQXG-Cya6gaDV.js → chunk-JSJVCQXG-Cbvlpkf7.js} +1 -1
- package/dist/assets/{chunk-KX2RTZJC-Bd7Ig6tF.js → chunk-KX2RTZJC-CcqIuGat.js} +1 -1
- package/dist/assets/{chunk-NQ4KR5QH-5UAE0Vg-.js → chunk-NQ4KR5QH-CgrcsRuX.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-BAxZ8m7w.js → chunk-QZHKN3VN-Cx0APOoV.js} +1 -1
- package/dist/assets/{chunk-WL4C6EOR-DjDPvUUP.js → chunk-WL4C6EOR-BbZirvBk.js} +1 -1
- package/dist/assets/classDiagram-VBA2DB6C-DxBtyz2A.js +1 -0
- package/dist/assets/classDiagram-v2-RAHNMMFH-DxBtyz2A.js +1 -0
- package/dist/assets/clone-C341l3d0.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-D-60XrkJ.js → cose-bilkent-S5V4N54A-CrvmGFLD.js} +1 -1
- package/dist/assets/{dagre-KLK3FWXG-bqu3ZS4K.js → dagre-KLK3FWXG-C-W6VPjS.js} +1 -1
- package/dist/assets/{diagram-E7M64L7V-BueeqoYm.js → diagram-E7M64L7V-IP2q3bL0.js} +1 -1
- package/dist/assets/{diagram-IFDJBPK2-D4fDv2E7.js → diagram-IFDJBPK2-CQaL-XyV.js} +1 -1
- package/dist/assets/{diagram-P4PSJMXO-WqipY3fN.js → diagram-P4PSJMXO-BxBLThfv.js} +1 -1
- package/dist/assets/{erDiagram-INFDFZHY-D0oVnO-x.js → erDiagram-INFDFZHY-Dyl7bJTt.js} +1 -1
- package/dist/assets/{flowDiagram-PKNHOUZH-DzbGyxrr.js → flowDiagram-PKNHOUZH-B7NFMgFK.js} +1 -1
- package/dist/assets/{ganttDiagram-A5KZAMGK-BwhbbgCP.js → ganttDiagram-A5KZAMGK-hReWSDu2.js} +1 -1
- package/dist/assets/{gitGraphDiagram-K3NZZRJ6-DZgAh_KM.js → gitGraphDiagram-K3NZZRJ6-gVgcr0ST.js} +1 -1
- package/dist/assets/{graph-DzKos-N0.js → graph-DNDiJhTn.js} +1 -1
- package/dist/assets/{highlighted-body-TPN3WLV5-CKDMgz3X.js → highlighted-body-TPN3WLV5-DclLmTou.js} +1 -1
- package/dist/assets/index-DBkz_W_P.css +1 -0
- package/dist/assets/index-DdRyoXKh.js +2 -0
- package/dist/assets/{infoDiagram-LFFYTUFH-BFicZbTf.js → infoDiagram-LFFYTUFH-CqQOOzDA.js} +1 -1
- package/dist/assets/{ishikawaDiagram-PHBUUO56-CtihxDxl.js → ishikawaDiagram-PHBUUO56-CZ0iLiHg.js} +1 -1
- package/dist/assets/{journeyDiagram-4ABVD52K-Du00J8_d.js → journeyDiagram-4ABVD52K-DdfYKfNh.js} +1 -1
- package/dist/assets/{kanban-definition-K7BYSVSG-BJi9S0iQ.js → kanban-definition-K7BYSVSG-C5Vf32u6.js} +1 -1
- package/dist/assets/{layout-B80Sityu.js → layout-rvTEu2KS.js} +1 -1
- package/dist/assets/{linear-sRQLOf5H.js → linear-CD9SiYze.js} +1 -1
- package/dist/assets/{mermaid-O7DHMXV3-CBuVs4eJ.js → mermaid-O7DHMXV3-OZ8qWWwa.js} +167 -157
- package/dist/assets/{mindmap-definition-YRQLILUH-C5IL_xi-.js → mindmap-definition-YRQLILUH-CQxrLNVc.js} +1 -1
- package/dist/assets/{pieDiagram-SKSYHLDU-CeTwlJ8z.js → pieDiagram-SKSYHLDU-XgAUByWg.js} +1 -1
- package/dist/assets/{quadrantDiagram-337W2JSQ-COfUcLWt.js → quadrantDiagram-337W2JSQ-CH16ls7G.js} +1 -1
- package/dist/assets/{requirementDiagram-Z7DCOOCP-DSb-CJ5B.js → requirementDiagram-Z7DCOOCP-B_kQO06L.js} +1 -1
- package/dist/assets/{sankeyDiagram-WA2Y5GQK-8jtuVb45.js → sankeyDiagram-WA2Y5GQK-ofe78CyS.js} +1 -1
- package/dist/assets/{sequenceDiagram-2WXFIKYE-C2VpkMwA.js → sequenceDiagram-2WXFIKYE-Ckbxwny6.js} +1 -1
- package/dist/assets/{stateDiagram-RAJIS63D-fmwMqxxc.js → stateDiagram-RAJIS63D-DNtzCk14.js} +1 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-B3VPhiE1.js +1 -0
- package/dist/assets/{timeline-definition-YZTLITO2-Dx1hP5lg.js → timeline-definition-YZTLITO2-zT6CklKt.js} +1 -1
- package/dist/assets/{treemap-KZPCXAKY-CkLOdYCZ.js → treemap-KZPCXAKY-y0U2c3xG.js} +1 -1
- package/dist/assets/vendor-codemirror-CMHSJ_9p.js +9 -0
- package/dist/assets/{vennDiagram-LZ73GAT5-D6KWcnln.js → vennDiagram-LZ73GAT5-xKj3SjYG.js} +1 -1
- package/dist/assets/{xychartDiagram-JWTSCODW-6fh6qmzN.js → xychartDiagram-JWTSCODW-Da_qyEoX.js} +1 -1
- package/dist/index.html +3 -3
- package/package.json +6 -5
- package/server/acp-runtime/client.js +120 -14
- package/server/acp-runtime/index.js +54 -0
- package/server/acp-runtime/registry.js +2 -2
- package/server/acp-runtime/session-store.js +75 -1
- package/server/cli.js +32 -8
- package/server/database/db.js +20 -0
- package/server/external-agent/ws.js +477 -24
- package/server/index.js +89 -147
- package/server/lan-access/core.js +79 -0
- package/server/lan-access/state.js +102 -0
- package/server/middleware/auth.js +57 -14
- package/server/projects.js +442 -535
- package/server/routes/auth.js +24 -4
- package/server/routes/cli-auth.js +21 -25
- package/server/routes/codex.js +84 -298
- package/server/routes/commands.js +335 -407
- package/server/routes/lan-access.js +231 -0
- package/server/routes/projects.js +154 -158
- package/server/routes/session-core.js +13 -7
- package/server/routes/settings.js +113 -99
- package/server/session-core/eventStore.js +15 -2
- package/server/session-core/providerAdapters.js +28 -28
- package/server/session-core/sessionListMerge.js +47 -0
- package/shared/conversationEvents.js +96 -1
- package/shared/modelConstants.js +79 -99
- package/dist/assets/App-GBcTeeUS.js +0 -460
- package/dist/assets/channel-V3MBjKys.js +0 -1
- package/dist/assets/classDiagram-VBA2DB6C-C790yYiY.js +0 -1
- package/dist/assets/classDiagram-v2-RAHNMMFH-C790yYiY.js +0 -1
- package/dist/assets/clone-BbMGfZwt.js +0 -1
- package/dist/assets/index-DiQlHzGj.js +0 -2
- package/dist/assets/index-Drat2nB9.css +0 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-9GGXVWrR.js +0 -1
- package/dist/assets/vendor-codemirror-BxPY6emf.js +0 -39
- package/server/routes/git.js +0 -1110
- package/server/routes/mcp-utils.js +0 -48
- package/server/routes/mcp.js +0 -536
- package/server/routes/taskmaster.js +0 -1963
- package/server/utils/mcp-detector.js +0 -198
- package/server/utils/taskmaster-websocket.js +0 -129
package/server/index.js
CHANGED
|
@@ -96,7 +96,7 @@ async function getPty() {
|
|
|
96
96
|
import fetch from 'node-fetch';
|
|
97
97
|
import mime from 'mime-types';
|
|
98
98
|
|
|
99
|
-
import { getProjects, getProjectsList, getProjectDetails, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache, clearProviderSessionLookupCaches, getGeminiSessionMessages } from './projects.js';
|
|
99
|
+
import { getProjects, getProjectsList, getProjectDetails, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache, clearProviderSessionLookupCaches, getGeminiSessionMessages, deleteGeminiSession } from './projects.js';
|
|
100
100
|
import {
|
|
101
101
|
executeAgentPrompt,
|
|
102
102
|
getActiveAgentSessions,
|
|
@@ -104,11 +104,7 @@ import {
|
|
|
104
104
|
resolveAgentPermission,
|
|
105
105
|
setAgentSessionMode
|
|
106
106
|
} from './acp-runtime/index.js';
|
|
107
|
-
import gitRoutes from './routes/git.js';
|
|
108
107
|
import authRoutes from './routes/auth.js';
|
|
109
|
-
import mcpRoutes from './routes/mcp.js';
|
|
110
|
-
import taskmasterRoutes from './routes/taskmaster.js';
|
|
111
|
-
import mcpUtilsRoutes from './routes/mcp-utils.js';
|
|
112
108
|
import commandsRoutes from './routes/commands.js';
|
|
113
109
|
import settingsRoutes from './routes/settings.js';
|
|
114
110
|
import channelsRoutes from './routes/channels.js';
|
|
@@ -127,13 +123,14 @@ import {
|
|
|
127
123
|
} from './utils/workspaceRoots.js';
|
|
128
124
|
import cliAuthRoutes, { detectProviderInstallationStatus } from './routes/cli-auth.js';
|
|
129
125
|
import ccConnectRoutes from './routes/cc-connect.js';
|
|
126
|
+
import { lanAccessApiRoutes, lanAccessPageRoutes } from './routes/lan-access.js';
|
|
130
127
|
import userRoutes from './routes/user.js';
|
|
131
128
|
import codexRoutes from './routes/codex.js';
|
|
132
129
|
import opencodeRoutes from './routes/opencode.js';
|
|
133
130
|
import sessionCoreRoutes from './routes/session-core.js';
|
|
134
131
|
import { parseCodexTokenCountInfo } from './utils/codexTokenUsage.js';
|
|
135
132
|
import { getConfiguredDefaultProjectPath, resolveWorkingDirectory } from './utils/defaultWorkingDirectory.js';
|
|
136
|
-
import { initializeDatabase } from './database/db.js';
|
|
133
|
+
import { initializeDatabase, userDb } from './database/db.js';
|
|
137
134
|
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
|
138
135
|
import { IS_PLATFORM } from './constants/config.js';
|
|
139
136
|
import { getChannelManager } from './channels/index.js';
|
|
@@ -423,7 +420,7 @@ const wss = new WebSocketServer({
|
|
|
423
420
|
|
|
424
421
|
// Platform mode: always allow internal UI WebSocket connections.
|
|
425
422
|
if (IS_PLATFORM) {
|
|
426
|
-
const user = authenticateWebSocket(null); // Will return first user
|
|
423
|
+
const user = authenticateWebSocket(null, info.req); // Will return first user
|
|
427
424
|
if (!user) {
|
|
428
425
|
console.log('[WARN] Platform mode: No user found in database');
|
|
429
426
|
done(false, 500, 'Platform mode: No user found in database');
|
|
@@ -438,7 +435,7 @@ const wss = new WebSocketServer({
|
|
|
438
435
|
const token = url.searchParams.get('token') ||
|
|
439
436
|
info.req.headers.authorization?.split(' ')[1];
|
|
440
437
|
|
|
441
|
-
const user = authenticateWebSocket(token);
|
|
438
|
+
const user = authenticateWebSocket(token, info.req);
|
|
442
439
|
if (!user) {
|
|
443
440
|
console.log('[WARN] WebSocket authentication failed');
|
|
444
441
|
done(false, 401, 'Unauthorized');
|
|
@@ -497,22 +494,12 @@ app.use('/api', validateApiKey);
|
|
|
497
494
|
|
|
498
495
|
// Authentication routes (public)
|
|
499
496
|
app.use('/api/auth', authRoutes);
|
|
497
|
+
app.use('/api/lan-access', lanAccessApiRoutes);
|
|
498
|
+
app.use('/lan-access', lanAccessPageRoutes);
|
|
500
499
|
|
|
501
500
|
// Projects API Routes (protected)
|
|
502
501
|
app.use('/api/projects', authenticateToken, projectsRoutes);
|
|
503
502
|
|
|
504
|
-
// Git API Routes (protected)
|
|
505
|
-
app.use('/api/git', authenticateToken, gitRoutes);
|
|
506
|
-
|
|
507
|
-
// MCP API Routes (protected)
|
|
508
|
-
app.use('/api/mcp', authenticateToken, mcpRoutes);
|
|
509
|
-
|
|
510
|
-
// TaskMaster API Routes (protected)
|
|
511
|
-
app.use('/api/taskmaster', authenticateToken, taskmasterRoutes);
|
|
512
|
-
|
|
513
|
-
// MCP utilities
|
|
514
|
-
app.use('/api/mcp-utils', authenticateToken, mcpUtilsRoutes);
|
|
515
|
-
|
|
516
503
|
// Commands API Routes (protected)
|
|
517
504
|
app.use('/api/commands', authenticateToken, commandsRoutes);
|
|
518
505
|
|
|
@@ -732,6 +719,16 @@ app.get('/api/gemini/sessions/:sessionId/messages', authenticateToken, async (re
|
|
|
732
719
|
}
|
|
733
720
|
});
|
|
734
721
|
|
|
722
|
+
app.delete('/api/gemini/sessions/:sessionId', authenticateToken, async (req, res) => {
|
|
723
|
+
try {
|
|
724
|
+
const { sessionId } = req.params;
|
|
725
|
+
await deleteGeminiSession(sessionId);
|
|
726
|
+
res.json({ success: true });
|
|
727
|
+
} catch (error) {
|
|
728
|
+
res.status(500).json({ error: error.message });
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
|
|
735
732
|
// Rename project endpoint
|
|
736
733
|
app.put('/api/projects/:projectName/rename', authenticateToken, async (req, res) => {
|
|
737
734
|
try {
|
|
@@ -976,6 +973,65 @@ app.post('/api/create-folder', authenticateToken, async (req, res) => {
|
|
|
976
973
|
}
|
|
977
974
|
});
|
|
978
975
|
|
|
976
|
+
function isWithinProjectRoot(projectRoot, candidatePath) {
|
|
977
|
+
const relativePath = path.relative(projectRoot, candidatePath);
|
|
978
|
+
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
async function getResolvedProjectRoot(projectName) {
|
|
982
|
+
const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
|
|
983
|
+
if (!projectRoot) {
|
|
984
|
+
const error = new Error('Project not found');
|
|
985
|
+
error.statusCode = 404;
|
|
986
|
+
throw error;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const resolvedRoot = path.resolve(projectRoot);
|
|
990
|
+
await fsPromises.access(resolvedRoot);
|
|
991
|
+
return resolvedRoot;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
async function resolveProjectFilePath(projectName, inputPath) {
|
|
995
|
+
const requestedPath = typeof inputPath === 'string' ? inputPath.trim() : '';
|
|
996
|
+
if (!requestedPath) {
|
|
997
|
+
const error = new Error('Invalid file path');
|
|
998
|
+
error.statusCode = 400;
|
|
999
|
+
throw error;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const projectRoot = await getResolvedProjectRoot(projectName);
|
|
1003
|
+
const absoluteTarget = path.isAbsolute(requestedPath)
|
|
1004
|
+
? path.resolve(requestedPath)
|
|
1005
|
+
: path.resolve(projectRoot, requestedPath);
|
|
1006
|
+
|
|
1007
|
+
if (!isWithinProjectRoot(projectRoot, absoluteTarget)) {
|
|
1008
|
+
const error = new Error('Path must be under project root');
|
|
1009
|
+
error.statusCode = 403;
|
|
1010
|
+
throw error;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
return {
|
|
1014
|
+
projectRoot,
|
|
1015
|
+
targetPath: absoluteTarget
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function sendProjectFileError(res, error, missingLabel = 'File not found') {
|
|
1020
|
+
if (error.statusCode) {
|
|
1021
|
+
return res.status(error.statusCode).json({ error: error.message });
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
if (error.code === 'ENOENT') {
|
|
1025
|
+
return res.status(404).json({ error: missingLabel });
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
if (error.code === 'EACCES') {
|
|
1029
|
+
return res.status(403).json({ error: 'Permission denied' });
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
return res.status(500).json({ error: error.message });
|
|
1033
|
+
}
|
|
1034
|
+
|
|
979
1035
|
// Read file content endpoint
|
|
980
1036
|
app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) => {
|
|
981
1037
|
try {
|
|
@@ -984,36 +1040,12 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) =
|
|
|
984
1040
|
|
|
985
1041
|
console.log('[DEBUG] File read request:', projectName, filePath);
|
|
986
1042
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
|
|
993
|
-
if (!projectRoot) {
|
|
994
|
-
return res.status(404).json({ error: 'Project not found' });
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
// Handle both absolute and relative paths
|
|
998
|
-
const resolved = path.isAbsolute(filePath)
|
|
999
|
-
? path.resolve(filePath)
|
|
1000
|
-
: path.resolve(projectRoot, filePath);
|
|
1001
|
-
const normalizedRoot = path.resolve(projectRoot) + path.sep;
|
|
1002
|
-
if (!resolved.startsWith(normalizedRoot)) {
|
|
1003
|
-
return res.status(403).json({ error: 'Path must be under project root' });
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
const content = await fsPromises.readFile(resolved, 'utf8');
|
|
1007
|
-
res.json({ content, path: resolved });
|
|
1043
|
+
const { targetPath } = await resolveProjectFilePath(projectName, filePath);
|
|
1044
|
+
const content = await fsPromises.readFile(targetPath, 'utf8');
|
|
1045
|
+
res.json({ content, path: targetPath });
|
|
1008
1046
|
} catch (error) {
|
|
1009
1047
|
console.error('Error reading file:', error);
|
|
1010
|
-
|
|
1011
|
-
res.status(404).json({ error: 'File not found' });
|
|
1012
|
-
} else if (error.code === 'EACCES') {
|
|
1013
|
-
res.status(403).json({ error: 'Permission denied' });
|
|
1014
|
-
} else {
|
|
1015
|
-
res.status(500).json({ error: error.message });
|
|
1016
|
-
}
|
|
1048
|
+
sendProjectFileError(res, error, 'File not found');
|
|
1017
1049
|
}
|
|
1018
1050
|
});
|
|
1019
1051
|
|
|
@@ -1025,35 +1057,15 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
|
|
|
1025
1057
|
|
|
1026
1058
|
console.log('[DEBUG] Binary file serve request:', projectName, filePath);
|
|
1027
1059
|
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
return res.status(400).json({ error: 'Invalid file path' });
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
|
|
1034
|
-
if (!projectRoot) {
|
|
1035
|
-
return res.status(404).json({ error: 'Project not found' });
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
const resolved = path.resolve(filePath);
|
|
1039
|
-
const normalizedRoot = path.resolve(projectRoot) + path.sep;
|
|
1040
|
-
if (!resolved.startsWith(normalizedRoot)) {
|
|
1041
|
-
return res.status(403).json({ error: 'Path must be under project root' });
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
// Check if file exists
|
|
1045
|
-
try {
|
|
1046
|
-
await fsPromises.access(resolved);
|
|
1047
|
-
} catch (error) {
|
|
1048
|
-
return res.status(404).json({ error: 'File not found' });
|
|
1049
|
-
}
|
|
1060
|
+
const { targetPath } = await resolveProjectFilePath(projectName, filePath);
|
|
1061
|
+
await fsPromises.access(targetPath);
|
|
1050
1062
|
|
|
1051
1063
|
// Get file extension and set appropriate content type
|
|
1052
|
-
const mimeType = mime.lookup(
|
|
1064
|
+
const mimeType = mime.lookup(targetPath) || 'application/octet-stream';
|
|
1053
1065
|
res.setHeader('Content-Type', mimeType);
|
|
1054
1066
|
|
|
1055
1067
|
// Stream the file
|
|
1056
|
-
const fileStream = fs.createReadStream(
|
|
1068
|
+
const fileStream = fs.createReadStream(targetPath);
|
|
1057
1069
|
fileStream.pipe(res);
|
|
1058
1070
|
|
|
1059
1071
|
fileStream.on('error', (error) => {
|
|
@@ -1066,90 +1078,19 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
|
|
|
1066
1078
|
} catch (error) {
|
|
1067
1079
|
console.error('Error serving binary file:', error);
|
|
1068
1080
|
if (!res.headersSent) {
|
|
1069
|
-
res
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
});
|
|
1073
|
-
|
|
1074
|
-
// Save file content endpoint
|
|
1075
|
-
app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) => {
|
|
1076
|
-
try {
|
|
1077
|
-
const { projectName } = req.params;
|
|
1078
|
-
const { filePath, content } = req.body;
|
|
1079
|
-
|
|
1080
|
-
console.log('[DEBUG] File save request:', projectName, filePath);
|
|
1081
|
-
|
|
1082
|
-
// Security: ensure the requested path is inside the project root
|
|
1083
|
-
if (!filePath) {
|
|
1084
|
-
return res.status(400).json({ error: 'Invalid file path' });
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
if (content === undefined) {
|
|
1088
|
-
return res.status(400).json({ error: 'Content is required' });
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
|
|
1092
|
-
if (!projectRoot) {
|
|
1093
|
-
return res.status(404).json({ error: 'Project not found' });
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
// Handle both absolute and relative paths
|
|
1097
|
-
const resolved = path.isAbsolute(filePath)
|
|
1098
|
-
? path.resolve(filePath)
|
|
1099
|
-
: path.resolve(projectRoot, filePath);
|
|
1100
|
-
const normalizedRoot = path.resolve(projectRoot) + path.sep;
|
|
1101
|
-
if (!resolved.startsWith(normalizedRoot)) {
|
|
1102
|
-
return res.status(403).json({ error: 'Path must be under project root' });
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
// Write the new content
|
|
1106
|
-
await fsPromises.writeFile(resolved, content, 'utf8');
|
|
1107
|
-
|
|
1108
|
-
res.json({
|
|
1109
|
-
success: true,
|
|
1110
|
-
path: resolved,
|
|
1111
|
-
message: 'File saved successfully'
|
|
1112
|
-
});
|
|
1113
|
-
} catch (error) {
|
|
1114
|
-
console.error('Error saving file:', error);
|
|
1115
|
-
if (error.code === 'ENOENT') {
|
|
1116
|
-
res.status(404).json({ error: 'File or directory not found' });
|
|
1117
|
-
} else if (error.code === 'EACCES') {
|
|
1118
|
-
res.status(403).json({ error: 'Permission denied' });
|
|
1119
|
-
} else {
|
|
1120
|
-
res.status(500).json({ error: error.message });
|
|
1081
|
+
sendProjectFileError(res, error, 'File not found');
|
|
1121
1082
|
}
|
|
1122
1083
|
}
|
|
1123
1084
|
});
|
|
1124
1085
|
|
|
1125
1086
|
app.get('/api/projects/:projectName/files', authenticateToken, async (req, res) => {
|
|
1126
1087
|
try {
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
// Use extractProjectDirectory to get the actual project path
|
|
1131
|
-
let actualPath;
|
|
1132
|
-
try {
|
|
1133
|
-
actualPath = await extractProjectDirectory(req.params.projectName);
|
|
1134
|
-
} catch (error) {
|
|
1135
|
-
console.error('Error extracting project directory:', error);
|
|
1136
|
-
// Fallback to simple dash replacement
|
|
1137
|
-
actualPath = req.params.projectName.replace(/-/g, '/');
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
// Check if path exists
|
|
1141
|
-
try {
|
|
1142
|
-
await fsPromises.access(actualPath);
|
|
1143
|
-
} catch (e) {
|
|
1144
|
-
return res.status(404).json({ error: `Project path not found: ${actualPath}` });
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
const files = await getFileTree(actualPath, 10, 0, true);
|
|
1148
|
-
const hiddenFiles = files.filter(f => f.name.startsWith('.'));
|
|
1088
|
+
const projectRoot = await getResolvedProjectRoot(req.params.projectName);
|
|
1089
|
+
const files = await getFileTree(projectRoot, 10, 0, true);
|
|
1149
1090
|
res.json(files);
|
|
1150
1091
|
} catch (error) {
|
|
1151
1092
|
console.error('[ERROR] File tree error:', error.message);
|
|
1152
|
-
res
|
|
1093
|
+
sendProjectFileError(res, error, error.statusCode === 404 ? error.message : 'Failed to list files');
|
|
1153
1094
|
}
|
|
1154
1095
|
});
|
|
1155
1096
|
|
|
@@ -2218,6 +2159,7 @@ async function startServer() {
|
|
|
2218
2159
|
try {
|
|
2219
2160
|
// Initialize authentication database
|
|
2220
2161
|
await initializeDatabase();
|
|
2162
|
+
userDb.ensureDefaultUser();
|
|
2221
2163
|
await getChannelManager().initialize();
|
|
2222
2164
|
|
|
2223
2165
|
// Check if running in production mode (dist folder exists)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
|
|
3
|
+
function normalizeHost(value) {
|
|
4
|
+
const raw = String(value || '').trim().toLowerCase();
|
|
5
|
+
if (!raw) {
|
|
6
|
+
return '';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (raw.startsWith('[')) {
|
|
10
|
+
const closingIndex = raw.indexOf(']');
|
|
11
|
+
return closingIndex >= 0 ? raw.slice(1, closingIndex) : raw;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const hostParts = raw.split(':');
|
|
15
|
+
if (hostParts.length === 2) {
|
|
16
|
+
return hostParts[0];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return raw;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeAddress(value) {
|
|
23
|
+
const raw = String(value || '').trim().toLowerCase();
|
|
24
|
+
if (!raw) {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return raw.replace(/^::ffff:/, '');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isLoopbackHost(host) {
|
|
32
|
+
const normalizedHost = normalizeHost(host);
|
|
33
|
+
return normalizedHost === 'localhost'
|
|
34
|
+
|| normalizedHost === '127.0.0.1'
|
|
35
|
+
|| normalizedHost === '::1';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isLoopbackAddress(address) {
|
|
39
|
+
const normalizedAddress = normalizeAddress(address);
|
|
40
|
+
return normalizedAddress === '127.0.0.1' || normalizedAddress === '::1';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isLoopbackRequest(req) {
|
|
44
|
+
const forwardedHost = String(req?.headers?.['x-forwarded-host'] || '').split(',')[0].trim();
|
|
45
|
+
const host = forwardedHost || req?.headers?.host || req?.hostname || '';
|
|
46
|
+
return isLoopbackHost(host);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isIPv4Family(value) {
|
|
50
|
+
return value === 'IPv4' || value === 4;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function collectLanIPv4Addresses(networkInterfaces = os.networkInterfaces()) {
|
|
54
|
+
const addresses = new Set();
|
|
55
|
+
|
|
56
|
+
for (const entries of Object.values(networkInterfaces || {})) {
|
|
57
|
+
for (const entry of entries || []) {
|
|
58
|
+
if (!entry || entry.internal || !isIPv4Family(entry.family)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const address = String(entry.address || '').trim();
|
|
63
|
+
if (!address) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
addresses.add(address);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return Array.from(addresses).sort((left, right) => left.localeCompare(right));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export {
|
|
75
|
+
collectLanIPv4Addresses,
|
|
76
|
+
isLoopbackAddress,
|
|
77
|
+
isLoopbackHost,
|
|
78
|
+
isLoopbackRequest,
|
|
79
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import bcrypt from 'bcrypt';
|
|
4
|
+
|
|
5
|
+
import { getDataFilePath } from '../database/db.js';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_STATE = Object.freeze({
|
|
8
|
+
passwordHash: null,
|
|
9
|
+
sessionVersion: 0,
|
|
10
|
+
updatedAt: null,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
function getLanAccessStatePath() {
|
|
14
|
+
if (process.env.LAN_ACCESS_STATE_PATH) {
|
|
15
|
+
return process.env.LAN_ACCESS_STATE_PATH;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return path.join(path.dirname(getDataFilePath()), 'lan-access-state.json');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeState(rawState) {
|
|
22
|
+
const source = rawState && typeof rawState === 'object' ? rawState : {};
|
|
23
|
+
return {
|
|
24
|
+
passwordHash: typeof source.passwordHash === 'string' && source.passwordHash.trim() ? source.passwordHash : null,
|
|
25
|
+
sessionVersion: Number.isInteger(source.sessionVersion) && source.sessionVersion >= 0 ? source.sessionVersion : 0,
|
|
26
|
+
updatedAt: typeof source.updatedAt === 'string' ? source.updatedAt : null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function ensureStateDir() {
|
|
31
|
+
const statePath = getLanAccessStatePath();
|
|
32
|
+
const stateDir = path.dirname(statePath);
|
|
33
|
+
if (!fs.existsSync(stateDir)) {
|
|
34
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readLanAccessState() {
|
|
39
|
+
const statePath = getLanAccessStatePath();
|
|
40
|
+
if (!fs.existsSync(statePath)) {
|
|
41
|
+
return { ...DEFAULT_STATE };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
46
|
+
return normalizeState(parsed);
|
|
47
|
+
} catch {
|
|
48
|
+
return { ...DEFAULT_STATE };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function writeLanAccessState(nextState) {
|
|
53
|
+
ensureStateDir();
|
|
54
|
+
const statePath = getLanAccessStatePath();
|
|
55
|
+
const normalized = normalizeState(nextState);
|
|
56
|
+
const tempPath = `${statePath}.tmp`;
|
|
57
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8');
|
|
58
|
+
fs.renameSync(tempPath, statePath);
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hasLanAccessPassword() {
|
|
63
|
+
return Boolean(readLanAccessState().passwordHash);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getLanSessionVersion() {
|
|
67
|
+
return readLanAccessState().sessionVersion;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function setLanAccessPassword(password) {
|
|
71
|
+
const trimmedPassword = String(password || '').trim();
|
|
72
|
+
if (!trimmedPassword) {
|
|
73
|
+
throw new Error('Access password is required');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const currentState = readLanAccessState();
|
|
77
|
+
const passwordHash = await bcrypt.hash(trimmedPassword, 12);
|
|
78
|
+
return writeLanAccessState({
|
|
79
|
+
passwordHash,
|
|
80
|
+
sessionVersion: currentState.sessionVersion + 1,
|
|
81
|
+
updatedAt: new Date().toISOString(),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function verifyLanAccessPassword(password) {
|
|
86
|
+
const state = readLanAccessState();
|
|
87
|
+
if (!state.passwordHash) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return bcrypt.compare(String(password || ''), state.passwordHash);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export {
|
|
95
|
+
getLanAccessStatePath,
|
|
96
|
+
getLanSessionVersion,
|
|
97
|
+
hasLanAccessPassword,
|
|
98
|
+
readLanAccessState,
|
|
99
|
+
setLanAccessPassword,
|
|
100
|
+
verifyLanAccessPassword,
|
|
101
|
+
writeLanAccessState,
|
|
102
|
+
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import jwt from 'jsonwebtoken';
|
|
2
2
|
import { userDb } from '../database/db.js';
|
|
3
3
|
import { IS_PLATFORM } from '../constants/config.js';
|
|
4
|
+
import { getLanSessionVersion } from '../lan-access/state.js';
|
|
5
|
+
import { isLoopbackRequest } from '../lan-access/core.js';
|
|
4
6
|
|
|
5
7
|
// Get JWT secret from environment or use default (for development)
|
|
6
8
|
const JWT_SECRET = process.env.JWT_SECRET || 'claude-ui-dev-secret-change-in-production';
|
|
@@ -10,6 +12,37 @@ const PUBLIC_GET_ROUTE_PATTERNS = [
|
|
|
10
12
|
/^\/api\/session-core\/providers(?:\/[^/]+)?\/?$/i,
|
|
11
13
|
];
|
|
12
14
|
|
|
15
|
+
const DEFAULT_TOKEN_EXPIRY = '30d';
|
|
16
|
+
|
|
17
|
+
function isSessionVersionAllowed(decodedToken, currentSessionVersion = getLanSessionVersion()) {
|
|
18
|
+
const expectedSessionVersion = Number.isInteger(currentSessionVersion) ? currentSessionVersion : 0;
|
|
19
|
+
const tokenSessionVersion = Number(decodedToken?.sessionVersion);
|
|
20
|
+
|
|
21
|
+
if (expectedSessionVersion <= 0) {
|
|
22
|
+
return tokenSessionVersion === 0 || Number.isNaN(tokenSessionVersion);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return Number.isInteger(tokenSessionVersion) && tokenSessionVersion === expectedSessionVersion;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function verifySignedToken(token, { expectedPurpose = null } = {}) {
|
|
29
|
+
const decoded = jwt.verify(token, JWT_SECRET);
|
|
30
|
+
|
|
31
|
+
if (expectedPurpose && decoded?.purpose !== expectedPurpose) {
|
|
32
|
+
const error = new Error('Invalid token purpose');
|
|
33
|
+
error.code = 'INVALID_TOKEN_PURPOSE';
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!isSessionVersionAllowed(decoded)) {
|
|
38
|
+
const error = new Error('Token session is no longer valid');
|
|
39
|
+
error.code = 'STALE_SESSION_VERSION';
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return decoded;
|
|
44
|
+
}
|
|
45
|
+
|
|
13
46
|
// Optional API key middleware
|
|
14
47
|
const validateApiKey = (req, res, next) => {
|
|
15
48
|
// Skip API key validation if not configured
|
|
@@ -36,17 +69,17 @@ const authenticateToken = async (req, res, next) => {
|
|
|
36
69
|
}
|
|
37
70
|
|
|
38
71
|
// Platform mode: use single database user
|
|
39
|
-
if (IS_PLATFORM) {
|
|
72
|
+
if (IS_PLATFORM || isLoopbackRequest(req)) {
|
|
40
73
|
try {
|
|
41
74
|
const user = userDb.getFirstUser();
|
|
42
75
|
if (!user) {
|
|
43
|
-
return res.status(500).json({ error: '
|
|
76
|
+
return res.status(500).json({ error: 'No default user found in database' });
|
|
44
77
|
}
|
|
45
78
|
req.user = user;
|
|
46
79
|
return next();
|
|
47
80
|
} catch (error) {
|
|
48
|
-
console.error('
|
|
49
|
-
return res.status(500).json({ error: '
|
|
81
|
+
console.error('Local authentication bypass error:', error);
|
|
82
|
+
return res.status(500).json({ error: 'Failed to resolve local user' });
|
|
50
83
|
}
|
|
51
84
|
}
|
|
52
85
|
|
|
@@ -64,7 +97,7 @@ const authenticateToken = async (req, res, next) => {
|
|
|
64
97
|
}
|
|
65
98
|
|
|
66
99
|
try {
|
|
67
|
-
const decoded =
|
|
100
|
+
const decoded = verifySignedToken(token);
|
|
68
101
|
|
|
69
102
|
// Verify user still exists and is active
|
|
70
103
|
const user = userDb.getUserById(decoded.userId);
|
|
@@ -81,21 +114,29 @@ const authenticateToken = async (req, res, next) => {
|
|
|
81
114
|
};
|
|
82
115
|
|
|
83
116
|
// Generate JWT token (never expires)
|
|
84
|
-
const generateToken = (user) => {
|
|
117
|
+
const generateToken = (user, options = {}) => {
|
|
118
|
+
const {
|
|
119
|
+
expiresIn = DEFAULT_TOKEN_EXPIRY,
|
|
120
|
+
purpose = 'app',
|
|
121
|
+
sessionVersion = getLanSessionVersion(),
|
|
122
|
+
} = options;
|
|
123
|
+
|
|
85
124
|
return jwt.sign(
|
|
86
|
-
{
|
|
87
|
-
userId: user.id,
|
|
88
|
-
username: user.username
|
|
125
|
+
{
|
|
126
|
+
userId: user.id,
|
|
127
|
+
username: user.username,
|
|
128
|
+
purpose,
|
|
129
|
+
sessionVersion,
|
|
89
130
|
},
|
|
90
|
-
JWT_SECRET
|
|
91
|
-
|
|
131
|
+
JWT_SECRET,
|
|
132
|
+
expiresIn ? { expiresIn } : undefined
|
|
92
133
|
);
|
|
93
134
|
};
|
|
94
135
|
|
|
95
136
|
// WebSocket authentication function
|
|
96
|
-
const authenticateWebSocket = (token) => {
|
|
137
|
+
const authenticateWebSocket = (token, request = null) => {
|
|
97
138
|
// Platform mode: bypass token validation, return first user
|
|
98
|
-
if (IS_PLATFORM) {
|
|
139
|
+
if (IS_PLATFORM || isLoopbackRequest(request)) {
|
|
99
140
|
try {
|
|
100
141
|
const user = userDb.getFirstUser();
|
|
101
142
|
if (user) {
|
|
@@ -114,7 +155,7 @@ const authenticateWebSocket = (token) => {
|
|
|
114
155
|
}
|
|
115
156
|
|
|
116
157
|
try {
|
|
117
|
-
const decoded =
|
|
158
|
+
const decoded = verifySignedToken(token);
|
|
118
159
|
return decoded;
|
|
119
160
|
} catch (error) {
|
|
120
161
|
console.error('WebSocket token verification error:', error);
|
|
@@ -127,5 +168,7 @@ export {
|
|
|
127
168
|
authenticateToken,
|
|
128
169
|
generateToken,
|
|
129
170
|
authenticateWebSocket,
|
|
171
|
+
isSessionVersionAllowed,
|
|
172
|
+
verifySignedToken,
|
|
130
173
|
JWT_SECRET
|
|
131
174
|
};
|