@axhub/genie 0.2.8 → 0.2.10

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 (106) hide show
  1. package/LICENSE +21 -675
  2. package/dist/api-docs.html +2 -2
  3. package/dist/assets/App-CYCCsgwf.js +264 -0
  4. package/dist/assets/ReviewApp-0srHIXwb.js +1 -0
  5. package/dist/assets/{_basePickBy-CqJbRZ9y.js → _basePickBy-DVVb07UV.js} +1 -1
  6. package/dist/assets/{_baseUniq-BS8YH8jO.js → _baseUniq-BtbziL5G.js} +1 -1
  7. package/dist/assets/{arc-BBmKEN-S.js → arc-BsCC8yBD.js} +1 -1
  8. package/dist/assets/{architectureDiagram-2XIMDMQ5-N5lcb82R.js → architectureDiagram-2XIMDMQ5-woFp6eNI.js} +1 -1
  9. package/dist/assets/{blockDiagram-WCTKOSBZ-DTMwHuLn.js → blockDiagram-WCTKOSBZ-ya8VAc2k.js} +1 -1
  10. package/dist/assets/{c4Diagram-IC4MRINW-BTKlkXI9.js → c4Diagram-IC4MRINW-CY1dZmIZ.js} +1 -1
  11. package/dist/assets/channel-BMhScXFe.js +1 -0
  12. package/dist/assets/{chunk-4BX2VUAB-DUdoTxAc.js → chunk-4BX2VUAB-CR1lAd74.js} +1 -1
  13. package/dist/assets/{chunk-55IACEB6-Bm_92xe4.js → chunk-55IACEB6-CP98WcFC.js} +1 -1
  14. package/dist/assets/{chunk-FMBD7UC4-CGW0g62g.js → chunk-FMBD7UC4-D9c7ijAB.js} +1 -1
  15. package/dist/assets/{chunk-JSJVCQXG-DYkTH3w1.js → chunk-JSJVCQXG-DQAGYOn-.js} +1 -1
  16. package/dist/assets/{chunk-KX2RTZJC-C9oTlISU.js → chunk-KX2RTZJC-BbTXiDq7.js} +1 -1
  17. package/dist/assets/{chunk-NQ4KR5QH-CM50ygWP.js → chunk-NQ4KR5QH-BI6AX0dr.js} +1 -1
  18. package/dist/assets/{chunk-QZHKN3VN-7dzpYeNJ.js → chunk-QZHKN3VN-DB3V2Ifo.js} +1 -1
  19. package/dist/assets/{chunk-WL4C6EOR-Cm9nQrsr.js → chunk-WL4C6EOR-DhzTthv6.js} +1 -1
  20. package/dist/assets/classDiagram-VBA2DB6C-CMIxlWcT.js +1 -0
  21. package/dist/assets/classDiagram-v2-RAHNMMFH-CMIxlWcT.js +1 -0
  22. package/dist/assets/clone-BPqOt4r3.js +1 -0
  23. package/dist/assets/{cose-bilkent-S5V4N54A-Ccp_p0JZ.js → cose-bilkent-S5V4N54A-BQ09ZE2j.js} +1 -1
  24. package/dist/assets/{dagre-KLK3FWXG-fBwTLUp9.js → dagre-KLK3FWXG-Dc2ueD_R.js} +1 -1
  25. package/dist/assets/{diagram-E7M64L7V-CeNVmFUp.js → diagram-E7M64L7V-DP-LsQoL.js} +1 -1
  26. package/dist/assets/{diagram-IFDJBPK2-CtavyLGa.js → diagram-IFDJBPK2-Cg6r42cB.js} +1 -1
  27. package/dist/assets/{diagram-P4PSJMXO-CpQTjQwc.js → diagram-P4PSJMXO-aHsfoUZE.js} +1 -1
  28. package/dist/assets/{erDiagram-INFDFZHY-B8R5vwhd.js → erDiagram-INFDFZHY-qBXJ4aAz.js} +1 -1
  29. package/dist/assets/{flowDiagram-PKNHOUZH-BvkVVwIQ.js → flowDiagram-PKNHOUZH-D_13emJM.js} +1 -1
  30. package/dist/assets/{ganttDiagram-A5KZAMGK-DOu3hSNa.js → ganttDiagram-A5KZAMGK-BvIcOLwz.js} +1 -1
  31. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-C7zT67YE.js → gitGraphDiagram-K3NZZRJ6-ad0vvNcU.js} +1 -1
  32. package/dist/assets/{graph-D11wiwHo.js → graph-CeJCMjan.js} +1 -1
  33. package/dist/assets/{highlighted-body-TPN3WLV5-Babpthg-.js → highlighted-body-TPN3WLV5-B_novwSz.js} +1 -1
  34. package/dist/assets/index-C514cLyb.js +2 -0
  35. package/dist/assets/index-h1DBl_g3.css +1 -0
  36. package/dist/assets/{infoDiagram-LFFYTUFH-BmA7IpQG.js → infoDiagram-LFFYTUFH-lOxAqb3m.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-PHBUUO56-BEquZd3E.js → ishikawaDiagram-PHBUUO56-DIr-51gj.js} +1 -1
  38. package/dist/assets/{journeyDiagram-4ABVD52K-BfemGz7f.js → journeyDiagram-4ABVD52K-CYcIW0ZU.js} +1 -1
  39. package/dist/assets/{kanban-definition-K7BYSVSG-CWja3mln.js → kanban-definition-K7BYSVSG-C1ZK616a.js} +1 -1
  40. package/dist/assets/{layout-BLUNf-PJ.js → layout-CI2RM-v6.js} +1 -1
  41. package/dist/assets/{linear-DukIV_Xv.js → linear-DE7bISck.js} +1 -1
  42. package/dist/assets/{mermaid-O7DHMXV3-SgtM28qI.js → mermaid-O7DHMXV3-XxAJo8EK.js} +6 -6
  43. package/dist/assets/{mindmap-definition-YRQLILUH-4UjqXITU.js → mindmap-definition-YRQLILUH-Dz6EFjmn.js} +1 -1
  44. package/dist/assets/{pieDiagram-SKSYHLDU-8AxqJd0M.js → pieDiagram-SKSYHLDU-DPpEzUed.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-337W2JSQ-D60m8V8r.js → quadrantDiagram-337W2JSQ-xdoXNet7.js} +1 -1
  46. package/dist/assets/{requirementDiagram-Z7DCOOCP-zqh9jBVf.js → requirementDiagram-Z7DCOOCP-DUq8H3CL.js} +1 -1
  47. package/dist/assets/{sankeyDiagram-WA2Y5GQK-CDZILTLI.js → sankeyDiagram-WA2Y5GQK-CmqEUxRu.js} +1 -1
  48. package/dist/assets/{sequenceDiagram-2WXFIKYE-7BReFd0L.js → sequenceDiagram-2WXFIKYE-DhtXRNiH.js} +1 -1
  49. package/dist/assets/{stateDiagram-RAJIS63D-HPTVdIG4.js → stateDiagram-RAJIS63D-Dj0HOlbN.js} +1 -1
  50. package/dist/assets/stateDiagram-v2-FVOUBMTO-C9utf5gv.js +1 -0
  51. package/dist/assets/{timeline-definition-YZTLITO2-CTVllFgr.js → timeline-definition-YZTLITO2-DUuJzZB5.js} +1 -1
  52. package/dist/assets/{treemap-KZPCXAKY-BtyxboJZ.js → treemap-KZPCXAKY-DpYBQ0qr.js} +1 -1
  53. package/dist/assets/vendor-codemirror-CMHSJ_9p.js +9 -0
  54. package/dist/assets/{vendor-react-Cpt6D04s.js → vendor-react-xmA_f8ig.js} +1 -1
  55. package/dist/assets/{vennDiagram-LZ73GAT5-D96ZI6Mg.js → vennDiagram-LZ73GAT5-DpePUyOd.js} +1 -1
  56. package/dist/assets/{xychartDiagram-JWTSCODW-eRk-39YO.js → xychartDiagram-JWTSCODW-Cfp1I4_U.js} +1 -1
  57. package/dist/index.html +5 -5
  58. package/package.json +8 -7
  59. package/server/acp-runtime/client.js +129 -16
  60. package/server/acp-runtime/index.js +54 -0
  61. package/server/acp-runtime/registry.js +2 -2
  62. package/server/acp-runtime/session-store.js +79 -5
  63. package/server/cli.js +55 -10
  64. package/server/database/db.js +20 -0
  65. package/server/external-agent/service.js +24 -6
  66. package/server/external-agent/ws.js +540 -27
  67. package/server/index.js +112 -151
  68. package/server/lan-access/core.js +79 -0
  69. package/server/lan-access/state.js +102 -0
  70. package/server/middleware/auth.js +57 -14
  71. package/server/projects.js +930 -667
  72. package/server/routes/auth.js +24 -4
  73. package/server/routes/cli-auth.js +21 -25
  74. package/server/routes/codex.js +84 -298
  75. package/server/routes/commands.js +322 -407
  76. package/server/routes/lan-access.js +231 -0
  77. package/server/routes/projects.js +154 -158
  78. package/server/routes/session-core.js +160 -91
  79. package/server/routes/settings.js +113 -99
  80. package/server/session-core/eventStore.js +60 -20
  81. package/server/session-core/providerAdapters.js +75 -38
  82. package/server/session-core/runtimeState.js +8 -0
  83. package/server/session-core/sessionListMerge.js +47 -0
  84. package/shared/conversationEvents.js +174 -15
  85. package/shared/modelConstants.js +79 -99
  86. package/dist/assets/App-CTKZtqB1.js +0 -460
  87. package/dist/assets/ReviewApp-DM6BNAzR.js +0 -1
  88. package/dist/assets/channel-1oJBvF-0.js +0 -1
  89. package/dist/assets/classDiagram-VBA2DB6C-d5TeKFM4.js +0 -1
  90. package/dist/assets/classDiagram-v2-RAHNMMFH-d5TeKFM4.js +0 -1
  91. package/dist/assets/clone-CinxIlEu.js +0 -1
  92. package/dist/assets/index-DFxzgWoO.js +0 -2
  93. package/dist/assets/index-YCFGDVKw.css +0 -1
  94. package/dist/assets/stateDiagram-v2-FVOUBMTO-DTUf5_gC.js +0 -1
  95. package/dist/assets/vendor-codemirror-Dz7_EqNA.js +0 -39
  96. package/server/_legacy-providers/README.md +0 -30
  97. package/server/_legacy-providers/claude-sdk.js +0 -956
  98. package/server/_legacy-providers/gemini-cli.js +0 -368
  99. package/server/_legacy-providers/openai-codex.js +0 -705
  100. package/server/_legacy-providers/opencode-cli.js +0 -674
  101. package/server/routes/git.js +0 -1110
  102. package/server/routes/mcp-utils.js +0 -48
  103. package/server/routes/mcp.js +0 -536
  104. package/server/routes/taskmaster.js +0 -1963
  105. package/server/utils/mcp-detector.js +0 -198
  106. 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, 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 } 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';
