@pixelbyte-software/pixcode 1.32.0 → 1.33.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.
@@ -1,9 +1,13 @@
1
- import Database from 'better-sqlite3';
2
- import path from 'path';
3
- import fs from 'fs';
4
- import crypto from 'crypto';
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import path from 'node:path';
5
5
  import { findAppRoot, getModuleDir } from '../utils/runtime-paths.js';
6
- import { APP_CONFIG_TABLE_SQL, USER_NOTIFICATION_PREFERENCES_TABLE_SQL, VAPID_KEYS_TABLE_SQL, PUSH_SUBSCRIPTIONS_TABLE_SQL, SESSION_NAMES_TABLE_SQL, SESSION_NAMES_LOOKUP_INDEX_SQL, TELEGRAM_CONFIG_TABLE_SQL, TELEGRAM_LINKS_TABLE_SQL, TELEGRAM_LINKS_CHAT_INDEX_SQL, TELEGRAM_LINKS_CODE_INDEX_SQL, DATABASE_SCHEMA_SQL } from './schema.js';
6
+ import { JsonStore, nowIso } from './json-store.js';
7
+ // CommonJS `require` shim — we only reach for it once, during the
8
+ // legacy-to-JSON migration below, to dynamically import better-sqlite3
9
+ // only when an old auth.db is actually present on disk.
10
+ const require = createRequire(import.meta.url);
7
11
  const __dirname = getModuleDir(import.meta.url);
8
12
  // The compiled backend lives under dist-server/server/database, but the install root we log
9
13
  // should still point at the project/app root. Resolving it here avoids build-layout drift.
@@ -20,468 +24,546 @@ const c = {
20
24
  bright: (text) => `${colors.bright}${text}${colors.reset}`,
21
25
  dim: (text) => `${colors.dim}${text}${colors.reset}`,
22
26
  };
