@zincapp/znvault-cli 2.19.0 → 2.19.2

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 (117) hide show
  1. package/dist/commands/dynamic-secrets/connection.d.ts +17 -0
  2. package/dist/commands/dynamic-secrets/connection.d.ts.map +1 -0
  3. package/dist/commands/dynamic-secrets/connection.js +217 -0
  4. package/dist/commands/dynamic-secrets/connection.js.map +1 -0
  5. package/dist/commands/dynamic-secrets/creds.d.ts +5 -0
  6. package/dist/commands/dynamic-secrets/creds.d.ts.map +1 -0
  7. package/dist/commands/dynamic-secrets/creds.js +39 -0
  8. package/dist/commands/dynamic-secrets/creds.js.map +1 -0
  9. package/dist/commands/dynamic-secrets/helpers.d.ts +5 -0
  10. package/dist/commands/dynamic-secrets/helpers.d.ts.map +1 -0
  11. package/dist/commands/dynamic-secrets/helpers.js +36 -0
  12. package/dist/commands/dynamic-secrets/helpers.js.map +1 -0
  13. package/dist/commands/dynamic-secrets/index.d.ts +7 -0
  14. package/dist/commands/dynamic-secrets/index.d.ts.map +1 -0
  15. package/dist/commands/dynamic-secrets/index.js +173 -0
  16. package/dist/commands/dynamic-secrets/index.js.map +1 -0
  17. package/dist/commands/dynamic-secrets/lease.d.ts +11 -0
  18. package/dist/commands/dynamic-secrets/lease.d.ts.map +1 -0
  19. package/dist/commands/dynamic-secrets/lease.js +137 -0
  20. package/dist/commands/dynamic-secrets/lease.js.map +1 -0
  21. package/dist/commands/dynamic-secrets/role.d.ts +15 -0
  22. package/dist/commands/dynamic-secrets/role.d.ts.map +1 -0
  23. package/dist/commands/dynamic-secrets/role.js +184 -0
  24. package/dist/commands/dynamic-secrets/role.js.map +1 -0
  25. package/dist/commands/dynamic-secrets/types.d.ts +125 -0
  26. package/dist/commands/dynamic-secrets/types.d.ts.map +1 -0
  27. package/dist/commands/dynamic-secrets/types.js +3 -0
  28. package/dist/commands/dynamic-secrets/types.js.map +1 -0
  29. package/dist/commands/dynamic-secrets.d.ts +6 -2
  30. package/dist/commands/dynamic-secrets.d.ts.map +1 -1
  31. package/dist/commands/dynamic-secrets.js +6 -754
  32. package/dist/commands/dynamic-secrets.js.map +1 -1
  33. package/dist/commands/policy/attachments.d.ts +9 -0
  34. package/dist/commands/policy/attachments.d.ts.map +1 -0
  35. package/dist/commands/policy/attachments.js +161 -0
  36. package/dist/commands/policy/attachments.js.map +1 -0
  37. package/dist/commands/policy/crud.d.ts +8 -0
  38. package/dist/commands/policy/crud.d.ts.map +1 -0
  39. package/dist/commands/policy/crud.js +232 -0
  40. package/dist/commands/policy/crud.js.map +1 -0
  41. package/dist/commands/policy/helpers.d.ts +13 -0
  42. package/dist/commands/policy/helpers.d.ts.map +1 -0
  43. package/dist/commands/policy/helpers.js +61 -0
  44. package/dist/commands/policy/helpers.js.map +1 -0
  45. package/dist/commands/policy/index.d.ts +7 -0
  46. package/dist/commands/policy/index.d.ts.map +1 -0
  47. package/dist/commands/policy/index.js +160 -0
  48. package/dist/commands/policy/index.js.map +1 -0
  49. package/dist/commands/policy/io.d.ts +4 -0
  50. package/dist/commands/policy/io.d.ts.map +1 -0
  51. package/dist/commands/policy/io.js +65 -0
  52. package/dist/commands/policy/io.js.map +1 -0
  53. package/dist/commands/policy/list.d.ts +4 -0
  54. package/dist/commands/policy/list.d.ts.map +1 -0
  55. package/dist/commands/policy/list.js +99 -0
  56. package/dist/commands/policy/list.js.map +1 -0
  57. package/dist/commands/policy/test.d.ts +3 -0
  58. package/dist/commands/policy/test.d.ts.map +1 -0
  59. package/dist/commands/policy/test.js +58 -0
  60. package/dist/commands/policy/test.js.map +1 -0
  61. package/dist/commands/policy/types.d.ts +84 -0
  62. package/dist/commands/policy/types.d.ts.map +1 -0
  63. package/dist/commands/policy/types.js +3 -0
  64. package/dist/commands/policy/types.js.map +1 -0
  65. package/dist/commands/policy.d.ts +6 -2
  66. package/dist/commands/policy.d.ts.map +1 -1
  67. package/dist/commands/policy.js +4 -770
  68. package/dist/commands/policy.js.map +1 -1
  69. package/dist/lib/config/index.d.ts +1 -1
  70. package/dist/lib/config/index.d.ts.map +1 -1
  71. package/dist/lib/config/index.js +1 -1
  72. package/dist/lib/config/index.js.map +1 -1
  73. package/dist/lib/config/store.d.ts +10 -0
  74. package/dist/lib/config/store.d.ts.map +1 -1
  75. package/dist/lib/config/store.js +49 -10
  76. package/dist/lib/config/store.js.map +1 -1
  77. package/dist/lib/db/audit.d.ts +16 -0
  78. package/dist/lib/db/audit.d.ts.map +1 -0
  79. package/dist/lib/db/audit.js +60 -0
  80. package/dist/lib/db/audit.js.map +1 -0
  81. package/dist/lib/db/client.d.ts +27 -0
  82. package/dist/lib/db/client.d.ts.map +1 -0
  83. package/dist/lib/db/client.js +70 -0
  84. package/dist/lib/db/client.js.map +1 -0
  85. package/dist/lib/db/emergency.d.ts +50 -0
  86. package/dist/lib/db/emergency.d.ts.map +1 -0
  87. package/dist/lib/db/emergency.js +180 -0
  88. package/dist/lib/db/emergency.js.map +1 -0
  89. package/dist/lib/db/health.d.ts +14 -0
  90. package/dist/lib/db/health.d.ts.map +1 -0
  91. package/dist/lib/db/health.js +177 -0
  92. package/dist/lib/db/health.js.map +1 -0
  93. package/dist/lib/db/index.d.ts +56 -0
  94. package/dist/lib/db/index.d.ts.map +1 -0
  95. package/dist/lib/db/index.js +107 -0
  96. package/dist/lib/db/index.js.map +1 -0
  97. package/dist/lib/db/lockdown.d.ts +15 -0
  98. package/dist/lib/db/lockdown.d.ts.map +1 -0
  99. package/dist/lib/db/lockdown.js +67 -0
  100. package/dist/lib/db/lockdown.js.map +1 -0
  101. package/dist/lib/db/tenants.d.ts +14 -0
  102. package/dist/lib/db/tenants.d.ts.map +1 -0
  103. package/dist/lib/db/tenants.js +88 -0
  104. package/dist/lib/db/tenants.js.map +1 -0
  105. package/dist/lib/db/types.d.ts +95 -0
  106. package/dist/lib/db/types.d.ts.map +1 -0
  107. package/dist/lib/db/types.js +3 -0
  108. package/dist/lib/db/types.js.map +1 -0
  109. package/dist/lib/db/users.d.ts +16 -0
  110. package/dist/lib/db/users.d.ts.map +1 -0
  111. package/dist/lib/db/users.js +95 -0
  112. package/dist/lib/db/users.js.map +1 -0
  113. package/dist/lib/db.d.ts +4 -112
  114. package/dist/lib/db.d.ts.map +1 -1
  115. package/dist/lib/db.js +4 -726
  116. package/dist/lib/db.js.map +1 -1
  117. package/package.json +1 -1
