@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.
- package/README.md +57 -13
- package/dist/commands/auth.js +7 -7
- 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 +7 -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 +4 -5
- 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.js +3 -3
- package/dist/utils/config.js +59 -10
- package/dist/utils/mcp-client.d.ts +4 -2
- package/dist/utils/mcp-client.js +86 -42
- 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,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
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
98
|
-
// Local MCP server connection
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 &&
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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(
|
|
243
|
+
provideNetworkTroubleshootingGuidance(_error) {
|
|
204
244
|
console.log(chalk.yellow('\n🌐 Network Issue Detected:'));
|
|
205
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
540
|
-
|
|
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 (
|
|
555
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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 {};
|