@lanonasis/cli 3.0.13 → 3.1.13
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/commands/auth.js +56 -0
- package/dist/index-simple.js +30 -0
- package/dist/index.js +30 -0
- package/dist/mcp/schemas/tool-schemas.d.ts +4 -4
- package/dist/utils/config.d.ts +17 -0
- package/dist/utils/config.js +283 -11
- package/package.json +1 -1
package/dist/commands/auth.js
CHANGED
|
@@ -15,6 +15,27 @@ const colors = {
|
|
|
15
15
|
muted: chalk.gray,
|
|
16
16
|
highlight: chalk.white.bold
|
|
17
17
|
};
|
|
18
|
+
// Helper function to handle authentication delays
|
|
19
|
+
async function handleAuthDelay(config) {
|
|
20
|
+
if (config.shouldDelayAuth()) {
|
|
21
|
+
const delayMs = config.getAuthDelayMs();
|
|
22
|
+
const failureCount = config.getFailureCount();
|
|
23
|
+
const lastFailure = config.getLastAuthFailure();
|
|
24
|
+
console.log();
|
|
25
|
+
console.log(chalk.yellow(`⚠️ Multiple authentication failures detected (${failureCount} attempts)`));
|
|
26
|
+
if (lastFailure) {
|
|
27
|
+
const lastFailureDate = new Date(lastFailure);
|
|
28
|
+
console.log(chalk.gray(`Last failure: ${lastFailureDate.toLocaleString()}`));
|
|
29
|
+
}
|
|
30
|
+
console.log(chalk.yellow(`Waiting ${Math.round(delayMs / 1000)} seconds before retry...`));
|
|
31
|
+
console.log(chalk.gray('This delay helps prevent account lockouts and reduces server load.'));
|
|
32
|
+
// Show countdown
|
|
33
|
+
const spinner = ora(`Waiting ${Math.round(delayMs / 1000)} seconds...`).start();
|
|
34
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
35
|
+
spinner.succeed('Ready to retry authentication');
|
|
36
|
+
console.log();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
18
39
|
export async function loginCommand(options) {
|
|
19
40
|
const config = new CLIConfig();
|
|
20
41
|
await config.init();
|
|
@@ -74,6 +95,8 @@ export async function loginCommand(options) {
|
|
|
74
95
|
}
|
|
75
96
|
}
|
|
76
97
|
async function handleVendorKeyAuth(vendorKey, config) {
|
|
98
|
+
// Check for authentication delay before attempting
|
|
99
|
+
await handleAuthDelay(config);
|
|
77
100
|
const spinner = ora('Validating vendor key...').start();
|
|
78
101
|
try {
|
|
79
102
|
await config.setVendorKey(vendorKey);
|
|
@@ -86,8 +109,19 @@ async function handleVendorKeyAuth(vendorKey, config) {
|
|
|
86
109
|
}
|
|
87
110
|
catch (error) {
|
|
88
111
|
spinner.fail('Vendor key validation failed');
|
|
112
|
+
// Increment failure count for failed authentication
|
|
113
|
+
await config.incrementFailureCount();
|
|
89
114
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
90
115
|
console.error(chalk.red('✖ Invalid vendor key:'), errorMessage);
|
|
116
|
+
// Provide guidance for repeated failures
|
|
117
|
+
const failureCount = config.getFailureCount();
|
|
118
|
+
if (failureCount >= 3) {
|
|
119
|
+
console.log();
|
|
120
|
+
console.log(chalk.yellow('💡 Troubleshooting tips:'));
|
|
121
|
+
console.log(chalk.gray('• Verify your vendor key format: pk_xxx.sk_xxx'));
|
|
122
|
+
console.log(chalk.gray('• Check if your key is active in your account dashboard'));
|
|
123
|
+
console.log(chalk.gray('• Try: lanonasis auth logout && lanonasis auth login'));
|
|
124
|
+
}
|
|
91
125
|
process.exit(1);
|
|
92
126
|
}
|
|
93
127
|
}
|
|
@@ -174,6 +208,8 @@ async function handleCredentialsFlow(options, config) {
|
|
|
174
208
|
console.log();
|
|
175
209
|
console.log(chalk.yellow('⚙️ Username/Password Authentication'));
|
|
176
210
|
console.log();
|
|
211
|
+
// Check for authentication delay before attempting
|
|
212
|
+
await handleAuthDelay(config);
|
|
177
213
|
let { email, password } = options;
|
|
178
214
|
// Get credentials if not provided
|
|
179
215
|
if (!email || !password) {
|
|
@@ -215,10 +251,21 @@ async function handleCredentialsFlow(options, config) {
|
|
|
215
251
|
}
|
|
216
252
|
catch (error) {
|
|
217
253
|
spinner.fail('Login failed');
|
|
254
|
+
// Increment failure count for failed authentication
|
|
255
|
+
await config.incrementFailureCount();
|
|
218
256
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
219
257
|
const errorResponse = error && typeof error === 'object' && 'response' in error ? error.response : null;
|
|
220
258
|
if (errorResponse && typeof errorResponse === 'object' && 'status' in errorResponse && errorResponse.status === 401) {
|
|
221
259
|
console.error(chalk.red('✖ Invalid email or password'));
|
|
260
|
+
// Provide guidance for repeated failures
|
|
261
|
+
const failureCount = config.getFailureCount();
|
|
262
|
+
if (failureCount >= 3) {
|
|
263
|
+
console.log();
|
|
264
|
+
console.log(chalk.yellow('💡 Multiple login failures detected. Consider:'));
|
|
265
|
+
console.log(chalk.gray('• Double-check your email and password'));
|
|
266
|
+
console.log(chalk.gray('• Reset your password if needed'));
|
|
267
|
+
console.log(chalk.gray('• Try using a vendor key instead: lanonasis auth login --vendor-key'));
|
|
268
|
+
}
|
|
222
269
|
// Ask if they want to register
|
|
223
270
|
const answer = await inquirer.prompt([
|
|
224
271
|
{
|
|
@@ -234,6 +281,15 @@ async function handleCredentialsFlow(options, config) {
|
|
|
234
281
|
}
|
|
235
282
|
else {
|
|
236
283
|
console.error(chalk.red('✖ Login failed:'), errorMessage);
|
|
284
|
+
// Provide guidance for repeated failures
|
|
285
|
+
const failureCount = config.getFailureCount();
|
|
286
|
+
if (failureCount >= 3) {
|
|
287
|
+
console.log();
|
|
288
|
+
console.log(chalk.yellow('💡 Connection issues detected. Try:'));
|
|
289
|
+
console.log(chalk.gray('• Check your internet connection'));
|
|
290
|
+
console.log(chalk.gray('• Verify the service is available'));
|
|
291
|
+
console.log(chalk.gray('• Try again later'));
|
|
292
|
+
}
|
|
237
293
|
}
|
|
238
294
|
process.exit(1);
|
|
239
295
|
}
|
package/dist/index-simple.js
CHANGED
|
@@ -247,16 +247,46 @@ authCmd
|
|
|
247
247
|
.action(async () => {
|
|
248
248
|
const isAuth = await cliConfig.isAuthenticated();
|
|
249
249
|
const user = await cliConfig.getCurrentUser();
|
|
250
|
+
const failureCount = cliConfig.getFailureCount();
|
|
251
|
+
const lastFailure = cliConfig.getLastAuthFailure();
|
|
252
|
+
const authMethod = cliConfig.get('authMethod');
|
|
253
|
+
const lastValidated = cliConfig.get('lastValidated');
|
|
254
|
+
console.log(chalk.blue.bold('🔐 Authentication Status'));
|
|
255
|
+
console.log('━'.repeat(40));
|
|
250
256
|
if (isAuth && user) {
|
|
251
257
|
console.log(chalk.green('✓ Authenticated'));
|
|
252
258
|
console.log(`Email: ${user.email}`);
|
|
253
259
|
console.log(`Organization: ${user.organization_id}`);
|
|
254
260
|
console.log(`Plan: ${user.plan}`);
|
|
261
|
+
if (authMethod) {
|
|
262
|
+
console.log(`Method: ${authMethod}`);
|
|
263
|
+
}
|
|
264
|
+
if (lastValidated) {
|
|
265
|
+
const validatedDate = new Date(lastValidated);
|
|
266
|
+
console.log(`Last validated: ${validatedDate.toLocaleString()}`);
|
|
267
|
+
}
|
|
255
268
|
}
|
|
256
269
|
else {
|
|
257
270
|
console.log(chalk.red('✖ Not authenticated'));
|
|
258
271
|
console.log(chalk.yellow('Run:'), chalk.white('memory login'));
|
|
259
272
|
}
|
|
273
|
+
// Show failure tracking information
|
|
274
|
+
if (failureCount > 0) {
|
|
275
|
+
console.log();
|
|
276
|
+
console.log(chalk.yellow('⚠️ Authentication Issues:'));
|
|
277
|
+
console.log(`Failed attempts: ${failureCount}`);
|
|
278
|
+
if (lastFailure) {
|
|
279
|
+
const failureDate = new Date(lastFailure);
|
|
280
|
+
console.log(`Last failure: ${failureDate.toLocaleString()}`);
|
|
281
|
+
}
|
|
282
|
+
if (cliConfig.shouldDelayAuth()) {
|
|
283
|
+
const delayMs = cliConfig.getAuthDelayMs();
|
|
284
|
+
console.log(chalk.yellow(`Next retry delay: ${Math.round(delayMs / 1000)} seconds`));
|
|
285
|
+
}
|
|
286
|
+
console.log();
|
|
287
|
+
console.log(chalk.cyan('💡 To reset failure count:'));
|
|
288
|
+
console.log(chalk.white(' lanonasis auth logout && lanonasis auth login'));
|
|
289
|
+
}
|
|
260
290
|
});
|
|
261
291
|
// MCP Commands (primary interface)
|
|
262
292
|
mcpCommands(program);
|
package/dist/index.js
CHANGED
|
@@ -211,16 +211,46 @@ authCmd
|
|
|
211
211
|
.action(async () => {
|
|
212
212
|
const isAuth = await cliConfig.isAuthenticated();
|
|
213
213
|
const user = await cliConfig.getCurrentUser();
|
|
214
|
+
const failureCount = cliConfig.getFailureCount();
|
|
215
|
+
const lastFailure = cliConfig.getLastAuthFailure();
|
|
216
|
+
const authMethod = cliConfig.get('authMethod');
|
|
217
|
+
const lastValidated = cliConfig.get('lastValidated');
|
|
218
|
+
console.log(chalk.blue.bold('🔐 Authentication Status'));
|
|
219
|
+
console.log('━'.repeat(40));
|
|
214
220
|
if (isAuth && user) {
|
|
215
221
|
console.log(chalk.green('✓ Authenticated'));
|
|
216
222
|
console.log(`Email: ${user.email}`);
|
|
217
223
|
console.log(`Organization: ${user.organization_id}`);
|
|
218
224
|
console.log(`Plan: ${user.plan}`);
|
|
225
|
+
if (authMethod) {
|
|
226
|
+
console.log(`Method: ${authMethod}`);
|
|
227
|
+
}
|
|
228
|
+
if (lastValidated) {
|
|
229
|
+
const validatedDate = new Date(lastValidated);
|
|
230
|
+
console.log(`Last validated: ${validatedDate.toLocaleString()}`);
|
|
231
|
+
}
|
|
219
232
|
}
|
|
220
233
|
else {
|
|
221
234
|
console.log(chalk.red('✖ Not authenticated'));
|
|
222
235
|
console.log(chalk.yellow('Run:'), chalk.white('memory login'));
|
|
223
236
|
}
|
|
237
|
+
// Show failure tracking information
|
|
238
|
+
if (failureCount > 0) {
|
|
239
|
+
console.log();
|
|
240
|
+
console.log(chalk.yellow('⚠️ Authentication Issues:'));
|
|
241
|
+
console.log(`Failed attempts: ${failureCount}`);
|
|
242
|
+
if (lastFailure) {
|
|
243
|
+
const failureDate = new Date(lastFailure);
|
|
244
|
+
console.log(`Last failure: ${failureDate.toLocaleString()}`);
|
|
245
|
+
}
|
|
246
|
+
if (cliConfig.shouldDelayAuth()) {
|
|
247
|
+
const delayMs = cliConfig.getAuthDelayMs();
|
|
248
|
+
console.log(chalk.yellow(`Next retry delay: ${Math.round(delayMs / 1000)} seconds`));
|
|
249
|
+
}
|
|
250
|
+
console.log();
|
|
251
|
+
console.log(chalk.cyan('💡 To reset failure count:'));
|
|
252
|
+
console.log(chalk.white(' lanonasis auth logout && lanonasis auth login'));
|
|
253
|
+
}
|
|
224
254
|
});
|
|
225
255
|
// MCP Commands (primary interface)
|
|
226
256
|
mcpCommands(program);
|
|
@@ -201,12 +201,12 @@ export declare const SystemConfigSchema: z.ZodObject<{
|
|
|
201
201
|
scope: z.ZodDefault<z.ZodEnum<["user", "global"]>>;
|
|
202
202
|
}, "strip", z.ZodTypeAny, {
|
|
203
203
|
value?: any;
|
|
204
|
-
action?: "get" | "
|
|
204
|
+
action?: "get" | "set" | "reset";
|
|
205
205
|
key?: string;
|
|
206
206
|
scope?: "user" | "global";
|
|
207
207
|
}, {
|
|
208
208
|
value?: any;
|
|
209
|
-
action?: "get" | "
|
|
209
|
+
action?: "get" | "set" | "reset";
|
|
210
210
|
key?: string;
|
|
211
211
|
scope?: "user" | "global";
|
|
212
212
|
}>;
|
|
@@ -579,12 +579,12 @@ export declare const MCPSchemas: {
|
|
|
579
579
|
scope: z.ZodDefault<z.ZodEnum<["user", "global"]>>;
|
|
580
580
|
}, "strip", z.ZodTypeAny, {
|
|
581
581
|
value?: any;
|
|
582
|
-
action?: "get" | "
|
|
582
|
+
action?: "get" | "set" | "reset";
|
|
583
583
|
key?: string;
|
|
584
584
|
scope?: "user" | "global";
|
|
585
585
|
}, {
|
|
586
586
|
value?: any;
|
|
587
|
-
action?: "get" | "
|
|
587
|
+
action?: "get" | "set" | "reset";
|
|
588
588
|
key?: string;
|
|
589
589
|
scope?: "user" | "global";
|
|
590
590
|
}>;
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -8,14 +8,22 @@ export declare class CLIConfig {
|
|
|
8
8
|
private configDir;
|
|
9
9
|
private configPath;
|
|
10
10
|
private config;
|
|
11
|
+
private lockFile;
|
|
12
|
+
private static readonly CONFIG_VERSION;
|
|
11
13
|
constructor();
|
|
12
14
|
init(): Promise<void>;
|
|
13
15
|
load(): Promise<void>;
|
|
16
|
+
private migrateConfigIfNeeded;
|
|
14
17
|
save(): Promise<void>;
|
|
18
|
+
atomicSave(): Promise<void>;
|
|
19
|
+
backupConfig(): Promise<string>;
|
|
20
|
+
private acquireLock;
|
|
21
|
+
private releaseLock;
|
|
15
22
|
getApiUrl(): string;
|
|
16
23
|
discoverServices(): Promise<void>;
|
|
17
24
|
getDiscoveredApiUrl(): string;
|
|
18
25
|
setVendorKey(vendorKey: string): Promise<void>;
|
|
26
|
+
private validateVendorKeyWithServer;
|
|
19
27
|
getVendorKey(): string | undefined;
|
|
20
28
|
hasVendorKey(): boolean;
|
|
21
29
|
setApiUrl(url: string): Promise<void>;
|
|
@@ -27,6 +35,15 @@ export declare class CLIConfig {
|
|
|
27
35
|
clear(): Promise<void>;
|
|
28
36
|
getConfigPath(): string;
|
|
29
37
|
exists(): Promise<boolean>;
|
|
38
|
+
validateStoredCredentials(): Promise<boolean>;
|
|
39
|
+
refreshTokenIfNeeded(): Promise<void>;
|
|
40
|
+
clearInvalidCredentials(): Promise<void>;
|
|
41
|
+
incrementFailureCount(): Promise<void>;
|
|
42
|
+
resetFailureCount(): Promise<void>;
|
|
43
|
+
getFailureCount(): number;
|
|
44
|
+
getLastAuthFailure(): string | undefined;
|
|
45
|
+
shouldDelayAuth(): boolean;
|
|
46
|
+
getAuthDelayMs(): number;
|
|
30
47
|
get<T = unknown>(key: string): T;
|
|
31
48
|
set(key: string, value: unknown): void;
|
|
32
49
|
setAndSave(key: string, value: unknown): Promise<void>;
|
package/dist/utils/config.js
CHANGED
|
@@ -2,16 +2,17 @@ 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
12
|
constructor() {
|
|
13
13
|
this.configDir = path.join(os.homedir(), '.maas');
|
|
14
14
|
this.configPath = path.join(this.configDir, 'config.json');
|
|
15
|
+
this.lockFile = path.join(this.configDir, 'config.lock');
|
|
15
16
|
}
|
|
16
17
|
async init() {
|
|
17
18
|
try {
|
|
@@ -26,15 +27,124 @@ export class CLIConfig {
|
|
|
26
27
|
try {
|
|
27
28
|
const data = await fs.readFile(this.configPath, 'utf-8');
|
|
28
29
|
this.config = JSON.parse(data);
|
|
30
|
+
// Handle version migration if needed
|
|
31
|
+
await this.migrateConfigIfNeeded();
|
|
29
32
|
}
|
|
30
33
|
catch {
|
|
31
34
|
this.config = {};
|
|
35
|
+
// Set version for new config
|
|
36
|
+
this.config.version = CLIConfig.CONFIG_VERSION;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async migrateConfigIfNeeded() {
|
|
40
|
+
const currentVersion = this.config.version;
|
|
41
|
+
if (!currentVersion) {
|
|
42
|
+
// Legacy config without version, migrate to current version
|
|
43
|
+
this.config.version = CLIConfig.CONFIG_VERSION;
|
|
44
|
+
// Perform any necessary migrations for legacy configs
|
|
45
|
+
// For now, just ensure the version is set
|
|
46
|
+
await this.save();
|
|
47
|
+
}
|
|
48
|
+
else if (currentVersion !== CLIConfig.CONFIG_VERSION) {
|
|
49
|
+
// Future version migrations would go here
|
|
50
|
+
// For now, just update the version
|
|
51
|
+
this.config.version = CLIConfig.CONFIG_VERSION;
|
|
52
|
+
await this.save();
|
|
32
53
|
}
|
|
33
54
|
}
|
|
34
55
|
async save() {
|
|
56
|
+
await this.atomicSave();
|
|
57
|
+
}
|
|
58
|
+
async atomicSave() {
|
|
35
59
|
await fs.mkdir(this.configDir, { recursive: true });
|
|
36
|
-
|
|
37
|
-
await
|
|
60
|
+
// Acquire file lock to prevent concurrent access
|
|
61
|
+
const lockAcquired = await this.acquireLock();
|
|
62
|
+
if (!lockAcquired) {
|
|
63
|
+
throw new Error('Could not acquire configuration lock. Another process may be modifying the config.');
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
// Set version and update timestamp
|
|
67
|
+
this.config.version = CLIConfig.CONFIG_VERSION;
|
|
68
|
+
this.config.lastUpdated = new Date().toISOString();
|
|
69
|
+
// Create temporary file with unique name
|
|
70
|
+
const tempPath = `${this.configPath}.tmp.${randomUUID()}`;
|
|
71
|
+
// Write to temporary file first
|
|
72
|
+
await fs.writeFile(tempPath, JSON.stringify(this.config, null, 2), 'utf-8');
|
|
73
|
+
// Atomic rename - this is the critical atomic operation
|
|
74
|
+
await fs.rename(tempPath, this.configPath);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
// Always release the lock
|
|
78
|
+
await this.releaseLock();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async backupConfig() {
|
|
82
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
83
|
+
const backupPath = path.join(this.configDir, `config.backup.${timestamp}.json`);
|
|
84
|
+
try {
|
|
85
|
+
// Check if config exists before backing up
|
|
86
|
+
await fs.access(this.configPath);
|
|
87
|
+
await fs.copyFile(this.configPath, backupPath);
|
|
88
|
+
return backupPath;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (error.code === 'ENOENT') {
|
|
92
|
+
// Config doesn't exist, create empty backup
|
|
93
|
+
await fs.writeFile(backupPath, JSON.stringify({}, null, 2));
|
|
94
|
+
return backupPath;
|
|
95
|
+
}
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async acquireLock(timeoutMs = 5000) {
|
|
100
|
+
const startTime = Date.now();
|
|
101
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
102
|
+
try {
|
|
103
|
+
// Try to create lock file exclusively
|
|
104
|
+
await fs.writeFile(this.lockFile, process.pid.toString(), { flag: 'wx' });
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (error.code === 'EEXIST') {
|
|
109
|
+
// Lock file exists, check if process is still running
|
|
110
|
+
try {
|
|
111
|
+
const pidStr = await fs.readFile(this.lockFile, 'utf-8');
|
|
112
|
+
const pid = parseInt(pidStr.trim());
|
|
113
|
+
if (!isNaN(pid)) {
|
|
114
|
+
try {
|
|
115
|
+
// Check if process is still running (works on Unix-like systems)
|
|
116
|
+
process.kill(pid, 0);
|
|
117
|
+
// Process is running, wait and retry
|
|
118
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// Process is not running, remove stale lock
|
|
123
|
+
await fs.unlink(this.lockFile).catch(() => { });
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Can't read lock file, remove it and retry
|
|
130
|
+
await fs.unlink(this.lockFile).catch(() => { });
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
async releaseLock() {
|
|
142
|
+
try {
|
|
143
|
+
await fs.unlink(this.lockFile);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// Lock file might not exist or already removed, ignore
|
|
147
|
+
}
|
|
38
148
|
}
|
|
39
149
|
getApiUrl() {
|
|
40
150
|
return process.env.MEMORY_API_URL ||
|
|
@@ -87,9 +197,43 @@ export class CLIConfig {
|
|
|
87
197
|
if (!vendorKey.match(/^pk_[a-zA-Z0-9]+\.sk_[a-zA-Z0-9]+$/)) {
|
|
88
198
|
throw new Error('Invalid vendor key format. Expected: pk_xxx.sk_xxx');
|
|
89
199
|
}
|
|
200
|
+
// Server-side validation
|
|
201
|
+
await this.validateVendorKeyWithServer(vendorKey);
|
|
90
202
|
this.config.vendorKey = vendorKey;
|
|
203
|
+
this.config.authMethod = 'vendor_key';
|
|
204
|
+
this.config.lastValidated = new Date().toISOString();
|
|
205
|
+
await this.resetFailureCount(); // Reset failure count on successful auth
|
|
91
206
|
await this.save();
|
|
92
207
|
}
|
|
208
|
+
async validateVendorKeyWithServer(vendorKey) {
|
|
209
|
+
try {
|
|
210
|
+
// Import axios dynamically to avoid circular dependency
|
|
211
|
+
const axios = (await import('axios')).default;
|
|
212
|
+
// Ensure service discovery is done
|
|
213
|
+
await this.discoverServices();
|
|
214
|
+
const authBase = this.config.discoveredServices?.auth_base || 'https://api.lanonasis.com';
|
|
215
|
+
// Test vendor key with health endpoint
|
|
216
|
+
await axios.get(`${authBase}/api/v1/health`, {
|
|
217
|
+
headers: {
|
|
218
|
+
'X-API-Key': vendorKey,
|
|
219
|
+
'X-Auth-Method': 'vendor_key',
|
|
220
|
+
'X-Project-Scope': 'lanonasis-maas'
|
|
221
|
+
},
|
|
222
|
+
timeout: 10000
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
if (error.response?.status === 401 || error.response?.status === 403) {
|
|
227
|
+
throw new Error('Invalid vendor key: Authentication failed with server');
|
|
228
|
+
}
|
|
229
|
+
else if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
|
|
230
|
+
throw new Error('Cannot validate vendor key: Server unreachable');
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
throw new Error(`Vendor key validation failed: ${error.message}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
93
237
|
getVendorKey() {
|
|
94
238
|
return this.config.vendorKey;
|
|
95
239
|
}
|
|
@@ -102,11 +246,17 @@ export class CLIConfig {
|
|
|
102
246
|
}
|
|
103
247
|
async setToken(token) {
|
|
104
248
|
this.config.token = token;
|
|
105
|
-
|
|
249
|
+
this.config.authMethod = 'jwt';
|
|
250
|
+
this.config.lastValidated = new Date().toISOString();
|
|
251
|
+
await this.resetFailureCount(); // Reset failure count on successful auth
|
|
252
|
+
// Decode token to get user info and expiry
|
|
106
253
|
try {
|
|
107
254
|
const decoded = jwtDecode(token);
|
|
108
|
-
//
|
|
109
|
-
|
|
255
|
+
// Store token expiry
|
|
256
|
+
if (typeof decoded.exp === 'number') {
|
|
257
|
+
this.config.tokenExpiry = decoded.exp;
|
|
258
|
+
}
|
|
259
|
+
// Store user info
|
|
110
260
|
this.config.user = {
|
|
111
261
|
email: String(decoded.email || ''),
|
|
112
262
|
organization_id: String(decoded.organizationId || ''),
|
|
@@ -115,7 +265,8 @@ export class CLIConfig {
|
|
|
115
265
|
};
|
|
116
266
|
}
|
|
117
267
|
catch {
|
|
118
|
-
// Invalid token, don't store user info
|
|
268
|
+
// Invalid token, don't store user info or expiry
|
|
269
|
+
this.config.tokenExpiry = undefined;
|
|
119
270
|
}
|
|
120
271
|
await this.save();
|
|
121
272
|
}
|
|
@@ -134,7 +285,8 @@ export class CLIConfig {
|
|
|
134
285
|
// Extract timestamp from CLI token
|
|
135
286
|
const parts = token.split('_');
|
|
136
287
|
if (parts.length >= 3) {
|
|
137
|
-
const
|
|
288
|
+
const lastPart = parts[parts.length - 1];
|
|
289
|
+
const timestamp = lastPart ? parseInt(lastPart) : NaN;
|
|
138
290
|
if (!isNaN(timestamp)) {
|
|
139
291
|
// CLI tokens are valid for 30 days
|
|
140
292
|
const thirtyDaysInMs = 30 * 24 * 60 * 60 * 1000;
|
|
@@ -175,6 +327,126 @@ export class CLIConfig {
|
|
|
175
327
|
return false;
|
|
176
328
|
}
|
|
177
329
|
}
|
|
330
|
+
// Enhanced credential validation methods
|
|
331
|
+
async validateStoredCredentials() {
|
|
332
|
+
try {
|
|
333
|
+
const vendorKey = this.getVendorKey();
|
|
334
|
+
const token = this.getToken();
|
|
335
|
+
if (!vendorKey && !token) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
// Import axios dynamically to avoid circular dependency
|
|
339
|
+
const axios = (await import('axios')).default;
|
|
340
|
+
// Ensure service discovery is done
|
|
341
|
+
await this.discoverServices();
|
|
342
|
+
const authBase = this.config.discoveredServices?.auth_base || 'https://api.lanonasis.com';
|
|
343
|
+
const headers = {
|
|
344
|
+
'X-Project-Scope': 'lanonasis-maas'
|
|
345
|
+
};
|
|
346
|
+
if (vendorKey) {
|
|
347
|
+
headers['X-API-Key'] = vendorKey;
|
|
348
|
+
headers['X-Auth-Method'] = 'vendor_key';
|
|
349
|
+
}
|
|
350
|
+
else if (token) {
|
|
351
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
352
|
+
headers['X-Auth-Method'] = 'jwt';
|
|
353
|
+
}
|
|
354
|
+
// Validate against server with health endpoint
|
|
355
|
+
await axios.get(`${authBase}/api/v1/health`, {
|
|
356
|
+
headers,
|
|
357
|
+
timeout: 10000
|
|
358
|
+
});
|
|
359
|
+
// Update last validated timestamp
|
|
360
|
+
this.config.lastValidated = new Date().toISOString();
|
|
361
|
+
await this.resetFailureCount();
|
|
362
|
+
await this.save();
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
// Increment failure count
|
|
367
|
+
await this.incrementFailureCount();
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
async refreshTokenIfNeeded() {
|
|
372
|
+
const token = this.getToken();
|
|
373
|
+
if (!token) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
// Check if token is JWT and if it's close to expiry
|
|
378
|
+
if (token.startsWith('cli_')) {
|
|
379
|
+
// CLI tokens don't need refresh, they're long-lived
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const decoded = jwtDecode(token);
|
|
383
|
+
const now = Date.now() / 1000;
|
|
384
|
+
const exp = typeof decoded.exp === 'number' ? decoded.exp : 0;
|
|
385
|
+
// Refresh if token expires within 5 minutes
|
|
386
|
+
if (exp > 0 && (exp - now) < 300) {
|
|
387
|
+
// Import axios dynamically
|
|
388
|
+
const axios = (await import('axios')).default;
|
|
389
|
+
await this.discoverServices();
|
|
390
|
+
const authBase = this.config.discoveredServices?.auth_base || 'https://api.lanonasis.com';
|
|
391
|
+
// Attempt token refresh
|
|
392
|
+
const response = await axios.post(`${authBase}/v1/auth/refresh`, {}, {
|
|
393
|
+
headers: {
|
|
394
|
+
'Authorization': `Bearer ${token}`,
|
|
395
|
+
'X-Project-Scope': 'lanonasis-maas'
|
|
396
|
+
},
|
|
397
|
+
timeout: 10000
|
|
398
|
+
});
|
|
399
|
+
if (response.data.token) {
|
|
400
|
+
await this.setToken(response.data.token);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
// If refresh fails, mark credentials as potentially invalid
|
|
406
|
+
await this.incrementFailureCount();
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
async clearInvalidCredentials() {
|
|
410
|
+
this.config.token = undefined;
|
|
411
|
+
this.config.vendorKey = undefined;
|
|
412
|
+
this.config.user = undefined;
|
|
413
|
+
this.config.authMethod = undefined;
|
|
414
|
+
this.config.tokenExpiry = undefined;
|
|
415
|
+
this.config.lastValidated = undefined;
|
|
416
|
+
this.config.authFailureCount = 0;
|
|
417
|
+
this.config.lastAuthFailure = undefined;
|
|
418
|
+
await this.save();
|
|
419
|
+
}
|
|
420
|
+
async incrementFailureCount() {
|
|
421
|
+
this.config.authFailureCount = (this.config.authFailureCount || 0) + 1;
|
|
422
|
+
this.config.lastAuthFailure = new Date().toISOString();
|
|
423
|
+
await this.save();
|
|
424
|
+
}
|
|
425
|
+
async resetFailureCount() {
|
|
426
|
+
this.config.authFailureCount = 0;
|
|
427
|
+
this.config.lastAuthFailure = undefined;
|
|
428
|
+
await this.save();
|
|
429
|
+
}
|
|
430
|
+
getFailureCount() {
|
|
431
|
+
return this.config.authFailureCount || 0;
|
|
432
|
+
}
|
|
433
|
+
getLastAuthFailure() {
|
|
434
|
+
return this.config.lastAuthFailure;
|
|
435
|
+
}
|
|
436
|
+
shouldDelayAuth() {
|
|
437
|
+
const failureCount = this.getFailureCount();
|
|
438
|
+
return failureCount >= 3;
|
|
439
|
+
}
|
|
440
|
+
getAuthDelayMs() {
|
|
441
|
+
const failureCount = this.getFailureCount();
|
|
442
|
+
if (failureCount < 3)
|
|
443
|
+
return 0;
|
|
444
|
+
// Progressive delays: 3 failures = 2s, 4 = 4s, 5 = 8s, 6+ = 16s max
|
|
445
|
+
const baseDelay = 2000; // 2 seconds
|
|
446
|
+
const maxDelay = 16000; // 16 seconds max
|
|
447
|
+
const delay = Math.min(baseDelay * Math.pow(2, failureCount - 3), maxDelay);
|
|
448
|
+
return delay;
|
|
449
|
+
}
|
|
178
450
|
// Generic get/set methods for MCP and other dynamic config
|
|
179
451
|
get(key) {
|
|
180
452
|
return this.config[key];
|
|
@@ -188,7 +460,7 @@ export class CLIConfig {
|
|
|
188
460
|
}
|
|
189
461
|
// MCP-specific helpers
|
|
190
462
|
getMCPServerPath() {
|
|
191
|
-
return this.config.mcpServerPath || path.join(
|
|
463
|
+
return this.config.mcpServerPath || path.join(process.cwd(), 'onasis-gateway/mcp-server/server.js');
|
|
192
464
|
}
|
|
193
465
|
getMCPServerUrl() {
|
|
194
466
|
return this.config.discoveredServices?.mcp_ws_base ||
|
package/package.json
CHANGED