@zincapp/znvault-cli 2.1.2 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -0
- package/dist/commands/agent.d.ts +1 -1
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +71 -71
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/apikey.d.ts +1 -1
- package/dist/commands/apikey.d.ts.map +1 -1
- package/dist/commands/apikey.js +47 -85
- package/dist/commands/apikey.js.map +1 -1
- package/dist/commands/audit.d.ts +1 -1
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +2 -2
- package/dist/commands/audit.js.map +1 -1
- package/dist/commands/auth.d.ts +1 -1
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +18 -14
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/backup.d.ts +3 -0
- package/dist/commands/backup.d.ts.map +1 -0
- package/dist/commands/backup.js +393 -0
- package/dist/commands/backup.js.map +1 -0
- package/dist/commands/cert.d.ts +1 -1
- package/dist/commands/cert.d.ts.map +1 -1
- package/dist/commands/cert.js +2 -2
- package/dist/commands/cert.js.map +1 -1
- package/dist/commands/cluster.d.ts +1 -1
- package/dist/commands/cluster.d.ts.map +1 -1
- package/dist/commands/cluster.js +2 -2
- package/dist/commands/cluster.js.map +1 -1
- package/dist/commands/emergency.d.ts +1 -1
- package/dist/commands/emergency.d.ts.map +1 -1
- package/dist/commands/emergency.js +19 -18
- package/dist/commands/emergency.js.map +1 -1
- package/dist/commands/health.d.ts +1 -1
- package/dist/commands/health.d.ts.map +1 -1
- package/dist/commands/health.js +75 -77
- package/dist/commands/health.js.map +1 -1
- package/dist/commands/kms.d.ts +3 -0
- package/dist/commands/kms.d.ts.map +1 -0
- package/dist/commands/kms.js +536 -0
- package/dist/commands/kms.js.map +1 -0
- package/dist/commands/lockdown.d.ts +1 -1
- package/dist/commands/lockdown.d.ts.map +1 -1
- package/dist/commands/lockdown.js +6 -6
- package/dist/commands/lockdown.js.map +1 -1
- package/dist/commands/notification.d.ts +3 -0
- package/dist/commands/notification.d.ts.map +1 -0
- package/dist/commands/notification.js +394 -0
- package/dist/commands/notification.js.map +1 -0
- package/dist/commands/permissions.d.ts +1 -1
- package/dist/commands/permissions.d.ts.map +1 -1
- package/dist/commands/permissions.js +7 -7
- package/dist/commands/permissions.js.map +1 -1
- package/dist/commands/policy.d.ts +1 -1
- package/dist/commands/policy.d.ts.map +1 -1
- package/dist/commands/policy.js +14 -14
- package/dist/commands/policy.js.map +1 -1
- package/dist/commands/role.d.ts +3 -0
- package/dist/commands/role.d.ts.map +1 -0
- package/dist/commands/role.js +400 -0
- package/dist/commands/role.js.map +1 -0
- package/dist/commands/secret.d.ts +3 -0
- package/dist/commands/secret.d.ts.map +1 -0
- package/dist/commands/secret.js +694 -0
- package/dist/commands/secret.js.map +1 -0
- package/dist/commands/self-update.d.ts +8 -0
- package/dist/commands/self-update.d.ts.map +1 -0
- package/dist/commands/self-update.js +114 -0
- package/dist/commands/self-update.js.map +1 -0
- package/dist/commands/superadmin.d.ts +1 -1
- package/dist/commands/superadmin.d.ts.map +1 -1
- package/dist/commands/superadmin.js +3 -3
- package/dist/commands/superadmin.js.map +1 -1
- package/dist/commands/tenant.d.ts +1 -1
- package/dist/commands/tenant.d.ts.map +1 -1
- package/dist/commands/tenant.js +16 -18
- package/dist/commands/tenant.js.map +1 -1
- package/dist/commands/tui.d.ts +3 -0
- package/dist/commands/tui.d.ts.map +1 -0
- package/dist/commands/tui.js +66 -0
- package/dist/commands/tui.js.map +1 -0
- package/dist/commands/update.d.ts +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +8 -8
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/user.d.ts +1 -1
- package/dist/commands/user.d.ts.map +1 -1
- package/dist/commands/user.js +7 -7
- package/dist/commands/user.js.map +1 -1
- package/dist/index.js +33 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/cli-update.d.ts +43 -0
- package/dist/lib/cli-update.d.ts.map +1 -0
- package/dist/lib/cli-update.js +257 -0
- package/dist/lib/cli-update.js.map +1 -0
- package/dist/lib/client.d.ts +48 -61
- package/dist/lib/client.d.ts.map +1 -1
- package/dist/lib/client.js +57 -30
- package/dist/lib/client.js.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +42 -37
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/db.d.ts.map +1 -1
- package/dist/lib/db.js +23 -23
- package/dist/lib/db.js.map +1 -1
- package/dist/lib/local.d.ts.map +1 -1
- package/dist/lib/local.js +9 -9
- package/dist/lib/local.js.map +1 -1
- package/dist/lib/mode.d.ts +1 -1
- package/dist/lib/mode.d.ts.map +1 -1
- package/dist/lib/mode.js +4 -7
- package/dist/lib/mode.js.map +1 -1
- package/dist/lib/output-mode.d.ts +31 -0
- package/dist/lib/output-mode.d.ts.map +1 -0
- package/dist/lib/output-mode.js +81 -0
- package/dist/lib/output-mode.js.map +1 -0
- package/dist/lib/output.d.ts +55 -5
- package/dist/lib/output.d.ts.map +1 -1
- package/dist/lib/output.js +279 -30
- package/dist/lib/output.js.map +1 -1
- package/dist/lib/prompts.d.ts +2 -2
- package/dist/lib/prompts.d.ts.map +1 -1
- package/dist/lib/prompts.js.map +1 -1
- package/dist/lib/visual.d.ts +71 -0
- package/dist/lib/visual.d.ts.map +1 -0
- package/dist/lib/visual.js +266 -0
- package/dist/lib/visual.js.map +1 -0
- package/dist/services/auto-update-daemon.d.ts.map +1 -1
- package/dist/services/auto-update-daemon.js +39 -19
- package/dist/services/auto-update-daemon.js.map +1 -1
- package/dist/services/signature-verifier.d.ts +0 -1
- package/dist/services/signature-verifier.d.ts.map +1 -1
- package/dist/services/signature-verifier.js +4 -8
- package/dist/services/signature-verifier.js.map +1 -1
- package/dist/services/update-checker.d.ts.map +1 -1
- package/dist/services/update-checker.js +3 -3
- package/dist/services/update-checker.js.map +1 -1
- package/dist/services/update-installer.d.ts.map +1 -1
- package/dist/services/update-installer.js +5 -5
- package/dist/services/update-installer.js.map +1 -1
- package/dist/tui/App.d.ts +9 -0
- package/dist/tui/App.d.ts.map +1 -0
- package/dist/tui/App.js +63 -0
- package/dist/tui/App.js.map +1 -0
- package/dist/tui/components/Header.d.ts +9 -0
- package/dist/tui/components/Header.d.ts.map +1 -0
- package/dist/tui/components/Header.js +9 -0
- package/dist/tui/components/Header.js.map +1 -0
- package/dist/tui/components/List.d.ts +61 -0
- package/dist/tui/components/List.d.ts.map +1 -0
- package/dist/tui/components/List.js +85 -0
- package/dist/tui/components/List.js.map +1 -0
- package/dist/tui/components/StatusCard.d.ts +29 -0
- package/dist/tui/components/StatusCard.d.ts.map +1 -0
- package/dist/tui/components/StatusCard.js +46 -0
- package/dist/tui/components/StatusCard.js.map +1 -0
- package/dist/tui/components/Table.d.ts +29 -0
- package/dist/tui/components/Table.d.ts.map +1 -0
- package/dist/tui/components/Table.js +131 -0
- package/dist/tui/components/Table.js.map +1 -0
- package/dist/tui/components/UpdateBanner.d.ts +9 -0
- package/dist/tui/components/UpdateBanner.d.ts.map +1 -0
- package/dist/tui/components/UpdateBanner.js +6 -0
- package/dist/tui/components/UpdateBanner.js.map +1 -0
- package/dist/tui/hooks/useApi.d.ts +75 -0
- package/dist/tui/hooks/useApi.d.ts.map +1 -0
- package/dist/tui/hooks/useApi.js +79 -0
- package/dist/tui/hooks/useApi.js.map +1 -0
- package/dist/tui/screens/Dashboard.d.ts +9 -0
- package/dist/tui/screens/Dashboard.d.ts.map +1 -0
- package/dist/tui/screens/Dashboard.js +66 -0
- package/dist/tui/screens/Dashboard.js.map +1 -0
- package/dist/types/index.d.ts +5 -5
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/platform.js +1 -1
- package/package.json +29 -13
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
// Path: znvault-cli/src/commands/secret.ts
|
|
2
|
+
// CLI commands for secrets management
|
|
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 formatDate(dateStr) {
|
|
12
|
+
if (!dateStr)
|
|
13
|
+
return '-';
|
|
14
|
+
return new Date(dateStr).toLocaleString();
|
|
15
|
+
}
|
|
16
|
+
function formatType(type, subType) {
|
|
17
|
+
if (subType)
|
|
18
|
+
return `${type}/${subType}`;
|
|
19
|
+
return type;
|
|
20
|
+
}
|
|
21
|
+
function formatTags(tags) {
|
|
22
|
+
if (!tags || tags.length === 0)
|
|
23
|
+
return '-';
|
|
24
|
+
if (tags.length <= 3)
|
|
25
|
+
return tags.join(', ');
|
|
26
|
+
return `${tags.slice(0, 2).join(', ')} +${tags.length - 2} more`;
|
|
27
|
+
}
|
|
28
|
+
function truncateAlias(alias, maxLen = 40) {
|
|
29
|
+
if (alias.length <= maxLen)
|
|
30
|
+
return alias;
|
|
31
|
+
return '...' + alias.slice(-(maxLen - 3));
|
|
32
|
+
}
|
|
33
|
+
function formatBytes(bytes) {
|
|
34
|
+
if (!bytes)
|
|
35
|
+
return '-';
|
|
36
|
+
if (bytes < 1024)
|
|
37
|
+
return `${bytes} B`;
|
|
38
|
+
if (bytes < 1024 * 1024)
|
|
39
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
40
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
41
|
+
}
|
|
42
|
+
function getDaysUntilExpiry(expiresAt) {
|
|
43
|
+
if (!expiresAt)
|
|
44
|
+
return null;
|
|
45
|
+
const expires = new Date(expiresAt);
|
|
46
|
+
const now = new Date();
|
|
47
|
+
return Math.ceil((expires.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
|
48
|
+
}
|
|
49
|
+
function formatExpiry(expiresAt) {
|
|
50
|
+
if (!expiresAt)
|
|
51
|
+
return '-';
|
|
52
|
+
const days = getDaysUntilExpiry(expiresAt);
|
|
53
|
+
if (days === null)
|
|
54
|
+
return '-';
|
|
55
|
+
if (days < 0)
|
|
56
|
+
return `Expired ${Math.abs(days)}d ago`;
|
|
57
|
+
if (days === 0)
|
|
58
|
+
return 'Expires today';
|
|
59
|
+
if (days <= 7)
|
|
60
|
+
return `${days}d (!)`;
|
|
61
|
+
if (days <= 30)
|
|
62
|
+
return `${days}d`;
|
|
63
|
+
return `${days}d`;
|
|
64
|
+
}
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// Command Implementations
|
|
67
|
+
// ============================================================================
|
|
68
|
+
async function listSecrets(options) {
|
|
69
|
+
const spinner = ora('Fetching secrets...').start();
|
|
70
|
+
try {
|
|
71
|
+
const query = {};
|
|
72
|
+
if (options.tenant)
|
|
73
|
+
query.tenant = options.tenant;
|
|
74
|
+
if (options.type)
|
|
75
|
+
query.type = options.type;
|
|
76
|
+
if (options.subType)
|
|
77
|
+
query.subType = options.subType;
|
|
78
|
+
if (options.aliasPrefix)
|
|
79
|
+
query.aliasPrefix = options.aliasPrefix;
|
|
80
|
+
if (options.expiring) {
|
|
81
|
+
const days = parseInt(options.expiring, 10);
|
|
82
|
+
const expiringBefore = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString();
|
|
83
|
+
query.expiringBefore = expiringBefore;
|
|
84
|
+
}
|
|
85
|
+
const secrets = await client.get('/v1/secrets?' + new URLSearchParams(query).toString());
|
|
86
|
+
spinner.stop();
|
|
87
|
+
if (options.json) {
|
|
88
|
+
output.json(secrets);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (secrets.length === 0) {
|
|
92
|
+
output.info('No secrets found');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const table = new Table({
|
|
96
|
+
head: ['ID', 'Alias', 'Tenant', 'Type', 'Ver', 'Expires', 'Tags', 'Updated'],
|
|
97
|
+
colWidths: [12, 42, 12, 16, 5, 14, 20, 20],
|
|
98
|
+
wordWrap: true,
|
|
99
|
+
});
|
|
100
|
+
for (const secret of secrets) {
|
|
101
|
+
table.push([
|
|
102
|
+
secret.id.slice(0, 10) + '...',
|
|
103
|
+
truncateAlias(secret.alias),
|
|
104
|
+
secret.tenant.slice(0, 10),
|
|
105
|
+
formatType(secret.type, secret.subType),
|
|
106
|
+
String(secret.version),
|
|
107
|
+
formatExpiry(secret.expiresAt || secret.ttlUntil),
|
|
108
|
+
formatTags(secret.tags),
|
|
109
|
+
formatDate(secret.updatedAt).split(',')[0], // Just date
|
|
110
|
+
]);
|
|
111
|
+
}
|
|
112
|
+
console.log(table.toString());
|
|
113
|
+
output.info(`Total: ${secrets.length} secret(s)`);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
spinner.fail('Failed to list secrets');
|
|
117
|
+
output.error(error.message);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function getSecret(id, options) {
|
|
122
|
+
const spinner = ora('Fetching secret metadata...').start();
|
|
123
|
+
try {
|
|
124
|
+
const secret = await client.get(`/v1/secrets/${id}/meta`);
|
|
125
|
+
spinner.stop();
|
|
126
|
+
if (options.json) {
|
|
127
|
+
output.json(secret);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const table = new Table({
|
|
131
|
+
colWidths: [20, 60],
|
|
132
|
+
});
|
|
133
|
+
table.push(['ID', secret.id], ['Alias', secret.alias], ['Tenant', secret.tenant], ['Type', formatType(secret.type, secret.subType)], ['Version', String(secret.version)]);
|
|
134
|
+
if (secret.fileName) {
|
|
135
|
+
table.push(['File Name', secret.fileName]);
|
|
136
|
+
}
|
|
137
|
+
if (secret.fileSize) {
|
|
138
|
+
table.push(['File Size', formatBytes(secret.fileSize)]);
|
|
139
|
+
}
|
|
140
|
+
if (secret.fileMime) {
|
|
141
|
+
table.push(['MIME Type', secret.fileMime]);
|
|
142
|
+
}
|
|
143
|
+
if (secret.contentType) {
|
|
144
|
+
table.push(['Content Type', secret.contentType]);
|
|
145
|
+
}
|
|
146
|
+
if (secret.expiresAt) {
|
|
147
|
+
table.push(['Expires At', formatDate(secret.expiresAt)]);
|
|
148
|
+
}
|
|
149
|
+
if (secret.ttlUntil) {
|
|
150
|
+
table.push(['TTL Until', formatDate(secret.ttlUntil)]);
|
|
151
|
+
}
|
|
152
|
+
if (secret.tags && secret.tags.length > 0) {
|
|
153
|
+
table.push(['Tags', secret.tags.join(', ')]);
|
|
154
|
+
}
|
|
155
|
+
table.push(['Created By', secret.createdBy || '-'], ['Created At', formatDate(secret.createdAt)], ['Updated At', formatDate(secret.updatedAt)]);
|
|
156
|
+
console.log(table.toString());
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
spinner.fail('Failed to get secret');
|
|
160
|
+
output.error(error.message);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function decryptSecret(id, options) {
|
|
165
|
+
const spinner = ora('Decrypting secret...').start();
|
|
166
|
+
try {
|
|
167
|
+
const secret = await client.post(`/v1/secrets/${id}/decrypt`, {});
|
|
168
|
+
spinner.stop();
|
|
169
|
+
if (options.json) {
|
|
170
|
+
output.json(secret);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
// If output file specified and it's a file-based secret
|
|
174
|
+
if (options.output && secret.data) {
|
|
175
|
+
const fs = await import('fs');
|
|
176
|
+
// Check if it's a file-based secret
|
|
177
|
+
if ('content' in secret.data && typeof secret.data.content === 'string') {
|
|
178
|
+
const content = Buffer.from(secret.data.content, 'base64');
|
|
179
|
+
fs.writeFileSync(options.output, content);
|
|
180
|
+
output.success(`File written to: ${options.output}`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// Otherwise write JSON
|
|
184
|
+
fs.writeFileSync(options.output, JSON.stringify(secret.data, null, 2));
|
|
185
|
+
output.success(`Data written to: ${options.output}`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// Display metadata
|
|
189
|
+
console.log('\n--- Secret Metadata ---');
|
|
190
|
+
console.log(`ID: ${secret.id}`);
|
|
191
|
+
console.log(`Alias: ${secret.alias}`);
|
|
192
|
+
console.log(`Tenant: ${secret.tenant}`);
|
|
193
|
+
console.log(`Type: ${formatType(secret.type, secret.subType)}`);
|
|
194
|
+
console.log(`Version: ${secret.version}`);
|
|
195
|
+
// Display data based on type
|
|
196
|
+
console.log('\n--- Secret Data ---');
|
|
197
|
+
if (secret.type === 'credential' && secret.data) {
|
|
198
|
+
if ('username' in secret.data)
|
|
199
|
+
console.log(`Username: ${secret.data.username}`);
|
|
200
|
+
if ('password' in secret.data)
|
|
201
|
+
console.log(`Password: ${secret.data.password}`);
|
|
202
|
+
// Show any additional fields
|
|
203
|
+
const knownFields = ['username', 'password'];
|
|
204
|
+
for (const [key, value] of Object.entries(secret.data)) {
|
|
205
|
+
if (!knownFields.includes(key)) {
|
|
206
|
+
console.log(`${key}: ${typeof value === 'object' ? JSON.stringify(value) : value}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else if (secret.data && 'text' in secret.data) {
|
|
211
|
+
// Plain text secret
|
|
212
|
+
console.log(secret.data.text);
|
|
213
|
+
}
|
|
214
|
+
else if (secret.data && 'content' in secret.data && 'filename' in secret.data) {
|
|
215
|
+
// File-based secret
|
|
216
|
+
console.log(`File: ${secret.data.filename}`);
|
|
217
|
+
console.log(`Size: ${formatBytes(Buffer.from(secret.data.content, 'base64').length)}`);
|
|
218
|
+
if (secret.data.contentType)
|
|
219
|
+
console.log(`Type: ${secret.data.contentType}`);
|
|
220
|
+
console.log('\nUse --output <file> to save the file content');
|
|
221
|
+
}
|
|
222
|
+
else if (secret.data && 'privateKey' in secret.data) {
|
|
223
|
+
// Key pair secret
|
|
224
|
+
console.log('Key Pair Secret:');
|
|
225
|
+
const pk = secret.data.privateKey;
|
|
226
|
+
const pub = secret.data.publicKey;
|
|
227
|
+
if (pk?.filename)
|
|
228
|
+
console.log(` Private Key: ${pk.filename}`);
|
|
229
|
+
if (pub?.filename)
|
|
230
|
+
console.log(` Public Key: ${pub.filename}`);
|
|
231
|
+
console.log('\nUse --output <file> to save the keys');
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
// Generic key-value
|
|
235
|
+
console.log(JSON.stringify(secret.data, null, 2));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
spinner.fail('Failed to decrypt secret');
|
|
240
|
+
output.error(error.message);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async function createSecret(alias, options) {
|
|
245
|
+
if (!options.tenant) {
|
|
246
|
+
output.error('Tenant is required. Use --tenant <id>');
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
let data = {};
|
|
250
|
+
let actualType = options.type || 'opaque';
|
|
251
|
+
// Check for non-interactive data options first
|
|
252
|
+
const hasNonInteractiveData = options.username || options.password || options.text || options.data || options.file;
|
|
253
|
+
if (hasNonInteractiveData) {
|
|
254
|
+
// Non-interactive mode: use CLI options
|
|
255
|
+
if (options.username || options.password) {
|
|
256
|
+
actualType = 'credential';
|
|
257
|
+
data = {
|
|
258
|
+
username: options.username || '',
|
|
259
|
+
password: options.password || '',
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
else if (options.text) {
|
|
263
|
+
data = { text: options.text };
|
|
264
|
+
}
|
|
265
|
+
else if (options.data) {
|
|
266
|
+
try {
|
|
267
|
+
data = JSON.parse(options.data);
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
output.error('Invalid JSON in --data option');
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else if (options.file) {
|
|
275
|
+
const fs = await import('fs');
|
|
276
|
+
const pathModule = await import('path');
|
|
277
|
+
if (!fs.existsSync(options.file)) {
|
|
278
|
+
output.error(`File not found: ${options.file}`);
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
const content = fs.readFileSync(options.file);
|
|
282
|
+
const filename = pathModule.basename(options.file);
|
|
283
|
+
data = {
|
|
284
|
+
filename,
|
|
285
|
+
content: content.toString('base64'),
|
|
286
|
+
contentType: options.contentType || 'application/octet-stream',
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
// Interactive mode: prompt for data
|
|
292
|
+
const { dataType } = await inquirer.prompt([
|
|
293
|
+
{
|
|
294
|
+
type: 'list',
|
|
295
|
+
name: 'dataType',
|
|
296
|
+
message: 'What type of data?',
|
|
297
|
+
choices: [
|
|
298
|
+
{ name: 'Credential (username/password)', value: 'credential' },
|
|
299
|
+
{ name: 'Plain Text', value: 'text' },
|
|
300
|
+
{ name: 'Key-Value pairs', value: 'keyvalue' },
|
|
301
|
+
{ name: 'File upload', value: 'file' },
|
|
302
|
+
],
|
|
303
|
+
},
|
|
304
|
+
]);
|
|
305
|
+
if (dataType === 'credential') {
|
|
306
|
+
actualType = 'credential';
|
|
307
|
+
const answers = await inquirer.prompt([
|
|
308
|
+
{ type: 'input', name: 'username', message: 'Username:' },
|
|
309
|
+
{ type: 'password', name: 'password', message: 'Password:', mask: '*' },
|
|
310
|
+
]);
|
|
311
|
+
data = answers;
|
|
312
|
+
}
|
|
313
|
+
else if (dataType === 'text') {
|
|
314
|
+
const { text } = await inquirer.prompt([
|
|
315
|
+
{ type: 'editor', name: 'text', message: 'Enter text content:' },
|
|
316
|
+
]);
|
|
317
|
+
data = { text: text.trim() };
|
|
318
|
+
}
|
|
319
|
+
else if (dataType === 'keyvalue') {
|
|
320
|
+
console.log('Enter key-value pairs (empty key to finish):');
|
|
321
|
+
while (true) {
|
|
322
|
+
const { key } = await inquirer.prompt([
|
|
323
|
+
{ type: 'input', name: 'key', message: 'Key:' },
|
|
324
|
+
]);
|
|
325
|
+
if (!key)
|
|
326
|
+
break;
|
|
327
|
+
const { value } = await inquirer.prompt([
|
|
328
|
+
{ type: 'input', name: 'value', message: `Value for "${key}":` },
|
|
329
|
+
]);
|
|
330
|
+
data[key] = value;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
else if (dataType === 'file') {
|
|
334
|
+
const { filePath } = await inquirer.prompt([
|
|
335
|
+
{ type: 'input', name: 'filePath', message: 'File path:' },
|
|
336
|
+
]);
|
|
337
|
+
const fs = await import('fs');
|
|
338
|
+
const pathModule = await import('path');
|
|
339
|
+
if (!fs.existsSync(filePath)) {
|
|
340
|
+
output.error(`File not found: ${filePath}`);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
const content = fs.readFileSync(filePath);
|
|
344
|
+
const filename = pathModule.basename(filePath);
|
|
345
|
+
data = {
|
|
346
|
+
filename,
|
|
347
|
+
content: content.toString('base64'),
|
|
348
|
+
contentType: options.contentType || 'application/octet-stream',
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const spinner = ora('Creating secret...').start();
|
|
353
|
+
try {
|
|
354
|
+
const body = {
|
|
355
|
+
alias,
|
|
356
|
+
tenant: options.tenant,
|
|
357
|
+
type: actualType,
|
|
358
|
+
data,
|
|
359
|
+
};
|
|
360
|
+
if (options.subType)
|
|
361
|
+
body.subType = options.subType;
|
|
362
|
+
if (options.tags)
|
|
363
|
+
body.tags = options.tags.split(',').map(t => t.trim());
|
|
364
|
+
if (options.ttl)
|
|
365
|
+
body.ttlUntil = options.ttl;
|
|
366
|
+
if (options.expires)
|
|
367
|
+
body.expiresAt = options.expires;
|
|
368
|
+
if (options.contentType)
|
|
369
|
+
body.contentType = options.contentType;
|
|
370
|
+
const result = await client.post('/v1/secrets', body);
|
|
371
|
+
spinner.stop();
|
|
372
|
+
if (options.json) {
|
|
373
|
+
output.json(result);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
output.success(`Secret created successfully!`);
|
|
377
|
+
console.log(` ID: ${result.id}`);
|
|
378
|
+
console.log(` Alias: ${result.alias}`);
|
|
379
|
+
console.log(` Tenant: ${result.tenant}`);
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
spinner.fail('Failed to create secret');
|
|
383
|
+
output.error(error.message);
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
async function updateSecret(id, options) {
|
|
388
|
+
let newData;
|
|
389
|
+
// Check for non-interactive data option
|
|
390
|
+
if (options.data) {
|
|
391
|
+
// Non-interactive mode: parse JSON data from CLI
|
|
392
|
+
try {
|
|
393
|
+
newData = JSON.parse(options.data);
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
output.error('Invalid JSON in --data option');
|
|
397
|
+
process.exit(1);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
// Interactive mode: prompt for data
|
|
402
|
+
const spinner = ora('Fetching current secret...').start();
|
|
403
|
+
try {
|
|
404
|
+
const current = await client.post(`/v1/secrets/${id}/decrypt`, {});
|
|
405
|
+
spinner.stop();
|
|
406
|
+
const { updateData } = await inquirer.prompt([
|
|
407
|
+
{
|
|
408
|
+
type: 'confirm',
|
|
409
|
+
name: 'updateData',
|
|
410
|
+
message: 'Update the secret data?',
|
|
411
|
+
default: false,
|
|
412
|
+
},
|
|
413
|
+
]);
|
|
414
|
+
newData = current.data;
|
|
415
|
+
if (updateData) {
|
|
416
|
+
if (current.type === 'credential') {
|
|
417
|
+
const answers = await inquirer.prompt([
|
|
418
|
+
{
|
|
419
|
+
type: 'input',
|
|
420
|
+
name: 'username',
|
|
421
|
+
message: 'Username:',
|
|
422
|
+
default: current.data.username
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
type: 'password',
|
|
426
|
+
name: 'password',
|
|
427
|
+
message: 'Password (leave empty to keep current):',
|
|
428
|
+
mask: '*'
|
|
429
|
+
},
|
|
430
|
+
]);
|
|
431
|
+
newData = {
|
|
432
|
+
username: answers.username,
|
|
433
|
+
password: answers.password || current.data.password,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
const { dataJson } = await inquirer.prompt([
|
|
438
|
+
{
|
|
439
|
+
type: 'editor',
|
|
440
|
+
name: 'dataJson',
|
|
441
|
+
message: 'Edit data (JSON):',
|
|
442
|
+
default: JSON.stringify(current.data, null, 2),
|
|
443
|
+
},
|
|
444
|
+
]);
|
|
445
|
+
try {
|
|
446
|
+
newData = JSON.parse(dataJson);
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
output.error('Invalid JSON data');
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
spinner.fail('Failed to fetch current secret');
|
|
457
|
+
output.error(error.message);
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const updateSpinner = ora('Updating secret...').start();
|
|
462
|
+
try {
|
|
463
|
+
const body = { data: newData };
|
|
464
|
+
if (options.tags)
|
|
465
|
+
body.tags = options.tags.split(',').map(t => t.trim());
|
|
466
|
+
if (options.ttl)
|
|
467
|
+
body.ttlUntil = options.ttl;
|
|
468
|
+
if (options.expires)
|
|
469
|
+
body.expiresAt = options.expires;
|
|
470
|
+
const result = await client.put(`/v1/secrets/${id}`, body);
|
|
471
|
+
updateSpinner.stop();
|
|
472
|
+
if (options.json) {
|
|
473
|
+
output.json(result);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
output.success('Secret updated successfully!');
|
|
477
|
+
console.log(` Version: ${result.version}`);
|
|
478
|
+
}
|
|
479
|
+
catch (error) {
|
|
480
|
+
updateSpinner.fail('Failed to update secret');
|
|
481
|
+
output.error(error.message);
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
async function deleteSecret(id, options) {
|
|
486
|
+
if (!options.force) {
|
|
487
|
+
// Get metadata first
|
|
488
|
+
const spinner = ora('Fetching secret...').start();
|
|
489
|
+
try {
|
|
490
|
+
const secret = await client.get(`/v1/secrets/${id}/meta`);
|
|
491
|
+
spinner.stop();
|
|
492
|
+
const { confirm } = await inquirer.prompt([
|
|
493
|
+
{
|
|
494
|
+
type: 'confirm',
|
|
495
|
+
name: 'confirm',
|
|
496
|
+
message: `Delete secret "${secret.alias}" (${id})? This cannot be undone.`,
|
|
497
|
+
default: false,
|
|
498
|
+
},
|
|
499
|
+
]);
|
|
500
|
+
if (!confirm) {
|
|
501
|
+
output.info('Deletion cancelled');
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
spinner.fail('Failed to fetch secret');
|
|
507
|
+
output.error(error.message);
|
|
508
|
+
process.exit(1);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const deleteSpinner = ora('Deleting secret...').start();
|
|
512
|
+
try {
|
|
513
|
+
await client.delete(`/v1/secrets/${id}`);
|
|
514
|
+
deleteSpinner.stop();
|
|
515
|
+
output.success('Secret deleted successfully');
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
deleteSpinner.fail('Failed to delete secret');
|
|
519
|
+
output.error(error.message);
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
async function rotateSecret(id, options) {
|
|
524
|
+
// Get current secret first
|
|
525
|
+
const spinner = ora('Fetching current secret...').start();
|
|
526
|
+
try {
|
|
527
|
+
const current = await client.post(`/v1/secrets/${id}/decrypt`, {});
|
|
528
|
+
spinner.stop();
|
|
529
|
+
console.log(`Current secret: ${current.alias} (v${current.version})`);
|
|
530
|
+
// Prompt for new data
|
|
531
|
+
let newData;
|
|
532
|
+
if (current.type === 'credential') {
|
|
533
|
+
const answers = await inquirer.prompt([
|
|
534
|
+
{
|
|
535
|
+
type: 'password',
|
|
536
|
+
name: 'password',
|
|
537
|
+
message: 'New password:',
|
|
538
|
+
mask: '*',
|
|
539
|
+
validate: (input) => input.length > 0 || 'Password is required',
|
|
540
|
+
},
|
|
541
|
+
]);
|
|
542
|
+
newData = {
|
|
543
|
+
username: current.data.username,
|
|
544
|
+
password: answers.password,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
const { dataJson } = await inquirer.prompt([
|
|
549
|
+
{
|
|
550
|
+
type: 'editor',
|
|
551
|
+
name: 'dataJson',
|
|
552
|
+
message: 'Enter new data (JSON):',
|
|
553
|
+
default: JSON.stringify(current.data, null, 2),
|
|
554
|
+
},
|
|
555
|
+
]);
|
|
556
|
+
try {
|
|
557
|
+
newData = JSON.parse(dataJson);
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
output.error('Invalid JSON data');
|
|
561
|
+
process.exit(1);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
const rotateSpinner = ora('Rotating secret...').start();
|
|
565
|
+
const result = await client.post(`/v1/secrets/${id}/rotate`, { data: newData });
|
|
566
|
+
rotateSpinner.stop();
|
|
567
|
+
if (options.json) {
|
|
568
|
+
output.json(result);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
output.success('Secret rotated successfully!');
|
|
572
|
+
console.log(` New Version: ${result.version}`);
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
spinner.fail('Failed to rotate secret');
|
|
576
|
+
output.error(error.message);
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async function showHistory(id, options) {
|
|
581
|
+
const spinner = ora('Fetching secret history...').start();
|
|
582
|
+
try {
|
|
583
|
+
const response = await client.get(`/v1/secrets/${id}/history`);
|
|
584
|
+
spinner.stop();
|
|
585
|
+
const history = response.history || [];
|
|
586
|
+
if (options.json) {
|
|
587
|
+
output.json(history);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
if (history.length === 0) {
|
|
591
|
+
output.info('No version history found');
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
const table = new Table({
|
|
595
|
+
head: ['Version', 'Created At', 'Superseded At', 'Created By'],
|
|
596
|
+
colWidths: [10, 25, 25, 30],
|
|
597
|
+
});
|
|
598
|
+
for (const entry of history) {
|
|
599
|
+
table.push([
|
|
600
|
+
String(entry.version),
|
|
601
|
+
formatDate(entry.createdAt),
|
|
602
|
+
entry.supersededAt ? formatDate(entry.supersededAt) : '-',
|
|
603
|
+
entry.createdBy || '-',
|
|
604
|
+
]);
|
|
605
|
+
}
|
|
606
|
+
console.log(table.toString());
|
|
607
|
+
console.log(`Total: ${response.count} version(s)`);
|
|
608
|
+
}
|
|
609
|
+
catch (error) {
|
|
610
|
+
spinner.fail('Failed to fetch history');
|
|
611
|
+
output.error(error.message);
|
|
612
|
+
process.exit(1);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
// ============================================================================
|
|
616
|
+
// Command Registration
|
|
617
|
+
// ============================================================================
|
|
618
|
+
export function registerSecretCommands(program) {
|
|
619
|
+
const secret = program
|
|
620
|
+
.command('secret')
|
|
621
|
+
.description('Manage secrets');
|
|
622
|
+
// List secrets
|
|
623
|
+
secret
|
|
624
|
+
.command('list')
|
|
625
|
+
.description('List secrets (metadata only)')
|
|
626
|
+
.option('-t, --tenant <id>', 'Filter by tenant')
|
|
627
|
+
.option('--type <type>', 'Filter by type (opaque, credential, setting)')
|
|
628
|
+
.option('--sub-type <subType>', 'Filter by sub-type')
|
|
629
|
+
.option('--alias-prefix <prefix>', 'Filter by alias prefix')
|
|
630
|
+
.option('--expiring <days>', 'Show secrets expiring within N days')
|
|
631
|
+
.option('--json', 'Output as JSON')
|
|
632
|
+
.action(listSecrets);
|
|
633
|
+
// Get secret metadata
|
|
634
|
+
secret
|
|
635
|
+
.command('get <id>')
|
|
636
|
+
.description('Get secret metadata (no value)')
|
|
637
|
+
.option('--json', 'Output as JSON')
|
|
638
|
+
.action(getSecret);
|
|
639
|
+
// Decrypt secret
|
|
640
|
+
secret
|
|
641
|
+
.command('decrypt <id>')
|
|
642
|
+
.description('Decrypt and show secret value')
|
|
643
|
+
.option('-o, --output <file>', 'Write content to file')
|
|
644
|
+
.option('--json', 'Output as JSON')
|
|
645
|
+
.action(decryptSecret);
|
|
646
|
+
// Create secret
|
|
647
|
+
secret
|
|
648
|
+
.command('create <alias>')
|
|
649
|
+
.description('Create a new secret')
|
|
650
|
+
.requiredOption('-t, --tenant <id>', 'Tenant ID')
|
|
651
|
+
.option('--type <type>', 'Secret type (opaque, credential, setting)', 'opaque')
|
|
652
|
+
.option('--sub-type <subType>', 'Semantic sub-type')
|
|
653
|
+
.option('--tags <tags>', 'Comma-separated tags')
|
|
654
|
+
.option('--ttl <datetime>', 'TTL expiration (ISO 8601)')
|
|
655
|
+
.option('--expires <datetime>', 'Natural expiration (ISO 8601)')
|
|
656
|
+
.option('--content-type <mime>', 'Content type for settings')
|
|
657
|
+
.option('--json', 'Output as JSON')
|
|
658
|
+
// Non-interactive data options
|
|
659
|
+
.option('--username <username>', 'Username for credential type (non-interactive)')
|
|
660
|
+
.option('--password <password>', 'Password for credential type (non-interactive)')
|
|
661
|
+
.option('--text <text>', 'Text content (non-interactive)')
|
|
662
|
+
.option('--data <json>', 'JSON data (non-interactive)')
|
|
663
|
+
.option('--file <path>', 'File to upload (non-interactive)')
|
|
664
|
+
.action(createSecret);
|
|
665
|
+
// Update secret
|
|
666
|
+
secret
|
|
667
|
+
.command('update <id>')
|
|
668
|
+
.description('Update a secret')
|
|
669
|
+
.option('--tags <tags>', 'Comma-separated tags')
|
|
670
|
+
.option('--ttl <datetime>', 'TTL expiration (ISO 8601)')
|
|
671
|
+
.option('--expires <datetime>', 'Natural expiration (ISO 8601)')
|
|
672
|
+
.option('--json', 'Output as JSON')
|
|
673
|
+
.option('--data <json>', 'New data as JSON (non-interactive)')
|
|
674
|
+
.action(updateSecret);
|
|
675
|
+
// Delete secret
|
|
676
|
+
secret
|
|
677
|
+
.command('delete <id>')
|
|
678
|
+
.description('Delete a secret')
|
|
679
|
+
.option('-f, --force', 'Skip confirmation')
|
|
680
|
+
.action(deleteSecret);
|
|
681
|
+
// Rotate secret
|
|
682
|
+
secret
|
|
683
|
+
.command('rotate <id>')
|
|
684
|
+
.description('Rotate secret (create new version)')
|
|
685
|
+
.option('--json', 'Output as JSON')
|
|
686
|
+
.action(rotateSecret);
|
|
687
|
+
// Show history
|
|
688
|
+
secret
|
|
689
|
+
.command('history <id>')
|
|
690
|
+
.description('Show secret version history')
|
|
691
|
+
.option('--json', 'Output as JSON')
|
|
692
|
+
.action(showHistory);
|
|
693
|
+
}
|
|
694
|
+
//# sourceMappingURL=secret.js.map
|