@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.
Files changed (96) hide show
  1. package/dist/api-docs.html +2 -2
  2. package/dist/assets/App-VH1wNUHs.js +259 -0
  3. package/dist/assets/{ReviewApp-C9K--AQE.js → ReviewApp-D_9EN4TM.js} +1 -1
  4. package/dist/assets/{_basePickBy-DR_8uFCo.js → _basePickBy-BDnj7-0Z.js} +1 -1
  5. package/dist/assets/{_baseUniq-D0njlQ_7.js → _baseUniq-Bl0JKOyl.js} +1 -1
  6. package/dist/assets/{arc-CKlr_Rec.js → arc-DY-4Kev3.js} +1 -1
  7. package/dist/assets/{architectureDiagram-2XIMDMQ5-BmO_uLUH.js → architectureDiagram-2XIMDMQ5-qw7crNVd.js} +1 -1
  8. package/dist/assets/{blockDiagram-WCTKOSBZ-DhAeO-56.js → blockDiagram-WCTKOSBZ-B9xg7ep3.js} +1 -1
  9. package/dist/assets/{c4Diagram-IC4MRINW-C67kFoXx.js → c4Diagram-IC4MRINW-H9xp3ytb.js} +1 -1
  10. package/dist/assets/channel-CyNUnRfc.js +1 -0
  11. package/dist/assets/{chunk-4BX2VUAB-mLLagvJi.js → chunk-4BX2VUAB-B3EVDUxI.js} +1 -1
  12. package/dist/assets/{chunk-55IACEB6-Lx-hOjlM.js → chunk-55IACEB6-CGv945ef.js} +1 -1
  13. package/dist/assets/{chunk-FMBD7UC4-Bt-XmVUV.js → chunk-FMBD7UC4-uAT4CKWM.js} +1 -1
  14. package/dist/assets/{chunk-JSJVCQXG-Cya6gaDV.js → chunk-JSJVCQXG-Cbvlpkf7.js} +1 -1
  15. package/dist/assets/{chunk-KX2RTZJC-Bd7Ig6tF.js → chunk-KX2RTZJC-CcqIuGat.js} +1 -1
  16. package/dist/assets/{chunk-NQ4KR5QH-5UAE0Vg-.js → chunk-NQ4KR5QH-CgrcsRuX.js} +1 -1
  17. package/dist/assets/{chunk-QZHKN3VN-BAxZ8m7w.js → chunk-QZHKN3VN-Cx0APOoV.js} +1 -1
  18. package/dist/assets/{chunk-WL4C6EOR-DjDPvUUP.js → chunk-WL4C6EOR-BbZirvBk.js} +1 -1
  19. package/dist/assets/classDiagram-VBA2DB6C-DxBtyz2A.js +1 -0
  20. package/dist/assets/classDiagram-v2-RAHNMMFH-DxBtyz2A.js +1 -0
  21. package/dist/assets/clone-C341l3d0.js +1 -0
  22. package/dist/assets/{cose-bilkent-S5V4N54A-D-60XrkJ.js → cose-bilkent-S5V4N54A-CrvmGFLD.js} +1 -1
  23. package/dist/assets/{dagre-KLK3FWXG-bqu3ZS4K.js → dagre-KLK3FWXG-C-W6VPjS.js} +1 -1
  24. package/dist/assets/{diagram-E7M64L7V-BueeqoYm.js → diagram-E7M64L7V-IP2q3bL0.js} +1 -1
  25. package/dist/assets/{diagram-IFDJBPK2-D4fDv2E7.js → diagram-IFDJBPK2-CQaL-XyV.js} +1 -1
  26. package/dist/assets/{diagram-P4PSJMXO-WqipY3fN.js → diagram-P4PSJMXO-BxBLThfv.js} +1 -1
  27. package/dist/assets/{erDiagram-INFDFZHY-D0oVnO-x.js → erDiagram-INFDFZHY-Dyl7bJTt.js} +1 -1
  28. package/dist/assets/{flowDiagram-PKNHOUZH-DzbGyxrr.js → flowDiagram-PKNHOUZH-B7NFMgFK.js} +1 -1
  29. package/dist/assets/{ganttDiagram-A5KZAMGK-BwhbbgCP.js → ganttDiagram-A5KZAMGK-hReWSDu2.js} +1 -1
  30. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-DZgAh_KM.js → gitGraphDiagram-K3NZZRJ6-gVgcr0ST.js} +1 -1
  31. package/dist/assets/{graph-DzKos-N0.js → graph-DNDiJhTn.js} +1 -1
  32. package/dist/assets/{highlighted-body-TPN3WLV5-CKDMgz3X.js → highlighted-body-TPN3WLV5-DclLmTou.js} +1 -1
  33. package/dist/assets/index-DBkz_W_P.css +1 -0
  34. package/dist/assets/index-DdRyoXKh.js +2 -0
  35. package/dist/assets/{infoDiagram-LFFYTUFH-BFicZbTf.js → infoDiagram-LFFYTUFH-CqQOOzDA.js} +1 -1
  36. package/dist/assets/{ishikawaDiagram-PHBUUO56-CtihxDxl.js → ishikawaDiagram-PHBUUO56-CZ0iLiHg.js} +1 -1
  37. package/dist/assets/{journeyDiagram-4ABVD52K-Du00J8_d.js → journeyDiagram-4ABVD52K-DdfYKfNh.js} +1 -1
  38. package/dist/assets/{kanban-definition-K7BYSVSG-BJi9S0iQ.js → kanban-definition-K7BYSVSG-C5Vf32u6.js} +1 -1
  39. package/dist/assets/{layout-B80Sityu.js → layout-rvTEu2KS.js} +1 -1
  40. package/dist/assets/{linear-sRQLOf5H.js → linear-CD9SiYze.js} +1 -1
  41. package/dist/assets/{mermaid-O7DHMXV3-CBuVs4eJ.js → mermaid-O7DHMXV3-OZ8qWWwa.js} +167 -157
  42. package/dist/assets/{mindmap-definition-YRQLILUH-C5IL_xi-.js → mindmap-definition-YRQLILUH-CQxrLNVc.js} +1 -1
  43. package/dist/assets/{pieDiagram-SKSYHLDU-CeTwlJ8z.js → pieDiagram-SKSYHLDU-XgAUByWg.js} +1 -1
  44. package/dist/assets/{quadrantDiagram-337W2JSQ-COfUcLWt.js → quadrantDiagram-337W2JSQ-CH16ls7G.js} +1 -1
  45. package/dist/assets/{requirementDiagram-Z7DCOOCP-DSb-CJ5B.js → requirementDiagram-Z7DCOOCP-B_kQO06L.js} +1 -1
  46. package/dist/assets/{sankeyDiagram-WA2Y5GQK-8jtuVb45.js → sankeyDiagram-WA2Y5GQK-ofe78CyS.js} +1 -1
  47. package/dist/assets/{sequenceDiagram-2WXFIKYE-C2VpkMwA.js → sequenceDiagram-2WXFIKYE-Ckbxwny6.js} +1 -1
  48. package/dist/assets/{stateDiagram-RAJIS63D-fmwMqxxc.js → stateDiagram-RAJIS63D-DNtzCk14.js} +1 -1
  49. package/dist/assets/stateDiagram-v2-FVOUBMTO-B3VPhiE1.js +1 -0
  50. package/dist/assets/{timeline-definition-YZTLITO2-Dx1hP5lg.js → timeline-definition-YZTLITO2-zT6CklKt.js} +1 -1
  51. package/dist/assets/{treemap-KZPCXAKY-CkLOdYCZ.js → treemap-KZPCXAKY-y0U2c3xG.js} +1 -1
  52. package/dist/assets/vendor-codemirror-CMHSJ_9p.js +9 -0
  53. package/dist/assets/{vennDiagram-LZ73GAT5-D6KWcnln.js → vennDiagram-LZ73GAT5-xKj3SjYG.js} +1 -1
  54. package/dist/assets/{xychartDiagram-JWTSCODW-6fh6qmzN.js → xychartDiagram-JWTSCODW-Da_qyEoX.js} +1 -1
  55. package/dist/index.html +3 -3
  56. package/package.json +6 -5
  57. package/server/acp-runtime/client.js +120 -14
  58. package/server/acp-runtime/index.js +54 -0
  59. package/server/acp-runtime/registry.js +2 -2
  60. package/server/acp-runtime/session-store.js +75 -1
  61. package/server/cli.js +32 -8
  62. package/server/database/db.js +20 -0
  63. package/server/external-agent/ws.js +477 -24
  64. package/server/index.js +89 -147
  65. package/server/lan-access/core.js +79 -0
  66. package/server/lan-access/state.js +102 -0
  67. package/server/middleware/auth.js +57 -14
  68. package/server/projects.js +442 -535
  69. package/server/routes/auth.js +24 -4
  70. package/server/routes/cli-auth.js +21 -25
  71. package/server/routes/codex.js +84 -298
  72. package/server/routes/commands.js +335 -407
  73. package/server/routes/lan-access.js +231 -0
  74. package/server/routes/projects.js +154 -158
  75. package/server/routes/session-core.js +13 -7
  76. package/server/routes/settings.js +113 -99
  77. package/server/session-core/eventStore.js +15 -2
  78. package/server/session-core/providerAdapters.js +28 -28
  79. package/server/session-core/sessionListMerge.js +47 -0
  80. package/shared/conversationEvents.js +96 -1
  81. package/shared/modelConstants.js +79 -99
  82. package/dist/assets/App-GBcTeeUS.js +0 -460
  83. package/dist/assets/channel-V3MBjKys.js +0 -1
  84. package/dist/assets/classDiagram-VBA2DB6C-C790yYiY.js +0 -1
  85. package/dist/assets/classDiagram-v2-RAHNMMFH-C790yYiY.js +0 -1
  86. package/dist/assets/clone-BbMGfZwt.js +0 -1
  87. package/dist/assets/index-DiQlHzGj.js +0 -2
  88. package/dist/assets/index-Drat2nB9.css +0 -1
  89. package/dist/assets/stateDiagram-v2-FVOUBMTO-9GGXVWrR.js +0 -1
  90. package/dist/assets/vendor-codemirror-BxPY6emf.js +0 -39
  91. package/server/routes/git.js +0 -1110
  92. package/server/routes/mcp-utils.js +0 -48
  93. package/server/routes/mcp.js +0 -536
  94. package/server/routes/taskmaster.js +0 -1963
  95. package/server/utils/mcp-detector.js +0 -198
  96. 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
- // Security: ensure the requested path is inside the project root
988
- if (!filePath) {
989
- return res.status(400).json({ error: 'Invalid file path' });
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
- if (error.code === 'ENOENT') {
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
- // Security: ensure the requested path is inside the project root
1029
- if (!filePath) {
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(resolved) || 'application/octet-stream';
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(resolved);
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.status(500).json({ error: error.message });
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
- // Using fsPromises from import
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.status(500).json({ error: error.message });
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: 'Platform mode: No user found in database' });
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('Platform mode error:', error);
49
- return res.status(500).json({ error: 'Platform mode: Failed to fetch user' });
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 = jwt.verify(token, JWT_SECRET);
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
- // No expiration - token lasts forever
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 = jwt.verify(token, JWT_SECRET);
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
  };