@zincapp/znvault-cli 2.16.0 → 2.16.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.
@@ -0,0 +1,743 @@
1
+ // Path: znvault-cli/src/commands/dynamic-secrets.ts
2
+ // CLI commands for dynamic secrets management (on-demand database credentials)
3
+ import ora from 'ora';
4
+ import Table from 'cli-table3';
5
+ import inquirer from 'inquirer';
6
+ import { client } from '../lib/client.js';
7
+ import * as output from '../lib/output.js';
8
+ // ============================================================================
9
+ // Helper Functions
10
+ // ============================================================================
11
+ function formatDuration(seconds) {
12
+ if (seconds < 60)
13
+ return `${seconds}s`;
14
+ if (seconds < 3600)
15
+ return `${Math.floor(seconds / 60)}m`;
16
+ if (seconds < 86400)
17
+ return `${Math.floor(seconds / 3600)}h`;
18
+ return `${Math.floor(seconds / 86400)}d`;
19
+ }
20
+ function formatStatus(status) {
21
+ switch (status) {
22
+ case 'ACTIVE': return output.isPlainMode() ? 'ACTIVE' : '\x1b[32mACTIVE\x1b[0m';
23
+ case 'DISABLED': return output.isPlainMode() ? 'DISABLED' : '\x1b[33mDISABLED\x1b[0m';
24
+ case 'FAILED': return output.isPlainMode() ? 'FAILED' : '\x1b[31mFAILED\x1b[0m';
25
+ case 'TESTING': return output.isPlainMode() ? 'TESTING' : '\x1b[36mTESTING\x1b[0m';
26
+ case 'EXPIRED': return output.isPlainMode() ? 'EXPIRED' : '\x1b[33mEXPIRED\x1b[0m';
27
+ case 'REVOKED': return output.isPlainMode() ? 'REVOKED' : '\x1b[31mREVOKED\x1b[0m';
28
+ default: return status;
29
+ }
30
+ }
31
+ function formatDate(dateStr) {
32
+ if (!dateStr)
33
+ return '-';
34
+ return new Date(dateStr).toLocaleString();
35
+ }
36
+ function formatTtl(seconds) {
37
+ if (seconds === null)
38
+ return 'inherit';
39
+ return formatDuration(seconds);
40
+ }
41
+ // ============================================================================
42
+ // Connection Commands
43
+ // ============================================================================
44
+ async function listConnections(options) {
45
+ const spinner = ora('Fetching connections...').start();
46
+ try {
47
+ const response = await client.get('/v1/dynamic-secrets/connections');
48
+ spinner.stop();
49
+ if (options.json) {
50
+ output.json(response);
51
+ return;
52
+ }
53
+ if (response.length === 0) {
54
+ output.info('No database connections found.');
55
+ return;
56
+ }
57
+ const table = new Table({
58
+ head: ['Name', 'Type', 'Status', 'Default TTL', 'Max TTL', 'Roles', 'Active Leases'],
59
+ style: { head: ['cyan'] },
60
+ });
61
+ for (const conn of response) {
62
+ table.push([
63
+ conn.name,
64
+ conn.connectionType,
65
+ formatStatus(conn.status),
66
+ formatTtl(conn.defaultTtlSeconds),
67
+ formatTtl(conn.maxTtlSeconds),
68
+ String(conn.roleCount ?? 0),
69
+ String(conn.activeLeases ?? 0),
70
+ ]);
71
+ }
72
+ console.log(table.toString());
73
+ output.info(`${response.length} connection(s) found`);
74
+ }
75
+ catch (err) {
76
+ spinner.fail('Failed to list connections');
77
+ output.error(err instanceof Error ? err.message : String(err));
78
+ process.exit(1);
79
+ }
80
+ }
81
+ async function getConnection(nameOrId, options) {
82
+ const spinner = ora('Fetching connection...').start();
83
+ try {
84
+ const response = await client.get(`/v1/dynamic-secrets/connections/${nameOrId}`);
85
+ spinner.stop();
86
+ if (options.json) {
87
+ output.json(response);
88
+ return;
89
+ }
90
+ output.keyValue({
91
+ 'ID': response.id,
92
+ 'Name': response.name,
93
+ 'Description': response.description || '-',
94
+ 'Type': response.connectionType,
95
+ 'Status': formatStatus(response.status),
96
+ 'Max Connections': response.maxOpenConnections,
97
+ 'Timeout': `${response.connectionTimeoutSeconds}s`,
98
+ 'Default TTL': formatTtl(response.defaultTtlSeconds),
99
+ 'Max TTL': formatTtl(response.maxTtlSeconds),
100
+ 'Last Health Check': formatDate(response.lastHealthCheck),
101
+ 'Health Status': response.lastHealthCheckStatus === null ? '-' : (response.lastHealthCheckStatus ? 'Healthy' : 'Unhealthy'),
102
+ 'Roles': String(response.roleCount ?? 0),
103
+ 'Active Leases': String(response.activeLeases ?? 0),
104
+ 'Created': formatDate(response.createdAt),
105
+ 'Updated': formatDate(response.updatedAt),
106
+ });
107
+ }
108
+ catch (err) {
109
+ spinner.fail('Failed to get connection');
110
+ output.error(err instanceof Error ? err.message : String(err));
111
+ process.exit(1);
112
+ }
113
+ }
114
+ async function createConnection(options) {
115
+ // Interactive prompts if options not provided
116
+ const name = options.name || (await inquirer.prompt([{
117
+ type: 'input',
118
+ name: 'name',
119
+ message: 'Connection name:',
120
+ validate: (input) => input.trim() ? true : 'Name is required',
121
+ }])).name;
122
+ const connectionType = options.type?.toUpperCase() || (await inquirer.prompt([{
123
+ type: 'list',
124
+ name: 'type',
125
+ message: 'Database type:',
126
+ choices: ['POSTGRESQL', 'MYSQL'],
127
+ }])).type;
128
+ const connectionString = options.connectionString || (await inquirer.prompt([{
129
+ type: 'password',
130
+ name: 'connectionString',
131
+ message: 'Connection string:',
132
+ mask: '*',
133
+ validate: (input) => input.trim() ? true : 'Connection string is required',
134
+ }])).connectionString;
135
+ const spinner = ora('Creating connection...').start();
136
+ try {
137
+ const body = {
138
+ name,
139
+ connectionType,
140
+ connectionString,
141
+ };
142
+ if (options.description)
143
+ body.description = options.description;
144
+ if (options.maxConnections)
145
+ body.maxOpenConnections = parseInt(options.maxConnections, 10);
146
+ if (options.timeout)
147
+ body.connectionTimeoutSeconds = parseInt(options.timeout, 10);
148
+ if (options.defaultTtl)
149
+ body.defaultTtlSeconds = parseInt(options.defaultTtl, 10);
150
+ if (options.maxTtl)
151
+ body.maxTtlSeconds = parseInt(options.maxTtl, 10);
152
+ const response = await client.post('/v1/dynamic-secrets/connections', body);
153
+ spinner.succeed('Connection created');
154
+ if (options.json) {
155
+ output.json(response);
156
+ }
157
+ else {
158
+ output.success(`Connection "${response.name}" created with ID: ${response.id}`);
159
+ }
160
+ }
161
+ catch (err) {
162
+ spinner.fail('Failed to create connection');
163
+ output.error(err instanceof Error ? err.message : String(err));
164
+ process.exit(1);
165
+ }
166
+ }
167
+ async function updateConnection(nameOrId, options) {
168
+ const spinner = ora('Updating connection...').start();
169
+ try {
170
+ const body = {};
171
+ if (options.description !== undefined)
172
+ body.description = options.description;
173
+ if (options.maxConnections)
174
+ body.maxOpenConnections = parseInt(options.maxConnections, 10);
175
+ if (options.timeout)
176
+ body.connectionTimeoutSeconds = parseInt(options.timeout, 10);
177
+ if (options.defaultTtl)
178
+ body.defaultTtlSeconds = parseInt(options.defaultTtl, 10);
179
+ if (options.maxTtl)
180
+ body.maxTtlSeconds = parseInt(options.maxTtl, 10);
181
+ if (options.status)
182
+ body.status = options.status.toUpperCase();
183
+ const response = await client.patch(`/v1/dynamic-secrets/connections/${nameOrId}`, body);
184
+ spinner.succeed('Connection updated');
185
+ if (options.json) {
186
+ output.json(response);
187
+ }
188
+ else {
189
+ output.success(`Connection "${response.name}" updated`);
190
+ }
191
+ }
192
+ catch (err) {
193
+ spinner.fail('Failed to update connection');
194
+ output.error(err instanceof Error ? err.message : String(err));
195
+ process.exit(1);
196
+ }
197
+ }
198
+ async function deleteConnection(nameOrId, options) {
199
+ if (!options.force) {
200
+ const { confirm } = await inquirer.prompt([{
201
+ type: 'confirm',
202
+ name: 'confirm',
203
+ message: `Are you sure you want to delete connection "${nameOrId}"? This will also delete all associated roles.`,
204
+ default: false,
205
+ }]);
206
+ if (!confirm) {
207
+ output.info('Cancelled');
208
+ return;
209
+ }
210
+ }
211
+ const spinner = ora('Deleting connection...').start();
212
+ try {
213
+ await client.delete(`/v1/dynamic-secrets/connections/${nameOrId}`);
214
+ spinner.succeed(`Connection "${nameOrId}" deleted`);
215
+ }
216
+ catch (err) {
217
+ spinner.fail('Failed to delete connection');
218
+ output.error(err instanceof Error ? err.message : String(err));
219
+ process.exit(1);
220
+ }
221
+ }
222
+ async function testConnection(nameOrId, options) {
223
+ const spinner = ora('Testing connection...').start();
224
+ try {
225
+ const response = await client.post(`/v1/dynamic-secrets/connections/${nameOrId}/test`, {});
226
+ if (response.success) {
227
+ spinner.succeed('Connection test successful');
228
+ if (options.json) {
229
+ output.json(response);
230
+ }
231
+ }
232
+ else {
233
+ spinner.fail('Connection test failed');
234
+ output.error(response.error || 'Unknown error');
235
+ if (options.json) {
236
+ output.json(response);
237
+ }
238
+ process.exit(1);
239
+ }
240
+ }
241
+ catch (err) {
242
+ spinner.fail('Failed to test connection');
243
+ output.error(err instanceof Error ? err.message : String(err));
244
+ process.exit(1);
245
+ }
246
+ }
247
+ // ============================================================================
248
+ // Role Commands
249
+ // ============================================================================
250
+ async function listRoles(options) {
251
+ const spinner = ora('Fetching roles...').start();
252
+ try {
253
+ let url = '/v1/dynamic-secrets/roles';
254
+ if (options.connection) {
255
+ url = `/v1/dynamic-secrets/connections/${options.connection}/roles`;
256
+ }
257
+ const response = await client.get(url);
258
+ spinner.stop();
259
+ if (options.json) {
260
+ output.json(response);
261
+ return;
262
+ }
263
+ if (response.length === 0) {
264
+ output.info('No roles found.');
265
+ return;
266
+ }
267
+ const table = new Table({
268
+ head: ['Name', 'Connection', 'Enabled', 'Default TTL', 'Max TTL', 'Active Leases'],
269
+ style: { head: ['cyan'] },
270
+ });
271
+ for (const role of response) {
272
+ table.push([
273
+ role.name,
274
+ role.connectionName || role.connectionId.substring(0, 8),
275
+ role.isEnabled ? 'Yes' : 'No',
276
+ formatTtl(role.defaultTtlSeconds),
277
+ formatTtl(role.maxTtlSeconds),
278
+ String(role.activeLeases ?? 0),
279
+ ]);
280
+ }
281
+ console.log(table.toString());
282
+ output.info(`${response.length} role(s) found`);
283
+ }
284
+ catch (err) {
285
+ spinner.fail('Failed to list roles');
286
+ output.error(err instanceof Error ? err.message : String(err));
287
+ process.exit(1);
288
+ }
289
+ }
290
+ async function getRole(roleId, options) {
291
+ const spinner = ora('Fetching role...').start();
292
+ try {
293
+ const response = await client.get(`/v1/dynamic-secrets/roles/${roleId}`);
294
+ spinner.stop();
295
+ if (options.json) {
296
+ output.json(response);
297
+ return;
298
+ }
299
+ output.keyValue({
300
+ 'ID': response.id,
301
+ 'Name': response.name,
302
+ 'Description': response.description || '-',
303
+ 'Connection': response.connectionName || response.connectionId,
304
+ 'Enabled': response.isEnabled ? 'Yes' : 'No',
305
+ 'Username Template': response.usernameTemplate,
306
+ 'Default TTL': formatTtl(response.defaultTtlSeconds),
307
+ 'Max TTL': formatTtl(response.maxTtlSeconds),
308
+ 'Active Leases': String(response.activeLeases ?? 0),
309
+ 'Created': formatDate(response.createdAt),
310
+ 'Updated': formatDate(response.updatedAt),
311
+ });
312
+ }
313
+ catch (err) {
314
+ spinner.fail('Failed to get role');
315
+ output.error(err instanceof Error ? err.message : String(err));
316
+ process.exit(1);
317
+ }
318
+ }
319
+ async function createRole(connectionId, options) {
320
+ // Interactive prompts if options not provided
321
+ const name = options.name || (await inquirer.prompt([{
322
+ type: 'input',
323
+ name: 'name',
324
+ message: 'Role name:',
325
+ validate: (input) => input.trim() ? true : 'Name is required',
326
+ }])).name;
327
+ const creationStatements = options.creationStatements?.split(';').filter(s => s.trim()) || (await inquirer.prompt([{
328
+ type: 'editor',
329
+ name: 'statements',
330
+ message: 'Creation SQL statements (one per line, use {{username}} and {{password}} placeholders):',
331
+ }])).statements.split('\n').filter((s) => s.trim());
332
+ const revocationStatements = options.revocationStatements?.split(';').filter(s => s.trim()) || (await inquirer.prompt([{
333
+ type: 'editor',
334
+ name: 'statements',
335
+ message: 'Revocation SQL statements (one per line, use {{username}} placeholder):',
336
+ }])).statements.split('\n').filter((s) => s.trim());
337
+ const spinner = ora('Creating role...').start();
338
+ try {
339
+ const body = {
340
+ name,
341
+ creationStatements,
342
+ revocationStatements,
343
+ };
344
+ if (options.description)
345
+ body.description = options.description;
346
+ if (options.renewStatements)
347
+ body.renewStatements = options.renewStatements.split(';').filter(s => s.trim());
348
+ if (options.defaultTtl)
349
+ body.defaultTtlSeconds = parseInt(options.defaultTtl, 10);
350
+ if (options.maxTtl)
351
+ body.maxTtlSeconds = parseInt(options.maxTtl, 10);
352
+ if (options.usernameTemplate)
353
+ body.usernameTemplate = options.usernameTemplate;
354
+ const response = await client.post(`/v1/dynamic-secrets/connections/${connectionId}/roles`, body);
355
+ spinner.succeed('Role created');
356
+ if (options.json) {
357
+ output.json(response);
358
+ }
359
+ else {
360
+ output.success(`Role "${response.name}" created with ID: ${response.id}`);
361
+ }
362
+ }
363
+ catch (err) {
364
+ spinner.fail('Failed to create role');
365
+ output.error(err instanceof Error ? err.message : String(err));
366
+ process.exit(1);
367
+ }
368
+ }
369
+ async function updateRole(roleId, options) {
370
+ const spinner = ora('Updating role...').start();
371
+ try {
372
+ const body = {};
373
+ if (options.description !== undefined)
374
+ body.description = options.description;
375
+ if (options.defaultTtl)
376
+ body.defaultTtlSeconds = parseInt(options.defaultTtl, 10);
377
+ if (options.maxTtl)
378
+ body.maxTtlSeconds = parseInt(options.maxTtl, 10);
379
+ if (options.enabled !== undefined)
380
+ body.isEnabled = options.enabled === 'true';
381
+ const response = await client.patch(`/v1/dynamic-secrets/roles/${roleId}`, body);
382
+ spinner.succeed('Role updated');
383
+ if (options.json) {
384
+ output.json(response);
385
+ }
386
+ else {
387
+ output.success(`Role "${response.name}" updated`);
388
+ }
389
+ }
390
+ catch (err) {
391
+ spinner.fail('Failed to update role');
392
+ output.error(err instanceof Error ? err.message : String(err));
393
+ process.exit(1);
394
+ }
395
+ }
396
+ async function deleteRole(roleId, options) {
397
+ if (!options.force) {
398
+ const { confirm } = await inquirer.prompt([{
399
+ type: 'confirm',
400
+ name: 'confirm',
401
+ message: `Are you sure you want to delete this role? Active leases will be revoked.`,
402
+ default: false,
403
+ }]);
404
+ if (!confirm) {
405
+ output.info('Cancelled');
406
+ return;
407
+ }
408
+ }
409
+ const spinner = ora('Deleting role...').start();
410
+ try {
411
+ await client.delete(`/v1/dynamic-secrets/roles/${roleId}`);
412
+ spinner.succeed('Role deleted');
413
+ }
414
+ catch (err) {
415
+ spinner.fail('Failed to delete role');
416
+ output.error(err instanceof Error ? err.message : String(err));
417
+ process.exit(1);
418
+ }
419
+ }
420
+ // ============================================================================
421
+ // Credential Commands
422
+ // ============================================================================
423
+ async function generateCredentials(roleId, options) {
424
+ const spinner = ora('Generating credentials...').start();
425
+ try {
426
+ const body = {};
427
+ if (options.ttl)
428
+ body.ttlSeconds = parseInt(options.ttl, 10);
429
+ const response = await client.post(`/v1/dynamic-secrets/roles/${roleId}/credentials`, body);
430
+ spinner.succeed('Credentials generated');
431
+ if (options.json) {
432
+ output.json(response);
433
+ return;
434
+ }
435
+ console.log('');
436
+ output.keyValue({
437
+ 'Lease ID': response.leaseId,
438
+ 'Username': response.username,
439
+ 'Password': response.password,
440
+ 'TTL': formatDuration(response.ttlSeconds),
441
+ 'Expires At': formatDate(response.expiresAt),
442
+ 'Max Expires At': formatDate(response.maxExpiresAt),
443
+ });
444
+ console.log('');
445
+ output.warn('The password is shown only once. Store it securely or use it immediately.');
446
+ }
447
+ catch (err) {
448
+ spinner.fail('Failed to generate credentials');
449
+ output.error(err instanceof Error ? err.message : String(err));
450
+ process.exit(1);
451
+ }
452
+ }
453
+ // ============================================================================
454
+ // Lease Commands
455
+ // ============================================================================
456
+ async function listLeases(options) {
457
+ const spinner = ora('Fetching leases...').start();
458
+ try {
459
+ const params = new URLSearchParams();
460
+ if (options.role)
461
+ params.append('roleId', options.role);
462
+ if (options.status)
463
+ params.append('status', options.status.toUpperCase());
464
+ const url = `/v1/dynamic-secrets/leases${params.toString() ? '?' + params.toString() : ''}`;
465
+ const response = await client.get(url);
466
+ spinner.stop();
467
+ if (options.json) {
468
+ output.json(response);
469
+ return;
470
+ }
471
+ if (response.length === 0) {
472
+ output.info('No leases found.');
473
+ return;
474
+ }
475
+ const table = new Table({
476
+ head: ['Lease ID', 'Username', 'Role', 'Status', 'TTL Remaining', 'Renewals'],
477
+ style: { head: ['cyan'] },
478
+ });
479
+ for (const lease of response) {
480
+ table.push([
481
+ lease.id.substring(0, 12),
482
+ lease.username,
483
+ lease.roleName || lease.roleId.substring(0, 8),
484
+ formatStatus(lease.status),
485
+ lease.status === 'ACTIVE' ? formatDuration(lease.ttlRemaining) : '-',
486
+ String(lease.renewalCount),
487
+ ]);
488
+ }
489
+ console.log(table.toString());
490
+ output.info(`${response.length} lease(s) found`);
491
+ }
492
+ catch (err) {
493
+ spinner.fail('Failed to list leases');
494
+ output.error(err instanceof Error ? err.message : String(err));
495
+ process.exit(1);
496
+ }
497
+ }
498
+ async function getLease(leaseId, options) {
499
+ const spinner = ora('Fetching lease...').start();
500
+ try {
501
+ const response = await client.get(`/v1/dynamic-secrets/leases/${leaseId}`);
502
+ spinner.stop();
503
+ if (options.json) {
504
+ output.json(response);
505
+ return;
506
+ }
507
+ output.keyValue({
508
+ 'Lease ID': response.id,
509
+ 'Username': response.username,
510
+ 'Role': response.roleName || response.roleId,
511
+ 'Connection': response.connectionName || response.connectionId,
512
+ 'Status': formatStatus(response.status),
513
+ 'TTL Remaining': response.status === 'ACTIVE' ? formatDuration(response.ttlRemaining) : '-',
514
+ 'Renewal Count': String(response.renewalCount),
515
+ 'Issued At': formatDate(response.issuedAt),
516
+ 'Expires At': formatDate(response.expiresAt),
517
+ 'Max Expires At': formatDate(response.maxExpiresAt),
518
+ 'Last Renewed': formatDate(response.lastRenewedAt),
519
+ 'Revoked At': formatDate(response.revokedAt),
520
+ 'Revoked By': response.revokedBy || '-',
521
+ 'Revoke Reason': response.revokeReason || '-',
522
+ });
523
+ }
524
+ catch (err) {
525
+ spinner.fail('Failed to get lease');
526
+ output.error(err instanceof Error ? err.message : String(err));
527
+ process.exit(1);
528
+ }
529
+ }
530
+ async function renewLease(leaseId, options) {
531
+ const spinner = ora('Renewing lease...').start();
532
+ try {
533
+ const body = {};
534
+ if (options.ttl)
535
+ body.ttlSeconds = parseInt(options.ttl, 10);
536
+ const response = await client.post(`/v1/dynamic-secrets/leases/${leaseId}/renew`, body);
537
+ spinner.succeed('Lease renewed');
538
+ if (options.json) {
539
+ output.json(response);
540
+ }
541
+ else {
542
+ output.success(`Lease renewed. New TTL: ${formatDuration(response.ttlSeconds)}, Renewal count: ${response.renewalCount}`);
543
+ }
544
+ }
545
+ catch (err) {
546
+ spinner.fail('Failed to renew lease');
547
+ output.error(err instanceof Error ? err.message : String(err));
548
+ process.exit(1);
549
+ }
550
+ }
551
+ async function revokeLease(leaseId, options) {
552
+ if (!options.force) {
553
+ const { confirm } = await inquirer.prompt([{
554
+ type: 'confirm',
555
+ name: 'confirm',
556
+ message: `Are you sure you want to revoke lease "${leaseId}"? This will immediately revoke the database credentials.`,
557
+ default: false,
558
+ }]);
559
+ if (!confirm) {
560
+ output.info('Cancelled');
561
+ return;
562
+ }
563
+ }
564
+ const spinner = ora('Revoking lease...').start();
565
+ try {
566
+ const body = {};
567
+ if (options.reason)
568
+ body.reason = options.reason;
569
+ await client.post(`/v1/dynamic-secrets/leases/${leaseId}/revoke`, body);
570
+ spinner.succeed('Lease revoked');
571
+ }
572
+ catch (err) {
573
+ spinner.fail('Failed to revoke lease');
574
+ output.error(err instanceof Error ? err.message : String(err));
575
+ process.exit(1);
576
+ }
577
+ }
578
+ // ============================================================================
579
+ // Command Registration
580
+ // ============================================================================
581
+ export function registerDynamicSecretsCommands(program) {
582
+ const dynasec = program
583
+ .command('dynasec')
584
+ .description('Dynamic secrets management (on-demand database credentials)')
585
+ .addHelpText('after', `
586
+ Examples:
587
+ # List all database connections
588
+ znvault dynasec connection list
589
+
590
+ # Create a PostgreSQL connection
591
+ znvault dynasec connection create --name my-pg --type postgresql \\
592
+ --connection-string "postgresql://admin:pass@localhost:5432/mydb"
593
+
594
+ # Create a role for the connection
595
+ znvault dynasec role create <connection-id> --name readonly \\
596
+ --creation-statements "CREATE ROLE \\"{{username}}\\" WITH LOGIN PASSWORD '{{password}}'" \\
597
+ --revocation-statements "DROP ROLE IF EXISTS \\"{{username}}\\""
598
+
599
+ # Generate credentials
600
+ znvault dynasec creds generate <role-id> --ttl 3600
601
+
602
+ # List active leases
603
+ znvault dynasec lease list --status active
604
+
605
+ # Revoke a lease
606
+ znvault dynasec lease revoke <lease-id> --reason "No longer needed"
607
+ `);
608
+ // -------------------------------------------------------------------------
609
+ // Connection Commands
610
+ // -------------------------------------------------------------------------
611
+ const connection = dynasec.command('connection').alias('conn').description('Manage database connections');
612
+ connection
613
+ .command('list')
614
+ .alias('ls')
615
+ .description('List all database connections')
616
+ .option('--json', 'Output as JSON')
617
+ .action(listConnections);
618
+ connection
619
+ .command('get <name-or-id>')
620
+ .description('Get connection details')
621
+ .option('--json', 'Output as JSON')
622
+ .action(getConnection);
623
+ connection
624
+ .command('create')
625
+ .description('Create a new database connection')
626
+ .option('--name <name>', 'Connection name')
627
+ .option('--type <type>', 'Database type (POSTGRESQL or MYSQL)')
628
+ .option('--connection-string <string>', 'Database connection string')
629
+ .option('--description <desc>', 'Connection description')
630
+ .option('--max-connections <n>', 'Maximum open connections')
631
+ .option('--timeout <seconds>', 'Connection timeout in seconds')
632
+ .option('--default-ttl <seconds>', 'Default credential TTL')
633
+ .option('--max-ttl <seconds>', 'Maximum credential TTL')
634
+ .option('--json', 'Output as JSON')
635
+ .action(createConnection);
636
+ connection
637
+ .command('update <name-or-id>')
638
+ .description('Update a database connection')
639
+ .option('--description <desc>', 'Connection description')
640
+ .option('--max-connections <n>', 'Maximum open connections')
641
+ .option('--timeout <seconds>', 'Connection timeout in seconds')
642
+ .option('--default-ttl <seconds>', 'Default credential TTL')
643
+ .option('--max-ttl <seconds>', 'Maximum credential TTL')
644
+ .option('--status <status>', 'Connection status (ACTIVE or DISABLED)')
645
+ .option('--json', 'Output as JSON')
646
+ .action(updateConnection);
647
+ connection
648
+ .command('delete <name-or-id>')
649
+ .alias('rm')
650
+ .description('Delete a database connection')
651
+ .option('--force', 'Skip confirmation')
652
+ .action(deleteConnection);
653
+ connection
654
+ .command('test <name-or-id>')
655
+ .description('Test a database connection')
656
+ .option('--json', 'Output as JSON')
657
+ .action(testConnection);
658
+ // -------------------------------------------------------------------------
659
+ // Role Commands
660
+ // -------------------------------------------------------------------------
661
+ const role = dynasec.command('role').description('Manage credential roles');
662
+ role
663
+ .command('list')
664
+ .alias('ls')
665
+ .description('List all roles')
666
+ .option('--connection <id>', 'Filter by connection ID')
667
+ .option('--json', 'Output as JSON')
668
+ .action(listRoles);
669
+ role
670
+ .command('get <role-id>')
671
+ .description('Get role details')
672
+ .option('--json', 'Output as JSON')
673
+ .action(getRole);
674
+ role
675
+ .command('create <connection-id>')
676
+ .description('Create a new role for a connection')
677
+ .option('--name <name>', 'Role name')
678
+ .option('--description <desc>', 'Role description')
679
+ .option('--creation-statements <sql>', 'SQL statements to create credentials (semicolon-separated)')
680
+ .option('--revocation-statements <sql>', 'SQL statements to revoke credentials (semicolon-separated)')
681
+ .option('--renew-statements <sql>', 'SQL statements to renew credentials (semicolon-separated)')
682
+ .option('--default-ttl <seconds>', 'Default credential TTL')
683
+ .option('--max-ttl <seconds>', 'Maximum credential TTL')
684
+ .option('--username-template <template>', 'Username template (e.g., v_{{role}}_{{random:8}})')
685
+ .option('--json', 'Output as JSON')
686
+ .action(createRole);
687
+ role
688
+ .command('update <role-id>')
689
+ .description('Update a role')
690
+ .option('--description <desc>', 'Role description')
691
+ .option('--default-ttl <seconds>', 'Default credential TTL')
692
+ .option('--max-ttl <seconds>', 'Maximum credential TTL')
693
+ .option('--enabled <bool>', 'Enable or disable role (true/false)')
694
+ .option('--json', 'Output as JSON')
695
+ .action(updateRole);
696
+ role
697
+ .command('delete <role-id>')
698
+ .alias('rm')
699
+ .description('Delete a role')
700
+ .option('--force', 'Skip confirmation')
701
+ .action(deleteRole);
702
+ // -------------------------------------------------------------------------
703
+ // Credentials Commands
704
+ // -------------------------------------------------------------------------
705
+ const creds = dynasec.command('creds').alias('credentials').description('Generate database credentials');
706
+ creds
707
+ .command('generate <role-id>')
708
+ .alias('gen')
709
+ .description('Generate new database credentials')
710
+ .option('--ttl <seconds>', 'Credential TTL in seconds')
711
+ .option('--json', 'Output as JSON')
712
+ .action(generateCredentials);
713
+ // -------------------------------------------------------------------------
714
+ // Lease Commands
715
+ // -------------------------------------------------------------------------
716
+ const lease = dynasec.command('lease').description('Manage credential leases');
717
+ lease
718
+ .command('list')
719
+ .alias('ls')
720
+ .description('List credential leases')
721
+ .option('--role <id>', 'Filter by role ID')
722
+ .option('--status <status>', 'Filter by status (ACTIVE, EXPIRED, REVOKED)')
723
+ .option('--json', 'Output as JSON')
724
+ .action(listLeases);
725
+ lease
726
+ .command('get <lease-id>')
727
+ .description('Get lease details')
728
+ .option('--json', 'Output as JSON')
729
+ .action(getLease);
730
+ lease
731
+ .command('renew <lease-id>')
732
+ .description('Renew a lease')
733
+ .option('--ttl <seconds>', 'New TTL in seconds')
734
+ .option('--json', 'Output as JSON')
735
+ .action(renewLease);
736
+ lease
737
+ .command('revoke <lease-id>')
738
+ .description('Revoke a lease (immediately revokes database credentials)')
739
+ .option('--reason <reason>', 'Revocation reason')
740
+ .option('--force', 'Skip confirmation')
741
+ .action(revokeLease);
742
+ }
743
+ //# sourceMappingURL=dynamic-secrets.js.map