@epiphytic/claudecodeui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/LICENSE +675 -0
  2. package/README.md +414 -0
  3. package/dist/api-docs.html +879 -0
  4. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  5. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  6. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  7. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  8. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  9. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  10. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  11. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  12. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  13. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  14. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  15. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  16. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  17. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  18. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  19. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  20. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  21. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  22. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  23. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  24. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  25. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  26. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  27. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  28. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  29. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  30. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  31. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  32. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  33. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  34. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  35. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  36. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  37. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  38. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  39. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  40. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  41. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  45. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  46. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  47. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  48. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  49. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  50. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  51. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  52. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  53. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  54. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  55. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  56. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  57. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  58. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  59. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  60. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  61. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  62. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  63. package/dist/assets/index-DfR9xEkp.css +32 -0
  64. package/dist/assets/index-DvlVn6Eb.js +1231 -0
  65. package/dist/assets/vendor-codemirror-CJLzwpLB.js +39 -0
  66. package/dist/assets/vendor-react-DcyRfQm3.js +59 -0
  67. package/dist/assets/vendor-xterm-DfaPXD3y.js +66 -0
  68. package/dist/clear-cache.html +85 -0
  69. package/dist/convert-icons.md +53 -0
  70. package/dist/favicon.png +0 -0
  71. package/dist/favicon.svg +9 -0
  72. package/dist/generate-icons.js +49 -0
  73. package/dist/icons/claude-ai-icon.svg +1 -0
  74. package/dist/icons/codex-white.svg +3 -0
  75. package/dist/icons/codex.svg +3 -0
  76. package/dist/icons/cursor-white.svg +12 -0
  77. package/dist/icons/cursor.svg +1 -0
  78. package/dist/icons/generate-icons.md +19 -0
  79. package/dist/icons/icon-128x128.png +0 -0
  80. package/dist/icons/icon-128x128.svg +12 -0
  81. package/dist/icons/icon-144x144.png +0 -0
  82. package/dist/icons/icon-144x144.svg +12 -0
  83. package/dist/icons/icon-152x152.png +0 -0
  84. package/dist/icons/icon-152x152.svg +12 -0
  85. package/dist/icons/icon-192x192.png +0 -0
  86. package/dist/icons/icon-192x192.svg +12 -0
  87. package/dist/icons/icon-384x384.png +0 -0
  88. package/dist/icons/icon-384x384.svg +12 -0
  89. package/dist/icons/icon-512x512.png +0 -0
  90. package/dist/icons/icon-512x512.svg +12 -0
  91. package/dist/icons/icon-72x72.png +0 -0
  92. package/dist/icons/icon-72x72.svg +12 -0
  93. package/dist/icons/icon-96x96.png +0 -0
  94. package/dist/icons/icon-96x96.svg +12 -0
  95. package/dist/icons/icon-template.svg +12 -0
  96. package/dist/index.html +52 -0
  97. package/dist/logo-128.png +0 -0
  98. package/dist/logo-256.png +0 -0
  99. package/dist/logo-32.png +0 -0
  100. package/dist/logo-512.png +0 -0
  101. package/dist/logo-64.png +0 -0
  102. package/dist/logo.svg +17 -0
  103. package/dist/manifest.json +61 -0
  104. package/dist/screenshots/cli-selection.png +0 -0
  105. package/dist/screenshots/desktop-main.png +0 -0
  106. package/dist/screenshots/mobile-chat.png +0 -0
  107. package/dist/screenshots/tools-modal.png +0 -0
  108. package/dist/sw.js +107 -0
  109. package/package.json +120 -0
  110. package/server/claude-sdk.js +721 -0
  111. package/server/cli.js +469 -0
  112. package/server/cursor-cli.js +267 -0
  113. package/server/database/db.js +554 -0
  114. package/server/database/init.sql +54 -0
  115. package/server/index.js +2120 -0
  116. package/server/middleware/auth.js +161 -0
  117. package/server/openai-codex.js +389 -0
  118. package/server/orchestrator/client.js +989 -0
  119. package/server/orchestrator/github-auth.js +308 -0
  120. package/server/orchestrator/index.js +216 -0
  121. package/server/orchestrator/protocol.js +299 -0
  122. package/server/orchestrator/proxy.js +364 -0
  123. package/server/orchestrator/status-tracker.js +226 -0
  124. package/server/projects.js +1604 -0
  125. package/server/routes/agent.js +1230 -0
  126. package/server/routes/auth.js +135 -0
  127. package/server/routes/cli-auth.js +341 -0
  128. package/server/routes/codex.js +345 -0
  129. package/server/routes/commands.js +521 -0
  130. package/server/routes/cursor.js +795 -0
  131. package/server/routes/git.js +1128 -0
  132. package/server/routes/mcp-utils.js +48 -0
  133. package/server/routes/mcp.js +650 -0
  134. package/server/routes/projects.js +378 -0
  135. package/server/routes/settings.js +178 -0
  136. package/server/routes/taskmaster.js +1963 -0
  137. package/server/routes/user.js +106 -0
  138. package/server/utils/commandParser.js +303 -0
  139. package/server/utils/gitConfig.js +24 -0
  140. package/server/utils/mcp-detector.js +198 -0
  141. package/server/utils/taskmaster-websocket.js +129 -0
  142. package/shared/modelConstants.js +65 -0
