@lanonasis/cli 3.9.13 → 3.9.14

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog - @lanonasis/cli
2
2
 
3
+ ## [3.9.14] - 2026-04-03
4
+
5
+ ### 🐛 Bug Fixes
6
+
7
+ - **Platform key project commands now hit the live auth-gateway contract**: `api-keys projects create/list` now use `/api/v1/projects` and route to `auth.lanonasis.com`, matching the current auth-gateway surface instead of the stale `/api/v1/auth/api-keys/projects` path.
8
+ - **Auth-gateway management calls no longer drift to the MCP/API host**: The CLI transport now treats `/api/v1/auth/api-keys/*` and `/api/v1/projects/*` as auth-gateway management endpoints, so token-authenticated key management requests consistently reach the auth service with the expected project scope header.
9
+ - **Stale MCP and analytics subcommands now fail honestly**: Deprecated `api-keys mcp *` and `api-keys analytics *` commands no longer call nonexistent gateway routes. They now exit immediately with explicit guidance toward the supported dashboard and service-scoping paths.
10
+
11
+ ### ✨ Improvements
12
+
13
+ - **`key_context` is now exposed in CLI key management**: `api-keys create` accepts `--key-context personal|team|enterprise`, and `api-keys list/get` now display the stored key context so context-bound platform keys are visible and intentional at the CLI layer.
14
+
3
15
  ## [3.9.13] - 2026-04-02
4
16
 
5
17
  ### 🐛 Bug Fixes
@@ -16,7 +16,9 @@ const colors = {
16
16
  highlight: chalk.white.bold
17
17
  };
18
18
  const AUTH_API_KEYS_BASE = '/api/v1/auth/api-keys';
19
+ const PROJECTS_API_BASE = '/api/v1/projects';
19
20
  const VALID_ACCESS_LEVELS = ['public', 'authenticated', 'team', 'admin', 'enterprise'];
21
+ const VALID_KEY_CONTEXTS = ['personal', 'team', 'enterprise'];
20
22
  function unwrapApiResponse(response) {
21
23
  if (response && typeof response === 'object' && 'data' in response) {
22
24
  return response.data ?? response;
@@ -33,6 +35,26 @@ function parseScopes(scopes) {
33
35
  .filter(Boolean);
34
36
  return parsed.length > 0 ? parsed : undefined;
35
37
  }
38
+ function parseKeyContext(keyContext) {
39
+ if (!keyContext) {
40
+ return undefined;
41
+ }
42
+ const normalized = keyContext.trim().toLowerCase();
43
+ if (!normalized) {
44
+ return undefined;
45
+ }
46
+ if (VALID_KEY_CONTEXTS.includes(normalized)) {
47
+ return normalized;
48
+ }
49
+ throw new Error('Invalid key context. Allowed: personal, team, enterprise');
50
+ }
51
+ function exitUnsupported(feature, guidance) {
52
+ console.error(colors.error(`✖ ${feature} is not exposed by the current auth-gateway API.`));
53
+ for (const line of guidance) {
54
+ console.error(colors.muted(` • ${line}`));
55
+ }
56
+ process.exit(1);
57
+ }
36
58
  const apiKeysCommand = new Command('api-keys')
37
59
  .alias('keys')
38
60
  .description(colors.info('🔐 Manage API keys securely with enterprise-grade encryption'));
@@ -83,7 +105,7 @@ projectsCommand
83
105
  ]);
84
106
  projectData = { ...projectData, ...answers };
85
107
  }
86
- const projectRes = await apiClient.post(`${AUTH_API_KEYS_BASE}/projects`, projectData);
108
+ const projectRes = await apiClient.post(PROJECTS_API_BASE, projectData);
87
109
  const project = unwrapApiResponse(projectRes);
88
110
  console.log(chalk.green('✅ Project created successfully!'));
89
111
  console.log(chalk.blue(`Project ID: ${project.id}`));
@@ -104,7 +126,7 @@ projectsCommand
104
126
  .option('--json', 'Output as JSON')
