@lanonasis/cli 3.3.15 → 3.5.15

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.
Files changed (38) hide show
  1. package/README.md +57 -13
  2. package/dist/commands/auth.js +11 -5
  3. package/dist/commands/completion.js +2 -0
  4. package/dist/commands/config.js +4 -4
  5. package/dist/commands/enhanced-memory.js +1 -1
  6. package/dist/commands/mcp.js +15 -9
  7. package/dist/core/achievements.js +1 -1
  8. package/dist/core/power-mode.js +5 -3
  9. package/dist/core/welcome.js +6 -6
  10. package/dist/enhanced-cli.js +6 -3
  11. package/dist/index-simple.js +5 -1
  12. package/dist/index.js +5 -1
  13. package/dist/mcp/access-control.d.ts +1 -1
  14. package/dist/mcp/access-control.js +4 -4
  15. package/dist/mcp/client/enhanced-client.js +5 -7
  16. package/dist/mcp/schemas/tool-schemas.d.ts +1 -1
  17. package/dist/mcp/schemas/tool-schemas.js +1 -1
  18. package/dist/mcp/server/lanonasis-server.d.ts +2 -1
  19. package/dist/mcp/server/lanonasis-server.js +7 -5
  20. package/dist/mcp/transports/transport-manager.js +3 -3
  21. package/dist/mcp-server.d.ts +1 -0
  22. package/dist/mcp-server.js +43 -9
  23. package/dist/utils/config.d.ts +10 -1
  24. package/dist/utils/config.js +97 -17
  25. package/dist/utils/mcp-client.d.ts +21 -2
  26. package/dist/utils/mcp-client.js +117 -46
  27. package/package.json +3 -3
  28. package/dist/__tests__/auth-persistence.test.d.ts +0 -1
  29. package/dist/__tests__/auth-persistence.test.js +0 -243
  30. package/dist/__tests__/cross-device-integration.test.d.ts +0 -1
  31. package/dist/__tests__/cross-device-integration.test.js +0 -305
  32. package/dist/__tests__/mcp-connection-reliability.test.d.ts +0 -1
  33. package/dist/__tests__/mcp-connection-reliability.test.js +0 -489
  34. package/dist/__tests__/setup.d.ts +0 -1
  35. package/dist/__tests__/setup.js +0 -26
  36. package/dist/mcp/server/mcp/server/lanonasis-server.js +0 -911
  37. package/dist/mcp/server/utils/api.js +0 -431
  38. package/dist/mcp/server/utils/config.js +0 -855
@@ -2,7 +2,6 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
2
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
3
3
  import chalk from 'chalk';
4
4
  import { CLIConfig } from './config.js';
5
- import * as path from 'path';
6
5
  import * as fs from 'fs';
7
6
  import { EventSource } from 'eventsource';
8
7
  import WebSocket from 'ws';
