@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.
- package/README.md +57 -13
- package/dist/commands/auth.js +11 -5
- package/dist/commands/completion.js +2 -0
- package/dist/commands/config.js +4 -4
- package/dist/commands/enhanced-memory.js +1 -1
- package/dist/commands/mcp.js +15 -9
- package/dist/core/achievements.js +1 -1
- package/dist/core/power-mode.js +5 -3
- package/dist/core/welcome.js +6 -6
- package/dist/enhanced-cli.js +6 -3
- package/dist/index-simple.js +5 -1
- package/dist/index.js +5 -1
- package/dist/mcp/access-control.d.ts +1 -1
- package/dist/mcp/access-control.js +4 -4
- package/dist/mcp/client/enhanced-client.js +5 -7
- package/dist/mcp/schemas/tool-schemas.d.ts +1 -1
- package/dist/mcp/schemas/tool-schemas.js +1 -1
- package/dist/mcp/server/lanonasis-server.d.ts +2 -1
- package/dist/mcp/server/lanonasis-server.js +7 -5
- package/dist/mcp/transports/transport-manager.js +3 -3
- package/dist/mcp-server.d.ts +1 -0
- package/dist/mcp-server.js +43 -9
- package/dist/utils/config.d.ts +10 -1
- package/dist/utils/config.js +97 -17
- package/dist/utils/mcp-client.d.ts +21 -2
- package/dist/utils/mcp-client.js +117 -46
- package/package.json +3 -3
- package/dist/__tests__/auth-persistence.test.d.ts +0 -1
- package/dist/__tests__/auth-persistence.test.js +0 -243
- package/dist/__tests__/cross-device-integration.test.d.ts +0 -1
- package/dist/__tests__/cross-device-integration.test.js +0 -305
- package/dist/__tests__/mcp-connection-reliability.test.d.ts +0 -1
- package/dist/__tests__/mcp-connection-reliability.test.js +0 -489
- package/dist/__tests__/setup.d.ts +0 -1
- package/dist/__tests__/setup.js +0 -26
- package/dist/mcp/server/mcp/server/lanonasis-server.js +0 -911
- package/dist/mcp/server/utils/api.js +0 -431
- package/dist/mcp/server/utils/config.js +0 -855
package/dist/utils/mcp-client.js
CHANGED
|
@@ -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
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
98
|
-
// Local MCP server connection
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 &&
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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(
|
|
268
|
+
provideNetworkTroubleshootingGuidance(_error) {
|
|
204
269
|
console.log(chalk.yellow('\n🌐 Network Issue Detected:'));
|
|
205
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
502
|
-
|
|
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 (
|
|
540
|
-
|
|
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 (
|
|
555
|
-
|
|
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.
|
|
636
|
+
const connectionMode = (this.activeConnectionMode || 'remote');
|
|
566
637
|
const options = {
|
|
567
|
-
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.
|
|
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
|
+
"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
|
|
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 {};
|