@zincapp/znvault-cli 2.4.0 → 2.6.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 +267 -215
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +159 -476
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +82 -13
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/backup/config.d.ts +10 -0
- package/dist/commands/backup/config.d.ts.map +1 -0
- package/dist/commands/backup/config.js +246 -0
- package/dist/commands/backup/config.js.map +1 -0
- package/dist/commands/backup/helpers.d.ts +8 -0
- package/dist/commands/backup/helpers.d.ts.map +1 -0
- package/dist/commands/backup/helpers.js +81 -0
- package/dist/commands/backup/helpers.js.map +1 -0
- package/dist/commands/{backup.d.ts → backup/index.d.ts} +2 -1
- package/dist/commands/backup/index.d.ts.map +1 -0
- package/dist/commands/backup/index.js +129 -0
- package/dist/commands/backup/index.js.map +1 -0
- package/dist/commands/backup/operations.d.ts +15 -0
- package/dist/commands/backup/operations.d.ts.map +1 -0
- package/dist/commands/backup/operations.js +390 -0
- package/dist/commands/backup/operations.js.map +1 -0
- package/dist/commands/backup/types.d.ts +144 -0
- package/dist/commands/backup/types.d.ts.map +1 -0
- package/dist/commands/backup/types.js +4 -0
- package/dist/commands/backup/types.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/config.d.ts +11 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +20 -1
- package/dist/lib/config.js.map +1 -1
- package/package.json +1 -1
- package/dist/commands/backup.d.ts.map +0 -1
- package/dist/commands/backup.js +0 -646
- package/dist/commands/backup.js.map +0 -1
package/dist/commands/agent.js
CHANGED
|
@@ -1,16 +1,7 @@
|
|
|
1
|
+
// Path: znvault-cli/src/commands/agent.ts
|
|
1
2
|
import ora from 'ora';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import * as os from 'os';
|
|
5
|
-
import { spawn } from 'child_process';
|
|
6
3
|
import * as mode from '../lib/mode.js';
|
|
7
4
|
import * as output from '../lib/output.js';
|
|
8
|
-
import * as config from '../lib/config.js';
|
|
9
|
-
// Config locations - match standalone agent
|
|
10
|
-
const SYSTEM_CONFIG_DIR = '/etc/zn-vault-agent';
|
|
11
|
-
const SYSTEM_CONFIG_FILE = path.join(SYSTEM_CONFIG_DIR, 'config.json');
|
|
12
|
-
const USER_CONFIG_DIR = path.join(os.homedir(), '.config', 'zn-vault-agent');
|
|
13
|
-
const USER_CONFIG_FILE = path.join(USER_CONFIG_DIR, 'config.json');
|
|
14
5
|
/**
|
|
15
6
|
* Format relative time for display
|
|
16
7
|
*/
|
|
@@ -29,470 +20,10 @@ function formatRelativeTime(dateStr) {
|
|
|
29
20
|
return `${diffHours}h ago`;
|
|
30
21
|
return `${diffDays}d ago`;
|
|
31
22
|
}
|
|
32
|
-
// State file location
|
|
33
|
-
const STATE_DIR = path.join(os.homedir(), '.local', 'state', 'zn-vault-agent');
|
|
34
|
-
const DEFAULT_STATE_FILE = path.join(STATE_DIR, 'state.json');
|
|
35
|
-
/**
|
|
36
|
-
* Get the appropriate config file path based on privileges
|
|
37
|
-
*/
|
|
38
|
-
function getConfigPath() {
|
|
39
|
-
// If running as root and system config exists, use it
|
|
40
|
-
if (process.getuid?.() === 0) {
|
|
41
|
-
return SYSTEM_CONFIG_FILE;
|
|
42
|
-
}
|
|
43
|
-
// Check if system config exists (for non-root reading)
|
|
44
|
-
if (fs.existsSync(SYSTEM_CONFIG_FILE)) {
|
|
45
|
-
return SYSTEM_CONFIG_FILE;
|
|
46
|
-
}
|
|
47
|
-
// Fall back to user config
|
|
48
|
-
return USER_CONFIG_FILE;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Load agent configuration
|
|
52
|
-
*/
|
|
53
|
-
function loadConfig(configPath) {
|
|
54
|
-
const filePath = configPath ?? getConfigPath();
|
|
55
|
-
if (!fs.existsSync(filePath)) {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
try {
|
|
59
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Save agent configuration
|
|
67
|
-
*/
|
|
68
|
-
function saveConfig(agentConfig, configPath) {
|
|
69
|
-
const filePath = configPath ?? getConfigPath();
|
|
70
|
-
const dir = path.dirname(filePath);
|
|
71
|
-
if (!fs.existsSync(dir)) {
|
|
72
|
-
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
73
|
-
}
|
|
74
|
-
fs.writeFileSync(filePath, JSON.stringify(agentConfig, null, 2), { mode: 0o600 });
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Create default config with current CLI credentials
|
|
78
|
-
*/
|
|
79
|
-
function createDefaultConfig() {
|
|
80
|
-
const cliConfig = config.getConfig();
|
|
81
|
-
const credentials = config.getCredentials();
|
|
82
|
-
const envCredentials = config.getEnvCredentials();
|
|
83
|
-
const apiKey = config.getApiKey();
|
|
84
|
-
// Get tenant from: env > stored credentials > default tenant
|
|
85
|
-
const tenantId = process.env.ZNVAULT_TENANT_ID ??
|
|
86
|
-
credentials?.tenantId ??
|
|
87
|
-
cliConfig.defaultTenant ??
|
|
88
|
-
'';
|
|
89
|
-
return {
|
|
90
|
-
vaultUrl: cliConfig.url,
|
|
91
|
-
tenantId,
|
|
92
|
-
auth: {
|
|
93
|
-
apiKey: apiKey,
|
|
94
|
-
username: envCredentials?.username,
|
|
95
|
-
password: envCredentials?.password,
|
|
96
|
-
},
|
|
97
|
-
insecure: cliConfig.insecure,
|
|
98
|
-
targets: [],
|
|
99
|
-
pollInterval: 3600,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
23
|
export function registerAgentCommands(program) {
|
|
103
24
|
const agent = program
|
|
104
25
|
.command('agent')
|
|
105
|
-
.description('
|
|
106
|
-
// Initialize agent configuration
|
|
107
|
-
agent
|
|
108
|
-
.command('init')
|
|
109
|
-
.description('Initialize agent configuration')
|
|
110
|
-
.option('-c, --config <path>', 'Config file path')
|
|
111
|
-
.action((options) => {
|
|
112
|
-
const configPath = options.config ?? getConfigPath();
|
|
113
|
-
if (fs.existsSync(configPath)) {
|
|
114
|
-
output.error(`Config already exists at ${configPath}`);
|
|
115
|
-
output.info('Use "znvault agent add" to add certificates');
|
|
116
|
-
process.exit(1);
|
|
117
|
-
}
|
|
118
|
-
const agentConfig = createDefaultConfig();
|
|
119
|
-
saveConfig(agentConfig, configPath);
|
|
120
|
-
console.log(`Agent configuration initialized at ${configPath}`);
|
|
121
|
-
console.log();
|
|
122
|
-
console.log('Next steps:');
|
|
123
|
-
console.log(' 1. Add certificates: znvault agent add <cert-id> --combined /path/to/cert.pem');
|
|
124
|
-
console.log(' 2. Start the agent: zn-vault-agent start');
|
|
125
|
-
console.log();
|
|
126
|
-
console.log('Or install as systemd service:');
|
|
127
|
-
console.log(' sudo systemctl enable --now zn-vault-agent');
|
|
128
|
-
});
|
|
129
|
-
// Add certificate to sync
|
|
130
|
-
agent
|
|
131
|
-
.command('add <cert-id>')
|
|
132
|
-
.description('Add a certificate to sync')
|
|
133
|
-
.option('-n, --name <name>', 'Human-readable name for the certificate')
|
|
134
|
-
.option('--combined <path>', 'Output path for combined cert+key file (HAProxy)')
|
|
135
|
-
.option('--cert <path>', 'Output path for certificate file')
|
|
136
|
-
.option('--key <path>', 'Output path for private key file')
|
|
137
|
-
.option('--chain <path>', 'Output path for CA chain file')
|
|
138
|
-
.option('--fullchain <path>', 'Output path for fullchain file (cert+chain)')
|
|
139
|
-
.option('--owner <user:group>', 'File ownership (e.g., haproxy:haproxy)')
|
|
140
|
-
.option('--mode <mode>', 'File permissions (e.g., 0640)', '0640')
|
|
141
|
-
.option('--reload <command>', 'Command to run after cert update')
|
|
142
|
-
.option('--health-check <command>', 'Health check command (must return 0)')
|
|
143
|
-
.option('-c, --config <path>', 'Config file path')
|
|
144
|
-
.action(async (certId, options) => {
|
|
145
|
-
const spinner = ora('Validating certificate...').start();
|
|
146
|
-
try {
|
|
147
|
-
// Validate the certificate exists
|
|
148
|
-
const cert = await mode.apiGet(`/v1/certificates/${certId}`);
|
|
149
|
-
spinner.stop();
|
|
150
|
-
// Load or create config
|
|
151
|
-
const configPath = options.config ?? getConfigPath();
|
|
152
|
-
let agentConfig = loadConfig(configPath);
|
|
153
|
-
if (!agentConfig) {
|
|
154
|
-
output.info('No config found, creating with current CLI credentials...');
|
|
155
|
-
agentConfig = createDefaultConfig();
|
|
156
|
-
}
|
|
157
|
-
// Check if already added
|
|
158
|
-
if (agentConfig.targets.some(t => t.certId === certId)) {
|
|
159
|
-
output.error(`Certificate ${certId} is already configured`);
|
|
160
|
-
process.exit(1);
|
|
161
|
-
}
|
|
162
|
-
// Validate at least one output is specified
|
|
163
|
-
if (!options.combined && !options.cert && !options.key && !options.chain && !options.fullchain) {
|
|
164
|
-
output.error('At least one output path is required (--combined, --cert, --key, --chain, or --fullchain)');
|
|
165
|
-
process.exit(1);
|
|
166
|
-
}
|
|
167
|
-
const target = {
|
|
168
|
-
certId,
|
|
169
|
-
name: options.name ?? cert.alias,
|
|
170
|
-
outputs: {},
|
|
171
|
-
mode: options.mode,
|
|
172
|
-
};
|
|
173
|
-
if (options.combined)
|
|
174
|
-
target.outputs.combined = options.combined;
|
|
175
|
-
if (options.cert)
|
|
176
|
-
target.outputs.cert = options.cert;
|
|
177
|
-
if (options.key)
|
|
178
|
-
target.outputs.key = options.key;
|
|
179
|
-
if (options.chain)
|
|
180
|
-
target.outputs.chain = options.chain;
|
|
181
|
-
if (options.fullchain)
|
|
182
|
-
target.outputs.fullchain = options.fullchain;
|
|
183
|
-
if (options.owner)
|
|
184
|
-
target.owner = options.owner;
|
|
185
|
-
if (options.reload)
|
|
186
|
-
target.reloadCmd = options.reload;
|
|
187
|
-
if (options.healthCheck)
|
|
188
|
-
target.healthCheckCmd = options.healthCheck;
|
|
189
|
-
agentConfig.targets.push(target);
|
|
190
|
-
saveConfig(agentConfig, configPath);
|
|
191
|
-
console.log(`Added certificate: ${target.name} (${certId})`);
|
|
192
|
-
if (target.outputs.combined)
|
|
193
|
-
console.log(` Combined: ${target.outputs.combined}`);
|
|
194
|
-
if (target.outputs.cert)
|
|
195
|
-
console.log(` Certificate: ${target.outputs.cert}`);
|
|
196
|
-
if (target.outputs.key)
|
|
197
|
-
console.log(` Private key: ${target.outputs.key}`);
|
|
198
|
-
if (target.outputs.chain)
|
|
199
|
-
console.log(` Chain: ${target.outputs.chain}`);
|
|
200
|
-
if (target.outputs.fullchain)
|
|
201
|
-
console.log(` Fullchain: ${target.outputs.fullchain}`);
|
|
202
|
-
if (target.reloadCmd)
|
|
203
|
-
console.log(` Reload: ${target.reloadCmd}`);
|
|
204
|
-
}
|
|
205
|
-
catch (err) {
|
|
206
|
-
spinner.fail('Failed to add certificate');
|
|
207
|
-
output.error(err instanceof Error ? err.message : String(err));
|
|
208
|
-
process.exit(1);
|
|
209
|
-
}
|
|
210
|
-
finally {
|
|
211
|
-
await mode.closeLocalClient();
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
// Remove certificate from sync
|
|
215
|
-
agent
|
|
216
|
-
.command('remove <cert-id-or-name>')
|
|
217
|
-
.description('Remove a certificate from sync')
|
|
218
|
-
.option('-c, --config <path>', 'Config file path')
|
|
219
|
-
.action((certIdOrName, options) => {
|
|
220
|
-
const configPath = options.config ?? getConfigPath();
|
|
221
|
-
const agentConfig = loadConfig(configPath);
|
|
222
|
-
if (!agentConfig) {
|
|
223
|
-
output.error(`Config not found. Run 'znvault agent init' first.`);
|
|
224
|
-
process.exit(1);
|
|
225
|
-
}
|
|
226
|
-
const idx = agentConfig.targets.findIndex(t => t.certId === certIdOrName || t.name === certIdOrName);
|
|
227
|
-
if (idx === -1) {
|
|
228
|
-
output.error(`Certificate "${certIdOrName}" not found in configuration`);
|
|
229
|
-
process.exit(1);
|
|
230
|
-
}
|
|
231
|
-
const removed = agentConfig.targets.splice(idx, 1)[0];
|
|
232
|
-
saveConfig(agentConfig, configPath);
|
|
233
|
-
console.log(`Removed certificate: ${removed.name} (${removed.certId})`);
|
|
234
|
-
});
|
|
235
|
-
// List configured certificates
|
|
236
|
-
agent
|
|
237
|
-
.command('list')
|
|
238
|
-
.description('List configured certificates')
|
|
239
|
-
.option('-c, --config <path>', 'Config file path')
|
|
240
|
-
.option('--json', 'Output as JSON')
|
|
241
|
-
.action((options) => {
|
|
242
|
-
const configPath = options.config ?? getConfigPath();
|
|
243
|
-
const agentConfig = loadConfig(configPath);
|
|
244
|
-
if (!agentConfig) {
|
|
245
|
-
output.error(`Config not found. Run 'znvault agent init' first.`);
|
|
246
|
-
process.exit(1);
|
|
247
|
-
}
|
|
248
|
-
if (options.json) {
|
|
249
|
-
output.json(agentConfig);
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
console.log(`Config: ${configPath}`);
|
|
253
|
-
console.log(`Vault: ${agentConfig.vaultUrl}`);
|
|
254
|
-
console.log(`Tenant: ${agentConfig.tenantId}`);
|
|
255
|
-
console.log(`Certificates: ${agentConfig.targets.length}`);
|
|
256
|
-
console.log();
|
|
257
|
-
if (agentConfig.targets.length === 0) {
|
|
258
|
-
console.log('No certificates configured. Use "znvault agent add <cert-id>" to add one.');
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
output.table(['Name', 'Cert ID', 'Outputs', 'Reload'], agentConfig.targets.map(t => [
|
|
262
|
-
t.name,
|
|
263
|
-
t.certId.substring(0, 8) + '...',
|
|
264
|
-
Object.entries(t.outputs).filter(([, v]) => v).map(([k]) => k).join(', '),
|
|
265
|
-
t.reloadCmd ? t.reloadCmd.substring(0, 30) : '-',
|
|
266
|
-
]));
|
|
267
|
-
});
|
|
268
|
-
// Sync certificates (one-time)
|
|
269
|
-
agent
|
|
270
|
-
.command('sync')
|
|
271
|
-
.description('Sync all configured certificates (one-time)')
|
|
272
|
-
.option('-c, --config <path>', 'Config file path')
|
|
273
|
-
.option('-s, --state <path>', 'State file path', DEFAULT_STATE_FILE)
|
|
274
|
-
.option('--force', 'Force sync even if unchanged')
|
|
275
|
-
.action(async (options) => {
|
|
276
|
-
const spinner = ora('Syncing certificates...').start();
|
|
277
|
-
try {
|
|
278
|
-
const configPath = options.config ?? getConfigPath();
|
|
279
|
-
const agentConfig = loadConfig(configPath);
|
|
280
|
-
if (!agentConfig) {
|
|
281
|
-
spinner.fail('Config not found');
|
|
282
|
-
output.error(`Run 'znvault agent init' first.`);
|
|
283
|
-
process.exit(1);
|
|
284
|
-
}
|
|
285
|
-
if (agentConfig.targets.length === 0) {
|
|
286
|
-
spinner.fail('No certificates configured');
|
|
287
|
-
output.error('Use "znvault agent add <cert-id>" to add certificates.');
|
|
288
|
-
process.exit(1);
|
|
289
|
-
}
|
|
290
|
-
// Load or create state
|
|
291
|
-
let state = { certificates: {}, lastUpdate: new Date().toISOString() };
|
|
292
|
-
if (fs.existsSync(options.state)) {
|
|
293
|
-
state = JSON.parse(fs.readFileSync(options.state, 'utf-8'));
|
|
294
|
-
}
|
|
295
|
-
let synced = 0;
|
|
296
|
-
let skipped = 0;
|
|
297
|
-
let failed = 0;
|
|
298
|
-
for (const target of agentConfig.targets) {
|
|
299
|
-
try {
|
|
300
|
-
// Get certificate with decrypted data
|
|
301
|
-
const cert = await mode.apiPost(`/v1/certificates/${target.certId}/decrypt`, { purpose: 'agent-sync' });
|
|
302
|
-
// Check if changed
|
|
303
|
-
const certFingerprint = cert.fingerprintSha256;
|
|
304
|
-
const existingState = state.certificates[target.certId];
|
|
305
|
-
if (!options.force && existingState.fingerprint === certFingerprint) {
|
|
306
|
-
skipped++;
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
// Decode certificate data
|
|
310
|
-
const certData = Buffer.from(cert.certificateData, 'base64').toString('utf-8');
|
|
311
|
-
const keyData = cert.privateKeyData ? Buffer.from(cert.privateKeyData, 'base64').toString('utf-8') : null;
|
|
312
|
-
const chainData = cert.chainData ? Buffer.from(cert.chainData, 'base64').toString('utf-8') : null;
|
|
313
|
-
const fileMode = parseInt(target.mode ?? '0640', 8);
|
|
314
|
-
// Write certificate file
|
|
315
|
-
if (target.outputs.cert) {
|
|
316
|
-
ensureDir(path.dirname(target.outputs.cert));
|
|
317
|
-
fs.writeFileSync(target.outputs.cert, certData, { mode: fileMode });
|
|
318
|
-
}
|
|
319
|
-
// Write private key
|
|
320
|
-
if (target.outputs.key && keyData) {
|
|
321
|
-
ensureDir(path.dirname(target.outputs.key));
|
|
322
|
-
fs.writeFileSync(target.outputs.key, keyData, { mode: 0o600 });
|
|
323
|
-
}
|
|
324
|
-
// Write chain
|
|
325
|
-
if (target.outputs.chain && chainData) {
|
|
326
|
-
ensureDir(path.dirname(target.outputs.chain));
|
|
327
|
-
fs.writeFileSync(target.outputs.chain, chainData, { mode: fileMode });
|
|
328
|
-
}
|
|
329
|
-
// Write fullchain (cert + chain)
|
|
330
|
-
if (target.outputs.fullchain) {
|
|
331
|
-
let fullchain = certData;
|
|
332
|
-
if (chainData)
|
|
333
|
-
fullchain += '\n' + chainData;
|
|
334
|
-
ensureDir(path.dirname(target.outputs.fullchain));
|
|
335
|
-
fs.writeFileSync(target.outputs.fullchain, fullchain, { mode: fileMode });
|
|
336
|
-
}
|
|
337
|
-
// Write combined (cert + key + chain)
|
|
338
|
-
if (target.outputs.combined) {
|
|
339
|
-
let combined = certData;
|
|
340
|
-
if (keyData)
|
|
341
|
-
combined += '\n' + keyData;
|
|
342
|
-
if (chainData)
|
|
343
|
-
combined += '\n' + chainData;
|
|
344
|
-
ensureDir(path.dirname(target.outputs.combined));
|
|
345
|
-
fs.writeFileSync(target.outputs.combined, combined, { mode: 0o600 });
|
|
346
|
-
}
|
|
347
|
-
// Update state
|
|
348
|
-
state.certificates[target.certId] = {
|
|
349
|
-
id: target.certId,
|
|
350
|
-
alias: cert.alias,
|
|
351
|
-
lastSync: new Date().toISOString(),
|
|
352
|
-
version: cert.version,
|
|
353
|
-
fingerprint: certFingerprint,
|
|
354
|
-
};
|
|
355
|
-
synced++;
|
|
356
|
-
}
|
|
357
|
-
catch (err) {
|
|
358
|
-
failed++;
|
|
359
|
-
console.error(`\nFailed to sync ${target.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
// Save state
|
|
363
|
-
state.lastUpdate = new Date().toISOString();
|
|
364
|
-
ensureDir(path.dirname(options.state));
|
|
365
|
-
fs.writeFileSync(options.state, JSON.stringify(state, null, 2));
|
|
366
|
-
spinner.stop();
|
|
367
|
-
console.log(`Sync complete: ${synced} synced, ${skipped} unchanged, ${failed} failed`);
|
|
368
|
-
}
|
|
369
|
-
catch (err) {
|
|
370
|
-
spinner.fail('Sync failed');
|
|
371
|
-
output.error(err instanceof Error ? err.message : String(err));
|
|
372
|
-
process.exit(1);
|
|
373
|
-
}
|
|
374
|
-
finally {
|
|
375
|
-
await mode.closeLocalClient();
|
|
376
|
-
}
|
|
377
|
-
});
|
|
378
|
-
// Start agent daemon (delegates to standalone agent)
|
|
379
|
-
agent
|
|
380
|
-
.command('start')
|
|
381
|
-
.description('Start the certificate sync agent daemon')
|
|
382
|
-
.option('-c, --config <path>', 'Config file path')
|
|
383
|
-
.option('-v, --verbose', 'Enable verbose logging')
|
|
384
|
-
.option('--health-port <port>', 'Enable health/metrics HTTP server')
|
|
385
|
-
.option('--foreground', 'Run in foreground')
|
|
386
|
-
.action((options) => {
|
|
387
|
-
const configPath = options.config ?? getConfigPath();
|
|
388
|
-
if (!fs.existsSync(configPath)) {
|
|
389
|
-
output.error(`Config not found at ${configPath}`);
|
|
390
|
-
output.info(`Run 'znvault agent init' first.`);
|
|
391
|
-
process.exit(1);
|
|
392
|
-
}
|
|
393
|
-
// Build command arguments
|
|
394
|
-
const args = ['start'];
|
|
395
|
-
if (options.verbose)
|
|
396
|
-
args.push('--verbose');
|
|
397
|
-
if (options.healthPort)
|
|
398
|
-
args.push('--health-port', options.healthPort);
|
|
399
|
-
// Set config path via environment if not default
|
|
400
|
-
const env = { ...process.env };
|
|
401
|
-
if (options.config) {
|
|
402
|
-
env.ZNVAULT_AGENT_CONFIG_DIR = path.dirname(options.config);
|
|
403
|
-
}
|
|
404
|
-
console.log('Starting zn-vault-agent daemon...');
|
|
405
|
-
console.log();
|
|
406
|
-
// Try to find the standalone agent
|
|
407
|
-
const agentPaths = [
|
|
408
|
-
'/usr/local/bin/zn-vault-agent',
|
|
409
|
-
'/usr/bin/zn-vault-agent',
|
|
410
|
-
path.join(os.homedir(), '.local', 'bin', 'zn-vault-agent'),
|
|
411
|
-
// Development: check sibling directory
|
|
412
|
-
path.resolve(__dirname, '..', '..', '..', '..', 'zn-vault-agent', 'dist', 'index.js'),
|
|
413
|
-
];
|
|
414
|
-
let agentPath = null;
|
|
415
|
-
for (const p of agentPaths) {
|
|
416
|
-
if (fs.existsSync(p)) {
|
|
417
|
-
agentPath = p;
|
|
418
|
-
break;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
if (!agentPath) {
|
|
422
|
-
output.error('zn-vault-agent not found');
|
|
423
|
-
console.log();
|
|
424
|
-
console.log('Install the standalone agent:');
|
|
425
|
-
console.log(' cd zn-vault-agent && npm install && npm run build');
|
|
426
|
-
console.log(' sudo ./deploy/install.sh');
|
|
427
|
-
console.log();
|
|
428
|
-
console.log('Or run directly:');
|
|
429
|
-
console.log(' cd zn-vault-agent && npm run start -- start');
|
|
430
|
-
process.exit(1);
|
|
431
|
-
}
|
|
432
|
-
// Determine how to run it
|
|
433
|
-
const isJsFile = agentPath.endsWith('.js');
|
|
434
|
-
const command = isJsFile ? 'node' : agentPath;
|
|
435
|
-
const spawnArgs = isJsFile ? [agentPath, ...args] : args;
|
|
436
|
-
// Spawn the agent
|
|
437
|
-
const child = spawn(command, spawnArgs, {
|
|
438
|
-
env,
|
|
439
|
-
stdio: 'inherit',
|
|
440
|
-
detached: !options.foreground,
|
|
441
|
-
});
|
|
442
|
-
if (!options.foreground) {
|
|
443
|
-
child.unref();
|
|
444
|
-
console.log(`Agent started with PID ${String(child.pid)}`);
|
|
445
|
-
process.exit(0);
|
|
446
|
-
}
|
|
447
|
-
// In foreground mode, wait for the process
|
|
448
|
-
child.on('exit', (code) => {
|
|
449
|
-
process.exit(code ?? 0);
|
|
450
|
-
});
|
|
451
|
-
});
|
|
452
|
-
// Show agent status
|
|
453
|
-
agent
|
|
454
|
-
.command('status')
|
|
455
|
-
.description('Show agent configuration and sync status')
|
|
456
|
-
.option('-c, --config <path>', 'Config file path')
|
|
457
|
-
.option('-s, --state <path>', 'State file path', DEFAULT_STATE_FILE)
|
|
458
|
-
.option('--json', 'Output as JSON')
|
|
459
|
-
.action((options) => {
|
|
460
|
-
const configPath = options.config ?? getConfigPath();
|
|
461
|
-
const agentConfig = loadConfig(configPath);
|
|
462
|
-
if (!agentConfig) {
|
|
463
|
-
output.error(`Config not found. Run 'znvault agent init' first.`);
|
|
464
|
-
process.exit(1);
|
|
465
|
-
}
|
|
466
|
-
let state = { certificates: {}, lastUpdate: 'never' };
|
|
467
|
-
if (fs.existsSync(options.state)) {
|
|
468
|
-
state = JSON.parse(fs.readFileSync(options.state, 'utf-8'));
|
|
469
|
-
}
|
|
470
|
-
if (options.json) {
|
|
471
|
-
output.json({ config: agentConfig, state });
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
console.log('Agent Configuration:');
|
|
475
|
-
console.log(` Config file: ${configPath}`);
|
|
476
|
-
console.log(` Vault URL: ${agentConfig.vaultUrl}`);
|
|
477
|
-
console.log(` Tenant: ${agentConfig.tenantId}`);
|
|
478
|
-
console.log(` Certificates: ${agentConfig.targets.length}`);
|
|
479
|
-
console.log(` Last sync: ${state.lastUpdate}`);
|
|
480
|
-
console.log();
|
|
481
|
-
if (agentConfig.targets.length === 0) {
|
|
482
|
-
console.log('No certificates configured.');
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
output.table(['Name', 'Cert ID', 'Last Sync', 'Version', 'Fingerprint'], agentConfig.targets.map(t => {
|
|
486
|
-
const s = state.certificates[t.certId];
|
|
487
|
-
return [
|
|
488
|
-
t.name,
|
|
489
|
-
t.certId.substring(0, 8) + '...',
|
|
490
|
-
new Date(s.lastSync).toLocaleString(),
|
|
491
|
-
String(s.version),
|
|
492
|
-
s.fingerprint.substring(0, 16) + '...',
|
|
493
|
-
];
|
|
494
|
-
}));
|
|
495
|
-
});
|
|
26
|
+
.description('Manage remote agents and registration tokens');
|
|
496
27
|
// ===== Remote Agent Management Commands =====
|
|
497
28
|
const remote = agent
|
|
498
29
|
.command('remote')
|
|
@@ -651,10 +182,162 @@ export function registerAgentCommands(program) {
|
|
|
651
182
|
await mode.closeLocalClient();
|
|
652
183
|
}
|
|
653
184
|
});
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
185
|
+
// ===== Registration Token Commands =====
|
|
186
|
+
const token = agent
|
|
187
|
+
.command('token')
|
|
188
|
+
.description('Manage registration tokens for agent bootstrapping');
|
|
189
|
+
// Create registration token
|
|
190
|
+
token
|
|
191
|
+
.command('create')
|
|
192
|
+
.description('Create a one-time registration token for managed key binding')
|
|
193
|
+
.requiredOption('-k, --managed-key <name>', 'Name of the managed key to bind')
|
|
194
|
+
.option('-e, --expires <duration>', 'Token expiration (e.g., "1h", "24h")', '1h')
|
|
195
|
+
.option('-d, --description <text>', 'Optional description for audit trail')
|
|
196
|
+
.option('--tenant <tenantId>', 'Target tenant ID (superadmin only)')
|
|
197
|
+
.action(async (options) => {
|
|
198
|
+
const spinner = ora('Creating registration token...').start();
|
|
199
|
+
try {
|
|
200
|
+
const tenantQuery = options.tenant ? `?tenantId=${encodeURIComponent(options.tenant)}` : '';
|
|
201
|
+
const response = await mode.apiPost(`/auth/api-keys/managed/${encodeURIComponent(options.managedKey)}/registration-tokens${tenantQuery}`, {
|
|
202
|
+
expiresIn: options.expires,
|
|
203
|
+
description: options.description,
|
|
204
|
+
});
|
|
205
|
+
spinner.succeed('Registration token created');
|
|
206
|
+
console.log();
|
|
207
|
+
console.log('Token (save this - shown only once!):');
|
|
208
|
+
console.log(` ${response.token}`);
|
|
209
|
+
console.log();
|
|
210
|
+
console.log('Details:');
|
|
211
|
+
console.log(` Prefix: ${response.prefix}`);
|
|
212
|
+
console.log(` Managed Key: ${response.managedKeyName}`);
|
|
213
|
+
console.log(` Tenant: ${response.tenantId}`);
|
|
214
|
+
console.log(` Expires: ${new Date(response.expiresAt).toLocaleString()}`);
|
|
215
|
+
if (response.description) {
|
|
216
|
+
console.log(` Description: ${response.description}`);
|
|
217
|
+
}
|
|
218
|
+
console.log();
|
|
219
|
+
console.log('Usage:');
|
|
220
|
+
console.log(` curl -sSL https://vault.example.com/agent/bootstrap.sh | ZNVAULT_TOKEN=${response.token} bash`);
|
|
221
|
+
console.log();
|
|
222
|
+
console.log('Or manually:');
|
|
223
|
+
console.log(` curl -X POST https://vault.example.com/agent/bootstrap \\`);
|
|
224
|
+
console.log(` -H "Content-Type: application/json" \\`);
|
|
225
|
+
console.log(` -d '{"token": "${response.token}"}'`);
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
spinner.fail('Failed to create registration token');
|
|
229
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
await mode.closeLocalClient();
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
// List registration tokens
|
|
237
|
+
token
|
|
238
|
+
.command('list')
|
|
239
|
+
.description('List registration tokens for a managed key')
|
|
240
|
+
.requiredOption('-k, --managed-key <name>', 'Name of the managed key')
|
|
241
|
+
.option('--include-used', 'Include already-used tokens')
|
|
242
|
+
.option('--tenant <tenantId>', 'Target tenant ID (superadmin only)')
|
|
243
|
+
.option('--json', 'Output as JSON')
|
|
244
|
+
.action(async (options) => {
|
|
245
|
+
const spinner = ora('Fetching registration tokens...').start();
|
|
246
|
+
try {
|
|
247
|
+
const params = new URLSearchParams();
|
|
248
|
+
if (options.tenant)
|
|
249
|
+
params.set('tenantId', options.tenant);
|
|
250
|
+
if (options.includeUsed)
|
|
251
|
+
params.set('includeUsed', 'true');
|
|
252
|
+
const query = params.toString();
|
|
253
|
+
const response = await mode.apiGet(`/auth/api-keys/managed/${encodeURIComponent(options.managedKey)}/registration-tokens${query ? `?${query}` : ''}`);
|
|
254
|
+
spinner.stop();
|
|
255
|
+
if (options.json) {
|
|
256
|
+
output.json(response);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (response.tokens.length === 0) {
|
|
260
|
+
console.log('No registration tokens found');
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
console.log(`Registration tokens for ${options.managedKey}:`);
|
|
264
|
+
console.log();
|
|
265
|
+
output.table(['Prefix', 'Status', 'Created', 'Expires', 'Description'], response.tokens.map(t => [
|
|
266
|
+
t.prefix,
|
|
267
|
+
t.status === 'active' ? '● active' :
|
|
268
|
+
t.status === 'used' ? '○ used' :
|
|
269
|
+
t.status === 'expired' ? '○ expired' : '○ revoked',
|
|
270
|
+
formatRelativeTime(t.createdAt),
|
|
271
|
+
formatRelativeTime(t.expiresAt),
|
|
272
|
+
t.description?.substring(0, 30) ?? '-',
|
|
273
|
+
]));
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
spinner.fail('Failed to fetch registration tokens');
|
|
277
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
await mode.closeLocalClient();
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
// Revoke registration token
|
|
285
|
+
token
|
|
286
|
+
.command('revoke <token-id>')
|
|
287
|
+
.description('Revoke a registration token (prevents future use)')
|
|
288
|
+
.requiredOption('-k, --managed-key <name>', 'Name of the managed key')
|
|
289
|
+
.option('--tenant <tenantId>', 'Target tenant ID (superadmin only)')
|
|
290
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
291
|
+
.action(async (tokenId, options) => {
|
|
292
|
+
if (!options.yes) {
|
|
293
|
+
const readline = await import('readline');
|
|
294
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
295
|
+
const answer = await new Promise(resolve => {
|
|
296
|
+
rl.question(`Revoke token ${tokenId}? This cannot be undone. [y/N] `, resolve);
|
|
297
|
+
});
|
|
298
|
+
rl.close();
|
|
299
|
+
if (answer.toLowerCase() !== 'y') {
|
|
300
|
+
console.log('Cancelled');
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const spinner = ora('Revoking registration token...').start();
|
|
305
|
+
try {
|
|
306
|
+
const tenantQuery = options.tenant ? `?tenantId=${encodeURIComponent(options.tenant)}` : '';
|
|
307
|
+
await mode.apiDelete(`/auth/api-keys/managed/${encodeURIComponent(options.managedKey)}/registration-tokens/${encodeURIComponent(tokenId)}${tenantQuery}`);
|
|
308
|
+
spinner.succeed('Registration token revoked');
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
spinner.fail('Failed to revoke registration token');
|
|
312
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
finally {
|
|
316
|
+
await mode.closeLocalClient();
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
// Help text pointing to zn-vault-agent
|
|
320
|
+
agent
|
|
321
|
+
.command('help-local')
|
|
322
|
+
.description('Show help for local agent operations')
|
|
323
|
+
.action(() => {
|
|
324
|
+
console.log('Local Agent Operations');
|
|
325
|
+
console.log('======================');
|
|
326
|
+
console.log();
|
|
327
|
+
console.log('For local agent configuration and sync operations, use the standalone agent:');
|
|
328
|
+
console.log();
|
|
329
|
+
console.log(' zn-vault-agent login # Authenticate with vault');
|
|
330
|
+
console.log(' zn-vault-agent setup # Interactive setup');
|
|
331
|
+
console.log(' zn-vault-agent sync # Sync secrets/certificates');
|
|
332
|
+
console.log(' zn-vault-agent start # Start agent daemon');
|
|
333
|
+
console.log(' zn-vault-agent status # Show agent status');
|
|
334
|
+
console.log(' zn-vault-agent exec # Execute with secrets injected');
|
|
335
|
+
console.log();
|
|
336
|
+
console.log('Install the standalone agent:');
|
|
337
|
+
console.log(' npm install -g @zincapp/zn-vault-agent');
|
|
338
|
+
console.log();
|
|
339
|
+
console.log('For more information:');
|
|
340
|
+
console.log(' zn-vault-agent --help');
|
|
341
|
+
});
|
|
659
342
|
}
|
|
660
343
|
//# sourceMappingURL=agent.js.map
|