@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
@@ -1,8 +1,18 @@
1
1
  import express from 'express';
2
- import { getProjects } from '../projects.js';
2
+ import crypto from 'node:crypto';
3
+ import path from 'path';
4
+ import {
5
+ getClaudeSessionMetadata,
6
+ getCodexSessionMetadata,
7
+ getGeminiSessionMetadata,
8
+ getOpencodeSessionMetadata,
9
+ getProjects,
10
+ getProjectsList
11
+ } from '../projects.js';
3
12
  import { discoverAllProviders, discoverProvider } from '../session-core/providerDiscovery.js';
4
13
  import { getProviderAdapter } from '../session-core/providerAdapters.js';
5
- import { listAcpSessions } from '../acp-runtime/session-store.js';
14
+ import { findAcpSessionRecord, listAcpSessions } from '../acp-runtime/session-store.js';
15
+ import { mergeSessionLists } from '../session-core/sessionListMerge.js';
6
16
 
7
17
  const router = express.Router();
8
18
 
@@ -21,12 +31,138 @@ async function flattenProjectSessions(project) {
21
31
  ];
22
32
 
23
33
  const acpSessions = await listAcpSessions({ projectPath });
34
+ const legacySessions = groups.flatMap(({ provider, items }) => (
35
+ items.map((item) => ({ ...item, provider, __provider: provider, source: item?.source || 'legacy' }))
36
+ ));
37
+ const normalizedAcpSessions = acpSessions.map((item) => ({
38
+ ...item,
39
+ provider: item.provider,
40
+ __provider: item.provider,
41
+ source: 'acp'
42
+ }));
24
43
 
25
- return [
26
- ...groups.flatMap(({ provider, items }) => items.map((item) => ({ ...item, provider, __provider: provider, source: item?.source || 'legacy' }))),
27
- ...acpSessions.map((item) => ({ ...item, provider: item.provider, __provider: item.provider, source: 'acp' }))
28
- ]
29
- .sort((a, b) => new Date(b.lastActivity || b.updated_at || b.createdAt || 0) - new Date(a.lastActivity || a.updated_at || a.createdAt || 0));
44
+ return mergeSessionLists(legacySessions, normalizedAcpSessions);
45
+ }
46
+
47
+ async function resolveProviderSessionRoute({
48
+ provider,
49
+ sessionId,
50
+ projectList = null
51
+ }) {
52
+ const normalizedProvider = String(provider || '').trim().toLowerCase();
53
+ const normalizedSessionId = String(sessionId || '').trim();
54
+
55
+ if (!normalizedProvider || !normalizedSessionId) {
56
+ return null;
57
+ }
58
+
59
+ const projects = Array.isArray(projectList) ? projectList : await getProjectsList();
60
+ const normalizeComparableProjectPath = (projectPath) => {
61
+ if (typeof projectPath !== 'string' || !projectPath.trim()) {
62
+ return '';
63
+ }
64
+
65
+ const withoutWindowsLongPathPrefix = projectPath.startsWith('\\\\?\\')
66
+ ? projectPath.slice(4)
67
+ : projectPath;
68
+
69
+ return path.normalize(withoutWindowsLongPathPrefix);
70
+ };
71
+
72
+ const findProjectByPath = (projectPath) => {
73
+ const normalizedProjectPath = normalizeComparableProjectPath(projectPath);
74
+ if (!normalizedProjectPath) {
75
+ return null;
76
+ }
77
+
78
+ return projects.find((project) => {
79
+ const candidatePath = project.fullPath || project.path || '';
80
+ return normalizeComparableProjectPath(candidatePath) === normalizedProjectPath;
81
+ }) || null;
82
+ };
83
+
84
+ const acpRecord = await findAcpSessionRecord(normalizedSessionId, normalizedProvider);
85
+ const acpProject = findProjectByPath(acpRecord?.projectPath || null);
86
+
87
+ let matchedProject = acpProject;
88
+ let resolvedSource = acpRecord ? 'acp' : 'legacy';
89
+
90
+ if (!matchedProject) {
91
+ switch (normalizedProvider) {
92
+ case 'claude': {
93
+ const metadata = await getClaudeSessionMetadata(normalizedSessionId);
94
+ matchedProject = findProjectByPath(metadata?.cwd || null);
95
+ break;
96
+ }
97
+ case 'codex': {
98
+ const metadata = await getCodexSessionMetadata(normalizedSessionId);
99
+ matchedProject = findProjectByPath(metadata?.cwd || null);
100
+ break;
101
+ }
102
+ case 'gemini': {
103
+ const metadata = await getGeminiSessionMetadata(normalizedSessionId);
104
+ const projectHash = String(metadata?.projectHash || '').trim();
105
+
106
+ if (projectHash) {
107
+ matchedProject = projects.find((project) => {
108
+ const projectPath = normalizeComparableProjectPath(project.fullPath || project.path || '');
109
+ if (!projectPath) {
110
+ return false;
111
+ }
112
+
113
+ const candidateHash = crypto.createHash('sha256').update(projectPath).digest('hex');
114
+ return candidateHash === projectHash;
115
+ }) || null;
116
+ }
117
+ break;
118
+ }
119
+ case 'opencode': {
120
+ const metadata = await getOpencodeSessionMetadata(normalizedSessionId);
121
+ matchedProject = findProjectByPath(metadata?.cwd || null);
122
+ break;
123
+ }
124
+ default:
125
+ break;
126
+ }
127
+ }
128
+
129
+ if (!matchedProject) {
130
+ return null;
131
+ }
132
+
133
+ const adapter = getProviderAdapter(normalizedProvider);
134
+ let sessions = [];
135
+
136
+ if (normalizedProvider === 'claude') {
137
+ sessions = await adapter.listSessions({
138
+ projectName: matchedProject.name,
139
+ projectPath: matchedProject.fullPath || matchedProject.path,
140
+ limit: 1000,
141
+ offset: 0
142
+ });
143
+ } else {
144
+ sessions = await adapter.listSessions({
145
+ projectPath: matchedProject.fullPath || matchedProject.path,
146
+ limit: 0
147
+ });
148
+ }
149
+
150
+ const matchedSession = (Array.isArray(sessions) ? sessions : []).find((session) => session.id === normalizedSessionId);
151
+ if (!matchedSession) {
152
+ return null;
153
+ }
154
+
155
+ return {
156
+ provider: normalizedProvider,
157
+ source: matchedSession.source || resolvedSource,
158
+ session: matchedSession,
159
+ project: {
160
+ name: matchedProject.name,
161
+ fullPath: matchedProject.fullPath || matchedProject.path,
162
+ path: matchedProject.path,
163
+ displayName: matchedProject.displayName || matchedProject.name
164
+ }
165
+ };
30
166
  }
