@zincapp/znvault-cli 2.26.4 → 2.29.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/dist/commands/group.d.ts +3 -0
- package/dist/commands/group.d.ts.map +1 -0
- package/dist/commands/group.js +319 -0
- package/dist/commands/group.js.map +1 -0
- package/dist/commands/host/bootstrap-token.d.ts.map +1 -1
- package/dist/commands/host/bootstrap-token.js +28 -10
- package/dist/commands/host/bootstrap-token.js.map +1 -1
- package/dist/commands/host/index.d.ts.map +1 -1
- package/dist/commands/host/index.js +4 -0
- package/dist/commands/host/index.js.map +1 -1
- package/dist/commands/host/link-agent.d.ts +26 -0
- package/dist/commands/host/link-agent.d.ts.map +1 -0
- package/dist/commands/host/link-agent.js +110 -0
- package/dist/commands/host/link-agent.js.map +1 -0
- package/dist/commands/host/types.d.ts +5 -0
- package/dist/commands/host/types.d.ts.map +1 -1
- package/dist/commands/ssh-ca/ca.d.ts +14 -0
- package/dist/commands/ssh-ca/ca.d.ts.map +1 -0
- package/dist/commands/ssh-ca/ca.js +169 -0
- package/dist/commands/ssh-ca/ca.js.map +1 -0
- package/dist/commands/ssh-ca/certificates.d.ts +7 -0
- package/dist/commands/ssh-ca/certificates.d.ts.map +1 -0
- package/dist/commands/ssh-ca/certificates.js +131 -0
- package/dist/commands/ssh-ca/certificates.js.map +1 -0
- package/dist/commands/ssh-ca/helpers.d.ts +37 -0
- package/dist/commands/ssh-ca/helpers.d.ts.map +1 -0
- package/dist/commands/ssh-ca/helpers.js +104 -0
- package/dist/commands/ssh-ca/helpers.js.map +1 -0
- package/dist/commands/ssh-ca/index.d.ts +7 -0
- package/dist/commands/ssh-ca/index.d.ts.map +1 -0
- package/dist/commands/ssh-ca/index.js +180 -0
- package/dist/commands/ssh-ca/index.js.map +1 -0
- package/dist/commands/ssh-ca/mappings.d.ts +11 -0
- package/dist/commands/ssh-ca/mappings.d.ts.map +1 -0
- package/dist/commands/ssh-ca/mappings.js +178 -0
- package/dist/commands/ssh-ca/mappings.js.map +1 -0
- package/dist/commands/ssh-ca/server-groups.d.ts +21 -0
- package/dist/commands/ssh-ca/server-groups.d.ts.map +1 -0
- package/dist/commands/ssh-ca/server-groups.js +252 -0
- package/dist/commands/ssh-ca/server-groups.js.map +1 -0
- package/dist/commands/ssh-ca/sign.d.ts +3 -0
- package/dist/commands/ssh-ca/sign.d.ts.map +1 -0
- package/dist/commands/ssh-ca/sign.js +79 -0
- package/dist/commands/ssh-ca/sign.js.map +1 -0
- package/dist/commands/ssh-ca/types.d.ts +135 -0
- package/dist/commands/ssh-ca/types.d.ts.map +1 -0
- package/dist/commands/ssh-ca/types.js +3 -0
- package/dist/commands/ssh-ca/types.js.map +1 -0
- package/dist/commands/ssh-ca.d.ts +7 -0
- package/dist/commands/ssh-ca.d.ts.map +1 -0
- package/dist/commands/ssh-ca.js +7 -0
- package/dist/commands/ssh-ca.js.map +1 -0
- package/dist/commands/ssh.d.ts +3 -0
- package/dist/commands/ssh.d.ts.map +1 -0
- package/dist/commands/ssh.js +814 -0
- package/dist/commands/ssh.js.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/client/tenants.d.ts.map +1 -1
- package/dist/lib/client/tenants.js +3 -6
- package/dist/lib/client/tenants.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
// Path: znvault-cli/src/commands/ssh.ts
|
|
2
|
+
// SSH Certificate Authority commands
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { client } from '../lib/client.js';
|
|
5
|
+
import { promptConfirm } from '../lib/prompts.js';
|
|
6
|
+
import * as output from '../lib/output.js';
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Helper Functions
|
|
9
|
+
// ============================================================================
|
|
10
|
+
function formatTtl(seconds) {
|
|
11
|
+
if (seconds < 60)
|
|
12
|
+
return `${seconds}s`;
|
|
13
|
+
if (seconds < 3600)
|
|
14
|
+
return `${Math.floor(seconds / 60)}m`;
|
|
15
|
+
if (seconds < 86400)
|
|
16
|
+
return `${Math.floor(seconds / 3600)}h`;
|
|
17
|
+
return `${Math.floor(seconds / 86400)}d`;
|
|
18
|
+
}
|
|
19
|
+
function parseTtl(ttl) {
|
|
20
|
+
const match = ttl.match(/^(\d+)([smhd])?$/i);
|
|
21
|
+
if (!match) {
|
|
22
|
+
throw new Error(`Invalid TTL format: ${ttl}. Use format like 8h, 30m, 1d, or 3600`);
|
|
23
|
+
}
|
|
24
|
+
const value = parseInt(match[1]);
|
|
25
|
+
const unit = match[2]?.toLowerCase() ?? 's';
|
|
26
|
+
switch (unit) {
|
|
27
|
+
case 's': return value;
|
|
28
|
+
case 'm': return value * 60;
|
|
29
|
+
case 'h': return value * 3600;
|
|
30
|
+
case 'd': return value * 86400;
|
|
31
|
+
default: return value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function isExpired(validBefore) {
|
|
35
|
+
return new Date(validBefore) < new Date();
|
|
36
|
+
}
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Commands
|
|
39
|
+
// ============================================================================
|
|
40
|
+
export function registerSSHCommands(program) {
|
|
41
|
+
const ssh = program
|
|
42
|
+
.command('ssh')
|
|
43
|
+
.description('SSH Certificate Authority management');
|
|
44
|
+
// ===========================================================================
|
|
45
|
+
// CA Management
|
|
46
|
+
// ===========================================================================
|
|
47
|
+
const ca = ssh
|
|
48
|
+
.command('ca')
|
|
49
|
+
.description('SSH CA management');
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Get CA Status
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
ca
|
|
54
|
+
.command('status')
|
|
55
|
+
.description('Get SSH CA status for current tenant')
|
|
56
|
+
.option('--tenant <id>', 'Tenant ID (superadmin only)')
|
|
57
|
+
.option('--json', 'Output as JSON')
|
|
58
|
+
.action(async (options) => {
|
|
59
|
+
const spinner = ora('Fetching CA status...').start();
|
|
60
|
+
try {
|
|
61
|
+
const query = options.tenant ? `?tenantId=${encodeURIComponent(options.tenant)}` : '';
|
|
62
|
+
const status = await client.get(`/v1/ssh/ca${query}`);
|
|
63
|
+
spinner.stop();
|
|
64
|
+
if (options.json) {
|
|
65
|
+
output.json(status);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (!status.initialized) {
|
|
69
|
+
output.warn('SSH CA is not initialized for this tenant');
|
|
70
|
+
output.info('Use "znvault ssh ca init" to initialize');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
output.section('SSH CA Status');
|
|
74
|
+
output.keyValue({
|
|
75
|
+
'Status': '✓ Initialized',
|
|
76
|
+
'Key Type': status.keyType ?? '-',
|
|
77
|
+
'Fingerprint': status.fingerprint ?? '-',
|
|
78
|
+
'Default TTL': status.defaultTtlSeconds ? formatTtl(status.defaultTtlSeconds) : '-',
|
|
79
|
+
'Max TTL': status.maxTtlSeconds ? formatTtl(status.maxTtlSeconds) : '-',
|
|
80
|
+
'Extensions': status.allowedExtensions?.join(', ') ?? '-',
|
|
81
|
+
'Total Certificates': status.totalCertificates ?? '-',
|
|
82
|
+
'Active Certificates': status.activeCertificates ?? '-',
|
|
83
|
+
'Created': status.createdAt ? output.formatDate(status.createdAt) : '-',
|
|
84
|
+
});
|
|
85
|
+
if (status.publicKey) {
|
|
86
|
+
output.section('CA Public Key');
|
|
87
|
+
console.log(status.publicKey);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
spinner.fail('Failed to fetch CA status');
|
|
92
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Initialize CA
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
ca
|
|
100
|
+
.command('init')
|
|
101
|
+
.description('Initialize SSH CA for tenant')
|
|
102
|
+
.option('--tenant <id>', 'Tenant ID (superadmin only)')
|
|
103
|
+
.option('--key-type <type>', 'Key type (ed25519 or rsa-4096)', 'ed25519')
|
|
104
|
+
.option('--default-ttl <ttl>', 'Default certificate TTL (e.g., 8h, 1d)', '8h')
|
|
105
|
+
.option('--max-ttl <ttl>', 'Maximum certificate TTL (e.g., 24h, 7d)', '24h')
|
|
106
|
+
.option('--extension <ext...>', 'Allowed extensions', ['permit-pty', 'permit-port-forwarding'])
|
|
107
|
+
.option('--json', 'Output as JSON')
|
|
108
|
+
.action(async (options) => {
|
|
109
|
+
const spinner = ora('Initializing SSH CA...').start();
|
|
110
|
+
try {
|
|
111
|
+
const query = options.tenant ? `?tenantId=${encodeURIComponent(options.tenant)}` : '';
|
|
112
|
+
const body = {
|
|
113
|
+
keyType: options.keyType,
|
|
114
|
+
defaultTtlSeconds: options.defaultTtl ? parseTtl(options.defaultTtl) : undefined,
|
|
115
|
+
maxTtlSeconds: options.maxTtl ? parseTtl(options.maxTtl) : undefined,
|
|
116
|
+
allowedExtensions: options.extension,
|
|
117
|
+
};
|
|
118
|
+
const ca = await client.post(`/v1/ssh/ca${query}`, body);
|
|
119
|
+
spinner.succeed('SSH CA initialized successfully');
|
|
120
|
+
if (options.json) {
|
|
121
|
+
output.json(ca);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
output.section('CA Configuration');
|
|
125
|
+
output.keyValue({
|
|
126
|
+
'ID': ca.id,
|
|
127
|
+
'Key Type': ca.keyType,
|
|
128
|
+
'Fingerprint': ca.fingerprint,
|
|
129
|
+
'Default TTL': formatTtl(ca.defaultTtlSeconds),
|
|
130
|
+
'Max TTL': formatTtl(ca.maxTtlSeconds),
|
|
131
|
+
'Extensions': ca.allowedExtensions.join(', '),
|
|
132
|
+
'Created': output.formatDate(ca.createdAt),
|
|
133
|
+
});
|
|
134
|
+
output.section('CA Public Key');
|
|
135
|
+
console.log(ca.publicKey);
|
|
136
|
+
console.log();
|
|
137
|
+
output.info('Add this public key to your servers\' TrustedUserCAKeys configuration.');
|
|
138
|
+
output.info('Example sshd_config:');
|
|
139
|
+
console.log(' TrustedUserCAKeys /etc/ssh/ca.pub');
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
spinner.fail('Failed to initialize SSH CA');
|
|
143
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Delete CA
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
ca
|
|
151
|
+
.command('delete')
|
|
152
|
+
.description('Delete SSH CA (DESTRUCTIVE)')
|
|
153
|
+
.option('--tenant <id>', 'Tenant ID (superadmin only)')
|
|
154
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
155
|
+
.action(async (options) => {
|
|
156
|
+
try {
|
|
157
|
+
const query = options.tenant ? `?tenantId=${encodeURIComponent(options.tenant)}` : '';
|
|
158
|
+
if (!options.yes) {
|
|
159
|
+
output.warn('This will permanently delete the SSH CA and invalidate all issued certificates!');
|
|
160
|
+
const confirmed = await promptConfirm('Are you sure you want to delete the SSH CA?');
|
|
161
|
+
if (!confirmed) {
|
|
162
|
+
output.info('Delete cancelled');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const spinner = ora('Deleting SSH CA...').start();
|
|
167
|
+
try {
|
|
168
|
+
await client.delete(`/v1/ssh/ca${query}`);
|
|
169
|
+
spinner.succeed('SSH CA deleted successfully');
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
spinner.fail('Failed to delete SSH CA');
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Get CA Public Key
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
ca
|
|
185
|
+
.command('public-key <tenantId>')
|
|
186
|
+
.description('Get CA public key (for server configuration)')
|
|
187
|
+
.option('--raw', 'Output raw key only (no formatting)')
|
|
188
|
+
.action(async (tenantId, options) => {
|
|
189
|
+
try {
|
|
190
|
+
const response = await client.get(`/v1/ssh/ca/${encodeURIComponent(tenantId)}/public-key`);
|
|
191
|
+
if (options.raw) {
|
|
192
|
+
console.log(response.publicKey);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
output.section('CA Public Key');
|
|
196
|
+
output.keyValue({
|
|
197
|
+
'Fingerprint': response.fingerprint,
|
|
198
|
+
});
|
|
199
|
+
console.log();
|
|
200
|
+
console.log(response.publicKey);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
// ===========================================================================
|
|
209
|
+
// Certificate Management
|
|
210
|
+
// ===========================================================================
|
|
211
|
+
const cert = ssh
|
|
212
|
+
.command('cert')
|
|
213
|
+
.description('SSH certificate management');
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// Sign Public Key
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
cert
|
|
218
|
+
.command('sign <publicKeyFile>')
|
|
219
|
+
.description('Sign SSH public key to create certificate')
|
|
220
|
+
.option('--tenant <id>', 'Tenant ID (superadmin only)')
|
|
221
|
+
.option('--ttl <ttl>', 'Certificate TTL (e.g., 8h, 1d)')
|
|
222
|
+
.option('-o, --output <file>', 'Output certificate to file')
|
|
223
|
+
.option('--json', 'Output as JSON')
|
|
224
|
+
.action(async (publicKeyFile, options) => {
|
|
225
|
+
const spinner = ora('Signing certificate...').start();
|
|
226
|
+
try {
|
|
227
|
+
const fs = await import('fs');
|
|
228
|
+
const path = await import('path');
|
|
229
|
+
// Read public key
|
|
230
|
+
const publicKeyPath = path.resolve(publicKeyFile);
|
|
231
|
+
if (!fs.existsSync(publicKeyPath)) {
|
|
232
|
+
spinner.fail('Public key file not found');
|
|
233
|
+
output.error(`File not found: ${publicKeyPath}`);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
const publicKey = fs.readFileSync(publicKeyPath, 'utf8').trim();
|
|
237
|
+
const query = options.tenant ? `?tenantId=${encodeURIComponent(options.tenant)}` : '';
|
|
238
|
+
const body = { publicKey };
|
|
239
|
+
if (options.ttl) {
|
|
240
|
+
body.ttlSeconds = parseTtl(options.ttl);
|
|
241
|
+
}
|
|
242
|
+
const result = await client.post(`/v1/ssh/sign${query}`, body);
|
|
243
|
+
spinner.succeed('Certificate signed successfully');
|
|
244
|
+
// Write certificate to file if requested
|
|
245
|
+
if (options.output) {
|
|
246
|
+
const outputPath = path.resolve(options.output);
|
|
247
|
+
fs.writeFileSync(outputPath, result.certificate + '\n');
|
|
248
|
+
output.success(`Certificate written to ${outputPath}`);
|
|
249
|
+
}
|
|
250
|
+
if (options.json) {
|
|
251
|
+
output.json(result);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
output.section('Certificate Details');
|
|
255
|
+
output.keyValue({
|
|
256
|
+
'Serial': result.serial,
|
|
257
|
+
'Fingerprint': result.fingerprint,
|
|
258
|
+
'Principals': result.principals.join(', '),
|
|
259
|
+
'Valid From': output.formatDate(result.validAfter),
|
|
260
|
+
'Valid Until': output.formatDate(result.validBefore),
|
|
261
|
+
});
|
|
262
|
+
if (!options.output) {
|
|
263
|
+
output.section('Certificate');
|
|
264
|
+
console.log(result.certificate);
|
|
265
|
+
console.log();
|
|
266
|
+
output.info('Save this certificate alongside your private key (e.g., id_ed25519-cert.pub)');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
spinner.fail('Failed to sign certificate');
|
|
271
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
// List Certificates
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
cert
|
|
279
|
+
.command('list')
|
|
280
|
+
.description('List issued certificates')
|
|
281
|
+
.option('--tenant <id>', 'Tenant ID (superadmin only)')
|
|
282
|
+
.option('--limit <n>', 'Maximum number of results', '50')
|
|
283
|
+
.option('--offset <n>', 'Offset for pagination', '0')
|
|
284
|
+
.option('--active-only', 'Show only non-expired certificates')
|
|
285
|
+
.option('--revoked', 'Show only revoked certificates')
|
|
286
|
+
.option('--user-id <id>', 'Filter by user ID')
|
|
287
|
+
.option('--json', 'Output as JSON')
|
|
288
|
+
.action(async (options) => {
|
|
289
|
+
const spinner = ora('Fetching certificates...').start();
|
|
290
|
+
try {
|
|
291
|
+
const params = new URLSearchParams();
|
|
292
|
+
if (options.tenant)
|
|
293
|
+
params.set('tenantId', options.tenant);
|
|
294
|
+
if (options.limit)
|
|
295
|
+
params.set('limit', options.limit);
|
|
296
|
+
if (options.offset)
|
|
297
|
+
params.set('offset', options.offset);
|
|
298
|
+
if (options.activeOnly)
|
|
299
|
+
params.set('activeOnly', 'true');
|
|
300
|
+
if (options.revoked !== undefined)
|
|
301
|
+
params.set('revoked', String(options.revoked));
|
|
302
|
+
if (options.userId)
|
|
303
|
+
params.set('userId', options.userId);
|
|
304
|
+
const queryString = params.toString();
|
|
305
|
+
const response = await client.get(`/v1/ssh/certificates${queryString ? `?${queryString}` : ''}`);
|
|
306
|
+
spinner.stop();
|
|
307
|
+
if (options.json) {
|
|
308
|
+
output.json(response);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (response.items.length === 0) {
|
|
312
|
+
output.info('No certificates found');
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
output.table(['Serial', 'User', 'Principals', 'Valid Until', 'Status'], response.items.map(cert => [
|
|
316
|
+
cert.serial.substring(0, 16) + (cert.serial.length > 16 ? '...' : ''),
|
|
317
|
+
cert.username ?? cert.userId.substring(0, 8),
|
|
318
|
+
cert.principals.slice(0, 3).join(', ') + (cert.principals.length > 3 ? '...' : ''),
|
|
319
|
+
output.formatDate(cert.validBefore),
|
|
320
|
+
cert.revoked
|
|
321
|
+
? '✗ Revoked'
|
|
322
|
+
: isExpired(cert.validBefore)
|
|
323
|
+
? '○ Expired'
|
|
324
|
+
: '✓ Active',
|
|
325
|
+
]));
|
|
326
|
+
output.info(`Total: ${response.pagination.total} certificate(s)`);
|
|
327
|
+
if (response.pagination.hasMore) {
|
|
328
|
+
output.info(`Use --offset to see more results`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
spinner.fail('Failed to list certificates');
|
|
333
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
// Get Certificate
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
cert
|
|
341
|
+
.command('get <id>')
|
|
342
|
+
.description('Get certificate details')
|
|
343
|
+
.option('--tenant <id>', 'Tenant ID (superadmin only)')
|
|
344
|
+
.option('--json', 'Output as JSON')
|
|
345
|
+
.action(async (id, options) => {
|
|
346
|
+
const spinner = ora('Fetching certificate...').start();
|
|
347
|
+
try {
|
|
348
|
+
const query = options.tenant ? `?tenantId=${encodeURIComponent(options.tenant)}` : '';
|
|
349
|
+
const cert = await client.get(`/v1/ssh/certificates/${encodeURIComponent(id)}${query}`);
|
|
350
|
+
spinner.stop();
|
|
351
|
+
if (options.json) {
|
|
352
|
+
output.json(cert);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
output.section('Certificate Details');
|
|
356
|
+
output.keyValue({
|
|
357
|
+
'ID': cert.id,
|
|
358
|
+
'Serial': cert.serial,
|
|
359
|
+
'User ID': cert.userId,
|
|
360
|
+
'Fingerprint': cert.fingerprint,
|
|
361
|
+
'Principals': cert.principals.join(', '),
|
|
362
|
+
'Extensions': cert.extensions?.join(', ') ?? '-',
|
|
363
|
+
'Valid From': output.formatDate(cert.validAfter),
|
|
364
|
+
'Valid Until': output.formatDate(cert.validBefore),
|
|
365
|
+
'Status': cert.revoked
|
|
366
|
+
? '✗ Revoked'
|
|
367
|
+
: isExpired(cert.validBefore)
|
|
368
|
+
? '○ Expired'
|
|
369
|
+
: '✓ Active',
|
|
370
|
+
'Request IP': cert.requestIp ?? '-',
|
|
371
|
+
'Created': output.formatDate(cert.createdAt),
|
|
372
|
+
});
|
|
373
|
+
if (cert.revoked) {
|
|
374
|
+
output.section('Revocation');
|
|
375
|
+
output.keyValue({
|
|
376
|
+
'Revoked At': cert.revokedAt ? output.formatDate(cert.revokedAt) : '-',
|
|
377
|
+
'Revoked By': cert.revokedBy ?? '-',
|
|
378
|
+
'Reason': cert.revocationReason ?? '-',
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
spinner.fail('Failed to get certificate');
|
|
384
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
385
|
+
process.exit(1);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
// Revoke Certificate
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
cert
|
|
392
|
+
.command('revoke <id>')
|
|
393
|
+
.description('Revoke a certificate')
|
|
394
|
+
.option('--tenant <id>', 'Tenant ID (superadmin only)')
|
|
395
|
+
.option('--reason <reason>', 'Revocation reason')
|
|
396
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
397
|
+
.action(async (id, options) => {
|
|
398
|
+
try {
|
|
399
|
+
const query = options.tenant ? `?tenantId=${encodeURIComponent(options.tenant)}` : '';
|
|
400
|
+
if (!options.yes) {
|
|
401
|
+
const confirmed = await promptConfirm(`Revoke certificate ${id}?`);
|
|
402
|
+
if (!confirmed) {
|
|
403
|
+
output.info('Revoke cancelled');
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const spinner = ora('Revoking certificate...').start();
|
|
408
|
+
try {
|
|
409
|
+
await client.post(`/v1/ssh/certificates/${encodeURIComponent(id)}/revoke${query}`, {
|
|
410
|
+
reason: options.reason,
|
|
411
|
+
});
|
|
412
|
+
spinner.succeed('Certificate revoked successfully');
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
spinner.fail('Failed to revoke certificate');
|
|
416
|
+
throw err;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
catch (err) {
|
|
420
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
// ===========================================================================
|
|
425
|
+
// Principal Mappings
|
|
426
|
+
// ===========================================================================
|
|
427
|
+
const mapping = ssh
|
|
428
|
+
.command('mapping')
|
|
429
|
+
.description('SSH principal mapping management (SSO groups → SSH principals)');
|
|
430
|
+
// ---------------------------------------------------------------------------
|
|
431
|
+
// List Mappings
|
|
432
|
+
// ---------------------------------------------------------------------------
|
|
433
|
+
mapping
|
|
434
|
+
.command('list')
|
|
435
|
+
.description('List principal mappings')
|
|
436
|
+
.option('--tenant <id>', 'Tenant ID (superadmin only)')
|
|
437
|
+
.option('--json', 'Output as JSON')
|
|
438
|
+
.action(async (options) => {
|
|
439
|
+
const spinner = ora('Fetching mappings...').start();
|
|
440
|
+
try {
|
|
441
|
+
const query = options.tenant ? `?tenantId=${encodeURIComponent(options.tenant)}` : '';
|
|
442
|
+
const response = await client.get(`/v1/ssh/principal-mappings${query}`);
|
|
443
|
+
spinner.stop();
|
|
444
|
+
if (options.json) {
|
|
445
|
+
output.json(response.items);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (response.items.length === 0) {
|
|
449
|
+
output.info('No principal mappings found');
|
|
450
|
+
output.info('Use "znvault ssh mapping create" to create a mapping');
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
output.table(['ID', 'Group', 'Principals', 'Created'], response.items.map(m => [
|
|
454
|
+
m.id.substring(0, 8) + '...',
|
|
455
|
+
m.groupDisplayName ?? m.groupName ?? m.groupId.substring(0, 8),
|
|
456
|
+
m.principals.join(', '),
|
|
457
|
+
output.formatDate(m.createdAt),
|
|
458
|
+
]));
|
|
459
|
+
output.info(`Total: ${response.items.length} mapping(s)`);
|
|
460
|
+
}
|
|
461
|
+
catch (err) {
|
|
462
|
+
spinner.fail('Failed to list mappings');
|
|
463
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
464
|
+
process.exit(1);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
// ---------------------------------------------------------------------------
|
|
468
|
+
// Create Mapping
|
|
469
|
+
// ---------------------------------------------------------------------------
|
|
470
|
+
mapping
|
|
471
|
+
.command('create <groupId> <principals...>')
|
|
472
|
+
.description('Create principal mapping (SSO group → SSH principals)')
|
|
473
|
+
.option('--tenant <id>', 'Tenant ID (superadmin only)')
|
|
474
|
+
.option('--json', 'Output as JSON')
|
|
475
|
+
.action(async (groupId, principals, options) => {
|
|
476
|
+
const spinner = ora('Creating mapping...').start();
|
|
477
|
+
try {
|
|
478
|
+
const query = options.tenant ? `?tenantId=${encodeURIComponent(options.tenant)}` : '';
|
|
479
|
+
const mapping = await client.post(`/v1/ssh/principal-mappings${query}`, {
|
|
480
|
+
groupId,
|
|
481
|
+
principals,
|
|
482
|
+
});
|
|
483
|
+
spinner.succeed('Mapping created successfully');
|
|
484
|
+
if (options.json) {
|
|
485
|
+
output.json(mapping);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
output.keyValue({
|
|
489
|
+
'ID': mapping.id,
|
|
490
|
+
'Group ID': mapping.groupId,
|
|
491
|
+
'Principals': mapping.principals.join(', '),
|
|
492
|
+
'Created': output.formatDate(mapping.createdAt),
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
catch (err) {
|
|
496
|
+
spinner.fail('Failed to create mapping');
|
|
497
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
// Update Mapping
|
|
503
|
+
// ---------------------------------------------------------------------------
|
|
504
|
+
mapping
|
|
505
|
+
.command('update <mappingId> <principals...>')
|
|
506
|
+
.description('Update principal mapping')
|
|
507
|
+
.option('--tenant <id>', 'Tenant ID (superadmin only)')
|
|
508
|
+
.action(async (mappingId, principals, options) => {
|
|
509
|
+
const spinner = ora('Updating mapping...').start();
|
|
510
|
+
try {
|
|
511
|
+
const query = options.tenant ? `?tenantId=${encodeURIComponent(options.tenant)}` : '';
|
|
512
|
+
await client.put(`/v1/ssh/principal-mappings/${encodeURIComponent(mappingId)}${query}`, {
|
|
513
|
+
principals,
|
|
514
|
+
});
|
|
515
|
+
spinner.succeed('Mapping updated successfully');
|
|
516
|
+
}
|
|
517
|
+
catch (err) {
|
|
518
|
+
spinner.fail('Failed to update mapping');
|
|
519
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
// ---------------------------------------------------------------------------
|
|
524
|
+
// Delete Mapping
|
|
525
|
+
// ---------------------------------------------------------------------------
|
|
526
|
+
mapping
|
|
527
|
+
.command('delete <mappingId>')
|
|
528
|
+
.description('Delete principal mapping')
|
|
529
|
+
.option('--tenant <id>', 'Tenant ID (superadmin only)')
|
|
530
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
531
|
+
.action(async (mappingId, options) => {
|
|
532
|
+
try {
|
|
533
|
+
const query = options.tenant ? `?tenantId=${encodeURIComponent(options.tenant)}` : '';
|
|
534
|
+
if (!options.yes) {
|
|
535
|
+
const confirmed = await promptConfirm('Delete this mapping?');
|
|
536
|
+
if (!confirmed) {
|
|
537
|
+
output.info('Delete cancelled');
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const spinner = ora('Deleting mapping...').start();
|
|
542
|
+
try {
|
|
543
|
+
await client.delete(`/v1/ssh/principal-mappings/${encodeURIComponent(mappingId)}${query}`);
|
|
544
|
+
spinner.succeed('Mapping deleted successfully');
|
|
545
|
+
}
|
|
546
|
+
catch (err) {
|
|
547
|
+
spinner.fail('Failed to delete mapping');
|
|
548
|
+
throw err;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
catch (err) {
|
|
552
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
// ===========================================================================
|
|
557
|
+
// Server Groups
|
|
558
|
+
// ===========================================================================
|
|
559
|
+
const group = ssh
|
|
560
|
+
.command('server-group')
|
|
561
|
+
.description('SSH server group management');
|
|
562
|
+
// ---------------------------------------------------------------------------
|
|
563
|
+
// List Server Groups
|
|
564
|
+
// ---------------------------------------------------------------------------
|
|
565
|
+
group
|
|
566
|
+
.command('list')
|
|
567
|
+
.description('List server groups')
|
|
568
|
+
.option('--tenant <id>', 'Tenant ID (superadmin only)')
|
|
569
|
+
.option('--json', 'Output as JSON')
|
|
570
|
+
.action(async (options) => {
|
|
571
|
+
const spinner = ora('Fetching server groups...').start();
|
|
572
|
+
try {
|
|
573
|
+
const query = options.tenant ? `?tenantId=${encodeURIComponent(options.tenant)}` : '';
|
|
574
|
+
const response = await client.get(`/v1/ssh/server-groups${query}`);
|
|
575
|
+
spinner.stop();
|
|
576
|
+
if (options.json) {
|
|
577
|
+
output.json(response.items);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (response.items.length === 0) {
|
|
581
|
+
output.info('No server groups found');
|
|
582
|
+
output.info('Use "znvault ssh server-group create" to create a group');
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
output.table(['ID', 'Name', 'Description', 'Created'], response.items.map(g => [
|
|
586
|
+
g.id.substring(0, 8) + '...',
|
|
587
|
+
g.name,
|
|
588
|
+
(g.description ?? '-').substring(0, 30),
|
|
589
|
+
output.formatDate(g.createdAt),
|
|
590
|
+
]));
|
|
591
|
+
output.info(`Total: ${response.items.length} server group(s)`);
|
|
592
|
+
}
|
|
593
|
+
catch (err) {
|
|
594
|
+
spinner.fail('Failed to list server groups');
|
|
595
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
596
|
+
process.exit(1);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
// ---------------------------------------------------------------------------
|
|
600
|
+
// Create Server Group
|
|
601
|
+
// ---------------------------------------------------------------------------
|
|
602
|
+
group
|
|
603
|
+
.command('create <name>')
|
|
604
|
+
.description('Create server group')
|
|
605
|
+
.option('--tenant <id>', 'Tenant ID (superadmin only)')
|
|
606
|
+
.option('-d, --description <text>', 'Group description')
|
|
607
|
+
.option('--json', 'Output as JSON')
|
|
608
|
+
.action(async (name, options) => {
|
|
609
|
+
const spinner = ora('Creating server group...').start();
|
|
610
|
+
try {
|
|
611
|
+
const query = options.tenant ? `?tenantId=${encodeURIComponent(options.tenant)}` : '';
|
|
612
|
+
const group = await client.post(`/v1/ssh/server-groups${query}`, {
|
|
613
|
+
name,
|
|
614
|
+
description: options.description,
|
|
615
|
+
});
|
|
616
|
+
spinner.succeed('Server group created successfully');
|
|
617
|
+
if (options.json) {
|
|
618
|
+
output.json(group);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
output.keyValue({
|
|
622
|
+
'ID': group.id,
|
|
623
|
+
'Name': group.name,
|
|
624
|
+
'Description': group.description ?? '-',
|
|
625
|
+
'Created': output.formatDate(group.createdAt),
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
catch (err) {
|
|
629
|
+
spinner.fail('Failed to create server group');
|
|
630
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
631
|
+
process.exit(1);
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
// ---------------------------------------------------------------------------
|
|
635
|
+
// Get Server Group
|
|
636
|
+
// ---------------------------------------------------------------------------
|
|
637
|
+
group
|
|
638
|
+
.command('get <id>')
|
|
639
|
+
.description('Get server group details')
|
|
640
|
+
.option('--tenant <id>', 'Tenant ID (superadmin only)')
|
|
641
|
+
.option('--json', 'Output as JSON')
|
|
642
|
+
.action(async (id, options) => {
|
|
643
|
+
const spinner = ora('Fetching server group...').start();
|
|
644
|
+
try {
|
|
645
|
+
const query = options.tenant ? `?tenantId=${encodeURIComponent(options.tenant)}` : '';
|
|
646
|
+
const group = await client.get(`/v1/ssh/server-groups/${encodeURIComponent(id)}${query}`);
|
|
647
|
+
spinner.stop();
|
|
648
|
+
if (options.json) {
|
|
649
|
+
output.json(group);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
output.section('Server Group');
|
|
653
|
+
output.keyValue({
|
|
654
|
+
'ID': group.id,
|
|
655
|
+
'Name': group.name,
|
|
656
|
+
'Description': group.description ?? '-',
|
|
657
|
+
'Created': output.formatDate(group.createdAt),
|
|
658
|
+
});
|
|
659
|
+
if (group.accessRules && group.accessRules.length > 0) {
|
|
660
|
+
output.section('Access Rules');
|
|
661
|
+
output.table(['Linux User', 'Allowed Principals'], group.accessRules.map(r => [
|
|
662
|
+
r.linuxUser,
|
|
663
|
+
r.allowedPrincipals.join(', '),
|
|
664
|
+
]));
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
output.section('Access Rules');
|
|
668
|
+
output.info('No access rules defined');
|
|
669
|
+
output.info('Use "znvault ssh server-group set-access" to add rules');
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
catch (err) {
|
|
673
|
+
spinner.fail('Failed to get server group');
|
|
674
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
675
|
+
process.exit(1);
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
// ---------------------------------------------------------------------------
|
|
679
|
+
// Delete Server Group
|
|
680
|
+
// ---------------------------------------------------------------------------
|
|
681
|
+
group
|
|
682
|
+
.command('delete <id>')
|
|
683
|
+
.description('Delete server group')
|
|
684
|
+
.option('--tenant <id>', 'Tenant ID (superadmin only)')
|
|
685
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
686
|
+
.action(async (id, options) => {
|
|
687
|
+
try {
|
|
688
|
+
const query = options.tenant ? `?tenantId=${encodeURIComponent(options.tenant)}` : '';
|
|
689
|
+
if (!options.yes) {
|
|
690
|
+
const confirmed = await promptConfirm('Delete this server group?');
|
|
691
|
+
if (!confirmed) {
|
|
692
|
+
output.info('Delete cancelled');
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const spinner = ora('Deleting server group...').start();
|
|
697
|
+
try {
|
|
698
|
+
await client.delete(`/v1/ssh/server-groups/${encodeURIComponent(id)}${query}`);
|
|
699
|
+
spinner.succeed('Server group deleted successfully');
|
|
700
|
+
}
|
|
701
|
+
catch (err) {
|
|
702
|
+
spinner.fail('Failed to delete server group');
|
|
703
|
+
throw err;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
catch (err) {
|
|
707
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
708
|
+
process.exit(1);
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
// ---------------------------------------------------------------------------
|
|
712
|
+
// Set Access Rule
|
|
713
|
+
// ---------------------------------------------------------------------------
|
|
714
|
+
group
|
|
715
|
+
.command('set-access <groupId> <linuxUser> <principals...>')
|
|
716
|
+
.description('Set access rule for server group (which principals can access which Linux user)')
|
|
717
|
+
.option('--tenant <id>', 'Tenant ID (superadmin only)')
|
|
718
|
+
.option('--json', 'Output as JSON')
|
|
719
|
+
.action(async (groupId, linuxUser, principals, options) => {
|
|
720
|
+
const spinner = ora('Setting access rule...').start();
|
|
721
|
+
try {
|
|
722
|
+
const query = options.tenant ? `?tenantId=${encodeURIComponent(options.tenant)}` : '';
|
|
723
|
+
const access = await client.put(`/v1/ssh/server-groups/${encodeURIComponent(groupId)}/access${query}`, { linuxUser, allowedPrincipals: principals });
|
|
724
|
+
spinner.succeed('Access rule set successfully');
|
|
725
|
+
if (options.json) {
|
|
726
|
+
output.json(access);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
output.keyValue({
|
|
730
|
+
'Linux User': access.linuxUser,
|
|
731
|
+
'Allowed Principals': access.allowedPrincipals.join(', '),
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
catch (err) {
|
|
735
|
+
spinner.fail('Failed to set access rule');
|
|
736
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
737
|
+
process.exit(1);
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
// ---------------------------------------------------------------------------
|
|
741
|
+
// Delete Access Rule
|
|
742
|
+
// ---------------------------------------------------------------------------
|
|
743
|
+
group
|
|
744
|
+
.command('delete-access <groupId> <linuxUser>')
|
|
745
|
+
.description('Delete access rule from server group')
|
|
746
|
+
.option('--tenant <id>', 'Tenant ID (superadmin only)')
|
|
747
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
748
|
+
.action(async (groupId, linuxUser, options) => {
|
|
749
|
+
try {
|
|
750
|
+
const query = options.tenant ? `?tenantId=${encodeURIComponent(options.tenant)}` : '';
|
|
751
|
+
if (!options.yes) {
|
|
752
|
+
const confirmed = await promptConfirm(`Delete access rule for Linux user "${linuxUser}"?`);
|
|
753
|
+
if (!confirmed) {
|
|
754
|
+
output.info('Delete cancelled');
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const spinner = ora('Deleting access rule...').start();
|
|
759
|
+
try {
|
|
760
|
+
await client.delete(`/v1/ssh/server-groups/${encodeURIComponent(groupId)}/access/${encodeURIComponent(linuxUser)}${query}`);
|
|
761
|
+
spinner.succeed('Access rule deleted successfully');
|
|
762
|
+
}
|
|
763
|
+
catch (err) {
|
|
764
|
+
spinner.fail('Failed to delete access rule');
|
|
765
|
+
throw err;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
catch (err) {
|
|
769
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
770
|
+
process.exit(1);
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
// ---------------------------------------------------------------------------
|
|
774
|
+
// Get Authorized Principals
|
|
775
|
+
// ---------------------------------------------------------------------------
|
|
776
|
+
group
|
|
777
|
+
.command('authorized-principals <groupId>')
|
|
778
|
+
.description('Get AuthorizedPrincipalsFile content for a server group')
|
|
779
|
+
.option('--tenant <id>', 'Tenant ID (superadmin only)')
|
|
780
|
+
.option('--output <file>', 'Output to file')
|
|
781
|
+
.action(async (groupId, options) => {
|
|
782
|
+
const spinner = ora('Generating authorized principals...').start();
|
|
783
|
+
try {
|
|
784
|
+
const query = options.tenant ? `?tenantId=${encodeURIComponent(options.tenant)}` : '';
|
|
785
|
+
const response = await client.get(`/v1/ssh/server-groups/${encodeURIComponent(groupId)}/authorized-principals${query}`);
|
|
786
|
+
spinner.stop();
|
|
787
|
+
// Format as AuthorizedPrincipalsFile content
|
|
788
|
+
const lines = [];
|
|
789
|
+
for (const [linuxUser, principals] of Object.entries(response)) {
|
|
790
|
+
lines.push(`# Linux user: ${linuxUser}`);
|
|
791
|
+
for (const principal of principals) {
|
|
792
|
+
lines.push(principal);
|
|
793
|
+
}
|
|
794
|
+
lines.push('');
|
|
795
|
+
}
|
|
796
|
+
const content = lines.join('\n');
|
|
797
|
+
if (options.output) {
|
|
798
|
+
const fs = await import('fs');
|
|
799
|
+
const path = await import('path');
|
|
800
|
+
fs.writeFileSync(path.resolve(options.output), content);
|
|
801
|
+
output.success(`Written to ${options.output}`);
|
|
802
|
+
}
|
|
803
|
+
else {
|
|
804
|
+
console.log(content);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
catch (err) {
|
|
808
|
+
spinner.fail('Failed to get authorized principals');
|
|
809
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
810
|
+
process.exit(1);
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
//# sourceMappingURL=ssh.js.map
|