@lanonasis/cli 3.6.7 → 3.7.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
@@ -1,4 +1,4 @@
1
- # @lanonasis/cli v3.4.15 - Enhanced MCP & Interactive CLI Experience
1
+ # @lanonasis/cli v3.7.0 - Enterprise Security & MCP Experience
2
2
 
3
3
  [![NPM Version](https://img.shields.io/npm/v/@lanonasis/cli)](https://www.npmjs.com/package/@lanonasis/cli)
4
4
  [![Downloads](https://img.shields.io/npm/dt/@lanonasis/cli)](https://www.npmjs.com/package/@lanonasis/cli)
@@ -53,7 +53,24 @@ memory list
53
53
  maas memory list
54
54
  ```
55
55
 
56
- ## šŸ” Authentication Methods
56
+ ## šŸ” Security & Authentication
57
+
58
+ ### Enterprise-Grade SHA-256 Security (v3.7.0+)
59
+
60
+ All API keys are now secured with SHA-256 cryptographic hashing:
61
+
62
+ - āœ… **Automatic Hash Normalization**: Keys are automatically hashed before transmission
63
+ - āœ… **Double-Hash Prevention**: Smart detection prevents re-hashing already hashed keys
64
+ - āœ… **Cross-Platform Compatibility**: Works seamlessly across Node.js and browser environments
65
+ - āœ… **Zero Configuration**: Security is automatic and transparent
66
+
67
+ ```typescript
68
+ // Hash utilities are built-in and automatic
69
+ // Your vendor keys are automatically secured
70
+ onasis login --vendor-key pk_xxxxx.sk_xxxxx // āœ… Automatically hashed
71
+ ```
72
+
73
+ ### Authentication Methods
57
74
 
58
75
  ### 1. Vendor Key Authentication (Recommended)
59
76
 
@@ -5,6 +5,7 @@ import open from 'open';
5
5
  import crypto from 'crypto';
6
6
  import http from 'http';
7
7
  import url from 'url';
8
+ import axios from 'axios';
8
9
  import { apiClient } from '../utils/api.js';
9
10
  import { CLIConfig } from '../utils/config.js';
10
11
  // Color scheme
@@ -242,16 +243,40 @@ function createCallbackServer(port = 8888) {
242
243
  /**
243
244
  * Exchange authorization code for OAuth2 tokens
244
245
  */
245
- async function exchangeCodeForTokens(code, verifier, authBase) {
246
+ async function exchangeCodeForTokens(code, verifier, authBase, redirectUri) {
246
247
  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;
248
+ try {
249
+ // Use axios directly to have full control over error handling
250
+ const response = await axios.post(tokenEndpoint, {
251
+ grant_type: 'authorization_code',
252
+ code,
253
+ code_verifier: verifier,
254
+ client_id: 'lanonasis-cli',
255
+ redirect_uri: redirectUri
256
+ }, {
257
+ headers: {
258
+ 'Content-Type': 'application/json',
259
+ }
260
+ });
261
+ return response.data;
262
+ }
263
+ catch (error) {
264
+ // Extract detailed error information from axios error response
265
+ if (error.response) {
266
+ const errorData = error.response.data || {};
267
+ const status = error.response.status;
268
+ const errorMessage = errorData.error_description || errorData.error || error.message || `Request failed with status code ${status}`;
269
+ const details = errorData.details;
270
+ const enhancedError = new Error(errorMessage);
271
+ enhancedError.response = error.response;
272
+ enhancedError.status = status;
273
+ enhancedError.details = details;
274
+ enhancedError.errorData = errorData;
275
+ throw enhancedError;
276
+ }
277
+ // If it's not an axios error, just rethrow
278
+ throw error;
279
+ }
255
280
  }
256
281
  /**
257
282
  * Refresh OAuth2 access token using refresh token
@@ -618,10 +643,11 @@ async function handleOAuthFlow(config) {
618
643
  console.log(chalk.gray(` āœ“ Started local callback server on port ${callbackPort}`));
619
644
  // Build OAuth2 authorization URL
620
645
  const authBase = config.getDiscoveredApiUrl();
646
+ const redirectUri = `http://localhost:${callbackPort}/callback`;
621
647
  const authUrl = new URL(`${authBase}/oauth/authorize`);
622
648
  authUrl.searchParams.set('response_type', 'code');
623
649
  authUrl.searchParams.set('client_id', 'lanonasis-cli');
624
- authUrl.searchParams.set('redirect_uri', `http://localhost:${callbackPort}/callback`);
650
+ authUrl.searchParams.set('redirect_uri', redirectUri);
625
651
  authUrl.searchParams.set('scope', 'read write offline_access');
626
652
  authUrl.searchParams.set('code_challenge', pkce.challenge);
627
653
  authUrl.searchParams.set('code_challenge_method', 'S256');
@@ -639,7 +665,14 @@ async function handleOAuthFlow(config) {
639
665
  // Exchange code for tokens
640
666
  spinner.text = 'Exchanging code for access tokens...';
641
667
  spinner.start();
642
- const tokens = await exchangeCodeForTokens(code, pkce.verifier, authBase);
668
+ // Debug logging in verbose mode
669
+ if (process.env.CLI_VERBOSE === 'true') {
670
+ console.log(chalk.dim(` Code length: ${code.length}`));
671
+ console.log(chalk.dim(` Verifier length: ${pkce.verifier.length}`));
672
+ console.log(chalk.dim(` Redirect URI: ${redirectUri}`));
673
+ console.log(chalk.dim(` Token endpoint: ${authBase}/oauth/token`));
674
+ }
675
+ const tokens = await exchangeCodeForTokens(code, pkce.verifier, authBase, redirectUri);
643
676
  spinner.succeed('Access tokens received');
644
677
  // Store tokens
645
678
  await config.setToken(tokens.access_token);
@@ -653,8 +686,49 @@ async function handleOAuthFlow(config) {
653
686
  }
654
687
  catch (error) {
655
688
  console.error(chalk.red('āœ– OAuth2 authentication failed'));
689
+ // Display detailed error information
656
690
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
657
691
  console.error(chalk.gray(` ${errorMessage}`));
692
+ // Show validation details if available
693
+ if (error.details) {
694
+ console.error(chalk.yellow('\n Validation errors:'));
695
+ for (const [field, messages] of Object.entries(error.details)) {
696
+ const msgArray = Array.isArray(messages) ? messages : [messages];
697
+ msgArray.forEach((msg) => {
698
+ console.error(chalk.gray(` • ${field}: ${msg}`));
699
+ });
700
+ }
701
+ }
702
+ // Show error data if available
703
+ if (error.errorData) {
704
+ const errorData = error.errorData;
705
+ if (errorData.error) {
706
+ console.error(chalk.yellow(`\n Error: ${errorData.error}`));
707
+ }
708
+ if (errorData.error_description) {
709
+ console.error(chalk.gray(` ${errorData.error_description}`));
710
+ }
711
+ // Show details if not already shown above
712
+ if (!error.details && errorData.details) {
713
+ console.error(chalk.yellow('\n Details:'));
714
+ console.error(chalk.gray(JSON.stringify(errorData.details, null, 2)));
715
+ }
716
+ }
717
+ // Show full error response in verbose mode
718
+ if (process.env.CLI_VERBOSE === 'true') {
719
+ if (error.response?.data) {
720
+ console.error(chalk.dim('\n Full error response:'));
721
+ console.error(chalk.dim(JSON.stringify(error.response.data, null, 2)));
722
+ }
723
+ if (error.response?.config) {
724
+ console.error(chalk.dim('\n Request config:'));
725
+ console.error(chalk.dim(JSON.stringify({
726
+ url: error.response.config.url,
727
+ method: error.response.config.method,
728
+ data: error.response.config.data
729
+ }, null, 2)));
730
+ }
731
+ }
658
732
  process.exit(1);
659
733
  }
660
734
  }
@@ -178,6 +178,50 @@ export function mcpCommands(program) {
178
178
  // Reload config from disk to get latest preference
179
179
  await client.init();
180
180
  const status = client.getConnectionStatus();
181
+ // Also perform a lightweight live health check against the MCP HTTP endpoint
182
+ const config = new CLIConfig();
183
+ await config.init();
184
+ let healthLabel = chalk.gray('Unknown');
185
+ let healthDetails;
186
+ try {
187
+ const axios = (await import('axios')).default;
188
+ // Derive MCP health URL from discovered REST base (e.g. https://mcp.lanonasis.com/api/v1 -> https://mcp.lanonasis.com/health)
189
+ const restUrl = config.getMCPRestUrl();
190
+ const rootBase = restUrl.replace(/\/api\/v1$/, '');
191
+ const healthUrl = `${rootBase}/health`;
192
+ const token = config.getToken();
193
+ const vendorKey = config.getVendorKey();
194
+ const headers = {};
195
+ if (vendorKey) {
196
+ headers['X-API-Key'] = vendorKey;
197
+ headers['X-Auth-Method'] = 'vendor_key';
198
+ }
199
+ else if (token) {
200
+ headers['Authorization'] = `Bearer ${token}`;
201
+ headers['X-Auth-Method'] = 'jwt';
202
+ }
203
+ const response = await axios.get(healthUrl, {
204
+ headers,
205
+ timeout: 5000
206
+ });
207
+ const overallStatus = String(response.data?.status ?? '').toLowerCase();
208
+ const ok = response.status === 200 && (!overallStatus || overallStatus === 'healthy');
209
+ if (ok) {
210
+ healthLabel = chalk.green('Reachable');
211
+ }
212
+ else {
213
+ healthLabel = chalk.yellow('Degraded');
214
+ }
215
+ }
216
+ catch (error) {
217
+ healthLabel = chalk.red('Unreachable');
218
+ if (error instanceof Error) {
219
+ healthDetails = error.message;
220
+ }
221
+ else if (error !== null && error !== undefined) {
222
+ healthDetails = String(error);
223
+ }
224
+ }
181
225
  console.log(chalk.cyan('\nšŸ“Š MCP Connection Status'));
182
226
  console.log(chalk.cyan('========================'));
183
227
  console.log(`Status: ${status.connected ? chalk.green('Connected') : chalk.red('Disconnected')}`);
@@ -198,6 +242,10 @@ export function mcpCommands(program) {
198
242
  }
199
243
  console.log(`Mode: ${modeDisplay}`);
200
244
  console.log(`Server: ${status.server}`);
245
+ console.log(`Health: ${healthLabel}`);
246
+ if (healthDetails && process.env.CLI_VERBOSE === 'true') {
247
+ console.log(chalk.gray(`Health details: ${healthDetails}`));
248
+ }
201
249
  if (status.connected) {
202
250
  if (status.mode === 'remote') {
203
251
  console.log(`\n${chalk.cyan('Features:')}`);
@@ -128,11 +128,7 @@ export class EnhancedMCPClient extends EventEmitter {
128
128
  name: `lanonasis-cli-${config.name}`,
129
129
  version: '3.0.1'
130
130
  }, {
131
- capabilities: {
132
- tools: {},
133
- resources: {},
134
- prompts: {}
135
- }
131
+ capabilities: {}
136
132
  });
137
133
  await client.connect(transport);
138
134
  return client;
package/dist/utils/api.js CHANGED
@@ -2,6 +2,7 @@ import axios from 'axios';
2
2
  import chalk from 'chalk';
3
3
  import { randomUUID } from 'crypto';
4
4
  import { CLIConfig } from './config.js';
5
+ import { ensureApiKeyHash } from './hash-utils.js';
5
6
  export class APIClient {
6
7
  client;
7
8
  config;
@@ -30,7 +31,7 @@ export class APIClient {
30
31
  const vendorKey = this.config.getVendorKey();
31
32
  if (vendorKey) {
32
33
  // Vendor key authentication (validated server-side)
33
- config.headers['X-API-Key'] = vendorKey;
34
+ config.headers['X-API-Key'] = ensureApiKeyHash(vendorKey);
34
35
  config.headers['X-Auth-Method'] = 'vendor_key';
35
36
  }
36
37
  else if (token) {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Local Hash Utilities for CLI
3
+ * Copied from shared/hash-utils.ts to avoid TypeScript rootDir issues
4
+ */
5
+ /**
6
+ * Determine if the provided value is already a SHA-256 hex digest
7
+ */
8
+ export declare function isSha256Hash(value: string): boolean;
9
+ /**
10
+ * Hash an API key with SHA-256
11
+ * Used for: Database storage, validation, lookups
12
+ *
13
+ * @param apiKey - The raw API key to hash
14
+ * @returns SHA-256 hash as hex string (64 characters)
15
+ */
16
+ export declare function hashApiKey(apiKey: string): string;
17
+ /**
18
+ * Normalize any API key input to a SHA-256 hex digest
19
+ * Leaves an existing 64-char hex hash untouched to prevent double hashing
20
+ */
21
+ export declare function ensureApiKeyHash(apiKey: string): string;
22
+ export type ApiKeyHash = string;
23
+ export type ApiKey = string;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Local Hash Utilities for CLI
3
+ * Copied from shared/hash-utils.ts to avoid TypeScript rootDir issues
4
+ */
5
+ import crypto from 'crypto';
6
+ /**
7
+ * Determine if the provided value is already a SHA-256 hex digest
8
+ */
9
+ export function isSha256Hash(value) {
10
+ return typeof value === 'string' && /^[a-f0-9]{64}$/i.test(value.trim());
11
+ }
12
+ /**
13
+ * Hash an API key with SHA-256
14
+ * Used for: Database storage, validation, lookups
15
+ *
16
+ * @param apiKey - The raw API key to hash
17
+ * @returns SHA-256 hash as hex string (64 characters)
18
+ */
19
+ export function hashApiKey(apiKey) {
20
+ if (!apiKey || typeof apiKey !== 'string') {
21
+ throw new Error('API key must be a non-empty string');
22
+ }
23
+ return crypto
24
+ .createHash('sha256')
25
+ .update(apiKey)
26
+ .digest('hex');
27
+ }
28
+ /**
29
+ * Normalize any API key input to a SHA-256 hex digest
30
+ * Leaves an existing 64-char hex hash untouched to prevent double hashing
31
+ */
32
+ export function ensureApiKeyHash(apiKey) {
33
+ if (isSha256Hash(apiKey)) {
34
+ return apiKey.toLowerCase();
35
+ }
36
+ return hashApiKey(apiKey);
37
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lanonasis/cli",
3
- "version": "3.6.7",
3
+ "version": "3.7.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",