31
167
 
32
168
  router.get('/providers', async (req, res) => {
@@ -68,98 +204,29 @@ router.get('/projects/:projectName/history-index', async (req, res) => {
68
204
  }
69
205
  });
70
206
 
71
- router.get('/sessions/:sessionId/resolve', async (req, res) => {
207
+ router.get('/sessions/:provider/:sessionId/resolve', async (req, res) => {
72
208
  try {
73
- const projects = await getProjects();
209
+ const requestedProvider = String(req.params.provider || '').trim().toLowerCase();
74
210
  const requestedSessionId = String(req.params.sessionId || '').trim();
75
- const providerHint = typeof req.query.provider === 'string' ? req.query.provider.trim().toLowerCase() : '';
76
-
77
- if (!requestedSessionId) {
78
- return res.status(400).json({ success: false, error: 'Session id is required' });
79
- }
80
211
 
81
- const providerOrder = [
82
- providerHint,
83
- 'codex',
84
- 'claude',
85
- 'gemini',
86
- 'opencode'
87
- ].filter((provider, index, all) => provider && all.indexOf(provider) === index);
88
-
89
- let directProjectMatch = null;
90
- let directMatchSessions = [];
91
-
92
- for (const project of projects) {
93
- const flattened = await flattenProjectSessions(project);
94
- if (flattened.some((session) => session.id === requestedSessionId)) {
95
- directProjectMatch = project;
96
- directMatchSessions = flattened;
97
- break;
98
- }
212
+ if (!requestedProvider || !requestedSessionId) {
213
+ return res.status(400).json({ success: false, error: 'provider and session id are required' });
99
214
  }
100
215
 
101
- if (directProjectMatch) {
102
- const matchedSession = directMatchSessions.find((session) => session.id === requestedSessionId);
103
-
104
- return res.json({
105
- success: true,
106
- found: true,
107
- provider: matchedSession.provider,
108
- source: matchedSession.source || 'legacy',
109
- session: matchedSession,
110
- project: {
111
- name: directProjectMatch.name,
112
- fullPath: directProjectMatch.fullPath || directProjectMatch.path,
113
- path: directProjectMatch.path,
114
- displayName: directProjectMatch.displayName || directProjectMatch.name
115
- }
116
- });
117
- }
118
-
119
- for (const provider of providerOrder) {
120
- const adapter = getProviderAdapter(provider);
121
-
122
- for (const project of projects) {
123
- let sessions = [];
124
-
125
- try {
126
- if (provider === 'claude') {
127
- const result = await adapter.listSessions({
128
- projectName: project.name,
129
- projectPath: project.fullPath || project.path,
130
- limit: 1000,
131
- offset: 0
132
- });
133
- sessions = Array.isArray(result) ? result : [];
134
- } else {
135
- sessions = await adapter.listSessions({ projectPath: project.fullPath || project.path, limit: 0 });
136
- }
137
- } catch (error) {
138
- continue;
139
- }
140
-
141
- const matchedSession = sessions.find((session) => session.id === requestedSessionId);
142
- if (!matchedSession) {
143
- continue;
144
- }
216
+ const result = await resolveProviderSessionRoute({
217
+ provider: requestedProvider,
218
+ sessionId: requestedSessionId
219
+ });
145
220
 
146
- return res.json({
147
- success: true,
148
- found: true,
149
- provider,
150
- source: matchedSession.source || 'legacy',
151
- session: matchedSession,
152
- project: {
153
- name: project.name,
154
- fullPath: project.fullPath || project.path,
155
- path: project.path,
156
- displayName: project.displayName || project.name
157
- }
158
- });
159
- }
221
+ if (!result) {
222
+ return res.status(404).json({ success: true, found: false });
160
223
  }
161
224
 
162
- res.status(404).json({ success: true, found: false });
225
+ res.json({
226
+ success: true,
227
+ found: true,
228
+ ...result
229
+ });
163
230
  } catch (error) {
164
231
  res.status(500).json({ success: false, error: error.message });
165
232
  }
@@ -208,4 +275,6 @@ router.get('/sessions/:provider/:sessionId/events', async (req, res) => {
208
275
  }
209
276
  });
210
277
 
278
+ export { flattenProjectSessions, resolveProviderSessionRoute };
279
+
211
280
  export default router;
@@ -2,40 +2,50 @@ import express from 'express';
2
2
  import { apiKeysDb, credentialsDb } from '../database/db.js';
3
3
 
4
4
  const router = express.Router();
5
-
6
- // ===============================
7
- // API Keys Management
8
- // ===============================
9
-
10
- // Get all API keys for the authenticated user
11
- router.get('/api-keys', async (req, res) => {
5
+ const DISALLOWED_CREDENTIAL_TYPES = new Set(['github_token']);
6
+
7
+ function parseNumericId(rawValue) {
8
+ const parsed = Number.parseInt(String(rawValue || ''), 10);
9
+ return Number.isFinite(parsed) ? parsed : null;
10
+ }
11
+
12
+ function sanitizeApiKeyRecord(record) {
13
+ const visiblePrefix = String(record?.api_key || '').slice(0, 10);
14
+ return {
15
+ ...record,
16
+ api_key: visiblePrefix ? `${visiblePrefix}...` : ''
17
+ };
18
+ }
19
+
20
+ function badRequest(res, error) {
21
+ return res.status(400).json({ error });
22
+ }
23
+
24
+ function notFound(res, error) {
25
+ return res.status(404).json({ error });
26
+ }
27
+
28
+ router.get('/api-keys', (req, res) => {
12
29
  try {
13
- const apiKeys = apiKeysDb.getApiKeys(req.user.id);
14
- // Don't send the full API key in the list for security
15
- const sanitizedKeys = apiKeys.map(key => ({
16
- ...key,
17
- api_key: key.api_key.substring(0, 10) + '...'
18
- }));
19
- res.json({ apiKeys: sanitizedKeys });
30
+ const apiKeys = apiKeysDb.getApiKeys(req.user.id).map(sanitizeApiKeyRecord);
31
+ res.json({ apiKeys });
20
32
  } catch (error) {
21
33
  console.error('Error fetching API keys:', error);
22
34
  res.status(500).json({ error: 'Failed to fetch API keys' });
23
35
  }
24
36
  });
25
37
 
26
- // Create a new API key
27
- router.post('/api-keys', async (req, res) => {
28
- try {
29
- const { keyName } = req.body;
30
-
31
- if (!keyName || !keyName.trim()) {
32
- return res.status(400).json({ error: 'Key name is required' });
33
- }
38
+ router.post('/api-keys', (req, res) => {
39
+ const keyName = String(req.body?.keyName || '').trim();
40
+ if (!keyName) {
41
+ return badRequest(res, 'Key name is required');
42
+ }
34
43
 
35
- const result = apiKeysDb.createApiKey(req.user.id, keyName.trim());
44
+ try {
45
+ const apiKey = apiKeysDb.createApiKey(req.user.id, keyName);
36
46
  res.json({
37
47
  success: true,
38
- apiKey: result
48
+ apiKey
39
49
  });
40
50
  } catch (error) {
41
51
  console.error('Error creating API key:', error);
@@ -43,56 +53,54 @@ router.post('/api-keys', async (req, res) => {
43
53
  }
44
54
  });
45
55
 
46
- // Delete an API key
47
- router.delete('/api-keys/:keyId', async (req, res) => {
48
- try {
49
- const { keyId } = req.params;
50
- const success = apiKeysDb.deleteApiKey(req.user.id, parseInt(keyId));
56
+ router.delete('/api-keys/:keyId', (req, res) => {
57
+ const keyId = parseNumericId(req.params.keyId);
58
+ if (keyId === null) {
59
+ return badRequest(res, 'Invalid API key id');
60
+ }
51
61
 
52
- if (success) {
53
- res.json({ success: true });
54
- } else {
55
- res.status(404).json({ error: 'API key not found' });
62
+ try {
63
+ const removed = apiKeysDb.deleteApiKey(req.user.id, keyId);
64
+ if (!removed) {
65
+ return notFound(res, 'API key not found');
56
66
  }
67
+
68
+ res.json({ success: true });
57
69
  } catch (error) {
58
70
  console.error('Error deleting API key:', error);
59
71
  res.status(500).json({ error: 'Failed to delete API key' });
60
72
  }
61
73
  });
62
74
 
63
- // Toggle API key active status
64
- router.patch('/api-keys/:keyId/toggle', async (req, res) => {
65
- try {
66
- const { keyId } = req.params;
67
- const { isActive } = req.body;
75
+ router.patch('/api-keys/:keyId/toggle', (req, res) => {
76
+ const keyId = parseNumericId(req.params.keyId);
77
+ const isActive = req.body?.isActive;
68
78
 
69
- if (typeof isActive !== 'boolean') {
70
- return res.status(400).json({ error: 'isActive must be a boolean' });
71
- }
79
+ if (keyId === null) {
80
+ return badRequest(res, 'Invalid API key id');
81
+ }
72
82
 
73
- const success = apiKeysDb.toggleApiKey(req.user.id, parseInt(keyId), isActive);
83
+ if (typeof isActive !== 'boolean') {
84
+ return badRequest(res, 'isActive must be a boolean');
85
+ }
74
86
 
75
- if (success) {
76
- res.json({ success: true });
77
- } else {
78
- res.status(404).json({ error: 'API key not found' });
87
+ try {
88
+ const updated = apiKeysDb.toggleApiKey(req.user.id, keyId, isActive);
89
+ if (!updated) {
90
+ return notFound(res, 'API key not found');
79
91
  }
92
+
93
+ res.json({ success: true });
80
94
  } catch (error) {
81
95
  console.error('Error toggling API key:', error);
82
96
  res.status(500).json({ error: 'Failed to toggle API key' });
83
97
  }
84
98
  });
85
99
 
86
- // ===============================
87
- // Generic Credentials Management
88
- // ===============================
89
-
90
- // Get all credentials for the authenticated user (optionally filtered by type)
91
- router.get('/credentials', async (req, res) => {
100
+ router.get('/credentials', (req, res) => {
92
101
  try {
93
- const { type } = req.query;
94
- const credentials = credentialsDb.getCredentials(req.user.id, type || null);
95
- // Don't send the actual credential values for security
102
+ const credentialType = typeof req.query.type === 'string' ? req.query.type.trim() : null;
103
+ const credentials = credentialsDb.getCredentials(req.user.id, credentialType || null);
96
104
  res.json({ credentials });
97
105
  } catch (error) {
98
106
  console.error('Error fetching credentials:', error);
@@ -100,38 +108,40 @@ router.get('/credentials', async (req, res) => {
100
108
  }
101
109
  });
102
110
 
103
- // Create a new credential
104
- router.post('/credentials', async (req, res) => {
105
- try {
106
- const { credentialName, credentialType, credentialValue, description } = req.body;
111
+ router.post('/credentials', (req, res) => {
112
+ const credentialName = String(req.body?.credentialName || '').trim();
113
+ const credentialType = String(req.body?.credentialType || '').trim();
114
+ const credentialValue = String(req.body?.credentialValue || '').trim();
115
+ const description = String(req.body?.description || '').trim();
107
116
 
108
- if (!credentialName || !credentialName.trim()) {
109
- return res.status(400).json({ error: 'Credential name is required' });
110
- }
117
+ if (!credentialName) {
118
+ return badRequest(res, 'Credential name is required');
119
+ }
111
120
 
112
- if (!credentialType || !credentialType.trim()) {
113
- return res.status(400).json({ error: 'Credential type is required' });
114
- }
121
+ if (!credentialType) {
122
+ return badRequest(res, 'Credential type is required');
123
+ }
115
124
 
116
- if (!credentialValue || !credentialValue.trim()) {
117
- return res.status(400).json({ error: 'Credential value is required' });
118
- }
125
+ if (!credentialValue) {
126
+ return badRequest(res, 'Credential value is required');
127
+ }
119
128
 
120
- if (credentialType.trim() === 'github_token') {
121
- return res.status(400).json({ error: 'GitHub tokens are no longer supported.' });
122
- }
129
+ if (DISALLOWED_CREDENTIAL_TYPES.has(credentialType.toLowerCase())) {
130
+ return badRequest(res, 'GitHub tokens are no longer supported.');
131
+ }
123
132
 
124
- const result = credentialsDb.createCredential(
133
+ try {
134
+ const credential = credentialsDb.createCredential(
125
135
  req.user.id,
126
- credentialName.trim(),
127
- credentialType.trim(),
128
- credentialValue.trim(),
129
- description?.trim() || null
136
+ credentialName,
137
+ credentialType,
138
+ credentialValue,
139
+ description || null
130
140
  );
131
141
 
132
142
  res.json({
133
143
  success: true,
134
- credential: result
144
+ credential
135
145
  });
136
146
  } catch (error) {
137
147
  console.error('Error creating credential:', error);
@@ -139,40 +149,44 @@ router.post('/credentials', async (req, res) => {
139
149
  }
140
150
  });
141
151
 
142
- // Delete a credential
143
- router.delete('/credentials/:credentialId', async (req, res) => {
144
- try {
145
- const { credentialId } = req.params;
146
- const success = credentialsDb.deleteCredential(req.user.id, parseInt(credentialId));
152
+ router.delete('/credentials/:credentialId', (req, res) => {
153
+ const credentialId = parseNumericId(req.params.credentialId);
154
+ if (credentialId === null) {
155
+ return badRequest(res, 'Invalid credential id');
156
+ }
147
157
 
148
- if (success) {
149
- res.json({ success: true });
150
- } else {
151
- res.status(404).json({ error: 'Credential not found' });
158
+ try {
159
+ const removed = credentialsDb.deleteCredential(req.user.id, credentialId);
160
+ if (!removed) {
161
+ return notFound(res, 'Credential not found');
152
162
  }
163
+
164
+ res.json({ success: true });
153
165
  } catch (error) {
154
166
  console.error('Error deleting credential:', error);
155
167
  res.status(500).json({ error: 'Failed to delete credential' });
156
168
  }
157
169
  });
158
170
 
159
- // Toggle credential active status
160
- router.patch('/credentials/:credentialId/toggle', async (req, res) => {
161
- try {
162
- const { credentialId } = req.params;
163
- const { isActive } = req.body;
171
+ router.patch('/credentials/:credentialId/toggle', (req, res) => {
172
+ const credentialId = parseNumericId(req.params.credentialId);
173
+ const isActive = req.body?.isActive;
164
174
 
165
- if (typeof isActive !== 'boolean') {
166
- return res.status(400).json({ error: 'isActive must be a boolean' });
167
- }
175
+ if (credentialId === null) {
176
+ return badRequest(res, 'Invalid credential id');
177
+ }
168
178
 
169
- const success = credentialsDb.toggleCredential(req.user.id, parseInt(credentialId), isActive);
179
+ if (typeof isActive !== 'boolean') {
180
+ return badRequest(res, 'isActive must be a boolean');
181
+ }
170
182
 
171
- if (success) {
172
- res.json({ success: true });
173
- } else {
174
- res.status(404).json({ error: 'Credential not found' });
183
+ try {
184
+ const updated = credentialsDb.toggleCredential(req.user.id, credentialId, isActive);
185
+ if (!updated) {
186
+ return notFound(res, 'Credential not found');
175
187
  }
188
+
189
+ res.json({ success: true });
176
190
  } catch (error) {
177
191
  console.error('Error toggling credential:', error);
178
192
  res.status(500).json({ error: 'Failed to toggle credential' });