@lanonasis/cli 3.5.15 → 3.6.0

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 CHANGED
@@ -452,3 +452,71 @@ MIT License - see [LICENSE](../LICENSE) for details.
452
452
  ---
453
453
 
454
454
  _Professional CLI for Enterprise Memory as a Service - Golden Contract Compliant_
455
+
456
+ ## OAuth2 Authentication (v3.5.0+)
457
+
458
+ ### Browser Login with OAuth2 PKCE
459
+
460
+ The CLI now supports secure OAuth2 authentication with PKCE (Proof Key for Code Exchange) for browser-based login:
461
+
462
+ ```bash
463
+ onasis auth login
464
+ # Choose: 🌐 Browser Login (Get token from web page)
465
+ ```
466
+
467
+ **How it works:**
468
+ 1. CLI starts a local callback server on port 8888
469
+ 2. Opens your browser to the OAuth2 authorization page
470
+ 3. You authenticate in the browser
471
+ 4. Authorization code is sent back to the CLI
472
+ 5. CLI exchanges code for access and refresh tokens
473
+ 6. Tokens are securely stored locally
474
+
475
+ **Benefits:**
476
+ - ✅ More secure (PKCE prevents code interception)
477
+ - ✅ Automatic token refresh
478
+ - ✅ Revocable access
479
+ - ✅ Industry-standard OAuth2 flow
480
+
481
+ ### Authentication Methods
482
+
483
+ The CLI supports three authentication methods:
484
+
485
+ 1. **🔑 Vendor Key** (Recommended for API access)
486
+ - Long-lived API key from dashboard
487
+ - Best for automation and CI/CD
488
+
489
+ 2. **🌐 Browser Login** (OAuth2 PKCE)
490
+ - Secure browser-based authentication
491
+ - Automatic token refresh
492
+ - Best for interactive use
493
+
494
+ 3. **⚙️ Username/Password** (Direct credentials)
495
+ - Traditional email/password login
496
+ - Returns JWT token
497
+ - Legacy method
498
+
499
+ ### Token Management
500
+
501
+ OAuth2 tokens are automatically refreshed when expired:
502
+
503
+ ```bash
504
+ # Check authentication status
505
+ onasis auth status
506
+
507
+ # Force re-authentication
508
+ onasis auth logout
509
+ onasis auth login
510
+ ```
511
+
512
+ ### Troubleshooting
513
+
514
+ **Port 8888 already in use:**
515
+ The CLI needs port 8888 for the OAuth callback. If it's in use, close the application using it or use the Vendor Key method instead.
516
+
517
+ **Browser doesn't open:**
518
+ The CLI will show the authorization URL - copy and paste it into your browser manually.
519
+
520
+ **Token refresh failed:**
521
+ Run `onasis auth login` to re-authenticate.
522
+
@@ -2,6 +2,9 @@ import chalk from 'chalk';
2
2
  import inquirer from 'inquirer';
3
3
  import ora from 'ora';
4
4
  import open from 'open';
5
+ import crypto from 'crypto';
6
+ import http from 'http';
7
+ import url from 'url';
5
8
  import { apiClient } from '../utils/api.js';
6
9
  import { CLIConfig } from '../utils/config.js';
7
10
  // Color scheme
