@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.
- package/dist/commands/dynamic-secrets/connection.d.ts +17 -0
- package/dist/commands/dynamic-secrets/connection.d.ts.map +1 -0
- package/dist/commands/dynamic-secrets/connection.js +217 -0
- package/dist/commands/dynamic-secrets/connection.js.map +1 -0
- package/dist/commands/dynamic-secrets/creds.d.ts +5 -0
- package/dist/commands/dynamic-secrets/creds.d.ts.map +1 -0
- package/dist/commands/dynamic-secrets/creds.js +39 -0
- package/dist/commands/dynamic-secrets/creds.js.map +1 -0
- package/dist/commands/dynamic-secrets/helpers.d.ts +5 -0
- package/dist/commands/dynamic-secrets/helpers.d.ts.map +1 -0
- package/dist/commands/dynamic-secrets/helpers.js +36 -0
- package/dist/commands/dynamic-secrets/helpers.js.map +1 -0
- package/dist/commands/dynamic-secrets/index.d.ts +7 -0
- package/dist/commands/dynamic-secrets/index.d.ts.map +1 -0
- package/dist/commands/dynamic-secrets/index.js +173 -0
- package/dist/commands/dynamic-secrets/index.js.map +1 -0
- package/dist/commands/dynamic-secrets/lease.d.ts +11 -0
- package/dist/commands/dynamic-secrets/lease.d.ts.map +1 -0
- package/dist/commands/dynamic-secrets/lease.js +137 -0
- package/dist/commands/dynamic-secrets/lease.js.map +1 -0
- package/dist/commands/dynamic-secrets/role.d.ts +15 -0
- package/dist/commands/dynamic-secrets/role.d.ts.map +1 -0
- package/dist/commands/dynamic-secrets/role.js +184 -0
- package/dist/commands/dynamic-secrets/role.js.map +1 -0
- package/dist/commands/dynamic-secrets/types.d.ts +125 -0
- package/dist/commands/dynamic-secrets/types.d.ts.map +1 -0
- package/dist/commands/dynamic-secrets/types.js +3 -0
- package/dist/commands/dynamic-secrets/types.js.map +1 -0
- package/dist/commands/dynamic-secrets.d.ts +6 -2
- package/dist/commands/dynamic-secrets.d.ts.map +1 -1
- package/dist/commands/dynamic-secrets.js +6 -754
- package/dist/commands/dynamic-secrets.js.map +1 -1
- package/dist/commands/policy/attachments.d.ts +9 -0
- package/dist/commands/policy/attachments.d.ts.map +1 -0
- package/dist/commands/policy/attachments.js +161 -0
- package/dist/commands/policy/attachments.js.map +1 -0
- package/dist/commands/policy/crud.d.ts +8 -0
- package/dist/commands/policy/crud.d.ts.map +1 -0
- package/dist/commands/policy/crud.js +232 -0
- package/dist/commands/policy/crud.js.map +1 -0
- package/dist/commands/policy/helpers.d.ts +13 -0
- package/dist/commands/policy/helpers.d.ts.map +1 -0
- package/dist/commands/policy/helpers.js +61 -0
- package/dist/commands/policy/helpers.js.map +1 -0
- package/dist/commands/policy/index.d.ts +7 -0
- package/dist/commands/policy/index.d.ts.map +1 -0
- package/dist/commands/policy/index.js +160 -0
- package/dist/commands/policy/index.js.map +1 -0
- package/dist/commands/policy/io.d.ts +4 -0
- package/dist/commands/policy/io.d.ts.map +1 -0
- package/dist/commands/policy/io.js +65 -0
- package/dist/commands/policy/io.js.map +1 -0
- package/dist/commands/policy/list.d.ts +4 -0
- package/dist/commands/policy/list.d.ts.map +1 -0
- package/dist/commands/policy/list.js +99 -0
- package/dist/commands/policy/list.js.map +1 -0
- package/dist/commands/policy/test.d.ts +3 -0
- package/dist/commands/policy/test.d.ts.map +1 -0
- package/dist/commands/policy/test.js +58 -0
- package/dist/commands/policy/test.js.map +1 -0
- package/dist/commands/policy/types.d.ts +84 -0
- package/dist/commands/policy/types.d.ts.map +1 -0
- package/dist/commands/policy/types.js +3 -0
- package/dist/commands/policy/types.js.map +1 -0
- package/dist/commands/policy.d.ts +6 -2
- package/dist/commands/policy.d.ts.map +1 -1
- package/dist/commands/policy.js +4 -770
- package/dist/commands/policy.js.map +1 -1
- package/dist/lib/config/index.d.ts +1 -1
- package/dist/lib/config/index.d.ts.map +1 -1
- package/dist/lib/config/index.js +1 -1
- package/dist/lib/config/index.js.map +1 -1
- package/dist/lib/config/store.d.ts +10 -0
- package/dist/lib/config/store.d.ts.map +1 -1
- package/dist/lib/config/store.js +49 -10
- package/dist/lib/config/store.js.map +1 -1
- package/dist/lib/db/audit.d.ts +16 -0
- package/dist/lib/db/audit.d.ts.map +1 -0
- package/dist/lib/db/audit.js +60 -0
- package/dist/lib/db/audit.js.map +1 -0
- package/dist/lib/db/client.d.ts +27 -0
- package/dist/lib/db/client.d.ts.map +1 -0
- package/dist/lib/db/client.js +70 -0
- package/dist/lib/db/client.js.map +1 -0
- package/dist/lib/db/emergency.d.ts +50 -0
- package/dist/lib/db/emergency.d.ts.map +1 -0
- package/dist/lib/db/emergency.js +180 -0
- package/dist/lib/db/emergency.js.map +1 -0
- package/dist/lib/db/health.d.ts +14 -0
- package/dist/lib/db/health.d.ts.map +1 -0
- package/dist/lib/db/health.js +177 -0
- package/dist/lib/db/health.js.map +1 -0
- package/dist/lib/db/index.d.ts +56 -0
- package/dist/lib/db/index.d.ts.map +1 -0
- package/dist/lib/db/index.js +107 -0
- package/dist/lib/db/index.js.map +1 -0
- package/dist/lib/db/lockdown.d.ts +15 -0
- package/dist/lib/db/lockdown.d.ts.map +1 -0
- package/dist/lib/db/lockdown.js +67 -0
- package/dist/lib/db/lockdown.js.map +1 -0
- package/dist/lib/db/tenants.d.ts +14 -0
- package/dist/lib/db/tenants.d.ts.map +1 -0
- package/dist/lib/db/tenants.js +88 -0
- package/dist/lib/db/tenants.js.map +1 -0
- package/dist/lib/db/types.d.ts +95 -0
- package/dist/lib/db/types.d.ts.map +1 -0
- package/dist/lib/db/types.js +3 -0
- package/dist/lib/db/types.js.map +1 -0
- package/dist/lib/db/users.d.ts +16 -0
- package/dist/lib/db/users.d.ts.map +1 -0
- package/dist/lib/db/users.js +95 -0
- package/dist/lib/db/users.js.map +1 -0
- package/dist/lib/db.d.ts +4 -112
- package/dist/lib/db.d.ts.map +1 -1
- package/dist/lib/db.js +4 -726
- package/dist/lib/db.js.map +1 -1
- package/package.json +1 -1
package/dist/lib/db.js
CHANGED
|
@@ -1,729 +1,7 @@
|
|
|
1
|
-
|
|
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
|
|
8
|
-
*
|
|
3
|
+
* Database client re-exports for backward compatibility.
|
|
4
|
+
* The actual implementation has been modularized into src/lib/db/
|
|
9
5
|
*/
|
|
10
|
-
export
|
|
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
|