105
127
  .action(async (options) => {
106
128
  try {
107
- const projects = unwrapApiResponse(await apiClient.get(`${AUTH_API_KEYS_BASE}/projects`));
129
+ const projects = unwrapApiResponse(await apiClient.get(PROJECTS_API_BASE));
108
130
  if (options.json) {
109
131
  console.log(JSON.stringify(projects, null, 2));
110
132
  return;
@@ -143,12 +165,14 @@ apiKeysCommand
143
165
  .option('-n, --name <name>', 'API key name')
144
166
  .option('-d, --description <description>', 'API key description (optional)')
145
167
  .option('--access-level <level>', 'Access level (public, authenticated, team, admin, enterprise)', 'team')
168
+ .option('--key-context <context>', 'Optional memory context (personal, team, enterprise)')
146
169
  .option('--expires-in-days <days>', 'Expiration in days (default: 365)', '365')
147
170
  .option('--scopes <scopes>', 'Comma-separated scopes (optional)')
148
171
  .option('--interactive', 'Interactive mode')
149
172
  .action(async (options) => {
150
173
  try {
151
174
  const accessLevel = (options.accessLevel || 'team').toLowerCase();
175
+ const keyContext = parseKeyContext(options.keyContext);
152
176
  const expiresInDays = parseInt(options.expiresInDays, 10);
153
177
  if (!VALID_ACCESS_LEVELS.includes(accessLevel)) {
154
178
  throw new Error('Invalid access level. Allowed: public, authenticated, team, admin, enterprise');
@@ -159,6 +183,7 @@ apiKeysCommand
159
183
  let keyData = {
160
184
  name: options.name,
161
185
  access_level: accessLevel,
186
+ key_context: keyContext,
162
187
  expires_in_days: expiresInDays,
163
188
  description: options.description?.trim() || undefined,
164
189
  scopes: parseScopes(options.scopes)
@@ -185,6 +210,16 @@ apiKeysCommand
185
210
  choices: VALID_ACCESS_LEVELS,
186
211
  default: keyData.access_level
187
212
  },
213
+ {
214
+ type: 'select',
215
+ name: 'key_context',
216
+ message: 'Memory context:',
217
+ choices: [
218
+ { name: 'legacy / unbounded (default)', value: '' },
219
+ ...VALID_KEY_CONTEXTS.map((context) => ({ name: context, value: context })),
220
+ ],
221
+ default: keyData.key_context || ''
222
+ },
188
223
  {
189
224
  type: 'number',
190
225
  name: 'expires_in_days',
@@ -202,6 +237,7 @@ apiKeysCommand
202
237
  keyData = {
203
238
  ...keyData,
204
239
  ...answers,
240
+ key_context: parseKeyContext(typeof answers.key_context === 'string' ? answers.key_context : undefined) ?? keyData.key_context,
205
241
  description: typeof answers.description === 'string'
206
242
  ? answers.description.trim() || undefined
207
243
  : keyData.description,
@@ -214,6 +250,7 @@ apiKeysCommand
214
250
  console.log(`${colors.highlight('Key ID:')} ${colors.primary(apiKey.id)}`);
215
251
  console.log(`${colors.highlight('Name:')} ${colors.accent(apiKey.name)}`);
216
252
  console.log(`${colors.highlight('Access Level:')} ${colors.info(apiKey.access_level || keyData.access_level)}`);
253
+ console.log(`${colors.highlight('Key Context:')} ${colors.info(apiKey.key_context || keyData.key_context || 'legacy')}`);
217
254
  console.log(`${colors.highlight('Permissions:')} ${colors.muted((apiKey.permissions || keyData.scopes || []).join(', ') || 'legacy:full_access')}`);
218
255
  if (apiKey.expires_at) {
219
256
  console.log(`${colors.highlight('Expires At:')} ${colors.warning(formatDate(apiKey.expires_at))}`);
@@ -258,7 +295,7 @@ apiKeysCommand
258
295
  console.log(colors.primary('🔐 API Key Management'));
259
296
  console.log(colors.info('═'.repeat(80)));
260
297
  const table = new Table({
261
- head: ['ID', 'Name', 'Access', 'Permissions', 'Service', 'Status', 'Expires'].map(h => colors.accent(h)),
298
+ head: ['ID', 'Name', 'Access', 'Context', 'Permissions', 'Service', 'Status', 'Expires'].map(h => colors.accent(h)),
262
299
  style: { head: [], border: [] }
263
300
  });
264
301
  apiKeys.forEach((key) => {
@@ -267,6 +304,7 @@ apiKeysCommand
267
304
  truncateText(key.id, 20),
268
305
  key.name,
269
306
  key.access_level,
307
+ key.key_context || 'legacy',
270
308
  truncateText((key.permissions || []).join(', ') || 'legacy:full_access', 28),
271
309
  key.service || 'all',
272
310
  statusColor(key.is_active ? 'active' : 'inactive'),
@@ -303,6 +341,7 @@ apiKeysCommand
303
341
  console.log(`${colors.highlight('Description:')} ${colors.muted(apiKey.description)}`);
304
342
  }
305
343
  console.log(`${colors.highlight('Access Level:')} ${colors.warning(apiKey.access_level)}`);
344
+ console.log(`${colors.highlight('Key Context:')} ${colors.info(apiKey.key_context || 'legacy')}`);
306
345
  console.log(`${colors.highlight('Permissions:')} ${colors.muted((apiKey.permissions || []).join(', ') || 'legacy:full_access')}`);
307
346
  console.log(`${colors.highlight('Service Scope:')} ${colors.info(apiKey.service || 'all')}`);
308
347
  const statusColor = apiKey.is_active ? colors.success : colors.error;
@@ -496,164 +535,23 @@ mcpCommand
496
535
  .option('--risk-level <level>', 'Risk level (low, medium, high, critical)', 'medium')
497
536
  .option('--interactive', 'Interactive mode')
498
537
  .action(async (options) => {
499
- try {
500
- let toolData = {
501
- toolId: options.toolId,
502
- toolName: options.toolName,
503
- organizationId: options.organizationId,
504
- permissions: {
505
- keys: options.keys ? options.keys.split(',').map((k) => k.trim()) : [],
506
- environments: options.environments ? options.environments.split(',').map((e) => e.trim()) : ['development'],
507
- maxConcurrentSessions: parseInt(options.maxSessions),
508
- maxSessionDuration: parseInt(options.maxDuration)
509
- },
510
- webhookUrl: options.webhookUrl,
511
- autoApprove: options.autoApprove || false,
512
- riskLevel: options.riskLevel
513
- };
514
- if (options.interactive || !toolData.toolId || !toolData.toolName || !toolData.organizationId) {
515
- const answers = await inquirer.prompt([
516
- {
517
- type: 'input',
518
- name: 'toolId',
519
- message: 'Tool ID:',
520
- when: !toolData.toolId,
521
- validate: (input) => input.length > 0 || 'Tool ID is required'
522
- },
523
- {
524
- type: 'input',
525
- name: 'toolName',
526
- message: 'Tool name:',
527
- when: !toolData.toolName,
528
- validate: (input) => input.length > 0 || 'Tool name is required'
529
- },
530
- {
531
- type: 'input',
532
- name: 'organizationId',
533
- message: 'Organization ID:',
534
- when: !toolData.organizationId,
535
- validate: (input) => {
536
- 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;
537
- return uuidRegex.test(input) || 'Please enter a valid UUID';
538
- }
539
- },
540
- {
541
- type: 'input',
542
- name: 'keys',
543
- message: 'Accessible key names (comma-separated):',
544
- filter: (input) => input.split(',').map((k) => k.trim()),
545
- when: toolData.permissions.keys.length === 0
546
- },
547
- {
548
- type: 'checkbox',
549
- name: 'environments',
550
- message: 'Accessible environments:',
551
- choices: ['development', 'staging', 'production'],
552
- default: ['development']
553
- },
554
- {
555
- type: 'number',
556
- name: 'maxConcurrentSessions',
557
- message: 'Maximum concurrent sessions:',
558
- default: 3,
559
- validate: (input) => input > 0 && input <= 10 || 'Must be between 1 and 10'
560
- },
561
- {
562
- type: 'number',
563
- name: 'maxSessionDuration',
564
- message: 'Maximum session duration (seconds):',
565
- default: 900,
566
- validate: (input) => input >= 60 && input <= 3600 || 'Must be between 60 and 3600 seconds'
567
- },
568
- {
569
- type: 'input',
570
- name: 'webhookUrl',
571
- message: 'Webhook URL (optional):'
572
- },
573
- {
574
- type: 'confirm',
575
- name: 'autoApprove',
576
- message: 'Enable auto-approval?',
577
- default: false
578
- },
579
- {
580
- type: 'select',
581
- name: 'riskLevel',
582
- message: 'Risk level:',
583
- choices: ['low', 'medium', 'high', 'critical'],
584
- default: 'medium'
585
- }
586
- ]);
587
- if (answers.keys)
588
- toolData.permissions.keys = answers.keys;
589
- if (answers.environments)
590
- toolData.permissions.environments = answers.environments;
591
- if (answers.maxConcurrentSessions)
592
- toolData.permissions.maxConcurrentSessions = answers.maxConcurrentSessions;
593
- if (answers.maxSessionDuration)
594
- toolData.permissions.maxSessionDuration = answers.maxSessionDuration;
595
- toolData = { ...toolData, ...answers };
596
- delete toolData.keys;
597
- delete toolData.environments;
598
- delete toolData.maxConcurrentSessions;
599
- delete toolData.maxSessionDuration;
600
- }
601
- const tool = unwrapApiResponse(await apiClient.post(`${AUTH_API_KEYS_BASE}/mcp/tools`, toolData));
602
- console.log(colors.success('🤖 MCP tool registered successfully!'));
603
- console.log(colors.info('━'.repeat(50)));
604
- console.log(`${colors.highlight('Tool ID:')} ${colors.primary(tool.toolId)}`);
605
- console.log(`${colors.highlight('Name:')} ${colors.accent(tool.toolName)}`);
606
- console.log(`${colors.highlight('Risk Level:')} ${colors.warning(tool.riskLevel)}`);
607
- console.log(`${colors.highlight('Auto Approve:')} ${tool.autoApprove ? colors.success('Yes') : colors.error('No')}`);
608
- console.log(colors.info('━'.repeat(50)));
609
- }
610
- catch (error) {
611
- console.error(colors.error('✖ Failed to register MCP tool:'), colors.muted(error.message));
612
- process.exit(1);
613
- }
538
+ void options;
539
+ exitUnsupported('MCP tool registration', [
540
+ 'No /api/v1/auth/api-keys/mcp/* routes exist on the current auth-gateway.',
541
+ 'Use API key service scoping instead: lanonasis api-keys create or update plus /service-scopes support on the gateway.',
542
+ 'Manage MCP-specific workflows from the dashboard until dedicated routes are exposed.',
543
+ ]);
614
544
  });
615
545
  mcpCommand
616
546
  .command('list-tools')
617
547
  .description('List registered MCP tools')
618
548
  .option('--json', 'Output as JSON')
619
549
  .action(async (options) => {
620
- try {
621
- const tools = unwrapApiResponse(await apiClient.get(`${AUTH_API_KEYS_BASE}/mcp/tools`));
622
- if (options.json) {
623
- console.log(JSON.stringify(tools, null, 2));
624
- return;
625
- }
626
- if (!Array.isArray(tools) || tools.length === 0) {
627
- console.log(colors.warning('⚠️ No MCP tools found'));
628
- console.log(colors.muted('Run: lanonasis api-keys mcp register-tool'));
629
- return;
630
- }
631
- console.log(colors.primary('🤖 Registered MCP Tools'));
632
- console.log(colors.info('═'.repeat(80)));
633
- const table = new Table({
634
- head: ['Tool ID', 'Name', 'Risk Level', 'Status', 'Auto Approve', 'Created'].map(h => colors.accent(h)),
635
- style: { head: [], border: [] }
636
- });
637
- tools.forEach((tool) => {
638
- const statusColor = tool.status === 'active' ? colors.success :
639
- tool.status === 'suspended' ? colors.error : colors.warning;
640
- table.push([
641
- tool.toolId,
642
- tool.toolName,
643
- tool.riskLevel,
644
- statusColor(tool.status),
645
- tool.autoApprove ? colors.success('Yes') : colors.error('No'),
646
- formatDate(tool.createdAt)
647
- ]);
648
- });
649
- console.log(table.toString());
650
- console.log(colors.info('═'.repeat(80)));
651
- console.log(colors.muted(`🤖 Total: ${colors.highlight(tools.length)} MCP tools`));
652
- }
653
- catch (error) {
654
- console.error(colors.error('✖ Failed to list MCP tools:'), colors.muted(error.message));
655
- process.exit(1);
656
- }
550
+ void options;
551
+ exitUnsupported('MCP tool listing', [
552
+ 'No /api/v1/auth/api-keys/mcp/* routes exist on the current auth-gateway.',
553
+ 'The current gateway exposes configured external services at /api/v1/auth/api-keys/services/configured.',
554
+ ]);
657
555
  });
658
556
  mcpCommand
659
557
  .command('request-access')
@@ -666,82 +564,11 @@ mcpCommand
666
564
  .option('--duration <seconds>', 'Estimated duration in seconds', '900')
667
565
  .option('--interactive', 'Interactive mode')
668
566
  .action(async (options) => {
669
- try {
670
- let requestData = {
671
- toolId: options.toolId,
672
- organizationId: options.organizationId,
673
- keyNames: options.keys ? options.keys.split(',').map((k) => k.trim()) : [],
674
- environment: options.environment,
675
- justification: options.justification,
676
- estimatedDuration: parseInt(options.duration),
677
- context: {}
678
- };
679
- if (options.interactive || !requestData.toolId || !requestData.organizationId ||
680
- requestData.keyNames.length === 0 || !requestData.environment || !requestData.justification) {
681
- const mcpTools = unwrapApiResponse(await apiClient.get(`${AUTH_API_KEYS_BASE}/mcp/tools`));
682
- const tools = Array.isArray(mcpTools) ? mcpTools : [];
683
- const answers = await inquirer.prompt([
684
- {
685
- type: 'select',
686
- name: 'toolId',
687
- message: 'Select MCP tool:',
688
- when: !requestData.toolId && tools.length > 0,
689
- choices: tools.map((t) => ({ name: `${t.toolName} (${t.toolId})`, value: t.toolId }))
690
- },
691
- {
692
- type: 'input',
693
- name: 'organizationId',
694
- message: 'Organization ID:',
695
- when: !requestData.organizationId,
696
- validate: (input) => {
697
- 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;
698
- return uuidRegex.test(input) || 'Please enter a valid UUID';
699
- }
700
- },
701
- {
702
- type: 'input',
703
- name: 'keyNames',
704
- message: 'Key names to access (comma-separated):',
705
- when: requestData.keyNames.length === 0,
706
- filter: (input) => input.split(',').map((k) => k.trim()),
707
- validate: (input) => input.length > 0 || 'At least one key name is required'
708
- },
709
- {
710
- type: 'select',
711
- name: 'environment',
712
- message: 'Environment:',
713
- when: !requestData.environment,
714
- choices: ['development', 'staging', 'production']
715
- },
716
- {
717
- type: 'input',
718
- name: 'justification',
719
- message: 'Justification for access:',
720
- when: !requestData.justification,
721
- validate: (input) => input.length > 0 || 'Justification is required'
722
- },
723
- {
724
- type: 'number',
725
- name: 'estimatedDuration',
726
- message: 'Estimated duration (seconds):',
727
- default: 900,
728
- validate: (input) => input >= 60 && input <= 3600 || 'Must be between 60 and 3600 seconds'
729
- }
730
- ]);
731
- requestData = { ...requestData, ...answers };
732
- }
733
- const response = unwrapApiResponse(await apiClient.post(`${AUTH_API_KEYS_BASE}/mcp/request-access`, requestData));
734
- console.log(colors.success('🔐 Access request created successfully!'));
735
- console.log(colors.info('━'.repeat(50)));
736
- console.log(`${colors.highlight('Request ID:')} ${colors.primary(response.requestId)}`);
737
- console.log(`${colors.highlight('Status:')} ${colors.accent(response.status)}`);
738
- console.log(colors.info('━'.repeat(50)));
739
- console.log(colors.warning('💡 Check the status with: lanonasis api-keys analytics usage'));
740
- }
741
- catch (error) {
742
- console.error(colors.error('✖ Failed to create access request:'), colors.muted(error.message));
743
- process.exit(1);
744
- }
567
+ void options;
568
+ exitUnsupported('MCP access requests', [
569
+ 'No /api/v1/auth/api-keys/mcp/* routes exist on the current auth-gateway.',
570
+ 'Use platform API keys plus service scoping for current gateway-backed access control.',
571
+ ]);
745
572
  });
746
573
  // ============================================================================
747
574
  // ANALYTICS COMMANDS
@@ -755,48 +582,11 @@ analyticsCommand
755
582
  .option('--days <days>', 'Number of days to look back', '30')
756
583
  .option('--json', 'Output as JSON')
757
584
  .action(async (options) => {
758
- try {
759
- let url = `${AUTH_API_KEYS_BASE}/analytics/usage`;
760
- const params = new URLSearchParams();
761
- if (options.keyId)
762
- params.append('keyId', options.keyId);
763
- if (options.days)
764
- params.append('days', options.days);
765
- if (params.toString()) {
766
- url += `?${params.toString()}`;
767
- }
768
- const analytics = unwrapApiResponse(await apiClient.get(url));
769
- if (options.json) {
770
- console.log(JSON.stringify(analytics, null, 2));
771
- return;
772
- }
773
- if (!Array.isArray(analytics) || analytics.length === 0) {
774
- console.log(chalk.yellow('No usage data found'));
775
- return;
776
- }
777
- const table = new Table({
778
- head: ['Key ID', 'Operation', 'Tool ID', 'Success', 'Timestamp'].map(h => chalk.cyan(h)),
779
- style: { head: [], border: [] }
780
- });
781
- analytics.forEach((entry) => {
782
- const successColor = entry.success ? colors.success('✓') : colors.error('✗');
783
- table.push([
784
- truncateText(entry.keyId || '-', 20),
785
- entry.operation,
786
- truncateText(entry.toolId || '-', 15),
787
- successColor,
788
- formatDate(entry.timestamp)
789
- ]);
790
- });
791
- console.log(table.toString());
792
- console.log(colors.info('═'.repeat(80)));
793
- console.log(colors.muted(`📈 Total: ${colors.highlight(analytics.length)} events`));
794
- }
795
- catch (error) {
796
- const errorMessage = error instanceof Error ? error.message : String(error);
797
- console.error(colors.error('✖ Failed to get usage analytics:'), colors.muted(errorMessage));
798
- process.exit(1);
799
- }
585
+ void options;
586
+ exitUnsupported('API key usage analytics', [
587
+ 'No /api/v1/auth/api-keys/analytics/* routes exist on the current auth-gateway.',
588
+ 'Use dashboard reporting or direct platform logs until analytics endpoints are exposed.',
589
+ ]);
800
590
  });
801
591
  analyticsCommand
802
592
  .command('security-events')
@@ -804,47 +594,11 @@ analyticsCommand
804
594
  .option('--severity <level>', 'Filter by severity (low, medium, high, critical)')
805
595
  .option('--json', 'Output as JSON')
806
596
  .action(async (options) => {
807
- try {
808
- let url = `${AUTH_API_KEYS_BASE}/analytics/security-events`;
809
- if (options.severity) {
810
- url += `?severity=${options.severity}`;
811
- }
812
- const events = unwrapApiResponse(await apiClient.get(url));
813
- if (options.json) {
814
- console.log(JSON.stringify(events, null, 2));
815
- return;
816
- }
817
- if (!Array.isArray(events) || events.length === 0) {
818
- console.log(colors.success('✅ No security events found'));
819
- return;
820
- }
821
- console.log(colors.primary('🛡️ Security Events Monitor'));
822
- console.log(colors.info('═'.repeat(80)));
823
- const table = new Table({
824
- head: ['Event Type', 'Severity', 'Description', 'Resolved', 'Timestamp'].map(h => colors.accent(h)),
825
- style: { head: [], border: [] }
826
- });
827
- events.forEach((event) => {
828
- const severityColor = event.severity === 'critical' ? colors.error :
829
- event.severity === 'high' ? colors.accent :
830
- event.severity === 'medium' ? colors.warning : colors.success;
831
- table.push([
832
- event.eventType,
833
- severityColor(event.severity.toUpperCase()),
834
- truncateText(event.description, 40),
835
- event.resolved ? colors.success('✓') : colors.warning('Pending'),
836
- formatDate(event.timestamp)
837
- ]);
838
- });
839
- console.log(table.toString());
840
- console.log(colors.info('═'.repeat(80)));
841
- console.log(colors.muted(`🛡️ Total: ${colors.highlight(events.length)} security events`));
842
- }
843
- catch (error) {
844
- const errorMessage = error instanceof Error ? error.message : String(error);
845
- console.error(colors.error('✖ Failed to get security events:'), colors.muted(errorMessage));
846
- process.exit(1);
847
- }
597
+ void options;
598
+ exitUnsupported('API key security event analytics', [
599
+ 'No /api/v1/auth/api-keys/analytics/* routes exist on the current auth-gateway.',
600
+ 'Use dashboard security reporting until gateway analytics endpoints are exposed.',
601
+ ]);
848
602
  });
849
603
  // Add subcommands
850
604
  apiKeysCommand.addCommand(projectsCommand);
@@ -662,8 +662,31 @@ async function handleOAuthFlow(config) {
662
662
  }
663
663
  ]);
664
664
  if (!openBrowser) {
665
- console.log(chalk.yellow('⚠️ Authentication cancelled'));
666
- return;
665
+ // Fallback: manual token paste for headless/remote environments
666
+ console.log();
667
+ console.log(chalk.cyan('📋 Manual Token Authentication'));
668
+ console.log(chalk.gray('Open this URL in any browser to authenticate:'));
669
+ const authBase = config.getDiscoveredApiUrl();
670
+ console.log(chalk.white(` ${authBase}/auth/cli-login`));
671
+ console.log();
672
+ console.log(chalk.gray('After logging in, expand "Headless/Remote?" to copy the token, then paste it below.'));
673
+ console.log();
674
+ const { token } = await inquirer.prompt([
675
+ {
676
+ type: 'password',
677
+ name: 'token',
678
+ message: 'Paste token:',
679
+ mask: '*',
680
+ validate: (input) => input.trim().length > 0 || 'Token is required',
681
+ }
682
+ ]);
683
+ const trimmed = token.trim();
684
+ await config.setToken(trimmed);
685
+ await config.set('authMethod', 'jwt');
686
+ console.log();
687
+ console.log(chalk.green('✓ Token saved successfully'));
688
+ console.log(colors.info('You can now use all Lanonasis services'));
689
+ process.exit(0);
667
690
  }
668
691
  try {
669
692
  // Generate PKCE challenge
package/dist/utils/api.js CHANGED
@@ -280,6 +280,9 @@ export class APIClient {
280
280
  await this.config.discoverServices();
281
281
  // Use appropriate base URL based on endpoint and auth method
282
282
  const isAuthEndpoint = config.url?.includes('/auth/') || config.url?.includes('/login') || config.url?.includes('/register') || config.url?.includes('/oauth/');
283
+ const isAuthGatewayManagementEndpoint = typeof config.url === 'string'
284
+ && (config.url.startsWith('/api/v1/auth/api-keys')
285
+ || config.url.startsWith('/api/v1/projects'));
283
286
  const discoveredServices = this.config.get('discoveredServices');
284
287
  const authMethod = this.config.get('authMethod');
285
288
  const vendorKey = await this.config.getVendorKeyAsync();
@@ -322,7 +325,7 @@ export class APIClient {
322
325
  // - Other direct API calls -> api.lanonasis.com (vendor AI proxy)
323
326
  let apiBaseUrl;
324
327
  const useMcpServer = !forceDirectApi && !isAuthEndpoint && (prefersTokenAuth || useVendorKeyAuth || isMemoryEndpoint);
325
- if (isAuthEndpoint) {
328
+ if (isAuthEndpoint || isAuthGatewayManagementEndpoint) {
326
329
  apiBaseUrl = discoveredServices?.auth_base || 'https://auth.lanonasis.com';
327
330
  }
328
331
  else if (forceDirectApi) {
@@ -343,7 +346,7 @@ export class APIClient {
343
346
  config.url = config.url.replace(/\/api\/v1\/memories/g, '/memory');
344
347
  }
345
348
  // Add project scope header for auth endpoints
346
- if (isAuthEndpoint) {
349
+ if (isAuthEndpoint || isAuthGatewayManagementEndpoint) {
347
350
  config.headers['X-Project-Scope'] = 'lanonasis-maas';
348
351
  }
349
352
  // Enhanced Authentication Support
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lanonasis/cli",
3
- "version": "3.9.13",
3
+ "version": "3.9.14",
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",