@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 +19 -2
- package/dist/commands/auth.js +85 -11
- package/dist/commands/mcp.js +48 -0
- package/dist/mcp/client/enhanced-client.js +1 -5
- package/dist/utils/api.js +2 -1
- package/dist/utils/hash-utils.d.ts +23 -0
- package/dist/utils/hash-utils.js +37 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @lanonasis/cli v3.
|
|
1
|
+
# @lanonasis/cli v3.7.0 - Enterprise Security & MCP Experience
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@lanonasis/cli)
|
|
4
4
|
[](https://www.npmjs.com/package/@lanonasis/cli)
|
|
@@ -53,7 +53,24 @@ memory list
|
|
|
53
53
|
maas memory list
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
## š Authentication
|
|
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
|
|
package/dist/commands/auth.js
CHANGED
|
@@ -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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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',
|
|
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
|
-
|
|
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
|
}
|
package/dist/commands/mcp.js
CHANGED
|
@@ -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
|
+
}
|