@lanonasis/cli 3.9.11 → 3.9.12
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/api-keys.js +196 -161
- package/dist/commands/mcp.js +6 -0
- package/dist/commands/memory.js +42 -9
- package/dist/index.js +14 -0
- package/dist/utils/api.d.ts +3 -0
- package/dist/utils/config.d.ts +4 -0
- package/dist/utils/config.js +47 -6
- package/dist/ux/implementations/ConnectionManagerImpl.js +14 -2
- package/package.json +1 -1
|
@@ -15,6 +15,24 @@ const colors = {
|
|
|
15
15
|
muted: chalk.gray,
|
|
16
16
|
highlight: chalk.white.bold
|
|
17
17
|
};
|
|
18
|
+
const AUTH_API_KEYS_BASE = '/api/v1/auth/api-keys';
|
|
19
|
+
const VALID_ACCESS_LEVELS = ['public', 'authenticated', 'team', 'admin', 'enterprise'];
|
|
20
|
+
function unwrapApiResponse(response) {
|
|
21
|
+
if (response && typeof response === 'object' && 'data' in response) {
|
|
22
|
+
return response.data ?? response;
|
|
23
|
+
}
|
|
24
|
+
return response;
|
|
25
|
+
}
|
|
26
|
+
function parseScopes(scopes) {
|
|
27
|
+
if (!scopes) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const parsed = scopes
|
|
31
|
+
.split(',')
|
|
32
|
+
.map((scope) => scope.trim())
|
|
33
|
+
.filter(Boolean);
|
|
34
|
+
return parsed.length > 0 ? parsed : undefined;
|
|
35
|
+
}
|
|
18
36
|
const apiKeysCommand = new Command('api-keys')
|
|
19
37
|
.alias('keys')
|
|
20
38
|
.description(colors.info('🔐 Manage API keys securely with enterprise-grade encryption'));
|
|
@@ -65,7 +83,8 @@ projectsCommand
|
|
|
65
83
|
]);
|
|
66
84
|
projectData = { ...projectData, ...answers };
|
|
67
85
|
}
|
|
68
|
-
const
|
|
86
|
+
const projectRes = await apiClient.post(`${AUTH_API_KEYS_BASE}/projects`, projectData);
|
|
87
|
+
const project = unwrapApiResponse(projectRes);
|
|
69
88
|
console.log(chalk.green('✅ Project created successfully!'));
|
|
70
89
|
console.log(chalk.blue(`Project ID: ${project.id}`));
|
|
71
90
|
console.log(chalk.blue(`Name: ${project.name}`));
|
|
@@ -85,12 +104,12 @@ projectsCommand
|
|
|
85
104
|
.option('--json', 'Output as JSON')
|
|
86
105
|
.action(async (options) => {
|
|
87
106
|
try {
|
|
88
|
-
const projects = await apiClient.get(
|
|
107
|
+
const projects = unwrapApiResponse(await apiClient.get(`${AUTH_API_KEYS_BASE}/projects`));
|
|
89
108
|
if (options.json) {
|
|
90
109
|
console.log(JSON.stringify(projects, null, 2));
|
|
91
110
|
return;
|
|
92
111
|
}
|
|
93
|
-
if (projects.length === 0) {
|
|
112
|
+
if (!Array.isArray(projects) || projects.length === 0) {
|
|
94
113
|
console.log(chalk.yellow('No projects found'));
|
|
95
114
|
return;
|
|
96
115
|
}
|
|
@@ -122,30 +141,29 @@ apiKeysCommand
|
|
|
122
141
|
.command('create')
|
|
123
142
|
.description('Create a new API key')
|
|
124
143
|
.option('-n, --name <name>', 'API key name')
|
|
125
|
-
.option('-
|
|
126
|
-
.option('-
|
|
127
|
-
.option('-
|
|
128
|
-
.option('
|
|
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')
|
|
144
|
+
.option('-d, --description <description>', 'API key description (optional)')
|
|
145
|
+
.option('--access-level <level>', 'Access level (public, authenticated, team, admin, enterprise)', 'team')
|
|
146
|
+
.option('--expires-in-days <days>', 'Expiration in days (default: 365)', '365')
|
|
147
|
+
.option('--scopes <scopes>', 'Comma-separated scopes (optional)')
|
|
133
148
|
.option('--interactive', 'Interactive mode')
|
|
134
149
|
.action(async (options) => {
|
|
135
150
|
try {
|
|
151
|
+
const accessLevel = (options.accessLevel || 'team').toLowerCase();
|
|
152
|
+
const expiresInDays = parseInt(options.expiresInDays, 10);
|
|
153
|
+
if (!VALID_ACCESS_LEVELS.includes(accessLevel)) {
|
|
154
|
+
throw new Error('Invalid access level. Allowed: public, authenticated, team, admin, enterprise');
|
|
155
|
+
}
|
|
156
|
+
if (!Number.isInteger(expiresInDays) || expiresInDays <= 0 || expiresInDays > 3650) {
|
|
157
|
+
throw new Error('expires-in-days must be a positive integer up to 3650');
|
|
158
|
+
}
|
|
136
159
|
let keyData = {
|
|
137
160
|
name: options.name,
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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)
|
|
161
|
+
access_level: accessLevel,
|
|
162
|
+
expires_in_days: expiresInDays,
|
|
163
|
+
description: options.description?.trim() || undefined,
|
|
164
|
+
scopes: parseScopes(options.scopes)
|
|
146
165
|
};
|
|
147
|
-
if (options.interactive || !keyData.name
|
|
148
|
-
const projects = await apiClient.get('/api-keys/projects');
|
|
166
|
+
if (options.interactive || !keyData.name) {
|
|
149
167
|
const answers = await inquirer.prompt([
|
|
150
168
|
{
|
|
151
169
|
type: 'input',
|
|
@@ -155,85 +173,62 @@ apiKeysCommand
|
|
|
155
173
|
validate: (input) => input.length > 0 || 'Name is required'
|
|
156
174
|
},
|
|
157
175
|
{
|
|
158
|
-
type: '
|
|
159
|
-
name: '
|
|
160
|
-
message: '
|
|
161
|
-
|
|
162
|
-
validate: (input) => input.length > 0 || 'Value is required'
|
|
163
|
-
},
|
|
164
|
-
{
|
|
165
|
-
type: 'select',
|
|
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: 'select',
|
|
181
|
-
name: 'environment',
|
|
182
|
-
message: 'Environment:',
|
|
183
|
-
choices: ['development', 'staging', 'production'],
|
|
184
|
-
default: 'development'
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
type: 'select',
|
|
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 }))
|
|
176
|
+
type: 'input',
|
|
177
|
+
name: 'description',
|
|
178
|
+
message: 'Description (optional):',
|
|
179
|
+
default: keyData.description || ''
|
|
192
180
|
},
|
|
193
181
|
{
|
|
194
182
|
type: 'select',
|
|
195
|
-
name: '
|
|
183
|
+
name: 'access_level',
|
|
196
184
|
message: 'Access level:',
|
|
197
|
-
choices:
|
|
198
|
-
default:
|
|
185
|
+
choices: VALID_ACCESS_LEVELS,
|
|
186
|
+
default: keyData.access_level
|
|
199
187
|
},
|
|
200
188
|
{
|
|
201
|
-
type: '
|
|
202
|
-
name: '
|
|
203
|
-
message: '
|
|
204
|
-
|
|
189
|
+
type: 'number',
|
|
190
|
+
name: 'expires_in_days',
|
|
191
|
+
message: 'Expires in days:',
|
|
192
|
+
default: keyData.expires_in_days,
|
|
193
|
+
validate: (input) => Number.isInteger(input) && input > 0 && input <= 3650 || 'Must be between 1 and 3650 days'
|
|
205
194
|
},
|
|
206
195
|
{
|
|
207
196
|
type: 'input',
|
|
208
|
-
name: '
|
|
209
|
-
message: '
|
|
210
|
-
|
|
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'
|
|
197
|
+
name: 'scopes',
|
|
198
|
+
message: 'Scopes (comma-separated, optional):',
|
|
199
|
+
default: (keyData.scopes || []).join(', ')
|
|
224
200
|
}
|
|
225
201
|
]);
|
|
226
|
-
keyData = {
|
|
202
|
+
keyData = {
|
|
203
|
+
...keyData,
|
|
204
|
+
...answers,
|
|
205
|
+
description: typeof answers.description === 'string'
|
|
206
|
+
? answers.description.trim() || undefined
|
|
207
|
+
: keyData.description,
|
|
208
|
+
scopes: parseScopes(typeof answers.scopes === 'string' ? answers.scopes : undefined) ?? keyData.scopes
|
|
209
|
+
};
|
|
227
210
|
}
|
|
228
|
-
const apiKey = await apiClient.post(
|
|
211
|
+
const apiKey = unwrapApiResponse(await apiClient.post(AUTH_API_KEYS_BASE, keyData));
|
|
229
212
|
console.log(colors.success('🔐 API key created successfully!'));
|
|
230
213
|
console.log(colors.info('━'.repeat(50)));
|
|
231
214
|
console.log(`${colors.highlight('Key ID:')} ${colors.primary(apiKey.id)}`);
|
|
232
215
|
console.log(`${colors.highlight('Name:')} ${colors.accent(apiKey.name)}`);
|
|
233
|
-
console.log(`${colors.highlight('
|
|
234
|
-
console.log(`${colors.highlight('
|
|
216
|
+
console.log(`${colors.highlight('Access Level:')} ${colors.info(apiKey.access_level || keyData.access_level)}`);
|
|
217
|
+
console.log(`${colors.highlight('Permissions:')} ${colors.muted((apiKey.permissions || keyData.scopes || []).join(', ') || 'legacy:full_access')}`);
|
|
218
|
+
if (apiKey.expires_at) {
|
|
219
|
+
console.log(`${colors.highlight('Expires At:')} ${colors.warning(formatDate(apiKey.expires_at))}`);
|
|
220
|
+
}
|
|
221
|
+
if (keyData.description) {
|
|
222
|
+
console.log(`${colors.highlight('Description:')} ${colors.muted(keyData.description)}`);
|
|
223
|
+
}
|
|
235
224
|
console.log(colors.info('━'.repeat(50)));
|
|
236
|
-
|
|
225
|
+
if (apiKey.key) {
|
|
226
|
+
console.log(`${colors.highlight('API Key:')} ${colors.primary(apiKey.key)}`);
|
|
227
|
+
console.log(colors.warning('⚠️ Save this key now. It will not be shown again.'));
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
console.log(colors.warning('⚠️ Key value was not returned. If newly created, it cannot be retrieved later.'));
|
|
231
|
+
}
|
|
237
232
|
}
|
|
238
233
|
catch (error) {
|
|
239
234
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -245,15 +240,12 @@ apiKeysCommand
|
|
|
245
240
|
.command('list')
|
|
246
241
|
.alias('ls')
|
|
247
242
|
.description('List API keys')
|
|
248
|
-
.option('
|
|
243
|
+
.option('--all', 'Include inactive keys')
|
|
249
244
|
.option('--json', 'Output as JSON')
|
|
250
245
|
.action(async (options) => {
|
|
251
246
|
try {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
url += `?projectId=${options.projectId}`;
|
|
255
|
-
}
|
|
256
|
-
const apiKeys = await apiClient.get(url);
|
|
247
|
+
const query = options.all ? '?active_only=false' : '';
|
|
248
|
+
const apiKeys = unwrapApiResponse(await apiClient.get(`${AUTH_API_KEYS_BASE}${query}`));
|
|
257
249
|
if (options.json) {
|
|
258
250
|
console.log(JSON.stringify(apiKeys, null, 2));
|
|
259
251
|
return;
|
|
@@ -266,20 +258,19 @@ apiKeysCommand
|
|
|
266
258
|
console.log(colors.primary('🔐 API Key Management'));
|
|
267
259
|
console.log(colors.info('═'.repeat(80)));
|
|
268
260
|
const table = new Table({
|
|
269
|
-
head: ['ID', 'Name', '
|
|
261
|
+
head: ['ID', 'Name', 'Access', 'Permissions', 'Service', 'Status', 'Expires'].map(h => colors.accent(h)),
|
|
270
262
|
style: { head: [], border: [] }
|
|
271
263
|
});
|
|
272
264
|
apiKeys.forEach((key) => {
|
|
273
|
-
const statusColor = key.
|
|
274
|
-
key.status === 'rotating' ? colors.warning : colors.error;
|
|
265
|
+
const statusColor = key.is_active ? colors.success : colors.error;
|
|
275
266
|
table.push([
|
|
276
267
|
truncateText(key.id, 20),
|
|
277
268
|
key.name,
|
|
278
|
-
key.
|
|
279
|
-
key.
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
formatDate(key.
|
|
269
|
+
key.access_level,
|
|
270
|
+
truncateText((key.permissions || []).join(', ') || 'legacy:full_access', 28),
|
|
271
|
+
key.service || 'all',
|
|
272
|
+
statusColor(key.is_active ? 'active' : 'inactive'),
|
|
273
|
+
key.expires_at ? formatDate(key.expires_at) : colors.muted('Never')
|
|
283
274
|
]);
|
|
284
275
|
});
|
|
285
276
|
console.log(table.toString());
|
|
@@ -299,7 +290,7 @@ apiKeysCommand
|
|
|
299
290
|
.option('--json', 'Output as JSON')
|
|
300
291
|
.action(async (keyId, options) => {
|
|
301
292
|
try {
|
|
302
|
-
const apiKey = await apiClient.get(
|
|
293
|
+
const apiKey = unwrapApiResponse(await apiClient.get(`${AUTH_API_KEYS_BASE}/${keyId}`));
|
|
303
294
|
if (options.json) {
|
|
304
295
|
console.log(JSON.stringify(apiKey, null, 2));
|
|
305
296
|
return;
|
|
@@ -308,21 +299,20 @@ apiKeysCommand
|
|
|
308
299
|
console.log(colors.info('═'.repeat(60)));
|
|
309
300
|
console.log(`${colors.highlight('ID:')} ${colors.primary(apiKey.id)}`);
|
|
310
301
|
console.log(`${colors.highlight('Name:')} ${colors.accent(apiKey.name)}`);
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
console.log(`${colors.highlight('Access Level:')} ${colors.warning(apiKey.
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
console.log(`${colors.highlight('
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
console.log(`${colors.highlight('Created:')} ${colors.muted(formatDate(apiKey.
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
console.log(`${colors.highlight('Expires:')} ${colors.warning(formatDate(apiKey.expiresAt))}`);
|
|
302
|
+
if (apiKey.description) {
|
|
303
|
+
console.log(`${colors.highlight('Description:')} ${colors.muted(apiKey.description)}`);
|
|
304
|
+
}
|
|
305
|
+
console.log(`${colors.highlight('Access Level:')} ${colors.warning(apiKey.access_level)}`);
|
|
306
|
+
console.log(`${colors.highlight('Permissions:')} ${colors.muted((apiKey.permissions || []).join(', ') || 'legacy:full_access')}`);
|
|
307
|
+
console.log(`${colors.highlight('Service Scope:')} ${colors.info(apiKey.service || 'all')}`);
|
|
308
|
+
const statusColor = apiKey.is_active ? colors.success : colors.error;
|
|
309
|
+
console.log(`${colors.highlight('Status:')} ${statusColor(apiKey.is_active ? 'active' : 'inactive')}`);
|
|
310
|
+
if (apiKey.last_used_at) {
|
|
311
|
+
console.log(`${colors.highlight('Last Used:')} ${colors.muted(formatDate(apiKey.last_used_at))}`);
|
|
312
|
+
}
|
|
313
|
+
console.log(`${colors.highlight('Created:')} ${colors.muted(formatDate(apiKey.created_at))}`);
|
|
314
|
+
if (apiKey.expires_at) {
|
|
315
|
+
console.log(`${colors.highlight('Expires:')} ${colors.warning(formatDate(apiKey.expires_at))}`);
|
|
326
316
|
}
|
|
327
317
|
console.log(colors.info('═'.repeat(60)));
|
|
328
318
|
}
|
|
@@ -337,23 +327,39 @@ apiKeysCommand
|
|
|
337
327
|
.description('Update an API key')
|
|
338
328
|
.argument('<keyId>', 'API key ID')
|
|
339
329
|
.option('-n, --name <name>', 'New name')
|
|
340
|
-
.option('-
|
|
341
|
-
.option('--
|
|
342
|
-
.option('--
|
|
330
|
+
.option('-d, --description <description>', 'New description')
|
|
331
|
+
.option('--access-level <level>', 'New access level')
|
|
332
|
+
.option('--expires-in-days <days>', 'Set a new expiry in days')
|
|
333
|
+
.option('--clear-expiry', 'Remove the current expiry')
|
|
334
|
+
.option('--scopes <scopes>', 'Replace scopes with a comma-separated list')
|
|
343
335
|
.option('--interactive', 'Interactive mode')
|
|
344
336
|
.action(async (keyId, options) => {
|
|
345
337
|
try {
|
|
346
338
|
let updateData = {};
|
|
347
339
|
if (options.name)
|
|
348
340
|
updateData.name = options.name;
|
|
349
|
-
if (options.
|
|
350
|
-
updateData.
|
|
351
|
-
if (options.
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
341
|
+
if (options.description !== undefined)
|
|
342
|
+
updateData.description = options.description.trim() || null;
|
|
343
|
+
if (options.accessLevel) {
|
|
344
|
+
const accessLevel = options.accessLevel.toLowerCase();
|
|
345
|
+
if (!VALID_ACCESS_LEVELS.includes(accessLevel)) {
|
|
346
|
+
throw new Error('Invalid access level. Allowed: public, authenticated, team, admin, enterprise');
|
|
347
|
+
}
|
|
348
|
+
updateData.access_level = accessLevel;
|
|
349
|
+
}
|
|
350
|
+
if (options.expiresInDays) {
|
|
351
|
+
const expiresInDays = parseInt(options.expiresInDays, 10);
|
|
352
|
+
if (!Number.isInteger(expiresInDays) || expiresInDays <= 0 || expiresInDays > 3650) {
|
|
353
|
+
throw new Error('expires-in-days must be a positive integer up to 3650');
|
|
354
|
+
}
|
|
355
|
+
updateData.expires_in_days = expiresInDays;
|
|
356
|
+
}
|
|
357
|
+
if (options.clearExpiry)
|
|
358
|
+
updateData.clear_expiry = true;
|
|
359
|
+
if (options.scopes)
|
|
360
|
+
updateData.scopes = parseScopes(options.scopes);
|
|
355
361
|
if (options.interactive || Object.keys(updateData).length === 0) {
|
|
356
|
-
const current = await apiClient.get(
|
|
362
|
+
const current = unwrapApiResponse(await apiClient.get(`${AUTH_API_KEYS_BASE}/${keyId}`));
|
|
357
363
|
const answers = await inquirer.prompt([
|
|
358
364
|
{
|
|
359
365
|
type: 'input',
|
|
@@ -362,47 +368,75 @@ apiKeysCommand
|
|
|
362
368
|
default: current.name,
|
|
363
369
|
when: !updateData.name
|
|
364
370
|
},
|
|
371
|
+
{
|
|
372
|
+
type: 'input',
|
|
373
|
+
name: 'description',
|
|
374
|
+
message: 'Description (optional):',
|
|
375
|
+
default: current.description || '',
|
|
376
|
+
when: !updateData.description
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
type: 'select',
|
|
380
|
+
name: 'access_level',
|
|
381
|
+
message: 'Access level:',
|
|
382
|
+
choices: VALID_ACCESS_LEVELS,
|
|
383
|
+
default: current.access_level,
|
|
384
|
+
when: !updateData.access_level
|
|
385
|
+
},
|
|
365
386
|
{
|
|
366
387
|
type: 'confirm',
|
|
367
|
-
name: '
|
|
368
|
-
message: '
|
|
388
|
+
name: 'changeExpiry',
|
|
389
|
+
message: 'Change expiry?',
|
|
369
390
|
default: false,
|
|
370
|
-
when: !updateData.
|
|
391
|
+
when: updateData.expires_in_days === undefined && !updateData.clear_expiry
|
|
371
392
|
},
|
|
372
393
|
{
|
|
373
|
-
type: '
|
|
374
|
-
name: '
|
|
375
|
-
message: '
|
|
376
|
-
|
|
394
|
+
type: 'number',
|
|
395
|
+
name: 'expires_in_days',
|
|
396
|
+
message: 'Expires in days:',
|
|
397
|
+
default: current.expires_at ? 365 : 365,
|
|
398
|
+
when: (answers) => answers.changeExpiry === true && updateData.expires_in_days === undefined && !updateData.clear_expiry,
|
|
399
|
+
validate: (input) => Number.isInteger(input) && input > 0 && input <= 3650 || 'Must be between 1 and 3650 days'
|
|
377
400
|
},
|
|
378
401
|
{
|
|
379
|
-
type: '
|
|
380
|
-
name: '
|
|
381
|
-
message: '
|
|
382
|
-
default:
|
|
383
|
-
|
|
384
|
-
when: !updateData.tags
|
|
402
|
+
type: 'confirm',
|
|
403
|
+
name: 'clear_expiry',
|
|
404
|
+
message: 'Clear expiry instead?',
|
|
405
|
+
default: false,
|
|
406
|
+
when: (answers) => answers.changeExpiry === true && updateData.expires_in_days === undefined && !updateData.clear_expiry && Boolean(current.expires_at)
|
|
385
407
|
},
|
|
386
408
|
{
|
|
387
|
-
type: '
|
|
388
|
-
name: '
|
|
389
|
-
message: '
|
|
390
|
-
default: current.
|
|
391
|
-
|
|
392
|
-
when: !updateData.rotationFrequency
|
|
409
|
+
type: 'input',
|
|
410
|
+
name: 'scopes',
|
|
411
|
+
message: 'Scopes (comma-separated, optional):',
|
|
412
|
+
default: (current.permissions || []).join(', '),
|
|
413
|
+
when: !updateData.scopes
|
|
393
414
|
}
|
|
394
415
|
]);
|
|
395
|
-
updateData = {
|
|
396
|
-
|
|
416
|
+
updateData = {
|
|
417
|
+
...updateData,
|
|
418
|
+
...answers,
|
|
419
|
+
description: typeof answers.description === 'string'
|
|
420
|
+
? answers.description.trim() || null
|
|
421
|
+
: updateData.description,
|
|
422
|
+
scopes: parseScopes(typeof answers.scopes === 'string' ? answers.scopes : undefined) ?? updateData.scopes
|
|
423
|
+
};
|
|
424
|
+
delete updateData.changeExpiry;
|
|
425
|
+
}
|
|
426
|
+
if (Object.keys(updateData).length === 0) {
|
|
427
|
+
console.log(colors.warning('🚫 Nothing to update'));
|
|
428
|
+
return;
|
|
397
429
|
}
|
|
398
|
-
const updatedKey = await apiClient.put(
|
|
430
|
+
const updatedKey = unwrapApiResponse(await apiClient.put(`${AUTH_API_KEYS_BASE}/${keyId}`, updateData));
|
|
399
431
|
console.log(colors.success('🔄 API key updated successfully!'));
|
|
400
432
|
console.log(colors.info('━'.repeat(40)));
|
|
401
433
|
console.log(`${colors.highlight('Name:')} ${colors.accent(updatedKey.name)}`);
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
console.log(colors.warning('⚠️ The key value has been updated and re-encrypted.'));
|
|
434
|
+
if (updatedKey.description || updateData.description) {
|
|
435
|
+
console.log(`${colors.highlight('Description:')} ${colors.muted(updatedKey.description || updateData.description)}`);
|
|
405
436
|
}
|
|
437
|
+
console.log(`${colors.highlight('Access Level:')} ${colors.info(updatedKey.access_level)}`);
|
|
438
|
+
console.log(`${colors.highlight('Permissions:')} ${colors.muted((updatedKey.permissions || []).join(', ') || 'legacy:full_access')}`);
|
|
439
|
+
console.log(`${colors.highlight('Expires:')} ${updatedKey.expires_at ? colors.warning(formatDate(updatedKey.expires_at)) : colors.muted('Never')}`);
|
|
406
440
|
console.log(colors.info('━'.repeat(40)));
|
|
407
441
|
}
|
|
408
442
|
catch (error) {
|
|
@@ -420,7 +454,7 @@ apiKeysCommand
|
|
|
420
454
|
.action(async (keyId, options) => {
|
|
421
455
|
try {
|
|
422
456
|
if (!options.force) {
|
|
423
|
-
const apiKey = await apiClient.get(
|
|
457
|
+
const apiKey = unwrapApiResponse(await apiClient.get(`${AUTH_API_KEYS_BASE}/${keyId}`));
|
|
424
458
|
const { confirm } = await inquirer.prompt([
|
|
425
459
|
{
|
|
426
460
|
type: 'confirm',
|
|
@@ -434,7 +468,7 @@ apiKeysCommand
|
|
|
434
468
|
return;
|
|
435
469
|
}
|
|
436
470
|
}
|
|
437
|
-
await apiClient.delete(
|
|
471
|
+
await apiClient.delete(`${AUTH_API_KEYS_BASE}/${keyId}`);
|
|
438
472
|
console.log(colors.success('🗑️ API key deleted successfully!'));
|
|
439
473
|
}
|
|
440
474
|
catch (error) {
|
|
@@ -564,7 +598,7 @@ mcpCommand
|
|
|
564
598
|
delete toolData.maxConcurrentSessions;
|
|
565
599
|
delete toolData.maxSessionDuration;
|
|
566
600
|
}
|
|
567
|
-
const tool = await apiClient.post(
|
|
601
|
+
const tool = unwrapApiResponse(await apiClient.post(`${AUTH_API_KEYS_BASE}/mcp/tools`, toolData));
|
|
568
602
|
console.log(colors.success('🤖 MCP tool registered successfully!'));
|
|
569
603
|
console.log(colors.info('━'.repeat(50)));
|
|
570
604
|
console.log(`${colors.highlight('Tool ID:')} ${colors.primary(tool.toolId)}`);
|
|
@@ -584,12 +618,12 @@ mcpCommand
|
|
|
584
618
|
.option('--json', 'Output as JSON')
|
|
585
619
|
.action(async (options) => {
|
|
586
620
|
try {
|
|
587
|
-
const tools = await apiClient.get(
|
|
621
|
+
const tools = unwrapApiResponse(await apiClient.get(`${AUTH_API_KEYS_BASE}/mcp/tools`));
|
|
588
622
|
if (options.json) {
|
|
589
623
|
console.log(JSON.stringify(tools, null, 2));
|
|
590
624
|
return;
|
|
591
625
|
}
|
|
592
|
-
if (tools.length === 0) {
|
|
626
|
+
if (!Array.isArray(tools) || tools.length === 0) {
|
|
593
627
|
console.log(colors.warning('⚠️ No MCP tools found'));
|
|
594
628
|
console.log(colors.muted('Run: lanonasis api-keys mcp register-tool'));
|
|
595
629
|
return;
|
|
@@ -644,7 +678,8 @@ mcpCommand
|
|
|
644
678
|
};
|
|
645
679
|
if (options.interactive || !requestData.toolId || !requestData.organizationId ||
|
|
646
680
|
requestData.keyNames.length === 0 || !requestData.environment || !requestData.justification) {
|
|
647
|
-
const
|
|
681
|
+
const mcpTools = unwrapApiResponse(await apiClient.get(`${AUTH_API_KEYS_BASE}/mcp/tools`));
|
|
682
|
+
const tools = Array.isArray(mcpTools) ? mcpTools : [];
|
|
648
683
|
const answers = await inquirer.prompt([
|
|
649
684
|
{
|
|
650
685
|
type: 'select',
|
|
@@ -695,7 +730,7 @@ mcpCommand
|
|
|
695
730
|
]);
|
|
696
731
|
requestData = { ...requestData, ...answers };
|
|
697
732
|
}
|
|
698
|
-
const response = await apiClient.post(
|
|
733
|
+
const response = unwrapApiResponse(await apiClient.post(`${AUTH_API_KEYS_BASE}/mcp/request-access`, requestData));
|
|
699
734
|
console.log(colors.success('🔐 Access request created successfully!'));
|
|
700
735
|
console.log(colors.info('━'.repeat(50)));
|
|
701
736
|
console.log(`${colors.highlight('Request ID:')} ${colors.primary(response.requestId)}`);
|
|
@@ -721,7 +756,7 @@ analyticsCommand
|
|
|
721
756
|
.option('--json', 'Output as JSON')
|
|
722
757
|
.action(async (options) => {
|
|
723
758
|
try {
|
|
724
|
-
let url =
|
|
759
|
+
let url = `${AUTH_API_KEYS_BASE}/analytics/usage`;
|
|
725
760
|
const params = new URLSearchParams();
|
|
726
761
|
if (options.keyId)
|
|
727
762
|
params.append('keyId', options.keyId);
|
|
@@ -730,12 +765,12 @@ analyticsCommand
|
|
|
730
765
|
if (params.toString()) {
|
|
731
766
|
url += `?${params.toString()}`;
|
|
732
767
|
}
|
|
733
|
-
const analytics = await apiClient.get(url);
|
|
768
|
+
const analytics = unwrapApiResponse(await apiClient.get(url));
|
|
734
769
|
if (options.json) {
|
|
735
770
|
console.log(JSON.stringify(analytics, null, 2));
|
|
736
771
|
return;
|
|
737
772
|
}
|
|
738
|
-
if (analytics.length === 0) {
|
|
773
|
+
if (!Array.isArray(analytics) || analytics.length === 0) {
|
|
739
774
|
console.log(chalk.yellow('No usage data found'));
|
|
740
775
|
return;
|
|
741
776
|
}
|
|
@@ -770,16 +805,16 @@ analyticsCommand
|
|
|
770
805
|
.option('--json', 'Output as JSON')
|
|
771
806
|
.action(async (options) => {
|
|
772
807
|
try {
|
|
773
|
-
let url =
|
|
808
|
+
let url = `${AUTH_API_KEYS_BASE}/analytics/security-events`;
|
|
774
809
|
if (options.severity) {
|
|
775
810
|
url += `?severity=${options.severity}`;
|
|
776
811
|
}
|
|
777
|
-
const events = await apiClient.get(url);
|
|
812
|
+
const events = unwrapApiResponse(await apiClient.get(url));
|
|
778
813
|
if (options.json) {
|
|
779
814
|
console.log(JSON.stringify(events, null, 2));
|
|
780
815
|
return;
|
|
781
816
|
}
|
|
782
|
-
if (events.length === 0) {
|
|
817
|
+
if (!Array.isArray(events) || events.length === 0) {
|
|
783
818
|
console.log(colors.success('✅ No security events found'));
|
|
784
819
|
return;
|
|
785
820
|
}
|
package/dist/commands/mcp.js
CHANGED
|
@@ -439,6 +439,12 @@ export function mcpCommands(program) {
|
|
|
439
439
|
if (options.args) {
|
|
440
440
|
try {
|
|
441
441
|
args = JSON.parse(options.args);
|
|
442
|
+
// Validate that args is a plain object (not array or primitive)
|
|
443
|
+
if (typeof args !== 'object' || args === null || Array.isArray(args)) {
|
|
444
|
+
spinner.fail('Arguments must be a JSON object, not an array or primitive value');
|
|
445
|
+
console.log(chalk.yellow('Example: --args \'{"key": "value"}\''));
|
|
446
|
+
process.exit(1);
|
|
447
|
+
}
|
|
442
448
|
}
|
|
443
449
|
catch {
|
|
444
450
|
spinner.fail('Invalid JSON arguments');
|
package/dist/commands/memory.js
CHANGED
|
@@ -103,12 +103,20 @@ const ensureBehaviorActions = (value, fieldName, options = {}) => {
|
|
|
103
103
|
if (!tool) {
|
|
104
104
|
throw new Error(`${fieldName}[${index}].tool is required`);
|
|
105
105
|
}
|
|
106
|
-
const
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
const outcome = typeof entry.outcome === 'string' ? entry.outcome.trim() : '';
|
|
107
|
+
if (!['success', 'partial', 'failed'].includes(outcome)) {
|
|
108
|
+
throw new Error(`${fieldName}[${index}].outcome must be success, partial, or failed`);
|
|
109
|
+
}
|
|
110
|
+
const parsed = {
|
|
111
|
+
tool,
|
|
112
|
+
outcome: outcome,
|
|
113
|
+
};
|
|
114
|
+
const rawParameters = entry.parameters ?? entry.params;
|
|
115
|
+
if (rawParameters !== undefined) {
|
|
116
|
+
if (!isPlainObject(rawParameters)) {
|
|
117
|
+
throw new Error(`${fieldName}[${index}].parameters must be a JSON object`);
|
|
110
118
|
}
|
|
111
|
-
parsed.
|
|
119
|
+
parsed.parameters = rawParameters;
|
|
112
120
|
}
|
|
113
121
|
if (entry.timestamp !== undefined) {
|
|
114
122
|
if (typeof entry.timestamp !== 'string' || !entry.timestamp.trim()) {
|
|
@@ -127,6 +135,33 @@ const ensureBehaviorActions = (value, fieldName, options = {}) => {
|
|
|
127
135
|
return parsed;
|
|
128
136
|
});
|
|
129
137
|
};
|
|
138
|
+
const ensureCompletedSteps = (value, fieldName) => {
|
|
139
|
+
if (value === undefined) {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
if (!Array.isArray(value)) {
|
|
143
|
+
throw new Error(`${fieldName} must be a JSON array`);
|
|
144
|
+
}
|
|
145
|
+
return value.map((entry, index) => {
|
|
146
|
+
if (typeof entry === 'string') {
|
|
147
|
+
const normalized = entry.trim();
|
|
148
|
+
if (!normalized) {
|
|
149
|
+
throw new Error(`${fieldName}[${index}] must be a non-empty string`);
|
|
150
|
+
}
|
|
151
|
+
return normalized;
|
|
152
|
+
}
|
|
153
|
+
if (isPlainObject(entry)) {
|
|
154
|
+
const tool = typeof entry.tool === 'string'
|
|
155
|
+
? entry.tool?.trim()
|
|
156
|
+
: '';
|
|
157
|
+
if (!tool) {
|
|
158
|
+
throw new Error(`${fieldName}[${index}].tool must be a non-empty string`);
|
|
159
|
+
}
|
|
160
|
+
return tool;
|
|
161
|
+
}
|
|
162
|
+
throw new Error(`${fieldName}[${index}] must be a string or an object with a tool field`);
|
|
163
|
+
});
|
|
164
|
+
};
|
|
130
165
|
const clampThreshold = (value) => {
|
|
131
166
|
if (!Number.isFinite(value))
|
|
132
167
|
return 0.55;
|
|
@@ -1215,7 +1250,7 @@ export function memoryCommands(program) {
|
|
|
1215
1250
|
.description('Suggest next actions from learned behavior patterns')
|
|
1216
1251
|
.requiredOption('--task <text>', 'Current task description')
|
|
1217
1252
|
.option('--state <json>', 'Additional current state JSON object')
|
|
1218
|
-
.option('--completed-steps <json>', 'Completed steps JSON array')
|
|
1253
|
+
.option('--completed-steps <json>', 'Completed steps JSON array (strings preferred)')
|
|
1219
1254
|
.option('--max-suggestions <number>', 'Maximum suggestions', '3')
|
|
1220
1255
|
.option('--json', 'Output raw JSON payload')
|
|
1221
1256
|
.action(async (options) => {
|
|
@@ -1226,9 +1261,7 @@ export function memoryCommands(program) {
|
|
|
1226
1261
|
const parsedState = parseJsonOption(options.state, '--state');
|
|
1227
1262
|
const state = ensureJsonObject(parsedState, '--state') || {};
|
|
1228
1263
|
const parsedCompletedSteps = parseJsonOption(options.completedSteps, '--completed-steps');
|
|
1229
|
-
const completedSteps = parsedCompletedSteps
|
|
1230
|
-
? undefined
|
|
1231
|
-
: ensureBehaviorActions(parsedCompletedSteps, '--completed-steps', { allowEmpty: true });
|
|
1264
|
+
const completedSteps = ensureCompletedSteps(parsedCompletedSteps, '--completed-steps');
|
|
1232
1265
|
const result = await postIntelligenceEndpoint(transport, '/intelligence/behavior-suggest', {
|
|
1233
1266
|
user_id: userId,
|
|
1234
1267
|
current_state: {
|
package/dist/index.js
CHANGED
|
@@ -280,10 +280,16 @@ authCmd
|
|
|
280
280
|
const profileClient = new APIClient();
|
|
281
281
|
profileClient.noExit = true;
|
|
282
282
|
const profile = await profileClient.getUserProfile();
|
|
283
|
+
await cliConfig.updateCurrentUserProfile(profile);
|
|
284
|
+
const organizationId = profile.organization_id || profile.organizationId;
|
|
283
285
|
console.log(`Email: ${profile.email}`);
|
|
284
286
|
if (profile.name)
|
|
285
287
|
console.log(`Name: ${profile.name}`);
|
|
288
|
+
if (organizationId)
|
|
289
|
+
console.log(`Organization: ${organizationId}`);
|
|
286
290
|
console.log(`Role: ${profile.role}`);
|
|
291
|
+
if (profile.plan)
|
|
292
|
+
console.log(`Plan: ${profile.plan}`);
|
|
287
293
|
if (profile.provider)
|
|
288
294
|
console.log(`Provider: ${profile.provider}`);
|
|
289
295
|
if (profile.last_sign_in_at) {
|
|
@@ -736,13 +742,21 @@ program
|
|
|
736
742
|
const profileClient = new APIClient();
|
|
737
743
|
profileClient.noExit = true;
|
|
738
744
|
const profile = await profileClient.getUserProfile();
|
|
745
|
+
await cliConfig.updateCurrentUserProfile(profile);
|
|
746
|
+
const organizationId = profile.organization_id || profile.organizationId;
|
|
739
747
|
console.log(chalk.blue.bold('👤 Current User'));
|
|
740
748
|
console.log('━'.repeat(40));
|
|
741
749
|
console.log(`Email: ${chalk.white(profile.email)}`);
|
|
742
750
|
if (profile.name) {
|
|
743
751
|
console.log(`Name: ${chalk.white(profile.name)}`);
|
|
744
752
|
}
|
|
753
|
+
if (organizationId) {
|
|
754
|
+
console.log(`Organization: ${chalk.white(organizationId)}`);
|
|
755
|
+
}
|
|
745
756
|
console.log(`Role: ${chalk.white(profile.role)}`);
|
|
757
|
+
if (profile.plan) {
|
|
758
|
+
console.log(`Plan: ${chalk.white(profile.plan)}`);
|
|
759
|
+
}
|
|
746
760
|
if (profile.provider) {
|
|
747
761
|
console.log(`Provider: ${chalk.white(profile.provider)}`);
|
|
748
762
|
}
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -169,7 +169,10 @@ export interface UserProfile {
|
|
|
169
169
|
email: string;
|
|
170
170
|
name: string | null;
|
|
171
171
|
avatar_url: string | null;
|
|
172
|
+
organization_id?: string | null;
|
|
173
|
+
organizationId?: string | null;
|
|
172
174
|
role: string;
|
|
175
|
+
plan?: string | null;
|
|
173
176
|
provider: string | null;
|
|
174
177
|
project_scope: string | null;
|
|
175
178
|
platform: string | null;
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -52,6 +52,9 @@ export declare class CLIConfig {
|
|
|
52
52
|
private vendorKeyCache?;
|
|
53
53
|
private isLegacyHashedCredential;
|
|
54
54
|
private getLegacyHashedVendorKeyReason;
|
|
55
|
+
private normalizeOptionalString;
|
|
56
|
+
private extractOrganizationId;
|
|
57
|
+
private buildUserProfile;
|
|
55
58
|
constructor();
|
|
56
59
|
private getApiKeyStorage;
|
|
57
60
|
/**
|
|
@@ -120,6 +123,7 @@ export declare class CLIConfig {
|
|
|
120
123
|
getToken(): string | undefined;
|
|
121
124
|
getAuthMethod(): string | undefined;
|
|
122
125
|
getCurrentUser(): Promise<UserProfile | undefined>;
|
|
126
|
+
updateCurrentUserProfile(profile: Record<string, unknown>): Promise<void>;
|
|
123
127
|
isAuthenticated(): Promise<boolean>;
|
|
124
128
|
logout(): Promise<void>;
|
|
125
129
|
clear(): Promise<void>;
|
package/dist/utils/config.js
CHANGED
|
@@ -21,6 +21,35 @@ export class CLIConfig {
|
|
|
21
21
|
getLegacyHashedVendorKeyReason() {
|
|
22
22
|
return 'Stored vendor key is in legacy hashed format. Run "lanonasis auth login --vendor-key <your-key>" to refresh secure storage.';
|
|
23
23
|
}
|
|
24
|
+
normalizeOptionalString(value) {
|
|
25
|
+
if (typeof value === 'string') {
|
|
26
|
+
const trimmed = value.trim();
|
|
27
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
28
|
+
}
|
|
29
|
+
if (value === null || value === undefined) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return String(value);
|
|
33
|
+
}
|
|
34
|
+
extractOrganizationId(source) {
|
|
35
|
+
return this.normalizeOptionalString(source.organization_id)
|
|
36
|
+
?? this.normalizeOptionalString(source.organizationId);
|
|
37
|
+
}
|
|
38
|
+
buildUserProfile(source, existing) {
|
|
39
|
+
const email = this.normalizeOptionalString(source.email) ?? existing?.email ?? '';
|
|
40
|
+
const organization_id = this.extractOrganizationId(source) ?? existing?.organization_id ?? '';
|
|
41
|
+
const role = this.normalizeOptionalString(source.role) ?? existing?.role ?? '';
|
|
42
|
+
const plan = this.normalizeOptionalString(source.plan) ?? existing?.plan ?? '';
|
|
43
|
+
if (!email && !organization_id && !role && !plan) {
|
|
44
|
+
return existing;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
email,
|
|
48
|
+
organization_id,
|
|
49
|
+
role,
|
|
50
|
+
plan
|
|
51
|
+
};
|
|
52
|
+
}
|
|
24
53
|
constructor() {
|
|
25
54
|
this.configDir = path.join(os.homedir(), '.maas');
|
|
26
55
|
this.configPath = path.join(this.configDir, 'config.json');
|
|
@@ -897,12 +926,7 @@ export class CLIConfig {
|
|
|
897
926
|
this.config.tokenExpiry = decoded.exp;
|
|
898
927
|
}
|
|
899
928
|
// Store user info
|
|
900
|
-
this.config.user =
|
|
901
|
-
email: String(decoded.email || ''),
|
|
902
|
-
organization_id: String(decoded.organizationId || ''),
|
|
903
|
-
role: String(decoded.role || ''),
|
|
904
|
-
plan: String(decoded.plan || '')
|
|
905
|
-
};
|
|
929
|
+
this.config.user = this.buildUserProfile(decoded, this.config.user);
|
|
906
930
|
}
|
|
907
931
|
catch {
|
|
908
932
|
// Invalid token, don't store user info or expiry
|
|
@@ -921,6 +945,23 @@ export class CLIConfig {
|
|
|
921
945
|
async getCurrentUser() {
|
|
922
946
|
return this.config.user;
|
|
923
947
|
}
|
|
948
|
+
async updateCurrentUserProfile(profile) {
|
|
949
|
+
const nextUser = this.buildUserProfile(profile, this.config.user);
|
|
950
|
+
if (!nextUser) {
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
const currentUser = this.config.user;
|
|
954
|
+
const changed = !currentUser
|
|
955
|
+
|| currentUser.email !== nextUser.email
|
|
956
|
+
|| currentUser.organization_id !== nextUser.organization_id
|
|
957
|
+
|| currentUser.role !== nextUser.role
|
|
958
|
+
|| currentUser.plan !== nextUser.plan;
|
|
959
|
+
if (!changed) {
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
this.config.user = nextUser;
|
|
963
|
+
await this.save();
|
|
964
|
+
}
|
|
924
965
|
async isAuthenticated() {
|
|
925
966
|
// Attempt refresh for OAuth sessions before checks (prevents intermittent auth dropouts).
|
|
926
967
|
// This is safe to call even when not using OAuth; it will no-op.
|
|
@@ -51,9 +51,21 @@ export class ConnectionManagerImpl {
|
|
|
51
51
|
try {
|
|
52
52
|
// Load persisted configuration first
|
|
53
53
|
await this.loadConfig();
|
|
54
|
-
//
|
|
54
|
+
// Prefer a configured path only if the file exists (ignore stale machine-specific commits)
|
|
55
55
|
const configuredPath = this.config.localServerPath?.trim();
|
|
56
|
-
|
|
56
|
+
let serverPath = null;
|
|
57
|
+
if (configuredPath) {
|
|
58
|
+
try {
|
|
59
|
+
await fs.access(configuredPath);
|
|
60
|
+
serverPath = configuredPath;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
serverPath = await this.detectServerPath();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
serverPath = await this.detectServerPath();
|
|
68
|
+
}
|
|
57
69
|
if (!serverPath) {
|
|
58
70
|
return {
|
|
59
71
|
success: false,
|
package/package.json
CHANGED