@lanonasis/cli 3.0.12 → 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.
@@ -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();
@@ -51,7 +72,7 @@ export async function loginCommand(options) {
51
72
  value: 'vendor'
52
73
  },
53
74
  {
54
- name: '🌐 Web OAuth (Browser-based)',
75
+ name: '🌐 Browser Login (Get token from web page)',
55
76
  value: 'oauth'
56
77
  },
57
78
  {
@@ -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
  }
@@ -116,7 +150,7 @@ async function handleVendorKeyFlow(config) {
116
150
  }
117
151
  async function handleOAuthFlow(config) {
118
152
  console.log();
119
- console.log(chalk.yellow('🌐 Web OAuth Authentication'));
153
+ console.log(chalk.yellow('🌐 Browser-Based Authentication'));
120
154
  console.log(chalk.gray('This will open your browser for secure authentication'));
121
155
  console.log();
122
156
  const { proceed } = await inquirer.prompt([
@@ -131,26 +165,38 @@ async function handleOAuthFlow(config) {
131
165
  console.log(chalk.yellow('Authentication cancelled'));
132
166
  return;
133
167
  }
134
- // Ensure proper URL joining to prevent double slashes
135
- const baseUrl = config.getDiscoveredApiUrl().replace(/\/+$/, ''); // Remove trailing slashes
136
- const authUrl = `${baseUrl}/oauth/authorize`;
168
+ // Use the browser-based CLI login endpoint from MCP service
169
+ // The discovery JSON points to mcp.lanonasis.com/auth/cli-login
170
+ const authUrl = 'https://mcp.lanonasis.com/auth/cli-login';
137
171
  try {
138
172
  console.log(colors.info('Opening browser...'));
139
173
  await open(authUrl);
140
174
  console.log();
141
175
  console.log(colors.info('Please complete authentication in your browser'));
176
+ console.log(colors.info('The page will display your authentication token'));
142
177
  console.log(colors.muted(`If browser doesn't open, visit: ${authUrl}`));
143
- // TODO: Implement OAuth callback handling or polling mechanism
178
+ console.log();
179
+ // Prompt for the token from the browser page
144
180
  const { token } = await inquirer.prompt([
145
181
  {
146
182
  type: 'input',
147
183
  name: 'token',
148
- message: 'Paste the authentication token from browser:'
184
+ message: 'Paste the authentication token from browser:',
185
+ validate: (input) => {
186
+ if (!input || input.trim().length === 0) {
187
+ return 'Token is required';
188
+ }
189
+ return true;
190
+ }
149
191
  }
150
192
  ]);
151
- if (token) {
152
- await config.setToken(token);
153
- console.log(chalk.green('✓ OAuth authentication successful'));
193
+ if (token && token.trim()) {
194
+ await config.setToken(token.trim());
195
+ console.log(chalk.green('✓ Browser authentication successful'));
196
+ console.log(colors.info('You can now use Lanonasis services'));
197
+ }
198
+ else {
199
+ console.log(chalk.yellow('⚠️ No token provided'));
154
200
  }
155
201
  }
156
202
  catch (error) {
@@ -162,6 +208,8 @@ async function handleCredentialsFlow(options, config) {
162
208
  console.log();
163
209
  console.log(chalk.yellow('⚙️ Username/Password Authentication'));
164
210
  console.log();
211
+ // Check for authentication delay before attempting
212
+ await handleAuthDelay(config);
165
213
  let { email, password } = options;
166
214
  // Get credentials if not provided
167
215
  if (!email || !password) {
@@ -203,10 +251,21 @@ async function handleCredentialsFlow(options, config) {
203
251
  }
204
252
  catch (error) {
205
253
  spinner.fail('Login failed');
254
+ // Increment failure count for failed authentication
255
+ await config.incrementFailureCount();
206
256
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
207
257
  const errorResponse = error && typeof error === 'object' && 'response' in error ? error.response : null;
208
258
  if (errorResponse && typeof errorResponse === 'object' && 'status' in errorResponse && errorResponse.status === 401) {
209
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
+ }
210
269
  // Ask if they want to register
211
270
  const answer = await inquirer.prompt([
212
271
  {
@@ -222,6 +281,15 @@ async function handleCredentialsFlow(options, config) {
222
281
  }
223
282
  else {
224
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
+ }
225
293
  }
226
294
  process.exit(1);
227
295
  }
@@ -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" | "reset" | "set";
204
+ action?: "get" | "set" | "reset";
205
205
  key?: string;
206
206
  scope?: "user" | "global";
207
207
  }, {
208
208
  value?: any;
209
- action?: "get" | "reset" | "set";
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" | "reset" | "set";
582
+ action?: "get" | "set" | "reset";
583
583
  key?: string;
584
584
  scope?: "user" | "global";
585
585
  }, {
586
586
  value?: any;
587
- action?: "get" | "reset" | "set";
587
+ action?: "get" | "set" | "reset";
588
588
  key?: string;
589
589
  scope?: "user" | "global";
590
590
  }>;
@@ -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>;
@@ -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 { fileURLToPath } from 'url';
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
- this.config.lastUpdated = new Date().toISOString();
37
- await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2));
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
- // Decode token to get user info
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
- // We'll need to fetch full user details from the API
109
- // For now, store what we can decode
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 timestamp = parseInt(parts[parts.length - 1]);
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(__dirname, '../../../../onasis-gateway/mcp-server/server.js');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lanonasis/cli",
3
- "version": "3.0.12",
3
+ "version": "3.1.13",
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": {