@lanonasis/cli 3.2.14 → 3.4.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 (36) hide show
  1. package/README.md +57 -13
  2. package/dist/commands/auth.js +7 -7
  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 +7 -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 +4 -5
  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.js +3 -3
  22. package/dist/utils/config.js +59 -10
  23. package/dist/utils/mcp-client.d.ts +4 -2
  24. package/dist/utils/mcp-client.js +86 -42
  25. package/package.json +3 -3
  26. package/dist/__tests__/auth-persistence.test.d.ts +0 -1
  27. package/dist/__tests__/auth-persistence.test.js +0 -243
  28. package/dist/__tests__/cross-device-integration.test.d.ts +0 -1
  29. package/dist/__tests__/cross-device-integration.test.js +0 -305
  30. package/dist/__tests__/mcp-connection-reliability.test.d.ts +0 -1
  31. package/dist/__tests__/mcp-connection-reliability.test.js +0 -489
  32. package/dist/__tests__/setup.d.ts +0 -1
  33. package/dist/__tests__/setup.js +0 -26
  34. package/dist/mcp/server/mcp/server/lanonasis-server.js +0 -911
  35. package/dist/mcp/server/utils/api.js +0 -431
  36. 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,6 +16,7 @@ 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
  }
@@ -43,13 +43,18 @@ export class MCPClient {
43
43
  await this.init();
44
44
  // Validate authentication before attempting connection
45
45
  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');
46
+ // Determine connection mode with clear precedence and safe defaults
47
+ // 1) explicit option
48
+ // 2) explicit flags
49
+ // 3) configured preference
50
+ // 4) default to 'websocket' (production-ready pm2 mcp-core)
51
+ const configuredMode = this.config.get('mcpConnectionMode');
52
+ const preferRemote = this.config.get('mcpUseRemote');
53
+ const connectionMode = options.connectionMode
54
+ ?? (options.useWebSocket ? 'websocket' : undefined)
55
+ ?? (options.useRemote ? 'remote' : undefined)
56
+ ?? configuredMode
57
+ ?? (preferRemote ? 'remote' : 'websocket');
53
58
  let wsUrl;
54
59
  let serverUrl;
55
60
  let serverPath;
@@ -70,6 +75,7 @@ export class MCPClient {
70
75
  // Initialize WebSocket connection
71
76
  await this.initializeWebSocket(wsUrl);
72
77
  this.isConnected = true;
78
+ this.activeConnectionMode = 'websocket';
73
79
  this.retryAttempts = 0;
74
80
  this.startHealthMonitoring();
75
81
  return true;
@@ -90,21 +96,24 @@ export class MCPClient {
90
96
  // Initialize SSE connection for real-time updates
91
97
  await this.initializeSSE(serverUrl);
92
98
  this.isConnected = true;
99
+ this.activeConnectionMode = 'remote';
93
100
  this.retryAttempts = 0;
94
101
  this.startHealthMonitoring();
95
102
  return true;
96
103
  }
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;
104
+ case 'local': {
105
+ // Local MCP server connection requires explicit path via option or config
106
+ serverPath = options.serverPath ?? this.config.get('mcpServerPath');
107
+ if (!serverPath) {
108
+ console.log(chalk.yellow('⚠️ No local MCP server path configured.'));
109
+ console.log(chalk.cyan('💡 Prefer using WebSocket mode (default). Or configure a local path via:'));
110
+ console.log(chalk.cyan(' lanonasis config set mcpServerPath /absolute/path/to/server.js'));
111
+ throw new Error('Local MCP server path not provided');
112
+ }
103
113
  // Check if the server file exists
104
114
  if (!fs.existsSync(serverPath)) {
105
115
  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'));
116
+ console.log(chalk.cyan('💡 For remote use WebSocket: lanonasis mcp connect --mode websocket --url wss://mcp.lanonasis.com/ws'));
108
117
  throw new Error(`MCP server not found at ${serverPath}`);
109
118
  }
110
119
  if (this.retryAttempts === 0) {
@@ -113,9 +122,20 @@ export class MCPClient {
113
122
  else {
114
123
  console.log(chalk.yellow(`Retry ${this.retryAttempts}/${this.maxRetries}: Connecting to local MCP server...`));
115
124
  }
125
+ // Allow passing extra args to local server (e.g., --stdio) via options or env/config
126
+ // Precedence: options.localArgs -> env.MCP_LOCAL_SERVER_ARGS -> config.mcpLocalArgs -> none
127
+ const envArgs = (process.env.MCP_LOCAL_SERVER_ARGS || '')
128
+ .split(' ')
129
+ .map(s => s.trim())
130
+ .filter(Boolean);
131
+ const configArgs = (this.config.get('mcpLocalArgs') || []);
132
+ const extraArgs = (options.localArgs && options.localArgs.length > 0)
133
+ ? options.localArgs
134
+ : (envArgs.length > 0 ? envArgs : configArgs);
135
+ const args = [serverPath, ...extraArgs];
116
136
  const localTransport = new StdioClientTransport({
117
137
  command: 'node',
118
- args: [serverPath]
138
+ args
119
139
  });
120
140
  this.client = new Client({
121
141
  name: '@lanonasis/cli',
@@ -123,11 +143,27 @@ export class MCPClient {
123
143
  });
124
144
  await this.client.connect(localTransport);
125
145
  this.isConnected = true;
146
+ this.activeConnectionMode = 'local';
126
147
  this.retryAttempts = 0;
127
148
  console.log(chalk.green('✓ Connected to MCP server'));
128
149
  this.startHealthMonitoring();
129
150
  return true;
130
151
  }
152
+ default: {
153
+ // Safety: if we reach default, fall back to remote (HTTP) rather than brittle local
154
+ const serverUrlValue = options.serverUrl
155
+ ?? this.config.get('mcpServerUrl')
156
+ ?? this.config.getMCPRestUrl()
157
+ ?? 'https://mcp.lanonasis.com/api/v1';
158
+ serverUrl = serverUrlValue;
159
+ console.log(chalk.yellow(`Unknown connection mode '${String(connectionMode)}', falling back to remote at ${serverUrl}`));
160
+ await this.initializeSSE(serverUrl);
161
+ this.isConnected = true;
162
+ this.activeConnectionMode = 'remote';
163
+ this.retryAttempts = 0;
164
+ this.startHealthMonitoring();
165
+ return true;
166
+ }
131
167
  }
132
168
  }
133
169
  catch (error) {
@@ -140,7 +176,8 @@ export class MCPClient {
140
176
  async handleConnectionFailure(error, options) {
141
177
  // Check if this is an authentication error (don't retry these)
142
178
  if (this.isAuthenticationError(error)) {
143
- console.error(chalk.red('Authentication failed:'), error.message);
179
+ const authMsg = error?.message ?? '';
180
+ console.error(chalk.red('Authentication failed:'), authMsg);
144
181
  this.provideAuthenticationGuidance(error);
145
182
  this.isConnected = false;
146
183
  return false;
@@ -155,7 +192,8 @@ export class MCPClient {
155
192
  // For network errors, retry with exponential backoff
156
193
  const delay = await this.exponentialBackoff(this.retryAttempts);
157
194
  console.log(chalk.yellow(`Network error, retrying in ${delay}ms... (${this.retryAttempts}/${this.maxRetries})`));
158
- console.log(chalk.gray(`Error: ${error.message}`));
195
+ const message = error?.message ?? String(error);
196
+ console.log(chalk.gray(`Error: ${message}`));
159
197
  await new Promise(resolve => setTimeout(resolve, delay));
160
198
  return this.connectWithRetry(options);
161
199
  }
@@ -163,7 +201,7 @@ export class MCPClient {
163
201
  * Check if error is authentication-related
164
202
  */
165
203
  isAuthenticationError(error) {
166
- const errorMessage = error.message?.toLowerCase() || '';
204
+ const errorMessage = error?.message?.toLowerCase() || '';
167
205
  return errorMessage.includes('authentication_required') ||
168
206
  errorMessage.includes('authentication_invalid') ||
169
207
  errorMessage.includes('unauthorized') ||
@@ -171,23 +209,25 @@ export class MCPClient {
171
209
  errorMessage.includes('token is invalid') ||
172
210
  errorMessage.includes('401') ||
173
211
  errorMessage.includes('403') ||
174
- (error.response?.status >= 401 && error.response?.status <= 403);
212
+ ((error.response?.status ?? 0) >= 401 &&
213
+ (error.response?.status ?? 0) <= 403);
175
214
  }
176
215
  /**
177
216
  * Provide authentication-specific guidance
178
217
  */
179
218
  provideAuthenticationGuidance(error) {
180
219
  console.log(chalk.yellow('\n🔐 Authentication Issue Detected:'));
181
- if (error.message?.includes('AUTHENTICATION_REQUIRED')) {
220
+ const msg = error?.message ?? '';
221
+ if (msg.includes('AUTHENTICATION_REQUIRED')) {
182
222
  console.log(chalk.cyan('• No credentials found. Run: lanonasis auth login'));
183
223
  console.log(chalk.cyan('• Or set vendor key: lanonasis auth login --vendor-key pk_xxx.sk_xxx'));
184
224
  }
185
- else if (error.message?.includes('AUTHENTICATION_INVALID')) {
225
+ else if (msg.includes('AUTHENTICATION_INVALID')) {
186
226
  console.log(chalk.cyan('• Invalid credentials. Check your vendor key format'));
187
227
  console.log(chalk.cyan('• Expected format: pk_xxx.sk_xxx'));
188
228
  console.log(chalk.cyan('• Try: lanonasis auth logout && lanonasis auth login'));
189
229
  }
190
- else if (error.message?.includes('expired')) {
230
+ else if (msg.includes('expired')) {
191
231
  console.log(chalk.cyan('• Token expired. Re-authenticate: lanonasis auth login'));
192
232
  console.log(chalk.cyan('• Or refresh: lanonasis auth refresh (if available)'));
193
233
  }
@@ -200,27 +240,28 @@ export class MCPClient {
200
240
  /**
201
241
  * Provide network troubleshooting guidance
202
242
  */
203
- provideNetworkTroubleshootingGuidance(error) {
243
+ provideNetworkTroubleshootingGuidance(_error) {
204
244
  console.log(chalk.yellow('\n🌐 Network Issue Detected:'));
205
- if (error.message?.includes('ECONNREFUSED') || error.message?.includes('connect ECONNREFUSED')) {
245
+ const msg = _error?.message ?? '';
246
+ if (msg.includes('ECONNREFUSED') || msg.includes('connect ECONNREFUSED')) {
206
247
  console.log(chalk.cyan('• Connection refused. Service may be down:'));
207
248
  console.log(chalk.cyan(' - For remote: Check https://mcp.lanonasis.com/health'));
208
249
  console.log(chalk.cyan(' - For WebSocket: Check wss://mcp.lanonasis.com/ws'));
209
250
  console.log(chalk.cyan(' - For local: Install local MCP server'));
210
251
  }
211
- else if (error.message?.includes('timeout') || error.message?.includes('ETIMEDOUT')) {
252
+ else if (msg.includes('timeout') || msg.includes('ETIMEDOUT')) {
212
253
  console.log(chalk.cyan('• Connection timeout. Check network:'));
213
254
  console.log(chalk.cyan(' - Verify internet connectivity'));
214
255
  console.log(chalk.cyan(' - Check firewall settings'));
215
256
  console.log(chalk.cyan(' - Try different connection mode: --mode remote'));
216
257
  }
217
- else if (error.message?.includes('ENOTFOUND') || error.message?.includes('getaddrinfo')) {
258
+ else if (msg.includes('ENOTFOUND') || msg.includes('getaddrinfo')) {
218
259
  console.log(chalk.cyan('• DNS resolution failed:'));
219
260
  console.log(chalk.cyan(' - Check DNS settings'));
220
261
  console.log(chalk.cyan(' - Verify server URL is correct'));
221
262
  console.log(chalk.cyan(' - Try using IP address instead of hostname'));
222
263
  }
223
- else if (error.message?.includes('certificate') || error.message?.includes('SSL') || error.message?.includes('TLS')) {
264
+ else if (msg.includes('certificate') || msg.includes('SSL') || msg.includes('TLS')) {
224
265
  console.log(chalk.cyan('• SSL/TLS certificate issue:'));
225
266
  console.log(chalk.cyan(' - Check system time and date'));
226
267
  console.log(chalk.cyan(' - Update CA certificates'));
@@ -297,7 +338,7 @@ export class MCPClient {
297
338
  }
298
339
  }
299
340
  }
300
- catch (error) {
341
+ catch {
301
342
  // If we can't decode the token, try to validate it with the server
302
343
  await this.validateTokenWithServer(token);
303
344
  }
@@ -323,7 +364,7 @@ export class MCPClient {
323
364
  console.log(chalk.green('✓ Token refreshed successfully'));
324
365
  }
325
366
  }
326
- catch (error) {
367
+ catch {
327
368
  throw new Error('Failed to refresh token. Please re-authenticate.');
328
369
  }
329
370
  }
@@ -343,10 +384,12 @@ export class MCPClient {
343
384
  });
344
385
  }
345
386
  catch (error) {
346
- if (error.response?.status === 401 || error.response?.status === 403) {
387
+ const status = error.response?.status;
388
+ if (status === 401 || status === 403) {
347
389
  throw new Error('Token is invalid or expired. Please re-authenticate.');
348
390
  }
349
- throw new Error(`Token validation failed: ${error.message}`);
391
+ const msg = error?.message || 'Unknown error';
392
+ throw new Error(`Token validation failed: ${msg}`);
350
393
  }
351
394
  }
352
395
  /**
@@ -498,7 +541,7 @@ export class MCPClient {
498
541
  break;
499
542
  }
500
543
  }
501
- catch (error) {
544
+ catch {
502
545
  console.log(chalk.yellow('⚠️ Health check failed, attempting reconnection...'));
503
546
  await this.handleHealthCheckFailure();
504
547
  }
@@ -536,8 +579,9 @@ export class MCPClient {
536
579
  timeout: 5000
537
580
  });
538
581
  }
539
- catch (error) {
540
- throw new Error(`Remote health check failed: ${error}`);
582
+ catch (e) {
583
+ const msg = e?.message ?? String(e);
584
+ throw new Error(`Remote health check failed: ${msg}`);
541
585
  }
542
586
  }
543
587
  /**
@@ -551,8 +595,9 @@ export class MCPClient {
551
595
  try {
552
596
  await this.client.listTools();
553
597
  }
554
- catch (error) {
555
- throw new Error(`Local health check failed: ${error}`);
598
+ catch (e) {
599
+ const msg = e?.message ?? String(e);
600
+ throw new Error(`Local health check failed: ${msg}`);
556
601
  }
557
602
  }
558
603
  /**
@@ -603,6 +648,7 @@ export class MCPClient {
603
648
  this.wsConnection = null;
604
649
  }
605
650
  this.isConnected = false;
651
+ this.activeConnectionMode = 'local'; // Reset to default
606
652
  }
607
653
  /**
608
654
  * Call an MCP tool
@@ -712,8 +758,7 @@ export class MCPClient {
712
758
  catch (error) {
713
759
  // Safely handle errors with type checking
714
760
  const errorObj = error;
715
- const errorMsg = errorObj.response?.data?.error ||
716
- (errorObj.message ? errorObj.message : 'Unknown error');
761
+ const errorMsg = errorObj.response?.data?.error || (errorObj.message ?? 'Unknown error');
717
762
  throw new Error(`Remote tool call failed: ${errorMsg}`);
718
763
  }
719
764
  }
@@ -757,8 +802,7 @@ export class MCPClient {
757
802
  * Get connection status details with health information
758
803
  */
759
804
  getConnectionStatus() {
760
- const connectionMode = this.config.get('mcpConnectionMode') ??
761
- (this.config.get('mcpUseRemote') ? 'remote' : 'local');
805
+ const connectionMode = this.activeConnectionMode;
762
806
  let server;
763
807
  switch (connectionMode) {
764
808
  case 'websocket':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lanonasis/cli",
3
- "version": "3.2.14",
3
+ "version": "3.4.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 {};