23
- // Use DATABASE_PATH environment variable if set, otherwise use default location
24
- const DB_PATH = process.env.DATABASE_PATH || path.join(__dirname, 'auth.db');
25
- // Ensure database directory exists if custom path is provided
26
- if (process.env.DATABASE_PATH) {
27
- const dbDir = path.dirname(DB_PATH);
27
+ // DATABASE_PATH keeps its historical meaning (user override), but points
28
+ // at a `.json` file now. If the user's env var still names a `.db`
29
+ // extension, we swap to the corresponding `.json` sibling — the auth
30
+ // store moved off SQLite in v1.33.0.
31
+ const rawDbPath = process.env.DATABASE_PATH || path.join(__dirname, 'auth.db');
32
+ const JSON_PATH = rawDbPath.endsWith('.db')
33
+ ? rawDbPath.replace(/\.db$/, '.json')
34
+ : (rawDbPath.endsWith('.json') ? rawDbPath : `${rawDbPath}.json`);
35
+ const LEGACY_SQLITE_PATH = rawDbPath.endsWith('.db')
36
+ ? rawDbPath
37
+ : rawDbPath.replace(/\.json$/, '.db');
38
+ // Ensure parent dir exists before migration / initial write.
39
+ {
40
+ const dir = path.dirname(JSON_PATH);
41
+ if (!fs.existsSync(dir)) {
42
+ try {
43
+ fs.mkdirSync(dir, { recursive: true });
44
+ }
45
+ catch (err) {
46
+ console.error(`Failed to create database directory ${dir}:`, err.message);
47
+ throw err;
48
+ }
49
+ }
50
+ }
51
+ /**
52
+ * One-time migration from the previous better-sqlite3 auth.db to the
53
+ * new JSON format. Triggered only when:
54
+ * - The legacy .db exists
55
+ * - AND no .json has been created yet
56
+ * The legacy file is kept in place as `<name>.db.migrated-<timestamp>`
57
+ * so a user can roll back by moving it into place and reinstalling
58
+ * better-sqlite3 if they hit a showstopper. Runs synchronously because
59
+ * all downstream modules (auth.js, vapid-keys.js) import `db` at module
60
+ * load and can't wait for async startup.
61
+ */
62
+ function migrateSqliteIfPresent() {
63
+ if (fs.existsSync(JSON_PATH))
64
+ return; // Already migrated or fresh install.
65
+ if (!fs.existsSync(LEGACY_SQLITE_PATH))
66
+ return; // Nothing to migrate.
67
+ console.log(`${c.info('[MIGRATION]')} Converting ${c.bright(LEGACY_SQLITE_PATH)} → ${c.bright(JSON_PATH)} (JSON auth store, v1.33.0)`);
68
+ let Database;
28
69
  try {
29
- if (!fs.existsSync(dbDir)) {
30
- fs.mkdirSync(dbDir, { recursive: true });
31
- console.log(`Created database directory: ${dbDir}`);
70
+ const mod = require('better-sqlite3');
71
+ Database = mod.default || mod;
72
+ }
73
+ catch {
74
+ // Auto-install path fell through — the user has a legacy file but
75
+ // better-sqlite3 isn't present (they may have a trimmed dep tree).
76
+ // Surface a clear error; we'd rather fail startup than silently
77
+ // skip migration and strand the user's saved credentials.
78
+ console.error('[MIGRATION] Legacy auth.db present but better-sqlite3 not installed.');
79
+ console.error('[MIGRATION] Install it once to migrate: `npm install better-sqlite3` in your pixcode install dir, then restart.');
80
+ throw new Error('Auth DB migration requires better-sqlite3 (legacy file detected).');
81
+ }
82
+ const legacy = new Database(LEGACY_SQLITE_PATH, { readonly: true });
83
+ const store = new JsonStore(JSON_PATH);
84
+ // Pull every table, skipping silently when it doesn't exist (old
85
+ // installs that never ran some migrations). We rely on `IF NOT EXISTS`
86
+ // patterns from the old schema.js — missing tables throw a SQLite
87
+ // error which we catch per-table.
88
+ const safeAll = (sql) => {
89
+ try {
90
+ return legacy.prepare(sql).all();
32
91
  }
92
+ catch {
93
+ return [];
94
+ }
95
+ };
96
+ const users = safeAll('SELECT id, username, password_hash, created_at, last_login, is_active, git_name, git_email, has_completed_onboarding FROM users');
97
+ for (const u of users) {
98
+ store.raw.users.push({
99
+ id: u.id,
100
+ username: u.username,
101
+ password_hash: u.password_hash,
102
+ created_at: u.created_at || nowIso(),
103
+ last_login: u.last_login || null,
104
+ is_active: u.is_active !== 0,
105
+ git_name: u.git_name || null,
106
+ git_email: u.git_email || null,
107
+ has_completed_onboarding: u.has_completed_onboarding === 1,
108
+ });
109
+ store.raw._sequences.users = Math.max(store.raw._sequences.users, u.id);
33
110
  }
34
- catch (error) {
35
- console.error(`Failed to create database directory ${dbDir}:`, error.message);
36
- throw error;
111
+ const apiKeys = safeAll('SELECT id, user_id, key_name, api_key, created_at, last_used, is_active FROM api_keys');
112
+ for (const k of apiKeys) {
113
+ store.raw.api_keys.push({
114
+ id: k.id,
115
+ user_id: k.user_id,
116
+ key_name: k.key_name,
117
+ api_key: k.api_key,
118
+ created_at: k.created_at || nowIso(),
119
+ last_used: k.last_used || null,
120
+ is_active: k.is_active !== 0,
121
+ });
122
+ store.raw._sequences.api_keys = Math.max(store.raw._sequences.api_keys, k.id);
37
123
  }
38
- }
39
- // As part of 1.19.2 we are introducing a new location for auth.db. The below handles exisitng moving legacy database from install directory to new location
40
- const LEGACY_DB_PATH = path.join(__dirname, 'auth.db');
41
- if (DB_PATH !== LEGACY_DB_PATH && !fs.existsSync(DB_PATH) && fs.existsSync(LEGACY_DB_PATH)) {
124
+ const credentials = safeAll('SELECT id, user_id, credential_name, credential_type, credential_value, description, created_at, is_active FROM user_credentials');
125
+ for (const cr of credentials) {
126
+ store.raw.user_credentials.push({
127
+ id: cr.id,
128
+ user_id: cr.user_id,
129
+ credential_name: cr.credential_name,
130
+ credential_type: cr.credential_type,
131
+ credential_value: cr.credential_value,
132
+ description: cr.description || null,
133
+ created_at: cr.created_at || nowIso(),
134
+ is_active: cr.is_active !== 0,
135
+ });
136
+ store.raw._sequences.user_credentials = Math.max(store.raw._sequences.user_credentials, cr.id);
137
+ }
138
+ const prefs = safeAll('SELECT user_id, preferences_json, updated_at FROM user_notification_preferences');
139
+ for (const p of prefs) {
140
+ store.raw.user_notification_preferences.push({
141
+ user_id: p.user_id,
142
+ preferences_json: p.preferences_json,
143
+ updated_at: p.updated_at || nowIso(),
144
+ });
145
+ }
146
+ const vapid = safeAll('SELECT id, public_key, private_key, created_at FROM vapid_keys');
147
+ for (const v of vapid) {
148
+ store.raw.vapid_keys.push({
149
+ id: v.id,
150
+ public_key: v.public_key,
151
+ private_key: v.private_key,
152
+ created_at: v.created_at || nowIso(),
153
+ });
154
+ store.raw._sequences.vapid_keys = Math.max(store.raw._sequences.vapid_keys, v.id);
155
+ }
156
+ const pushSubs = safeAll('SELECT id, user_id, endpoint, keys_p256dh, keys_auth, created_at FROM push_subscriptions');
157
+ for (const s of pushSubs) {
158
+ store.raw.push_subscriptions.push({
159
+ id: s.id,
160
+ user_id: s.user_id,
161
+ endpoint: s.endpoint,
162
+ keys_p256dh: s.keys_p256dh,
163
+ keys_auth: s.keys_auth,
164
+ created_at: s.created_at || nowIso(),
165
+ });
166
+ store.raw._sequences.push_subscriptions = Math.max(store.raw._sequences.push_subscriptions, s.id);
167
+ }
168
+ const sessionNames = safeAll('SELECT session_id, provider, custom_name, created_at, updated_at FROM session_names');
169
+ for (const sn of sessionNames) {
170
+ store.raw.session_names.push({
171
+ session_id: sn.session_id,
172
+ provider: sn.provider,
173
+ custom_name: sn.custom_name,
174
+ created_at: sn.created_at || nowIso(),
175
+ updated_at: sn.updated_at || nowIso(),
176
+ });
177
+ }
178
+ const appConfig = safeAll('SELECT key, value, created_at FROM app_config');
179
+ for (const a of appConfig) {
180
+ store.raw.app_config.push({
181
+ key: a.key,
182
+ value: a.value,
183
+ created_at: a.created_at || nowIso(),
184
+ });
185
+ }
186
+ const telegramConfig = safeAll('SELECT id, bot_token, bot_username, updated_at FROM telegram_config');
187
+ for (const t of telegramConfig) {
188
+ store.raw.telegram_config.push({
189
+ id: 1,
190
+ bot_token: t.bot_token,
191
+ bot_username: t.bot_username || null,
192
+ updated_at: t.updated_at || nowIso(),
193
+ });
194
+ }
195
+ const telegramLinks = safeAll('SELECT user_id, chat_id, telegram_username, language, pairing_code, pairing_code_expires_at, verified_at, notifications_enabled, bridge_enabled, updated_at FROM telegram_links');
196
+ for (const tl of telegramLinks) {
197
+ store.raw.telegram_links.push({
198
+ user_id: tl.user_id,
199
+ chat_id: tl.chat_id || null,
200
+ telegram_username: tl.telegram_username || null,
201
+ language: tl.language || 'en',
202
+ pairing_code: tl.pairing_code || null,
203
+ pairing_code_expires_at: tl.pairing_code_expires_at || null,
204
+ verified_at: tl.verified_at || null,
205
+ notifications_enabled: tl.notifications_enabled !== 0,
206
+ bridge_enabled: tl.bridge_enabled !== 0,
207
+ updated_at: tl.updated_at || nowIso(),
208
+ });
209
+ }
210
+ store.save();
211
+ legacy.close();
212
+ // Rename the old .db out of the way so we never migrate it twice.
213
+ // Keep it (not delete) so the user can roll back if needed.
214
+ const backup = `${LEGACY_SQLITE_PATH}.migrated-${Date.now()}`;
42
215
  try {
43
- fs.copyFileSync(LEGACY_DB_PATH, DB_PATH);
44
- console.log(`[MIGRATION] Copied database from ${LEGACY_DB_PATH} to ${DB_PATH}`);
216
+ fs.renameSync(LEGACY_SQLITE_PATH, backup);
45
217
  for (const suffix of ['-wal', '-shm']) {
46
- if (fs.existsSync(LEGACY_DB_PATH + suffix)) {
47
- fs.copyFileSync(LEGACY_DB_PATH + suffix, DB_PATH + suffix);
218
+ if (fs.existsSync(LEGACY_SQLITE_PATH + suffix)) {
219
+ try {
220
+ fs.renameSync(LEGACY_SQLITE_PATH + suffix, backup + suffix);
221
+ }
222
+ catch { /* noop */ }
48
223
  }
49
224
  }
50
225
  }
51
226
  catch (err) {
52
- console.warn(`[MIGRATION] Could not copy legacy database: ${err.message}`);
227
+ console.warn(`[MIGRATION] Migration succeeded but could not rename old DB: ${err.message}`);
53
228
  }
229
+ console.log(`${c.info('[MIGRATION]')} Migration complete. Old DB preserved as ${c.dim(backup)}.`);
54
230
  }
55
- // Create database connection
56
- const db = new Database(DB_PATH);
57
- // app_config must exist before any other module imports (auth.js reads the JWT secret at load time).
58
- // runMigrations() also creates this table, but it runs too late for existing installations
59
- // where auth.js is imported before initializeDatabase() is called.
60
- db.exec(APP_CONFIG_TABLE_SQL);
61
- // Show app installation path prominently
62
- const appInstallPath = APP_ROOT;
231
+ // Module-load order matters — auth middleware imports `db` and reads the
232
+ // JWT secret before anything else gets to boot. So the store has to be
233
+ // ready synchronously at the first `import { db } from './db.js'`.
234
+ migrateSqliteIfPresent();
235
+ const store = new JsonStore(JSON_PATH);
63
236
  console.log('');
64
237
  console.log(c.dim('═'.repeat(60)));
65
- console.log(`${c.info('[INFO]')} App Installation: ${c.bright(appInstallPath)}`);
66
- console.log(`${c.info('[INFO]')} Database: ${c.dim(path.relative(appInstallPath, DB_PATH))}`);
238
+ console.log(`${c.info('[INFO]')} App Installation: ${c.bright(APP_ROOT)}`);
239
+ console.log(`${c.info('[INFO]')} Auth store: ${c.dim(path.relative(APP_ROOT, JSON_PATH))}`);
67
240
  if (process.env.DATABASE_PATH) {
68
241
  console.log(` ${c.dim('(Using custom DATABASE_PATH from environment)')}`);
69
242
  }
70
243
  console.log(c.dim('═'.repeat(60)));
71
244
  console.log('');
72
- const runMigrations = () => {
73
- try {
74
- const tableInfo = db.prepare("PRAGMA table_info(users)").all();
75
- const columnNames = tableInfo.map(col => col.name);
76
- if (!columnNames.includes('git_name')) {
77
- console.log('Running migration: Adding git_name column');
78
- db.exec('ALTER TABLE users ADD COLUMN git_name TEXT');
79
- }
80
- if (!columnNames.includes('git_email')) {
81
- console.log('Running migration: Adding git_email column');
82
- db.exec('ALTER TABLE users ADD COLUMN git_email TEXT');
83
- }
84
- if (!columnNames.includes('has_completed_onboarding')) {
85
- console.log('Running migration: Adding has_completed_onboarding column');
86
- db.exec('ALTER TABLE users ADD COLUMN has_completed_onboarding BOOLEAN DEFAULT 0');
87
- }
88
- db.exec(USER_NOTIFICATION_PREFERENCES_TABLE_SQL);
89
- db.exec(VAPID_KEYS_TABLE_SQL);
90
- db.exec(PUSH_SUBSCRIPTIONS_TABLE_SQL);
91
- db.exec(APP_CONFIG_TABLE_SQL);
92
- db.exec(SESSION_NAMES_TABLE_SQL);
93
- db.exec(SESSION_NAMES_LOOKUP_INDEX_SQL);
94
- db.exec(TELEGRAM_CONFIG_TABLE_SQL);
95
- db.exec(TELEGRAM_LINKS_TABLE_SQL);
96
- db.exec(TELEGRAM_LINKS_CHAT_INDEX_SQL);
97
- db.exec(TELEGRAM_LINKS_CODE_INDEX_SQL);
98
- console.log('Database migrations completed successfully');
99
- }
100
- catch (error) {
101
- console.error('Error running migrations:', error.message);
102
- throw error;
103
- }
104
- };
105
- // Initialize database with schema
106
- const initializeDatabase = async () => {
107
- try {
108
- db.exec(DATABASE_SCHEMA_SQL);
109
- console.log('Database initialized successfully');
110
- runMigrations();
111
- }
112
- catch (error) {
113
- console.error('Error initializing database:', error.message);
114
- throw error;
115
- }
245
+ // initializeDatabase used to run `CREATE TABLE IF NOT EXISTS` + migrations.
246
+ // The JSON store always has its shape pre-baked, so this is a no-op —
247
+ // keeping the export preserves the server boot sequence.
248
+ const initializeDatabase = async () => { };
249
+ // ---------------------------------------------------------------------------
250
+ // Back-compat `db` shim — a few modules outside database/db.js imported `db`
251
+ // and called `db.prepare(sql)` directly. Most of those uses were for trivial
252
+ // transaction markers (BEGIN/COMMIT/ROLLBACK — no-op under our JSON store)
253
+ // or very specific SELECTs we've wrapped in explicit helpers. This shim
254
+ // lets those call sites keep working without a bigger refactor.
255
+ // ---------------------------------------------------------------------------
256
+ const db = {
257
+ prepare(sql) {
258
+ const normalized = sql.trim().toUpperCase();
259
+ // Transaction markers our store writes atomically per op, so
260
+ // there's no real transaction to start. No-op + success result.
261
+ if (normalized === 'BEGIN' || normalized === 'COMMIT' || normalized === 'ROLLBACK') {
262
+ return {
263
+ run: () => ({ changes: 0, lastInsertRowid: 0 }),
264
+ };
265
+ }
266
+ // Specific query routed through the new helpers — used by
267
+ // server/routes/projects.js to fetch a single github credential.
268
+ if (/FROM USER_CREDENTIALS\s+WHERE ID = \?\s+AND USER_ID = \?\s+AND CREDENTIAL_TYPE = \?\s+AND IS_ACTIVE = 1/.test(normalized)) {
269
+ return {
270
+ get: (id, userId, credentialType) => store.findWhere('user_credentials', (r) => r.id === id && r.user_id === userId && r.credential_type === credentialType && r.is_active)
271
+ || undefined,
272
+ };
273
+ }
274
+ throw new Error(`db.prepare: unsupported SQL passed through the compat shim: ${sql.slice(0, 80)}`
275
+ + '\nExtend server/database/db.js or use a dedicated helper (userDb, credentialsDb, …).');
276
+ },
116
277
  };
117
- // User database operations
278
+ // ---------------------------------------------------------------------------
279
+ // User operations
280
+ // ---------------------------------------------------------------------------
118
281
  const userDb = {
119
- // Check if any users exist
120
- hasUsers: () => {
121
- try {
122
- const row = db.prepare('SELECT COUNT(*) as count FROM users').get();
123
- return row.count > 0;
124
- }
125
- catch (err) {
126
- throw err;
127
- }
128
- },
129
- // Create a new user
282
+ hasUsers: () => store.count('users', (r) => r.is_active) > 0,
130
283
  createUser: (username, passwordHash) => {
131
- try {
132
- const stmt = db.prepare('INSERT INTO users (username, password_hash) VALUES (?, ?)');
133
- const result = stmt.run(username, passwordHash);
134
- return { id: result.lastInsertRowid, username };
135
- }
136
- catch (err) {
137
- throw err;
138
- }
139
- },
140
- // Get user by username
141
- getUserByUsername: (username) => {
142
- try {
143
- const row = db.prepare('SELECT * FROM users WHERE username = ? AND is_active = 1').get(username);
144
- return row;
145
- }
146
- catch (err) {
147
- throw err;
148
- }
149
- },
150
- // Update last login time (non-fatal — logged but not thrown)
284
+ const row = store.insert('users', {
285
+ username,
286
+ password_hash: passwordHash,
287
+ created_at: nowIso(),
288
+ last_login: null,
289
+ is_active: true,
290
+ git_name: null,
291
+ git_email: null,
292
+ has_completed_onboarding: false,
293
+ });
294
+ return { id: row.id, username: row.username };
295
+ },
296
+ getUserByUsername: (username) => store.findWhere('users', (r) => r.username === username && r.is_active) || undefined,
151
297
  updateLastLogin: (userId) => {
152
298
  try {
153
- db.prepare('UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?').run(userId);
299
+ store.updateWhere('users', (r) => r.id === userId, { last_login: nowIso() });
154
300
  }
155
301
  catch (err) {
156
302
  console.warn('Failed to update last login:', err.message);
157
303
  }
158
304
  },
159
- // Get user by ID
160
305
  getUserById: (userId) => {
161
- try {
162
- const row = db.prepare('SELECT id, username, created_at, last_login FROM users WHERE id = ? AND is_active = 1').get(userId);
163
- return row;
164
- }
165
- catch (err) {
166
- throw err;
167
- }
306
+ const row = store.findWhere('users', (r) => r.id === userId && r.is_active);
307
+ if (!row)
308
+ return undefined;
309
+ return {
310
+ id: row.id,
311
+ username: row.username,
312
+ created_at: row.created_at,
313
+ last_login: row.last_login,
314
+ };
168
315
  },
169
316
  getFirstUser: () => {
170
- try {
171
- const row = db.prepare('SELECT id, username, created_at, last_login FROM users WHERE is_active = 1 LIMIT 1').get();
172
- return row;
173
- }
174
- catch (err) {
175
- throw err;
176
- }
317
+ const row = store.raw.users.find((r) => r.is_active);
318
+ if (!row)
319
+ return undefined;
320
+ return {
321
+ id: row.id,
322
+ username: row.username,
323
+ created_at: row.created_at,
324
+ last_login: row.last_login,
325
+ };
177
326
  },
178
327
  updateGitConfig: (userId, gitName, gitEmail) => {
179
- try {
180
- const stmt = db.prepare('UPDATE users SET git_name = ?, git_email = ? WHERE id = ?');
181
- stmt.run(gitName, gitEmail, userId);
182
- }
183
- catch (err) {
184
- throw err;
185
- }
328
+ store.updateWhere('users', (r) => r.id === userId, { git_name: gitName, git_email: gitEmail });
186
329
  },
187
330
  getGitConfig: (userId) => {
188
- try {
189
- const row = db.prepare('SELECT git_name, git_email FROM users WHERE id = ?').get(userId);
190
- return row;
191
- }
192
- catch (err) {
193
- throw err;
194
- }
331
+ const row = store.findWhere('users', (r) => r.id === userId);
332
+ if (!row)
333
+ return undefined;
334
+ return { git_name: row.git_name || null, git_email: row.git_email || null };
195
335
  },
196
336
  completeOnboarding: (userId) => {
197
- try {
198
- const stmt = db.prepare('UPDATE users SET has_completed_onboarding = 1 WHERE id = ?');
199
- stmt.run(userId);
200
- }
201
- catch (err) {
202
- throw err;
203
- }
337
+ store.updateWhere('users', (r) => r.id === userId, { has_completed_onboarding: true });
204
338
  },
205
339
  hasCompletedOnboarding: (userId) => {
206
- try {
207
- const row = db.prepare('SELECT has_completed_onboarding FROM users WHERE id = ?').get(userId);
208
- return row?.has_completed_onboarding === 1;
209
- }
210
- catch (err) {
211
- throw err;
212
- }
213
- }
340
+ const row = store.findWhere('users', (r) => r.id === userId);
341
+ return Boolean(row?.has_completed_onboarding);
342
+ },
214
343
  };
215
- // API Keys database operations
344
+ // ---------------------------------------------------------------------------
345
+ // API key operations
346
+ // ---------------------------------------------------------------------------
216
347
  const apiKeysDb = {
217
- // Generate a new API key
218
- generateApiKey: () => {
219
- return 'ck_' + crypto.randomBytes(32).toString('hex');
220
- },
221
- // Create a new API key
348
+ generateApiKey: () => 'ck_' + crypto.randomBytes(32).toString('hex'),
222
349
  createApiKey: (userId, keyName) => {
223
- try {
224
- const apiKey = apiKeysDb.generateApiKey();
225
- const stmt = db.prepare('INSERT INTO api_keys (user_id, key_name, api_key) VALUES (?, ?, ?)');
226
- const result = stmt.run(userId, keyName, apiKey);
227
- return { id: result.lastInsertRowid, keyName, apiKey };
228
- }
229
- catch (err) {
230
- throw err;
231
- }
350
+ const apiKey = apiKeysDb.generateApiKey();
351
+ const row = store.insert('api_keys', {
352
+ user_id: userId,
353
+ key_name: keyName,
354
+ api_key: apiKey,
355
+ created_at: nowIso(),
356
+ last_used: null,
357
+ is_active: true,
358
+ });
359
+ return { id: row.id, keyName, apiKey };
232
360
  },
233
- // Get all API keys for a user
234
361
  getApiKeys: (userId) => {
235
- try {
236
- const rows = db.prepare('SELECT id, key_name, api_key, created_at, last_used, is_active FROM api_keys WHERE user_id = ? ORDER BY created_at DESC').all(userId);
237
- return rows;
238
- }
239
- catch (err) {
240
- throw err;
241
- }
362
+ const rows = store.filterWhere('api_keys', (r) => r.user_id === userId);
363
+ // Match the old ORDER BY created_at DESC behaviour.
364
+ return rows
365
+ .slice()
366
+ .sort((a, b) => String(b.created_at).localeCompare(String(a.created_at)))
367
+ .map((r) => ({
368
+ id: r.id,
369
+ key_name: r.key_name,
370
+ api_key: r.api_key,
371
+ created_at: r.created_at,
372
+ last_used: r.last_used,
373
+ is_active: r.is_active ? 1 : 0,
374
+ }));
242
375
  },
243
- // Validate API key and get user
244
376
  validateApiKey: (apiKey) => {
245
- try {
246
- const row = db.prepare(`
247
- SELECT u.id, u.username, ak.id as api_key_id
248
- FROM api_keys ak
249
- JOIN users u ON ak.user_id = u.id
250
- WHERE ak.api_key = ? AND ak.is_active = 1 AND u.is_active = 1
251
- `).get(apiKey);
252
- if (row) {
253
- // Update last_used timestamp
254
- db.prepare('UPDATE api_keys SET last_used = CURRENT_TIMESTAMP WHERE id = ?').run(row.api_key_id);
255
- }
256
- return row;
257
- }
258
- catch (err) {
259
- throw err;
260
- }
261
- },
262
- // Delete an API key
263
- deleteApiKey: (userId, apiKeyId) => {
264
- try {
265
- const stmt = db.prepare('DELETE FROM api_keys WHERE id = ? AND user_id = ?');
266
- const result = stmt.run(apiKeyId, userId);
267
- return result.changes > 0;
268
- }
269
- catch (err) {
270
- throw err;
271
- }
272
- },
273
- // Toggle API key active status
274
- toggleApiKey: (userId, apiKeyId, isActive) => {
275
- try {
276
- const stmt = db.prepare('UPDATE api_keys SET is_active = ? WHERE id = ? AND user_id = ?');
277
- const result = stmt.run(isActive ? 1 : 0, apiKeyId, userId);
278
- return result.changes > 0;
279
- }
280
- catch (err) {
281
- throw err;
282
- }
283
- }
377
+ const key = store.findWhere('api_keys', (r) => r.api_key === apiKey && r.is_active);
378
+ if (!key)
379
+ return undefined;
380
+ const user = store.findWhere('users', (r) => r.id === key.user_id && r.is_active);
381
+ if (!user)
382
+ return undefined;
383
+ // Mirror the SQL-era side effect: stamp last_used. Only relevant
384
+ // for the "which key was used last" display in the UI.
385
+ store.updateWhere('api_keys', (r) => r.id === key.id, { last_used: nowIso() });
386
+ return {
387
+ id: user.id,
388
+ username: user.username,
389
+ api_key_id: key.id,
390
+ };
391
+ },
392
+ deleteApiKey: (userId, apiKeyId) => store.deleteWhere('api_keys', (r) => r.id === apiKeyId && r.user_id === userId) > 0,
393
+ toggleApiKey: (userId, apiKeyId, isActive) => store.updateWhere('api_keys', (r) => r.id === apiKeyId && r.user_id === userId, { is_active: Boolean(isActive) }) > 0,
284
394
  };
285
- // User credentials database operations (for GitHub tokens, GitLab tokens, etc.)
395
+ // ---------------------------------------------------------------------------
396
+ // Credentials (GitHub tokens, etc.) operations
397
+ // ---------------------------------------------------------------------------
286
398
  const credentialsDb = {
287
- // Create a new credential
288
399
  createCredential: (userId, credentialName, credentialType, credentialValue, description = null) => {
289
- try {
290
- const stmt = db.prepare('INSERT INTO user_credentials (user_id, credential_name, credential_type, credential_value, description) VALUES (?, ?, ?, ?, ?)');
291
- const result = stmt.run(userId, credentialName, credentialType, credentialValue, description);
292
- return { id: result.lastInsertRowid, credentialName, credentialType };
293
- }
294
- catch (err) {
295
- throw err;
296
- }
400
+ const row = store.insert('user_credentials', {
401
+ user_id: userId,
402
+ credential_name: credentialName,
403
+ credential_type: credentialType,
404
+ credential_value: credentialValue,
405
+ description,
406
+ created_at: nowIso(),
407
+ is_active: true,
408
+ });
409
+ return { id: row.id, credentialName, credentialType };
297
410
  },
298
- // Get all credentials for a user, optionally filtered by type
299
411
  getCredentials: (userId, credentialType = null) => {
300
- try {
301
- let query = 'SELECT id, credential_name, credential_type, description, created_at, is_active FROM user_credentials WHERE user_id = ?';
302
- const params = [userId];
303
- if (credentialType) {
304
- query += ' AND credential_type = ?';
305
- params.push(credentialType);
306
- }
307
- query += ' ORDER BY created_at DESC';
308
- const rows = db.prepare(query).all(...params);
309
- return rows;
310
- }
311
- catch (err) {
312
- throw err;
313
- }
412
+ const rows = store.filterWhere('user_credentials', (r) => r.user_id === userId && (credentialType == null || r.credential_type === credentialType));
413
+ return rows
414
+ .slice()
415
+ .sort((a, b) => String(b.created_at).localeCompare(String(a.created_at)))
416
+ .map((r) => ({
417
+ id: r.id,
418
+ credential_name: r.credential_name,
419
+ credential_type: r.credential_type,
420
+ description: r.description,
421
+ created_at: r.created_at,
422
+ is_active: r.is_active ? 1 : 0,
423
+ }));
314
424
  },
315
- // Get active credential value for a user by type (returns most recent active)
316
425
  getActiveCredential: (userId, credentialType) => {
317
- try {
318
- const row = db.prepare('SELECT credential_value FROM user_credentials WHERE user_id = ? AND credential_type = ? AND is_active = 1 ORDER BY created_at DESC LIMIT 1').get(userId, credentialType);
319
- return row?.credential_value || null;
320
- }
321
- catch (err) {
322
- throw err;
323
- }
426
+ const rows = store.filterWhere('user_credentials', (r) => r.user_id === userId && r.credential_type === credentialType && r.is_active);
427
+ if (rows.length === 0)
428
+ return null;
429
+ // "Most recent active" — mirror ORDER BY created_at DESC LIMIT 1
430
+ rows.sort((a, b) => String(b.created_at).localeCompare(String(a.created_at)));
431
+ return rows[0].credential_value;
324
432
  },
325
- // Delete a credential
326
- deleteCredential: (userId, credentialId) => {
327
- try {
328
- const stmt = db.prepare('DELETE FROM user_credentials WHERE id = ? AND user_id = ?');
329
- const result = stmt.run(credentialId, userId);
330
- return result.changes > 0;
331
- }
332
- catch (err) {
333
- throw err;
334
- }
433
+ getCredentialById: (userId, credentialId, credentialType = null) => {
434
+ const row = store.findWhere('user_credentials', (r) => r.id === credentialId && r.user_id === userId && r.is_active
435
+ && (credentialType == null || r.credential_type === credentialType));
436
+ return row || undefined;
335
437
  },
336
- // Toggle credential active status
337
- toggleCredential: (userId, credentialId, isActive) => {
338
- try {
339
- const stmt = db.prepare('UPDATE user_credentials SET is_active = ? WHERE id = ? AND user_id = ?');
340
- const result = stmt.run(isActive ? 1 : 0, credentialId, userId);
341
- return result.changes > 0;
342
- }
343
- catch (err) {
344
- throw err;
345
- }
346
- }
438
+ deleteCredential: (userId, credentialId) => store.deleteWhere('user_credentials', (r) => r.id === credentialId && r.user_id === userId) > 0,
439
+ toggleCredential: (userId, credentialId, isActive) => store.updateWhere('user_credentials', (r) => r.id === credentialId && r.user_id === userId, { is_active: Boolean(isActive) }) > 0,
347
440
  };
441
+ // ---------------------------------------------------------------------------
442
+ // Notification preferences
443
+ // ---------------------------------------------------------------------------
348
444
  const DEFAULT_NOTIFICATION_PREFERENCES = {
349
- channels: {
350
- inApp: false,
351
- webPush: false
352
- },
353
- events: {
354
- actionRequired: true,
355
- stop: true,
356
- error: true
357
- }
445
+ channels: { inApp: false, webPush: false },
446
+ events: { actionRequired: true, stop: true, error: true },
358
447
  };
359
448
  const normalizeNotificationPreferences = (value) => {
360
449
  const source = value && typeof value === 'object' ? value : {};
361
450
  return {
362
451
  channels: {
363
452
  inApp: source.channels?.inApp === true,
364
- webPush: source.channels?.webPush === true
453
+ webPush: source.channels?.webPush === true,
365
454
  },
366
455
  events: {
367
456
  actionRequired: source.events?.actionRequired !== false,
368
457
  stop: source.events?.stop !== false,
369
- error: source.events?.error !== false
370
- }
458
+ error: source.events?.error !== false,
459
+ },
371
460
  };
372
461
  };
373
462
  const notificationPreferencesDb = {
374
463
  getPreferences: (userId) => {
464
+ const row = store.findWhere('user_notification_preferences', (r) => r.user_id === userId);
465
+ if (!row) {
466
+ const defaults = normalizeNotificationPreferences(DEFAULT_NOTIFICATION_PREFERENCES);
467
+ store.insert('user_notification_preferences', {
468
+ user_id: userId,
469
+ preferences_json: JSON.stringify(defaults),
470
+ updated_at: nowIso(),
471
+ }, { autoId: false });
472
+ return defaults;
473
+ }
375
474
  try {
376
- const row = db.prepare('SELECT preferences_json FROM user_notification_preferences WHERE user_id = ?').get(userId);
377
- if (!row) {
378
- const defaults = normalizeNotificationPreferences(DEFAULT_NOTIFICATION_PREFERENCES);
379
- db.prepare('INSERT INTO user_notification_preferences (user_id, preferences_json, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)').run(userId, JSON.stringify(defaults));
380
- return defaults;
381
- }
382
- let parsed;
383
- try {
384
- parsed = JSON.parse(row.preferences_json);
385
- }
386
- catch {
387
- parsed = DEFAULT_NOTIFICATION_PREFERENCES;
388
- }
389
- return normalizeNotificationPreferences(parsed);
475
+ return normalizeNotificationPreferences(JSON.parse(row.preferences_json));
390
476
  }
391
- catch (err) {
392
- throw err;
477
+ catch {
478
+ return normalizeNotificationPreferences(DEFAULT_NOTIFICATION_PREFERENCES);
393
479
  }
394
480
  },
395
481
  updatePreferences: (userId, preferences) => {
396
- try {
397
- const normalized = normalizeNotificationPreferences(preferences);
398
- db.prepare(`INSERT INTO user_notification_preferences (user_id, preferences_json, updated_at)
399
- VALUES (?, ?, CURRENT_TIMESTAMP)
400
- ON CONFLICT(user_id) DO UPDATE SET
401
- preferences_json = excluded.preferences_json,
402
- updated_at = CURRENT_TIMESTAMP`).run(userId, JSON.stringify(normalized));
403
- return normalized;
404
- }
405
- catch (err) {
406
- throw err;
407
- }
408
- }
482
+ const normalized = normalizeNotificationPreferences(preferences);
483
+ store.upsertWhere('user_notification_preferences', (r) => r.user_id === userId, { user_id: userId, preferences_json: JSON.stringify(normalized), updated_at: nowIso() });
484
+ return normalized;
485
+ },
409
486
  };
487
+ // ---------------------------------------------------------------------------
488
+ // Push subscriptions
489
+ // ---------------------------------------------------------------------------
410
490
  const pushSubscriptionsDb = {
411
491
  saveSubscription: (userId, endpoint, keysP256dh, keysAuth) => {
412
- try {
413
- db.prepare(`INSERT INTO push_subscriptions (user_id, endpoint, keys_p256dh, keys_auth)
414
- VALUES (?, ?, ?, ?)
415
- ON CONFLICT(endpoint) DO UPDATE SET
416
- user_id = excluded.user_id,
417
- keys_p256dh = excluded.keys_p256dh,
418
- keys_auth = excluded.keys_auth`).run(userId, endpoint, keysP256dh, keysAuth);
419
- }
420
- catch (err) {
421
- throw err;
422
- }
423
- },
424
- getSubscriptions: (userId) => {
425
- try {
426
- return db.prepare('SELECT endpoint, keys_p256dh, keys_auth FROM push_subscriptions WHERE user_id = ?').all(userId);
427
- }
428
- catch (err) {
429
- throw err;
492
+ // The old SQL path upserted on `endpoint`, updating user_id as
493
+ // well. Preserve that exact behaviour.
494
+ const existing = store.findWhere('push_subscriptions', (r) => r.endpoint === endpoint);
495
+ if (existing) {
496
+ store.updateWhere('push_subscriptions', (r) => r.endpoint === endpoint, {
497
+ user_id: userId,
498
+ keys_p256dh: keysP256dh,
499
+ keys_auth: keysAuth,
500
+ });
501
+ return;
430
502
  }
431
- },
503
+ store.insert('push_subscriptions', {
504
+ user_id: userId,
505
+ endpoint,
506
+ keys_p256dh: keysP256dh,
507
+ keys_auth: keysAuth,
508
+ created_at: nowIso(),
509
+ });
510
+ },
511
+ getSubscriptions: (userId) => store.filterWhere('push_subscriptions', (r) => r.user_id === userId).map((r) => ({
512
+ endpoint: r.endpoint,
513
+ keys_p256dh: r.keys_p256dh,
514
+ keys_auth: r.keys_auth,
515
+ })),
432
516
  removeSubscription: (endpoint) => {
433
- try {
434
- db.prepare('DELETE FROM push_subscriptions WHERE endpoint = ?').run(endpoint);
435
- }
436
- catch (err) {
437
- throw err;
438
- }
517
+ store.deleteWhere('push_subscriptions', (r) => r.endpoint === endpoint);
439
518
  },
440
519
  removeAllForUser: (userId) => {
441
- try {
442
- db.prepare('DELETE FROM push_subscriptions WHERE user_id = ?').run(userId);
443
- }
444
- catch (err) {
445
- throw err;
446
- }
447
- }
520
+ store.deleteWhere('push_subscriptions', (r) => r.user_id === userId);
521
+ },
522
+ };
523
+ // ---------------------------------------------------------------------------
524
+ // VAPID key storage (for web push)
525
+ // ---------------------------------------------------------------------------
526
+ const vapidKeysDb = {
527
+ getLatest: () => {
528
+ if (store.raw.vapid_keys.length === 0)
529
+ return null;
530
+ // Mirror ORDER BY id DESC LIMIT 1 — take the newest row.
531
+ const rows = store.raw.vapid_keys.slice().sort((a, b) => b.id - a.id);
532
+ return { public_key: rows[0].public_key, private_key: rows[0].private_key };
533
+ },
534
+ insert: (publicKey, privateKey) => {
535
+ store.insert('vapid_keys', {
536
+ public_key: publicKey,
537
+ private_key: privateKey,
538
+ created_at: nowIso(),
539
+ });
540
+ },
448
541
  };
449
- // Session custom names database operations
542
+ // ---------------------------------------------------------------------------
543
+ // Session custom names
544
+ // ---------------------------------------------------------------------------
450
545
  const sessionNamesDb = {
451
- // Set (insert or update) a custom session name
452
546
  setName: (sessionId, provider, customName) => {
453
- db.prepare(`
454
- INSERT INTO session_names (session_id, provider, custom_name)
455
- VALUES (?, ?, ?)
456
- ON CONFLICT(session_id, provider)
457
- DO UPDATE SET custom_name = excluded.custom_name, updated_at = CURRENT_TIMESTAMP
458
- `).run(sessionId, provider, customName);
459
- },
460
- // Get a single custom session name
547
+ store.upsertWhere('session_names', (r) => r.session_id === sessionId && r.provider === provider, { session_id: sessionId, provider, custom_name: customName, updated_at: nowIso(), created_at: nowIso() });
548
+ },
461
549
  getName: (sessionId, provider) => {
462
- const row = db.prepare('SELECT custom_name FROM session_names WHERE session_id = ? AND provider = ?').get(sessionId, provider);
550
+ const row = store.findWhere('session_names', (r) => r.session_id === sessionId && r.provider === provider);
463
551
  return row?.custom_name || null;
464
552
  },
465
- // Batch lookup — returns Map<sessionId, customName>
466
553
  getNames: (sessionIds, provider) => {
467
554
  if (!sessionIds.length)
468
555
  return new Map();
469
- const placeholders = sessionIds.map(() => '?').join(',');
470
- const rows = db.prepare(`SELECT session_id, custom_name FROM session_names
471
- WHERE session_id IN (${placeholders}) AND provider = ?`).all(...sessionIds, provider);
472
- return new Map(rows.map(r => [r.session_id, r.custom_name]));
473
- },
474
- // Delete a custom session name
475
- deleteName: (sessionId, provider) => {
476
- return db.prepare('DELETE FROM session_names WHERE session_id = ? AND provider = ?').run(sessionId, provider).changes > 0;
556
+ const lookup = new Set(sessionIds);
557
+ const matches = store.filterWhere('session_names', (r) => r.provider === provider && lookup.has(r.session_id));
558
+ return new Map(matches.map((r) => [r.session_id, r.custom_name]));
477
559
  },
560
+ deleteName: (sessionId, provider) => store.deleteWhere('session_names', (r) => r.session_id === sessionId && r.provider === provider) > 0,
478
561
  };