package/dist/lib/db.js CHANGED
@@ -1,729 +1,7 @@
1
- import pg from 'pg';
2
- import bcryptjs from 'bcryptjs';
3
- import { getLocalConfig } from './local.js';
4
- import { REDIS_PING_TIMEOUT_MS, REDIS_SENTINEL_TIMEOUT_MS } from './constants.js';
5
- const { Client } = pg;
1
+ // Path: src/lib/db.ts
6
2
  /**
7
- * Database client for direct PostgreSQL operations.
8
- * Used for local mode (running on vault nodes) and emergency operations.
3
+ * Database client re-exports for backward compatibility.
4
+ * The actual implementation has been modularized into src/lib/db/
9
5
  */
10
- export class LocalDBClient {
11
- client;
12
- connected = false;
13
- constructor() {
14
- const config = getLocalConfig();
15
- if (!config) {
16
- throw new Error('Database configuration not available.\n' +
17
- 'Either set DATABASE_URL environment variable or run with sudo on a vault node.');
18
- }
19
- this.client = new Client({
20
- connectionString: config.databaseUrl,
21
- ssl: config.databaseSsl ? { rejectUnauthorized: false } : false,
22
- });
23
- }
24
- async connect() {
25
- if (!this.connected) {
26
- await this.client.connect();
27
- this.connected = true;
28
- }
29
- }
30
- async close() {
31
- if (this.connected) {
32
- await this.client.end();
33
- this.connected = false;
34
- }
35
- }
36
- // Alias for compatibility
37
- async disconnect() {
38
- return this.close();
39
- }
40
- async query(sql, params) {
41
- await this.connect();
42
- const result = await this.client.query(sql, params);
43
- return result.rows;
44
- }
45
- async queryOne(sql, params) {
46
- const rows = await this.query(sql, params);
47
- return rows[0] ?? null;
48
- }
49
- // ============ Health ============
50
- async health() {
51
- await this.connect();
52
- // Get basic stats - check database connectivity
53
- const dbTime = await this.queryOne('SELECT NOW() as now');
54
- // Get version from manifest or default
55
- const version = await this.getVaultVersion();
56
- // Check HA from environment
57
- const haEnabled = process.env.HA_ENABLED === 'true';
58
- const nodeId = process.env.HA_NODE_ID ?? 'standalone';
59
- // Get PostgreSQL status
60
- const pgStatus = await this.getPostgresStatus();
61
- // Get Redis status
62
- const redisStatus = await this.getRedisStatus();
63
- // Get cluster info from Redis if available
64
- let clusterSize = 1;
65
- let isLeader = false;
66
- if (haEnabled && redisStatus.status === 'ok') {
67
- const clusterInfo = await this.getClusterInfoFromRedis();
68
- clusterSize = clusterInfo.nodeCount;
69
- isLeader = clusterInfo.leaderNodeId === nodeId;
70
- }
71
- return {
72
- status: 'ok',
73
- version,
74
- uptime: process.uptime(),
75
- timestamp: dbTime?.now.toISOString() ?? new Date().toISOString(),
76
- database: pgStatus,
77
- redis: redisStatus.status !== 'unavailable' ? redisStatus : undefined,
78
- ha: haEnabled ? {
79
- enabled: true,
80
- nodeId,
81
- isLeader,
82
- clusterSize,
83
- } : undefined,
84
- };
85
- }
86
- async getVaultVersion() {
87
- // Try to read from manifest file (MANIFEST.json inside release)
88
- try {
89
- const fs = await import('node:fs');
90
- const manifestPaths = [
91
- '/opt/znvault/current/MANIFEST.json',
92
- '/opt/znvault/current/manifest.json',
93
- ];
94
- for (const manifestPath of manifestPaths) {
95
- if (fs.existsSync(manifestPath)) {
96
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
97
- return manifest.version ?? '1.2.9';
98
- }
99
- }
100
- }
101
- catch {
102
- // Ignore
103
- }
104
- return process.env.npm_package_version ?? '1.2.9';
105
- }
106
- async getPostgresStatus() {
107
- try {
108
- // Check if this is primary or replica
109
- const recovery = await this.queryOne('SELECT pg_is_in_recovery() as in_recovery');
110
- if (recovery?.in_recovery) {
111
- // This is a replica - check replication lag
112
- const lag = await this.queryOne("SELECT pg_wal_lsn_diff(pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn()) as lag_bytes");
113
- return {
114
- status: 'ok',
115
- role: 'replica',
116
- replicationLag: lag ? parseInt(lag.lag_bytes, 10) : 0,
117
- };
118
- }
119
- // This is primary - check replication status
120
- const replicas = await this.query('SELECT client_addr, state FROM pg_stat_replication');
121
- return {
122
- status: 'ok',
123
- role: 'primary',
124
- replicationLag: replicas.length > 0 ? 0 : undefined,
125
- };
126
- }
127
- catch {
128
- return { status: 'ok' }; // Basic connection works
129
- }
130
- }
131
- async getRedisStatus() {
132
- const sentinelNodes = process.env.REDIS_SENTINEL_NODES;
133
- const sentinelMaster = process.env.REDIS_SENTINEL_MASTER ?? 'znvault-master';
134
- if (!sentinelNodes) {
135
- // Check for simple Redis URL
136
- if (process.env.REDIS_URL) {
137
- try {
138
- const result = await this.execAsync(`redis-cli -u "${process.env.REDIS_URL}" PING 2>/dev/null`, REDIS_PING_TIMEOUT_MS);
139
- return { status: result.trim() === 'PONG' ? 'ok' : 'error' };
140
- }
141
- catch {
142
- return { status: 'error' };
143
- }
144
- }
145
- return { status: 'unavailable' };
146
- }
147
- // Check Redis Sentinel nodes in parallel
148
- try {
149
- const nodes = sentinelNodes.split(',');
150
- // Check all sentinel nodes in parallel
151
- const nodeResults = await Promise.allSettled(nodes.map(async (node) => {
152
- const [host, port] = node.split(':');
153
- const result = await this.execAsync(`redis-cli -h ${host} -p ${port} SENTINEL get-master-addr-by-name ${sentinelMaster} 2>/dev/null`, REDIS_SENTINEL_TIMEOUT_MS);
154
- return result.trim();
155
- }));
156
- let healthyNodes = 0;
157
- let masterHost = '';
158
- for (const result of nodeResults) {
159
- if (result.status === 'fulfilled' && result.value) {
160
- healthyNodes++;
161
- if (!masterHost) {
162
- const lines = result.value.split('\n');
163
- masterHost = lines[0] ?? '';
164
- }
165
- }
166
- }
167
- return {
168
- status: healthyNodes >= 2 ? 'ok' : (healthyNodes > 0 ? 'degraded' : 'error'),
169
- sentinelNodes: healthyNodes,
170
- master: masterHost || undefined,
171
- };
172
- }
173
- catch {
174
- return { status: 'error' };
175
- }
176
- }
177
- /**
178
- * Execute a command asynchronously with timeout
179
- */
180
- async execAsync(command, timeoutMs) {
181
- const { exec } = await import('node:child_process');
182
- const { promisify } = await import('node:util');
183
- const execPromise = promisify(exec);
184
- const { stdout } = await execPromise(command, {
185
- encoding: 'utf-8',
186
- timeout: timeoutMs,
187
- });
188
- return stdout;
189
- }
190
- async getClusterInfoFromRedis() {
191
- const sentinelNodes = process.env.REDIS_SENTINEL_NODES;
192
- const sentinelMaster = process.env.REDIS_SENTINEL_MASTER ?? 'znvault-master';
193
- if (!sentinelNodes) {
194
- return { nodeCount: 1, leaderNodeId: null };
195
- }
196
- try {
197
- const nodes = sentinelNodes.split(',');
198
- const [host, port] = nodes[0].split(':');
199
- // Get master info
200
- const masterResult = await this.execAsync(`redis-cli -h ${host} -p ${port} SENTINEL get-master-addr-by-name ${sentinelMaster} 2>/dev/null`, 3000);
201
- const masterHost = masterResult.trim().split('\n')[0];
202
- // Try to get cluster nodes from Redis
203
- const masterPort = masterResult.trim().split('\n')[1] ?? '6379';
204
- // Fetch nodes and leader in parallel
205
- const [nodesResult, leaderResult] = await Promise.all([
206
- this.execAsync(`redis-cli -h ${masterHost} -p ${masterPort} HGETALL 'zn-vault:nodes' 2>/dev/null`, 3000),
207
- this.execAsync(`redis-cli -h ${masterHost} -p ${masterPort} GET 'zn-vault:leader' 2>/dev/null`, 3000),
208
- ]);
209
- // Parse node data - HGETALL returns key1, value1, key2, value2, etc.
210
- const lines = nodesResult.trim().split('\n').filter(l => l);
211
- const nodeCount = Math.max(Math.floor(lines.length / 2), 1);
212
- const leaderNodeId = leaderResult.trim() || null;
213
- return { nodeCount, leaderNodeId };
214
- }
215
- catch {
216
- return { nodeCount: 3, leaderNodeId: null }; // Default assumption for 3-node cluster
217
- }
218
- }
219
- // ============ Cluster ============
220
- async clusterStatus() {
221
- await this.connect();
222
- // Check HA from environment
223
- const haEnabled = process.env.HA_ENABLED === 'true';
224
- const nodeId = process.env.HA_NODE_ID ?? 'unknown';
225
- // Get cluster nodes from ha_nodes table if it exists
226
- let nodes = [];
227
- try {
228
- const dbNodes = await this.query(`
229
- SELECT node_id, advertised_host, advertised_port, is_leader, last_heartbeat, status
230
- FROM ha_nodes
231
- ORDER BY node_id
232
- `);
233
- nodes = dbNodes.map(n => ({
234
- nodeId: n.node_id,
235
- host: n.advertised_host,
236
- port: n.advertised_port,
237
- isLeader: n.is_leader,
238
- isHealthy: n.status === 'healthy',
239
- lastHeartbeat: n.last_heartbeat.toISOString(),
240
- }));
241
- }
242
- catch {
243
- // Table might not exist in non-HA setups
244
- }
245
- // Find leader
246
- const leader = nodes.find(n => n.isLeader);
247
- return {
248
- enabled: haEnabled,
249
- nodeId,
250
- isLeader: leader?.nodeId === nodeId,
251
- leaderNodeId: leader?.nodeId ?? null,
252
- nodes,
253
- };
254
- }
255
- // ============ Tenants ============
256
- async listTenants(options) {
257
- // Use single query with subqueries to avoid N+1 when withUsage is true
258
- const withUsage = options?.withUsage ?? false;
259
- let sql;
260
- if (withUsage) {
261
- // Single query with aggregated counts using subqueries
262
- sql = `
263
- SELECT t.id, t.name, t.status, t.max_secrets, t.max_kms_keys, t.contact_email,
264
- t.created_at, t.updated_at,
265
- (SELECT COUNT(*) FROM secrets WHERE tenant = t.id) as secrets_count,
266
- (SELECT COUNT(*) FROM kms_keys WHERE tenant_id = t.id) as kms_keys_count,
267
- (SELECT COUNT(*) FROM users WHERE tenant_id = t.id) as users_count,
268
- (SELECT COUNT(*) FROM api_keys WHERE tenant_id = t.id) as api_keys_count
269
- FROM tenants t
270
- `;
271
- }
272
- else {
273
- sql = `
274
- SELECT t.id, t.name, t.status, t.max_secrets, t.max_kms_keys, t.contact_email,
275
- t.created_at, t.updated_at
276
- FROM tenants t
277
- `;
278
- }
279
- const params = [];
280
- if (options?.status) {
281
- sql += ' WHERE t.status = $1';
282
- params.push(options.status);
283
- }
284
- sql += ' ORDER BY t.name';
285
- const rows = await this.query(sql, params);
286
- return rows.map(r => {
287
- const tenant = {
288
- id: r.id,
289
- name: r.name,
290
- status: r.status,
291
- maxSecrets: r.max_secrets ?? undefined,
292
- maxKmsKeys: r.max_kms_keys ?? undefined,
293
- contactEmail: r.contact_email ?? undefined,
294
- createdAt: r.created_at.toISOString(),
295
- updatedAt: r.updated_at.toISOString(),
296
- };
297
- if (withUsage) {
298
- tenant.usage = {
299
- secretsCount: parseInt(r.secrets_count ?? '0', 10),
300
- kmsKeysCount: parseInt(r.kms_keys_count ?? '0', 10),
301
- storageUsedMb: 0, // Would need additional calculation
302
- usersCount: parseInt(r.users_count ?? '0', 10),
303
- apiKeysCount: parseInt(r.api_keys_count ?? '0', 10),
304
- };
305
- }
306
- return tenant;
307
- });
308
- }
309
- async getTenant(id, withUsage) {
310
- const row = await this.queryOne('SELECT * FROM tenants WHERE id = $1', [id]);
311
- if (!row)
312
- return null;
313
- const tenant = {
314
- id: row.id,
315
- name: row.name,
316
- status: row.status,
317
- maxSecrets: row.max_secrets ?? undefined,
318
- maxKmsKeys: row.max_kms_keys ?? undefined,
319
- contactEmail: row.contact_email ?? undefined,
320
- createdAt: row.created_at.toISOString(),
321
- updatedAt: row.updated_at.toISOString(),
322
- };
323
- if (withUsage) {
324
- tenant.usage = await this.getTenantUsage(id);
325
- }
326
- return tenant;
327
- }
328
- async getTenantUsage(id) {
329
- const secrets = await this.queryOne('SELECT COUNT(*) as count FROM secrets WHERE tenant = $1', [id]);
330
- const kmsKeys = await this.queryOne('SELECT COUNT(*) as count FROM kms_keys WHERE tenant_id = $1', [id]);
331
- const users = await this.queryOne('SELECT COUNT(*) as count FROM users WHERE tenant_id = $1', [id]);
332
- const apiKeys = await this.queryOne('SELECT COUNT(*) as count FROM api_keys WHERE tenant_id = $1', [id]);
333
- return {
334
- secretsCount: parseInt(secrets?.count ?? '0', 10),
335
- kmsKeysCount: parseInt(kmsKeys?.count ?? '0', 10),
336
- storageUsedMb: 0, // Would need to calculate
337
- usersCount: parseInt(users?.count ?? '0', 10),
338
- apiKeysCount: parseInt(apiKeys?.count ?? '0', 10),
339
- };
340
- }
341
- // ============ Users ============
342
- async listUsers(options) {
343
- let sql = `
344
- SELECT id, username, email, role, tenant_id, status, totp_enabled,
345
- failed_attempts, locked_until, last_login, created_at, updated_at
346
- FROM users
347
- WHERE 1=1
348
- `;
349
- const params = [];
350
- let paramIndex = 1;
351
- if (options?.tenantId) {
352
- sql += ` AND tenant_id = $${paramIndex++}`;
353
- params.push(options.tenantId);
354
- }
355
- if (options?.role) {
356
- sql += ` AND role = $${paramIndex++}`;
357
- params.push(options.role);
358
- }
359
- if (options?.status) {
360
- sql += ` AND status = $${paramIndex++}`;
361
- params.push(options.status);
362
- }
363
- sql += ' ORDER BY username';
364
- const rows = await this.query(sql, params);
365
- return rows.map(r => ({
366
- id: r.id,
367
- username: r.username,
368
- email: r.email ?? undefined,
369
- role: r.role,
370
- tenantId: r.tenant_id ?? undefined,
371
- status: r.status,
372
- totpEnabled: r.totp_enabled,
373
- failedAttempts: r.failed_attempts,
374
- lockedUntil: r.locked_until?.toISOString(),
375
- lastLogin: r.last_login?.toISOString(),
376
- createdAt: r.created_at.toISOString(),
377
- updatedAt: r.updated_at.toISOString(),
378
- }));
379
- }
380
- async getUser(id) {
381
- const row = await this.queryOne('SELECT * FROM users WHERE id = $1', [id]);
382
- if (!row)
383
- return null;
384
- return {
385
- id: row.id,
386
- username: row.username,
387
- email: row.email ?? undefined,
388
- role: row.role,
389
- tenantId: row.tenant_id ?? undefined,
390
- status: row.status,
391
- totpEnabled: row.totp_enabled,
392
- failedAttempts: row.failed_attempts,
393
- lockedUntil: row.locked_until?.toISOString(),
394
- lastLogin: row.last_login?.toISOString(),
395
- createdAt: row.created_at.toISOString(),
396
- updatedAt: row.updated_at.toISOString(),
397
- };
398
- }
399
- async getUserByUsername(username) {
400
- const row = await this.queryOne('SELECT * FROM users WHERE username = $1 OR email = $1', [username]);
401
- if (!row)
402
- return null;
403
- return {
404
- id: row.id,
405
- username: row.username,
406
- email: row.email ?? undefined,
407
- role: row.role,
408
- tenantId: row.tenant_id ?? undefined,
409
- status: row.status,
410
- totpEnabled: row.totp_enabled,
411
- failedAttempts: row.failed_attempts,
412
- lockedUntil: row.locked_until?.toISOString(),
413
- lastLogin: row.last_login?.toISOString(),
414
- createdAt: row.created_at.toISOString(),
415
- updatedAt: row.updated_at.toISOString(),
416
- };
417
- }
418
- // ============ Superadmins ============
419
- async listSuperadmins() {
420
- const users = await this.listUsers({ role: 'superadmin' });
421
- return users.map(u => ({
422
- id: u.id,
423
- username: u.username,
424
- email: u.email,
425
- status: u.status,
426
- totpEnabled: u.totpEnabled,
427
- failedAttempts: u.failedAttempts,
428
- lockedUntil: u.lockedUntil,
429
- lastLogin: u.lastLogin,
430
- createdAt: u.createdAt,
431
- }));
432
- }
433
- // ============ Lockdown ============
434
- async getLockdownStatus() {
435
- const row = await this.queryOne('SELECT * FROM lockdown_state ORDER BY updated_at DESC LIMIT 1');
436
- if (!row) {
437
- return {
438
- scope: 'SYSTEM',
439
- status: 'NORMAL',
440
- escalationCount: 0,
441
- };
442
- }
443
- return {
444
- scope: row.scope,
445
- tenantId: row.tenant_id ?? undefined,
446
- status: row.status,
447
- reason: row.reason ?? undefined,
448
- triggeredAt: row.triggered_at?.toISOString(),
449
- triggeredBy: row.triggered_by ?? undefined,
450
- escalationCount: row.escalation_count,
451
- };
452
- }
453
- async getLockdownHistory(limit = 50) {
454
- const rows = await this.query('SELECT * FROM lockdown_history ORDER BY created_at DESC LIMIT $1', [limit]);
455
- return rows.map(r => ({
456
- id: r.id,
457
- previousStatus: r.previous_status,
458
- newStatus: r.new_status,
459
- transitionReason: r.transition_reason,
460
- changedByUserId: r.changed_by_user_id ?? undefined,
461
- changedBySystem: r.changed_by_system,
462
- ts: r.created_at.toISOString(),
463
- }));
464
- }
465
- async getThreats(options) {
466
- let sql = 'SELECT * FROM threat_events WHERE 1=1';
467
- const params = [];
468
- let paramIndex = 1;
469
- if (options?.category) {
470
- sql += ` AND category = $${paramIndex++}`;
471
- params.push(options.category);
472
- }
473
- if (options?.since) {
474
- sql += ` AND created_at >= $${paramIndex++}`;
475
- params.push(new Date(options.since));
476
- }
477
- sql += ` ORDER BY created_at DESC LIMIT $${paramIndex}`;
478
- params.push(options?.limit ?? 100);
479
- const rows = await this.query(sql, params);
480
- return rows.map(r => ({
481
- id: r.id,
482
- ts: r.created_at.toISOString(),
483
- tenantId: r.tenant_id ?? undefined,
484
- userId: r.user_id ?? undefined,
485
- ip: r.ip,
486
- userAgent: r.user_agent ?? undefined,
487
- category: r.category,
488
- signal: r.signal,
489
- suggestedLevel: r.suggested_level,
490
- endpoint: r.endpoint,
491
- method: r.method,
492
- statusCode: r.status_code,
493
- escalated: r.escalated,
494
- }));
495
- }
496
- // ============ Audit ============
497
- async listAudit(options) {
498
- let sql = 'SELECT * FROM audit_log WHERE 1=1';
499
- const params = [];
500
- let paramIndex = 1;
501
- if (options?.user) {
502
- sql += ` AND (client_cn = $${paramIndex} OR user_id = $${paramIndex})`;
503
- params.push(options.user);
504
- paramIndex++;
505
- }
506
- if (options?.action) {
507
- sql += ` AND action = $${paramIndex++}`;
508
- params.push(options.action);
509
- }
510
- if (options?.startDate) {
511
- sql += ` AND timestamp >= $${paramIndex++}`;
512
- params.push(new Date(options.startDate));
513
- }
514
- if (options?.endDate) {
515
- sql += ` AND timestamp <= $${paramIndex++}`;
516
- params.push(new Date(options.endDate));
517
- }
518
- sql += ` ORDER BY timestamp DESC LIMIT $${paramIndex}`;
519
- params.push(options?.limit ?? 100);
520
- const rows = await this.query(sql, params);
521
- return rows.map(r => ({
522
- id: r.id,
523
- ts: r.timestamp.toISOString(),
524
- clientCn: r.client_cn ?? '',
525
- action: r.action,
526
- resource: r.resource_type ? `${r.resource_type}/${r.resource_id ?? ''}` : '',
527
- statusCode: r.status_code,
528
- tenantId: r.tenant_id ?? undefined,
529
- ip: r.ip_address ?? undefined,
530
- }));
531
- }
532
- async verifyAuditChain() {
533
- // Get total count
534
- const countResult = await this.queryOne('SELECT COUNT(*) as count FROM audit_log');
535
- const total = parseInt(countResult?.count ?? '0', 10);
536
- if (total === 0) {
537
- return {
538
- valid: true,
539
- totalEntries: 0,
540
- verifiedEntries: 0,
541
- message: 'No audit entries to verify',
542
- };
543
- }
544
- // For now, return a basic verification
545
- // Full HMAC chain verification would require the secret key
546
- return {
547
- valid: true,
548
- totalEntries: total,
549
- verifiedEntries: total,
550
- message: `Verified ${total} audit entries (chain integrity check requires API access)`,
551
- };
552
- }
553
- // ============ Emergency Operations ============
554
- /**
555
- * Test database connection
556
- */
557
- async testConnection() {
558
- try {
559
- await this.connect();
560
- const result = await this.queryOne('SELECT NOW() as time, current_database() as db');
561
- return {
562
- success: true,
563
- message: `Connected to database '${result?.db ?? 'unknown'}' at ${result?.time.toISOString() ?? 'unknown'}`,
564
- };
565
- }
566
- catch (err) {
567
- return {
568
- success: false,
569
- message: `Connection failed: ${err instanceof Error ? err.message : String(err)}`,
570
- };
571
- }
572
- }
573
- /**
574
- * Get user status (for diagnostics)
575
- */
576
- async getUserStatus(username) {
577
- const user = await this.getUserByUsername(username);
578
- if (!user) {
579
- return { found: false };
580
- }
581
- return {
582
- found: true,
583
- user: {
584
- id: user.id,
585
- username: user.username,
586
- email: user.email ?? null,
587
- role: user.role,
588
- status: user.status,
589
- totpEnabled: user.totpEnabled,
590
- failedAttempts: user.failedAttempts,
591
- lockedUntil: user.lockedUntil ?? null,
592
- lastLogin: user.lastLogin ?? null,
593
- },
594
- };
595
- }
596
- /**
597
- * Reset a user's password directly in the database.
598
- */
599
- async resetPassword(username, newPassword) {
600
- await this.connect();
601
- try {
602
- const passwordHash = bcryptjs.hashSync(newPassword, 12);
603
- const findResult = await this.queryOne('SELECT id, username FROM users WHERE username = $1 OR email = $1', [username]);
604
- if (!findResult) {
605
- return { success: false, message: `User '${username}' not found` };
606
- }
607
- await this.client.query(`UPDATE users SET
608
- password_hash = $1,
609
- totp_enabled = false,
610
- totp_secret_cipher = NULL,
611
- totp_nonce = NULL,
612
- totp_tag = NULL,
613
- backup_codes_cipher = NULL,
614
- backup_codes_nonce = NULL,
615
- backup_codes_tag = NULL,
616
- failed_attempts = 0,
617
- locked_until = NULL,
618
- status = 'active',
619
- password_must_change = true,
620
- updated_at = NOW()
621
- WHERE id = $2`, [passwordHash, findResult.id]);
622
- return {
623
- success: true,
624
- message: `Password reset for user '${findResult.username}'. TOTP disabled, account unlocked.`,
625
- };
626
- }
627
- catch (err) {
628
- return {
629
- success: false,
630
- message: `Failed to reset password: ${err instanceof Error ? err.message : String(err)}`,
631
- };
632
- }
633
- }
634
- /**
635
- * Unlock a locked user account
636
- */
637
- async unlockUser(username) {
638
- await this.connect();
639
- try {
640
- const findResult = await this.queryOne('SELECT id, username, status, failed_attempts, locked_until FROM users WHERE username = $1 OR email = $1', [username]);
641
- if (!findResult) {
642
- return { success: false, message: `User '${username}' not found` };
643
- }
644
- if (findResult.status === 'active' && findResult.failed_attempts === 0 && !findResult.locked_until) {
645
- return { success: true, message: `User '${findResult.username}' is already unlocked` };
646
- }
647
- await this.client.query(`UPDATE users SET
648
- status = 'active',
649
- failed_attempts = 0,
650
- locked_until = NULL,
651
- updated_at = NOW()
652
- WHERE id = $1`, [findResult.id]);
653
- return {
654
- success: true,
655
- message: `User '${findResult.username}' has been unlocked`,
656
- };
657
- }
658
- catch (err) {
659
- return {
660
- success: false,
661
- message: `Failed to unlock user: ${err instanceof Error ? err.message : String(err)}`,
662
- };
663
- }
664
- }
665
- /**
666
- * Disable TOTP for a user
667
- */
668
- async disableTotp(username) {
669
- await this.connect();
670
- try {
671
- const findResult = await this.queryOne('SELECT id, username, totp_enabled FROM users WHERE username = $1 OR email = $1', [username]);
672
- if (!findResult) {
673
- return { success: false, message: `User '${username}' not found` };
674
- }
675
- if (!findResult.totp_enabled) {
676
- return { success: true, message: `TOTP is already disabled for '${findResult.username}'` };
677
- }
678
- await this.client.query(`UPDATE users SET
679
- totp_enabled = false,
680
- totp_secret_cipher = NULL,
681
- totp_nonce = NULL,
682
- totp_tag = NULL,
683
- backup_codes_cipher = NULL,
684
- backup_codes_nonce = NULL,
685
- backup_codes_tag = NULL,
686
- updated_at = NOW()
687
- WHERE id = $1`, [findResult.id]);
688
- return {
689
- success: true,
690
- message: `TOTP disabled for user '${findResult.username}'`,
691
- };
692
- }
693
- catch (err) {
694
- return {
695
- success: false,
696
- message: `Failed to disable TOTP: ${err instanceof Error ? err.message : String(err)}`,
697
- };
698
- }
699
- }
700
- }
701
- // ============ Legacy exports for backward compatibility ============
702
- /**
703
- * Legacy EmergencyDBClient class (alias for LocalDBClient)
704
- * @deprecated Use LocalDBClient instead
705
- */
706
- export class EmergencyDBClient extends LocalDBClient {
707
- constructor() {
708
- // For emergency operations, DATABASE_URL must be set
709
- if (!process.env.DATABASE_URL) {
710
- throw new Error('DATABASE_URL environment variable is required for emergency operations.\n' +
711
- 'This should only be set when running directly on a vault node.');
712
- }
713
- super();
714
- }
715
- }
716
- /**
717
- * Check if emergency DB access is available
718
- */
719
- export function isEmergencyDbAvailable() {
720
- return !!process.env.DATABASE_URL;
721
- }
722
- /**
723
- * Check if local mode is available (more comprehensive check)
724
- */
725
- export function isLocalDbAvailable() {
726
- const config = getLocalConfig();
727
- return config !== null;
728
- }
6
+ export { LocalDBClient, EmergencyDBClient, isEmergencyDbAvailable, isLocalDbAvailable, } from './db/index.js';
729
7
  //# sourceMappingURL=db.js.map