@cloudcli-ai/cloudcli 1.30.0 → 1.31.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 (237) hide show
  1. package/dist/assets/{index-DI22MZMb.css → index-DF8TTVHl.css} +1 -1
  2. package/dist/assets/index-DbZAcc9z.js +1262 -0
  3. package/dist/index.html +2 -2
  4. package/dist-server/server/index.js +115 -955
  5. package/dist-server/server/index.js.map +1 -1
  6. package/dist-server/server/middleware/auth.js +1 -1
  7. package/dist-server/server/middleware/auth.js.map +1 -1
  8. package/dist-server/server/modules/database/connection.js +125 -0
  9. package/dist-server/server/modules/database/connection.js.map +1 -0
  10. package/dist-server/server/modules/database/index.js +13 -0
  11. package/dist-server/server/modules/database/index.js.map +1 -0
  12. package/dist-server/server/modules/database/init-db.js +18 -0
  13. package/dist-server/server/modules/database/init-db.js.map +1 -0
  14. package/dist-server/server/modules/database/migrations.js +370 -0
  15. package/dist-server/server/modules/database/migrations.js.map +1 -0
  16. package/dist-server/server/modules/database/repositories/api-keys.js +72 -0
  17. package/dist-server/server/modules/database/repositories/api-keys.js.map +1 -0
  18. package/dist-server/server/modules/database/repositories/app-config.js +47 -0
  19. package/dist-server/server/modules/database/repositories/app-config.js.map +1 -0
  20. package/dist-server/server/modules/database/repositories/credentials.js +68 -0
  21. package/dist-server/server/modules/database/repositories/credentials.js.map +1 -0
  22. package/dist-server/server/modules/database/repositories/github-tokens.js +54 -0
  23. package/dist-server/server/modules/database/repositories/github-tokens.js.map +1 -0
  24. package/dist-server/server/modules/database/repositories/notification-preferences.js +72 -0
  25. package/dist-server/server/modules/database/repositories/notification-preferences.js.map +1 -0
  26. package/dist-server/server/modules/database/repositories/projects.db.integration.test.js +64 -0
  27. package/dist-server/server/modules/database/repositories/projects.db.integration.test.js.map +1 -0
  28. package/dist-server/server/modules/database/repositories/projects.db.js +160 -0
  29. package/dist-server/server/modules/database/repositories/projects.db.js.map +1 -0
  30. package/dist-server/server/modules/database/repositories/push-subscriptions.js +49 -0
  31. package/dist-server/server/modules/database/repositories/push-subscriptions.js.map +1 -0
  32. package/dist-server/server/modules/database/repositories/scan-state.db.js +31 -0
  33. package/dist-server/server/modules/database/repositories/scan-state.db.js.map +1 -0
  34. package/dist-server/server/modules/database/repositories/sessions.db.js +109 -0
  35. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -0
  36. package/dist-server/server/modules/database/repositories/users.js +88 -0
  37. package/dist-server/server/modules/database/repositories/users.js.map +1 -0
  38. package/dist-server/server/modules/database/repositories/vapid-keys.js +38 -0
  39. package/dist-server/server/modules/database/repositories/vapid-keys.js.map +1 -0
  40. package/dist-server/server/modules/database/schema.js +143 -0
  41. package/dist-server/server/modules/database/schema.js.map +1 -0
  42. package/dist-server/server/modules/projects/index.js +4 -0
  43. package/dist-server/server/modules/projects/index.js.map +1 -0
  44. package/dist-server/server/modules/projects/projects.routes.js +188 -0
  45. package/dist-server/server/modules/projects/projects.routes.js.map +1 -0
  46. package/dist-server/server/modules/projects/services/project-clone.service.js +219 -0
  47. package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -0
  48. package/dist-server/server/modules/projects/services/project-delete.service.js +65 -0
  49. package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -0
  50. package/dist-server/server/modules/projects/services/project-management.service.js +93 -0
  51. package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -0
  52. package/dist-server/server/modules/projects/services/project-star.service.js +60 -0
  53. package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -0
  54. package/dist-server/server/modules/projects/services/projects-has-taskmaster.service.js +167 -0
  55. package/dist-server/server/modules/projects/services/projects-has-taskmaster.service.js.map +1 -0
  56. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +169 -0
  57. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -0
  58. package/dist-server/server/modules/projects/tests/project-clone.service.test.js +126 -0
  59. package/dist-server/server/modules/projects/tests/project-clone.service.test.js.map +1 -0
  60. package/dist-server/server/modules/projects/tests/project-management.service.test.js +85 -0
  61. package/dist-server/server/modules/projects/tests/project-management.service.test.js.map +1 -0
  62. package/dist-server/server/modules/projects/tests/project-star.service.test.js +95 -0
  63. package/dist-server/server/modules/projects/tests/project-star.service.test.js.map +1 -0
  64. package/dist-server/server/modules/projects/tests/projects-has-taskmaster.service.test.js +87 -0
  65. package/dist-server/server/modules/projects/tests/projects-has-taskmaster.service.test.js.map +1 -0
  66. package/dist-server/server/modules/providers/index.js +4 -0
  67. package/dist-server/server/modules/providers/index.js.map +1 -0
  68. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +63 -0
  69. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -0
  70. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +144 -7
  71. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  72. package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
  73. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
  74. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +71 -0
  75. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -0
  76. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +225 -13
  77. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  78. package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
  79. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
  80. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +127 -0
  81. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -0
  82. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +2 -0
  83. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -1
  84. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +296 -0
  85. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +1 -0
  86. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +277 -10
  87. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
  88. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +2 -0
  89. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -1
  90. package/dist-server/server/modules/providers/provider.routes.js +172 -0
  91. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  92. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +868 -0
  93. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -0
  94. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +56 -0
  95. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -0
  96. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +234 -0
  97. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -0
  98. package/dist-server/server/modules/providers/services/sessions.service.js +78 -3
  99. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  100. package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -1
  101. package/dist-server/server/modules/websocket/index.js +3 -0
  102. package/dist-server/server/modules/websocket/index.js.map +1 -0
  103. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +188 -0
  104. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -0
  105. package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js +52 -0
  106. package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js.map +1 -0
  107. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +334 -0
  108. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -0
  109. package/dist-server/server/modules/websocket/services/websocket-auth.service.js +32 -0
  110. package/dist-server/server/modules/websocket/services/websocket-auth.service.js.map +1 -0
  111. package/dist-server/server/modules/websocket/services/websocket-server.service.js +36 -0
  112. package/dist-server/server/modules/websocket/services/websocket-server.service.js.map +1 -0
  113. package/dist-server/server/modules/websocket/services/websocket-state.service.js +14 -0
  114. package/dist-server/server/modules/websocket/services/websocket-state.service.js.map +1 -0
  115. package/dist-server/server/modules/websocket/services/websocket-writer.service.js +32 -0
  116. package/dist-server/server/modules/websocket/services/websocket-writer.service.js.map +1 -0
  117. package/dist-server/server/routes/agent.js +10 -17
  118. package/dist-server/server/routes/agent.js.map +1 -1
  119. package/dist-server/server/routes/auth.js +3 -1
  120. package/dist-server/server/routes/auth.js.map +1 -1
  121. package/dist-server/server/routes/commands.js +1 -1
  122. package/dist-server/server/routes/commands.js.map +1 -1
  123. package/dist-server/server/routes/gemini.js +2 -2
  124. package/dist-server/server/routes/gemini.js.map +1 -1
  125. package/dist-server/server/routes/git.js +33 -29
  126. package/dist-server/server/routes/git.js.map +1 -1
  127. package/dist-server/server/routes/settings.js +1 -1
  128. package/dist-server/server/routes/settings.js.map +1 -1
  129. package/dist-server/server/routes/taskmaster.js +97 -110
  130. package/dist-server/server/routes/taskmaster.js.map +1 -1
  131. package/dist-server/server/routes/user.js +1 -1
  132. package/dist-server/server/routes/user.js.map +1 -1
  133. package/dist-server/server/services/notification-orchestrator.js +2 -2
  134. package/dist-server/server/services/notification-orchestrator.js.map +1 -1
  135. package/dist-server/server/services/vapid-keys.js +2 -1
  136. package/dist-server/server/services/vapid-keys.js.map +1 -1
  137. package/dist-server/server/shared/types.js +0 -1
  138. package/dist-server/server/shared/types.js.map +1 -1
  139. package/dist-server/server/shared/utils.js +408 -7
  140. package/dist-server/server/shared/utils.js.map +1 -1
  141. package/dist-server/server/utils/taskmaster-websocket.js +18 -12
  142. package/dist-server/server/utils/taskmaster-websocket.js.map +1 -1
  143. package/dist-server/shared/modelConstants.js +2 -1
  144. package/dist-server/shared/modelConstants.js.map +1 -1
  145. package/package.json +3 -1
  146. package/server/index.js +146 -1012
  147. package/server/middleware/auth.js +1 -1
  148. package/server/modules/database/connection.ts +143 -0
  149. package/server/modules/database/index.ts +12 -0
  150. package/server/modules/database/init-db.ts +17 -0
  151. package/server/modules/database/migrations.ts +442 -0
  152. package/server/modules/database/repositories/api-keys.ts +119 -0
  153. package/server/modules/database/repositories/app-config.ts +53 -0
  154. package/server/modules/database/repositories/credentials.ts +106 -0
  155. package/server/modules/database/repositories/github-tokens.ts +100 -0
  156. package/server/modules/database/repositories/notification-preferences.ts +103 -0
  157. package/server/modules/database/repositories/projects.db.integration.test.ts +72 -0
  158. package/server/modules/database/repositories/projects.db.ts +183 -0
  159. package/server/modules/database/repositories/push-subscriptions.ts +80 -0
  160. package/server/modules/database/repositories/scan-state.db.ts +42 -0
  161. package/server/modules/database/repositories/sessions.db.ts +174 -0
  162. package/server/modules/database/repositories/users.ts +140 -0
  163. package/server/modules/database/repositories/vapid-keys.ts +57 -0
  164. package/server/modules/database/schema.ts +152 -0
  165. package/server/modules/projects/index.ts +6 -0
  166. package/server/modules/projects/projects.routes.ts +247 -0
  167. package/server/modules/projects/services/project-clone.service.ts +321 -0
  168. package/server/modules/projects/services/project-delete.service.ts +75 -0
  169. package/server/modules/projects/services/project-management.service.ts +150 -0
  170. package/server/modules/projects/services/project-star.service.ts +78 -0
  171. package/server/modules/projects/services/projects-has-taskmaster.service.ts +248 -0
  172. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +285 -0
  173. package/server/modules/projects/tests/project-clone.service.test.ts +183 -0
  174. package/server/modules/projects/tests/project-management.service.test.ts +117 -0
  175. package/server/modules/projects/tests/project-star.service.test.ts +123 -0
  176. package/server/modules/projects/tests/projects-has-taskmaster.service.test.ts +105 -0
  177. package/server/modules/providers/index.ts +4 -0
  178. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +110 -0
  179. package/server/modules/providers/list/claude/claude-sessions.provider.ts +182 -13
  180. package/server/modules/providers/list/claude/claude.provider.ts +3 -1
  181. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +119 -0
  182. package/server/modules/providers/list/codex/codex-sessions.provider.ts +262 -17
  183. package/server/modules/providers/list/codex/codex.provider.ts +3 -1
  184. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +176 -0
  185. package/server/modules/providers/list/cursor/cursor.provider.ts +3 -1
  186. package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +401 -0
  187. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +327 -13
  188. package/server/modules/providers/list/gemini/gemini.provider.ts +3 -1
  189. package/server/modules/providers/provider.routes.ts +220 -12
  190. package/server/modules/providers/services/session-conversations-search.service.ts +1167 -0
  191. package/server/modules/providers/services/session-synchronizer.service.ts +74 -0
  192. package/server/modules/providers/services/sessions-watcher.service.ts +283 -0
  193. package/server/modules/providers/services/sessions.service.ts +89 -4
  194. package/server/modules/providers/shared/base/abstract.provider.ts +8 -1
  195. package/server/modules/websocket/README.md +267 -0
  196. package/server/modules/websocket/index.ts +2 -0
  197. package/server/modules/websocket/services/chat-websocket.service.ts +271 -0
  198. package/server/modules/websocket/services/plugin-websocket-proxy.service.ts +65 -0
  199. package/server/modules/websocket/services/shell-websocket.service.ts +453 -0
  200. package/server/modules/websocket/services/websocket-auth.service.ts +54 -0
  201. package/server/modules/websocket/services/websocket-server.service.ts +58 -0
  202. package/server/modules/websocket/services/websocket-state.service.ts +16 -0
  203. package/server/modules/websocket/services/websocket-writer.service.ts +38 -0
  204. package/server/routes/agent.js +11 -16
  205. package/server/routes/auth.js +4 -2
  206. package/server/routes/commands.js +1 -1
  207. package/server/routes/gemini.js +3 -2
  208. package/server/routes/git.js +33 -28
  209. package/server/routes/settings.js +1 -1
  210. package/server/routes/taskmaster.js +108 -111
  211. package/server/routes/user.js +1 -1
  212. package/server/services/notification-orchestrator.js +3 -2
  213. package/server/services/vapid-keys.js +2 -1
  214. package/server/shared/interfaces.ts +39 -1
  215. package/server/shared/types.ts +185 -30
  216. package/server/shared/utils.ts +485 -9
  217. package/server/utils/taskmaster-websocket.js +19 -13
  218. package/shared/modelConstants.js +2 -1
  219. package/dist/assets/index-RX-W_g_Q.js +0 -1262
  220. package/dist-server/server/database/db.js +0 -535
  221. package/dist-server/server/database/db.js.map +0 -1
  222. package/dist-server/server/database/schema.js +0 -97
  223. package/dist-server/server/database/schema.js.map +0 -1
  224. package/dist-server/server/projects.js +0 -2325
  225. package/dist-server/server/projects.js.map +0 -1
  226. package/dist-server/server/routes/codex.js +0 -18
  227. package/dist-server/server/routes/codex.js.map +0 -1
  228. package/dist-server/server/routes/messages.js +0 -56
  229. package/dist-server/server/routes/messages.js.map +0 -1
  230. package/dist-server/server/routes/projects.js +0 -505
  231. package/dist-server/server/routes/projects.js.map +0 -1
  232. package/server/database/db.js +0 -593
  233. package/server/database/schema.js +0 -102
  234. package/server/projects.js +0 -2555
  235. package/server/routes/codex.js +0 -19
  236. package/server/routes/messages.js +0 -61
  237. package/server/routes/projects.js +0 -548