@@ -17,9 +16,35 @@ export class MCPClient {
17
16
  healthCheckInterval = null;
18
17
  connectionStartTime = 0;
19
18
  lastHealthCheck = null;
19
+ activeConnectionMode = 'local'; // Track actual connection mode
20
20
  constructor() {
21
21
  this.config = new CLIConfig();
22
22
  }
23
+ /**
24
+ * Overrides the configuration directory used by the underlying CLI config.
25
+ * Useful for tests that need isolated config state.
26
+ */
27
+ setConfigDirectory(configDir) {
28
+ this.config.setConfigDirectory(configDir);
29
+ }
30
+ /**
31
+ * Returns the current config file path. Primarily used for test introspection.
32
+ */
33
+ getConfigPath() {
34
+ return this.config.getConfigPath();
35
+ }
36
+ /**
37
+ * Helper for tests to seed authentication tokens without accessing internals.
38
+ */
39
+ async setTokenForTesting(token) {
40
+ await this.config.setToken(token);
41
+ }
42
+ /**
43
+ * Helper for tests to seed vendor keys without accessing internals.
44
+ */
45
+ async setVendorKeyForTesting(vendorKey) {
46
+ await this.config.setVendorKey(vendorKey);
47
+ }
23
48
  /**
24
49
  * Initialize the MCP client configuration
25
50
  */
@@ -43,13 +68,18 @@ export class MCPClient {
43
68
  await this.init();
44
69
  // Validate authentication before attempting connection
45
70
  await this.validateAuthBeforeConnect();
46
- // Determine connection mode with priority to explicit mode option
47
- // Default to 'remote' for better user experience
48
- const connectionMode = options.connectionMode ??
49
- (options.useWebSocket ? 'websocket' :
50
- options.useRemote ? 'remote' :
51
- this.config.get('mcpConnectionMode') ??
52
- this.config.get('mcpUseRemote') ? 'remote' : 'remote');
71
+ // Determine connection mode with clear precedence and safe defaults
72
+ // 1) explicit option
73
+ // 2) explicit flags
74
+ // 3) configured preference
75
+ // 4) default to 'websocket' (production-ready pm2 mcp-core)
76
+ const configuredMode = this.config.get('mcpConnectionMode');
77
+ const preferRemote = this.config.get('mcpUseRemote');
78
+ const connectionMode = options.connectionMode
79
+ ?? (options.useWebSocket ? 'websocket' : undefined)
80
+ ?? (options.useRemote ? 'remote' : undefined)
81
+ ?? configuredMode
82
+ ?? (preferRemote ? 'remote' : 'websocket');
53
83
  let wsUrl;
54
84
  let serverUrl;
55
85
  let serverPath;
@@ -70,6 +100,7 @@ export class MCPClient {
70
100
  // Initialize WebSocket connection
71
101
  await this.initializeWebSocket(wsUrl);
72
102
  this.isConnected = true;
103
+ this.activeConnectionMode = 'websocket';
73
104
  this.retryAttempts = 0;
74
105
  this.startHealthMonitoring();
75
106
  return true;
@@ -90,21 +121,24 @@ export class MCPClient {
90
121
  // Initialize SSE connection for real-time updates
91
122
  await this.initializeSSE(serverUrl);
92
123
  this.isConnected = true;
124
+ this.activeConnectionMode = 'remote';
93
125
  this.retryAttempts = 0;
94
126
  this.startHealthMonitoring();
95
127
  return true;
96
128
  }
97
- default: {
98
- // Local MCP server connection (default)
99
- const serverPathValue = options.serverPath ??
100
- this.config.get('mcpServerPath') ??
101
- path.join(path.resolve(), '../../../../onasis-gateway/mcp-server/server.js');
102
- serverPath = serverPathValue;
129
+ case 'local': {
130
+ // Local MCP server connection requires explicit path via option or config
131
+ serverPath = options.serverPath ?? this.config.get('mcpServerPath');
132
+ if (!serverPath) {
133
+ console.log(chalk.yellow('⚠️ No local MCP server path configured.'));
134
+ console.log(chalk.cyan('💡 Prefer using WebSocket mode (default). Or configure a local path via:'));
135
+ console.log(chalk.cyan(' lanonasis config set mcpServerPath /absolute/path/to/server.js'));
136
+ throw new Error('Local MCP server path not provided');
137
+ }
103
138
  // Check if the server file exists
104
139
  if (!fs.existsSync(serverPath)) {
105
140
  console.log(chalk.yellow(`⚠️ Local MCP server not found at ${serverPath}`));
106
- console.log(chalk.cyan('💡 For remote connection, use: onasis mcp connect --url wss://mcp.lanonasis.com/ws'));
107
- console.log(chalk.cyan('💡 Or install local server: npm install -g @lanonasis/mcp-server'));
141
+ console.log(chalk.cyan('💡 For remote use WebSocket: lanonasis mcp connect --mode websocket --url wss://mcp.lanonasis.com/ws'));
108
142
  throw new Error(`MCP server not found at ${serverPath}`);
109
143
  }
110
144
  if (this.retryAttempts === 0) {
@@ -113,9 +147,20 @@ export class MCPClient {
113
147
  else {
114
148
  console.log(chalk.yellow(`Retry ${this.retryAttempts}/${this.maxRetries}: Connecting to local MCP server...`));
115
149
  }
150
+ // Allow passing extra args to local server (e.g., --stdio) via options or env/config
151
+ // Precedence: options.localArgs -> env.MCP_LOCAL_SERVER_ARGS -> config.mcpLocalArgs -> none
152
+ const envArgs = (process.env.MCP_LOCAL_SERVER_ARGS || '')
153
+ .split(' ')
154
+ .map(s => s.trim())
155
+ .filter(Boolean);
156
+ const configArgs = (this.config.get('mcpLocalArgs') || []);
157
+ const extraArgs = (options.localArgs && options.localArgs.length > 0)
158
+ ? options.localArgs
159
+ : (envArgs.length > 0 ? envArgs : configArgs);
160
+ const args = [serverPath, ...extraArgs];
116
161
  const localTransport = new StdioClientTransport({
117
162
  command: 'node',
118
- args: [serverPath]
163
+ args
119
164
  });
120
165
  this.client = new Client({
121
166
  name: '@lanonasis/cli',
@@ -123,11 +168,27 @@ export class MCPClient {
123
168
  });
124
169
  await this.client.connect(localTransport);
125
170
  this.isConnected = true;
171
+ this.activeConnectionMode = 'local';
126
172
  this.retryAttempts = 0;
127
173
  console.log(chalk.green('✓ Connected to MCP server'));
128
174
  this.startHealthMonitoring();
129
175
  return true;
130
176
  }
177
+ default: {
178
+ // Safety: if we reach default, fall back to remote (HTTP) rather than brittle local
179
+ const serverUrlValue = options.serverUrl
180
+ ?? this.config.get('mcpServerUrl')
181
+ ?? this.config.getMCPRestUrl()
182
+ ?? 'https://mcp.lanonasis.com/api/v1';
183
+ serverUrl = serverUrlValue;
184
+ console.log(chalk.yellow(`Unknown connection mode '${String(connectionMode)}', falling back to remote at ${serverUrl}`));
185
+ await this.initializeSSE(serverUrl);
186
+ this.isConnected = true;
187
+ this.activeConnectionMode = 'remote';
188
+ this.retryAttempts = 0;
189
+ this.startHealthMonitoring();
190
+ return true;
191
+ }
131
192
  }
132
193
  }
133
194
  catch (error) {
@@ -140,7 +201,8 @@ export class MCPClient {
140
201
  async handleConnectionFailure(error, options) {
141
202
  // Check if this is an authentication error (don't retry these)
142
203
  if (this.isAuthenticationError(error)) {
143
- console.error(chalk.red('Authentication failed:'), error.message);
204
+ const authMsg = error?.message ?? '';
205
+ console.error(chalk.red('Authentication failed:'), authMsg);
144
206
  this.provideAuthenticationGuidance(error);
145
207
  this.isConnected = false;
146
208
  return false;
@@ -155,7 +217,8 @@ export class MCPClient {
155
217
  // For network errors, retry with exponential backoff
156
218
  const delay = await this.exponentialBackoff(this.retryAttempts);
157
219
  console.log(chalk.yellow(`Network error, retrying in ${delay}ms... (${this.retryAttempts}/${this.maxRetries})`));
158
- console.log(chalk.gray(`Error: ${error.message}`));
220
+ const message = error?.message ?? String(error);
221
+ console.log(chalk.gray(`Error: ${message}`));
159
222
  await new Promise(resolve => setTimeout(resolve, delay));
160
223
  return this.connectWithRetry(options);
161
224
  }
@@ -163,7 +226,7 @@ export class MCPClient {
163
226
  * Check if error is authentication-related
164
227
  */
165
228
  isAuthenticationError(error) {
166
- const errorMessage = error.message?.toLowerCase() || '';
229
+ const errorMessage = error?.message?.toLowerCase() || '';
167
230
  return errorMessage.includes('authentication_required') ||
168
231
  errorMessage.includes('authentication_invalid') ||
169
232
  errorMessage.includes('unauthorized') ||
@@ -171,23 +234,25 @@ export class MCPClient {
171
234
  errorMessage.includes('token is invalid') ||
172
235
  errorMessage.includes('401') ||
173
236
  errorMessage.includes('403') ||
174
- (error.response?.status >= 401 && error.response?.status <= 403);
237
+ ((error.response?.status ?? 0) >= 401 &&
238
+ (error.response?.status ?? 0) <= 403);
175
239
  }
176
240
  /**
177
241
  * Provide authentication-specific guidance
178
242
  */
179
243
  provideAuthenticationGuidance(error) {
180
244
  console.log(chalk.yellow('\n🔐 Authentication Issue Detected:'));
181
- if (error.message?.includes('AUTHENTICATION_REQUIRED')) {
245
+ const msg = error?.message ?? '';
246
+ if (msg.includes('AUTHENTICATION_REQUIRED')) {
182
247
  console.log(chalk.cyan('• No credentials found. Run: lanonasis auth login'));
183
248
  console.log(chalk.cyan('• Or set vendor key: lanonasis auth login --vendor-key pk_xxx.sk_xxx'));
184
249
  }
185
- else if (error.message?.includes('AUTHENTICATION_INVALID')) {
250
+ else if (msg.includes('AUTHENTICATION_INVALID')) {
186
251
  console.log(chalk.cyan('• Invalid credentials. Check your vendor key format'));
187
252
  console.log(chalk.cyan('• Expected format: pk_xxx.sk_xxx'));
188
253
  console.log(chalk.cyan('• Try: lanonasis auth logout && lanonasis auth login'));
189
254
  }
190
- else if (error.message?.includes('expired')) {
255
+ else if (msg.includes('expired')) {
191
256
  console.log(chalk.cyan('• Token expired. Re-authenticate: lanonasis auth login'));
192
257
  console.log(chalk.cyan('• Or refresh: lanonasis auth refresh (if available)'));
193
258
  }
@@ -200,27 +265,28 @@ export class MCPClient {
200
265
  /**
201
266
  * Provide network troubleshooting guidance
202
267
  */
203
- provideNetworkTroubleshootingGuidance(error) {
268
+ provideNetworkTroubleshootingGuidance(_error) {
204
269
  console.log(chalk.yellow('\n🌐 Network Issue Detected:'));
205
- if (error.message?.includes('ECONNREFUSED') || error.message?.includes('connect ECONNREFUSED')) {
270
+ const msg = _error?.message ?? '';
271
+ if (msg.includes('ECONNREFUSED') || msg.includes('connect ECONNREFUSED')) {
206
272
  console.log(chalk.cyan('• Connection refused. Service may be down:'));
207
273
  console.log(chalk.cyan(' - For remote: Check https://mcp.lanonasis.com/health'));
208
274
  console.log(chalk.cyan(' - For WebSocket: Check wss://mcp.lanonasis.com/ws'));
209
275
  console.log(chalk.cyan(' - For local: Install local MCP server'));
210
276
  }
211
- else if (error.message?.includes('timeout') || error.message?.includes('ETIMEDOUT')) {
277
+ else if (msg.includes('timeout') || msg.includes('ETIMEDOUT')) {
212
278
  console.log(chalk.cyan('• Connection timeout. Check network:'));
213
279
  console.log(chalk.cyan(' - Verify internet connectivity'));
214
280
  console.log(chalk.cyan(' - Check firewall settings'));
215
281
  console.log(chalk.cyan(' - Try different connection mode: --mode remote'));
216
282
  }
217
- else if (error.message?.includes('ENOTFOUND') || error.message?.includes('getaddrinfo')) {
283
+ else if (msg.includes('ENOTFOUND') || msg.includes('getaddrinfo')) {
218
284
  console.log(chalk.cyan('• DNS resolution failed:'));
219
285
  console.log(chalk.cyan(' - Check DNS settings'));
220
286
  console.log(chalk.cyan(' - Verify server URL is correct'));
221
287
  console.log(chalk.cyan(' - Try using IP address instead of hostname'));
222
288
  }
223
- else if (error.message?.includes('certificate') || error.message?.includes('SSL') || error.message?.includes('TLS')) {
289
+ else if (msg.includes('certificate') || msg.includes('SSL') || msg.includes('TLS')) {
224
290
  console.log(chalk.cyan('• SSL/TLS certificate issue:'));
225
291
  console.log(chalk.cyan(' - Check system time and date'));
226
292
  console.log(chalk.cyan(' - Update CA certificates'));
@@ -297,7 +363,7 @@ export class MCPClient {
297
363
  }
298
364
  }
299
365
  }
300
- catch (error) {
366
+ catch {
301
367
  // If we can't decode the token, try to validate it with the server
302
368
  await this.validateTokenWithServer(token);
303
369
  }
@@ -323,7 +389,7 @@ export class MCPClient {
323
389
  console.log(chalk.green('✓ Token refreshed successfully'));
324
390
  }
325
391
  }
326
- catch (error) {
392
+ catch {
327
393
  throw new Error('Failed to refresh token. Please re-authenticate.');
328
394
  }
329
395
  }
@@ -343,10 +409,12 @@ export class MCPClient {
343
409
  });
344
410
  }
345
411
  catch (error) {
346
- if (error.response?.status === 401 || error.response?.status === 403) {
412
+ const status = error.response?.status;
413
+ if (status === 401 || status === 403) {
347
414
  throw new Error('Token is invalid or expired. Please re-authenticate.');
348
415
  }
349
- throw new Error(`Token validation failed: ${error.message}`);
416
+ const msg = error?.message || 'Unknown error';
417
+ throw new Error(`Token validation failed: ${msg}`);
350
418
  }
351
419
  }
352
420
  /**
@@ -485,7 +553,7 @@ export class MCPClient {
485
553
  }
486
554
  try {
487
555
  this.lastHealthCheck = new Date();
488
- const connectionMode = this.config.get('mcpConnectionMode') ?? 'remote';
556
+ const connectionMode = this.activeConnectionMode || 'remote';
489
557
  switch (connectionMode) {
490
558
  case 'websocket':
491
559
  await this.checkWebSocketHealth();
@@ -498,8 +566,9 @@ export class MCPClient {
498
566
  break;
499
567
  }
500
568
  }
501
- catch (error) {
502
- console.log(chalk.yellow('⚠️ Health check failed, attempting reconnection...'));
569
+ catch {
570
+ const connectionMode = this.activeConnectionMode || 'remote';
571
+ console.log(chalk.yellow(`⚠️ ${connectionMode} connection health check failed, attempting reconnection...`));
503
572
  await this.handleHealthCheckFailure();
504
573
  }
505
574
  }
@@ -536,8 +605,9 @@ export class MCPClient {
536
605
  timeout: 5000
537
606
  });
538
607
  }
539
- catch (error) {
540
- throw new Error(`Remote health check failed: ${error}`);
608
+ catch (e) {
609
+ const msg = e?.message ?? String(e);
610
+ throw new Error(`Remote health check failed: ${msg}`);
541
611
  }
542
612
  }
543
613
  /**
@@ -551,8 +621,9 @@ export class MCPClient {
551
621
  try {
552
622
  await this.client.listTools();
553
623
  }
554
- catch (error) {
555
- throw new Error(`Local health check failed: ${error}`);
624
+ catch (e) {
625
+ const msg = e?.message ?? String(e);
626
+ throw new Error(`Local health check failed: ${msg}`);
556
627
  }
557
628
  }
558
629
  /**
@@ -562,10 +633,11 @@ export class MCPClient {
562
633
  this.isConnected = false;
563
634
  this.stopHealthMonitoring();
564
635
  // Attempt to reconnect with current configuration
565
- const connectionMode = this.config.get('mcpConnectionMode') ?? 'remote';
636
+ const connectionMode = (this.activeConnectionMode || 'remote');
566
637
  const options = {
567
- connectionMode: connectionMode
638
+ connectionMode
568
639
  };
640
+ console.log(chalk.yellow(`↻ Attempting reconnection using ${connectionMode} mode...`));
569
641
  // Add specific URLs if available
570
642
  if (connectionMode === 'websocket') {
571
643
  options.serverUrl = this.config.get('mcpWebSocketUrl');
@@ -603,6 +675,7 @@ export class MCPClient {
603
675
  this.wsConnection = null;
604
676
  }
605
677
  this.isConnected = false;
678
+ this.activeConnectionMode = 'websocket'; // Reset to default
606
679
  }
607
680
  /**
608
681
  * Call an MCP tool
@@ -712,8 +785,7 @@ export class MCPClient {
712
785
  catch (error) {
713
786
  // Safely handle errors with type checking
714
787
  const errorObj = error;
715
- const errorMsg = errorObj.response?.data?.error ||
716
- (errorObj.message ? errorObj.message : 'Unknown error');
788
+ const errorMsg = errorObj.response?.data?.error || (errorObj.message ?? 'Unknown error');
717
789
  throw new Error(`Remote tool call failed: ${errorMsg}`);
718
790
  }
719
791
  }
@@ -757,8 +829,7 @@ export class MCPClient {
757
829
  * Get connection status details with health information
758
830
  */
759
831
  getConnectionStatus() {
760
- const connectionMode = this.config.get('mcpConnectionMode') ??
761
- (this.config.get('mcpUseRemote') ? 'remote' : 'local');
832
+ const connectionMode = this.activeConnectionMode;
762
833
  let server;
763
834
  switch (connectionMode) {
764
835
  case 'websocket':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lanonasis/cli",
3
- "version": "3.3.15",
3
+ "version": "3.5.15",
4
4
  "description": "LanOnasis Enterprise CLI - Memory as a Service, API Key Management, and Infrastructure Orchestration",
5
5
  "main": "dist/index-simple.js",
6
6
  "bin": {
@@ -24,7 +24,7 @@
24
24
  "test": "NODE_OPTIONS=--experimental-vm-modules jest",
25
25
  "test:mcp": "NODE_OPTIONS=--experimental-vm-modules jest --testPathPattern=mcp",
26
26
  "test:integration": "npm run build:mcp && npm run test:mcp",
27
- "lint": "eslint src/**/*.ts",
27
+ "lint": "eslint .",
28
28
  "type-check": "tsc --noEmit",
29
29
  "prepare": "npm run build",
30
30
  "publish:with-mcp": "npm run build:mcp && npm publish"
@@ -99,4 +99,4 @@
99
99
  "engines": {
100
100
  "node": ">=18.0.0"
101
101
  }
102
- }
102
+ }
@@ -1 +0,0 @@
1
- export {};
@@ -1,243 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
2
- import { CLIConfig } from '../utils/config.js';
3
- import * as fs from 'fs/promises';
4
- import * as path from 'path';
5
- import * as os from 'os';
6
- // Mock axios for network calls
7
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
- const mockAxios = {
9
- get: jest.fn(),
10
- post: jest.fn()
11
- };
12
- jest.mock('axios', () => ({
13
- default: mockAxios,
14
- get: mockAxios.get,
15
- post: mockAxios.post
16
- }));
17
- describe('Authentication Persistence Tests', () => {
18
- let testConfigDir;
19
- let config;
20
- beforeEach(async () => {
21
- // Create a temporary test directory for each test
22
- testConfigDir = path.join(os.tmpdir(), `test-auth-persistence-${Date.now()}-${Math.random()}`);
23
- await fs.mkdir(testConfigDir, { recursive: true });
24
- // Create a new config instance with test directory
25
- config = new CLIConfig();
26
- config.configDir = testConfigDir;
27
- config.configPath = path.join(testConfigDir, 'config.json');
28
- config.lockFile = path.join(testConfigDir, 'config.lock');
29
- await config.init();
30
- // Clear axios mocks
31
- mockAxios.get.mockClear();
32
- mockAxios.post.mockClear();
33
- });
34
- afterEach(async () => {
35
- // Clean up test directory
36
- try {
37
- await fs.rm(testConfigDir, { recursive: true, force: true });
38
- }
39
- catch {
40
- // Ignore cleanup errors
41
- }
42
- });
43
- describe('Credential Storage and Retrieval', () => {
44
- it('should store and retrieve vendor key credentials across CLI sessions', async () => {
45
- const testVendorKey = 'pk_test123456789.sk_test123456789012345';
46
- // Mock successful server validation
47
- mockAxios.get.mockResolvedValueOnce({ status: 200, data: { status: 'ok' } });
48
- // Store vendor key
49
- await config.setVendorKey(testVendorKey);
50
- // Verify storage
51
- expect(config.getVendorKey()).toBe(testVendorKey);
52
- expect(config.get('authMethod')).toBe('vendor_key');
53
- expect(config.get('lastValidated')).toBeDefined();
54
- // Simulate new CLI session by creating new config instance
55
- const newConfig = new CLIConfig();
56
- newConfig.configDir = testConfigDir;
57
- newConfig.configPath = path.join(testConfigDir, 'config.json');
58
- newConfig.lockFile = path.join(testConfigDir, 'config.lock');
59
- await newConfig.init();
60
- // Verify credentials persist across sessions
61
- expect(newConfig.getVendorKey()).toBe(testVendorKey);
62
- expect(newConfig.get('authMethod')).toBe('vendor_key');
63
- });
64
- it('should store and retrieve JWT token credentials across CLI sessions', async () => {
65
- const testToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
66
- // Store JWT token
67
- await config.setToken(testToken);
68
- // Verify storage
69
- expect(config.getToken()).toBe(testToken);
70
- expect(config.get('authMethod')).toBe('jwt');
71
- expect(config.get('lastValidated')).toBeDefined();
72
- // Simulate new CLI session
73
- const newConfig = new CLIConfig();
74
- newConfig.configDir = testConfigDir;
75
- newConfig.configPath = path.join(testConfigDir, 'config.json');
76
- newConfig.lockFile = path.join(testConfigDir, 'config.lock');
77
- await newConfig.init();
78
- // Verify token persists across sessions
79
- expect(newConfig.getToken()).toBe(testToken);
80
- expect(newConfig.get('authMethod')).toBe('jwt');
81
- });
82
- });
83
- describe('Authentication Failure Tracking', () => {
84
- it('should track authentication failure count and last failure time', async () => {
85
- // Initially no failures
86
- expect(config.getFailureCount()).toBe(0);
87
- expect(config.getLastAuthFailure()).toBeUndefined();
88
- // Increment failure count
89
- await config.incrementFailureCount();
90
- // Verify failure count increased
91
- expect(config.getFailureCount()).toBe(1);
92
- expect(config.getLastAuthFailure()).toBeDefined();
93
- // Increment again
94
- await config.incrementFailureCount();
95
- // Verify count is now 2
96
- expect(config.getFailureCount()).toBe(2);
97
- });
98
- it('should reset failure count when authentication succeeds', async () => {
99
- // Add some failures
100
- await config.incrementFailureCount();
101
- await config.incrementFailureCount();
102
- expect(config.getFailureCount()).toBe(2);
103
- // Reset failure count (simulating successful auth)
104
- await config.resetFailureCount();
105
- // Verify reset
106
- expect(config.getFailureCount()).toBe(0);
107
- expect(config.getLastAuthFailure()).toBeUndefined();
108
- });
109
- it('should apply progressive delays based on failure count', async () => {
110
- // Initially no delay
111
- expect(config.shouldDelayAuth()).toBe(false);
112
- expect(config.getAuthDelayMs()).toBe(0);
113
- // After 3 failures, should delay
114
- await config.incrementFailureCount(); // 1
115
- await config.incrementFailureCount(); // 2
116
- await config.incrementFailureCount(); // 3
117
- expect(config.shouldDelayAuth()).toBe(true);
118
- expect(config.getAuthDelayMs()).toBeGreaterThanOrEqual(1500); // 2000ms ± 25%
119
- expect(config.getAuthDelayMs()).toBeLessThanOrEqual(2500);
120
- // After 4 failures, delay should increase
121
- await config.incrementFailureCount(); // 4
122
- expect(config.getAuthDelayMs()).toBeGreaterThanOrEqual(3000); // 4000ms ± 25%
123
- expect(config.getAuthDelayMs()).toBeLessThanOrEqual(5000);
124
- });
125
- });
126
- describe('Vendor Key Validation', () => {
127
- it('should validate correct vendor key format', () => {
128
- const validKey = 'pk_test123456789.sk_test123456789012345';
129
- const result = config.validateVendorKeyFormat(validKey);
130
- expect(result).toBe(true);
131
- });
132
- it('should reject invalid vendor key formats', () => {
133
- const invalidKeys = [
134
- 'invalid-key', // no dot
135
- 'pk_.sk_test123456789012345', // empty public part
136
- 'pk_test123456789.sk_', // empty secret part
137
- 'pk_test.sk_test', // too short
138
- 'pk_test123456789', // missing secret part
139
- 'sk_test123456789.pk_test123456789', // wrong order
140
- 'pk_test@invalid.sk_test123456789012345', // invalid chars
141
- ];
142
- invalidKeys.forEach(key => {
143
- const result = config.validateVendorKeyFormat(key);
144
- expect(result).not.toBe(true);
145
- expect(typeof result).toBe('string'); // Should return error message
146
- });
147
- });
148
- it('should accept valid vendor key formats', () => {
149
- const validKeys = [
150
- 'pk_123456789ABCDEF.sk_1234567890123456789012345',
151
- 'pk_A0123456789ABC.sk_XYZ123456789012345',
152
- ];
153
- validKeys.forEach(key => {
154
- const result = config.validateVendorKeyFormat(key);
155
- expect(result).toBe(true);
156
- });
157
- });
158
- });
159
- describe('Token Expiry Handling', () => {
160
- it('should detect if JWT token is authenticated', async () => {
161
- // Valid token (in the future)
162
- const validToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjk5OTk5OTk5OTl9.03fBznX7YvIa8e1GjN0dYF1zR2vZ3xP4wQ5rE6sT7uA';
163
- await config.setToken(validToken);
164
- const isAuthenticated = await config.isAuthenticated();
165
- expect(isAuthenticated).toBe(true);
166
- });
167
- it('should detect if JWT token is expired', async () => {
168
- // Expired token (in the past)
169
- const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
170
- await config.setToken(expiredToken);
171
- const isAuthenticated = await config.isAuthenticated();
172
- expect(isAuthenticated).toBe(false);
173
- });
174
- });
175
- describe('Device ID Management', () => {
176
- it('should generate and maintain consistent device ID across sessions', async () => {
177
- // Get first device ID
178
- const deviceId1 = await config.getDeviceId();
179
- expect(deviceId1).toBeDefined();
180
- expect(typeof deviceId1).toBe('string');
181
- expect(deviceId1.length).toBeGreaterThan(0);
182
- // Get device ID again - should be the same
183
- const deviceId2 = await config.getDeviceId();
184
- expect(deviceId1).toBe(deviceId2);
185
- // Simulate new session
186
- const newConfig = new CLIConfig();
187
- newConfig.configDir = testConfigDir;
188
- newConfig.configPath = path.join(testConfigDir, 'config.json');
189
- newConfig.lockFile = path.join(testConfigDir, 'config.lock');
190
- await newConfig.init();
191
- // Device ID should persist across sessions
192
- const deviceId3 = await newConfig.getDeviceId();
193
- expect(deviceId1).toBe(deviceId3);
194
- });
195
- });
196
- describe('Configuration Versioning and Migration', () => {
197
- it('should maintain configuration version compatibility', async () => {
198
- // Check that config has version
199
- const version = config.get('version');
200
- expect(version).toBeDefined();
201
- expect(version).toBe('1.0.0');
202
- });
203
- it('should handle atomic configuration saves', async () => {
204
- // Set some data
205
- await config.setAndSave('testKey', 'testValue');
206
- // Verify it was saved
207
- expect(config.get('testKey')).toBe('testValue');
208
- // Create new config instance and verify data persists
209
- const newConfig = new CLIConfig();
210
- newConfig.configDir = testConfigDir;
211
- newConfig.configPath = path.join(testConfigDir, 'config.json');
212
- newConfig.lockFile = path.join(testConfigDir, 'config.lock');
213
- await newConfig.init();
214
- expect(newConfig.get('testKey')).toBe('testValue');
215
- });
216
- });
217
- describe('Credential Validation Against Server', () => {
218
- it('should validate stored credentials against server', async () => {
219
- const testVendorKey = 'pk_test123456789.sk_test123456789012345';
220
- // Mock successful server validation
221
- mockAxios.get.mockResolvedValue({ status: 200, data: { status: 'ok' } });
222
- // Set vendor key
223
- await config.setVendorKey(testVendorKey);
224
- // Validate credentials
225
- const isValid = await config.validateStoredCredentials();
226
- expect(isValid).toBe(true);
227
- // Verify server was called
228
- expect(mockAxios.get).toHaveBeenCalledTimes(1);
229
- });
230
- it('should return false when credentials are invalid', async () => {
231
- const testVendorKey = 'pk_test123456789.sk_test123456789012345';
232
- // Mock failed server validation
233
- mockAxios.get.mockRejectedValue({ response: { status: 401 } });
234
- // Set vendor key
235
- await config.setVendorKey(testVendorKey);
236
- // Validate credentials
237
- const isValid = await config.validateStoredCredentials();
238
- expect(isValid).toBe(false);
239
- // Verify server was called
240
- expect(mockAxios.get).toHaveBeenCalledTimes(1);
241
- });
242
- });
243
- });
@@ -1 +0,0 @@
1
- export {};