@lanonasis/cli 1.2.0 → 1.2.1
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 +20 -4
- package/dist/commands/api-keys.d.ts +3 -0
- package/dist/commands/api-keys.js +812 -0
- package/dist/commands/mcp.js +2 -2
- package/dist/commands/memory.js +22 -20
- package/dist/index-simple.js +522 -189
- package/dist/index.js +320 -23
- package/dist/utils/api.d.ts +12 -2
- package/dist/utils/api.js +17 -0
- package/dist/utils/formatting.d.ts +2 -0
- package/dist/utils/formatting.js +13 -0
- package/dist/utils/mcp-client.js +13 -7
- package/package.json +8 -3
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import Table from 'cli-table3';
|
|
5
|
+
import { apiClient } from '../utils/api.js';
|
|
6
|
+
import { formatDate, truncateText } from '../utils/formatting.js';
|
|
7
|
+
// Enhanced VPS-style color scheme
|
|
8
|
+
const colors = {
|
|
9
|
+
primary: chalk.blue.bold,
|
|
10
|
+
success: chalk.green,
|
|
11
|
+
warning: chalk.yellow,
|
|
12
|
+
error: chalk.red,
|
|
13
|
+
info: chalk.cyan,
|
|
14
|
+
accent: chalk.magenta,
|
|
15
|
+
muted: chalk.gray,
|
|
16
|
+
highlight: chalk.white.bold
|
|
17
|
+
};
|
|
18
|
+
const apiKeysCommand = new Command('api-keys')
|
|
19
|
+
.alias('keys')
|
|
20
|
+
.description(colors.info('🔐 Manage API keys securely with enterprise-grade encryption'));
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// PROJECT COMMANDS
|
|
23
|
+
// ============================================================================
|
|
24
|
+
const projectsCommand = new Command('projects')
|
|
25
|
+
.description(colors.accent('📁 Manage API key projects and organization'));
|
|
26
|
+
projectsCommand
|
|
27
|
+
.command('create')
|
|
28
|
+
.description('Create a new API key project')
|
|
29
|
+
.option('-n, --name <name>', 'Project name')
|
|
30
|
+
.option('-d, --description [description]', 'Project description')
|
|
31
|
+
.option('-o, --organization-id <id>', 'Organization ID')
|
|
32
|
+
.option('--interactive', 'Interactive mode')
|
|
33
|
+
.action(async (options) => {
|
|
34
|
+
try {
|
|
35
|
+
let projectData = {
|
|
36
|
+
name: options.name,
|
|
37
|
+
description: options.description,
|
|
38
|
+
organizationId: options.organizationId
|
|
39
|
+
};
|
|
40
|
+
if (options.interactive || !projectData.name || !projectData.organizationId) {
|
|
41
|
+
const answers = await inquirer.prompt([
|
|
42
|
+
{
|
|
43
|
+
type: 'input',
|
|
44
|
+
name: 'name',
|
|
45
|
+
message: 'Project name:',
|
|
46
|
+
when: !projectData.name,
|
|
47
|
+
validate: (input) => input.length > 0 || 'Project name is required'
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: 'input',
|
|
51
|
+
name: 'description',
|
|
52
|
+
message: 'Project description (optional):',
|
|
53
|
+
when: !projectData.description
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
type: 'input',
|
|
57
|
+
name: 'organizationId',
|
|
58
|
+
message: 'Organization ID:',
|
|
59
|
+
when: !projectData.organizationId,
|
|
60
|
+
validate: (input) => {
|
|
61
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
62
|
+
return uuidRegex.test(input) || 'Please enter a valid UUID';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
]);
|
|
66
|
+
projectData = { ...projectData, ...answers };
|
|
67
|
+
}
|
|
68
|
+
const project = await apiClient.post('/api-keys/projects', projectData);
|
|
69
|
+
console.log(chalk.green('✅ Project created successfully!'));
|
|
70
|
+
console.log(chalk.blue(`Project ID: ${project.id}`));
|
|
71
|
+
console.log(chalk.blue(`Name: ${project.name}`));
|
|
72
|
+
if (project.description) {
|
|
73
|
+
console.log(chalk.blue(`Description: ${project.description}`));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
console.error(chalk.red('❌ Failed to create project:'), error.message);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
projectsCommand
|
|
82
|
+
.command('list')
|
|
83
|
+
.alias('ls')
|
|
84
|
+
.description('List all API key projects')
|
|
85
|
+
.option('--json', 'Output as JSON')
|
|
86
|
+
.action(async (options) => {
|
|
87
|
+
try {
|
|
88
|
+
const projects = await apiClient.get('/api-keys/projects');
|
|
89
|
+
if (options.json) {
|
|
90
|
+
console.log(JSON.stringify(projects, null, 2));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (projects.length === 0) {
|
|
94
|
+
console.log(chalk.yellow('No projects found'));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const table = new Table({
|
|
98
|
+
head: ['ID', 'Name', 'Description', 'Owner', 'Created'].map(h => chalk.cyan(h)),
|
|
99
|
+
style: { head: [], border: [] }
|
|
100
|
+
});
|
|
101
|
+
projects.forEach((project) => {
|
|
102
|
+
table.push([
|
|
103
|
+
truncateText(project.id, 20),
|
|
104
|
+
project.name,
|
|
105
|
+
truncateText(project.description || '-', 30),
|
|
106
|
+
truncateText(project.ownerId, 20),
|
|
107
|
+
formatDate(project.createdAt)
|
|
108
|
+
]);
|
|
109
|
+
});
|
|
110
|
+
console.log(table.toString());
|
|
111
|
+
console.log(chalk.gray(`Total: ${projects.length} projects`));
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
console.error(chalk.red('❌ Failed to list projects:'), error.message);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// API KEY COMMANDS
|
|
120
|
+
// ============================================================================
|
|
121
|
+
apiKeysCommand
|
|
122
|
+
.command('create')
|
|
123
|
+
.description('Create a new API key')
|
|
124
|
+
.option('-n, --name <name>', 'API key name')
|
|
125
|
+
.option('-v, --value <value>', 'API key value')
|
|
126
|
+
.option('-t, --type <type>', 'Key type (api_key, database_url, oauth_token, etc.)')
|
|
127
|
+
.option('-e, --environment <env>', 'Environment (development, staging, production)')
|
|
128
|
+
.option('-p, --project-id <id>', 'Project ID')
|
|
129
|
+
.option('--access-level <level>', 'Access level (public, authenticated, team, admin, enterprise)')
|
|
130
|
+
.option('--tags <tags>', 'Comma-separated tags')
|
|
131
|
+
.option('--expires-at <date>', 'Expiration date (ISO format)')
|
|
132
|
+
.option('--rotation-frequency <days>', 'Rotation frequency in days', '90')
|
|
133
|
+
.option('--interactive', 'Interactive mode')
|
|
134
|
+
.action(async (options) => {
|
|
135
|
+
try {
|
|
136
|
+
let keyData = {
|
|
137
|
+
name: options.name,
|
|
138
|
+
value: options.value,
|
|
139
|
+
keyType: options.type,
|
|
140
|
+
environment: options.environment || 'development',
|
|
141
|
+
projectId: options.projectId,
|
|
142
|
+
accessLevel: options.accessLevel || 'team',
|
|
143
|
+
tags: options.tags ? options.tags.split(',').map((tag) => tag.trim()) : [],
|
|
144
|
+
expiresAt: options.expiresAt,
|
|
145
|
+
rotationFrequency: parseInt(options.rotationFrequency)
|
|
146
|
+
};
|
|
147
|
+
if (options.interactive || !keyData.name || !keyData.value || !keyData.projectId) {
|
|
148
|
+
const projects = await apiClient.get('/api-keys/projects');
|
|
149
|
+
const answers = await inquirer.prompt([
|
|
150
|
+
{
|
|
151
|
+
type: 'input',
|
|
152
|
+
name: 'name',
|
|
153
|
+
message: 'API key name:',
|
|
154
|
+
when: !keyData.name,
|
|
155
|
+
validate: (input) => input.length > 0 || 'Name is required'
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
type: 'password',
|
|
159
|
+
name: 'value',
|
|
160
|
+
message: 'API key value:',
|
|
161
|
+
when: !keyData.value,
|
|
162
|
+
validate: (input) => input.length > 0 || 'Value is required'
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
type: 'list',
|
|
166
|
+
name: 'keyType',
|
|
167
|
+
message: 'Key type:',
|
|
168
|
+
when: !keyData.keyType,
|
|
169
|
+
choices: [
|
|
170
|
+
'api_key',
|
|
171
|
+
'database_url',
|
|
172
|
+
'oauth_token',
|
|
173
|
+
'certificate',
|
|
174
|
+
'ssh_key',
|
|
175
|
+
'webhook_secret',
|
|
176
|
+
'encryption_key'
|
|
177
|
+
]
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
type: 'list',
|
|
181
|
+
name: 'environment',
|
|
182
|
+
message: 'Environment:',
|
|
183
|
+
choices: ['development', 'staging', 'production'],
|
|
184
|
+
default: 'development'
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
type: 'list',
|
|
188
|
+
name: 'projectId',
|
|
189
|
+
message: 'Select project:',
|
|
190
|
+
when: !keyData.projectId && projects.length > 0,
|
|
191
|
+
choices: projects.map((p) => ({ name: `${p.name} (${p.id})`, value: p.id }))
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
type: 'list',
|
|
195
|
+
name: 'accessLevel',
|
|
196
|
+
message: 'Access level:',
|
|
197
|
+
choices: ['public', 'authenticated', 'team', 'admin', 'enterprise'],
|
|
198
|
+
default: 'team'
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
type: 'input',
|
|
202
|
+
name: 'tags',
|
|
203
|
+
message: 'Tags (comma-separated, optional):',
|
|
204
|
+
filter: (input) => input ? input.split(',').map((tag) => tag.trim()) : []
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
type: 'input',
|
|
208
|
+
name: 'expiresAt',
|
|
209
|
+
message: 'Expiration date (YYYY-MM-DD, optional):',
|
|
210
|
+
validate: (input) => {
|
|
211
|
+
if (!input)
|
|
212
|
+
return true;
|
|
213
|
+
const date = new Date(input);
|
|
214
|
+
return !isNaN(date.getTime()) || 'Please enter a valid date';
|
|
215
|
+
},
|
|
216
|
+
filter: (input) => input ? new Date(input).toISOString() : undefined
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
type: 'number',
|
|
220
|
+
name: 'rotationFrequency',
|
|
221
|
+
message: 'Rotation frequency (days):',
|
|
222
|
+
default: 90,
|
|
223
|
+
validate: (input) => input > 0 && input <= 365 || 'Must be between 1 and 365 days'
|
|
224
|
+
}
|
|
225
|
+
]);
|
|
226
|
+
keyData = { ...keyData, ...answers };
|
|
227
|
+
}
|
|
228
|
+
const apiKey = await apiClient.post('/api-keys', keyData);
|
|
229
|
+
console.log(colors.success('🔐 API key created successfully!'));
|
|
230
|
+
console.log(colors.info('━'.repeat(50)));
|
|
231
|
+
console.log(`${colors.highlight('Key ID:')} ${colors.primary(apiKey.id)}`);
|
|
232
|
+
console.log(`${colors.highlight('Name:')} ${colors.accent(apiKey.name)}`);
|
|
233
|
+
console.log(`${colors.highlight('Type:')} ${colors.info(apiKey.keyType)}`);
|
|
234
|
+
console.log(`${colors.highlight('Environment:')} ${colors.accent(apiKey.environment)}`);
|
|
235
|
+
console.log(colors.info('━'.repeat(50)));
|
|
236
|
+
console.log(colors.warning('⚠️ The key value is securely encrypted and cannot be retrieved later.'));
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
console.error(colors.error('✖ Failed to create API key:'), colors.muted(error.message));
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
apiKeysCommand
|
|
244
|
+
.command('list')
|
|
245
|
+
.alias('ls')
|
|
246
|
+
.description('List API keys')
|
|
247
|
+
.option('-p, --project-id <id>', 'Filter by project ID')
|
|
248
|
+
.option('--json', 'Output as JSON')
|
|
249
|
+
.action(async (options) => {
|
|
250
|
+
try {
|
|
251
|
+
let url = '/api-keys';
|
|
252
|
+
if (options.projectId) {
|
|
253
|
+
url += `?projectId=${options.projectId}`;
|
|
254
|
+
}
|
|
255
|
+
const apiKeys = await apiClient.get(url);
|
|
256
|
+
if (options.json) {
|
|
257
|
+
console.log(JSON.stringify(apiKeys, null, 2));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (apiKeys.length === 0) {
|
|
261
|
+
console.log(colors.warning('⚠️ No API keys found'));
|
|
262
|
+
console.log(colors.muted('Run: lanonasis api-keys create'));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
console.log(colors.primary('🔐 API Key Management'));
|
|
266
|
+
console.log(colors.info('═'.repeat(80)));
|
|
267
|
+
const table = new Table({
|
|
268
|
+
head: ['ID', 'Name', 'Type', 'Environment', 'Status', 'Usage', 'Last Rotated'].map(h => colors.accent(h)),
|
|
269
|
+
style: { head: [], border: [] }
|
|
270
|
+
});
|
|
271
|
+
apiKeys.forEach((key) => {
|
|
272
|
+
const statusColor = key.status === 'active' ? colors.success :
|
|
273
|
+
key.status === 'rotating' ? colors.warning : colors.error;
|
|
274
|
+
table.push([
|
|
275
|
+
truncateText(key.id, 20),
|
|
276
|
+
key.name,
|
|
277
|
+
key.keyType,
|
|
278
|
+
key.environment,
|
|
279
|
+
statusColor(key.status),
|
|
280
|
+
colors.highlight(key.usageCount.toString()),
|
|
281
|
+
formatDate(key.lastRotated)
|
|
282
|
+
]);
|
|
283
|
+
});
|
|
284
|
+
console.log(table.toString());
|
|
285
|
+
console.log(colors.info('═'.repeat(80)));
|
|
286
|
+
console.log(colors.muted(`🔢 Total: ${colors.highlight(apiKeys.length)} API keys`));
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
console.error(colors.error('✖ Failed to list API keys:'), colors.muted(error.message));
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
apiKeysCommand
|
|
294
|
+
.command('get')
|
|
295
|
+
.description('Get details of a specific API key')
|
|
296
|
+
.argument('<keyId>', 'API key ID')
|
|
297
|
+
.option('--json', 'Output as JSON')
|
|
298
|
+
.action(async (keyId, options) => {
|
|
299
|
+
try {
|
|
300
|
+
const apiKey = await apiClient.get(`/api-keys/${keyId}`);
|
|
301
|
+
if (options.json) {
|
|
302
|
+
console.log(JSON.stringify(apiKey, null, 2));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
console.log(colors.primary('🔍 API Key Details'));
|
|
306
|
+
console.log(colors.info('═'.repeat(60)));
|
|
307
|
+
console.log(`${colors.highlight('ID:')} ${colors.primary(apiKey.id)}`);
|
|
308
|
+
console.log(`${colors.highlight('Name:')} ${colors.accent(apiKey.name)}`);
|
|
309
|
+
console.log(`${colors.highlight('Type:')} ${colors.info(apiKey.keyType)}`);
|
|
310
|
+
console.log(`${colors.highlight('Environment:')} ${colors.accent(apiKey.environment)}`);
|
|
311
|
+
console.log(`${colors.highlight('Project ID:')} ${colors.muted(apiKey.projectId)}`);
|
|
312
|
+
console.log(`${colors.highlight('Access Level:')} ${colors.warning(apiKey.accessLevel)}`);
|
|
313
|
+
const statusColor = apiKey.status === 'active' ? colors.success :
|
|
314
|
+
apiKey.status === 'rotating' ? colors.warning : colors.error;
|
|
315
|
+
console.log(`${colors.highlight('Status:')} ${statusColor(apiKey.status)}`);
|
|
316
|
+
console.log(`${colors.highlight('Usage Count:')} ${colors.accent(apiKey.usageCount)}`);
|
|
317
|
+
console.log(`${colors.highlight('Tags:')} ${colors.muted(apiKey.tags.join(', ') || 'None')}`);
|
|
318
|
+
console.log(`${colors.highlight('Rotation Frequency:')} ${colors.info(apiKey.rotationFrequency)} days`);
|
|
319
|
+
console.log(`${colors.highlight('Last Rotated:')} ${colors.muted(formatDate(apiKey.lastRotated))}`);
|
|
320
|
+
console.log(`${colors.highlight('Created:')} ${colors.muted(formatDate(apiKey.createdAt))}`);
|
|
321
|
+
console.log(`${colors.highlight('Updated:')} ${colors.muted(formatDate(apiKey.updatedAt))}`);
|
|
322
|
+
if (apiKey.expiresAt) {
|
|
323
|
+
console.log(`${colors.highlight('Expires:')} ${colors.warning(formatDate(apiKey.expiresAt))}`);
|
|
324
|
+
}
|
|
325
|
+
console.log(colors.info('═'.repeat(60)));
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
console.error(colors.error('✖ Failed to get API key:'), colors.muted(error.message));
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
apiKeysCommand
|
|
333
|
+
.command('update')
|
|
334
|
+
.description('Update an API key')
|
|
335
|
+
.argument('<keyId>', 'API key ID')
|
|
336
|
+
.option('-n, --name <name>', 'New name')
|
|
337
|
+
.option('-v, --value <value>', 'New value')
|
|
338
|
+
.option('--tags <tags>', 'Comma-separated tags')
|
|
339
|
+
.option('--rotation-frequency <days>', 'Rotation frequency in days')
|
|
340
|
+
.option('--interactive', 'Interactive mode')
|
|
341
|
+
.action(async (keyId, options) => {
|
|
342
|
+
try {
|
|
343
|
+
let updateData = {};
|
|
344
|
+
if (options.name)
|
|
345
|
+
updateData.name = options.name;
|
|
346
|
+
if (options.value)
|
|
347
|
+
updateData.value = options.value;
|
|
348
|
+
if (options.tags)
|
|
349
|
+
updateData.tags = options.tags.split(',').map((tag) => tag.trim());
|
|
350
|
+
if (options.rotationFrequency)
|
|
351
|
+
updateData.rotationFrequency = parseInt(options.rotationFrequency);
|
|
352
|
+
if (options.interactive || Object.keys(updateData).length === 0) {
|
|
353
|
+
const current = await apiClient.get(`/api-keys/${keyId}`);
|
|
354
|
+
const answers = await inquirer.prompt([
|
|
355
|
+
{
|
|
356
|
+
type: 'input',
|
|
357
|
+
name: 'name',
|
|
358
|
+
message: 'Name:',
|
|
359
|
+
default: current.name,
|
|
360
|
+
when: !updateData.name
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
type: 'confirm',
|
|
364
|
+
name: 'updateValue',
|
|
365
|
+
message: 'Update the key value?',
|
|
366
|
+
default: false,
|
|
367
|
+
when: !updateData.value
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
type: 'password',
|
|
371
|
+
name: 'value',
|
|
372
|
+
message: 'New key value:',
|
|
373
|
+
when: (answers) => answers.updateValue
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
type: 'input',
|
|
377
|
+
name: 'tags',
|
|
378
|
+
message: 'Tags (comma-separated):',
|
|
379
|
+
default: current.tags.join(', '),
|
|
380
|
+
filter: (input) => input.split(',').map((tag) => tag.trim()),
|
|
381
|
+
when: !updateData.tags
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
type: 'number',
|
|
385
|
+
name: 'rotationFrequency',
|
|
386
|
+
message: 'Rotation frequency (days):',
|
|
387
|
+
default: current.rotationFrequency,
|
|
388
|
+
validate: (input) => input > 0 && input <= 365 || 'Must be between 1 and 365 days',
|
|
389
|
+
when: !updateData.rotationFrequency
|
|
390
|
+
}
|
|
391
|
+
]);
|
|
392
|
+
updateData = { ...updateData, ...answers };
|
|
393
|
+
delete updateData.updateValue;
|
|
394
|
+
}
|
|
395
|
+
const updatedKey = await apiClient.put(`/api-keys/${keyId}`, updateData);
|
|
396
|
+
console.log(colors.success('🔄 API key updated successfully!'));
|
|
397
|
+
console.log(colors.info('━'.repeat(40)));
|
|
398
|
+
console.log(`${colors.highlight('Name:')} ${colors.accent(updatedKey.name)}`);
|
|
399
|
+
console.log(`${colors.highlight('Status:')} ${colors.success(updatedKey.status)}`);
|
|
400
|
+
if (updateData.value) {
|
|
401
|
+
console.log(colors.warning('⚠️ The key value has been updated and re-encrypted.'));
|
|
402
|
+
}
|
|
403
|
+
console.log(colors.info('━'.repeat(40)));
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
console.error(colors.error('✖ Failed to update API key:'), colors.muted(error.message));
|
|
407
|
+
process.exit(1);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
apiKeysCommand
|
|
411
|
+
.command('delete')
|
|
412
|
+
.alias('rm')
|
|
413
|
+
.description('Delete an API key')
|
|
414
|
+
.argument('<keyId>', 'API key ID')
|
|
415
|
+
.option('-f, --force', 'Skip confirmation')
|
|
416
|
+
.action(async (keyId, options) => {
|
|
417
|
+
try {
|
|
418
|
+
if (!options.force) {
|
|
419
|
+
const apiKey = await apiClient.get(`/api-keys/${keyId}`);
|
|
420
|
+
const { confirm } = await inquirer.prompt([
|
|
421
|
+
{
|
|
422
|
+
type: 'confirm',
|
|
423
|
+
name: 'confirm',
|
|
424
|
+
message: `Are you sure you want to delete "${apiKey.name}"? This action cannot be undone.`,
|
|
425
|
+
default: false
|
|
426
|
+
}
|
|
427
|
+
]);
|
|
428
|
+
if (!confirm) {
|
|
429
|
+
console.log(colors.warning('🚫 Operation cancelled'));
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
await apiClient.delete(`/api-keys/${keyId}`);
|
|
434
|
+
console.log(colors.success('🗑️ API key deleted successfully!'));
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
console.error(colors.error('✖ Failed to delete API key:'), colors.muted(error.message));
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
// ============================================================================
|
|
442
|
+
// MCP COMMANDS
|
|
443
|
+
// ============================================================================
|
|
444
|
+
const mcpCommand = new Command('mcp')
|
|
445
|
+
.description(colors.accent('🤖 Model Context Protocol (MCP) - Secure AI agent access'));
|
|
446
|
+
mcpCommand
|
|
447
|
+
.command('register-tool')
|
|
448
|
+
.description('Register a new MCP tool')
|
|
449
|
+
.option('--tool-id <id>', 'Tool ID')
|
|
450
|
+
.option('--tool-name <name>', 'Tool name')
|
|
451
|
+
.option('--organization-id <id>', 'Organization ID')
|
|
452
|
+
.option('--keys <keys>', 'Comma-separated list of accessible key names')
|
|
453
|
+
.option('--environments <envs>', 'Comma-separated list of environments')
|
|
454
|
+
.option('--max-sessions <num>', 'Maximum concurrent sessions', '3')
|
|
455
|
+
.option('--max-duration <seconds>', 'Maximum session duration in seconds', '900')
|
|
456
|
+
.option('--webhook-url <url>', 'Webhook URL for notifications')
|
|
457
|
+
.option('--auto-approve', 'Enable auto-approval for low-risk requests')
|
|
458
|
+
.option('--risk-level <level>', 'Risk level (low, medium, high, critical)', 'medium')
|
|
459
|
+
.option('--interactive', 'Interactive mode')
|
|
460
|
+
.action(async (options) => {
|
|
461
|
+
try {
|
|
462
|
+
let toolData = {
|
|
463
|
+
toolId: options.toolId,
|
|
464
|
+
toolName: options.toolName,
|
|
465
|
+
organizationId: options.organizationId,
|
|
466
|
+
permissions: {
|
|
467
|
+
keys: options.keys ? options.keys.split(',').map((k) => k.trim()) : [],
|
|
468
|
+
environments: options.environments ? options.environments.split(',').map((e) => e.trim()) : ['development'],
|
|
469
|
+
maxConcurrentSessions: parseInt(options.maxSessions),
|
|
470
|
+
maxSessionDuration: parseInt(options.maxDuration)
|
|
471
|
+
},
|
|
472
|
+
webhookUrl: options.webhookUrl,
|
|
473
|
+
autoApprove: options.autoApprove || false,
|
|
474
|
+
riskLevel: options.riskLevel
|
|
475
|
+
};
|
|
476
|
+
if (options.interactive || !toolData.toolId || !toolData.toolName || !toolData.organizationId) {
|
|
477
|
+
const answers = await inquirer.prompt([
|
|
478
|
+
{
|
|
479
|
+
type: 'input',
|
|
480
|
+
name: 'toolId',
|
|
481
|
+
message: 'Tool ID:',
|
|
482
|
+
when: !toolData.toolId,
|
|
483
|
+
validate: (input) => input.length > 0 || 'Tool ID is required'
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
type: 'input',
|
|
487
|
+
name: 'toolName',
|
|
488
|
+
message: 'Tool name:',
|
|
489
|
+
when: !toolData.toolName,
|
|
490
|
+
validate: (input) => input.length > 0 || 'Tool name is required'
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
type: 'input',
|
|
494
|
+
name: 'organizationId',
|
|
495
|
+
message: 'Organization ID:',
|
|
496
|
+
when: !toolData.organizationId,
|
|
497
|
+
validate: (input) => {
|
|
498
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
499
|
+
return uuidRegex.test(input) || 'Please enter a valid UUID';
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
type: 'input',
|
|
504
|
+
name: 'keys',
|
|
505
|
+
message: 'Accessible key names (comma-separated):',
|
|
506
|
+
filter: (input) => input.split(',').map((k) => k.trim()),
|
|
507
|
+
when: toolData.permissions.keys.length === 0
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
type: 'checkbox',
|
|
511
|
+
name: 'environments',
|
|
512
|
+
message: 'Accessible environments:',
|
|
513
|
+
choices: ['development', 'staging', 'production'],
|
|
514
|
+
default: ['development']
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
type: 'number',
|
|
518
|
+
name: 'maxConcurrentSessions',
|
|
519
|
+
message: 'Maximum concurrent sessions:',
|
|
520
|
+
default: 3,
|
|
521
|
+
validate: (input) => input > 0 && input <= 10 || 'Must be between 1 and 10'
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
type: 'number',
|
|
525
|
+
name: 'maxSessionDuration',
|
|
526
|
+
message: 'Maximum session duration (seconds):',
|
|
527
|
+
default: 900,
|
|
528
|
+
validate: (input) => input >= 60 && input <= 3600 || 'Must be between 60 and 3600 seconds'
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
type: 'input',
|
|
532
|
+
name: 'webhookUrl',
|
|
533
|
+
message: 'Webhook URL (optional):'
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
type: 'confirm',
|
|
537
|
+
name: 'autoApprove',
|
|
538
|
+
message: 'Enable auto-approval?',
|
|
539
|
+
default: false
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
type: 'list',
|
|
543
|
+
name: 'riskLevel',
|
|
544
|
+
message: 'Risk level:',
|
|
545
|
+
choices: ['low', 'medium', 'high', 'critical'],
|
|
546
|
+
default: 'medium'
|
|
547
|
+
}
|
|
548
|
+
]);
|
|
549
|
+
if (answers.keys)
|
|
550
|
+
toolData.permissions.keys = answers.keys;
|
|
551
|
+
if (answers.environments)
|
|
552
|
+
toolData.permissions.environments = answers.environments;
|
|
553
|
+
if (answers.maxConcurrentSessions)
|
|
554
|
+
toolData.permissions.maxConcurrentSessions = answers.maxConcurrentSessions;
|
|
555
|
+
if (answers.maxSessionDuration)
|
|
556
|
+
toolData.permissions.maxSessionDuration = answers.maxSessionDuration;
|
|
557
|
+
toolData = { ...toolData, ...answers };
|
|
558
|
+
delete toolData.keys;
|
|
559
|
+
delete toolData.environments;
|
|
560
|
+
delete toolData.maxConcurrentSessions;
|
|
561
|
+
delete toolData.maxSessionDuration;
|
|
562
|
+
}
|
|
563
|
+
const tool = await apiClient.post('/api-keys/mcp/tools', toolData);
|
|
564
|
+
console.log(colors.success('🤖 MCP tool registered successfully!'));
|
|
565
|
+
console.log(colors.info('━'.repeat(50)));
|
|
566
|
+
console.log(`${colors.highlight('Tool ID:')} ${colors.primary(tool.toolId)}`);
|
|
567
|
+
console.log(`${colors.highlight('Name:')} ${colors.accent(tool.toolName)}`);
|
|
568
|
+
console.log(`${colors.highlight('Risk Level:')} ${colors.warning(tool.riskLevel)}`);
|
|
569
|
+
console.log(`${colors.highlight('Auto Approve:')} ${tool.autoApprove ? colors.success('Yes') : colors.error('No')}`);
|
|
570
|
+
console.log(colors.info('━'.repeat(50)));
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
console.error(colors.error('✖ Failed to register MCP tool:'), colors.muted(error.message));
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
mcpCommand
|
|
578
|
+
.command('list-tools')
|
|
579
|
+
.description('List registered MCP tools')
|
|
580
|
+
.option('--json', 'Output as JSON')
|
|
581
|
+
.action(async (options) => {
|
|
582
|
+
try {
|
|
583
|
+
const tools = await apiClient.get('/api-keys/mcp/tools');
|
|
584
|
+
if (options.json) {
|
|
585
|
+
console.log(JSON.stringify(tools, null, 2));
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (tools.length === 0) {
|
|
589
|
+
console.log(colors.warning('⚠️ No MCP tools found'));
|
|
590
|
+
console.log(colors.muted('Run: lanonasis api-keys mcp register-tool'));
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
console.log(colors.primary('🤖 Registered MCP Tools'));
|
|
594
|
+
console.log(colors.info('═'.repeat(80)));
|
|
595
|
+
const table = new Table({
|
|
596
|
+
head: ['Tool ID', 'Name', 'Risk Level', 'Status', 'Auto Approve', 'Created'].map(h => colors.accent(h)),
|
|
597
|
+
style: { head: [], border: [] }
|
|
598
|
+
});
|
|
599
|
+
tools.forEach((tool) => {
|
|
600
|
+
const statusColor = tool.status === 'active' ? colors.success :
|
|
601
|
+
tool.status === 'suspended' ? colors.error : colors.warning;
|
|
602
|
+
table.push([
|
|
603
|
+
tool.toolId,
|
|
604
|
+
tool.toolName,
|
|
605
|
+
tool.riskLevel,
|
|
606
|
+
statusColor(tool.status),
|
|
607
|
+
tool.autoApprove ? colors.success('Yes') : colors.error('No'),
|
|
608
|
+
formatDate(tool.createdAt)
|
|
609
|
+
]);
|
|
610
|
+
});
|
|
611
|
+
console.log(table.toString());
|
|
612
|
+
console.log(colors.info('═'.repeat(80)));
|
|
613
|
+
console.log(colors.muted(`🤖 Total: ${colors.highlight(tools.length)} MCP tools`));
|
|
614
|
+
}
|
|
615
|
+
catch (error) {
|
|
616
|
+
console.error(colors.error('✖ Failed to list MCP tools:'), colors.muted(error.message));
|
|
617
|
+
process.exit(1);
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
mcpCommand
|
|
621
|
+
.command('request-access')
|
|
622
|
+
.description('Request access to API keys via MCP')
|
|
623
|
+
.option('--tool-id <id>', 'Tool ID')
|
|
624
|
+
.option('--organization-id <id>', 'Organization ID')
|
|
625
|
+
.option('--keys <keys>', 'Comma-separated list of key names')
|
|
626
|
+
.option('--environment <env>', 'Environment (development, staging, production)')
|
|
627
|
+
.option('--justification <text>', 'Justification for access')
|
|
628
|
+
.option('--duration <seconds>', 'Estimated duration in seconds', '900')
|
|
629
|
+
.option('--interactive', 'Interactive mode')
|
|
630
|
+
.action(async (options) => {
|
|
631
|
+
try {
|
|
632
|
+
let requestData = {
|
|
633
|
+
toolId: options.toolId,
|
|
634
|
+
organizationId: options.organizationId,
|
|
635
|
+
keyNames: options.keys ? options.keys.split(',').map((k) => k.trim()) : [],
|
|
636
|
+
environment: options.environment,
|
|
637
|
+
justification: options.justification,
|
|
638
|
+
estimatedDuration: parseInt(options.duration),
|
|
639
|
+
context: {}
|
|
640
|
+
};
|
|
641
|
+
if (options.interactive || !requestData.toolId || !requestData.organizationId ||
|
|
642
|
+
requestData.keyNames.length === 0 || !requestData.environment || !requestData.justification) {
|
|
643
|
+
const tools = await apiClient.get('/api-keys/mcp/tools');
|
|
644
|
+
const answers = await inquirer.prompt([
|
|
645
|
+
{
|
|
646
|
+
type: 'list',
|
|
647
|
+
name: 'toolId',
|
|
648
|
+
message: 'Select MCP tool:',
|
|
649
|
+
when: !requestData.toolId && tools.length > 0,
|
|
650
|
+
choices: tools.map((t) => ({ name: `${t.toolName} (${t.toolId})`, value: t.toolId }))
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
type: 'input',
|
|
654
|
+
name: 'organizationId',
|
|
655
|
+
message: 'Organization ID:',
|
|
656
|
+
when: !requestData.organizationId,
|
|
657
|
+
validate: (input) => {
|
|
658
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
659
|
+
return uuidRegex.test(input) || 'Please enter a valid UUID';
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
type: 'input',
|
|
664
|
+
name: 'keyNames',
|
|
665
|
+
message: 'Key names to access (comma-separated):',
|
|
666
|
+
when: requestData.keyNames.length === 0,
|
|
667
|
+
filter: (input) => input.split(',').map((k) => k.trim()),
|
|
668
|
+
validate: (input) => input.length > 0 || 'At least one key name is required'
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
type: 'list',
|
|
672
|
+
name: 'environment',
|
|
673
|
+
message: 'Environment:',
|
|
674
|
+
when: !requestData.environment,
|
|
675
|
+
choices: ['development', 'staging', 'production']
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
type: 'input',
|
|
679
|
+
name: 'justification',
|
|
680
|
+
message: 'Justification for access:',
|
|
681
|
+
when: !requestData.justification,
|
|
682
|
+
validate: (input) => input.length > 0 || 'Justification is required'
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
type: 'number',
|
|
686
|
+
name: 'estimatedDuration',
|
|
687
|
+
message: 'Estimated duration (seconds):',
|
|
688
|
+
default: 900,
|
|
689
|
+
validate: (input) => input >= 60 && input <= 3600 || 'Must be between 60 and 3600 seconds'
|
|
690
|
+
}
|
|
691
|
+
]);
|
|
692
|
+
requestData = { ...requestData, ...answers };
|
|
693
|
+
}
|
|
694
|
+
const response = await apiClient.post('/api-keys/mcp/request-access', requestData);
|
|
695
|
+
console.log(colors.success('🔐 Access request created successfully!'));
|
|
696
|
+
console.log(colors.info('━'.repeat(50)));
|
|
697
|
+
console.log(`${colors.highlight('Request ID:')} ${colors.primary(response.requestId)}`);
|
|
698
|
+
console.log(`${colors.highlight('Status:')} ${colors.accent(response.status)}`);
|
|
699
|
+
console.log(colors.info('━'.repeat(50)));
|
|
700
|
+
console.log(colors.warning('💡 Check the status with: lanonasis api-keys analytics usage'));
|
|
701
|
+
}
|
|
702
|
+
catch (error) {
|
|
703
|
+
console.error(colors.error('✖ Failed to create access request:'), colors.muted(error.message));
|
|
704
|
+
process.exit(1);
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
// ============================================================================
|
|
708
|
+
// ANALYTICS COMMANDS
|
|
709
|
+
// ============================================================================
|
|
710
|
+
const analyticsCommand = new Command('analytics')
|
|
711
|
+
.description('View API key usage analytics and security events');
|
|
712
|
+
analyticsCommand
|
|
713
|
+
.command('usage')
|
|
714
|
+
.description('View usage analytics')
|
|
715
|
+
.option('--key-id <id>', 'Filter by specific API key')
|
|
716
|
+
.option('--days <days>', 'Number of days to look back', '30')
|
|
717
|
+
.option('--json', 'Output as JSON')
|
|
718
|
+
.action(async (options) => {
|
|
719
|
+
try {
|
|
720
|
+
let url = '/api-keys/analytics/usage';
|
|
721
|
+
const params = new URLSearchParams();
|
|
722
|
+
if (options.keyId)
|
|
723
|
+
params.append('keyId', options.keyId);
|
|
724
|
+
if (options.days)
|
|
725
|
+
params.append('days', options.days);
|
|
726
|
+
if (params.toString()) {
|
|
727
|
+
url += `?${params.toString()}`;
|
|
728
|
+
}
|
|
729
|
+
const analytics = await apiClient.get(url);
|
|
730
|
+
if (options.json) {
|
|
731
|
+
console.log(JSON.stringify(analytics, null, 2));
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
if (analytics.length === 0) {
|
|
735
|
+
console.log(chalk.yellow('No usage data found'));
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
const table = new Table({
|
|
739
|
+
head: ['Key ID', 'Operation', 'Tool ID', 'Success', 'Timestamp'].map(h => chalk.cyan(h)),
|
|
740
|
+
style: { head: [], border: [] }
|
|
741
|
+
});
|
|
742
|
+
analytics.forEach((entry) => {
|
|
743
|
+
const successColor = entry.success ? colors.success('✓') : colors.error('✗');
|
|
744
|
+
table.push([
|
|
745
|
+
truncateText(entry.keyId || '-', 20),
|
|
746
|
+
entry.operation,
|
|
747
|
+
truncateText(entry.toolId || '-', 15),
|
|
748
|
+
successColor,
|
|
749
|
+
formatDate(entry.timestamp)
|
|
750
|
+
]);
|
|
751
|
+
});
|
|
752
|
+
console.log(table.toString());
|
|
753
|
+
console.log(colors.info('═'.repeat(80)));
|
|
754
|
+
console.log(colors.muted(`📈 Total: ${colors.highlight(analytics.length)} events`));
|
|
755
|
+
}
|
|
756
|
+
catch (error) {
|
|
757
|
+
console.error(colors.error('✖ Failed to get usage analytics:'), colors.muted(error.message));
|
|
758
|
+
process.exit(1);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
analyticsCommand
|
|
762
|
+
.command('security-events')
|
|
763
|
+
.description('View security events')
|
|
764
|
+
.option('--severity <level>', 'Filter by severity (low, medium, high, critical)')
|
|
765
|
+
.option('--json', 'Output as JSON')
|
|
766
|
+
.action(async (options) => {
|
|
767
|
+
try {
|
|
768
|
+
let url = '/api-keys/analytics/security-events';
|
|
769
|
+
if (options.severity) {
|
|
770
|
+
url += `?severity=${options.severity}`;
|
|
771
|
+
}
|
|
772
|
+
const events = await apiClient.get(url);
|
|
773
|
+
if (options.json) {
|
|
774
|
+
console.log(JSON.stringify(events, null, 2));
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
if (events.length === 0) {
|
|
778
|
+
console.log(colors.success('✅ No security events found'));
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
console.log(colors.primary('🛡️ Security Events Monitor'));
|
|
782
|
+
console.log(colors.info('═'.repeat(80)));
|
|
783
|
+
const table = new Table({
|
|
784
|
+
head: ['Event Type', 'Severity', 'Description', 'Resolved', 'Timestamp'].map(h => colors.accent(h)),
|
|
785
|
+
style: { head: [], border: [] }
|
|
786
|
+
});
|
|
787
|
+
events.forEach((event) => {
|
|
788
|
+
const severityColor = event.severity === 'critical' ? colors.error :
|
|
789
|
+
event.severity === 'high' ? colors.accent :
|
|
790
|
+
event.severity === 'medium' ? colors.warning : colors.success;
|
|
791
|
+
table.push([
|
|
792
|
+
event.eventType,
|
|
793
|
+
severityColor(event.severity.toUpperCase()),
|
|
794
|
+
truncateText(event.description, 40),
|
|
795
|
+
event.resolved ? colors.success('✓') : colors.warning('Pending'),
|
|
796
|
+
formatDate(event.timestamp)
|
|
797
|
+
]);
|
|
798
|
+
});
|
|
799
|
+
console.log(table.toString());
|
|
800
|
+
console.log(colors.info('═'.repeat(80)));
|
|
801
|
+
console.log(colors.muted(`🛡️ Total: ${colors.highlight(events.length)} security events`));
|
|
802
|
+
}
|
|
803
|
+
catch (error) {
|
|
804
|
+
console.error(colors.error('✖ Failed to get security events:'), colors.muted(error.message));
|
|
805
|
+
process.exit(1);
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
// Add subcommands
|
|
809
|
+
apiKeysCommand.addCommand(projectsCommand);
|
|
810
|
+
apiKeysCommand.addCommand(mcpCommand);
|
|
811
|
+
apiKeysCommand.addCommand(analyticsCommand);
|
|
812
|
+
export default apiKeysCommand;
|