479
- // Apply custom session names from the database (overrides CLI-generated summaries)
480
562
  function applyCustomSessionNames(sessions, provider) {
481
563
  if (!sessions?.length)
482
564
  return;
483
565
  try {
484
- const ids = sessions.map(s => s.id);
566
+ const ids = sessions.map((s) => s.id);
485
567
  const customNames = sessionNamesDb.getNames(ids, provider);
486
568
  for (const session of sessions) {
487
569
  const custom = customNames.get(session.id);
@@ -493,19 +575,16 @@ function applyCustomSessionNames(sessions, provider) {
493
575
  console.warn(`[DB] Failed to apply custom session names for ${provider}:`, error.message);
494
576
  }
495
577
  }
496
- // App config database operations
578
+ // ---------------------------------------------------------------------------
579
+ // App config (key/value)
580
+ // ---------------------------------------------------------------------------
497
581
  const appConfigDb = {
498
582
  get: (key) => {
499
- try {
500
- const row = db.prepare('SELECT value FROM app_config WHERE key = ?').get(key);
501
- return row?.value || null;
502
- }
503
- catch (err) {
504
- return null;
505
- }
583
+ const row = store.findWhere('app_config', (r) => r.key === key);
584
+ return row?.value || null;
506
585
  },
507
586
  set: (key, value) => {
508
- db.prepare('INSERT INTO app_config (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value').run(key, value);
587
+ store.upsertWhere('app_config', (r) => r.key === key, { key, value, created_at: nowIso() });
509
588
  },
510
589
  getOrCreateJwtSecret: () => {
511
590
  let secret = appConfigDb.get('jwt_secret');
@@ -514,114 +593,117 @@ const appConfigDb = {
514
593
  appConfigDb.set('jwt_secret', secret);
515
594
  }
516
595
  return secret;
517
- }
596
+ },
518
597
  };
519
- // Telegram integration database operations
598
+ // ---------------------------------------------------------------------------
599
+ // Telegram — singleton config + per-user links
600
+ // ---------------------------------------------------------------------------
520
601
  const telegramConfigDb = {
521
602
  get: () => {
522
- try {
523
- return db.prepare('SELECT bot_token, bot_username, updated_at FROM telegram_config WHERE id = 1').get() || null;
524
- }
525
- catch (err) {
526
- console.warn('telegramConfigDb.get failed:', err.message);
603
+ const row = store.raw.telegram_config[0];
604
+ if (!row)
527
605
  return null;
528
- }
606
+ return { bot_token: row.bot_token, bot_username: row.bot_username, updated_at: row.updated_at };
529
607
  },
530
608
  set: (botToken, botUsername = null) => {
531
- db.prepare(`INSERT INTO telegram_config (id, bot_token, bot_username, updated_at)
532
- VALUES (1, ?, ?, CURRENT_TIMESTAMP)
533
- ON CONFLICT(id) DO UPDATE SET
534
- bot_token = excluded.bot_token,
535
- bot_username = excluded.bot_username,
536
- updated_at = CURRENT_TIMESTAMP`).run(botToken, botUsername);
609
+ store.raw.telegram_config = [{
610
+ id: 1,
611
+ bot_token: botToken,
612
+ bot_username: botUsername,
613
+ updated_at: nowIso(),
614
+ }];
615
+ store.save();
537
616
  },
538
617
  clear: () => {
539
- db.prepare('DELETE FROM telegram_config WHERE id = 1').run();
618
+ store.raw.telegram_config = [];
619
+ store.save();
540
620
  },
541
621
  };
542
622
  const telegramLinksDb = {
543
- // Write a fresh pairing code for a user and wipe any prior verification —
544
- // regenerating a code implies "start over", not "keep the old binding".
545
623
  setPairingCode: (userId, code, expiresAt, language) => {
546
- db.prepare(`INSERT INTO telegram_links (user_id, pairing_code, pairing_code_expires_at, language, updated_at)
547
- VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
548
- ON CONFLICT(user_id) DO UPDATE SET
549
- pairing_code = excluded.pairing_code,
550
- pairing_code_expires_at = excluded.pairing_code_expires_at,
551
- language = excluded.language,
552
- chat_id = NULL,
553
- telegram_username = NULL,
554
- verified_at = NULL,
555
- updated_at = CURRENT_TIMESTAMP`).run(userId, code, expiresAt, language);
624
+ store.upsertWhere('telegram_links', (r) => r.user_id === userId, {
625
+ user_id: userId,
626
+ pairing_code: code,
627
+ pairing_code_expires_at: expiresAt,
628
+ language,
629
+ chat_id: null,
630
+ telegram_username: null,
631
+ verified_at: null,
632
+ notifications_enabled: true,
633
+ bridge_enabled: true,
634
+ updated_at: nowIso(),
635
+ });
556
636
  },
557
637
  findByPairingCode: (code) => {
558
- return db.prepare(`SELECT user_id, pairing_code, pairing_code_expires_at, language
559
- FROM telegram_links WHERE pairing_code = ?`).get(code) || null;
638
+ const row = store.findWhere('telegram_links', (r) => r.pairing_code === code);
639
+ if (!row)
640
+ return null;
641
+ return {
642
+ user_id: row.user_id,
643
+ pairing_code: row.pairing_code,
644
+ pairing_code_expires_at: row.pairing_code_expires_at,
645
+ language: row.language,
646
+ };
560
647
  },
561
648
  verify: (userId, chatId, telegramUsername) => {
562
- db.prepare(`UPDATE telegram_links
563
- SET chat_id = ?, telegram_username = ?, verified_at = CURRENT_TIMESTAMP,
564
- pairing_code = NULL, pairing_code_expires_at = NULL, updated_at = CURRENT_TIMESTAMP
565
- WHERE user_id = ?`).run(chatId, telegramUsername, userId);
649
+ store.updateWhere('telegram_links', (r) => r.user_id === userId, {
650
+ chat_id: chatId,
651
+ telegram_username: telegramUsername,
652
+ verified_at: nowIso(),
653
+ pairing_code: null,
654
+ pairing_code_expires_at: null,
655
+ updated_at: nowIso(),
656
+ });
566
657
  },
567
658
  getByUserId: (userId) => {
568
- return db.prepare(`SELECT user_id, chat_id, telegram_username, language, pairing_code, pairing_code_expires_at,
569
- verified_at, notifications_enabled, bridge_enabled, updated_at
570
- FROM telegram_links WHERE user_id = ?`).get(userId) || null;
659
+ const row = store.findWhere('telegram_links', (r) => r.user_id === userId);
660
+ return row ? { ...row } : null;
571
661
  },
572
662
  getByChatId: (chatId) => {
573
- return db.prepare(`SELECT user_id, chat_id, telegram_username, language, notifications_enabled, bridge_enabled
574
- FROM telegram_links WHERE chat_id = ?`).get(chatId) || null;
575
- },
576
- listVerified: () => {
577
- return db.prepare(`SELECT user_id, chat_id, telegram_username, language, notifications_enabled, bridge_enabled
578
- FROM telegram_links WHERE chat_id IS NOT NULL AND verified_at IS NOT NULL`).all();
579
- },
663
+ const row = store.findWhere('telegram_links', (r) => r.chat_id === chatId);
664
+ if (!row)
665
+ return null;
666
+ return {
667
+ user_id: row.user_id,
668
+ chat_id: row.chat_id,
669
+ telegram_username: row.telegram_username,
670
+ language: row.language,
671
+ notifications_enabled: row.notifications_enabled,
672
+ bridge_enabled: row.bridge_enabled,
673
+ };
674
+ },
675
+ listVerified: () => store.filterWhere('telegram_links', (r) => r.chat_id && r.verified_at).map((r) => ({
676
+ user_id: r.user_id,
677
+ chat_id: r.chat_id,
678
+ telegram_username: r.telegram_username,
679
+ language: r.language,
680
+ notifications_enabled: r.notifications_enabled,
681
+ bridge_enabled: r.bridge_enabled,
682
+ })),
580
683
  updatePreferences: (userId, { language, notificationsEnabled, bridgeEnabled }) => {
581
- // Only update keys the caller provided — partial updates are expected
582
- // from the UI (toggling one switch at a time).
583
- const sets = [];
584
- const params = [];
585
- if (language !== undefined) {
586
- sets.push('language = ?');
587
- params.push(language);
588
- }
589
- if (notificationsEnabled !== undefined) {
590
- sets.push('notifications_enabled = ?');
591
- params.push(notificationsEnabled ? 1 : 0);
592
- }
593
- if (bridgeEnabled !== undefined) {
594
- sets.push('bridge_enabled = ?');
595
- params.push(bridgeEnabled ? 1 : 0);
596
- }
597
- if (!sets.length)
598
- return;
599
- sets.push('updated_at = CURRENT_TIMESTAMP');
600
- params.push(userId);
601
- db.prepare(`UPDATE telegram_links SET ${sets.join(', ')} WHERE user_id = ?`).run(...params);
684
+ const patch = { updated_at: nowIso() };
685
+ if (language !== undefined)
686
+ patch.language = language;
687
+ if (notificationsEnabled !== undefined)
688
+ patch.notifications_enabled = Boolean(notificationsEnabled);
689
+ if (bridgeEnabled !== undefined)
690
+ patch.bridge_enabled = Boolean(bridgeEnabled);
691
+ if (Object.keys(patch).length === 1)
692
+ return; // only updated_at → no real change
693
+ store.updateWhere('telegram_links', (r) => r.user_id === userId, patch);
602
694
  },
603
695
  unlink: (userId) => {
604
- db.prepare('DELETE FROM telegram_links WHERE user_id = ?').run(userId);
696
+ store.deleteWhere('telegram_links', (r) => r.user_id === userId);
605
697
  },
606
698
  };