@@ -49,9 +52,9 @@ async function handleAuthenticationFailure(error, config, authMethod = 'jwt') {
49
52
  case 'invalid_credentials':
50
53
  console.log(chalk.red('Invalid credentials provided'));
51
54
  if (authMethod === 'vendor_key') {
52
- console.log(chalk.gray('• Check your vendor key format: pk_xxx.sk_xxx'));
53
- console.log(chalk.gray('• Verify the key is active in your account dashboard'));
54
- console.log(chalk.gray('• Ensure you copied the complete key including both parts'));
55
+ console.log(chalk.gray('• Verify the vendor key matches the value shown in your dashboard'));
56
+ console.log(chalk.gray('• Confirm the key is active and has not been revoked'));
57
+ console.log(chalk.gray('• Ensure you copied the entire key without extra spaces'));
55
58
  }
56
59
  else {
57
60
  console.log(chalk.gray('• Double-check your email and password'));
@@ -163,6 +166,120 @@ function categorizeAuthError(error) {
163
166
  }
164
167
  return 'unknown';
165
168
  }
169
+ // ============================================
170
+ // OAuth2 PKCE Helper Functions
171
+ // ============================================
172
+ /**
173
+ * Generate PKCE code verifier and challenge for OAuth2
174
+ */
175
+ function generatePKCE() {
176
+ // Generate random verifier (43-128 chars, base64url)
177
+ const verifier = crypto.randomBytes(32).toString('base64url');
178
+ // Generate challenge: base64url(sha256(verifier))
179
+ const challenge = crypto
180
+ .createHash('sha256')
181
+ .update(verifier)
182
+ .digest('base64url');
183
+ return { verifier, challenge };
184
+ }
185
+ /**
186
+ * Start local HTTP server to catch OAuth2 callback
187
+ */
188
+ function createCallbackServer(port = 8888) {
189
+ return new Promise((resolve, reject) => {
190
+ const server = http.createServer((req, res) => {
191
+ const parsedUrl = url.parse(req.url, true);
192
+ if (parsedUrl.pathname === '/callback') {
193
+ const { code, state, error, error_description } = parsedUrl.query;
194
+ // Send response to browser
195
+ if (error) {
196
+ res.writeHead(400, { 'Content-Type': 'text/html' });
197
+ res.end(`
198
+ <html>
199
+ <head><title>Authentication Failed</title></head>
200
+ <body style="font-family: sans-serif; text-align: center; padding: 50px;">
201
+ <h1>❌ Authentication Failed</h1>
202
+ <p>${error_description || error}</p>
203
+ <p style="color: gray;">You can close this window.</p>
204
+ </body>
205
+ </html>
206
+ `);
207
+ reject(new Error(`OAuth error: ${error_description || error}`));
208
+ }
209
+ else if (code) {
210
+ res.writeHead(200, { 'Content-Type': 'text/html' });
211
+ res.end(`
212
+ <html>
213
+ <head><title>Authentication Successful</title></head>
214
+ <body style="font-family: sans-serif; text-align: center; padding: 50px;">
215
+ <h1>✅ Authentication Successful</h1>
216
+ <p>You can close this window and return to the CLI.</p>
217
+ <script>setTimeout(() => window.close(), 2000);</script>
218
+ </body>
219
+ </html>
220
+ `);
221
+ resolve({ code: code, state: state });
222
+ }
223
+ else {
224
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
225
+ res.end('Invalid callback');
226
+ reject(new Error('No authorization code received'));
227
+ }
228
+ // Close server after handling request
229
+ server.close();
230
+ }
231
+ });
232
+ server.listen(port, () => {
233
+ console.log(chalk.gray(` Local callback server listening on port ${port}`));
234
+ });
235
+ // Timeout after 5 minutes
236
+ setTimeout(() => {
237
+ server.close();
238
+ reject(new Error('Authentication timeout - please try again'));
239
+ }, 300000);
240
+ });
241
+ }
242
+ /**
243
+ * Exchange authorization code for OAuth2 tokens
244
+ */
245
+ async function exchangeCodeForTokens(code, verifier, authBase) {
246
+ const tokenEndpoint = `${authBase}/oauth/token`;
247
+ const response = await apiClient.post(tokenEndpoint, {
248
+ grant_type: 'authorization_code',
249
+ code,
250
+ code_verifier: verifier,
251
+ client_id: 'lanonasis-cli',
252
+ redirect_uri: 'http://localhost:8888/callback'
253
+ });
254
+ return response;
255
+ }
256
+ /**
257
+ * Refresh OAuth2 access token using refresh token
258
+ */
259
+ async function refreshOAuth2Token(config) {
260
+ const refreshToken = config.get('refresh_token');
261
+ if (!refreshToken) {
262
+ return false;
263
+ }
264
+ try {
265
+ const authBase = config.getDiscoveredApiUrl();
266
+ const response = await apiClient.post(`${authBase}/oauth/token`, {
267
+ grant_type: 'refresh_token',
268
+ refresh_token: refreshToken,
269
+ client_id: 'lanonasis-cli'
270
+ });
271
+ await config.setToken(response.access_token);
272
+ if (response.refresh_token) {
273
+ await config.set('refresh_token', response.refresh_token);
274
+ }
275
+ await config.set('token_expires_at', Date.now() + (response.expires_in * 1000));
276
+ return true;
277
+ }
278
+ catch (error) {
279
+ console.error(chalk.yellow('⚠️ Token refresh failed, please re-authenticate'));
280
+ return false;
281
+ }
282
+ }
166
283
  export async function diagnoseCommand() {
167
284
  const config = new CLIConfig();
168
285
  await config.init();
@@ -206,14 +323,10 @@ export async function diagnoseCommand() {
206
323
  diagnostics.hasCredentials = true;
207
324
  diagnostics.credentialType = 'vendor_key';
208
325
  console.log(chalk.green(' ✓ Vendor key found'));
209
- // Validate vendor key format
326
+ // Validate vendor key presence
210
327
  const formatValidation = config.validateVendorKeyFormat(vendorKey);
211
- if (formatValidation === true) {
212
- console.log(chalk.green(' Vendor key format is valid'));
213
- }
214
- else {
215
- console.log(chalk.red(' ✖ Vendor key format is invalid:'));
216
- console.log(chalk.gray(` ${formatValidation}`));
328
+ if (formatValidation !== true) {
329
+ console.log(chalk.red(` Vendor key issue: ${formatValidation}`));
217
330
  }
218
331
  }
219
332
  else if (token) {
@@ -335,7 +448,7 @@ export async function diagnoseCommand() {
335
448
  }
336
449
  if (!diagnostics.hasCredentials) {
337
450
  issues.push('No authentication credentials stored');
338
- recommendations.push('Run: lanonasis auth login --vendor-key pk_xxx.sk_xxx');
451
+ recommendations.push('Run: lanonasis auth login --vendor-key <your-key>');
339
452
  }
340
453
  if (diagnostics.hasCredentials && !diagnostics.credentialsValid) {
341
454
  issues.push('Stored credentials are invalid');
@@ -370,7 +483,7 @@ export async function diagnoseCommand() {
370
483
  // Additional troubleshooting info
371
484
  if (diagnostics.authFailures > 0 || !diagnostics.credentialsValid) {
372
485
  console.log(chalk.gray('\n🔧 Additional troubleshooting:'));
373
- console.log(chalk.gray(' • Verify your vendor key format: pk_xxx.sk_xxx'));
486
+ console.log(chalk.gray(' • Verify the vendor key matches the value shown in your dashboard'));
374
487
  console.log(chalk.gray(' • Check if your key is active in the dashboard'));
375
488
  console.log(chalk.gray(' • Try browser authentication: lanonasis auth login --use-web-auth'));
376
489
  console.log(chalk.gray(' • Contact support if issues persist'));
@@ -457,84 +570,37 @@ async function handleVendorKeyAuth(vendorKey, config) {
457
570
  async function handleVendorKeyFlow(config) {
458
571
  console.log();
459
572
  console.log(chalk.yellow('🔑 Vendor Key Authentication'));
460
- console.log(chalk.gray('Vendor keys provide secure API access with format: pk_xxx.sk_xxx'));
573
+ console.log(chalk.gray('Vendor keys provide secure API access for automation and integrations.'));
461
574
  console.log();
462
575
  // Enhanced guidance for obtaining vendor keys
463
576
  console.log(chalk.cyan('📋 How to get your vendor key:'));
464
577
  console.log(chalk.gray('1. Visit your Lanonasis dashboard at https://app.lanonasis.com'));
465
578
  console.log(chalk.gray('2. Navigate to Settings → API Keys'));
466
- console.log(chalk.gray('3. Click "Generate New Key" and copy the full key'));
467
- console.log(chalk.gray('4. The key format should be: pk_[letters/numbers].sk_[letters/numbers]'));
579
+ console.log(chalk.gray('3. Click "Generate New Key" and copy the full key value'));
468
580
  console.log();
469
581
  const { vendorKey } = await inquirer.prompt([
470
582
  {
471
583
  type: 'password',
472
584
  name: 'vendorKey',
473
- message: 'Enter your vendor key (pk_xxx.sk_xxx):',
585
+ message: 'Enter your vendor key:',
474
586
  mask: '*',
475
587
  validate: (input) => {
476
- return validateVendorKeyFormat(input);
588
+ return config.validateVendorKeyFormat(input);
477
589
  }
478
590
  }
479
591
  ]);
480
592
  await handleVendorKeyAuth(vendorKey, config);
481
593
  }
482
- // Enhanced vendor key format validation with detailed error messages
483
- function validateVendorKeyFormat(input) {
484
- if (!input || input.trim().length === 0) {
485
- return 'Vendor key is required';
486
- }
487
- const trimmed = input.trim();
488
- // Check basic format
489
- if (!trimmed.includes('.')) {
490
- return 'Invalid format: Vendor key must contain a dot (.) separator\nExpected format: pk_xxx.sk_xxx';
491
- }
492
- const parts = trimmed.split('.');
493
- if (parts.length !== 2) {
494
- return 'Invalid format: Vendor key must have exactly two parts separated by a dot\nExpected format: pk_xxx.sk_xxx';
495
- }
496
- const [publicPart, secretPart] = parts;
497
- // Validate public key part
498
- if (!publicPart.startsWith('pk_')) {
499
- return 'Invalid format: First part must start with "pk_"\nExpected format: pk_xxx.sk_xxx';
500
- }
501
- if (publicPart.length < 4) {
502
- return 'Invalid format: Public key part is too short\nExpected format: pk_xxx.sk_xxx (where xxx is alphanumeric)';
503
- }
504
- const publicKeyContent = publicPart.substring(3); // Remove 'pk_'
505
- if (!/^[a-zA-Z0-9]+$/.test(publicKeyContent)) {
506
- return 'Invalid format: Public key part contains invalid characters\nOnly letters and numbers are allowed after "pk_"';
507
- }
508
- // Validate secret key part
509
- if (!secretPart.startsWith('sk_')) {
510
- return 'Invalid format: Second part must start with "sk_"\nExpected format: pk_xxx.sk_xxx';
511
- }
512
- if (secretPart.length < 4) {
513
- return 'Invalid format: Secret key part is too short\nExpected format: pk_xxx.sk_xxx (where xxx is alphanumeric)';
514
- }
515
- const secretKeyContent = secretPart.substring(3); // Remove 'sk_'
516
- if (!/^[a-zA-Z0-9]+$/.test(secretKeyContent)) {
517
- return 'Invalid format: Secret key part contains invalid characters\nOnly letters and numbers are allowed after "sk_"';
518
- }
519
- // Check minimum length requirements
520
- if (publicKeyContent.length < 8) {
521
- return 'Invalid format: Public key part is too short (minimum 8 characters after "pk_")';
522
- }
523
- if (secretKeyContent.length < 16) {
524
- return 'Invalid format: Secret key part is too short (minimum 16 characters after "sk_")';
525
- }
526
- return true;
527
- }
528
594
  async function handleOAuthFlow(config) {
529
595
  console.log();
530
- console.log(chalk.yellow('🌐 Browser-Based Authentication'));
531
- console.log(chalk.gray('This will open your browser for secure authentication'));
596
+ console.log(chalk.yellow('🌐 Browser-Based OAuth2 Authentication'));
597
+ console.log(chalk.gray('Secure authentication using OAuth2 with PKCE'));
532
598
  console.log();
533
599
  const { openBrowser } = await inquirer.prompt([
534
600
  {
535
601
  type: 'confirm',
536
602
  name: 'openBrowser',
537
- message: 'Open browser for authentication?',
603
+ message: 'Open browser for OAuth2 authentication?',
538
604
  default: true
539
605
  }
540
606
  ]);
@@ -542,63 +608,53 @@ async function handleOAuthFlow(config) {
542
608
  console.log(chalk.yellow('⚠️ Authentication cancelled'));
543
609
  return;
544
610
  }
545
- // Use the browser-based CLI login endpoint discovered from auth_base
546
- const authBase = config.getDiscoveredApiUrl();
547
- const authUrl = `${authBase.replace(/\/$/, '')}/auth/cli-login`;
548
611
  try {
549
- console.log(colors.info('Opening browser...'));
550
- await open(authUrl);
612
+ // Generate PKCE challenge
613
+ const pkce = generatePKCE();
614
+ console.log(chalk.gray(' ✓ Generated PKCE challenge'));
615
+ // Start local callback server
616
+ const callbackPort = 8888;
617
+ const callbackPromise = createCallbackServer(callbackPort);
618
+ console.log(chalk.gray(` ✓ Started local callback server on port ${callbackPort}`));
619
+ // Build OAuth2 authorization URL
620
+ const authBase = config.getDiscoveredApiUrl();
621
+ const authUrl = new URL(`${authBase}/oauth/authorize`);
622
+ authUrl.searchParams.set('response_type', 'code');
623
+ authUrl.searchParams.set('client_id', 'lanonasis-cli');
624
+ authUrl.searchParams.set('redirect_uri', `http://localhost:${callbackPort}/callback`);
625
+ authUrl.searchParams.set('scope', 'read write offline_access');
626
+ authUrl.searchParams.set('code_challenge', pkce.challenge);
627
+ authUrl.searchParams.set('code_challenge_method', 'S256');
628
+ authUrl.searchParams.set('state', crypto.randomBytes(16).toString('hex'));
551
629
  console.log();
552
- console.log(colors.info('Please complete authentication in your browser'));
553
- console.log(colors.info('The page will display your authentication token'));
554
- console.log(colors.muted(`If browser doesn't open, visit: ${authUrl}`));
630
+ console.log(colors.info('Opening browser for authentication...'));
631
+ await open(authUrl.toString());
632
+ console.log(colors.info('Waiting for authentication in browser...'));
633
+ console.log(colors.muted(`If browser doesn't open, visit: ${authUrl.toString()}`));
555
634
  console.log();
556
- // Prompt for the token from the browser page
557
- const { token } = await inquirer.prompt([
558
- {
559
- type: 'input',
560
- name: 'token',
561
- message: 'Paste the authentication token from browser:',
562
- validate: async (input) => {
563
- if (!input || input.trim().length === 0) {
564
- return 'Token is required';
565
- }
566
- const trimmed = input.trim();
567
- // Reject if user pasted a URL instead of token
568
- if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
569
- return 'Please paste the TOKEN from the page, not the URL';
570
- }
571
- // Check token format - should start with 'cli_' or be a JWT
572
- if (!trimmed.startsWith('cli_') && !trimmed.match(/^[\w-]+\.[\w-]+\.[\w-]+$/)) {
573
- return 'Invalid token format. Expected format: cli_xxx or JWT token';
574
- }
575
- // Verify token with server
576
- try {
577
- const response = await apiClient.post('/auth/verify', { token: trimmed });
578
- if (!response.valid) {
579
- return 'Token verification failed. Please try again.';
580
- }
581
- }
582
- catch (error) {
583
- const errorMessage = error instanceof Error ? error.message : 'Server verification failed';
584
- return `Token verification error: ${errorMessage}`;
585
- }
586
- return true;
587
- }
588
- }
589
- ]);
590
- if (token && token.trim()) {
591
- await config.setToken(token.trim());
592
- console.log(chalk.green('✓ Browser authentication successful'));
593
- console.log(colors.info('You can now use Lanonasis services'));
594
- }
595
- else {
596
- console.log(chalk.yellow('⚠️ No token provided'));
597
- }
635
+ // Wait for callback
636
+ const spinner = ora('Waiting for authorization...').start();
637
+ const { code } = await callbackPromise;
638
+ spinner.succeed('Authorization code received');
639
+ // Exchange code for tokens
640
+ spinner.text = 'Exchanging code for access tokens...';
641
+ spinner.start();
642
+ const tokens = await exchangeCodeForTokens(code, pkce.verifier, authBase);
643
+ spinner.succeed('Access tokens received');
644
+ // Store tokens
645
+ await config.setToken(tokens.access_token);
646
+ await config.set('refresh_token', tokens.refresh_token);
647
+ await config.set('token_expires_at', Date.now() + (tokens.expires_in * 1000));
648
+ await config.set('authMethod', 'oauth2');
649
+ console.log();
650
+ console.log(chalk.green('✓ OAuth2 authentication successful'));
651
+ console.log(colors.info('You can now use Lanonasis services'));
598
652
  }
599
- catch {
600
- console.error(chalk.red('✖ Failed to open browser'));
601
- console.log(colors.muted(`Please visit manually: ${authUrl}`));
653
+ catch (error) {
654
+ console.error(chalk.red('✖ OAuth2 authentication failed'));
655
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
656
+ console.error(chalk.gray(` ${errorMessage}`));
657
+ process.exit(1);
602
658
  }
603
659
  }
604
660
  async function handleCredentialsFlow(options, config) {
@@ -48,7 +48,7 @@ export async function generateCompletionData() {
48
48
  options: [
49
49
  { name: '--email', description: 'Email address', type: 'string' },
50
50
  { name: '--password', description: 'Password', type: 'string' },
51
- { name: '--vendor-key', description: 'Vendor key (pk_xxx.sk_xxx)', type: 'string' },
51
+ { name: '--vendor-key', description: 'Vendor key value', type: 'string' },
52
52
  { name: '--oauth', description: 'Use OAuth flow', type: 'boolean' }
53
53
  ]
54
54
  },
@@ -411,15 +411,15 @@ export function configCommands(program) {
411
411
  console.log(chalk.cyan(' → Repaired: Set auth method to jwt'));
412
412
  }
413
413
  }
414
- // Validate vendor key format if present
414
+ // Validate vendor key presence if present
415
415
  if (vendorKey) {
416
416
  const formatValidation = config.validateVendorKeyFormat(vendorKey);
417
417
  if (formatValidation === true) {
418
- console.log(chalk.green(' ✓ Vendor key format is valid'));
418
+ console.log(chalk.green(' ✓ Vendor key is set'));
419
419
  }
420
420
  else {
421
- console.log(chalk.red(' ✖ Vendor key format is invalid'));
422
- validation.issues.push('Invalid vendor key format');
421
+ console.log(chalk.red(` ✖ Vendor key issue: ${formatValidation}`));
422
+ validation.issues.push('Vendor key missing or invalid');
423
423
  }
424
424
  }
425
425
  // Test authentication validity
@@ -233,7 +233,7 @@ export class UserGuidanceSystem {
233
233
  switch (authMethod) {
234
234
  case 'vendor_key':
235
235
  console.log(chalk.yellow('📝 Vendor keys provide secure, programmatic access'));
236
- console.log(chalk.gray('Format: pk_xxxxx.sk_xxxxx'));
236
+ console.log(chalk.gray('Find it in your dashboard under API Keys.'));
237
237
  console.log();
238
238
  break;
239
239
  case 'oauth':
@@ -409,7 +409,7 @@ export async function quickStartCommand() {
409
409
  category: 'Setup',
410
410
  commands: [
411
411
  { cmd: 'lanonasis init', desc: 'Initialize configuration' },
412
- { cmd: 'lanonasis login --vendor-key pk_xxx.sk_xxx', desc: 'Authenticate with vendor key' },
412
+ { cmd: 'lanonasis login --vendor-key <your-key>', desc: 'Authenticate with vendor key' },
413
413
  { cmd: 'lanonasis health', desc: 'Verify system health' }
414
414
  ]
415
415
  },
@@ -293,7 +293,7 @@ export class PowerUserMode {
293
293
  const subCommand = args[0];
294
294
  switch (subCommand) {
295
295
  case 'keys':
296
- console.log('API Keys: pk_xxx...xxx (active), pk_yyy...yyy (revoked)');
296
+ console.log('API Keys: vendor-key-1... (active), vendor-key-2... (revoked)');
297
297
  break;
298
298
  case 'limits':
299
299
  console.log('Rate Limits: 1000/hour (432 used)');
@@ -296,7 +296,7 @@ export class InteractiveSetup {
296
296
  message: 'Select authentication method:',
297
297
  choices: [
298
298
  {
299
- name: authBox('🔑 Vendor Key', 'Secure API access with pk_xxx.sk_xxx format', ['No expiration', 'Ideal for CI/CD', 'Full API access']),
299
+ name: authBox('🔑 Vendor Key', 'Secure API access with long-lived credentials', ['No expiration', 'Ideal for CI/CD', 'Full API access']),
300
300
  value: 'vendor',
301
301
  short: 'Vendor Key'
302
302
  },
@@ -327,32 +327,21 @@ export class InteractiveSetup {
327
327
  }
328
328
  }
329
329
  async authenticateWithVendorKey() {
330
- const creds = await inquirer.prompt([
331
- {
332
- type: 'input',
333
- name: 'publicKey',
334
- message: 'Enter your Public Key (pk_xxx):',
335
- validate: (input) => {
336
- if (!input.startsWith('pk_')) {
337
- return 'Public key must start with pk_';
338
- }
339
- return true;
340
- }
341
- },
330
+ const { vendorKey } = await inquirer.prompt([
342
331
  {
343
332
  type: 'password',
344
- name: 'secretKey',
345
- message: 'Enter your Secret Key (sk_xxx):',
333
+ name: 'vendorKey',
334
+ message: 'Enter your vendor key:',
346
335
  mask: '*',
347
336
  validate: (input) => {
348
- if (!input.startsWith('sk_')) {
349
- return 'Secret key must start with sk_';
337
+ if (!input || input.trim().length === 0) {
338
+ return 'Vendor key is required';
350
339
  }
351
340
  return true;
352
341
  }
353
342
  }
354
343
  ]);
355
- void creds; // collected for future use when hooking real auth
344
+ void vendorKey; // collected for future use when hooking real auth
356
345
  const spinner = ora('Authenticating...').start();
357
346
  await this.simulateDelay(1000);
358
347
  spinner.succeed('Authentication successful!');
@@ -123,7 +123,7 @@ const showWelcome = () => {
123
123
  console.log();
124
124
  if (isOnasisInvocation) {
125
125
  console.log(colors.info('🔑 Golden Contract Authentication:'));
126
- console.log(` ${colors.success(`${cmdName} login --vendor-key pk_xxx.sk_xxx`)} ${colors.muted('# Vendor key auth')}`);
126
+ console.log(` ${colors.success(`${cmdName} login --vendor-key <your-key>`)} ${colors.muted('# Vendor key auth')}`);
127
127
  console.log(` ${colors.success(`${cmdName} login --oauth`)} ${colors.muted('# Browser OAuth')}`);
128
128
  console.log();
129
129
  }
@@ -175,11 +175,24 @@ const healthCheck = async () => {
175
175
  process.stdout.write('MCP Server status: ');
176
176
  try {
177
177
  const client = getMCPClient();
178
+ const status = client.getConnectionStatus();
179
+ const mcpPreference = await cliConfig.get('mcpPreference') || 'auto';
178
180
  if (client.isConnectedToServer()) {
179
181
  console.log(colors.success('✅ Connected'));
182
+ console.log(` Mode: ${colors.highlight(status.mode)}`);
183
+ console.log(` Server: ${colors.highlight(status.server || 'N/A')}`);
180
184
  }
181
185
  else {
182
186
  console.log(colors.warning('⚠️ Disconnected'));
187
+ console.log(` Configured preference: ${colors.highlight(mcpPreference)}`);
188
+ if (mcpPreference === 'remote') {
189
+ const mcpUrl = cliConfig.getMCPServerUrl();
190
+ console.log(` Remote server: ${colors.highlight(mcpUrl)}`);
191
+ }
192
+ else if (mcpPreference === 'local') {
193
+ const mcpPath = cliConfig.getMCPServerPath();
194
+ console.log(` Local server: ${colors.highlight(mcpPath || 'Not configured')}`);
195
+ }
183
196
  }
184
197
  }
185
198
  catch (error) {
@@ -229,7 +242,7 @@ authCmd
229
242
  .description('Login to your MaaS account')
230
243
  .option('-e, --email <email>', 'email address')
231
244
  .option('-p, --password <password>', 'password')
232
- .option('--vendor-key <key>', 'vendor key (pk_xxx.sk_xxx format)')
245
+ .option('--vendor-key <key>', 'vendor key (as provided in your dashboard)')
233
246
  .option('--oauth', 'use OAuth browser flow')
234
247
  .action(async (options) => {
235
248
  // Handle oauth flag
@@ -249,6 +262,8 @@ authCmd
249
262
  .command('status')
250
263
  .description('Show authentication status')
251
264
  .action(async () => {
265
+ // Initialize config first
266
+ await cliConfig.init();
252
267
  const isAuth = await cliConfig.isAuthenticated();
253
268
  const user = await cliConfig.getCurrentUser();
254
269
  const failureCount = cliConfig.getFailureCount();
@@ -523,6 +538,8 @@ program
523
538
  .command('status')
524
539
  .description('Show overall system status')
525
540
  .action(async () => {
541
+ // Initialize config first
542
+ await cliConfig.init();
526
543
  const isAuth = await cliConfig.isAuthenticated();
527
544
  const apiUrl = cliConfig.getApiUrl();
528
545
  console.log(chalk.blue.bold('MaaS CLI Status'));
package/dist/index.js CHANGED
@@ -270,6 +270,47 @@ const memoryCmd = program
270
270
  requireAuth(memoryCmd);
271
271
  memoryCommands(memoryCmd);
272
272
  // Note: Memory commands are now MCP-powered when available
273
+ // REPL command (lightweight REPL for memory operations)
274
+ program
275
+ .command('repl')
276
+ .description('Start lightweight REPL session for memory operations')
277
+ .option('--mcp', 'Use MCP mode')
278
+ .option('--api <url>', 'Override API URL')
279
+ .option('--token <token>', 'Authentication token')
280
+ .action(async (options) => {
281
+ try {
282
+ // Try to use the REPL package if available
283
+ const { spawn } = await import('child_process');
284
+ const { fileURLToPath } = await import('url');
285
+ const { dirname, join } = await import('path');
286
+ // Try to find the REPL package
287
+ const replPath = join(process.cwd(), 'packages', 'repl-cli', 'dist', 'index.js');
288
+ const args = ['start'];
289
+ if (options.mcp)
290
+ args.push('--mcp');
291
+ if (options.api)
292
+ args.push('--api', options.api);
293
+ if (options.token)
294
+ args.push('--token', options.token);
295
+ const repl = spawn('node', [replPath, ...args], {
296
+ stdio: 'inherit',
297
+ cwd: process.cwd()
298
+ });
299
+ repl.on('error', (err) => {
300
+ console.error(colors.error('Failed to start REPL:'), err.message);
301
+ console.log(colors.muted('Make sure the REPL package is built: cd packages/repl-cli && bun run build'));
302
+ process.exit(1);
303
+ });
304
+ repl.on('exit', (code) => {
305
+ process.exit(code || 0);
306
+ });
307
+ }
308
+ catch (error) {
309
+ console.error(colors.error('Failed to start REPL:'), error instanceof Error ? error.message : String(error));
310
+ console.log(colors.muted('Install the REPL package: cd packages/repl-cli && bun install && bun run build'));
311
+ process.exit(1);
312
+ }
313
+ });
273
314
  // Topic commands (require auth)
274
315
  const topicCmd = program
275
316
  .command('topic')
@@ -202,13 +202,13 @@ export declare const SystemConfigSchema: z.ZodObject<{
202
202
  }, "strip", z.ZodTypeAny, {
203
203
  value?: any;
204
204
  action?: "get" | "set" | "reset";
205
- key?: string;
206
205
  scope?: "user" | "global";
206
+ key?: string;
207
207
  }, {
208
208
  value?: any;
209
209
  action?: "get" | "set" | "reset";
210
- key?: string;
211
210
  scope?: "user" | "global";
211
+ key?: string;
212
212
  }>;
213
213
  export declare const BulkOperationSchema: z.ZodObject<{
214
214
  operation: z.ZodEnum<["create", "update", "delete"]>;
@@ -580,13 +580,13 @@ export declare const MCPSchemas: {
580
580
  }, "strip", z.ZodTypeAny, {
581
581
  value?: any;
582
582
  action?: "get" | "set" | "reset";
583
- key?: string;
584
583
  scope?: "user" | "global";
584
+ key?: string;
585
585
  }, {
586
586
  value?: any;
587
587
  action?: "get" | "set" | "reset";
588
- key?: string;
589
588
  scope?: "user" | "global";
589
+ key?: string;
590
590
  }>;
591
591
  };
592
592
  operations: {
package/dist/utils/api.js CHANGED
@@ -27,7 +27,7 @@ export class APIClient {
27
27
  const token = this.config.getToken();
28
28
  const vendorKey = this.config.getVendorKey();
29
29
  if (vendorKey) {
30
- // Vendor key authentication (pk_*.sk_* format)
30
+ // Vendor key authentication (validated server-side)
31
31
  config.headers['X-API-Key'] = vendorKey;
32
32
  config.headers['X-Auth-Method'] = 'vendor_key';
33
33
  }
@@ -354,55 +354,25 @@ export class CLIConfig {
354
354
  }
355
355
  // Enhanced authentication support
356
356
  async setVendorKey(vendorKey) {
357
- // Enhanced format validation with detailed error messages
358
- const formatValidation = this.validateVendorKeyFormat(vendorKey);
357
+ const trimmedKey = typeof vendorKey === 'string' ? vendorKey.trim() : '';
358
+ // Minimal format validation (non-empty); rely on server-side checks for everything else
359
+ const formatValidation = this.validateVendorKeyFormat(trimmedKey);
359
360
  if (formatValidation !== true) {
360
- throw new Error(typeof formatValidation === 'string' ? formatValidation : 'Invalid vendor key format');
361
+ throw new Error(typeof formatValidation === 'string' ? formatValidation : 'Vendor key is invalid');
361
362
  }
362
363
  // Server-side validation
363
- await this.validateVendorKeyWithServer(vendorKey);
364
- this.config.vendorKey = vendorKey;
364
+ await this.validateVendorKeyWithServer(trimmedKey);
365
+ this.config.vendorKey = trimmedKey;
365
366
  this.config.authMethod = 'vendor_key';
366
367
  this.config.lastValidated = new Date().toISOString();
367
368
  await this.resetFailureCount(); // Reset failure count on successful auth
368
369
  await this.save();
369
370
  }
370
371
  validateVendorKeyFormat(vendorKey) {
371
- if (!vendorKey || vendorKey.trim().length === 0) {
372
+ const trimmed = typeof vendorKey === 'string' ? vendorKey.trim() : '';
373
+ if (!trimmed) {
372
374
  return 'Vendor key is required';
373
375
  }
374
- const trimmed = vendorKey.trim();
375
- // Check basic format
376
- if (!trimmed.includes('.')) {
377
- return 'Invalid vendor key format: Must contain a dot (.) separator. Expected format: pk_xxx.sk_xxx';
378
- }
379
- const parts = trimmed.split('.');
380
- if (parts.length !== 2) {
381
- return 'Invalid vendor key format: Must have exactly two parts separated by a dot. Expected format: pk_xxx.sk_xxx';
382
- }
383
- const [publicPart, secretPart] = parts;
384
- // Validate public key part
385
- if (!publicPart.startsWith('pk_')) {
386
- return 'Invalid vendor key format: First part must start with "pk_". Expected format: pk_xxx.sk_xxx';
387
- }
388
- if (publicPart.length < 11) { // pk_ + minimum 8 chars
389
- return 'Invalid vendor key format: Public key part is too short. Expected format: pk_xxx.sk_xxx (minimum 8 characters after "pk_")';
390
- }
391
- const publicKeyContent = publicPart.substring(3); // Remove 'pk_'
392
- if (!/^[a-zA-Z0-9]+$/.test(publicKeyContent)) {
393
- return 'Invalid vendor key format: Public key part contains invalid characters. Only letters and numbers are allowed after "pk_"';
394
- }
395
- // Validate secret key part
396
- if (!secretPart.startsWith('sk_')) {
397
- return 'Invalid vendor key format: Second part must start with "sk_". Expected format: pk_xxx.sk_xxx';
398
- }
399
- if (secretPart.length < 19) { // sk_ + minimum 16 chars
400
- return 'Invalid vendor key format: Secret key part is too short. Expected format: pk_xxx.sk_xxx (minimum 16 characters after "sk_")';
401
- }
402
- const secretKeyContent = secretPart.substring(3); // Remove 'sk_'
403
- if (!/^[a-zA-Z0-9]+$/.test(secretKeyContent)) {
404
- return 'Invalid vendor key format: Secret key part contains invalid characters. Only letters and numbers are allowed after "sk_"';
405
- }
406
376
  return true;
407
377
  }
408
378
  async validateVendorKeyWithServer(vendorKey) {
@@ -576,7 +546,17 @@ export class CLIConfig {
576
546
  this.authCheckCache = { isValid: false, timestamp: Date.now() };
577
547
  return false;
578
548
  }
579
- // Verify with server (security check)
549
+ // Token is locally valid - check if we need server validation
550
+ // Skip server validation if we have a recent lastValidated timestamp (within 24 hours)
551
+ const lastValidated = this.config.lastValidated;
552
+ const skipServerValidation = lastValidated &&
553
+ (Date.now() - new Date(lastValidated).getTime()) < (24 * 60 * 60 * 1000); // 24 hours
554
+ if (skipServerValidation) {
555
+ // Trust the local validation if it was recently validated
556
+ this.authCheckCache = { isValid: locallyValid, timestamp: Date.now() };
557
+ return locallyValid;
558
+ }
559
+ // Verify with server (security check) for tokens that haven't been validated recently
580
560
  try {
581
561
  const axios = (await import('axios')).default;
582
562
  // Try auth-gateway first (port 4000), then fall back to Netlify function
@@ -599,16 +579,29 @@ export class CLIConfig {
599
579
  }
600
580
  }
601
581
  if (!response || response.data.valid !== true) {
582
+ // Server says invalid - but if locally valid and recent, trust local
583
+ if (locallyValid) {
584
+ if (process.env.CLI_VERBOSE === 'true') {
585
+ console.warn('⚠️ Server validation failed, but token is locally valid - using local validation');
586
+ }
587
+ this.authCheckCache = { isValid: locallyValid, timestamp: Date.now() };
588
+ return locallyValid;
589
+ }
602
590
  this.authCheckCache = { isValid: false, timestamp: Date.now() };
603
591
  return false;
604
592
  }
593
+ // Update lastValidated on successful server validation
594
+ this.config.lastValidated = new Date().toISOString();
595
+ await this.save().catch(() => { }); // Don't fail auth check if save fails
605
596
  this.authCheckCache = { isValid: true, timestamp: Date.now() };
606
597
  return true;
607
598
  }
608
599
  catch {
609
600
  // If all server checks fail, fall back to local validation
610
601
  // This allows offline usage but is less secure
611
- console.warn('⚠️ Unable to verify token with server, using local validation');
602
+ if (process.env.CLI_VERBOSE === 'true') {
603
+ console.warn('⚠️ Unable to verify token with server, using local validation');
604
+ }
612
605
  this.authCheckCache = { isValid: locallyValid, timestamp: Date.now() };
613
606
  return locallyValid;
614
607
  }
@@ -110,10 +110,6 @@ export declare class MCPClient {
110
110
  * Validate authentication credentials before attempting MCP connection
111
111
  */
112
112
  private validateAuthBeforeConnect;
113
- /**
114
- * Validate vendor key format
115
- */
116
- private validateVendorKeyFormat;
117
113
  /**
118
114
  * Validate and refresh token if needed
119
115
  */
@@ -245,11 +245,10 @@ export class MCPClient {
245
245
  const msg = error?.message ?? '';
246
246
  if (msg.includes('AUTHENTICATION_REQUIRED')) {
247
247
  console.log(chalk.cyan('• No credentials found. Run: lanonasis auth login'));
248
- console.log(chalk.cyan('• Or set vendor key: lanonasis auth login --vendor-key pk_xxx.sk_xxx'));
248
+ console.log(chalk.cyan('• Or set a vendor key: lanonasis auth login --vendor-key <your-key>'));
249
249
  }
250
250
  else if (msg.includes('AUTHENTICATION_INVALID')) {
251
- console.log(chalk.cyan('• Invalid credentials. Check your vendor key format'));
252
- console.log(chalk.cyan('• Expected format: pk_xxx.sk_xxx'));
251
+ console.log(chalk.cyan('• Invalid credentials. Confirm the vendor key matches your dashboard value'));
253
252
  console.log(chalk.cyan('• Try: lanonasis auth logout && lanonasis auth login'));
254
253
  }
255
254
  else if (msg.includes('expired')) {
@@ -259,7 +258,7 @@ export class MCPClient {
259
258
  else {
260
259
  console.log(chalk.cyan('• Check authentication status: lanonasis auth status'));
261
260
  console.log(chalk.cyan('• Re-authenticate: lanonasis auth login'));
262
- console.log(chalk.cyan('• Verify vendor key: lanonasis auth login --vendor-key pk_xxx.sk_xxx'));
261
+ console.log(chalk.cyan('• Verify vendor key: lanonasis auth login --vendor-key <your-key>'));
263
262
  }
264
263
  }
265
264
  /**
@@ -331,21 +330,14 @@ export class MCPClient {
331
330
  throw new Error(`AUTHENTICATION_INVALID: ${error instanceof Error ? error.message : 'Token validation failed'}`);
332
331
  }
333
332
  }
334
- // If we have a vendor key, validate its format
333
+ // If we have a vendor key, ensure it is valid (non-empty)
335
334
  if (vendorKey && !token) {
336
- if (!this.validateVendorKeyFormat(vendorKey)) {
337
- throw new Error('AUTHENTICATION_INVALID: Invalid vendor key format. Expected format: pk_xxx.sk_xxx');
335
+ const validationResult = this.config.validateVendorKeyFormat(vendorKey);
336
+ if (validationResult !== true) {
337
+ throw new Error(`AUTHENTICATION_INVALID: ${typeof validationResult === 'string' ? validationResult : 'Vendor key is invalid'}`);
338
338
  }
339
339
  }
340
340
  }
341
- /**
342
- * Validate vendor key format
343
- */
344
- validateVendorKeyFormat(vendorKey) {
345
- // Vendor key should be in format: pk_xxx.sk_xxx
346
- const vendorKeyPattern = /^pk_[a-zA-Z0-9]+\.sk_[a-zA-Z0-9]+$/;
347
- return vendorKeyPattern.test(vendorKey);
348
- }
349
341
  /**
350
342
  * Validate and refresh token if needed
351
343
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lanonasis/cli",
3
- "version": "3.5.15",
3
+ "version": "3.6.0",
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": {
@@ -99,4 +99,4 @@
99
99
  "engines": {
100
100
  "node": ">=18.0.0"
101
101
  }
102
- }
102
+ }