@@ -0,0 +1,119 @@
1
+ /**
2
+ * API keys repository.
3
+ *
4
+ * Manages API keys used for external/programmatic access to the backend.
5
+ * Keys are prefixed with `ck_` and tied to a user via foreign key.
6
+ */
7
+
8
+ import crypto from 'crypto';
9
+
10
+ import { getConnection } from '@/modules/database/connection.js';
11
+
12
+ type ApiKeyRow = {
13
+ id: number;
14
+ key_name: string;
15
+ api_key: string;
16
+ created_at: string;
17
+ last_used: string | null;
18
+ is_active: number;
19
+ };
20
+
21
+ type CreateApiKeyResult = {
22
+ id: number | bigint;
23
+ keyName: string;
24
+ apiKey: string;
25
+ };
26
+
27
+ type ValidatedApiKeyUser = {
28
+ id: number;
29
+ username: string;
30
+ api_key_id: number;
31
+ };
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Helpers
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /** Generates a cryptographically random API key with the `ck_` prefix. */
38
+ function generateApiKey(): string {
39
+ return 'ck_' + crypto.randomBytes(32).toString('hex');
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Queries
44
+ // ---------------------------------------------------------------------------
45
+
46
+ export const apiKeysDb = {
47
+ generateApiKey,
48
+
49
+ /** Creates a new API key for the given user and returns it for one-time display. */
50
+ createApiKey(userId: number, keyName: string): CreateApiKeyResult {
51
+ const db = getConnection();
52
+ const apiKey = generateApiKey();
53
+ const result = db
54
+ .prepare(
55
+ 'INSERT INTO api_keys (user_id, key_name, api_key) VALUES (?, ?, ?)'
56
+ )
57
+ .run(userId, keyName, apiKey);
58
+ return { id: result.lastInsertRowid, keyName, apiKey };
59
+ },
60
+
61
+ /** Lists all API keys for a user, most recent first. */
62
+ getApiKeys(userId: number): ApiKeyRow[] {
63
+ const db = getConnection();
64
+ return db
65
+ .prepare(
66
+ 'SELECT id, key_name, api_key, created_at, last_used, is_active FROM api_keys WHERE user_id = ? ORDER BY created_at DESC'
67
+ )
68
+ .all(userId) as ApiKeyRow[];
69
+ },
70
+
71
+ /**
72
+ * Validates an API key and resolves the owning user.
73
+ * If the key is valid, its `last_used` timestamp is updated as a side effect.
74
+ * Returns undefined when the key is invalid or the user is inactive.
75
+ */
76
+ validateApiKey(apiKey: string): ValidatedApiKeyUser | undefined {
77
+ const db = getConnection();
78
+ const row = db
79
+ .prepare(
80
+ `SELECT u.id, u.username, ak.id as api_key_id
81
+ FROM api_keys ak
82
+ JOIN users u ON ak.user_id = u.id
83
+ WHERE ak.api_key = ? AND ak.is_active = 1 AND u.is_active = 1`
84
+ )
85
+ .get(apiKey) as ValidatedApiKeyUser | undefined;
86
+
87
+ if (row) {
88
+ db.prepare(
89
+ 'UPDATE api_keys SET last_used = CURRENT_TIMESTAMP WHERE id = ?'
90
+ ).run(row.api_key_id);
91
+ }
92
+
93
+ return row;
94
+ },
95
+
96
+ /** Permanently removes an API key. Returns true if a row was deleted. */
97
+ deleteApiKey(userId: number, apiKeyId: number): boolean {
98
+ const db = getConnection();
99
+ const result = db
100
+ .prepare('DELETE FROM api_keys WHERE id = ? AND user_id = ?')
101
+ .run(apiKeyId, userId);
102
+ return result.changes > 0;
103
+ },
104
+
105
+ /** Enables or disables an API key without deleting it. */
106
+ toggleApiKey(
107
+ userId: number,
108
+ apiKeyId: number,
109
+ isActive: boolean
110
+ ): boolean {
111
+ const db = getConnection();
112
+ const result = db
113
+ .prepare(
114
+ 'UPDATE api_keys SET is_active = ? WHERE id = ? AND user_id = ?'
115
+ )
116
+ .run(isActive ? 1 : 0, apiKeyId, userId);
117
+ return result.changes > 0;
118
+ },
119
+ };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * App config repository.
3
+ *
4
+ * Key-value store for application-level configuration that persists
5
+ * across restarts (JWT secret, feature flags, etc.). Values are always
6
+ * stored as strings; callers handle parsing.
7
+ */
8
+
9
+ import crypto from 'crypto';
10
+
11
+ import { getConnection } from '@/modules/database/connection.js';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Queries
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export const appConfigDb = {
18
+ /** Returns the stored value for a config key, or null if missing. */
19
+ get(key: string): string | null {
20
+ try {
21
+ const db = getConnection();
22
+ const row = db
23
+ .prepare('SELECT value FROM app_config WHERE key = ?')
24
+ .get(key) as { value: string } | undefined;
25
+ return row?.value ?? null;
26
+ } catch {
27
+ // Swallow errors so early-startup reads (e.g. JWT secret) do not crash.
28
+ return null;
29
+ }
30
+ },
31
+
32
+ /** Inserts or updates a config key (upsert). */
33
+ set(key: string, value: string): void {
34
+ const db = getConnection();
35
+ db.prepare(
36
+ 'INSERT INTO app_config (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value'
37
+ ).run(key, value);
38
+ },
39
+
40
+ /**
41
+ * Returns the JWT signing secret, generating and persisting one
42
+ * if it does not already exist. This ensures the secret survives
43
+ * server restarts while being created automatically on first boot.
44
+ */
45
+ getOrCreateJwtSecret(): string {
46
+ let secret = appConfigDb.get('jwt_secret');
47
+ if (!secret) {
48
+ secret = crypto.randomBytes(64).toString('hex');
49
+ appConfigDb.set('jwt_secret', secret);
50
+ }
51
+ return secret;
52
+ },
53
+ };
@@ -0,0 +1,106 @@
1
+ /**
2
+ * User credentials repository.
3
+ *
4
+ * Manages external service tokens (GitHub, GitLab, Bitbucket, etc.)
5
+ * stored per-user. Each credential has a type discriminator so multiple
6
+ * credential kinds can coexist in the same table.
7
+ */
8
+
9
+ import { getConnection } from '@/modules/database/connection.js';
10
+ import type {
11
+ CreateCredentialResult,
12
+ CredentialPublicRow,
13
+ } from '@/shared/types.js';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Queries
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export const credentialsDb = {
20
+ /** Stores a new credential and returns a safe (no raw value) result. */
21
+ createCredential(
22
+ userId: number,
23
+ credentialName: string,
24
+ credentialType: string,
25
+ credentialValue: string,
26
+ description: string | null = null
27
+ ): CreateCredentialResult {
28
+ const db = getConnection();
29
+ const result = db
30
+ .prepare(
31
+ 'INSERT INTO user_credentials (user_id, credential_name, credential_type, credential_value, description) VALUES (?, ?, ?, ?, ?)'
32
+ )
33
+ .run(userId, credentialName, credentialType, credentialValue, description);
34
+ return {
35
+ id: result.lastInsertRowid,
36
+ credentialName,
37
+ credentialType,
38
+ };
39
+ },
40
+
41
+ /**
42
+ * Lists credentials for a user (excluding raw values).
43
+ * Optionally filters by credential type (e.g. 'github_token').
44
+ */
45
+ getCredentials(
46
+ userId: number,
47
+ credentialType: string | null = null
48
+ ): CredentialPublicRow[] {
49
+ const db = getConnection();
50
+
51
+ if (credentialType) {
52
+ return db
53
+ .prepare(
54
+ 'SELECT id, credential_name, credential_type, description, created_at, is_active FROM user_credentials WHERE user_id = ? AND credential_type = ? ORDER BY created_at DESC'
55
+ )
56
+ .all(userId, credentialType) as CredentialPublicRow[];
57
+ }
58
+
59
+ return db
60
+ .prepare(
61
+ 'SELECT id, credential_name, credential_type, description, created_at, is_active FROM user_credentials WHERE user_id = ? ORDER BY created_at DESC'
62
+ )
63
+ .all(userId) as CredentialPublicRow[];
64
+ },
65
+
66
+ /**
67
+ * Returns the raw credential value for the most recent active
68
+ * credential of the given type, or null if none exists.
69
+ */
70
+ getActiveCredential(
71
+ userId: number,
72
+ credentialType: string
73
+ ): string | null {
74
+ const db = getConnection();
75
+ const row = db
76
+ .prepare(
77
+ 'SELECT credential_value FROM user_credentials WHERE user_id = ? AND credential_type = ? AND is_active = 1 ORDER BY created_at DESC LIMIT 1'
78
+ )
79
+ .get(userId, credentialType) as { credential_value: string } | undefined;
80
+ return row?.credential_value ?? null;
81
+ },
82
+
83
+ /** Permanently removes a credential. Returns true if a row was deleted. */
84
+ deleteCredential(userId: number, credentialId: number): boolean {
85
+ const db = getConnection();
86
+ const result = db
87
+ .prepare('DELETE FROM user_credentials WHERE id = ? AND user_id = ?')
88
+ .run(credentialId, userId);
89
+ return result.changes > 0;
90
+ },
91
+
92
+ /** Enables or disables a credential without deleting it. */
93
+ toggleCredential(
94
+ userId: number,
95
+ credentialId: number,
96
+ isActive: boolean
97
+ ): boolean {
98
+ const db = getConnection();
99
+ const result = db
100
+ .prepare(
101
+ 'UPDATE user_credentials SET is_active = ? WHERE id = ? AND user_id = ?'
102
+ )
103
+ .run(isActive ? 1 : 0, credentialId, userId);
104
+ return result.changes > 0;
105
+ },
106
+ };
@@ -0,0 +1,100 @@
1
+ /**
2
+ * GitHub tokens repository.
3
+ *
4
+ * Backward-compatible helper layer over generic credentials storage.
5
+ * Tokens are stored in `user_credentials` with `credential_type = 'github_token'`.
6
+ */
7
+
8
+ import { getConnection } from '@/modules/database/connection.js';
9
+ import { credentialsDb } from '@/modules/database/repositories/credentials.js';
10
+ import type {
11
+ CredentialPublicRow,
12
+ CreateCredentialResult,
13
+ } from '@/shared/types.js';
14
+
15
+ const GITHUB_TOKEN_TYPE = 'github_token';
16
+
17
+ type CredentialRow = {
18
+ id: number;
19
+ user_id: number;
20
+ credential_name: string;
21
+ credential_type: string;
22
+ credential_value: string;
23
+ description: string | null;
24
+ created_at: string;
25
+ is_active: number;
26
+ };
27
+
28
+ type GithubTokenLookup = CredentialRow & {
29
+ github_token: string;
30
+ };
31
+
32
+ export const githubTokensDb = {
33
+ /** Creates a GitHub token credential entry. */
34
+ createGithubToken(
35
+ userId: number,
36
+ tokenName: string,
37
+ githubToken: string,
38
+ description: string | null = null
39
+ ): CreateCredentialResult {
40
+ return credentialsDb.createCredential(
41
+ userId,
42
+ tokenName,
43
+ GITHUB_TOKEN_TYPE,
44
+ githubToken,
45
+ description
46
+ );
47
+ },
48
+
49
+ /** Returns all GitHub tokens (safe shape: no credential value). */
50
+ getGithubTokens(userId: number): CredentialPublicRow[] {
51
+ return credentialsDb.getCredentials(userId, GITHUB_TOKEN_TYPE);
52
+ },
53
+
54
+ /** Returns the most recent active GitHub token value for a user. */
55
+ getActiveGithubToken(userId: number): string | null {
56
+ return credentialsDb.getActiveCredential(userId, GITHUB_TOKEN_TYPE);
57
+ },
58
+
59
+ /**
60
+ * Returns a specific active GitHub token row by id/user, including
61
+ * a `github_token` compatibility field.
62
+ */
63
+ getGithubTokenById(userId: number, tokenId: number): GithubTokenLookup | null {
64
+ const db = getConnection();
65
+ const row = db
66
+ .prepare(
67
+ `SELECT *
68
+ FROM user_credentials
69
+ WHERE id = ? AND user_id = ? AND credential_type = ? AND is_active = 1`
70
+ )
71
+ .get(tokenId, userId, GITHUB_TOKEN_TYPE) as CredentialRow | undefined;
72
+
73
+ if (!row) return null;
74
+
75
+ return {
76
+ ...row,
77
+ github_token: row.credential_value,
78
+ };
79
+ },
80
+
81
+ /** Updates active state for a GitHub token. */
82
+ updateGithubToken(
83
+ userId: number,
84
+ tokenId: number,
85
+ isActive: boolean
86
+ ): boolean {
87
+ return credentialsDb.toggleCredential(userId, tokenId, isActive);
88
+ },
89
+
90
+ /** Deletes a GitHub token. */
91
+ deleteGithubToken(userId: number, tokenId: number): boolean {
92
+ return credentialsDb.deleteCredential(userId, tokenId);
93
+ },
94
+
95
+ // Legacy alias used by existing routes
96
+ toggleGithubToken(userId: number, tokenId: number, isActive: boolean): boolean {
97
+ return githubTokensDb.updateGithubToken(userId, tokenId, isActive);
98
+ },
99
+ };
100
+
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Notification preferences repository.
3
+ *
4
+ * Stores per-user notification channel/event preferences as JSON.
5
+ */
6
+
7
+ import { getConnection } from '@/modules/database/connection.js';
8
+
9
+ type NotificationPreferences = {
10
+ channels: {
11
+ inApp: boolean;
12
+ webPush: boolean;
13
+ };
14
+ events: {
15
+ actionRequired: boolean;
16
+ stop: boolean;
17
+ error: boolean;
18
+ };
19
+ };
20
+
21
+ const DEFAULT_NOTIFICATION_PREFERENCES: NotificationPreferences = {
22
+ channels: {
23
+ inApp: false,
24
+ webPush: false,
25
+ },
26
+ events: {
27
+ actionRequired: true,
28
+ stop: true,
29
+ error: true,
30
+ },
31
+ };
32
+
33
+ function normalizeNotificationPreferences(value: unknown): NotificationPreferences {
34
+ const source = value && typeof value === 'object' ? (value as Record<string, any>) : {};
35
+
36
+ return {
37
+ channels: {
38
+ inApp: source.channels?.inApp === true,
39
+ webPush: source.channels?.webPush === true,
40
+ },
41
+ events: {
42
+ actionRequired: source.events?.actionRequired !== false,
43
+ stop: source.events?.stop !== false,
44
+ error: source.events?.error !== false,
45
+ },
46
+ };
47
+ }
48
+
49
+ export const notificationPreferencesDb = {
50
+ /** Returns the normalized preferences for a user, creating defaults on first read. */
51
+ getNotificationPreferences(userId: number): NotificationPreferences {
52
+ const db = getConnection();
53
+ const row = db
54
+ .prepare(
55
+ 'SELECT preferences_json FROM user_notification_preferences WHERE user_id = ?'
56
+ )
57
+ .get(userId) as { preferences_json: string } | undefined;
58
+
59
+ if (!row) {
60
+ const defaults = normalizeNotificationPreferences(DEFAULT_NOTIFICATION_PREFERENCES);
61
+ db.prepare(
62
+ 'INSERT INTO user_notification_preferences (user_id, preferences_json, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)'
63
+ ).run(userId, JSON.stringify(defaults));
64
+ return defaults;
65
+ }
66
+
67
+ let parsed: unknown;
68
+ try {
69
+ parsed = JSON.parse(row.preferences_json);
70
+ } catch {
71
+ parsed = DEFAULT_NOTIFICATION_PREFERENCES;
72
+ }
73
+ return normalizeNotificationPreferences(parsed);
74
+ },
75
+
76
+ /** Upserts normalized preferences for a user and returns the stored value. */
77
+ updateNotificationPreferences(
78
+ userId: number,
79
+ preferences: unknown
80
+ ): NotificationPreferences {
81
+ const normalized = normalizeNotificationPreferences(preferences);
82
+ const db = getConnection();
83
+
84
+ db.prepare(
85
+ `INSERT INTO user_notification_preferences (user_id, preferences_json, updated_at)
86
+ VALUES (?, ?, CURRENT_TIMESTAMP)
87
+ ON CONFLICT(user_id) DO UPDATE SET
88
+ preferences_json = excluded.preferences_json,
89
+ updated_at = CURRENT_TIMESTAMP`
90
+ ).run(userId, JSON.stringify(normalized));
91
+
92
+ return normalized;
93
+ },
94
+
95
+ // Legacy aliases used by existing services/routes
96
+ getPreferences(userId: number): NotificationPreferences {
97
+ return notificationPreferencesDb.getNotificationPreferences(userId);
98
+ },
99
+ updatePreferences(userId: number, preferences: unknown): NotificationPreferences {
100
+ return notificationPreferencesDb.updateNotificationPreferences(userId, preferences);
101
+ },
102
+ };
103
+
@@ -0,0 +1,72 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtemp, rm } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import { closeConnection } from '@/modules/database/connection.js';
8
+ import { initializeDatabase } from '@/modules/database/init-db.js';
9
+ import { projectsDb } from '@/modules/database/repositories/projects.db.js';
10
+
11
+ async function withIsolatedDatabase(runTest: () => void | Promise<void>): Promise<void> {
12
+ const previousDatabasePath = process.env.DATABASE_PATH;
13
+ const tempDirectory = await mkdtemp(path.join(tmpdir(), 'projects-db-'));
14
+ const databasePath = path.join(tempDirectory, 'auth.db');
15
+
16
+ closeConnection();
17
+ process.env.DATABASE_PATH = databasePath;
18
+ await initializeDatabase();
19
+
20
+ try {
21
+ await runTest();
22
+ } finally {
23
+ closeConnection();
24
+ if (previousDatabasePath === undefined) {
25
+ delete process.env.DATABASE_PATH;
26
+ } else {
27
+ process.env.DATABASE_PATH = previousDatabasePath;
28
+ }
29
+ await rm(tempDirectory, { recursive: true, force: true });
30
+ }
31
+ }
32
+
33
+ test('projectsDb.createProjectPath returns created for fresh paths', async () => {
34
+ await withIsolatedDatabase(() => {
35
+ const created = projectsDb.createProjectPath('/workspace/new-project');
36
+
37
+ assert.equal(created.outcome, 'created');
38
+ assert.ok(created.project);
39
+ assert.equal(created.project?.project_path, '/workspace/new-project');
40
+ assert.equal(created.project?.isArchived, 0);
41
+ });
42
+ });
43
+
44
+ test('projectsDb.createProjectPath returns reactivated_archived for archived duplicates', async () => {
45
+ await withIsolatedDatabase(() => {
46
+ const initial = projectsDb.createProjectPath('/workspace/archived-project', 'Archived Project');
47
+ assert.equal(initial.outcome, 'created');
48
+ assert.ok(initial.project);
49
+
50
+ projectsDb.updateProjectIsArchived('/workspace/archived-project', true);
51
+
52
+ const reused = projectsDb.createProjectPath('/workspace/archived-project', 'Renamed Project');
53
+ assert.equal(reused.outcome, 'reactivated_archived');
54
+ assert.ok(reused.project);
55
+ assert.equal(reused.project?.project_id, initial.project?.project_id);
56
+ assert.equal(reused.project?.isArchived, 0);
57
+ });
58
+ });
59
+
60
+ test('projectsDb.createProjectPath returns active_conflict for active duplicates', async () => {
61
+ await withIsolatedDatabase(() => {
62
+ const initial = projectsDb.createProjectPath('/workspace/active-project');
63
+ assert.equal(initial.outcome, 'created');
64
+ assert.ok(initial.project);
65
+
66
+ const conflict = projectsDb.createProjectPath('/workspace/active-project');
67
+ assert.equal(conflict.outcome, 'active_conflict');
68
+ assert.ok(conflict.project);
69
+ assert.equal(conflict.project?.project_id, initial.project?.project_id);
70
+ assert.equal(conflict.project?.isArchived, 0);
71
+ });
72
+ });