@@ -0,0 +1,554 @@
1
+ import Database from "better-sqlite3";
2
+ import path from "path";
3
+ import fs from "fs";
4
+ import crypto from "crypto";
5
+ import bcrypt from "bcrypt";
6
+ import { fileURLToPath } from "url";
7
+ import { dirname } from "path";
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+
12
+ // ANSI color codes for terminal output
13
+ const colors = {
14
+ reset: "\x1b[0m",
15
+ bright: "\x1b[1m",
16
+ cyan: "\x1b[36m",
17
+ dim: "\x1b[2m",
18
+ };
19
+
20
+ const c = {
21
+ info: (text) => `${colors.cyan}${text}${colors.reset}`,
22
+ bright: (text) => `${colors.bright}${text}${colors.reset}`,
23
+ dim: (text) => `${colors.dim}${text}${colors.reset}`,
24
+ };
25
+
26
+ // Use DATABASE_PATH environment variable if set, otherwise use default location
27
+ const DB_PATH = process.env.DATABASE_PATH || path.join(__dirname, "auth.db");
28
+ const INIT_SQL_PATH = path.join(__dirname, "init.sql");
29
+
30
+ // Ensure database directory exists if custom path is provided
31
+ if (process.env.DATABASE_PATH) {
32
+ const dbDir = path.dirname(DB_PATH);
33
+ try {
34
+ if (!fs.existsSync(dbDir)) {
35
+ fs.mkdirSync(dbDir, { recursive: true });
36
+ console.log(`Created database directory: ${dbDir}`);
37
+ }
38
+ } catch (error) {
39
+ console.error(
40
+ `Failed to create database directory ${dbDir}:`,
41
+ error.message,
42
+ );
43
+ throw error;
44
+ }
45
+ }
46
+
47
+ // Create database connection
48
+ const db = new Database(DB_PATH);
49
+
50
+ // Show app installation path prominently
51
+ const appInstallPath = path.join(__dirname, "../..");
52
+ console.log("");
53
+ console.log(c.dim("═".repeat(60)));
54
+ console.log(
55
+ `${c.info("[INFO]")} App Installation: ${c.bright(appInstallPath)}`,
56
+ );
57
+ console.log(
58
+ `${c.info("[INFO]")} Database: ${c.dim(path.relative(appInstallPath, DB_PATH))}`,
59
+ );
60
+ if (process.env.DATABASE_PATH) {
61
+ console.log(
62
+ ` ${c.dim("(Using custom DATABASE_PATH from environment)")}`,
63
+ );
64
+ }
65
+ console.log(c.dim("═".repeat(60)));
66
+ console.log("");
67
+
68
+ const runMigrations = () => {
69
+ try {
70
+ const tableInfo = db.prepare("PRAGMA table_info(users)").all();
71
+ const columnNames = tableInfo.map((col) => col.name);
72
+
73
+ if (!columnNames.includes("git_name")) {
74
+ console.log("Running migration: Adding git_name column");
75
+ db.exec("ALTER TABLE users ADD COLUMN git_name TEXT");
76
+ }
77
+
78
+ if (!columnNames.includes("git_email")) {
79
+ console.log("Running migration: Adding git_email column");
80
+ db.exec("ALTER TABLE users ADD COLUMN git_email TEXT");
81
+ }
82
+
83
+ if (!columnNames.includes("has_completed_onboarding")) {
84
+ console.log("Running migration: Adding has_completed_onboarding column");
85
+ db.exec(
86
+ "ALTER TABLE users ADD COLUMN has_completed_onboarding BOOLEAN DEFAULT 0",
87
+ );
88
+ }
89
+
90
+ if (!columnNames.includes("github_id")) {
91
+ console.log("Running migration: Adding github_id column");
92
+ // SQLite doesn't allow adding UNIQUE columns directly with ALTER TABLE
93
+ // Add column first, then create unique index
94
+ db.exec("ALTER TABLE users ADD COLUMN github_id TEXT");
95
+ db.exec(
96
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_users_github_id ON users(github_id)",
97
+ );
98
+ }
99
+
100
+ console.log("Database migrations completed successfully");
101
+ } catch (error) {
102
+ console.error("Error running migrations:", error.message);
103
+ throw error;
104
+ }
105
+ };
106
+
107
+ // Initialize database with schema
108
+ const initializeDatabase = async () => {
109
+ try {
110
+ const initSQL = fs.readFileSync(INIT_SQL_PATH, "utf8");
111
+ db.exec(initSQL);
112
+ console.log("Database initialized successfully");
113
+ runMigrations();
114
+ } catch (error) {
115
+ console.error("Error initializing database:", error.message);
116
+ throw error;
117
+ }
118
+ };
119
+
120
+ // User database operations
121
+ const userDb = {
122
+ // Check if any users exist
123
+ hasUsers: () => {
124
+ try {
125
+ const row = db.prepare("SELECT COUNT(*) as count FROM users").get();
126
+ return row.count > 0;
127
+ } catch (err) {
128
+ throw err;
129
+ }
130
+ },
131
+
132
+ // Create a new user
133
+ createUser: (username, passwordHash) => {
134
+ try {
135
+ const stmt = db.prepare(
136
+ "INSERT INTO users (username, password_hash) VALUES (?, ?)",
137
+ );
138
+ const result = stmt.run(username, passwordHash);
139
+ return { id: result.lastInsertRowid, username };
140
+ } catch (err) {
141
+ throw err;
142
+ }
143
+ },
144
+
145
+ // Get user by username
146
+ getUserByUsername: (username) => {
147
+ try {
148
+ const row = db
149
+ .prepare("SELECT * FROM users WHERE username = ? AND is_active = 1")
150
+ .get(username);
151
+ return row;
152
+ } catch (err) {
153
+ throw err;
154
+ }
155
+ },
156
+
157
+ // Update last login time
158
+ updateLastLogin: (userId) => {
159
+ try {
160
+ db.prepare(
161
+ "UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?",
162
+ ).run(userId);
163
+ } catch (err) {
164
+ throw err;
165
+ }
166
+ },
167
+
168
+ // Get user by ID
169
+ getUserById: (userId) => {
170
+ try {
171
+ const row = db
172
+ .prepare(
173
+ "SELECT id, username, created_at, last_login FROM users WHERE id = ? AND is_active = 1",
174
+ )
175
+ .get(userId);
176
+ return row;
177
+ } catch (err) {
178
+ throw err;
179
+ }
180
+ },
181
+
182
+ getFirstUser: () => {
183
+ try {
184
+ const row = db
185
+ .prepare(
186
+ "SELECT id, username, created_at, last_login FROM users WHERE is_active = 1 LIMIT 1",
187
+ )
188
+ .get();
189
+ return row;
190
+ } catch (err) {
191
+ throw err;
192
+ }
193
+ },
194
+
195
+ updateGitConfig: (userId, gitName, gitEmail) => {
196
+ try {
197
+ const stmt = db.prepare(
198
+ "UPDATE users SET git_name = ?, git_email = ? WHERE id = ?",
199
+ );
200
+ stmt.run(gitName, gitEmail, userId);
201
+ } catch (err) {
202
+ throw err;
203
+ }
204
+ },
205
+
206
+ getGitConfig: (userId) => {
207
+ try {
208
+ const row = db
209
+ .prepare("SELECT git_name, git_email FROM users WHERE id = ?")
210
+ .get(userId);
211
+ return row;
212
+ } catch (err) {
213
+ throw err;
214
+ }
215
+ },
216
+
217
+ completeOnboarding: (userId) => {
218
+ try {
219
+ const stmt = db.prepare(
220
+ "UPDATE users SET has_completed_onboarding = 1 WHERE id = ?",
221
+ );
222
+ stmt.run(userId);
223
+ } catch (err) {
224
+ throw err;
225
+ }
226
+ },
227
+
228
+ hasCompletedOnboarding: (userId) => {
229
+ try {
230
+ const row = db
231
+ .prepare("SELECT has_completed_onboarding FROM users WHERE id = ?")
232
+ .get(userId);
233
+ return row?.has_completed_onboarding === 1;
234
+ } catch (err) {
235
+ throw err;
236
+ }
237
+ },
238
+
239
+ // Get or create user for orchestrator authentication
240
+ // When connecting via orchestrator proxy, auto-create user if needed
241
+ getOrCreateOrchestratorUser: async (githubId, githubUsername) => {
242
+ try {
243
+ // Validate required parameters
244
+ if (
245
+ !githubUsername ||
246
+ typeof githubUsername !== "string" ||
247
+ !githubUsername.trim()
248
+ ) {
249
+ throw new Error(
250
+ "githubUsername is required and must be a non-empty string",
251
+ );
252
+ }
253
+ if (!githubId || typeof githubId !== "string" || !githubId.trim()) {
254
+ throw new Error("githubId is required and must be a non-empty string");
255
+ }
256
+
257
+ // First, try to find user by github_id (most authoritative)
258
+ let user = db
259
+ .prepare(
260
+ "SELECT id, username, github_id, created_at, last_login FROM users WHERE github_id = ? AND is_active = 1",
261
+ )
262
+ .get(githubId);
263
+
264
+ if (user) {
265
+ // Found user by github_id, update last_login
266
+ db.prepare(
267
+ "UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?",
268
+ ).run(user.id);
269
+ return user;
270
+ }
271
+
272
+ // No user found by github_id, check if username exists
273
+ const existingByUsername = db
274
+ .prepare(
275
+ "SELECT id, username, github_id, created_at, last_login FROM users WHERE username = ? AND is_active = 1",
276
+ )
277
+ .get(githubUsername);
278
+
279
+ if (existingByUsername) {
280
+ // Username exists - check if it has a different github_id
281
+ if (
282
+ existingByUsername.github_id &&
283
+ existingByUsername.github_id !== githubId
284
+ ) {
285
+ // Username collision: local account has different github_id
286
+ // Create namespaced orchestrator user instead of reusing
287
+ const namespacedUsername = `orch_${githubId}_${githubUsername}`;
288
+ const randomPassword = crypto.randomBytes(32).toString("hex");
289
+ const passwordHash = await bcrypt.hash(randomPassword, 12);
290
+
291
+ const stmt = db.prepare(
292
+ "INSERT INTO users (username, password_hash, github_id, has_completed_onboarding, last_login) VALUES (?, ?, ?, 1, CURRENT_TIMESTAMP)",
293
+ );
294
+ const result = stmt.run(namespacedUsername, passwordHash, githubId);
295
+ user = {
296
+ id: result.lastInsertRowid,
297
+ username: namespacedUsername,
298
+ github_id: githubId,
299
+ };
300
+ console.log(
301
+ `[ORCHESTRATOR] Created namespaced user for orchestrator auth: ${namespacedUsername} (GitHub ID: ${githubId})`,
302
+ );
303
+ return user;
304
+ }
305
+
306
+ // Username exists with no github_id or matching github_id - bind the github_id
307
+ db.prepare(
308
+ "UPDATE users SET github_id = ?, last_login = CURRENT_TIMESTAMP WHERE id = ?",
309
+ ).run(githubId, existingByUsername.id);
310
+ console.log(
311
+ `[ORCHESTRATOR] Bound github_id ${githubId} to existing user: ${githubUsername}`,
312
+ );
313
+ return { ...existingByUsername, github_id: githubId };
314
+ }
315
+
316
+ // No existing user found, create new one
317
+ const randomPassword = crypto.randomBytes(32).toString("hex");
318
+ const passwordHash = await bcrypt.hash(randomPassword, 12);
319
+
320
+ const stmt = db.prepare(
321
+ "INSERT INTO users (username, password_hash, github_id, has_completed_onboarding, last_login) VALUES (?, ?, ?, 1, CURRENT_TIMESTAMP)",
322
+ );
323
+ const result = stmt.run(githubUsername, passwordHash, githubId);
324
+ user = {
325
+ id: result.lastInsertRowid,
326
+ username: githubUsername,
327
+ github_id: githubId,
328
+ };
329
+ console.log(
330
+ `[ORCHESTRATOR] Created new user for orchestrator auth: ${githubUsername} (GitHub ID: ${githubId})`,
331
+ );
332
+
333
+ return user;
334
+ } catch (err) {
335
+ console.error(
336
+ "[ORCHESTRATOR] Error in getOrCreateOrchestratorUser:",
337
+ err,
338
+ );
339
+ throw err;
340
+ }
341
+ },
342
+ };
343
+
344
+ // API Keys database operations
345
+ const apiKeysDb = {
346
+ // Generate a new API key
347
+ generateApiKey: () => {
348
+ return "ck_" + crypto.randomBytes(32).toString("hex");
349
+ },
350
+
351
+ // Create a new API key
352
+ createApiKey: (userId, keyName) => {
353
+ try {
354
+ const apiKey = apiKeysDb.generateApiKey();
355
+ const stmt = db.prepare(
356
+ "INSERT INTO api_keys (user_id, key_name, api_key) VALUES (?, ?, ?)",
357
+ );
358
+ const result = stmt.run(userId, keyName, apiKey);
359
+ return { id: result.lastInsertRowid, keyName, apiKey };
360
+ } catch (err) {
361
+ throw err;
362
+ }
363
+ },
364
+
365
+ // Get all API keys for a user
366
+ getApiKeys: (userId) => {
367
+ try {
368
+ const rows = db
369
+ .prepare(
370
+ "SELECT id, key_name, api_key, created_at, last_used, is_active FROM api_keys WHERE user_id = ? ORDER BY created_at DESC",
371
+ )
372
+ .all(userId);
373
+ return rows;
374
+ } catch (err) {
375
+ throw err;
376
+ }
377
+ },
378
+
379
+ // Validate API key and get user
380
+ validateApiKey: (apiKey) => {
381
+ try {
382
+ const row = db
383
+ .prepare(
384
+ `
385
+ SELECT u.id, u.username, ak.id as api_key_id
386
+ FROM api_keys ak
387
+ JOIN users u ON ak.user_id = u.id
388
+ WHERE ak.api_key = ? AND ak.is_active = 1 AND u.is_active = 1
389
+ `,
390
+ )
391
+ .get(apiKey);
392
+
393
+ if (row) {
394
+ // Update last_used timestamp
395
+ db.prepare(
396
+ "UPDATE api_keys SET last_used = CURRENT_TIMESTAMP WHERE id = ?",
397
+ ).run(row.api_key_id);
398
+ }
399
+
400
+ return row;
401
+ } catch (err) {
402
+ throw err;
403
+ }
404
+ },
405
+
406
+ // Delete an API key
407
+ deleteApiKey: (userId, apiKeyId) => {
408
+ try {
409
+ const stmt = db.prepare(
410
+ "DELETE FROM api_keys WHERE id = ? AND user_id = ?",
411
+ );
412
+ const result = stmt.run(apiKeyId, userId);
413
+ return result.changes > 0;
414
+ } catch (err) {
415
+ throw err;
416
+ }
417
+ },
418
+
419
+ // Toggle API key active status
420
+ toggleApiKey: (userId, apiKeyId, isActive) => {
421
+ try {
422
+ const stmt = db.prepare(
423
+ "UPDATE api_keys SET is_active = ? WHERE id = ? AND user_id = ?",
424
+ );
425
+ const result = stmt.run(isActive ? 1 : 0, apiKeyId, userId);
426
+ return result.changes > 0;
427
+ } catch (err) {
428
+ throw err;
429
+ }
430
+ },
431
+ };
432
+
433
+ // User credentials database operations (for GitHub tokens, GitLab tokens, etc.)
434
+ const credentialsDb = {
435
+ // Create a new credential
436
+ createCredential: (
437
+ userId,
438
+ credentialName,
439
+ credentialType,
440
+ credentialValue,
441
+ description = null,
442
+ ) => {
443
+ try {
444
+ const stmt = db.prepare(
445
+ "INSERT INTO user_credentials (user_id, credential_name, credential_type, credential_value, description) VALUES (?, ?, ?, ?, ?)",
446
+ );
447
+ const result = stmt.run(
448
+ userId,
449
+ credentialName,
450
+ credentialType,
451
+ credentialValue,
452
+ description,
453
+ );
454
+ return { id: result.lastInsertRowid, credentialName, credentialType };
455
+ } catch (err) {
456
+ throw err;
457
+ }
458
+ },
459
+
460
+ // Get all credentials for a user, optionally filtered by type
461
+ getCredentials: (userId, credentialType = null) => {
462
+ try {
463
+ let query =
464
+ "SELECT id, credential_name, credential_type, description, created_at, is_active FROM user_credentials WHERE user_id = ?";
465
+ const params = [userId];
466
+
467
+ if (credentialType) {
468
+ query += " AND credential_type = ?";
469
+ params.push(credentialType);
470
+ }
471
+
472
+ query += " ORDER BY created_at DESC";
473
+
474
+ const rows = db.prepare(query).all(...params);
475
+ return rows;
476
+ } catch (err) {
477
+ throw err;
478
+ }
479
+ },
480
+
481
+ // Get active credential value for a user by type (returns most recent active)
482
+ getActiveCredential: (userId, credentialType) => {
483
+ try {
484
+ const row = db
485
+ .prepare(
486
+ "SELECT credential_value FROM user_credentials WHERE user_id = ? AND credential_type = ? AND is_active = 1 ORDER BY created_at DESC LIMIT 1",
487
+ )
488
+ .get(userId, credentialType);
489
+ return row?.credential_value || null;
490
+ } catch (err) {
491
+ throw err;
492
+ }
493
+ },
494
+
495
+ // Delete a credential
496
+ deleteCredential: (userId, credentialId) => {
497
+ try {
498
+ const stmt = db.prepare(
499
+ "DELETE FROM user_credentials WHERE id = ? AND user_id = ?",
500
+ );
501
+ const result = stmt.run(credentialId, userId);
502
+ return result.changes > 0;
503
+ } catch (err) {
504
+ throw err;
505
+ }
506
+ },
507
+
508
+ // Toggle credential active status
509
+ toggleCredential: (userId, credentialId, isActive) => {
510
+ try {
511
+ const stmt = db.prepare(
512
+ "UPDATE user_credentials SET is_active = ? WHERE id = ? AND user_id = ?",
513
+ );
514
+ const result = stmt.run(isActive ? 1 : 0, credentialId, userId);
515
+ return result.changes > 0;
516
+ } catch (err) {
517
+ throw err;
518
+ }
519
+ },
520
+ };
521
+
522
+ // Backward compatibility - keep old names pointing to new system
523
+ const githubTokensDb = {
524
+ createGithubToken: (userId, tokenName, githubToken, description = null) => {
525
+ return credentialsDb.createCredential(
526
+ userId,
527
+ tokenName,
528
+ "github_token",
529
+ githubToken,
530
+ description,
531
+ );
532
+ },
533
+ getGithubTokens: (userId) => {
534
+ return credentialsDb.getCredentials(userId, "github_token");
535
+ },
536
+ getActiveGithubToken: (userId) => {
537
+ return credentialsDb.getActiveCredential(userId, "github_token");
538
+ },
539
+ deleteGithubToken: (userId, tokenId) => {
540
+ return credentialsDb.deleteCredential(userId, tokenId);
541
+ },
542
+ toggleGithubToken: (userId, tokenId, isActive) => {
543
+ return credentialsDb.toggleCredential(userId, tokenId, isActive);
544
+ },
545
+ };
546
+
547
+ export {
548
+ db,
549
+ initializeDatabase,
550
+ userDb,
551
+ apiKeysDb,
552
+ credentialsDb,
553
+ githubTokensDb, // Backward compatibility
554
+ };
@@ -0,0 +1,54 @@
1
+ -- Initialize authentication database
2
+ PRAGMA foreign_keys = ON;
3
+
4
+ -- Users table (single user system)
5
+ CREATE TABLE IF NOT EXISTS users (
6
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
7
+ username TEXT UNIQUE NOT NULL,
8
+ password_hash TEXT NOT NULL,
9
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
10
+ last_login DATETIME,
11
+ is_active BOOLEAN DEFAULT 1,
12
+ git_name TEXT,
13
+ git_email TEXT,
14
+ has_completed_onboarding BOOLEAN DEFAULT 0,
15
+ github_id TEXT UNIQUE
16
+ );
17
+
18
+ -- Indexes for performance
19
+ CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
20
+ CREATE INDEX IF NOT EXISTS idx_users_active ON users(is_active);
21
+ -- Note: idx_users_github_id is created by migration in db.js for existing databases
22
+
23
+ -- API Keys table for external API access
24
+ CREATE TABLE IF NOT EXISTS api_keys (
25
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
26
+ user_id INTEGER NOT NULL,
27
+ key_name TEXT NOT NULL,
28
+ api_key TEXT UNIQUE NOT NULL,
29
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
30
+ last_used DATETIME,
31
+ is_active BOOLEAN DEFAULT 1,
32
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
33
+ );
34
+
35
+ CREATE INDEX IF NOT EXISTS idx_api_keys_key ON api_keys(api_key);
36
+ CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id);
37
+ CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(is_active);
38
+
39
+ -- User credentials table for storing various tokens/credentials (GitHub, GitLab, etc.)
40
+ CREATE TABLE IF NOT EXISTS user_credentials (
41
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
42
+ user_id INTEGER NOT NULL,
43
+ credential_name TEXT NOT NULL,
44
+ credential_type TEXT NOT NULL, -- 'github_token', 'gitlab_token', 'bitbucket_token', etc.
45
+ credential_value TEXT NOT NULL,
46
+ description TEXT,
47
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
48
+ is_active BOOLEAN DEFAULT 1,
49
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
50
+ );
51
+
52
+ CREATE INDEX IF NOT EXISTS idx_user_credentials_user_id ON user_credentials(user_id);
53
+ CREATE INDEX IF NOT EXISTS idx_user_credentials_type ON user_credentials(credential_type);
54
+ CREATE INDEX IF NOT EXISTS idx_user_credentials_active ON user_credentials(is_active);