@lanonasis/cli 3.7.4 → 3.7.6

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.
@@ -91,7 +91,7 @@ async function handleAuthenticationFailure(error, config, authMethod = 'jwt') {
91
91
  await config.clearInvalidCredentials();
92
92
  break;
93
93
  default:
94
- console.log(chalk.red(`Unexpected error: ${error.message || 'Unknown error'}`));
94
+ console.log(chalk.red(`Unexpected error: ${sanitizeErrorMessage(error.message || 'Unknown error')}`));
95
95
  console.log(chalk.gray('• Please try again'));
96
96
  console.log(chalk.gray('• If the problem persists, contact support'));
97
97
  }
@@ -183,12 +183,38 @@ function generatePKCE() {
183
183
  .digest('base64url');
184
184
  return { verifier, challenge };
185
185
  }
186
+ /**
187
+ * Sanitize error messages to prevent command injection
188
+ */
189
+ function sanitizeErrorMessage(message) {
190
+ if (typeof message !== 'string')
191
+ return 'Unknown error';
192
+ // Remove potential command injection characters
193
+ return message
194
+ .replace(/[;&|`$()]/g, '') // Remove shell metacharacters
195
+ .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '') // Remove script tags
196
+ .replace(/javascript:/gi, '') // Remove javascript: URLs
197
+ .trim();
198
+ }
186
199
  /**
187
200
  * Start local HTTP server to catch OAuth2 callback
188
201
  */
189
202
  function createCallbackServer(port = 8888) {
190
203
  return new Promise((resolve, reject) => {
204
+ // Sanitize HTML to prevent XSS
205
+ function sanitizeHtml(str) {
206
+ return str
207
+ .replace(/&/g, '&amp;')
208
+ .replace(/</g, '&lt;')
209
+ .replace(/>/g, '&gt;')
210
+ .replace(/"/g, '&quot;')
211
+ .replace(/'/g, '&#x27;');
212
+ }
191
213
  const server = http.createServer((req, res) => {
214
+ // Set security headers
215
+ res.setHeader('X-Content-Type-Options', 'nosniff');
216
+ res.setHeader('X-Frame-Options', 'DENY');
217
+ res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
192
218
  const parsedUrl = url.parse(req.url, true);
193
219
  if (parsedUrl.pathname === '/callback') {
194
220
  const { code, state, error, error_description } = parsedUrl.query;
@@ -200,7 +226,7 @@ function createCallbackServer(port = 8888) {
200
226
  <head><title>Authentication Failed</title></head>
201
227
  <body style="font-family: sans-serif; text-align: center; padding: 50px;">
202
228
  <h1>❌ Authentication Failed</h1>
203
- <p>${error_description || error}</p>
229
+ <p>${sanitizeHtml(String(error_description || error))}</p>
204
230
  <p style="color: gray;">You can close this window.</p>
205
231
  </body>
206
232
  </html>
@@ -280,7 +306,9 @@ async function exchangeCodeForTokens(code, verifier, authBase, redirectUri) {
280
306
  }
281
307
  /**
282
308
  * Refresh OAuth2 access token using refresh token
309
+ * @internal Used for token refresh flows
283
310
  */
311
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
284
312
  async function refreshOAuth2Token(config) {
285
313
  const refreshToken = config.get('refresh_token');
286
314
  if (!refreshToken) {
@@ -300,45 +328,11 @@ async function refreshOAuth2Token(config) {
300
328
  await config.set('token_expires_at', Date.now() + (response.expires_in * 1000));
301
329
  return true;
302
330
  }
303
- catch (error) {
331
+ catch {
304
332
  console.error(chalk.yellow('⚠️ Token refresh failed, please re-authenticate'));
305
333
  return false;
306
334
  }
307
335
  }
308
- /**
309
- * Exchange Supabase JWT token for auth-gateway API key
310
- * This enables CLI to work with MCP WebSocket and all services seamlessly
311
- */
312
- async function exchangeSupabaseTokenForApiKey(supabaseToken, config) {
313
- try {
314
- const discoveredServices = config.get('discoveredServices');
315
- const authBase = discoveredServices?.auth_base || 'https://auth.lanonasis.com';
316
- if (process.env.CLI_VERBOSE === 'true') {
317
- console.log(chalk.dim(` Exchanging token at: ${authBase}/v1/auth/token/exchange`));
318
- }
319
- const response = await axios.post(`${authBase}/v1/auth/token/exchange`, {
320
- project_scope: 'lanonasis-maas',
321
- platform: 'cli'
322
- }, {
323
- headers: {
324
- 'Authorization': `Bearer ${supabaseToken}`,
325
- 'Content-Type': 'application/json',
326
- 'X-Project-Scope': 'lanonasis-maas'
327
- }
328
- });
329
- return {
330
- access_token: response.data.access_token,
331
- user: response.data.user
332
- };
333
- }
334
- catch (error) {
335
- console.error(chalk.yellow('⚠️ Token exchange failed:', error.message));
336
- if (process.env.CLI_VERBOSE === 'true' && error.response) {
337
- console.error(chalk.dim(' Response:', JSON.stringify(error.response.data, null, 2)));
338
- }
339
- return null;
340
- }
341
- }
342
336
  export async function diagnoseCommand() {
343
337
  const config = new CLIConfig();
344
338
  await config.init();
@@ -708,33 +702,23 @@ async function handleOAuthFlow(config) {
708
702
  }
709
703
  const tokens = await exchangeCodeForTokens(code, pkce.verifier, authBase, redirectUri);
710
704
  spinner.succeed('Access tokens received');
711
- // Store OAuth tokens
705
+ // Store OAuth tokens - these are already valid auth-gateway tokens from /oauth/token
706
+ // No need for additional exchange since /oauth/token returns auth-gateway's own tokens
712
707
  await config.setToken(tokens.access_token);
713
708
  await config.set('refresh_token', tokens.refresh_token);
714
709
  await config.set('token_expires_at', Date.now() + (tokens.expires_in * 1000));
715
- // Exchange for unified API key
710
+ await config.set('authMethod', 'oauth2');
711
+ // The OAuth access token from auth-gateway works as the API token for all services
712
+ // Store it as the vendor key equivalent for MCP and API access
716
713
  spinner.text = 'Configuring unified access...';
717
714
  spinner.start();
718
- const exchangeResult = await exchangeSupabaseTokenForApiKey(tokens.access_token, config);
719
- if (exchangeResult) {
720
- // Store the auth-gateway API key for MCP and other services
721
- await config.setVendorKey(exchangeResult.access_token);
722
- await config.set('authMethod', 'oauth2');
723
- spinner.succeed('Unified authentication configured');
724
- console.log();
725
- console.log(chalk.green('✓ OAuth2 authentication successful'));
726
- console.log(colors.info('You can now use all Lanonasis services'));
727
- console.log(chalk.gray('✓ MCP, API, and CLI access configured'));
728
- }
729
- else {
730
- // Fallback
731
- await config.set('authMethod', 'oauth2');
732
- spinner.warn('Token exchange failed, OAuth token stored');
733
- console.log();
734
- console.log(chalk.green('✓ OAuth2 authentication successful'));
735
- console.log(colors.info('You can now use Lanonasis services'));
736
- console.log(chalk.yellow('⚠️ Some services may require re-authentication'));
737
- }
715
+ // Use the OAuth access token directly - it's already an auth-gateway token
716
+ await config.setVendorKey(tokens.access_token);
717
+ spinner.succeed('Unified authentication configured');
718
+ console.log();
719
+ console.log(chalk.green(' OAuth2 authentication successful'));
720
+ console.log(colors.info('You can now use all Lanonasis services'));
721
+ console.log(chalk.gray('✓ MCP, API, and CLI access configured'));
738
722
  process.exit(0);
739
723
  }
740
724
  catch (error) {
package/dist/utils/api.js CHANGED
@@ -2,7 +2,6 @@ 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';
6
5
  export class APIClient {
7
6
  client;
8
7
  config;
@@ -31,7 +30,8 @@ export class APIClient {
31
30
  const vendorKey = this.config.getVendorKey();
32
31
  if (vendorKey) {
33
32
  // Vendor key authentication (validated server-side)
34
- config.headers['X-API-Key'] = ensureApiKeyHash(vendorKey);
33
+ // Send raw key - server handles hashing for comparison
34
+ config.headers['X-API-Key'] = vendorKey;
35
35
  config.headers['X-Auth-Method'] = 'vendor_key';
36
36
  }
37
37
  else if (token) {
@@ -100,61 +100,62 @@ export class APIClient {
100
100
  return response.data;
101
101
  }
102
102
  // Memory operations - aligned with existing schema
103
+ // All memory endpoints use /api/v1/memory path
103
104
  async createMemory(data) {
104
- const response = await this.client.post('/memory', data);
105
+ const response = await this.client.post('/api/v1/memory', data);
105
106
  return response.data;
106
107
  }
107
108
  async getMemories(params = {}) {
108
- const response = await this.client.get('/memory', { params });
109
+ const response = await this.client.get('/api/v1/memory', { params });
109
110
  return response.data;
110
111
  }
111
112
  async getMemory(id) {
112
- const response = await this.client.get(`/memory/${id}`);
113
+ const response = await this.client.get(`/api/v1/memory/${id}`);
113
114
  return response.data;
114
115
  }
115
116
  async updateMemory(id, data) {
116
- const response = await this.client.put(`/memory/${id}`, data);
117
+ const response = await this.client.put(`/api/v1/memory/${id}`, data);
117
118
  return response.data;
118
119
  }
119
120
  async deleteMemory(id) {
120
- await this.client.delete(`/memory/${id}`);
121
+ await this.client.delete(`/api/v1/memory/${id}`);
121
122
  }
122
123
  async searchMemories(query, options = {}) {
123
- const response = await this.client.post('/memory/search', {
124
+ const response = await this.client.post('/api/v1/memory/search', {
124
125
  query,
125
126
  ...options
126
127
  });
127
128
  return response.data;
128
129
  }
129
130
  async getMemoryStats() {
130
- const response = await this.client.get('/memory/stats');
131
+ const response = await this.client.get('/api/v1/memory/stats');
131
132
  return response.data;
132
133
  }
133
134
  async bulkDeleteMemories(memoryIds) {
134
- const response = await this.client.post('/memory/bulk/delete', {
135
+ const response = await this.client.post('/api/v1/memory/bulk/delete', {
135
136
  memory_ids: memoryIds
136
137
  });
137
138
  return response.data;
138
139
  }
139
140
  // Topic operations - working with existing memory_topics table
140
141
  async createTopic(data) {
141
- const response = await this.client.post('/topics', data);
142
+ const response = await this.client.post('/api/v1/topics', data);
142
143
  return response.data;
143
144
  }
144
145
  async getTopics() {
145
- const response = await this.client.get('/topics');
146
+ const response = await this.client.get('/api/v1/topics');
146
147
  return response.data;
147
148
  }
148
149
  async getTopic(id) {
149
- const response = await this.client.get(`/topics/${id}`);
150
+ const response = await this.client.get(`/api/v1/topics/${id}`);
150
151
  return response.data;
151
152
  }
152
153
  async updateTopic(id, data) {
153
- const response = await this.client.put(`/topics/${id}`, data);
154
+ const response = await this.client.put(`/api/v1/topics/${id}`, data);
154
155
  return response.data;
155
156
  }
156
157
  async deleteTopic(id) {
157
- await this.client.delete(`/topics/${id}`);
158
+ await this.client.delete(`/api/v1/topics/${id}`);
158
159
  }
159
160
  // Health check
160
161
  async getHealth() {
@@ -168,9 +168,11 @@ export class CLIConfig {
168
168
  }
169
169
  }
170
170
  getApiUrl() {
171
- return process.env.MEMORY_API_URL ||
171
+ const baseUrl = process.env.MEMORY_API_URL ||
172
172
  this.config.apiUrl ||
173
- 'https://api.lanonasis.com';
173
+ 'https://mcp.lanonasis.com';
174
+ // Ensure we don't double-append /api/v1 - strip it if present since APIClient adds it
175
+ return baseUrl.replace(/\/api\/v1\/?$/, '');
174
176
  }
175
177
  // Get API URLs with fallbacks - try multiple endpoints
176
178
  getApiUrlsWithFallbacks() {
@@ -190,8 +192,8 @@ export class CLIConfig {
190
192
  if (!this.config.discoveredServices) {
191
193
  this.config.discoveredServices = {
192
194
  auth_base: 'https://auth.lanonasis.com',
193
- memory_base: 'https://mcp.lanonasis.com/api/v1',
194
- mcp_base: 'https://mcp.lanonasis.com/api/v1',
195
+ memory_base: 'https://mcp.lanonasis.com', // Base URL without /api/v1
196
+ mcp_base: 'https://mcp.lanonasis.com/api/v1', // Full MCP REST path
195
197
  mcp_ws_base: 'wss://mcp.lanonasis.com/ws',
196
198
  mcp_sse_base: 'https://mcp.lanonasis.com/api/v1/events',
197
199
  project_scope: 'lanonasis-maas'
@@ -243,13 +245,16 @@ export class CLIConfig {
243
245
  if (authBase.includes('localhost') || authBase.includes('127.0.0.1')) {
244
246
  authBase = 'https://auth.lanonasis.com';
245
247
  }
246
- const memoryBase = discovered.endpoints?.http || 'https://mcp.lanonasis.com/api/v1';
248
+ // Memory base should be the MCP base URL without /api/v1 suffix
249
+ // The API client will append the path as needed
250
+ const rawMemoryBase = discovered.endpoints?.http || 'https://mcp.lanonasis.com/api/v1';
251
+ const memoryBase = rawMemoryBase.replace(/\/api\/v1\/?$/, '') || 'https://mcp.lanonasis.com';
247
252
  this.config.discoveredServices = {
248
253
  auth_base: authBase || 'https://auth.lanonasis.com',
249
254
  memory_base: memoryBase,
250
- mcp_base: memoryBase,
255
+ mcp_base: `${memoryBase}/api/v1`, // Full path for MCP REST calls
251
256
  mcp_ws_base: discovered.endpoints?.websocket || 'wss://mcp.lanonasis.com/ws',
252
- mcp_sse_base: discovered.endpoints?.sse || 'https://mcp.lanonasis.com/api/v1/events',
257
+ mcp_sse_base: discovered.endpoints?.sse || `${memoryBase}/api/v1/events`,
253
258
  project_scope: 'lanonasis-maas'
254
259
  };
255
260
  this.config.apiUrl = memoryBase;
@@ -366,8 +371,8 @@ export class CLIConfig {
366
371
  const nodeEnv = (process.env.NODE_ENV ?? '').toLowerCase();
367
372
  const isDevEnvironment = nodeEnv === 'development' || nodeEnv === 'test';
368
373
  const defaultAuthBase = isDevEnvironment ? 'http://localhost:4000' : 'https://auth.lanonasis.com';
369
- const defaultMemoryBase = isDevEnvironment ? 'http://localhost:4000/api/v1' : 'https://mcp.lanonasis.com/api/v1';
370
- const defaultMcpBase = isDevEnvironment ? 'http://localhost:4100/api/v1' : 'https://mcp.lanonasis.com/api/v1';
374
+ const defaultMemoryBase = isDevEnvironment ? 'http://localhost:4000' : 'https://mcp.lanonasis.com'; // Base URL without /api/v1
375
+ const defaultMcpBase = isDevEnvironment ? 'http://localhost:4100/api/v1' : 'https://mcp.lanonasis.com/api/v1'; // Full MCP REST path
371
376
  const defaultMcpWsBase = isDevEnvironment ? 'ws://localhost:4100/ws' : 'wss://mcp.lanonasis.com/ws';
372
377
  const defaultMcpSseBase = isDevEnvironment ? 'http://localhost:4100/api/v1/events' : 'https://mcp.lanonasis.com/api/v1/events';
373
378
  const endpoints = {
@@ -433,8 +438,8 @@ export class CLIConfig {
433
438
  }
434
439
  const currentServices = this.config.discoveredServices ?? {
435
440
  auth_base: 'https://auth.lanonasis.com',
436
- memory_base: 'https://mcp.lanonasis.com/api/v1',
437
- mcp_base: 'https://mcp.lanonasis.com/api/v1',
441
+ memory_base: 'https://mcp.lanonasis.com', // Base URL without /api/v1
442
+ mcp_base: 'https://mcp.lanonasis.com/api/v1', // Full MCP REST path
438
443
  mcp_ws_base: 'wss://mcp.lanonasis.com/ws',
439
444
  mcp_sse_base: 'https://mcp.lanonasis.com/api/v1/events',
440
445
  project_scope: 'lanonasis-maas'
@@ -456,9 +456,11 @@ export class MCPClient {
456
456
  // Use the proper SSE endpoint from config
457
457
  const sseUrl = this.config.getMCPSSEUrl() ?? `${serverUrl}/events`;
458
458
  const token = this.config.get('token');
459
- if (token) {
459
+ const vendorKey = this.config.get('vendorKey');
460
+ const authKey = token || vendorKey || process.env.LANONASIS_API_KEY;
461
+ if (authKey) {
460
462
  // EventSource doesn't support headers directly, append token to URL
461
- this.sseConnection = new EventSource(`${sseUrl}?token=${encodeURIComponent(token)}`);
463
+ this.sseConnection = new EventSource(`${sseUrl}?token=${encodeURIComponent(authKey)}`);
462
464
  this.sseConnection.onmessage = (event) => {
463
465
  try {
464
466
  const data = JSON.parse(event.data);
@@ -478,7 +480,9 @@ export class MCPClient {
478
480
  */
479
481
  async initializeWebSocket(wsUrl) {
480
482
  const token = this.config.get('token');
481
- if (!token) {
483
+ const vendorKey = this.config.get('vendorKey');
484
+ const authKey = token || vendorKey || process.env.LANONASIS_API_KEY;
485
+ if (!authKey) {
482
486
  throw new Error('API key required for WebSocket mode. Set LANONASIS_API_KEY or login first.');
483
487
  }
484
488
  return new Promise((resolve, reject) => {
@@ -491,8 +495,8 @@ export class MCPClient {
491
495
  // Create new WebSocket connection with authentication
492
496
  this.wsConnection = new WebSocket(wsUrl, [], {
493
497
  headers: {
494
- 'Authorization': `Bearer ${token}`,
495
- 'X-API-Key': token
498
+ 'Authorization': `Bearer ${authKey}`,
499
+ 'X-API-Key': authKey
496
500
  }
497
501
  });
498
502
  this.wsConnection.on('open', () => {
@@ -624,15 +628,17 @@ export class MCPClient {
624
628
  async checkRemoteHealth() {
625
629
  const apiUrl = this.config.getMCPRestUrl() ?? 'https://mcp.lanonasis.com/api/v1';
626
630
  const token = this.config.get('token');
627
- if (!token) {
631
+ const vendorKey = this.config.get('vendorKey');
632
+ const authKey = token || vendorKey || process.env.LANONASIS_API_KEY;
633
+ if (!authKey) {
628
634
  throw new Error('No authentication token available');
629
635
  }
630
636
  try {
631
637
  const axios = (await import('axios')).default;
632
638
  await axios.get(`${apiUrl}/health`, {
633
639
  headers: {
634
- 'Authorization': `Bearer ${token}`,
635
- 'x-api-key': String(token)
640
+ 'Authorization': `Bearer ${authKey}`,
641
+ 'X-API-Key': authKey
636
642
  },
637
643
  timeout: 5000
638
644
  });
@@ -749,7 +755,9 @@ export class MCPClient {
749
755
  async callRemoteTool(toolName, args) {
750
756
  const apiUrl = this.config.getMCPRestUrl() ?? 'https://mcp.lanonasis.com/api/v1';
751
757
  const token = this.config.get('token');
752
- if (!token) {
758
+ const vendorKey = this.config.get('vendorKey');
759
+ const authKey = token || vendorKey || process.env.LANONASIS_API_KEY;
760
+ if (!authKey) {
753
761
  throw new Error('Authentication required. Run "lanonasis auth login" first.');
754
762
  }
755
763
  // Map MCP tool names to REST API endpoints
@@ -805,8 +813,8 @@ export class MCPClient {
805
813
  method: mapping.method,
806
814
  url: `${apiUrl}${endpoint}`,
807
815
  headers: {
808
- 'Authorization': `Bearer ${token}`,
809
- 'x-api-key': String(token),
816
+ 'Authorization': `Bearer ${authKey}`,
817
+ 'X-API-Key': authKey,
810
818
  'Content-Type': 'application/json'
811
819
  },
812
820
  data: mapping.transform ? mapping.transform(args) : undefined,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lanonasis/cli",
3
- "version": "3.7.4",
3
+ "version": "3.7.6",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -46,7 +46,7 @@
46
46
  "@jest/globals": "^29.7.0",
47
47
  "@types/cli-progress": "^3.11.6",
48
48
  "@types/inquirer": "^9.0.7",
49
- "@types/node": "^22.10.2",
49
+ "@types/node": "^22.19.3",
50
50
  "@types/ws": "^8.5.12",
51
51
  "jest": "^29.7.0",
52
52
  "rimraf": "^5.0.7",