@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.
- package/dist/assets/{index-DI22MZMb.css → index-DF8TTVHl.css} +1 -1
- package/dist/assets/index-DbZAcc9z.js +1262 -0
- package/dist/index.html +2 -2
- package/dist-server/server/index.js +115 -955
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/middleware/auth.js +1 -1
- package/dist-server/server/middleware/auth.js.map +1 -1
- package/dist-server/server/modules/database/connection.js +125 -0
- package/dist-server/server/modules/database/connection.js.map +1 -0
- package/dist-server/server/modules/database/index.js +13 -0
- package/dist-server/server/modules/database/index.js.map +1 -0
- package/dist-server/server/modules/database/init-db.js +18 -0
- package/dist-server/server/modules/database/init-db.js.map +1 -0
- package/dist-server/server/modules/database/migrations.js +370 -0
- package/dist-server/server/modules/database/migrations.js.map +1 -0
- package/dist-server/server/modules/database/repositories/api-keys.js +72 -0
- package/dist-server/server/modules/database/repositories/api-keys.js.map +1 -0
- package/dist-server/server/modules/database/repositories/app-config.js +47 -0
- package/dist-server/server/modules/database/repositories/app-config.js.map +1 -0
- package/dist-server/server/modules/database/repositories/credentials.js +68 -0
- package/dist-server/server/modules/database/repositories/credentials.js.map +1 -0
- package/dist-server/server/modules/database/repositories/github-tokens.js +54 -0
- package/dist-server/server/modules/database/repositories/github-tokens.js.map +1 -0
- package/dist-server/server/modules/database/repositories/notification-preferences.js +72 -0
- package/dist-server/server/modules/database/repositories/notification-preferences.js.map +1 -0
- package/dist-server/server/modules/database/repositories/projects.db.integration.test.js +64 -0
- package/dist-server/server/modules/database/repositories/projects.db.integration.test.js.map +1 -0
- package/dist-server/server/modules/database/repositories/projects.db.js +160 -0
- package/dist-server/server/modules/database/repositories/projects.db.js.map +1 -0
- package/dist-server/server/modules/database/repositories/push-subscriptions.js +49 -0
- package/dist-server/server/modules/database/repositories/push-subscriptions.js.map +1 -0
- package/dist-server/server/modules/database/repositories/scan-state.db.js +31 -0
- package/dist-server/server/modules/database/repositories/scan-state.db.js.map +1 -0
- package/dist-server/server/modules/database/repositories/sessions.db.js +109 -0
- package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -0
- package/dist-server/server/modules/database/repositories/users.js +88 -0
- package/dist-server/server/modules/database/repositories/users.js.map +1 -0
- package/dist-server/server/modules/database/repositories/vapid-keys.js +38 -0
- package/dist-server/server/modules/database/repositories/vapid-keys.js.map +1 -0
- package/dist-server/server/modules/database/schema.js +143 -0
- package/dist-server/server/modules/database/schema.js.map +1 -0
- package/dist-server/server/modules/projects/index.js +4 -0
- package/dist-server/server/modules/projects/index.js.map +1 -0
- package/dist-server/server/modules/projects/projects.routes.js +188 -0
- package/dist-server/server/modules/projects/projects.routes.js.map +1 -0
- package/dist-server/server/modules/projects/services/project-clone.service.js +219 -0
- package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/project-delete.service.js +65 -0
- package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/project-management.service.js +93 -0
- package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/project-star.service.js +60 -0
- package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/projects-has-taskmaster.service.js +167 -0
- package/dist-server/server/modules/projects/services/projects-has-taskmaster.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +169 -0
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -0
- package/dist-server/server/modules/projects/tests/project-clone.service.test.js +126 -0
- package/dist-server/server/modules/projects/tests/project-clone.service.test.js.map +1 -0
- package/dist-server/server/modules/projects/tests/project-management.service.test.js +85 -0
- package/dist-server/server/modules/projects/tests/project-management.service.test.js.map +1 -0
- package/dist-server/server/modules/projects/tests/project-star.service.test.js +95 -0
- package/dist-server/server/modules/projects/tests/project-star.service.test.js.map +1 -0
- package/dist-server/server/modules/projects/tests/projects-has-taskmaster.service.test.js +87 -0
- package/dist-server/server/modules/projects/tests/projects-has-taskmaster.service.test.js.map +1 -0
- package/dist-server/server/modules/providers/index.js +4 -0
- package/dist-server/server/modules/providers/index.js.map +1 -0
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +63 -0
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +144 -7
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
- package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +71 -0
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +225 -13
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
- package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +127 -0
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +2 -0
- package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +296 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +277 -10
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +2 -0
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +172 -0
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js +868 -0
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js +56 -0
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js +234 -0
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/sessions.service.js +78 -3
- package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
- package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -1
- package/dist-server/server/modules/websocket/index.js +3 -0
- package/dist-server/server/modules/websocket/index.js.map +1 -0
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +188 -0
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js +52 -0
- package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js +334 -0
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/websocket-auth.service.js +32 -0
- package/dist-server/server/modules/websocket/services/websocket-auth.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/websocket-server.service.js +36 -0
- package/dist-server/server/modules/websocket/services/websocket-server.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/websocket-state.service.js +14 -0
- package/dist-server/server/modules/websocket/services/websocket-state.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/websocket-writer.service.js +32 -0
- package/dist-server/server/modules/websocket/services/websocket-writer.service.js.map +1 -0
- package/dist-server/server/routes/agent.js +10 -17
- package/dist-server/server/routes/agent.js.map +1 -1
- package/dist-server/server/routes/auth.js +3 -1
- package/dist-server/server/routes/auth.js.map +1 -1
- package/dist-server/server/routes/commands.js +1 -1
- package/dist-server/server/routes/commands.js.map +1 -1
- package/dist-server/server/routes/gemini.js +2 -2
- package/dist-server/server/routes/gemini.js.map +1 -1
- package/dist-server/server/routes/git.js +33 -29
- package/dist-server/server/routes/git.js.map +1 -1
- package/dist-server/server/routes/settings.js +1 -1
- package/dist-server/server/routes/settings.js.map +1 -1
- package/dist-server/server/routes/taskmaster.js +97 -110
- package/dist-server/server/routes/taskmaster.js.map +1 -1
- package/dist-server/server/routes/user.js +1 -1
- package/dist-server/server/routes/user.js.map +1 -1
- package/dist-server/server/services/notification-orchestrator.js +2 -2
- package/dist-server/server/services/notification-orchestrator.js.map +1 -1
- package/dist-server/server/services/vapid-keys.js +2 -1
- package/dist-server/server/services/vapid-keys.js.map +1 -1
- package/dist-server/server/shared/types.js +0 -1
- package/dist-server/server/shared/types.js.map +1 -1
- package/dist-server/server/shared/utils.js +408 -7
- package/dist-server/server/shared/utils.js.map +1 -1
- package/dist-server/server/utils/taskmaster-websocket.js +18 -12
- package/dist-server/server/utils/taskmaster-websocket.js.map +1 -1
- package/dist-server/shared/modelConstants.js +2 -1
- package/dist-server/shared/modelConstants.js.map +1 -1
- package/package.json +3 -1
- package/server/index.js +146 -1012
- package/server/middleware/auth.js +1 -1
- package/server/modules/database/connection.ts +143 -0
- package/server/modules/database/index.ts +12 -0
- package/server/modules/database/init-db.ts +17 -0
- package/server/modules/database/migrations.ts +442 -0
- package/server/modules/database/repositories/api-keys.ts +119 -0
- package/server/modules/database/repositories/app-config.ts +53 -0
- package/server/modules/database/repositories/credentials.ts +106 -0
- package/server/modules/database/repositories/github-tokens.ts +100 -0
- package/server/modules/database/repositories/notification-preferences.ts +103 -0
- package/server/modules/database/repositories/projects.db.integration.test.ts +72 -0
- package/server/modules/database/repositories/projects.db.ts +183 -0
- package/server/modules/database/repositories/push-subscriptions.ts +80 -0
- package/server/modules/database/repositories/scan-state.db.ts +42 -0
- package/server/modules/database/repositories/sessions.db.ts +174 -0
- package/server/modules/database/repositories/users.ts +140 -0
- package/server/modules/database/repositories/vapid-keys.ts +57 -0
- package/server/modules/database/schema.ts +152 -0
- package/server/modules/projects/index.ts +6 -0
- package/server/modules/projects/projects.routes.ts +247 -0
- package/server/modules/projects/services/project-clone.service.ts +321 -0
- package/server/modules/projects/services/project-delete.service.ts +75 -0
- package/server/modules/projects/services/project-management.service.ts +150 -0
- package/server/modules/projects/services/project-star.service.ts +78 -0
- package/server/modules/projects/services/projects-has-taskmaster.service.ts +248 -0
- package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +285 -0
- package/server/modules/projects/tests/project-clone.service.test.ts +183 -0
- package/server/modules/projects/tests/project-management.service.test.ts +117 -0
- package/server/modules/projects/tests/project-star.service.test.ts +123 -0
- package/server/modules/projects/tests/projects-has-taskmaster.service.test.ts +105 -0
- package/server/modules/providers/index.ts +4 -0
- package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +110 -0
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +182 -13
- package/server/modules/providers/list/claude/claude.provider.ts +3 -1
- package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +119 -0
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +262 -17
- package/server/modules/providers/list/codex/codex.provider.ts +3 -1
- package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +176 -0
- package/server/modules/providers/list/cursor/cursor.provider.ts +3 -1
- package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +401 -0
- package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +327 -13
- package/server/modules/providers/list/gemini/gemini.provider.ts +3 -1
- package/server/modules/providers/provider.routes.ts +220 -12
- package/server/modules/providers/services/session-conversations-search.service.ts +1167 -0
- package/server/modules/providers/services/session-synchronizer.service.ts +74 -0
- package/server/modules/providers/services/sessions-watcher.service.ts +283 -0
- package/server/modules/providers/services/sessions.service.ts +89 -4
- package/server/modules/providers/shared/base/abstract.provider.ts +8 -1
- package/server/modules/websocket/README.md +267 -0
- package/server/modules/websocket/index.ts +2 -0
- package/server/modules/websocket/services/chat-websocket.service.ts +271 -0
- package/server/modules/websocket/services/plugin-websocket-proxy.service.ts +65 -0
- package/server/modules/websocket/services/shell-websocket.service.ts +453 -0
- package/server/modules/websocket/services/websocket-auth.service.ts +54 -0
- package/server/modules/websocket/services/websocket-server.service.ts +58 -0
- package/server/modules/websocket/services/websocket-state.service.ts +16 -0
- package/server/modules/websocket/services/websocket-writer.service.ts +38 -0
- package/server/routes/agent.js +11 -16
- package/server/routes/auth.js +4 -2
- package/server/routes/commands.js +1 -1
- package/server/routes/gemini.js +3 -2
- package/server/routes/git.js +33 -28
- package/server/routes/settings.js +1 -1
- package/server/routes/taskmaster.js +108 -111
- package/server/routes/user.js +1 -1
- package/server/services/notification-orchestrator.js +3 -2
- package/server/services/vapid-keys.js +2 -1
- package/server/shared/interfaces.ts +39 -1
- package/server/shared/types.ts +185 -30
- package/server/shared/utils.ts +485 -9
- package/server/utils/taskmaster-websocket.js +19 -13
- package/shared/modelConstants.js +2 -1
- package/dist/assets/index-RX-W_g_Q.js +0 -1262
- package/dist-server/server/database/db.js +0 -535
- package/dist-server/server/database/db.js.map +0 -1
- package/dist-server/server/database/schema.js +0 -97
- package/dist-server/server/database/schema.js.map +0 -1
- package/dist-server/server/projects.js +0 -2325
- package/dist-server/server/projects.js.map +0 -1
- package/dist-server/server/routes/codex.js +0 -18
- package/dist-server/server/routes/codex.js.map +0 -1
- package/dist-server/server/routes/messages.js +0 -56
- package/dist-server/server/routes/messages.js.map +0 -1
- package/dist-server/server/routes/projects.js +0 -505
- package/dist-server/server/routes/projects.js.map +0 -1
- package/server/database/db.js +0 -593
- package/server/database/schema.js +0 -102
- package/server/projects.js +0 -2555
- package/server/routes/codex.js +0 -19
- package/server/routes/messages.js +0 -61
- 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
|
+
});
|