607
- // Backward compatibility - keep old names pointing to new system
699
+ // Back-compat surface older callers used `githubTokensDb.*`; internally
700
+ // they always delegated to credentialsDb with `credential_type='github_token'`.
608
701
  const githubTokensDb = {
609
- createGithubToken: (userId, tokenName, githubToken, description = null) => {
610
- return credentialsDb.createCredential(userId, tokenName, 'github_token', githubToken, description);
611
- },
612
- getGithubTokens: (userId) => {
613
- return credentialsDb.getCredentials(userId, 'github_token');
614
- },
615
- getActiveGithubToken: (userId) => {
616
- return credentialsDb.getActiveCredential(userId, 'github_token');
617
- },
618
- deleteGithubToken: (userId, tokenId) => {
619
- return credentialsDb.deleteCredential(userId, tokenId);
620
- },
621
- toggleGithubToken: (userId, tokenId, isActive) => {
622
- return credentialsDb.toggleCredential(userId, tokenId, isActive);
623
- }
702
+ createGithubToken: (userId, tokenName, githubToken, description = null) => credentialsDb.createCredential(userId, tokenName, 'github_token', githubToken, description),
703
+ getGithubTokens: (userId) => credentialsDb.getCredentials(userId, 'github_token'),
704
+ getActiveGithubToken: (userId) => credentialsDb.getActiveCredential(userId, 'github_token'),
705
+ deleteGithubToken: (userId, tokenId) => credentialsDb.deleteCredential(userId, tokenId),
706
+ toggleGithubToken: (userId, tokenId, isActive) => credentialsDb.toggleCredential(userId, tokenId, isActive),
624
707
  };
625
- export { db, initializeDatabase, userDb, apiKeysDb, credentialsDb, notificationPreferencesDb, pushSubscriptionsDb, sessionNamesDb, applyCustomSessionNames, appConfigDb, telegramConfigDb, telegramLinksDb, githubTokensDb // Backward compatibility
626
- };
708
+ export { db, initializeDatabase, userDb, apiKeysDb, credentialsDb, notificationPreferencesDb, pushSubscriptionsDb, vapidKeysDb, sessionNamesDb, applyCustomSessionNames, appConfigDb, telegramConfigDb, telegramLinksDb, githubTokensDb, };
627
709
  //# sourceMappingURL=db.js.map