@@ -333,8 +330,8 @@ async function setupProjectsWatcher() {
333
330
  clearProjectDirectoryCache();
334
331
  clearProviderSessionLookupCaches();
335
332
 
336
- // Get updated projects list
337
- const updatedProjects = await getProjects(broadcastProgress);
333
+ // Get updated lightweight projects list
334
+ const updatedProjects = await getProjectsList(broadcastProgress);
338
335
 
339
336
  // Notify all connected clients about the project changes
340
337
  const changedProvider = filePath.startsWith(codexSessionsPath)
@@ -343,14 +340,21 @@ async function setupProjectsWatcher() {
343
340
  const changedFileRoot = changedProvider === 'codex'
344
341
  ? codexSessionsPath
345
342
  : claudeProjectsPath;
343
+ const normalizedRelativePath = path.relative(changedFileRoot, filePath);
344
+ const changedPathSegments = normalizedRelativePath.split(path.sep).filter(Boolean);
345
+ const affectedProjectName = changedProvider === 'claude'
346
+ ? (changedPathSegments[0] || null)
347
+ : null;
346
348
  const updateMessage = JSON.stringify({
347
349
  type: 'projects_updated',
348
350
  projects: updatedProjects,
349
351
  timestamp: new Date().toISOString(),
350
352
  changeType: eventType,
351
- changedFile: path.relative(changedFileRoot, filePath),
353
+ changedFile: normalizedRelativePath,
352
354
  changedProvider,
353
- changedSessionId: extractSessionIdFromChangedFile(filePath)
355
+ changedSessionId: extractSessionIdFromChangedFile(filePath),
356
+ affectedProjectName,
357
+ requiresDetailsRefresh: Boolean(affectedProjectName)
354
358
  });
355
359
 
356
360
  connectedClients.forEach(client => {
@@ -416,7 +420,7 @@ const wss = new WebSocketServer({
416
420
 
417
421
  // Platform mode: always allow internal UI WebSocket connections.
418
422
  if (IS_PLATFORM) {
419
- const user = authenticateWebSocket(null); // Will return first user
423
+ const user = authenticateWebSocket(null, info.req); // Will return first user
420
424
  if (!user) {
421
425
  console.log('[WARN] Platform mode: No user found in database');
422
426
  done(false, 500, 'Platform mode: No user found in database');
@@ -431,7 +435,7 @@ const wss = new WebSocketServer({
431
435
  const token = url.searchParams.get('token') ||
432
436
  info.req.headers.authorization?.split(' ')[1];
433
437
 
434
- const user = authenticateWebSocket(token);
438
+ const user = authenticateWebSocket(token, info.req);
435
439
  if (!user) {
436
440
  console.log('[WARN] WebSocket authentication failed');
437
441
  done(false, 401, 'Unauthorized');
@@ -490,22 +494,12 @@ app.use('/api', validateApiKey);
490
494
 
491
495
  // Authentication routes (public)
492
496
  app.use('/api/auth', authRoutes);
497
+ app.use('/api/lan-access', lanAccessApiRoutes);
498
+ app.use('/lan-access', lanAccessPageRoutes);
493
499
 
494
500
  // Projects API Routes (protected)
495
501
  app.use('/api/projects', authenticateToken, projectsRoutes);
496
502
 
497
- // Git API Routes (protected)
498
- app.use('/api/git', authenticateToken, gitRoutes);
499
-
500
- // MCP API Routes (protected)
501
- app.use('/api/mcp', authenticateToken, mcpRoutes);
502
-
503
- // TaskMaster API Routes (protected)
504
- app.use('/api/taskmaster', authenticateToken, taskmasterRoutes);
505
-
506
- // MCP utilities
507
- app.use('/api/mcp-utils', authenticateToken, mcpUtilsRoutes);
508
-
509
503
  // Commands API Routes (protected)
510
504
  app.use('/api/commands', authenticateToken, commandsRoutes);
511
505
 
@@ -647,6 +641,7 @@ app.post('/api/system/update', authenticateToken, async (req, res) => {
647
641
 
648
642
  app.get('/api/projects', authenticateToken, async (req, res) => {
649
643
  try {
644
+ res.setHeader('Deprecation', 'true');
650
645
  const projects = await getProjects(broadcastProgress);
651
646
  res.json(projects);
652
647
  } catch (error) {
@@ -654,6 +649,27 @@ app.get('/api/projects', authenticateToken, async (req, res) => {
654
649
  }
655
650
  });
656
651
 
652
+ app.get('/api/projects/list', authenticateToken, async (req, res) => {
653
+ try {
654
+ const projects = await getProjectsList(broadcastProgress);
655
+ res.json(projects);
656
+ } catch (error) {
657
+ res.status(500).json({ error: error.message });
658
+ }
659
+ });
660
+
661
+ app.get('/api/projects/:projectName/details', authenticateToken, async (req, res) => {
662
+ try {
663
+ const project = await getProjectDetails(req.params.projectName);
664
+ res.json(project);
665
+ } catch (error) {
666
+ if (/Project not found/i.test(error.message)) {
667
+ return res.status(404).json({ error: error.message });
668
+ }
669
+ res.status(500).json({ error: error.message });
670
+ }
671
+ });
672
+
657
673
  app.get('/api/projects/:projectName/sessions', authenticateToken, async (req, res) => {
658
674
  try {
659
675
  const { limit = 5, offset = 0 } = req.query;
@@ -947,6 +963,65 @@ app.post('/api/create-folder', authenticateToken, async (req, res) => {
947
963
  }
948
964
  });
949
965
 
966
+ function isWithinProjectRoot(projectRoot, candidatePath) {
967
+ const relativePath = path.relative(projectRoot, candidatePath);
968
+ return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
969
+ }
970
+
971
+ async function getResolvedProjectRoot(projectName) {
972
+ const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
973
+ if (!projectRoot) {
974
+ const error = new Error('Project not found');
975
+ error.statusCode = 404;
976
+ throw error;
977
+ }
978
+
979
+ const resolvedRoot = path.resolve(projectRoot);
980
+ await fsPromises.access(resolvedRoot);
981
+ return resolvedRoot;
982
+ }
983
+
984
+ async function resolveProjectFilePath(projectName, inputPath) {
985
+ const requestedPath = typeof inputPath === 'string' ? inputPath.trim() : '';
986
+ if (!requestedPath) {
987
+ const error = new Error('Invalid file path');
988
+ error.statusCode = 400;
989
+ throw error;
990
+ }
991
+
992
+ const projectRoot = await getResolvedProjectRoot(projectName);
993
+ const absoluteTarget = path.isAbsolute(requestedPath)
994
+ ? path.resolve(requestedPath)
995
+ : path.resolve(projectRoot, requestedPath);
996
+
997
+ if (!isWithinProjectRoot(projectRoot, absoluteTarget)) {
998
+ const error = new Error('Path must be under project root');
999
+ error.statusCode = 403;
1000
+ throw error;
1001
+ }
1002
+
1003
+ return {
1004
+ projectRoot,
1005
+ targetPath: absoluteTarget
1006
+ };
1007
+ }
1008
+
1009
+ function sendProjectFileError(res, error, missingLabel = 'File not found') {
1010
+ if (error.statusCode) {
1011
+ return res.status(error.statusCode).json({ error: error.message });
1012
+ }
1013
+
1014
+ if (error.code === 'ENOENT') {
1015
+ return res.status(404).json({ error: missingLabel });
1016
+ }
1017
+
1018
+ if (error.code === 'EACCES') {
1019
+ return res.status(403).json({ error: 'Permission denied' });
1020
+ }
1021
+
1022
+ return res.status(500).json({ error: error.message });
1023
+ }
1024
+
950
1025
  // Read file content endpoint
951
1026
  app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) => {
952
1027
  try {
@@ -955,36 +1030,12 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) =
955
1030
 
956
1031
  console.log('[DEBUG] File read request:', projectName, filePath);
957
1032
 
958
- // Security: ensure the requested path is inside the project root
959
- if (!filePath) {
960
- return res.status(400).json({ error: 'Invalid file path' });
961
- }
962
-
963
- const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
964
- if (!projectRoot) {
965
- return res.status(404).json({ error: 'Project not found' });
966
- }
967
-
968
- // Handle both absolute and relative paths
969
- const resolved = path.isAbsolute(filePath)
970
- ? path.resolve(filePath)
971
- : path.resolve(projectRoot, filePath);
972
- const normalizedRoot = path.resolve(projectRoot) + path.sep;
973
- if (!resolved.startsWith(normalizedRoot)) {
974
- return res.status(403).json({ error: 'Path must be under project root' });
975
- }
976
-
977
- const content = await fsPromises.readFile(resolved, 'utf8');
978
- res.json({ content, path: resolved });
1033
+ const { targetPath } = await resolveProjectFilePath(projectName, filePath);
1034
+ const content = await fsPromises.readFile(targetPath, 'utf8');
1035
+ res.json({ content, path: targetPath });
979
1036
  } catch (error) {
980
1037
  console.error('Error reading file:', error);
981
- if (error.code === 'ENOENT') {
982
- res.status(404).json({ error: 'File not found' });
983
- } else if (error.code === 'EACCES') {
984
- res.status(403).json({ error: 'Permission denied' });
985
- } else {
986
- res.status(500).json({ error: error.message });
987
- }
1038
+ sendProjectFileError(res, error, 'File not found');
988
1039
  }
989
1040
  });
990
1041
 
@@ -996,35 +1047,15 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
996
1047
 
997
1048
  console.log('[DEBUG] Binary file serve request:', projectName, filePath);
998
1049
 
999
- // Security: ensure the requested path is inside the project root
1000
- if (!filePath) {
1001
- return res.status(400).json({ error: 'Invalid file path' });
1002
- }
1003
-
1004
- const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
1005
- if (!projectRoot) {
1006
- return res.status(404).json({ error: 'Project not found' });
1007
- }
1008
-
1009
- const resolved = path.resolve(filePath);
1010
- const normalizedRoot = path.resolve(projectRoot) + path.sep;
1011
- if (!resolved.startsWith(normalizedRoot)) {
1012
- return res.status(403).json({ error: 'Path must be under project root' });
1013
- }
1014
-
1015
- // Check if file exists
1016
- try {
1017
- await fsPromises.access(resolved);
1018
- } catch (error) {
1019
- return res.status(404).json({ error: 'File not found' });
1020
- }
1050
+ const { targetPath } = await resolveProjectFilePath(projectName, filePath);
1051
+ await fsPromises.access(targetPath);
1021
1052
 
1022
1053
  // Get file extension and set appropriate content type
1023
- const mimeType = mime.lookup(resolved) || 'application/octet-stream';
1054
+ const mimeType = mime.lookup(targetPath) || 'application/octet-stream';
1024
1055
  res.setHeader('Content-Type', mimeType);
1025
1056
 
1026
1057
  // Stream the file
1027
- const fileStream = fs.createReadStream(resolved);
1058
+ const fileStream = fs.createReadStream(targetPath);
1028
1059
  fileStream.pipe(res);
1029
1060
 
1030
1061
  fileStream.on('error', (error) => {
@@ -1037,90 +1068,19 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
1037
1068
  } catch (error) {
1038
1069
  console.error('Error serving binary file:', error);
1039
1070
  if (!res.headersSent) {
1040
- res.status(500).json({ error: error.message });
1041
- }
1042
- }
1043
- });
1044
-
1045
- // Save file content endpoint
1046
- app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) => {
1047
- try {
1048
- const { projectName } = req.params;
1049
- const { filePath, content } = req.body;
1050
-
1051
- console.log('[DEBUG] File save request:', projectName, filePath);
1052
-
1053
- // Security: ensure the requested path is inside the project root
1054
- if (!filePath) {
1055
- return res.status(400).json({ error: 'Invalid file path' });
1056
- }
1057
-
1058
- if (content === undefined) {
1059
- return res.status(400).json({ error: 'Content is required' });
1060
- }
1061
-
1062
- const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
1063
- if (!projectRoot) {
1064
- return res.status(404).json({ error: 'Project not found' });
1065
- }
1066
-
1067
- // Handle both absolute and relative paths
1068
- const resolved = path.isAbsolute(filePath)
1069
- ? path.resolve(filePath)
1070
- : path.resolve(projectRoot, filePath);
1071
- const normalizedRoot = path.resolve(projectRoot) + path.sep;
1072
- if (!resolved.startsWith(normalizedRoot)) {
1073
- return res.status(403).json({ error: 'Path must be under project root' });
1074
- }
1075
-
1076
- // Write the new content
1077
- await fsPromises.writeFile(resolved, content, 'utf8');
1078
-
1079
- res.json({
1080
- success: true,
1081
- path: resolved,
1082
- message: 'File saved successfully'
1083
- });
1084
- } catch (error) {
1085
- console.error('Error saving file:', error);
1086
- if (error.code === 'ENOENT') {
1087
- res.status(404).json({ error: 'File or directory not found' });
1088
- } else if (error.code === 'EACCES') {
1089
- res.status(403).json({ error: 'Permission denied' });
1090
- } else {
1091
- res.status(500).json({ error: error.message });
1071
+ sendProjectFileError(res, error, 'File not found');
1092
1072
  }
1093
1073
  }
1094
1074
  });
1095
1075
 
1096
1076
  app.get('/api/projects/:projectName/files', authenticateToken, async (req, res) => {
1097
1077
  try {
1098
-
1099
- // Using fsPromises from import
1100
-
1101
- // Use extractProjectDirectory to get the actual project path
1102
- let actualPath;
1103
- try {
1104
- actualPath = await extractProjectDirectory(req.params.projectName);
1105
- } catch (error) {
1106
- console.error('Error extracting project directory:', error);
1107
- // Fallback to simple dash replacement
1108
- actualPath = req.params.projectName.replace(/-/g, '/');
1109
- }
1110
-
1111
- // Check if path exists
1112
- try {
1113
- await fsPromises.access(actualPath);
1114
- } catch (e) {
1115
- return res.status(404).json({ error: `Project path not found: ${actualPath}` });
1116
- }
1117
-
1118
- const files = await getFileTree(actualPath, 10, 0, true);
1119
- const hiddenFiles = files.filter(f => f.name.startsWith('.'));
1078
+ const projectRoot = await getResolvedProjectRoot(req.params.projectName);
1079
+ const files = await getFileTree(projectRoot, 10, 0, true);
1120
1080
  res.json(files);
1121
1081
  } catch (error) {
1122
1082
  console.error('[ERROR] File tree error:', error.message);
1123
- res.status(500).json({ error: error.message });
1083
+ sendProjectFileError(res, error, error.statusCode === 404 ? error.message : 'Failed to list files');
1124
1084
  }
1125
1085
  });
1126
1086
 
@@ -2189,6 +2149,7 @@ async function startServer() {
2189
2149
  try {
2190
2150
  // Initialize authentication database
2191
2151
  await initializeDatabase();
2152
+ userDb.ensureDefaultUser();
2192
2153
  await getChannelManager().initialize();
2193
2154
 
2194
2155
  // 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
+ };