@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.
@@ -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 project = await apiClient.post('/api-keys/projects', projectData);
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('/api-keys/projects');
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('-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')
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
- 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)
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 || !keyData.value || !keyData.projectId) {
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: '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: '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: 'accessLevel',
183
+ name: 'access_level',
196
184
  message: 'Access level:',
197
- choices: ['public', 'authenticated', 'team', 'admin', 'enterprise'],
198
- default: 'team'
185
+ choices: VALID_ACCESS_LEVELS,
186
+ default: keyData.access_level
199
187
  },
200
188
  {
201
- type: 'input',
202
- name: 'tags',
203
- message: 'Tags (comma-separated, optional):',
204
- filter: (input) => input ? input.split(',').map((tag) => tag.trim()) : []
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: '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'
197
+ name: 'scopes',
198
+ message: 'Scopes (comma-separated, optional):',
199
+ default: (keyData.scopes || []).join(', ')
224
200
  }
225
201
  ]);
226
- keyData = { ...keyData, ...answers };
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('/api-keys', keyData);
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('Type:')} ${colors.info(apiKey.keyType)}`);
234
- console.log(`${colors.highlight('Environment:')} ${colors.accent(apiKey.environment)}`);
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
- console.log(colors.warning('⚠️ The key value is securely encrypted and cannot be retrieved later.'));
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('-p, --project-id <id>', 'Filter by project ID')
243
+ .option('--all', 'Include inactive keys')
249
244
  .option('--json', 'Output as JSON')
250
245
  .action(async (options) => {
251
246
  try {
252
- let url = '/api-keys';
253
- if (options.projectId) {
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', 'Type', 'Environment', 'Status', 'Usage', 'Last Rotated'].map(h => colors.accent(h)),
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.status === 'active' ? colors.success :
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.keyType,
279
- key.environment,
280
- statusColor(key.status),
281
- colors.highlight(key.usageCount.toString()),
282
- formatDate(key.lastRotated)
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(`/api-keys/${keyId}`);
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
- console.log(`${colors.highlight('Type:')} ${colors.info(apiKey.keyType)}`);
312
- console.log(`${colors.highlight('Environment:')} ${colors.accent(apiKey.environment)}`);
313
- console.log(`${colors.highlight('Project ID:')} ${colors.muted(apiKey.projectId)}`);
314
- console.log(`${colors.highlight('Access Level:')} ${colors.warning(apiKey.accessLevel)}`);
315
- const statusColor = apiKey.status === 'active' ? colors.success :
316
- apiKey.status === 'rotating' ? colors.warning : colors.error;
317
- console.log(`${colors.highlight('Status:')} ${statusColor(apiKey.status)}`);
318
- console.log(`${colors.highlight('Usage Count:')} ${colors.accent(apiKey.usageCount)}`);
319
- console.log(`${colors.highlight('Tags:')} ${colors.muted(apiKey.tags.join(', ') || 'None')}`);
320
- console.log(`${colors.highlight('Rotation Frequency:')} ${colors.info(apiKey.rotationFrequency)} days`);
321
- console.log(`${colors.highlight('Last Rotated:')} ${colors.muted(formatDate(apiKey.lastRotated))}`);
322
- console.log(`${colors.highlight('Created:')} ${colors.muted(formatDate(apiKey.createdAt))}`);
323
- console.log(`${colors.highlight('Updated:')} ${colors.muted(formatDate(apiKey.updatedAt))}`);
324
- if (apiKey.expiresAt) {
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('-v, --value <value>', 'New value')
341
- .option('--tags <tags>', 'Comma-separated tags')
342
- .option('--rotation-frequency <days>', 'Rotation frequency in days')
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.value)
350
- updateData.value = options.value;
351
- if (options.tags)
352
- updateData.tags = options.tags.split(',').map((tag) => tag.trim());
353
- if (options.rotationFrequency)
354
- updateData.rotationFrequency = parseInt(options.rotationFrequency);
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(`/api-keys/${keyId}`);
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: 'updateValue',
368
- message: 'Update the key value?',
388
+ name: 'changeExpiry',
389
+ message: 'Change expiry?',
369
390
  default: false,
370
- when: !updateData.value
391
+ when: updateData.expires_in_days === undefined && !updateData.clear_expiry
371
392
  },
372
393
  {
373
- type: 'password',
374
- name: 'value',
375
- message: 'New key value:',
376
- when: (answers) => answers.updateValue
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: 'input',
380
- name: 'tags',
381
- message: 'Tags (comma-separated):',
382
- default: current.tags.join(', '),
383
- filter: (input) => input.split(',').map((tag) => tag.trim()),
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: 'number',
388
- name: 'rotationFrequency',
389
- message: 'Rotation frequency (days):',
390
- default: current.rotationFrequency,
391
- validate: (input) => input > 0 && input <= 365 || 'Must be between 1 and 365 days',
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 = { ...updateData, ...answers };
396
- delete updateData.updateValue;
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(`/api-keys/${keyId}`, updateData);
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
- console.log(`${colors.highlight('Status:')} ${colors.success(updatedKey.status)}`);
403
- if (updateData.value) {
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(`/api-keys/${keyId}`);
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(`/api-keys/${keyId}`);
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('/api-keys/mcp/tools', toolData);
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('/api-keys/mcp/tools');
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 tools = await apiClient.get('/api-keys/mcp/tools');
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('/api-keys/mcp/request-access', requestData);
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 = '/api-keys/analytics/usage';
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 = '/api-keys/analytics/security-events';
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
  }
@@ -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');
@@ -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 parsed = { tool };
107
- if (entry.params !== undefined) {
108
- if (!isPlainObject(entry.params)) {
109
- throw new Error(`${fieldName}[${index}].params must be a JSON object`);
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.params = entry.params;
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 === undefined
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
  }
@@ -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;
@@ -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>;
@@ -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
- // First, try to detect the server path
54
+ // Prefer a configured path only if the file exists (ignore stale machine-specific commits)
55
55
  const configuredPath = this.config.localServerPath?.trim();
56
- const serverPath = configuredPath || (await this.detectServerPath());
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lanonasis/cli",
3
- "version": "3.9.11",
3
+ "version": "3.9.12",
4
4
  "description": "Professional CLI for LanOnasis Memory as a Service (MaaS) with MCP support, seamless inline editing, and enterprise-grade security",
5
5
  "keywords": [
6
6
  "lanonasis",