@lanonasis/cli 3.0.13 → 3.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/auth-persistence.test.d.ts +1 -0
- package/dist/__tests__/auth-persistence.test.js +243 -0
- package/dist/__tests__/cross-device-integration.test.d.ts +1 -0
- package/dist/__tests__/cross-device-integration.test.js +305 -0
- package/dist/__tests__/mcp-connection-reliability.test.d.ts +1 -0
- package/dist/__tests__/mcp-connection-reliability.test.js +489 -0
- package/dist/__tests__/setup.d.ts +1 -0
- package/dist/__tests__/setup.js +26 -0
- package/dist/commands/api-keys.js +12 -6
- package/dist/commands/auth.d.ts +1 -0
- package/dist/commands/auth.js +445 -19
- package/dist/commands/config.js +519 -1
- package/dist/commands/mcp.js +299 -0
- package/dist/index-simple.js +30 -0
- package/dist/index.js +35 -1
- package/dist/mcp/schemas/tool-schemas.d.ts +4 -4
- package/dist/mcp/server/lanonasis-server.d.ts +161 -6
- package/dist/mcp/server/lanonasis-server.js +813 -17
- package/dist/mcp/server/mcp/server/lanonasis-server.js +911 -0
- package/dist/mcp/server/utils/api.js +431 -0
- package/dist/mcp/server/utils/config.js +855 -0
- package/dist/utils/config.d.ts +57 -1
- package/dist/utils/config.js +530 -42
- package/dist/utils/mcp-client.d.ts +83 -2
- package/dist/utils/mcp-client.js +414 -15
- package/package.json +8 -4
package/dist/utils/config.js
CHANGED
|
@@ -2,16 +2,19 @@ import * as fs from 'fs/promises';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import * as os from 'os';
|
|
4
4
|
import { jwtDecode } from 'jwt-decode';
|
|
5
|
-
import {
|
|
6
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
-
const __dirname = path.dirname(__filename);
|
|
5
|
+
import { randomUUID } from 'crypto';
|
|
8
6
|
export class CLIConfig {
|
|
9
7
|
configDir;
|
|
10
8
|
configPath;
|
|
11
9
|
config = {};
|
|
10
|
+
lockFile;
|
|
11
|
+
static CONFIG_VERSION = '1.0.0';
|
|
12
|
+
authCheckCache = null;
|
|
13
|
+
AUTH_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
12
14
|
constructor() {
|
|
13
15
|
this.configDir = path.join(os.homedir(), '.maas');
|
|
14
16
|
this.configPath = path.join(this.configDir, 'config.json');
|
|
17
|
+
this.lockFile = path.join(this.configDir, 'config.lock');
|
|
15
18
|
}
|
|
16
19
|
async init() {
|
|
17
20
|
try {
|
|
@@ -26,28 +29,145 @@ export class CLIConfig {
|
|
|
26
29
|
try {
|
|
27
30
|
const data = await fs.readFile(this.configPath, 'utf-8');
|
|
28
31
|
this.config = JSON.parse(data);
|
|
32
|
+
// Handle version migration if needed
|
|
33
|
+
await this.migrateConfigIfNeeded();
|
|
29
34
|
}
|
|
30
35
|
catch {
|
|
31
36
|
this.config = {};
|
|
37
|
+
// Set version for new config
|
|
38
|
+
this.config.version = CLIConfig.CONFIG_VERSION;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async migrateConfigIfNeeded() {
|
|
42
|
+
const currentVersion = this.config.version;
|
|
43
|
+
if (!currentVersion) {
|
|
44
|
+
// Legacy config without version, migrate to current version
|
|
45
|
+
this.config.version = CLIConfig.CONFIG_VERSION;
|
|
46
|
+
// Perform any necessary migrations for legacy configs
|
|
47
|
+
// For now, just ensure the version is set
|
|
48
|
+
await this.save();
|
|
49
|
+
}
|
|
50
|
+
else if (currentVersion !== CLIConfig.CONFIG_VERSION) {
|
|
51
|
+
// Future version migrations would go here
|
|
52
|
+
// For now, just update the version
|
|
53
|
+
this.config.version = CLIConfig.CONFIG_VERSION;
|
|
54
|
+
await this.save();
|
|
32
55
|
}
|
|
33
56
|
}
|
|
34
57
|
async save() {
|
|
58
|
+
await this.atomicSave();
|
|
59
|
+
}
|
|
60
|
+
async atomicSave() {
|
|
35
61
|
await fs.mkdir(this.configDir, { recursive: true });
|
|
36
|
-
|
|
37
|
-
await
|
|
62
|
+
// Acquire file lock to prevent concurrent access
|
|
63
|
+
const lockAcquired = await this.acquireLock();
|
|
64
|
+
if (!lockAcquired) {
|
|
65
|
+
throw new Error('Could not acquire configuration lock. Another process may be modifying the config.');
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
// Set version and update timestamp
|
|
69
|
+
this.config.version = CLIConfig.CONFIG_VERSION;
|
|
70
|
+
this.config.lastUpdated = new Date().toISOString();
|
|
71
|
+
// Create temporary file with unique name
|
|
72
|
+
const tempPath = `${this.configPath}.tmp.${randomUUID()}`;
|
|
73
|
+
// Write to temporary file first
|
|
74
|
+
await fs.writeFile(tempPath, JSON.stringify(this.config, null, 2), 'utf-8');
|
|
75
|
+
// Atomic rename - this is the critical atomic operation
|
|
76
|
+
await fs.rename(tempPath, this.configPath);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
// Always release the lock
|
|
80
|
+
await this.releaseLock();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async backupConfig() {
|
|
84
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
85
|
+
const backupPath = path.join(this.configDir, `config.backup.${timestamp}.json`);
|
|
86
|
+
try {
|
|
87
|
+
// Check if config exists before backing up
|
|
88
|
+
await fs.access(this.configPath);
|
|
89
|
+
await fs.copyFile(this.configPath, backupPath);
|
|
90
|
+
return backupPath;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (error.code === 'ENOENT') {
|
|
94
|
+
// Config doesn't exist, create empty backup
|
|
95
|
+
await fs.writeFile(backupPath, JSON.stringify({}, null, 2));
|
|
96
|
+
return backupPath;
|
|
97
|
+
}
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async acquireLock(timeoutMs = 5000) {
|
|
102
|
+
const startTime = Date.now();
|
|
103
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
104
|
+
try {
|
|
105
|
+
// Try to create lock file exclusively
|
|
106
|
+
await fs.writeFile(this.lockFile, process.pid.toString(), { flag: 'wx' });
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (error.code === 'EEXIST') {
|
|
111
|
+
// Lock file exists, check if process is still running
|
|
112
|
+
try {
|
|
113
|
+
const pidStr = await fs.readFile(this.lockFile, 'utf-8');
|
|
114
|
+
const pid = parseInt(pidStr.trim());
|
|
115
|
+
if (!isNaN(pid)) {
|
|
116
|
+
try {
|
|
117
|
+
// Check if process is still running (works on Unix-like systems)
|
|
118
|
+
process.kill(pid, 0);
|
|
119
|
+
// Process is running, wait and retry
|
|
120
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Process is not running, remove stale lock
|
|
125
|
+
await fs.unlink(this.lockFile).catch(() => { });
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// Can't read lock file, remove it and retry
|
|
132
|
+
await fs.unlink(this.lockFile).catch(() => { });
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
async releaseLock() {
|
|
144
|
+
try {
|
|
145
|
+
await fs.unlink(this.lockFile);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Lock file might not exist or already removed, ignore
|
|
149
|
+
}
|
|
38
150
|
}
|
|
39
151
|
getApiUrl() {
|
|
40
152
|
return process.env.MEMORY_API_URL ||
|
|
41
153
|
this.config.apiUrl ||
|
|
42
154
|
'https://api.lanonasis.com/api/v1';
|
|
43
155
|
}
|
|
44
|
-
// Service Discovery Integration
|
|
45
|
-
async discoverServices() {
|
|
156
|
+
// Enhanced Service Discovery Integration
|
|
157
|
+
async discoverServices(verbose = false) {
|
|
158
|
+
const discoveryUrl = 'https://mcp.lanonasis.com/.well-known/onasis.json';
|
|
46
159
|
try {
|
|
47
160
|
// Use axios instead of fetch for consistency
|
|
48
161
|
const axios = (await import('axios')).default;
|
|
49
|
-
|
|
50
|
-
|
|
162
|
+
if (verbose) {
|
|
163
|
+
console.log(`🔍 Discovering services from ${discoveryUrl}...`);
|
|
164
|
+
}
|
|
165
|
+
const response = await axios.get(discoveryUrl, {
|
|
166
|
+
timeout: 10000,
|
|
167
|
+
headers: {
|
|
168
|
+
'User-Agent': 'Lanonasis-CLI/3.0.13'
|
|
169
|
+
}
|
|
170
|
+
});
|
|
51
171
|
// Map discovery response to our config format
|
|
52
172
|
const discovered = response.data;
|
|
53
173
|
this.config.discoveredServices = {
|
|
@@ -58,38 +178,241 @@ export class CLIConfig {
|
|
|
58
178
|
mcp_sse_base: discovered.endpoints?.sse || 'https://mcp.lanonasis.com/api/v1/events',
|
|
59
179
|
project_scope: 'lanonasis-maas'
|
|
60
180
|
};
|
|
181
|
+
// Mark discovery as successful
|
|
182
|
+
this.config.lastServiceDiscovery = new Date().toISOString();
|
|
61
183
|
await this.save();
|
|
184
|
+
if (verbose) {
|
|
185
|
+
console.log('✓ Service discovery completed successfully');
|
|
186
|
+
console.log(` Auth: ${this.config.discoveredServices.auth_base}`);
|
|
187
|
+
console.log(` MCP: ${this.config.discoveredServices.mcp_base}`);
|
|
188
|
+
console.log(` WebSocket: ${this.config.discoveredServices.mcp_ws_base}`);
|
|
189
|
+
}
|
|
62
190
|
}
|
|
63
|
-
catch {
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
191
|
+
catch (error) {
|
|
192
|
+
// Enhanced error handling with user-visible messages
|
|
193
|
+
await this.handleServiceDiscoveryFailure(error, verbose);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async handleServiceDiscoveryFailure(error, verbose) {
|
|
197
|
+
const errorType = this.categorizeServiceDiscoveryError(error);
|
|
198
|
+
if (verbose || process.env.CLI_VERBOSE === 'true') {
|
|
199
|
+
console.log('⚠️ Service discovery failed, using cached/fallback endpoints');
|
|
200
|
+
switch (errorType) {
|
|
201
|
+
case 'network_error':
|
|
202
|
+
console.log(' Reason: Network connection failed');
|
|
203
|
+
console.log(' This is normal when offline or behind restrictive firewalls');
|
|
204
|
+
break;
|
|
205
|
+
case 'timeout':
|
|
206
|
+
console.log(' Reason: Request timed out');
|
|
207
|
+
console.log(' The discovery service may be temporarily slow');
|
|
208
|
+
break;
|
|
209
|
+
case 'server_error':
|
|
210
|
+
console.log(' Reason: Discovery service returned an error');
|
|
211
|
+
console.log(' The service may be temporarily unavailable');
|
|
212
|
+
break;
|
|
213
|
+
case 'invalid_response':
|
|
214
|
+
console.log(' Reason: Invalid response format from discovery service');
|
|
215
|
+
console.log(' Using known working endpoints instead');
|
|
216
|
+
break;
|
|
217
|
+
default:
|
|
218
|
+
console.log(` Reason: ${error.message || 'Unknown error'}`);
|
|
67
219
|
}
|
|
68
|
-
// Set fallback service endpoints to prevent double slash issues
|
|
69
|
-
// Use mcp.lanonasis.com for MCP services (proxied to port 3001)
|
|
70
|
-
this.config.discoveredServices = {
|
|
71
|
-
auth_base: 'https://api.lanonasis.com', // CLI auth goes to central auth system
|
|
72
|
-
memory_base: 'https://api.lanonasis.com/api/v1', // Memory via onasis-core
|
|
73
|
-
mcp_base: 'https://mcp.lanonasis.com/api/v1', // MCP HTTP/REST
|
|
74
|
-
mcp_ws_base: 'wss://mcp.lanonasis.com/ws', // MCP WebSocket
|
|
75
|
-
mcp_sse_base: 'https://mcp.lanonasis.com/api/v1/events', // MCP SSE
|
|
76
|
-
project_scope: 'lanonasis-maas' // Correct project scope
|
|
77
|
-
};
|
|
78
|
-
await this.save();
|
|
79
220
|
}
|
|
221
|
+
// Use cached endpoints if available and recent (within 24 hours)
|
|
222
|
+
if (this.config.discoveredServices && this.config.lastServiceDiscovery) {
|
|
223
|
+
const lastDiscovery = new Date(this.config.lastServiceDiscovery);
|
|
224
|
+
const hoursSinceDiscovery = (Date.now() - lastDiscovery.getTime()) / (1000 * 60 * 60);
|
|
225
|
+
if (hoursSinceDiscovery < 24) {
|
|
226
|
+
if (verbose) {
|
|
227
|
+
console.log('✓ Using cached service endpoints (less than 24 hours old)');
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Set fallback service endpoints
|
|
233
|
+
this.config.discoveredServices = {
|
|
234
|
+
auth_base: 'https://api.lanonasis.com', // CLI auth goes to central auth system
|
|
235
|
+
memory_base: 'https://api.lanonasis.com/api/v1', // Memory via onasis-core
|
|
236
|
+
mcp_base: 'https://mcp.lanonasis.com/api/v1', // MCP HTTP/REST
|
|
237
|
+
mcp_ws_base: 'wss://mcp.lanonasis.com/ws', // MCP WebSocket
|
|
238
|
+
mcp_sse_base: 'https://mcp.lanonasis.com/api/v1/events', // MCP SSE
|
|
239
|
+
project_scope: 'lanonasis-maas' // Correct project scope
|
|
240
|
+
};
|
|
241
|
+
// Mark as fallback (don't set lastServiceDiscovery)
|
|
242
|
+
await this.save();
|
|
243
|
+
if (verbose) {
|
|
244
|
+
console.log('✓ Using fallback service endpoints');
|
|
245
|
+
console.log(' These are the standard production endpoints');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
categorizeServiceDiscoveryError(error) {
|
|
249
|
+
if (error.code) {
|
|
250
|
+
switch (error.code) {
|
|
251
|
+
case 'ECONNREFUSED':
|
|
252
|
+
case 'ENOTFOUND':
|
|
253
|
+
case 'ECONNRESET':
|
|
254
|
+
case 'ENETUNREACH':
|
|
255
|
+
return 'network_error';
|
|
256
|
+
case 'ETIMEDOUT':
|
|
257
|
+
return 'timeout';
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (error.response?.status >= 500) {
|
|
261
|
+
return 'server_error';
|
|
262
|
+
}
|
|
263
|
+
if (error.response?.status === 404) {
|
|
264
|
+
return 'invalid_response';
|
|
265
|
+
}
|
|
266
|
+
const message = error.message?.toLowerCase() || '';
|
|
267
|
+
if (message.includes('timeout')) {
|
|
268
|
+
return 'timeout';
|
|
269
|
+
}
|
|
270
|
+
if (message.includes('network') || message.includes('connection')) {
|
|
271
|
+
return 'network_error';
|
|
272
|
+
}
|
|
273
|
+
return 'unknown';
|
|
274
|
+
}
|
|
275
|
+
// Manual endpoint override functionality
|
|
276
|
+
async setManualEndpoints(endpoints) {
|
|
277
|
+
if (!this.config.discoveredServices) {
|
|
278
|
+
// Initialize with defaults first
|
|
279
|
+
await this.discoverServices();
|
|
280
|
+
}
|
|
281
|
+
// Merge manual overrides with existing endpoints
|
|
282
|
+
this.config.discoveredServices = {
|
|
283
|
+
...this.config.discoveredServices,
|
|
284
|
+
...endpoints
|
|
285
|
+
};
|
|
286
|
+
// Mark as manually configured
|
|
287
|
+
this.config.manualEndpointOverrides = true;
|
|
288
|
+
this.config.lastManualEndpointUpdate = new Date().toISOString();
|
|
289
|
+
await this.save();
|
|
290
|
+
}
|
|
291
|
+
hasManualEndpointOverrides() {
|
|
292
|
+
return !!this.config.manualEndpointOverrides;
|
|
293
|
+
}
|
|
294
|
+
async clearManualEndpointOverrides() {
|
|
295
|
+
this.config.manualEndpointOverrides = undefined;
|
|
296
|
+
this.config.lastManualEndpointUpdate = undefined;
|
|
297
|
+
// Rediscover services
|
|
298
|
+
await this.discoverServices();
|
|
80
299
|
}
|
|
81
300
|
getDiscoveredApiUrl() {
|
|
82
301
|
return this.config.discoveredServices?.auth_base || this.getApiUrl();
|
|
83
302
|
}
|
|
84
303
|
// Enhanced authentication support
|
|
85
304
|
async setVendorKey(vendorKey) {
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
305
|
+
// Enhanced format validation with detailed error messages
|
|
306
|
+
const formatValidation = this.validateVendorKeyFormat(vendorKey);
|
|
307
|
+
if (formatValidation !== true) {
|
|
308
|
+
throw new Error(typeof formatValidation === 'string' ? formatValidation : 'Invalid vendor key format');
|
|
89
309
|
}
|
|
310
|
+
// Server-side validation
|
|
311
|
+
await this.validateVendorKeyWithServer(vendorKey);
|
|
90
312
|
this.config.vendorKey = vendorKey;
|
|
313
|
+
this.config.authMethod = 'vendor_key';
|
|
314
|
+
this.config.lastValidated = new Date().toISOString();
|
|
315
|
+
await this.resetFailureCount(); // Reset failure count on successful auth
|
|
91
316
|
await this.save();
|
|
92
317
|
}
|
|
318
|
+
validateVendorKeyFormat(vendorKey) {
|
|
319
|
+
if (!vendorKey || vendorKey.trim().length === 0) {
|
|
320
|
+
return 'Vendor key is required';
|
|
321
|
+
}
|
|
322
|
+
const trimmed = vendorKey.trim();
|
|
323
|
+
// Check basic format
|
|
324
|
+
if (!trimmed.includes('.')) {
|
|
325
|
+
return 'Invalid vendor key format: Must contain a dot (.) separator. Expected format: pk_xxx.sk_xxx';
|
|
326
|
+
}
|
|
327
|
+
const parts = trimmed.split('.');
|
|
328
|
+
if (parts.length !== 2) {
|
|
329
|
+
return 'Invalid vendor key format: Must have exactly two parts separated by a dot. Expected format: pk_xxx.sk_xxx';
|
|
330
|
+
}
|
|
331
|
+
const [publicPart, secretPart] = parts;
|
|
332
|
+
// Validate public key part
|
|
333
|
+
if (!publicPart.startsWith('pk_')) {
|
|
334
|
+
return 'Invalid vendor key format: First part must start with "pk_". Expected format: pk_xxx.sk_xxx';
|
|
335
|
+
}
|
|
336
|
+
if (publicPart.length < 11) { // pk_ + minimum 8 chars
|
|
337
|
+
return 'Invalid vendor key format: Public key part is too short. Expected format: pk_xxx.sk_xxx (minimum 8 characters after "pk_")';
|
|
338
|
+
}
|
|
339
|
+
const publicKeyContent = publicPart.substring(3); // Remove 'pk_'
|
|
340
|
+
if (!/^[a-zA-Z0-9]+$/.test(publicKeyContent)) {
|
|
341
|
+
return 'Invalid vendor key format: Public key part contains invalid characters. Only letters and numbers are allowed after "pk_"';
|
|
342
|
+
}
|
|
343
|
+
// Validate secret key part
|
|
344
|
+
if (!secretPart.startsWith('sk_')) {
|
|
345
|
+
return 'Invalid vendor key format: Second part must start with "sk_". Expected format: pk_xxx.sk_xxx';
|
|
346
|
+
}
|
|
347
|
+
if (secretPart.length < 19) { // sk_ + minimum 16 chars
|
|
348
|
+
return 'Invalid vendor key format: Secret key part is too short. Expected format: pk_xxx.sk_xxx (minimum 16 characters after "sk_")';
|
|
349
|
+
}
|
|
350
|
+
const secretKeyContent = secretPart.substring(3); // Remove 'sk_'
|
|
351
|
+
if (!/^[a-zA-Z0-9]+$/.test(secretKeyContent)) {
|
|
352
|
+
return 'Invalid vendor key format: Secret key part contains invalid characters. Only letters and numbers are allowed after "sk_"';
|
|
353
|
+
}
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
async validateVendorKeyWithServer(vendorKey) {
|
|
357
|
+
try {
|
|
358
|
+
// Import axios dynamically to avoid circular dependency
|
|
359
|
+
const axios = (await import('axios')).default;
|
|
360
|
+
// Ensure service discovery is done
|
|
361
|
+
await this.discoverServices();
|
|
362
|
+
const authBase = this.config.discoveredServices?.auth_base || 'https://api.lanonasis.com';
|
|
363
|
+
// Test vendor key with health endpoint
|
|
364
|
+
await axios.get(`${authBase}/api/v1/health`, {
|
|
365
|
+
headers: {
|
|
366
|
+
'X-API-Key': vendorKey,
|
|
367
|
+
'X-Auth-Method': 'vendor_key',
|
|
368
|
+
'X-Project-Scope': 'lanonasis-maas'
|
|
369
|
+
},
|
|
370
|
+
timeout: 10000
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
// Provide specific error messages based on response
|
|
375
|
+
if (error.response?.status === 401) {
|
|
376
|
+
const errorData = error.response.data;
|
|
377
|
+
if (errorData?.error?.includes('expired') || errorData?.message?.includes('expired')) {
|
|
378
|
+
throw new Error('Vendor key has expired. Please generate a new key from your dashboard.');
|
|
379
|
+
}
|
|
380
|
+
else if (errorData?.error?.includes('revoked') || errorData?.message?.includes('revoked')) {
|
|
381
|
+
throw new Error('Vendor key has been revoked. Please generate a new key from your dashboard.');
|
|
382
|
+
}
|
|
383
|
+
else if (errorData?.error?.includes('invalid') || errorData?.message?.includes('invalid')) {
|
|
384
|
+
throw new Error('Vendor key is invalid. Please check the key format and ensure it was copied correctly.');
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
throw new Error('Vendor key authentication failed. The key may be invalid, expired, or revoked.');
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
else if (error.response?.status === 403) {
|
|
391
|
+
throw new Error('Vendor key access denied. The key may not have sufficient permissions for this operation.');
|
|
392
|
+
}
|
|
393
|
+
else if (error.response?.status === 429) {
|
|
394
|
+
throw new Error('Too many validation attempts. Please wait a moment before trying again.');
|
|
395
|
+
}
|
|
396
|
+
else if (error.response?.status >= 500) {
|
|
397
|
+
throw new Error('Server error during validation. Please try again in a few moments.');
|
|
398
|
+
}
|
|
399
|
+
else if (error.code === 'ECONNREFUSED') {
|
|
400
|
+
throw new Error('Cannot connect to authentication server. Please check your internet connection and try again.');
|
|
401
|
+
}
|
|
402
|
+
else if (error.code === 'ENOTFOUND') {
|
|
403
|
+
throw new Error('Authentication server not found. Please check your internet connection.');
|
|
404
|
+
}
|
|
405
|
+
else if (error.code === 'ETIMEDOUT') {
|
|
406
|
+
throw new Error('Validation request timed out. Please check your internet connection and try again.');
|
|
407
|
+
}
|
|
408
|
+
else if (error.code === 'ECONNRESET') {
|
|
409
|
+
throw new Error('Connection was reset during validation. Please try again.');
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
throw new Error(`Vendor key validation failed: ${error.message || 'Unknown error'}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
93
416
|
getVendorKey() {
|
|
94
417
|
return this.config.vendorKey;
|
|
95
418
|
}
|
|
@@ -102,11 +425,17 @@ export class CLIConfig {
|
|
|
102
425
|
}
|
|
103
426
|
async setToken(token) {
|
|
104
427
|
this.config.token = token;
|
|
105
|
-
|
|
428
|
+
this.config.authMethod = 'jwt';
|
|
429
|
+
this.config.lastValidated = new Date().toISOString();
|
|
430
|
+
await this.resetFailureCount(); // Reset failure count on successful auth
|
|
431
|
+
// Decode token to get user info and expiry
|
|
106
432
|
try {
|
|
107
433
|
const decoded = jwtDecode(token);
|
|
108
|
-
//
|
|
109
|
-
|
|
434
|
+
// Store token expiry
|
|
435
|
+
if (typeof decoded.exp === 'number') {
|
|
436
|
+
this.config.tokenExpiry = decoded.exp;
|
|
437
|
+
}
|
|
438
|
+
// Store user info
|
|
110
439
|
this.config.user = {
|
|
111
440
|
email: String(decoded.email || ''),
|
|
112
441
|
organization_id: String(decoded.organizationId || ''),
|
|
@@ -115,7 +444,8 @@ export class CLIConfig {
|
|
|
115
444
|
};
|
|
116
445
|
}
|
|
117
446
|
catch {
|
|
118
|
-
// Invalid token, don't store user info
|
|
447
|
+
// Invalid token, don't store user info or expiry
|
|
448
|
+
this.config.tokenExpiry = undefined;
|
|
119
449
|
}
|
|
120
450
|
await this.save();
|
|
121
451
|
}
|
|
@@ -129,30 +459,60 @@ export class CLIConfig {
|
|
|
129
459
|
const token = this.getToken();
|
|
130
460
|
if (!token)
|
|
131
461
|
return false;
|
|
462
|
+
// Check cache first
|
|
463
|
+
if (this.authCheckCache && (Date.now() - this.authCheckCache.timestamp) < this.AUTH_CACHE_TTL) {
|
|
464
|
+
return this.authCheckCache.isValid;
|
|
465
|
+
}
|
|
466
|
+
// Local expiry check first (fast)
|
|
467
|
+
let locallyValid = false;
|
|
132
468
|
// Handle simple CLI tokens (format: cli_xxx_timestamp)
|
|
133
469
|
if (token.startsWith('cli_')) {
|
|
134
470
|
// Extract timestamp from CLI token
|
|
135
471
|
const parts = token.split('_');
|
|
136
472
|
if (parts.length >= 3) {
|
|
137
|
-
const
|
|
473
|
+
const lastPart = parts[parts.length - 1];
|
|
474
|
+
const timestamp = lastPart ? parseInt(lastPart) : NaN;
|
|
138
475
|
if (!isNaN(timestamp)) {
|
|
139
476
|
// CLI tokens are valid for 30 days
|
|
140
477
|
const thirtyDaysInMs = 30 * 24 * 60 * 60 * 1000;
|
|
141
|
-
|
|
478
|
+
locallyValid = (Date.now() - timestamp) < thirtyDaysInMs;
|
|
142
479
|
}
|
|
143
480
|
}
|
|
144
|
-
|
|
145
|
-
|
|
481
|
+
else {
|
|
482
|
+
locallyValid = true; // Fallback for old format
|
|
483
|
+
}
|
|
146
484
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
485
|
+
else {
|
|
486
|
+
// Handle JWT tokens
|
|
487
|
+
try {
|
|
488
|
+
const decoded = jwtDecode(token);
|
|
489
|
+
const now = Date.now() / 1000;
|
|
490
|
+
locallyValid = typeof decoded.exp === 'number' && decoded.exp > now;
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
locallyValid = false;
|
|
494
|
+
}
|
|
152
495
|
}
|
|
153
|
-
|
|
496
|
+
// If expired locally, no need to check server
|
|
497
|
+
if (!locallyValid) {
|
|
498
|
+
this.authCheckCache = { isValid: false, timestamp: Date.now() };
|
|
154
499
|
return false;
|
|
155
500
|
}
|
|
501
|
+
// Verify with server (security check)
|
|
502
|
+
try {
|
|
503
|
+
const axios = (await import('axios')).default;
|
|
504
|
+
const response = await axios.post('https://api.lanonasis.com/auth/verify', { token }, { timeout: 5000 });
|
|
505
|
+
const isValid = response.data.valid === true;
|
|
506
|
+
this.authCheckCache = { isValid, timestamp: Date.now() };
|
|
507
|
+
return isValid;
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
// If server check fails, fall back to local validation
|
|
511
|
+
// This allows offline usage but is less secure
|
|
512
|
+
console.warn('⚠️ Unable to verify token with server, using local validation');
|
|
513
|
+
this.authCheckCache = { isValid: locallyValid, timestamp: Date.now() };
|
|
514
|
+
return locallyValid;
|
|
515
|
+
}
|
|
156
516
|
}
|
|
157
517
|
async logout() {
|
|
158
518
|
this.config.token = undefined;
|
|
@@ -175,6 +535,134 @@ export class CLIConfig {
|
|
|
175
535
|
return false;
|
|
176
536
|
}
|
|
177
537
|
}
|
|
538
|
+
// Enhanced credential validation methods
|
|
539
|
+
async validateStoredCredentials() {
|
|
540
|
+
try {
|
|
541
|
+
const vendorKey = this.getVendorKey();
|
|
542
|
+
const token = this.getToken();
|
|
543
|
+
if (!vendorKey && !token) {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
// Import axios dynamically to avoid circular dependency
|
|
547
|
+
const axios = (await import('axios')).default;
|
|
548
|
+
// Ensure service discovery is done
|
|
549
|
+
await this.discoverServices();
|
|
550
|
+
const authBase = this.config.discoveredServices?.auth_base || 'https://api.lanonasis.com';
|
|
551
|
+
const headers = {
|
|
552
|
+
'X-Project-Scope': 'lanonasis-maas'
|
|
553
|
+
};
|
|
554
|
+
if (vendorKey) {
|
|
555
|
+
headers['X-API-Key'] = vendorKey;
|
|
556
|
+
headers['X-Auth-Method'] = 'vendor_key';
|
|
557
|
+
}
|
|
558
|
+
else if (token) {
|
|
559
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
560
|
+
headers['X-Auth-Method'] = 'jwt';
|
|
561
|
+
}
|
|
562
|
+
// Validate against server with health endpoint
|
|
563
|
+
await axios.get(`${authBase}/api/v1/health`, {
|
|
564
|
+
headers,
|
|
565
|
+
timeout: 10000
|
|
566
|
+
});
|
|
567
|
+
// Update last validated timestamp
|
|
568
|
+
this.config.lastValidated = new Date().toISOString();
|
|
569
|
+
await this.resetFailureCount();
|
|
570
|
+
await this.save();
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
// Increment failure count
|
|
575
|
+
await this.incrementFailureCount();
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
async refreshTokenIfNeeded() {
|
|
580
|
+
const token = this.getToken();
|
|
581
|
+
if (!token) {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
try {
|
|
585
|
+
// Check if token is JWT and if it's close to expiry
|
|
586
|
+
if (token.startsWith('cli_')) {
|
|
587
|
+
// CLI tokens don't need refresh, they're long-lived
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
const decoded = jwtDecode(token);
|
|
591
|
+
const now = Date.now() / 1000;
|
|
592
|
+
const exp = typeof decoded.exp === 'number' ? decoded.exp : 0;
|
|
593
|
+
// Refresh if token expires within 5 minutes
|
|
594
|
+
if (exp > 0 && (exp - now) < 300) {
|
|
595
|
+
// Import axios dynamically
|
|
596
|
+
const axios = (await import('axios')).default;
|
|
597
|
+
await this.discoverServices();
|
|
598
|
+
const authBase = this.config.discoveredServices?.auth_base || 'https://api.lanonasis.com';
|
|
599
|
+
// Attempt token refresh
|
|
600
|
+
const response = await axios.post(`${authBase}/v1/auth/refresh`, {}, {
|
|
601
|
+
headers: {
|
|
602
|
+
'Authorization': `Bearer ${token}`,
|
|
603
|
+
'X-Project-Scope': 'lanonasis-maas'
|
|
604
|
+
},
|
|
605
|
+
timeout: 10000
|
|
606
|
+
});
|
|
607
|
+
if (response.data.token) {
|
|
608
|
+
await this.setToken(response.data.token);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
catch (error) {
|
|
613
|
+
// If refresh fails, mark credentials as potentially invalid
|
|
614
|
+
await this.incrementFailureCount();
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
async clearInvalidCredentials() {
|
|
618
|
+
this.config.token = undefined;
|
|
619
|
+
this.config.vendorKey = undefined;
|
|
620
|
+
this.config.user = undefined;
|
|
621
|
+
this.config.authMethod = undefined;
|
|
622
|
+
this.config.tokenExpiry = undefined;
|
|
623
|
+
this.config.lastValidated = undefined;
|
|
624
|
+
this.config.authFailureCount = 0;
|
|
625
|
+
this.config.lastAuthFailure = undefined;
|
|
626
|
+
await this.save();
|
|
627
|
+
}
|
|
628
|
+
async incrementFailureCount() {
|
|
629
|
+
this.config.authFailureCount = (this.config.authFailureCount || 0) + 1;
|
|
630
|
+
this.config.lastAuthFailure = new Date().toISOString();
|
|
631
|
+
await this.save();
|
|
632
|
+
}
|
|
633
|
+
async resetFailureCount() {
|
|
634
|
+
this.config.authFailureCount = 0;
|
|
635
|
+
this.config.lastAuthFailure = undefined;
|
|
636
|
+
await this.save();
|
|
637
|
+
}
|
|
638
|
+
getFailureCount() {
|
|
639
|
+
return this.config.authFailureCount || 0;
|
|
640
|
+
}
|
|
641
|
+
getLastAuthFailure() {
|
|
642
|
+
return this.config.lastAuthFailure;
|
|
643
|
+
}
|
|
644
|
+
shouldDelayAuth() {
|
|
645
|
+
const failureCount = this.getFailureCount();
|
|
646
|
+
return failureCount >= 3;
|
|
647
|
+
}
|
|
648
|
+
getAuthDelayMs() {
|
|
649
|
+
const failureCount = this.getFailureCount();
|
|
650
|
+
if (failureCount < 3)
|
|
651
|
+
return 0;
|
|
652
|
+
// Progressive delays: 3 failures = 2s, 4 = 4s, 5 = 8s, 6+ = 16s max
|
|
653
|
+
const baseDelay = 2000; // 2 seconds
|
|
654
|
+
const maxDelay = 16000; // 16 seconds max
|
|
655
|
+
const delay = Math.min(baseDelay * Math.pow(2, failureCount - 3), maxDelay);
|
|
656
|
+
return delay;
|
|
657
|
+
}
|
|
658
|
+
async getDeviceId() {
|
|
659
|
+
if (!this.config.deviceId) {
|
|
660
|
+
// Generate a new device ID
|
|
661
|
+
this.config.deviceId = randomUUID();
|
|
662
|
+
await this.save();
|
|
663
|
+
}
|
|
664
|
+
return this.config.deviceId;
|
|
665
|
+
}
|
|
178
666
|
// Generic get/set methods for MCP and other dynamic config
|
|
179
667
|
get(key) {
|
|
180
668
|
return this.config[key];
|
|
@@ -188,7 +676,7 @@ export class CLIConfig {
|
|
|
188
676
|
}
|
|
189
677
|
// MCP-specific helpers
|
|
190
678
|
getMCPServerPath() {
|
|
191
|
-
return this.config.mcpServerPath || path.join(
|
|
679
|
+
return this.config.mcpServerPath || path.join(process.cwd(), 'onasis-gateway/mcp-server/server.js');
|
|
192
680
|
}
|
|
193
681
|
getMCPServerUrl() {
|
|
194
682
|
return this.config.discoveredServices?.mcp_ws_base ||
|